Conversation
Add selected-chain state, chain-aware label and vault registry loading, multi-chain filters, and chain indicators across vault lists and details.
📝 WalkthroughWalkthroughThis PR converts the application from single-chain to multi-chain selection. It adds ChangesMulti-chain support
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant SelectChainModal
participant useEulerAddresses
participant useEulerLabels
participant useVaults
participant useWallets
User->>SelectChainModal: toggle chain selection
SelectChainModal->>useEulerAddresses: toggleSelectedChainId(chainId)
useEulerAddresses->>useEulerAddresses: update selectedChainIds
useEulerAddresses-->>useEulerLabels: selectedChainIds changed
useEulerLabels->>useEulerLabels: loadLabels(chainIds) per-chain fetch/merge
useEulerLabels-->>useVaults: labels ready
useVaults->>useVaults: loadVaultsForChain per selected chain
useVaults-->>useWallets: loadedChainIds updated
useWallets->>useWallets: updateBalances across target chains
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
🚅 Deployed to the euler-lite-pr-619 environment in euler-lite(dev,PR previews)
|
Load and resolve token, label, curator, and market metadata by vault chain so multi-chain list views do not fall back to the active chain only.
Thread chain context through selected-chain rewards, wallet balances, aggregate sorting, and discovery caches so overlapping addresses do not read current-chain data.
Resolve borrow page conflicts between multi-chain data flow fixes and the latest filter UI changes.
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
utils/eulerLabelsUtils.ts (1)
113-120: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winPass
chainIdinto the earn-entry lookup.The product lookup is chain-scoped, but the earn-entry tag check still uses the merged/default label data, so same-address earn vaults can inherit “recently added” from another chain.
Proposed fix
- || earnEntryHasTag(getEarnEntryByVault(normalized), 'recently added') + || earnEntryHasTag(getEarnEntryByVault(normalized, chainId), 'recently added')🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@utils/eulerLabelsUtils.ts` around lines 113 - 120, The earn-entry lookup in isVaultRecentlyAdded is not chain-scoped, so the recently added check can pull tags from merged/default data for the same address on other chains. Update the earn-entry path to pass chainId through to the lookup and tag check, using the existing helpers around getEarnEntryByVault and earnEntryHasTag so the vault is evaluated against the correct chain-specific labels.composables/useEulerLabels.ts (1)
270-272: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftKey wrap pairs by chain as well as address.
wrapPairs[assetAddress] = underlyingcan collide when the same address exists on multiple selected chains; the last probe wins and later label resolution can use the wrong underlying. Store wrap pairs perchainId:addressor as a nested map and update readers accordingly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@composables/useEulerLabels.ts` around lines 270 - 272, The wrap pair lookup in useEulerLabels is keyed only by asset address, which can overwrite entries when the same address exists on multiple selected chains. Update the wrapPairs data structure in the probing logic to key by chainId plus address, or switch to a nested map, and make the label-resolution readers use the same chain-aware lookup. Keep the change centered around the wrapPairs assignment and any code that later consumes wrapPairs.composables/useMarketGroups.ts (1)
529-550: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winStamp on-demand fetched vaults with
targetChainId.
memberVaultsfromsdk.eVaultService.fetchVaults()are used by chain-aware helpers (getVaultKey, label checks, metrics), but they are not assignedchainId. Direct market pages for a non-current chain can then key these asundefined:<address>and miss registry/label matches.- memberVaults.push(...(result.result.filter(Boolean) as EVault[])) + memberVaults.push( + ...(result.result.filter(Boolean) as EVault[]) + .map(vault => Object.assign(vault, { chainId: targetChainId })), + )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@composables/useMarketGroups.ts` around lines 529 - 550, The on-demand vaults returned from fetchMarketGroupOnDemand are missing chain context, so they can be treated as undefined by chain-aware helpers. After sdk.eVaultService.fetchVaults() returns and before pushing into memberVaults, stamp each fetched EVault with targetChainId so getVaultKey, label checks, and metrics see the correct chain. Use the fetchMarketGroupOnDemand flow and memberVaults handling as the place to apply this fix.pages/borrow/index.vue (1)
371-421: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winChain-scoped filter values silently break legacy shared/bookmarked links.
buildAssetFilterOptions/matchesAssetFilterSelectionnow key collateral/debt options as${chainId}:${address}(Lines 378, 412), andmarketOptionskeys markets as${chainId}:${market.name}(Line 448 area). Previously these were presumably bare addresses/names. Any existing bookmarked or shared URL containing?collateral=,?debt=, or?market=with the old (chain-less) value will no longer match any asset (selectedValue === assetValuefails, and the value isn't a category-filter token either), silently producing an empty filtered list instead of the previously selected filter — with no error surfaced to the user.Consider accepting the legacy bare-address/name format as a fallback match (e.g., if
selectedValuedoesn't contain a chain prefix, compare againstasset.address/market.namealone) to avoid breaking existing links.🔧 Suggested fallback for legacy (chain-less) filter values
const matchesAssetFilterSelection = ( asset: BorrowFilterAsset, selected: readonly string[], ): boolean => { if (!selected.length) return true const assetValue = `${asset.chainId}:${asset.address}` return selected.some(selectedValue => - selectedValue === assetValue || tokenAddressMatchesCategoryFilter(asset.address, selectedValue, address => getTokenCategoryTags(address, asset.chainId)), + selectedValue === assetValue + || selectedValue.toLowerCase() === asset.address.toLowerCase() // legacy bare-address links + || tokenAddressMatchesCategoryFilter(asset.address, selectedValue, address => getTokenCategoryTags(address, asset.chainId)), ) }Also applies to: 437-450
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pages/borrow/index.vue` around lines 371 - 421, Chain-scoped filter values in buildAssetFilterOptions, matchesAssetFilterSelection, and the market filter logic now only match ${chainId}:... values, which breaks legacy bookmarked/shared links that still use bare addresses or market names. Update the selection matching in matchesAssetFilterSelection and the market option lookup to accept both the new chain-prefixed form and the old chain-less form as a fallback, so existing ?collateral=, ?debt=, and ?market= URLs continue to resolve correctly.pages/explore/index.vue (2)
322-344: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winTreat
selectedChainsas an active clearable filter.A chain-only filter currently does not set
hasActiveFilters, andclearExploreFilters()leaves it selected.Proposed fix
const hasActiveFilters = computed(() => searchQuery.value.trim().length > 0 + || selectedChains.value.length > 0 || selectedMarkets.value.length > 0 || selectedAssets.value.length > 0 || selectedRiskManagers.value.length > 0 @@ const clearExploreFilters = () => { clearSearch() + selectedChains.value = [] selectedMarkets.value = [] selectedAssets.value = [] selectedRiskManagers.value = []Also applies to: 379-387
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pages/explore/index.vue` around lines 322 - 344, Treat selectedChains as an active filter in the explore state: update the computed hasActiveFilters in pages/explore/index.vue to include selectedChains.value.length, and make clearExploreFilters also reset selectedChains so chain-only filtering is detected and cleared like the other filters. Apply the same change anywhere else the filter summary/clear logic is duplicated in the explore view.
31-39: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winPass
chainIdthroughapplyVaultOverrides.The search index uses chain-scoped product data, but override resolution is still address-only; this can index/display another chain’s override for overlapping vault addresses.
Proposed fix
- const product = applyVaultOverrides(getProductByVault(addr, vault.chainId), addr) + const product = applyVaultOverrides(getProductByVault(addr, vault.chainId), addr, vault.chainId)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pages/explore/index.vue` around lines 31 - 39, The search indexing in the useVaultSearch callback is still resolving overrides without chain context, which can mix product overrides across chains for the same vault address. Update the call site in the explore page to pass the vault’s chainId through applyVaultOverrides, alongside getProductByVault(addr, vault.chainId), so override resolution stays chain-scoped and the indexed/displayed product data matches the correct network.pages/earn/index.vue (1)
249-269: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winInclude the chain filter in active and clear filter state.
With only
selectedChainsset, the page can show an unfiltered empty-state message and the clear action will not reset the chain filter.Proposed fix
const hasActiveFilters = computed(() => searchQuery.value.trim().length > 0 + || selectedChains.value.length > 0 || selectedCollateral.value.length > 0 || selectedCurators.value.length > 0 || customFilters.value.length > 0, ) @@ const clearEarnFilters = () => { clearSearch() + selectedChains.value = [] selectedCollateral.value = [] selectedCurators.value = [] clearCustomFilters() }Also applies to: 301-309
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pages/earn/index.vue` around lines 249 - 269, The active/clear filter state in the earn page ignores the chain selector, so add selectedChains to the filter checks and reset flow. Update hasActiveFilters and any related empty-state logic in the earn page to treat selectedChains as an active filter, and make clearEarnFilters also clear selectedChains alongside the other filter arrays. If there is a second mirrored filter state block around the earn page’s later section, keep its chain handling consistent with the same symbols.pages/lend/index.vue (1)
303-325: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winInclude
selectedChainsin clearable filter state.A chain-only filter is not counted as active and is not cleared by
clearLendFilters(), leaving users without a clear action when that filter empties the list.Proposed fix
const hasActiveFilters = computed(() => searchQuery.value.trim().length > 0 + || selectedChains.value.length > 0 || selectedCollateral.value.length > 0 || selectedMarkets.value.length > 0 || selectedRiskManagers.value.length > 0 @@ const clearLendFilters = () => { clearSearch() + selectedChains.value = [] selectedCollateral.value = [] selectedMarkets.value = [] selectedRiskManagers.value = []Also applies to: 357-365
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pages/lend/index.vue` around lines 303 - 325, The filter state logic in the lend page is missing selectedChains, so chain-only filtering can hide markets without being reflected in the active-filter state or reset action. Update the computed active-filter check used by hasActiveFilters to include selectedChains, and make clearLendFilters clear selectedChains alongside the other filter arrays; use the existing selectedChains, hasActiveFilters, and clearLendFilters symbols to keep the empty-state and reset behavior consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@components/entities/chains/SelectChainModal.vue`:
- Around line 21-29: The SelectChainModal click handler currently allows the
last selected chain to be deselected, which can leave useEulerAddresses with an
empty selectedChainIds array and cause chainId to fall back to 0. Update the
onClick logic in SelectChainModal.vue to guard against deselecting the final
remaining chain by no-oping or disabling the action when
selectedChainIds.value.length === 1 and isSelected(chainId) is true. Keep the
behavior aligned with toggleSelectedChainId from useEulerAddresses so downstream
chainId-dependent lookups do not receive an invalid value.
In `@components/layout/TheHeader.vue`:
- Around line 18-24: The header is using the lowest selected chain instead of
the actual active chain, so the “current chain” display becomes incorrect.
Update TheHeader.vue to derive the chain logo/label from chainId directly rather
than selectedChainIds[0], and keep selectedChainIds only for the multi-chain
count text in selectedChainLabel. Ensure primarySelectedChainId (or its
replacement) tracks chainId so the avatar and single-chain label always reflect
the active chain.
In `@composables/useEulerLabels.ts`:
- Around line 76-77: The chain-specific lookup in getEulerLabelsDataForChain
currently falls back to labelsData, which can return merged cross-chain metadata
for a requested chainId on cache miss. Update this function so explicit chain
lookups only return the entry from labelsByChainId for that chain, and otherwise
return an empty EulerLabelsData or another clearly scoped per-chain default
instead of the merged dataset.
- Around line 188-210: The loadLabels() flow in useEulerLabels.ts is swallowing
failure/stale state: the inner Promise<boolean> currently returns true in the
catch path, and the outer async wrapper awaits it but never returns the boolean
to callers like app.vue. Update loadLabels() so the catch path yields false for
failed loads, and make the outer function return the awaited promise result
while preserving the pendingLabelsLoad cleanup and the
generation/isLoading/isReady logic.
In `@composables/useVaultRegistry.ts`:
- Around line 77-80: The chain-specific lookup in get(address, chainId) still
falls back to findUniqueEntryByAddress(address), which can return a value from
another chain when the requested chain key is missing; change this so an
explicit chainId only checks the exact registry key and does not cross-fallback.
Apply the same exact-chain-only behavior to isKnownEscrowAddress(address,
chainId) so both helpers honor chain isolation consistently, using the existing
normalizeRegistryKey, registry.value.get, and findUniqueEntryByAddress symbols
to locate the logic.
In `@composables/useVaults.ts`:
- Around line 817-842: The global readiness state is being updated too early
because loadVaultsForChain() marks isReady and mutates loadedChainIds
independently, which can make multi-chain pages look complete before all
requested chains finish loading. Move the global isReady.value = true logic out
of loadVaultsForChain() and keep it in loadVaults after the Promise.all over
targetChainIds completes, or otherwise gate it on all requested chains settling.
Keep the chain-specific loadedChainId/loadedChainIds updates in
loadVaultsForChain(), but only publish global readiness from loadVaults once
every target chain is done.
- Around line 179-182: `isCurrentVaultLoad()` is using different chain fallback
logic than `loadVaults()`, so it can reject a load when `selectedChainIds.value`
is empty. Update `isCurrentVaultLoad()` in `useVaults.ts` to mirror the same
fallback used by `loadVaults()`: derive the active chain IDs from
`selectedChainIds.value` when present, otherwise fall back to `chainId.value`,
and then compare `targetChainId` against that resolved list. Keep
`loadGeneration` as the generation check and ensure the logic stays consistent
with `useEulerAddresses()`.
In `@pages/earn/index.vue`:
- Around line 38-40: The override lookup in useVaultSearch for EulerEarn is only
using the vault address, which can select the wrong metadata when the same
address exists on multiple chains. Update the product resolution flow so
applyVaultOverrides receives the vault chainId as well as the address, and
thread that chain context through the getProductByVault result handling in
pages/earn/index.vue. Keep the change localized to the search callback so
cross-chain vaults resolve the correct market metadata.
In `@pages/lend/index.vue`:
- Around line 130-134: The borrowable vault filter in computed borrowableVaults
is matching borrowList entries by address only, which can incorrectly mark
same-address vaults on different chains as borrowable. Update the some() check
to match both borrow.address and borrow.chainId against the current
vault.address and vault.chainId, keeping the existing showAllLabelEntries and
isOpDisabled conditions unchanged.
- Around line 43-44: The vault search lookup is resolving overrides using only
the vault address, which can surface the wrong override when the same address
exists on multiple chains. Update the lookup inside useVaultSearch so the
override resolution path in applyVaultOverrides also receives vault.chainId,
matching the chain-aware getProductByVault call. Keep the fix localized to the
vault product resolution logic in pages/lend/index.vue and ensure both lookup
steps use the same chain context.
---
Outside diff comments:
In `@composables/useEulerLabels.ts`:
- Around line 270-272: The wrap pair lookup in useEulerLabels is keyed only by
asset address, which can overwrite entries when the same address exists on
multiple selected chains. Update the wrapPairs data structure in the probing
logic to key by chainId plus address, or switch to a nested map, and make the
label-resolution readers use the same chain-aware lookup. Keep the change
centered around the wrapPairs assignment and any code that later consumes
wrapPairs.
In `@composables/useMarketGroups.ts`:
- Around line 529-550: The on-demand vaults returned from
fetchMarketGroupOnDemand are missing chain context, so they can be treated as
undefined by chain-aware helpers. After sdk.eVaultService.fetchVaults() returns
and before pushing into memberVaults, stamp each fetched EVault with
targetChainId so getVaultKey, label checks, and metrics see the correct chain.
Use the fetchMarketGroupOnDemand flow and memberVaults handling as the place to
apply this fix.
In `@pages/borrow/index.vue`:
- Around line 371-421: Chain-scoped filter values in buildAssetFilterOptions,
matchesAssetFilterSelection, and the market filter logic now only match
${chainId}:... values, which breaks legacy bookmarked/shared links that still
use bare addresses or market names. Update the selection matching in
matchesAssetFilterSelection and the market option lookup to accept both the new
chain-prefixed form and the old chain-less form as a fallback, so existing
?collateral=, ?debt=, and ?market= URLs continue to resolve correctly.
In `@pages/earn/index.vue`:
- Around line 249-269: The active/clear filter state in the earn page ignores
the chain selector, so add selectedChains to the filter checks and reset flow.
Update hasActiveFilters and any related empty-state logic in the earn page to
treat selectedChains as an active filter, and make clearEarnFilters also clear
selectedChains alongside the other filter arrays. If there is a second mirrored
filter state block around the earn page’s later section, keep its chain handling
consistent with the same symbols.
In `@pages/explore/index.vue`:
- Around line 322-344: Treat selectedChains as an active filter in the explore
state: update the computed hasActiveFilters in pages/explore/index.vue to
include selectedChains.value.length, and make clearExploreFilters also reset
selectedChains so chain-only filtering is detected and cleared like the other
filters. Apply the same change anywhere else the filter summary/clear logic is
duplicated in the explore view.
- Around line 31-39: The search indexing in the useVaultSearch callback is still
resolving overrides without chain context, which can mix product overrides
across chains for the same vault address. Update the call site in the explore
page to pass the vault’s chainId through applyVaultOverrides, alongside
getProductByVault(addr, vault.chainId), so override resolution stays
chain-scoped and the indexed/displayed product data matches the correct network.
In `@pages/lend/index.vue`:
- Around line 303-325: The filter state logic in the lend page is missing
selectedChains, so chain-only filtering can hide markets without being reflected
in the active-filter state or reset action. Update the computed active-filter
check used by hasActiveFilters to include selectedChains, and make
clearLendFilters clear selectedChains alongside the other filter arrays; use the
existing selectedChains, hasActiveFilters, and clearLendFilters symbols to keep
the empty-state and reset behavior consistent.
In `@utils/eulerLabelsUtils.ts`:
- Around line 113-120: The earn-entry lookup in isVaultRecentlyAdded is not
chain-scoped, so the recently added check can pull tags from merged/default data
for the same address on other chains. Update the earn-entry path to pass chainId
through to the lookup and tag check, using the existing helpers around
getEarnEntryByVault and earnEntryHasTag so the vault is evaluated against the
correct chain-specific labels.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: euler-xyz/coderabbit/.coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 376005c3-5884-4cf1-bb2f-b9cb4b966ff1
📒 Files selected for processing (39)
app.vuecomponents/entities/asset/AssetAvatar.vuecomponents/entities/chains/ChainSelectorItem.vuecomponents/entities/chains/SelectChainModal.vuecomponents/entities/vault/SecuritizeVaultItem.vuecomponents/entities/vault/VaultBorrowItem.vuecomponents/entities/vault/VaultEarnItem.vuecomponents/entities/vault/VaultItem.vuecomponents/entities/vault/VaultLabelsAndAssets.vuecomponents/entities/vault/VaultMaxRoeModal.vuecomponents/entities/vault/discovery/DiscoveryMarketAccordion.vuecomponents/entities/vault/discovery/DiscoveryMarketAttributeMatrix.vuecomponents/entities/vault/discovery/DiscoveryMarketCard.vuecomponents/entities/vault/discovery/DiscoveryMarketGraph.vuecomponents/entities/vault/discovery/DiscoveryMarketMatrix.vuecomponents/layout/TheHeader.vuecomposables/useBestMaxROE.tscomposables/useEulerAddresses.tscomposables/useEulerLabels.tscomposables/useMarketGroups.tscomposables/useRewardsApy.tscomposables/useTokenList.tscomposables/useVaultRegistry.tscomposables/useVaults.tscomposables/useWagmi.tscomposables/useWallets.tsentities/lend-discovery.tsmiddleware/01.network.global.tspages/borrow/index.vuepages/earn/[vault]/index.vuepages/earn/index.vuepages/explore/index.vuepages/lend/[vault]/index.vuepages/lend/index.vuetests/composables/useEulerAccount.test.tstests/utils/discovery-calculations.test.tsutils/discoveryCalculations.tsutils/eulerLabelsUtils.tsutils/vault/categories.ts
| const { selectedChainIds, toggleSelectedChainId } = useEulerAddresses() | ||
|
|
||
| const handleClose = () => { | ||
| emits('close') | ||
| } | ||
| const isSelected = (chainId: number) => selectedChainIds.value.includes(chainId) | ||
| const onClick = (chainId: number) => { | ||
| emits('close') | ||
| setTimeout(() => { | ||
| changeChain(chainId) | ||
| }, 400) | ||
| toggleSelectedChainId(chainId) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Allow deselecting all chains — no minimum-selection guard.
onClick calls toggleSelectedChainId(chainId) unconditionally. Per the upstream contract in useEulerAddresses.ts, if the user toggles off every chain, selectedChainIds becomes [] and chainId.value falls back to 0 (selectedChainIds.value[0] || 0). A chainId of 0 is not a valid chain and will likely break downstream label/vault/registry lookups that key off chainId.
Consider preventing deselection of the last remaining chain (e.g., disable the click or no-op when selectedChainIds.value.length === 1 && isSelected(chainId)).
🛡️ Proposed guard
const onClick = (chainId: number) => {
+ if (selectedChainIds.value.length === 1 && isSelected(chainId)) return
toggleSelectedChainId(chainId)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { selectedChainIds, toggleSelectedChainId } = useEulerAddresses() | |
| const handleClose = () => { | |
| emits('close') | |
| } | |
| const isSelected = (chainId: number) => selectedChainIds.value.includes(chainId) | |
| const onClick = (chainId: number) => { | |
| emits('close') | |
| setTimeout(() => { | |
| changeChain(chainId) | |
| }, 400) | |
| toggleSelectedChainId(chainId) | |
| } | |
| const { selectedChainIds, toggleSelectedChainId } = useEulerAddresses() | |
| const handleClose = () => { | |
| emits('close') | |
| } | |
| const isSelected = (chainId: number) => selectedChainIds.value.includes(chainId) | |
| const onClick = (chainId: number) => { | |
| if (selectedChainIds.value.length === 1 && isSelected(chainId)) return | |
| toggleSelectedChainId(chainId) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/entities/chains/SelectChainModal.vue` around lines 21 - 29, The
SelectChainModal click handler currently allows the last selected chain to be
deselected, which can leave useEulerAddresses with an empty selectedChainIds
array and cause chainId to fall back to 0. Update the onClick logic in
SelectChainModal.vue to guard against deselecting the final remaining chain by
no-oping or disabling the action when selectedChainIds.value.length === 1 and
isSelected(chainId) is true. Keep the behavior aligned with
toggleSelectedChainId from useEulerAddresses so downstream chainId-dependent
lookups do not receive an invalid value.
| const { chainId, selectedChainIds, allowedChainIds } = useEulerAddresses() | ||
| const primarySelectedChainId = computed(() => selectedChainIds.value[0] ?? chainId.value) | ||
| const chainLogoSrc = computed(() => getChainLogoUrl(primarySelectedChainId.value)) | ||
| const selectedChainLabel = computed(() => { | ||
| const count = selectedChainIds.value.length || 1 | ||
| return count === 1 ? String(primarySelectedChainId.value) : `${count} chains` | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Header shows wrong "current chain" when active chain isn't the lowest selected id.
selectedChainIds is sorted ascending and only resyncs to chainId when the active chain is removed from selection — otherwise chainId can be any element, not necessarily selectedChainIds[0]. Since selectedChainIds is never empty by design, selectedChainIds.value[0] ?? chainId.value always resolves to the lowest-id selected chain, never the actual active chain. E.g., user is on chain 137, adds chain 1 → header now shows chain 1's logo/label as "current chain" even though the app's active/transaction chain is still 137. This replaces a prior direct binding to chainId for the avatar label.
🐛 Proposed fix
-const primarySelectedChainId = computed(() => selectedChainIds.value[0] ?? chainId.value)
+const primarySelectedChainId = computed(() => chainId.value || selectedChainIds.value[0])Also applies to: 247-252, 259-264
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/layout/TheHeader.vue` around lines 18 - 24, The header is using
the lowest selected chain instead of the actual active chain, so the “current
chain” display becomes incorrect. Update TheHeader.vue to derive the chain
logo/label from chainId directly rather than selectedChainIds[0], and keep
selectedChainIds only for the multi-chain count text in selectedChainLabel.
Ensure primarySelectedChainId (or its replacement) tracks chainId so the avatar
and single-chain label always reflect the active chain.
| export const getEulerLabelsDataForChain = (chainId: number): EulerLabelsData => | ||
| labelsByChainId.value.get(chainId) ?? labelsData.value |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Avoid falling back to merged labels for explicit chain lookups.
When a caller asks for a specific chainId, returning merged labels on cache miss can attach another chain’s product/entity metadata to the same address. Return an empty dataset or require the per-chain cache to be populated.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@composables/useEulerLabels.ts` around lines 76 - 77, The chain-specific
lookup in getEulerLabelsDataForChain currently falls back to labelsData, which
can return merged cross-chain metadata for a requested chainId on cache miss.
Update this function so explicit chain lookups only return the entry from
labelsByChainId for that chain, and otherwise return an empty EulerLabelsData or
another clearly scoped per-chain default instead of the merged dataset.
| catch (e) { | ||
| logWarn('labels/load', e) | ||
| return true | ||
| } | ||
| finally { | ||
| isLoading.value = false | ||
| isReady.value = true | ||
| if (generation === labelsLoadGeneration) { | ||
| isLoading.value = false | ||
| isReady.value = true | ||
| } | ||
| } | ||
| })() | ||
|
|
||
| pendingLabelsLoad = promise | ||
| pendingLabelsLoadKey = targetKey | ||
| try { | ||
| await promise | ||
| } | ||
| finally { | ||
| if (pendingLabelsLoad === promise) pendingLabelsLoad = undefined | ||
| if (pendingLabelsLoad === promise) { | ||
| pendingLabelsLoad = undefined | ||
| pendingLabelsLoadKey = '' | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Propagate failed or stale label-load results.
loadLabels() builds a Promise<boolean>, but the outer function only awaits it and returns undefined, so callers like app.vue cannot honor loaded === false. Also, the catch path currently reports success after a failed fetch.
Proposed fix
catch (e) {
logWarn('labels/load', e)
- return true
+ return false
}
@@
pendingLabelsLoad = promise
pendingLabelsLoadKey = targetKey
try {
- await promise
+ return await promise
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| catch (e) { | |
| logWarn('labels/load', e) | |
| return true | |
| } | |
| finally { | |
| isLoading.value = false | |
| isReady.value = true | |
| if (generation === labelsLoadGeneration) { | |
| isLoading.value = false | |
| isReady.value = true | |
| } | |
| } | |
| })() | |
| pendingLabelsLoad = promise | |
| pendingLabelsLoadKey = targetKey | |
| try { | |
| await promise | |
| } | |
| finally { | |
| if (pendingLabelsLoad === promise) pendingLabelsLoad = undefined | |
| if (pendingLabelsLoad === promise) { | |
| pendingLabelsLoad = undefined | |
| pendingLabelsLoadKey = '' | |
| } | |
| } | |
| catch (e) { | |
| logWarn('labels/load', e) | |
| return false | |
| } | |
| finally { | |
| if (generation === labelsLoadGeneration) { | |
| isLoading.value = false | |
| isReady.value = true | |
| } | |
| } | |
| })() | |
| pendingLabelsLoad = promise | |
| pendingLabelsLoadKey = targetKey | |
| try { | |
| return await promise | |
| } | |
| finally { | |
| if (pendingLabelsLoad === promise) { | |
| pendingLabelsLoad = undefined | |
| pendingLabelsLoadKey = '' | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@composables/useEulerLabels.ts` around lines 188 - 210, The loadLabels() flow
in useEulerLabels.ts is swallowing failure/stale state: the inner
Promise<boolean> currently returns true in the catch path, and the outer async
wrapper awaits it but never returns the boolean to callers like app.vue. Update
loadLabels() so the catch path yields false for failed loads, and make the outer
function return the awaited promise result while preserving the
pendingLabelsLoad cleanup and the generation/isLoading/isReady logic.
| const get = (address: string, chainId = getDefaultChainId()): VaultEntry | undefined => { | ||
| const keyed = chainId ? registry.value.get(normalizeRegistryKey(address, chainId)) : undefined | ||
| return keyed ?? findUniqueEntryByAddress(address) | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Avoid cross-chain fallback when a chain is specified.
get(address, chainId) and isKnownEscrowAddress(address, chainId) can return data from another chain if the target-chain key is missing but the address exists uniquely elsewhere. That reintroduces cross-chain address collisions for callers that explicitly pass chainId.
Proposed direction
-const get = (address: string, chainId = getDefaultChainId()): VaultEntry | undefined => {
- const keyed = chainId ? registry.value.get(normalizeRegistryKey(address, chainId)) : undefined
- return keyed ?? findUniqueEntryByAddress(address)
+const get = (address: string, chainId?: number): VaultEntry | undefined => {
+ const targetChainId = chainId ?? getDefaultChainId()
+ if (targetChainId) return registry.value.get(normalizeRegistryKey(address, targetChainId))
+ return findUniqueEntryByAddress(address)
}Apply the same exact-chain behavior to isKnownEscrowAddress.
As per path instructions, chain-specific cache/registry values must not collide across Euler V2 chains.
Also applies to: 158-162
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@composables/useVaultRegistry.ts` around lines 77 - 80, The chain-specific
lookup in get(address, chainId) still falls back to
findUniqueEntryByAddress(address), which can return a value from another chain
when the requested chain key is missing; change this so an explicit chainId only
checks the exact registry key and does not cross-fallback. Apply the same
exact-chain-only behavior to isKnownEscrowAddress(address, chainId) so both
helpers honor chain isolation consistently, using the existing
normalizeRegistryKey, registry.value.get, and findUniqueEntryByAddress symbols
to locate the logic.
Source: Path instructions
| const isCurrentVaultLoad = (generation: number, targetChainId: number): boolean => { | ||
| const { chainId } = useEulerAddresses() | ||
| return loadGeneration.value === generation && chainId.value === targetChainId | ||
| const { chainId, selectedChainIds } = useEulerAddresses() | ||
| const activeChainIds = selectedChainIds?.value ?? [chainId.value].filter(Boolean) | ||
| return loadGeneration.value === generation && activeChainIds.includes(targetChainId) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use the same fallback logic as loadVaults().
When selectedChainIds.value is [], loadVaults() loads chainId.value, but isCurrentVaultLoad() checks against [] and rejects that load.
- const activeChainIds = selectedChainIds?.value ?? [chainId.value].filter(Boolean)
+ const selectedIds = selectedChainIds?.value ?? []
+ const activeChainIds = selectedIds.length ? selectedIds : [chainId.value].filter(Boolean)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const isCurrentVaultLoad = (generation: number, targetChainId: number): boolean => { | |
| const { chainId } = useEulerAddresses() | |
| return loadGeneration.value === generation && chainId.value === targetChainId | |
| const { chainId, selectedChainIds } = useEulerAddresses() | |
| const activeChainIds = selectedChainIds?.value ?? [chainId.value].filter(Boolean) | |
| return loadGeneration.value === generation && activeChainIds.includes(targetChainId) | |
| const isCurrentVaultLoad = (generation: number, targetChainId: number): boolean => { | |
| const { chainId, selectedChainIds } = useEulerAddresses() | |
| const selectedIds = selectedChainIds?.value ?? [] | |
| const activeChainIds = selectedIds.length ? selectedIds : [chainId.value].filter(Boolean) | |
| return loadGeneration.value === generation && activeChainIds.includes(targetChainId) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@composables/useVaults.ts` around lines 179 - 182, `isCurrentVaultLoad()` is
using different chain fallback logic than `loadVaults()`, so it can reject a
load when `selectedChainIds.value` is empty. Update `isCurrentVaultLoad()` in
`useVaults.ts` to mirror the same fallback used by `loadVaults()`: derive the
active chain IDs from `selectedChainIds.value` when present, otherwise fall back
to `chainId.value`, and then compare `targetChainId` against that resolved list.
Keep `loadGeneration` as the generation check and ensure the logic stays
consistent with `useEulerAddresses()`.
| if (loadGeneration.value === generation) { | ||
| isReady.value = true | ||
| loadedChainId.value = startChainId | ||
| if (!loadedChainIds.value.includes(startChainId)) { | ||
| loadedChainIds.value = [...loadedChainIds.value, startChainId].sort((a, b) => a - b) | ||
| } | ||
| const { chainId } = useEulerAddresses() | ||
| if (chainId.value === startChainId) { | ||
| loadedChainId.value = startChainId | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const loadVaults = async () => { | ||
| const { selectedChainIds, chainId } = useEulerAddresses() | ||
| resetVaultsState() | ||
| const generation = loadGeneration.value | ||
| const selectedIds = selectedChainIds?.value ?? [] | ||
| const targetChainIds = selectedIds.length ? selectedIds : [chainId.value].filter(Boolean) | ||
|
|
||
| await Promise.all(targetChainIds.map(targetChainId => loadVaultsForChain(targetChainId, generation))) | ||
|
|
||
| if (loadGeneration.value === generation) { | ||
| isReady.value = true | ||
| if (!loadedChainId.value && chainId.value && loadedChainIds.value.includes(chainId.value)) { | ||
| loadedChainId.value = chainId.value |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Don’t mark global vault state ready after the first chain finishes.
loadVaultsForChain() sets isReady and appends loadedChainIds independently, so multi-chain pages can render as ready while other selected chains are still loading. Move global readiness to the outer Promise.all completion, or track readiness per target chain and only publish global readiness once every requested chain has settled.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@composables/useVaults.ts` around lines 817 - 842, The global readiness state
is being updated too early because loadVaultsForChain() marks isReady and
mutates loadedChainIds independently, which can make multi-chain pages look
complete before all requested chains finish loading. Move the global
isReady.value = true logic out of loadVaultsForChain() and keep it in loadVaults
after the Promise.all over targetChainIds completes, or otherwise gate it on all
requested chains settling. Keep the chain-specific loadedChainId/loadedChainIds
updates in loadVaultsForChain(), but only publish global readiness from
loadVaults once every target chain is done.
| const { searchQuery, matchesSearch, clearSearch } = useVaultSearch<EulerEarn>((vault) => { | ||
| const product = applyVaultOverrides(getProductByVault(vault.address), vault.address) | ||
| const product = applyVaultOverrides(getProductByVault(vault.address, vault.chainId), vault.address) | ||
| return [ |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Pass the vault chain into override resolution.
getProductByVault is chain-scoped, but applyVaultOverrides still receives only the address, so cross-chain address collisions can apply the wrong market metadata.
Proposed fix
- const product = applyVaultOverrides(getProductByVault(vault.address, vault.chainId), vault.address)
+ const product = applyVaultOverrides(getProductByVault(vault.address, vault.chainId), vault.address, vault.chainId)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { searchQuery, matchesSearch, clearSearch } = useVaultSearch<EulerEarn>((vault) => { | |
| const product = applyVaultOverrides(getProductByVault(vault.address), vault.address) | |
| const product = applyVaultOverrides(getProductByVault(vault.address, vault.chainId), vault.address) | |
| return [ | |
| const { searchQuery, matchesSearch, clearSearch } = useVaultSearch<EulerEarn>((vault) => { | |
| const product = applyVaultOverrides(getProductByVault(vault.address, vault.chainId), vault.address, vault.chainId) | |
| return [ |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pages/earn/index.vue` around lines 38 - 40, The override lookup in
useVaultSearch for EulerEarn is only using the vault address, which can select
the wrong metadata when the same address exists on multiple chains. Update the
product resolution flow so applyVaultOverrides receives the vault chainId as
well as the address, and thread that chain context through the getProductByVault
result handling in pages/earn/index.vue. Keep the change localized to the search
callback so cross-chain vaults resolve the correct market metadata.
| const { searchQuery, matchesSearch, clearSearch } = useVaultSearch<EVault>((vault) => { | ||
| const product = applyVaultOverrides(getProductByVault(vault.address), vault.address) | ||
| const product = applyVaultOverrides(getProductByVault(vault.address, vault.chainId), vault.address) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Pass chainId through vault override lookup.
The base product lookup is chain-aware, but override resolution remains address-only, which can show/search the wrong override for duplicate vault addresses across chains.
Proposed fix
- const product = applyVaultOverrides(getProductByVault(vault.address, vault.chainId), vault.address)
+ const product = applyVaultOverrides(getProductByVault(vault.address, vault.chainId), vault.address, vault.chainId)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { searchQuery, matchesSearch, clearSearch } = useVaultSearch<EVault>((vault) => { | |
| const product = applyVaultOverrides(getProductByVault(vault.address), vault.address) | |
| const product = applyVaultOverrides(getProductByVault(vault.address, vault.chainId), vault.address) | |
| const { searchQuery, matchesSearch, clearSearch } = useVaultSearch<EVault>((vault) => { | |
| const product = applyVaultOverrides(getProductByVault(vault.address, vault.chainId), vault.address, vault.chainId) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pages/lend/index.vue` around lines 43 - 44, The vault search lookup is
resolving overrides using only the vault address, which can surface the wrong
override when the same address exists on multiple chains. Update the lookup
inside useVaultSearch so the override resolution path in applyVaultOverrides
also receives vault.chainId, matching the chain-aware getProductByVault call.
Keep the fix localized to the vault product resolution logic in
pages/lend/index.vue and ensure both lookup steps use the same chain context.
| const borrowableVaults = computed(() => { | ||
| return list.value.filter(vault => | ||
| (showAllLabelEntries.value || !isVaultNotExplorableLend(vault.address)) | ||
| (showAllLabelEntries.value || !isVaultNotExplorableLend(vault.address, vault.chainId)) | ||
| && borrowList.value.some(pair => pair.borrow.address === vault.address) | ||
| && !isOpDisabled(vault, OP_DEPOSIT), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Match borrowable vault pairs by chain and address.
borrowList is now multi-chain, so address-only matching can mark a vault borrowable because another chain has the same vault address.
Proposed fix
- && borrowList.value.some(pair => pair.borrow.address === vault.address)
+ && borrowList.value.some(pair =>
+ pair.borrow.chainId === vault.chainId
+ && pair.borrow.address.toLowerCase() === vault.address.toLowerCase(),
+ )📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const borrowableVaults = computed(() => { | |
| return list.value.filter(vault => | |
| (showAllLabelEntries.value || !isVaultNotExplorableLend(vault.address)) | |
| (showAllLabelEntries.value || !isVaultNotExplorableLend(vault.address, vault.chainId)) | |
| && borrowList.value.some(pair => pair.borrow.address === vault.address) | |
| && !isOpDisabled(vault, OP_DEPOSIT), | |
| const borrowableVaults = computed(() => { | |
| return list.value.filter(vault => | |
| (showAllLabelEntries.value || !isVaultNotExplorableLend(vault.address, vault.chainId)) | |
| && borrowList.value.some(pair => | |
| pair.borrow.chainId === vault.chainId | |
| && pair.borrow.address.toLowerCase() === vault.address.toLowerCase(), | |
| ) | |
| && !isOpDisabled(vault, OP_DEPOSIT), |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@pages/lend/index.vue` around lines 130 - 134, The borrowable vault filter in
computed borrowableVaults is matching borrowList entries by address only, which
can incorrectly mark same-address vaults on different chains as borrowable.
Update the some() check to match both borrow.address and borrow.chainId against
the current vault.address and vault.chainId, keeping the existing
showAllLabelEntries and isOpDisabled conditions unchanged.
LeonardEulerXYZ
left a comment
There was a problem hiding this comment.
Leonard review — PR #619
Verdict: Request changes.
I reviewed the current head 46a0160 as a multi-chain browsing/risk-assessment pass. The main architecture is moving in the right direction — chain-qualified keys are being threaded through labels, rewards, registry, list filters, and discovery metrics — but I found several correctness/UX issues that should be fixed before merge.
Scope covered:
- data/composables: labels, vault registry, vault loading, token/reward lookups, market groups
- user-facing routes: Explore, Lend, Earn, Borrow, header chain selector, vault cards/detail anchoring
- bot feedback: CodeRabbit currently only has the processing/walkthrough comment on this head; no actionable inline comments were present to verify
- scalability / maintainability hygiene: checked sibling list pages and shared chain-scoped registry/data paths, not only the first touched component
Validation performed:
npm test -- --run tests/composables/useEulerAccount.test.ts tests/utils/discovery-calculations.test.ts— pass, 19 testsnpm run typecheck— passnpm run build— pass, with existing chunk/module-preload style warningsgit diff --check origin/development...HEADon the reviewed diff — pass- Browser visual smoke on the PR Railway preview, headed Chromium under Xvfb:
- desktop
/lend?network=1&networks=1— loaded real Lend content; header renders the selected chain as1 - mobile
/lend?network=1&networks=1— loaded real Lend content; chain chip is icon-only on mobile but text extraction still exposes1 - desktop
/explore?network=1&networks=1,8453— loaded real Explore content; header renders2 chains
- desktop
Screenshot evidence:
- Desktop Lend, single selected chain shows numeric
1 - Mobile Lend, same route at 390px
- Desktop Explore, multi-chain selection shows
2 chains
Findings are inline. The two highest-risk ones are the explicit-chain registry fallback and the global readiness flag becoming true before all selected chains finish loading. Both can make a multi-chain page look settled while it is using wrong-chain or partial-chain data — a small symbol error with a rather large denominator.
Smoke coverage: browser visual smoke + mobile smoke, no wallet/signing smoke.
| return registry.value.get(normalizeAddress(address)) | ||
| const get = (address: string, chainId = getDefaultChainId()): VaultEntry | undefined => { | ||
| const keyed = chainId ? registry.value.get(normalizeRegistryKey(address, chainId)) : undefined | ||
| return keyed ?? findUniqueEntryByAddress(address) |
There was a problem hiding this comment.
This fallback is unsafe when the caller supplied an explicit chainId. If chain 2 asks for get(address, 2) before that chain's entry is loaded, and the same address exists only once in the registry on chain 1, findUniqueEntryByAddress() returns the chain-1 vault. The downstream typed helpers (has, getType, isVerifiedVault, escrow/category checks) then treat a wrong-chain entry as valid. For multi-chain browsing this is a data-integrity bug: address-only fallback should only apply to genuinely address-only legacy calls, not after an explicit chain miss. Please add a focused same-address/different-chain registry test as well.
| } | ||
| finally { | ||
| if (loadGeneration.value === generation && chainId.value === startChainId) { | ||
| if (loadGeneration.value === generation) { |
There was a problem hiding this comment.
This makes the global isReady flag true as soon as the first selected chain finishes. loadVaults() starts all selected chains concurrently, but every loadVaultsForChain() publishes readiness in its own finally; consumers such as market groups treat that as the selected multi-chain dataset being ready. A fast chain can therefore render a partial catalogue while slower selected chains are still loading. Please keep readiness false until all target chain loads complete, or derive it from selectedChainIds.every(id => loadedChainIds.includes(id)).
| const sdk = await getEulerSdk() | ||
| const result = await sdk.eVaultService.fetchVaults( | ||
| chainId.value, | ||
| targetChainId, |
There was a problem hiding this comment.
The on-demand direct-market path fetches from targetChainId, but the returned SDK vaults are pushed into memberVaults without tagging them with that targetChainId. The main vault-loading path now uses chain-tagged vaults, and the rest of this file keys labels/graph data off vault.chainId; direct routes for a non-current or chainId:productKey market can end up with undefined chain IDs and wrong/fallback label lookups. Please assign the target chain to each fetched vault before computing metrics/graph data, and cover the direct-route/on-demand case in a focused test.
| return getUniqueEntitiesByVaults(group.vaults).some(e => selectedRiskManagers.value.includes(e.name)) | ||
| } | ||
|
|
||
| const matchesChainFilter = (group: MarketGroup): boolean => { |
There was a problem hiding this comment.
The new Chain selector is a real filter, but the active-filter/clear-filter path does not include it. On Explore, Lend, Earn, and Borrow, selectedChains is applied to the list but omitted from hasActiveFilters/hasClearableFilters and from the corresponding clear function. If a chain filter hides all results, users can get a misleading No markets/vaults yet state with no useful clear action; if another filter exposes Clear filters, the chain filter remains applied. Please wire selectedChains.length and reset it consistently across all four list pages.
| const chainLogoSrc = computed(() => getChainLogoUrl(primarySelectedChainId.value)) | ||
| const selectedChainLabel = computed(() => { | ||
| const count = selectedChainIds.value.length || 1 | ||
| return count === 1 ? String(primarySelectedChainId.value) : `${count} chains` |
There was a problem hiding this comment.
For a single selected chain this renders the raw numeric chain id (1, 8453, etc.) as the visible header label. The chain modal/options already use chain names, and the PR smoke confirmed the desktop Lend header displays 1. This is a small but user-visible regression; use the chain registry name with numeric id only as fallback.
Summary
Changes
networks, and a multi-select chain picker in the header.Test plan
Summary by CodeRabbit
New Features
Bug Fixes