This document describes how Euler Lite loads, filters, enriches, and renders protocol activity for vault overviews and the portfolio Activity tab.
Activity is a V3-backed history surface. Lite does not invent an activity indexer: it calls the Euler V2 SDK activityService, which talks to upstream V3 through the same-origin /api/internal/v3 proxy. Lite owns UI availability gating, display filtering, liquidation enrichment, and presentation.
Vault overview accordion ─┐
Portfolio /activity tab ─┤
▼
useActivityAvailability
(V3 chain gate + SDK capabilities)
▼
ActivityFeed.vue
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
useActivityFeed useActivityLiquidationDetails activity-display.ts
(events pages) (/v3/liquidations enrich) (filters, labels, groups)
│ │
└────────┬────────┘
▼
getEulerSdkForChain → activityService
▼
/api/internal/v3 → upstream V3
Two scopes:
| Scope | SDK call | Upstream path |
|---|---|---|
| Account | fetchAccountActivityEvents |
GET /v3/activity/accounts/0x…/events |
| Vault | fetchVaultActivityEvents |
GET /v3/activity/vaults/{chainId}/0x…/events?vaultType=… |
| Liquidation enrich | fetchLiquidations |
GET /v3/liquidations |
There are no dedicated Nuxt activity routes beyond the shared V3 proxy allowlist in server/utils/v3-proxy.ts.
| Surface | Path | Scope |
|---|---|---|
| EVK / Earn / Securitize vault overview | VaultOverviewBlockActivity.vue |
Vault (vaultType = evk / earn / securitize) |
| Portfolio Activity tab | pages/portfolio/activity.vue |
Account (useEffectiveAddress() — spy address when spy mode is active) |
Vault accordion mounts closed by default. It stays hidden until availability is known (or the capability check fails and retry UI is shown). Runtime coverage.status === 'unsupported' with no category filter selected hides the whole section / portfolio tab.
useActivityAvailability(scope, chainId):
- Rejects invalid chain ids (
invalid-chain). - Requires
useV3ChainGate().isV3EnabledForChain(v3-disabledwhen gated off). - Reads
activityService.getCapabilities()andgetScopeSupport(...). - Treats scope support
unknownas requestable — response coverage is authoritative. Only explicitunsupportedhides the surface.
shouldRender is true when supported or when the capability check failed (capability-check-failed), so users can retry instead of losing the section silently.
Portfolio tab visibility is coordinated through usePortfolioActivityRuntimeSupport (useState keyed by owner:chainId) between pages/portfolio.vue and pages/portfolio/activity.vue.
useActivityFeed({ scope, enabled, categories, limit = 25 }):
- Always sends scope-specific
eventTypesfromgetDisplayActivityEventTypes. - Omits
categoriesfrom the request when the UI selection is empty (= union of chip categories, not “every upstream category”). - Dedupes pages by
event.id(mergeActivityEvents). - Stale head:
ACTIVITY_QUERY_STALE_TIME_MS= 60s (utils/sdk-query-policy.ts). Reopening a stale feed refreshes while keeping last-good rows. - Invalidates via
subscribeToSdkQueryInvalidationsonqueryAccountActivityEvents/queryVaultActivityEvents(bothinvalidateAfterTx: true). - Auto display-fill: up to 3 append pages when client filtering empties the visible set (
AUTO_DISPLAY_FILL_PAGE_LIMIT). - Pagination guard: append throws if
nextCursordoes not advance. - Mid-flight context changes are discarded via
activeRequestId+buildActivityFeedContextKey.
Returned state flags worth knowing: hasColdError (error + no rows), hasStaleError (error + retained rows), isPartial / isSyncing / isUnsupported from coverage meta.
getVaultActivityFilterOptions / getDefaultVaultActivityFilter:
| Vault type | Default chip | Categories |
|---|---|---|
evk |
lending-borrowing |
lending + borrowing; also governance; liquidations |
earn |
lending |
lending; governance (no liquidations chip) |
securitize |
lending |
lending; governance |
Default is not “All”. Opening a vault Activity section fetches lending(+borrowing) first. Selecting nothing (“All”) resolves to the union of that vault type’s option categories.
Borrowability of the live vault does not remove historical borrowing filters.
- Unfiltered query categories:
lending,borrowing,liquidations(getAccountActivityCategories). - Visible chips: liquidations only (
getAccountActivityFilterOptions). Lending/borrowing remain in the All query; verbs and tx grouping communicate them. - The SDK
accountcategory is deliberately omitted — none of its event types are displayed on Lite. - Categories present in label maps but not queried here:
swaps,rewards, and account-scopegovernance.
getDisplayActivityEventTypes + filterActivityEventsForDisplay keep only Lite-relevant types.
Account: deposit, withdraw, borrow, repay, pull_debt, liquidation.
EVK vault: lending/borrowing ops plus transfer, debt_socialized, pull_debt, liquidation, approval, balance_forwarder_status, convert_fees, and governance setters (set_caps, set_ltv, hooks, IRM, fees, …).
Earn / Securitize: deposit/withdraw/transfer plus their reallocation / queue / governance ops (see VAULT_ACTIVITY_EVENT_TYPES in utils/activity-display.ts).
Explicitly excluded noise (covered by tests): interest_accrued, accrue_interest, mint, burn, earn update_last_total_assets / update_lost_assets.
After the allowlist, the feed always runs with hideZeroLiquidations: true and also:
- Drops zero-value liquidations (both assets + collateral
amountRaware0; malformed values are kept). - Drops violator
repayevents that match a same-tx liquidation (same account as violator + matching assets amount). Liquidator repayments are kept. - Drops shadow
transferof shares that pair with deposit/withdraw/liquidation/reallocation in the same tx (same share address + amountRaw).
Pairing re-runs on the merged raw event list, so a shadow transfer on page 1 can disappear after a later page brings its paired deposit.
useActivityLiquidationDetails({ events }) is a second, fail-soft pass:
- Groups events by
chainId:vault, windows min/max event unix ± 1s. - Pages
/v3/liquidationsat limit 100, max 3 pages. - Join key:
chainId:txHash:vault:violator:collateral:repayAssets(lowercased). Event payload falls back to account / collateral asset / assets amountRaw. - Fetch failures roll back the covered-window claim; rows stay unenriched with no feed-level error.
getActivityLiquidationDisplayDetails maps a joined record into optional display fields. Every field is independent: a missing piece leaves that part of the event row as-is.
| Display field | Source | When it is absent |
|---|---|---|
repayUsd |
record.repayAssetsUsd |
Event-row USD (formatActivityAssetUsd) if the event carried amountUsd |
collateralUsd |
record.collateralAssetsUsd |
Same event-row USD fallback |
collateralAmount |
collateralAssets + collateralAssetDecimals (symbol from token metadata) |
Keep the event's share quantity; do not hide USD |
bonus / bonusTone |
Signed bonusUsd first; else unitOfAccountValuation.bonusValue |
No bonus row |
getActivityAssetDisplayValues in activityEventRowDetails.ts applies those fields:
- USD and native conversion are independent. Oracle-enriched USD (
repayUsd/collateralUsd) is preferred even when share decimals are missing or native collateral metadata isnull. Converted underlying units do not require a USD leg, and a USD leg does not imply conversion. - Collateral label becomes "Collateral seized" only after a successful native conversion. Otherwise it stays "Collateral shares seized" (or the generic event label).
- A dedicated liquidation USD of
$0.00is kept; it is not treated as missing. - Bonus title is event-time USD when
bonusUsdis present, otherwise "quoted by the protocol oracle at the liquidation" for the unit-of-account fallback. Lite does not recompute the bonus from rounded USD legs.
The pinned @eulerxyz/euler-v2-sdk 3.4.1 parser accepts valuation.source of historical-price-snapshots or historical-protocol-oracle (required on typed records). Lite display does not branch on that source. A row may mix a snapshot debt USD with oracle-valued collateral.
Regression fixtures live in tests/fixtures/liquidation-oracle-usd.ts (Unichain mixed oracle USD with null native collateral).
- Portfolio:
groupActivityEventsByTransaction→chainId:txHash. Header is"Liquidation transaction"when any event is a liquidation, else"Transaction". Groups collapse after 3 events. - Vault: one synthetic group per event (no tx bundling).
Allowlisted GET patterns only (server/utils/v3-proxy.ts):
/v3/activity/accounts/0x[40hex]/events/v3/activity/vaults/[chainId]/0x[40hex]/events/v3/liquidations
Anything else 404s at the edge. Proxy logs keep chain / vault / categories / eventTypes / safe ranges and intentionally omit account / violator / liquidator addresses. Backoff keys redact owner/vault path segments.
| Condition | Behavior |
|---|---|
| V3 disabled / invalid chain / unsupported scope | Hide surface |
| Capability check throws | Keep surface + retry |
| Cold feed error | Error card + retry |
| Refresh error with rows | Stale warning; keep last events |
| Liquidation enrich failure | Silent; rows unenriched |
Coverage unsupported + All filters |
Hide tab / vault section |
Coverage unsupported + specific chip |
“Not available for selected categories” |
Coverage partial / syncing |
Banners / empty messaging |
- Vault default filter ≠ All — documenting “shows every category on open” is wrong.
- Portfolio chip row ≠ query set — All still fetches lending + borrowing + liquidations.
- Event-type filtering is both a request
eventTypeslist and a client allowlist. - Liquidation USD/bonus comes from a separate endpoint; absence is expected and silent.
- Do not gate liquidation USD on native collateral conversion (or the reverse). A share-quantity "Amount unavailable" can still show
$8.15. - Account feed groups by transaction; vault feed does not.
ActivityAddresssupportslinkKind: 'spy', but current display helpers only emitexplorer/vaultlinks.
| File | Purpose |
|---|---|
composables/useActivityFeed.ts |
Paged event loading, stale refresh, display-fill |
composables/useActivityAvailability.ts |
Capability / scope gating |
composables/useActivityLiquidationDetails.ts |
Fail-soft /v3/liquidations enrichment |
composables/useActivityNowMs.ts |
Shared 60s clock for relative timestamps |
composables/usePortfolioActivityRuntimeSupport.ts |
Portfolio tab visibility coordination |
utils/activity-display.ts |
Filters, allowlists, labels, grouping, getActivityLiquidationDisplayDetails |
components/entities/activity/activityEventRowDetails.ts |
Independent USD vs native liquidation display helpers |
components/entities/activity/ActivityFeed.vue |
Orchestrator |
components/entities/activity/ActivityEventRow.vue |
Row / expand / registry metadata |
components/entities/activity/ActivityCategoryFilters.vue |
All + multi-select chips |
components/entities/activity/ActivityAddress.vue |
Explorer / vault / spy links |
components/entities/vault/overview/VaultOverviewBlockActivity.vue |
Vault accordion host |
pages/portfolio/activity.vue |
Account feed page |
server/utils/v3-proxy.ts |
Allowlist + log redaction |
utils/sdk-query-policy.ts |
60s stale + post-tx invalidation |