Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 96 additions & 19 deletions components/entities/activity/ActivityEventRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import type { ActivityCategory, ActivityEvent, LiquidationRecord } from '@eulerxyz/euler-v2-sdk'
import { getAddress } from 'viem'
import { getExplorerLink } from '~/utils/block-explorer'
import { fetchVaultCategory } from '~/utils/vault/categories'
import {
getActivityAddressCollectionSummary,
getActivityLiquidationBonusEntry,
Expand Down Expand Up @@ -56,6 +57,7 @@ const {
registryVersion,
} = useVaultRegistry()
const { getTokenByAddress } = useTokenList()
const { buildKnownSymbols, resolveSymbol: resolveTokenSymbol } = useTokenSymbolResolver()
const vaultAddress = computed(() => event.vault ?? '')
const vaultProduct = useEulerProductOfVault(vaultAddress)
const collateralVaultAddress = computed(() =>
Expand All @@ -68,8 +70,16 @@ const collateralProduct = useEulerProductOfVault(collateralVaultAddress)
// so none of them fall back to a raw shortened address.
const metadataVaultAddresses = computed(() => getActivityResolvableVaultAddresses(event))

// Factory membership gates the registry fetch: addresses the vault resolver
// doesn't recognize as Euler vaults (e.g. an external ERC-4626 configured as
// a router resolved vault) skip the registry's on-chain fetch fallback and
// deterministically render as their raw token symbol instead.
watch(metadataVaultAddresses, (addresses) => {
for (const address of addresses) void getOrFetchRegistryVault(address)
for (const address of addresses) {
void fetchVaultCategory(address).then((category) => {
if (category) void getOrFetchRegistryVault(address)
})
}
}, { immediate: true })

const tokenMetadata = (address: `0x${string}`) => {
Expand All @@ -93,6 +103,15 @@ const activityVaultMetadata = (address: `0x${string}`) => {
}
}

const knownTokenSymbols = computed(() => {
void registryVersion.value
return buildKnownSymbols()
})

const activityTokenSymbol = (address: `0x${string}`) =>
tokenMetadata(address)?.symbol
?? resolveTokenSymbol(address, knownTokenSymbols.value)

const resolveAvatarAsset = (asset: ActivityEvent['assets'][number]) => {
const representsVaultShares = asset.kind === 'shares'
|| (event.category === 'liquidations' && ['collateral', 'yield'].includes(asset.kind))
Expand Down Expand Up @@ -179,15 +198,23 @@ const assets = computed(() => {
const changes = computed(() => {
// Re-resolve human-readable vault names when registry metadata arrives.
void registryVersion.value
return getActivityChangeEntries(event, activityVaultMetadata)
return getActivityChangeEntries(event, activityVaultMetadata, activityTokenSymbol)
.filter(entry => event.category !== 'liquidations' || entry.field !== 'collateral')
.map(entry => ({
kind: 'change' as const,
key: `change:${entry.field}`,
label: entry.label,
value: entry.value,
valueTitle: entry.value,
summary: entry.summary,
addresses: entry.addresses,
avatarAssets: entry.field === 'asset_pair'
|| (event.type === 'set_resolved_vault' && entry.field === 'asset')
? entry.addresses?.map(address => ({
address: address.address,
symbol: address.label ?? '',
}))
: undefined,
}))
})
// The seized collateral vault trails the numbers as its own metadata line, so
Expand Down Expand Up @@ -237,15 +264,17 @@ const portfolioPosition = computed(() => showVault
const COLLAPSED_ENTRY_COUNT = 1
const hiddenEntryCount = computed(() =>
Math.max(0, details.value.length - COLLAPSED_ENTRY_COUNT))
const addressCollectionSummary = (count: number) =>
getActivityAddressCollectionSummary(event.type, count)
const addressCollectionSummary = (detail: (typeof details.value)[number]) =>
'summary' in detail && detail.summary
? detail.summary
: getActivityAddressCollectionSummary(event.type, detailAddressCount(detail))
const detailAddressCount = (detail: (typeof details.value)[number]) =>
'addresses' in detail && Array.isArray(detail.addresses) ? detail.addresses.length : 0
const hasExpandableDetails = computed(() =>
hiddenEntryCount.value > 0
|| details.value
.slice(0, COLLAPSED_ENTRY_COUNT)
.some(detail => addressCollectionSummary(detailAddressCount(detail)) !== null))
.some(detail => addressCollectionSummary(detail) !== null))
const eventIcon = computed(() => getActivityEventIcon(event))
const eventLabel = computed(() => portfolioPosition.value
? `${portfolioPosition.value.label} liquidated`
Expand Down Expand Up @@ -361,7 +390,11 @@ const vaultDisplay = computed(() => {
v-for="(detail, index) in details"
:key="detail.key"
class="activity-event-row__detail min-w-0"
:class="index >= COLLAPSED_ENTRY_COUNT && !expanded ? 'activity-event-row__secondary-detail hidden' : ''"
:class="[
index >= COLLAPSED_ENTRY_COUNT && !expanded ? 'activity-event-row__secondary-detail hidden' : '',
detailAddressCount(detail) ? 'activity-event-row__detail--address-list' : '',
detail.key === 'change:asset_pair' ? 'activity-event-row__detail--asset-pair' : '',
]"
>
<div
v-if="detail.label"
Expand Down Expand Up @@ -415,25 +448,43 @@ const vaultDisplay = computed(() => {
<template v-else>
<template v-if="detail.addresses?.length">
<div
v-if="!expanded && addressCollectionSummary(detail.addresses.length)"
class="break-words text-p3 text-content-primary"
v-if="!expanded && addressCollectionSummary(detail)"
class="mt-2 flex min-w-0 items-center gap-8"
>
{{ addressCollectionSummary(detail.addresses.length) }}
<AssetAvatar
v-if="'avatarAssets' in detail && detail.avatarAssets?.length"
:asset="detail.avatarAssets"
size="20"
class="shrink-0"
/>
<span class="min-w-0 break-words text-p3 text-content-primary">
{{ addressCollectionSummary(detail) }}
</span>
</div>
<div
v-else
class="activity-event-row__detail-addresses mt-2 flex min-w-0 flex-col items-start gap-2 text-p3"
>
<ActivityAddress
v-for="address in detail.addresses"
:key="address.address"
:address="address.address"
:chain-id="event.chainId"
:label="address.label"
:link-kind="address.linkKind"
:vault-type="address.vaultType"
compact-vault
/>
<div
v-for="(address, addressIndex) in detail.addresses"
:key="`${address.address}:${addressIndex}`"
class="flex w-full min-w-0 items-center gap-8"
>
<AssetAvatar
v-if="'avatarAssets' in detail && detail.avatarAssets?.[addressIndex]"
:asset="detail.avatarAssets[addressIndex]"
size="20"
class="shrink-0"
/>
<ActivityAddress
:address="address.address"
:chain-id="event.chainId"
:label="address.label"
:link-kind="address.linkKind"
:vault-type="address.vaultType"
compact-vault
/>
</div>
</div>
</template>
<div
Expand Down Expand Up @@ -633,6 +684,20 @@ const vaultDisplay = computed(() => {
column-gap: 12px;
}

/* Multi-row values such as oracle asset pairs should start beside their
label instead of centering the label between the two assets. */
.activity-event-row__details--expanded .activity-event-row__detail--address-list:not(.hidden) {
align-items: start;
}

.activity-event-row__details--expanded .activity-event-row__detail--address-list .activity-event-row__detail-label {
padding-top: 2px;
}

.activity-event-row__details--expanded .activity-event-row__detail--address-list .activity-event-row__detail-addresses {
margin-top: 0;
}

.activity-event-row__details--expanded .activity-event-row__detail > :not(.activity-event-row__detail-label) {
grid-column: 2;
}
Expand Down Expand Up @@ -675,4 +740,16 @@ const vaultDisplay = computed(() => {
margin-top: 2px;
}
}

.activity-event-row__details--expanded .activity-event-row__detail--asset-pair:not(.hidden) {
display: block;
}

.activity-event-row__details--expanded .activity-event-row__detail--asset-pair .activity-event-row__detail-label {
padding-top: 0;
}

.activity-event-row__details--expanded .activity-event-row__detail--asset-pair .activity-event-row__detail-addresses {
margin-top: 2px;
}
</style>
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
]
},
"dependencies": {
"@eulerxyz/euler-v2-sdk": "1.2.4",
"@eulerxyz/euler-v2-sdk": "1.2.5",
"@floating-ui/vue": "2.0.1",
"@gvade/nuxt3-svg-sprite": "1.0.3",
"@keyringnetwork/keyring-connect-sdk": "3.2.0",
Expand Down
129 changes: 126 additions & 3 deletions tests/utils/activity-display.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ describe('activity display helpers', () => {
'repay',
'set_caps',
'set_ltv',
'set_oracle_config',
'set_fallback_oracle',
'set_resolved_vault',
'set_oracle_governor',
'liquidation',
]))

Expand Down Expand Up @@ -192,9 +196,15 @@ describe('activity display helpers', () => {

it('uses normalized labels and titleizes fallback event types', () => {
expect(formatActivityEventLabel({ label: 'Borrowed USDC', type: 'borrow' })).toBe('Borrowed USDC')
expect(formatActivityEventLabel({ type: 'set_supply_cap' })).toBe('Set supply cap')
expect(formatActivityEventLabel({ type: 'set_ltv' })).toBe('Set LTV')
expect(formatActivityEventLabel({ type: 'set_interest_rate_model' })).toBe('Set interest rate model')
expect(formatActivityEventLabel({ type: 'set_supply_cap' })).toBe('Supply cap updated')
expect(formatActivityEventLabel({ type: 'set_ltv' })).toBe('LTV updated')
expect(formatActivityEventLabel({ type: 'set_interest_rate_model' })).toBe('Interest rate model updated')
expect(formatActivityEventLabel({ type: 'set_liquidation_cool_off_time' })).toBe('Liquidation cool-off time updated')
expect(formatActivityEventLabel({ type: 'set_is_allocator' })).toBe('Allocator status updated')
expect(formatActivityEventLabel({ type: 'set_oracle_config' })).toBe('Oracle route updated')
expect(formatActivityEventLabel({ type: 'set_fallback_oracle' })).toBe('Fallback oracle updated')
expect(formatActivityEventLabel({ type: 'set_resolved_vault' })).toBe('Resolved vault updated')
expect(formatActivityEventLabel({ type: 'set_oracle_governor' })).toBe('Oracle governor updated')
})

it('labels and styles vault share transfers relative to the event position', () => {
Expand Down Expand Up @@ -703,6 +713,119 @@ describe('activity display helpers', () => {
}, getVaultMetadata)).toEqual([
{ field: 'new_supply_cap', label: 'New supply cap', value: '155M USDC' },
])

expect(getActivityChangeEntries({
type: 'set_oracle_config',
vault: VAULT,
vaultType: 'evk',
change: {
fields: {
router: OTHER_VAULT,
oracle: VAULT,
asset1: SHARES,
asset0: ASSET,
},
},
}, getVaultMetadata, address => address === ASSET
? 'AUSD'
: address === SHARES
? 'PT-AUSD'
: undefined)).toEqual([
{
field: 'asset_pair',
label: 'Asset pair',
summary: 'AUSD / PT-AUSD',
addresses: [
{ address: ASSET, label: 'AUSD', linkKind: 'explorer' },
{ address: SHARES, label: 'PT-AUSD', linkKind: 'explorer' },
],
},
{
field: 'oracle',
label: 'Oracle',
addresses: [{ address: VAULT, linkKind: 'explorer' }],
},
{
field: 'router',
label: 'Router',
addresses: [{ address: OTHER_VAULT, linkKind: 'explorer' }],
},
])

// The resolved vault leads (the collapsed row shows only the first
// entry), and the asset address decodes into its token symbol.
expect(getActivityChangeEntries({
type: 'set_resolved_vault',
vault: VAULT,
vaultType: 'evk',
change: {
fields: {
router: ASSET,
resolved_vault: OTHER_VAULT,
asset: SHARES,
},
},
}, getVaultMetadata, address => address === SHARES ? 'PT-AUSD' : undefined)).toEqual([
{
field: 'resolved_vault',
label: 'Resolved vault',
addresses: [{
address: OTHER_VAULT,
label: 'Collateral vault',
linkKind: 'vault',
vaultType: 'evk',
}],
},
{
field: 'asset',
label: 'Asset',
addresses: [{ address: SHARES, label: 'PT-AUSD', linkKind: 'explorer' }],
},
{
field: 'router',
label: 'Router',
addresses: [{ address: ASSET, linkKind: 'explorer' }],
},
])

// A resolved vault the registry cannot resolve (e.g. a non-Euler
// ERC-4626) falls back to its token symbol and an explorer link instead
// of a dead internal vault page.
expect(getActivityChangeEntries({
type: 'set_resolved_vault',
vault: VAULT,
vaultType: 'evk',
change: { fields: { resolved_vault: SHARES, asset: ASSET } },
}, getVaultMetadata, address => address === SHARES
? 'sUSDS'
: address === ASSET
? 'USDC'
: undefined)).toEqual([
{
field: 'resolved_vault',
label: 'Resolved vault',
addresses: [{ address: SHARES, label: 'sUSDS', linkKind: 'explorer' }],
},
{
field: 'asset',
label: 'Asset',
addresses: [{ address: ASSET, label: 'USDC', linkKind: 'explorer' }],
},
])

// Without a symbol source the asset falls back to its plain address link.
expect(getActivityChangeEntries({
type: 'set_resolved_vault',
vault: VAULT,
vaultType: 'evk',
change: { fields: { asset: SHARES } },
}, getVaultMetadata)).toEqual([
{
field: 'asset',
label: 'Asset',
addresses: [{ address: SHARES, linkKind: 'explorer' }],
},
])
})

it('orders LTV change fields and trims ramp fields on immediate changes', () => {
Expand Down
Loading
Loading