From 1fd59edacbea8dbfef6429962c03b3e5ce0f194c Mon Sep 17 00:00:00 2001 From: Seranged <80223622+Seranged@users.noreply.github.com> Date: Tue, 11 Aug 2026 12:42:47 +0100 Subject: [PATCH 1/4] fix: decode flow cap activity details --- .../entities/activity/ActivityEventRow.vue | 7 +- tests/utils/activity-display.test.ts | 46 ++++++++ utils/activity-display.ts | 104 +++++++++++++++++- 3 files changed, 153 insertions(+), 4 deletions(-) diff --git a/components/entities/activity/ActivityEventRow.vue b/components/entities/activity/ActivityEventRow.vue index b4c348ae4..6ce363ed6 100644 --- a/components/entities/activity/ActivityEventRow.vue +++ b/components/entities/activity/ActivityEventRow.vue @@ -208,6 +208,7 @@ const changes = computed(() => { valueTitle: entry.value, summary: entry.summary, addresses: entry.addresses, + addressDetails: entry.addressDetails, avatarAssets: entry.field === 'asset_pair' || (event.type === 'set_resolved_vault' && entry.field === 'asset') ? entry.addresses?.map(address => ({ @@ -468,7 +469,7 @@ const vaultDisplay = computed(() => {
{ :vault-type="address.vaultType" compact-vault /> + {{ detail.addressDetails[addressIndex] }}
diff --git a/tests/utils/activity-display.test.ts b/tests/utils/activity-display.test.ts index 7a8e19e79..6050b83dc 100644 --- a/tests/utils/activity-display.test.ts +++ b/tests/utils/activity-display.test.ts @@ -21,6 +21,7 @@ import { getActivityChangeEntries, getActivityEventIcon, getActivityLiquidationDisplayDetails, + getActivityResolvableVaultAddresses, getPortfolioActivityPositionParticipant, getActivityTransferDirection, getActivityTransactionGroupLabel, @@ -714,6 +715,51 @@ describe('activity display helpers', () => { { field: 'new_supply_cap', label: 'New supply cap', value: '155M USDC' }, ]) + const flowCapsConfig = JSON.stringify([ + { id: OTHER_VAULT, caps: { maxIn: '10000000000', maxOut: '2500000000' } }, + { id: SHARES, caps: { maxIn: '0', maxOut: '10000000000' } }, + ]) + expect(getActivityChangeEntries({ + type: 'set_flow_caps', + vault: VAULT, + vaultType: 'earn', + change: { fields: { config: flowCapsConfig } }, + }, getVaultMetadata)).toEqual([{ + field: 'config', + label: 'Strategies', + summary: '2 strategies', + addresses: [ + { + address: OTHER_VAULT, + label: 'Collateral vault', + linkKind: 'vault', + vaultType: 'evk', + }, + { address: SHARES, linkKind: 'explorer' }, + ], + addressDetails: [ + 'Max in 10K USDC · Max out 2.5K USDC', + 'Max in 0 USDC · Max out 10K USDC', + ], + }]) + expect(getActivityResolvableVaultAddresses({ + type: 'set_flow_caps', + vault: VAULT, + change: { fields: { config: flowCapsConfig } }, + })).toEqual([VAULT, OTHER_VAULT, SHARES]) + + const malformedFlowCapsConfig = '[{"id":"not-an-address"}]' + expect(getActivityChangeEntries({ + type: 'set_flow_caps', + vault: VAULT, + vaultType: 'earn', + change: { fields: { config: malformedFlowCapsConfig } }, + }, getVaultMetadata)).toEqual([{ + field: 'config', + label: 'Config', + value: malformedFlowCapsConfig, + }]) + expect(getActivityChangeEntries({ type: 'set_oracle_config', vault: VAULT, diff --git a/utils/activity-display.ts b/utils/activity-display.ts index 1df1af2b6..a31ca2053 100644 --- a/utils/activity-display.ts +++ b/utils/activity-display.ts @@ -9,7 +9,7 @@ import type { ActivityVaultType, LiquidationRecord, } from '@eulerxyz/euler-v2-sdk' -import { formatUnits, isAddress, maxUint256, zeroAddress, type Address } from 'viem' +import { formatUnits, getAddress, isAddress, maxUint256, zeroAddress, type Address } from 'viem' import { compactNumber, formatCompactUsdValue, formatSmartAmount, shortenAddress } from '~/utils/string-utils' import { CFG_DONT_SOCIALIZE_DEBT } from '~/entities/constants' import { decodeHookedOperationsMask, getHookedOperationMetas } from '~/utils/vault-hooks' @@ -76,6 +76,7 @@ const CATEGORY_LABELS: Record = { governance: 'Governance', } +const UINT128_MAX = 2n ** 128n - 1n const UINT136_MAX = 2n ** 136n - 1n const ACCOUNT_ACTIVITY_EVENT_TYPES = [ @@ -832,6 +833,7 @@ export interface ActivityChangeEntry { value?: string summary?: string addresses?: ActivityChangeAddress[] + addressDetails?: string[] } type ActivityChangeEventSource = Pick @@ -869,6 +871,56 @@ const parseActivityInteger = (value: ActivityChangeValue): bigint | null => { } } +interface ActivityFlowCapsConfig { + id: Address + maxIn: string + maxOut: string +} + +const isActivityRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null && !Array.isArray(value) + +const isActivityUint128 = (value: unknown): value is string => + typeof value === 'string' + && value.length <= UINT128_MAX.toString().length + && /^\d+$/.test(value) + && BigInt(value) <= UINT128_MAX + +/** + * The activity API serializes FlowCapsConfig[] into a string because the SDK + * change-value contract intentionally accepts only scalars and string arrays. + * Keep this parser event-specific and fail closed to the generic raw display. + */ +const parseActivityFlowCapsConfig = ( + value: ActivityChangeValue | undefined, +): ActivityFlowCapsConfig[] | null => { + if (typeof value !== 'string') return null + + try { + const parsed: unknown = JSON.parse(value) + if (!Array.isArray(parsed)) return null + + const configs: ActivityFlowCapsConfig[] = [] + for (const item of parsed) { + if ( + !isActivityRecord(item) + || typeof item.id !== 'string' + || !isAddress(item.id) + || !isActivityRecord(item.caps) + ) { + return null + } + const { maxIn, maxOut } = item.caps + if (!isActivityUint128(maxIn) || !isActivityUint128(maxOut)) return null + configs.push({ id: getAddress(item.id), maxIn, maxOut }) + } + return configs + } + catch { + return null + } +} + /** Decodes the EVK AmountCap uint16 encoding into underlying token units. */ export const decodeEvkAmountCap = (value: ActivityChangeValue): bigint | null => { const raw = parseActivityInteger(value) @@ -1094,14 +1146,51 @@ const resolveOracleAssetPair = ( } } +const resolveFlowCapsConfig = ( + event: ActivityChangeEventSource, + getVaultMetadata: ActivityVaultMetadataLookup | undefined, + getTokenSymbol: ActivityAddressLabelLookup | undefined, +): ActivityChangeEntry | null => { + if (event.type !== 'set_flow_caps') return null + const configs = parseActivityFlowCapsConfig(event.change?.fields.config) + if (configs === null) return null + if (configs.length === 0) { + return { field: 'config', label: 'Strategies', value: 'None' } + } + + const addresses = resolveChangeAddresses( + event, + 'strategy', + configs.map(config => config.id), + getVaultMetadata, + getTokenSymbol, + ) + if (!addresses) return null + + const asset = event.vault ? getVaultMetadata?.(event.vault)?.asset : undefined + const formatCap = (value: string) => + formatActivityTokenAmount(value, asset, true) ?? value + return { + field: 'config', + label: configs.length === 1 ? 'Strategy' : 'Strategies', + ...(configs.length > 1 ? { summary: `${configs.length} strategies` } : {}), + addresses, + addressDetails: configs.map(config => + `Max in ${formatCap(config.maxIn)} · Max out ${formatCap(config.maxOut)}`), + } +} + export const getActivityChangeEntries = ( event: ActivityChangeEventSource, getVaultMetadata?: ActivityVaultMetadataLookup, getTokenSymbol?: ActivityAddressLabelLookup, ): ActivityChangeEntry[] => { const assetPair = resolveOracleAssetPair(event, getTokenSymbol) + const flowCapsConfig = resolveFlowCapsConfig(event, getVaultMetadata, getTokenSymbol) const fields = orderedActivityChangeFields(event) - .filter(([field]) => !assetPair || (field !== 'asset0' && field !== 'asset1')) + .filter(([field]) => + (!assetPair || (field !== 'asset0' && field !== 'asset1')) + && (!flowCapsConfig || field !== 'config')) const entries = fields.map(([field, value]): ActivityChangeEntry => { // The zero address reads better as an explicit "None" than as a linked, @@ -1179,7 +1268,11 @@ export const getActivityChangeEntries = ( value: formatted ?? formatActivityChangeValue(value), } }) - return assetPair ? [assetPair, ...entries] : entries + return [ + ...(assetPair ? [assetPair] : []), + ...(flowCapsConfig ? [flowCapsConfig] : []), + ...entries, + ] } interface ActivityParticipantSource { @@ -1235,6 +1328,11 @@ export const getActivityResolvableVaultAddresses = ( if (typeof item === 'string' && isAddress(item)) addresses.add(item as Address) } } + if (event.type === 'set_flow_caps') { + for (const config of parseActivityFlowCapsConfig(event.change?.fields.config) ?? []) { + addresses.add(config.id) + } + } return [...addresses] } From 4e95c0b4be709c22a83d84a31053a74377be29ee Mon Sep 17 00:00:00 2001 From: Seranged <80223622+Seranged@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:23:38 +0100 Subject: [PATCH 2/4] fix: label flow caps as strategy caps --- tests/utils/activity-display.test.ts | 1 + utils/activity-display.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/utils/activity-display.test.ts b/tests/utils/activity-display.test.ts index 6050b83dc..50ef31550 100644 --- a/tests/utils/activity-display.test.ts +++ b/tests/utils/activity-display.test.ts @@ -202,6 +202,7 @@ describe('activity display helpers', () => { 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({ label: 'Flow caps updated', type: 'set_flow_caps' })).toBe('Strategy caps 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') diff --git a/utils/activity-display.ts b/utils/activity-display.ts index a31ca2053..dfcb9b7f8 100644 --- a/utils/activity-display.ts +++ b/utils/activity-display.ts @@ -448,6 +448,7 @@ export const getActivityTransferDirection = ( export const formatActivityEventLabel = ( event: ActivityEventLabelSource, ): string => { + if (event.type === 'set_flow_caps') return 'Strategy caps updated' const sourceLabel = event.label?.trim() if (sourceLabel) return sourceLabel const normalizedLabel = { From 8717c995d59571a09861b71caea30a132327c7bb Mon Sep 17 00:00:00 2001 From: Seranged <80223622+Seranged@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:41:29 +0100 Subject: [PATCH 3/4] fix: use public allocator flow cap terminology --- tests/utils/activity-display.test.ts | 2 +- utils/activity-display.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/utils/activity-display.test.ts b/tests/utils/activity-display.test.ts index 50ef31550..02f2e6f69 100644 --- a/tests/utils/activity-display.test.ts +++ b/tests/utils/activity-display.test.ts @@ -202,7 +202,7 @@ describe('activity display helpers', () => { 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({ label: 'Flow caps updated', type: 'set_flow_caps' })).toBe('Strategy caps updated') + expect(formatActivityEventLabel({ type: 'set_flow_caps' })).toBe('Flow caps 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') diff --git a/utils/activity-display.ts b/utils/activity-display.ts index dfcb9b7f8..a31ca2053 100644 --- a/utils/activity-display.ts +++ b/utils/activity-display.ts @@ -448,7 +448,6 @@ export const getActivityTransferDirection = ( export const formatActivityEventLabel = ( event: ActivityEventLabelSource, ): string => { - if (event.type === 'set_flow_caps') return 'Strategy caps updated' const sourceLabel = event.label?.trim() if (sourceLabel) return sourceLabel const normalizedLabel = { From c26bb5c623489956ac912d4fba190401c6a5e422 Mon Sep 17 00:00:00 2001 From: Seranged <80223622+Seranged@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:42:08 +0100 Subject: [PATCH 4/4] fix: clarify public allocator limit activity --- tests/utils/activity-display.test.ts | 2 +- utils/activity-display.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/utils/activity-display.test.ts b/tests/utils/activity-display.test.ts index 02f2e6f69..bef5c755d 100644 --- a/tests/utils/activity-display.test.ts +++ b/tests/utils/activity-display.test.ts @@ -202,7 +202,7 @@ describe('activity display helpers', () => { 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_flow_caps' })).toBe('Flow caps updated') + expect(formatActivityEventLabel({ label: 'Flow caps updated', type: 'set_flow_caps' })).toBe('Public allocator limits 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') diff --git a/utils/activity-display.ts b/utils/activity-display.ts index a31ca2053..e5e27fc3d 100644 --- a/utils/activity-display.ts +++ b/utils/activity-display.ts @@ -448,6 +448,7 @@ export const getActivityTransferDirection = ( export const formatActivityEventLabel = ( event: ActivityEventLabelSource, ): string => { + if (event.type === 'set_flow_caps') return 'Public allocator limits updated' const sourceLabel = event.label?.trim() if (sourceLabel) return sourceLabel const normalizedLabel = {