- {{ addressCollectionSummary(detail.addresses.length) }}
+
+
+ {{ addressCollectionSummary(detail) }}
+
{
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;
}
@@ -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;
+}
diff --git a/package-lock.json b/package-lock.json
index 6ccc8b051..a6ce61644 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"hasInstallScript": true,
"license": "MIT",
"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",
@@ -1551,9 +1551,9 @@
}
},
"node_modules/@eulerxyz/euler-v2-sdk": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@eulerxyz/euler-v2-sdk/-/euler-v2-sdk-1.2.4.tgz",
- "integrity": "sha512-wMwjOi9gWTEL87lio+nKjUxw0hV7n3J5NI5jOaOv5MkPblVByPSgJj971dsRCy7O+b95UTIt2Lh1rGAHRHqqmQ==",
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@eulerxyz/euler-v2-sdk/-/euler-v2-sdk-1.2.5.tgz",
+ "integrity": "sha512-Ug9FzKyV/UdVaTzjKO5RqIhspAO9NHdvfsSrv2b8maIgsfBr9l6s4qLqjmlek9fjOOPC+4aurDEmpHT4GTUDNA==",
"license": "MIT",
"dependencies": {
"viem": "2.48.8"
diff --git a/package.json b/package.json
index 06b7e0d6c..e9e4e1c92 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/tests/utils/activity-display.test.ts b/tests/utils/activity-display.test.ts
index c9f873740..7a8e19e79 100644
--- a/tests/utils/activity-display.test.ts
+++ b/tests/utils/activity-display.test.ts
@@ -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',
]))
@@ -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', () => {
@@ -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', () => {
diff --git a/utils/activity-display.ts b/utils/activity-display.ts
index dcfc9eb87..1df1af2b6 100644
--- a/utils/activity-display.ts
+++ b/utils/activity-display.ts
@@ -110,6 +110,10 @@ const VAULT_ACTIVITY_EVENT_TYPES = {
'set_interest_rate_model',
'set_liquidation_cool_off_time',
'set_max_liquidation_discount',
+ 'set_oracle_config',
+ 'set_fallback_oracle',
+ 'set_resolved_vault',
+ 'set_oracle_governor',
],
earn: [
'deposit',
@@ -445,6 +449,19 @@ export const formatActivityEventLabel = (
): string => {
const sourceLabel = event.label?.trim()
if (sourceLabel) return sourceLabel
+ const normalizedLabel = {
+ set_oracle_config: 'Oracle route updated',
+ set_fallback_oracle: 'Fallback oracle updated',
+ set_resolved_vault: 'Resolved vault updated',
+ set_oracle_governor: 'Oracle governor updated',
+ set_liquidation_cool_off_time: 'Liquidation cool-off time updated',
+ set_is_allocator: 'Allocator status updated',
+ }[event.type]
+ if (normalizedLabel) return normalizedLabel
+ if (event.type.startsWith('set_')) {
+ const setting = titleizeActivityType(event.type.slice('set_'.length))
+ return `${applyActivityAcronyms(setting)} updated`
+ }
if (event.type === 'transfer') {
const direction = getActivityTransferDirection(event)
if (direction === 'sent') return 'Vault shares sent'
@@ -813,6 +830,7 @@ export interface ActivityChangeEntry {
field: string
label: string
value?: string
+ summary?: string
addresses?: ActivityChangeAddress[]
}
@@ -829,12 +847,18 @@ const VAULT_ADDRESS_FIELDS_BY_EVENT: Partial> = {
+ set_resolved_vault: ['asset'],
+}
+
const parseActivityInteger = (value: ActivityChangeValue): bigint | null => {
if (typeof value !== 'string' && typeof value !== 'number') return null
try {
@@ -951,6 +975,7 @@ const isZeroAddressValue = (value: ActivityChangeValue): boolean => {
/** Display order for change fields whose upstream order is unhelpful. */
const CHANGE_FIELD_PRIORITY: Partial> = {
+ set_fallback_oracle: ['fallback_oracle', 'router'],
set_ltv: [
'collateral',
'borrow_ltv',
@@ -959,6 +984,9 @@ const CHANGE_FIELD_PRIORITY: Partial {
const values = Array.isArray(value) ? value : [value]
if (!values.length || !values.every(item => typeof item === 'string' && isAddress(item))) return null
const isVaultAddress = VAULT_ADDRESS_FIELDS_BY_EVENT[event.type]?.includes(field) ?? false
+ const isTokenAddress = TOKEN_ADDRESS_FIELDS_BY_EVENT[event.type]?.includes(field) ?? false
+ const tokenLabel = (address: Address) =>
+ getTokenSymbol?.(address) ?? getSpecialAddressLabel(address)
return values.map((item) => {
const address = item as Address
- if (!isVaultAddress) return { address, linkKind: 'explorer' as const }
- const display = getVaultMetadata
- ? resolveActivityVaultDisplay(address, getVaultMetadata)
- : null
+ if (!isVaultAddress) {
+ const label = isTokenAddress ? tokenLabel(address) : undefined
+ return { address, linkKind: 'explorer' as const, ...(label ? { label } : {}) }
+ }
+ const metadata = getVaultMetadata?.(address)
+ // A vault the registry cannot resolve (e.g. a non-Euler ERC-4626 resolved
+ // vault) still has a token symbol — show that and link to the explorer
+ // instead of a dead internal vault page.
+ if (!metadata) {
+ const label = tokenLabel(address)
+ return { address, linkKind: 'explorer' as const, ...(label ? { label } : {}) }
+ }
+ const display = resolveActivityVaultDisplay(address, getVaultMetadata!)
return {
address,
linkKind: 'vault' as const,
label: display?.name ?? display?.addressLabel,
- vaultType: getVaultMetadata?.(address)?.vaultType ?? event.vaultType,
+ vaultType: metadata.vaultType ?? event.vaultType,
}
})
}
+const resolveOracleAssetPair = (
+ event: ActivityChangeEventSource,
+ getTokenSymbol: ActivityAddressLabelLookup | undefined,
+): ActivityChangeEntry | null => {
+ if (event.type !== 'set_oracle_config') return null
+ const asset0 = event.change?.fields.asset0
+ const asset1 = event.change?.fields.asset1
+ if (
+ typeof asset0 !== 'string'
+ || typeof asset1 !== 'string'
+ || !isAddress(asset0)
+ || !isAddress(asset1)
+ ) return null
+
+ const addresses = [asset0, asset1].map((value) => {
+ const address = value as Address
+ return {
+ address,
+ label: getTokenSymbol?.(address) ?? getSpecialAddressLabel(address) ?? shortenAddress(address),
+ linkKind: 'explorer' as const,
+ }
+ })
+ return {
+ field: 'asset_pair',
+ label: 'Asset pair',
+ summary: addresses.map(address => address.label).join(' / '),
+ addresses,
+ }
+}
+
export const getActivityChangeEntries = (
event: ActivityChangeEventSource,
getVaultMetadata?: ActivityVaultMetadataLookup,
-): ActivityChangeEntry[] => orderedActivityChangeFields(event).map(([field, value]) => {
+ getTokenSymbol?: ActivityAddressLabelLookup,
+): ActivityChangeEntry[] => {
+ const assetPair = resolveOracleAssetPair(event, getTokenSymbol)
+ const fields = orderedActivityChangeFields(event)
+ .filter(([field]) => !assetPair || (field !== 'asset0' && field !== 'asset1'))
+
+ const entries = fields.map(([field, value]): ActivityChangeEntry => {
// The zero address reads better as an explicit "None" than as a linked,
// copyable 0x0000…0000 (e.g. a renounced governor or cleared receiver).
- if (isZeroAddressValue(value)) {
- return { field, label: formatActivityChangeLabel(field), value: 'None' }
- }
- const addresses = resolveChangeAddresses(event, field, value, getVaultMetadata)
- if (addresses) return { field, label: formatActivityChangeLabel(field), addresses }
+ if (isZeroAddressValue(value)) {
+ return { field, label: formatActivityChangeLabel(field), value: 'None' }
+ }
+ const addresses = resolveChangeAddresses(event, field, value, getVaultMetadata, getTokenSymbol)
+ if (addresses) return { field, label: formatActivityChangeLabel(field), addresses }
- let formatted: string | null = null
- const vaultMetadata = event.vault ? getVaultMetadata?.(event.vault) : undefined
- if (event.type === 'set_caps' && (field === 'supply_cap' || field === 'borrow_cap')) {
- formatted = formatActivityCap(value, event.vault, getVaultMetadata)
- }
- else if (
- (event.type === 'set_cap' || event.type === 'submit_cap')
- && field === 'cap'
- ) {
- const cap = parseActivityInteger(value)
- formatted = event.vaultType === 'earn' && cap !== null && cap >= UINT136_MAX
- ? 'Unlimited'
- : formatActivityTokenAmount(value, vaultMetadata?.asset, true)
- }
- else if (
- event.type === 'set_supply_cap'
- && (field === 'cap' || field === 'supply_cap' || field === 'new_supply_cap')
- ) {
- formatted = formatActivityTokenAmount(value, vaultMetadata?.asset, true)
- }
- else if (
- (event.type === 'reallocate_supply' || event.type === 'reallocate_withdraw')
- && (field === 'supplied_assets' || field === 'withdrawn_assets')
- ) {
- formatted = formatActivityTokenAmount(value, vaultMetadata?.asset, true)
- }
- else if (event.type === 'set_ltv' && field.endsWith('_ltv')) {
- formatted = formatActivityBps(value)
- }
- else if (
- event.type === 'set_config_flags'
- && (field === 'config_flags' || field === 'new_config_flags')
- ) {
- formatted = formatActivityConfigFlags(value)
- }
- // EVK ConfigAmounts are scaled over 1e4 — 500 reads as 5%, 350 as 3.5%.
- else if (
- event.type === 'set_interest_fee'
- && ['fee', 'new_fee', 'interest_fee', 'new_interest_fee'].includes(field)
- ) {
- formatted = formatActivityBps(value)
- }
- else if (
- event.type === 'set_max_liquidation_discount'
- && ['discount', 'new_discount', 'max_liquidation_discount', 'new_max_liquidation_discount'].includes(field)
- ) {
- formatted = formatActivityBps(value)
- }
- else if (
- event.type === 'set_hook_config'
- && (field === 'hooked_ops' || field === 'new_hooked_ops')
- ) {
- formatted = formatActivityHookedOperations(value)
- }
- else if (field === 'target_timestamp') {
- formatted = formatActivityUnixTimestamp(value)
- }
- else if (field === 'ramp_duration' || field.endsWith('_timelock') || field.endsWith('_cool_off_time')) {
- formatted = formatActivityDuration(value)
- }
+ let formatted: string | null = null
+ const vaultMetadata = event.vault ? getVaultMetadata?.(event.vault) : undefined
+ if (event.type === 'set_caps' && (field === 'supply_cap' || field === 'borrow_cap')) {
+ formatted = formatActivityCap(value, event.vault, getVaultMetadata)
+ }
+ else if (
+ (event.type === 'set_cap' || event.type === 'submit_cap')
+ && field === 'cap'
+ ) {
+ const cap = parseActivityInteger(value)
+ formatted = event.vaultType === 'earn' && cap !== null && cap >= UINT136_MAX
+ ? 'Unlimited'
+ : formatActivityTokenAmount(value, vaultMetadata?.asset, true)
+ }
+ else if (
+ event.type === 'set_supply_cap'
+ && (field === 'cap' || field === 'supply_cap' || field === 'new_supply_cap')
+ ) {
+ formatted = formatActivityTokenAmount(value, vaultMetadata?.asset, true)
+ }
+ else if (
+ (event.type === 'reallocate_supply' || event.type === 'reallocate_withdraw')
+ && (field === 'supplied_assets' || field === 'withdrawn_assets')
+ ) {
+ formatted = formatActivityTokenAmount(value, vaultMetadata?.asset, true)
+ }
+ else if (event.type === 'set_ltv' && field.endsWith('_ltv')) {
+ formatted = formatActivityBps(value)
+ }
+ else if (
+ event.type === 'set_config_flags'
+ && (field === 'config_flags' || field === 'new_config_flags')
+ ) {
+ formatted = formatActivityConfigFlags(value)
+ }
+ // EVK ConfigAmounts are scaled over 1e4 — 500 reads as 5%, 350 as 3.5%.
+ else if (
+ event.type === 'set_interest_fee'
+ && ['fee', 'new_fee', 'interest_fee', 'new_interest_fee'].includes(field)
+ ) {
+ formatted = formatActivityBps(value)
+ }
+ else if (
+ event.type === 'set_max_liquidation_discount'
+ && ['discount', 'new_discount', 'max_liquidation_discount', 'new_max_liquidation_discount'].includes(field)
+ ) {
+ formatted = formatActivityBps(value)
+ }
+ else if (
+ event.type === 'set_hook_config'
+ && (field === 'hooked_ops' || field === 'new_hooked_ops')
+ ) {
+ formatted = formatActivityHookedOperations(value)
+ }
+ else if (field === 'target_timestamp') {
+ formatted = formatActivityUnixTimestamp(value)
+ }
+ else if (field === 'ramp_duration' || field.endsWith('_timelock') || field.endsWith('_cool_off_time')) {
+ formatted = formatActivityDuration(value)
+ }
- return {
- field,
- label: formatActivityChangeLabel(field),
- value: formatted ?? formatActivityChangeValue(value),
- }
-})
+ return {
+ field,
+ label: formatActivityChangeLabel(field),
+ value: formatted ?? formatActivityChangeValue(value),
+ }
+ })
+ return assetPair ? [assetPair, ...entries] : entries
+}
interface ActivityParticipantSource {
account?: Address