@swarmclawai/swarmclaw 1.2.8 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +39 -6
  2. package/package.json +2 -2
  3. package/src/app/agents/[id]/page.tsx +1 -18
  4. package/src/app/api/activity/route.ts +9 -23
  5. package/src/app/api/agents/route.ts +17 -1
  6. package/src/app/api/agents/thread-route.test.ts +0 -1
  7. package/src/app/api/approvals/route.test.ts +6 -22
  8. package/src/app/api/approvals/route.ts +13 -5
  9. package/src/app/api/connectors/route.ts +2 -2
  10. package/src/app/api/credentials/[id]/route.ts +2 -0
  11. package/src/app/api/credentials/route.ts +4 -1
  12. package/src/app/api/goals/[id]/route.ts +28 -0
  13. package/src/app/api/goals/route.ts +33 -0
  14. package/src/app/api/portability/export/route.ts +8 -0
  15. package/src/app/api/portability/import/route.test.ts +80 -0
  16. package/src/app/api/portability/import/route.ts +28 -0
  17. package/src/app/api/protocols/templates/[id]/route.ts +2 -1
  18. package/src/app/api/protocols/templates/route.ts +2 -1
  19. package/src/app/api/settings/route.ts +13 -2
  20. package/src/app/api/wallets/[id]/route.ts +15 -157
  21. package/src/app/api/wallets/generate/route.ts +22 -0
  22. package/src/app/api/wallets/route.test.ts +147 -0
  23. package/src/app/api/wallets/route.ts +13 -95
  24. package/src/app/autonomy/page.tsx +2 -57
  25. package/src/app/home/page.tsx +3 -0
  26. package/src/app/protocols/page.tsx +2 -21
  27. package/src/app/settings/page.tsx +0 -9
  28. package/src/app/wallets/page.tsx +105 -5
  29. package/src/cli/index.js +32 -33
  30. package/src/cli/spec.js +26 -27
  31. package/src/components/agents/agent-sheet.tsx +2 -40
  32. package/src/components/agents/inspector-panel.tsx +0 -83
  33. package/src/components/chat/chat-card.tsx +0 -31
  34. package/src/components/chat/message-bubble.tsx +1 -108
  35. package/src/components/connectors/connector-sheet.tsx +25 -1
  36. package/src/components/layout/sidebar-rail.tsx +6 -10
  37. package/src/components/projects/project-detail.tsx +3 -35
  38. package/src/components/projects/tabs/overview-tab.tsx +3 -59
  39. package/src/components/projects/tabs/work-tab.tsx +7 -77
  40. package/src/components/protocols/structured-session-launcher.tsx +1 -22
  41. package/src/components/shared/connector-platform-icon.tsx +1 -0
  42. package/src/components/tasks/task-card.tsx +4 -34
  43. package/src/components/tasks/task-sheet.tsx +6 -36
  44. package/src/components/wallets/wallet-list.tsx +150 -0
  45. package/src/lib/app/navigation.test.ts +0 -13
  46. package/src/lib/app/navigation.ts +2 -7
  47. package/src/lib/app/view-constants.ts +14 -19
  48. package/src/lib/server/activity/activity-log.ts +16 -1
  49. package/src/lib/server/agents/agent-service.ts +24 -11
  50. package/src/lib/server/agents/agent-thread-session.ts +0 -1
  51. package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
  52. package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
  53. package/src/lib/server/agents/delegation-jobs.ts +0 -25
  54. package/src/lib/server/agents/main-agent-loop.ts +1 -49
  55. package/src/lib/server/agents/subagent-runtime.ts +0 -1
  56. package/src/lib/server/approval-match.ts +14 -85
  57. package/src/lib/server/approvals/approval-hooks.ts +81 -0
  58. package/src/lib/server/approvals.test.ts +6 -6
  59. package/src/lib/server/approvals.ts +11 -6
  60. package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
  61. package/src/lib/server/builtin-extensions.ts +0 -2
  62. package/src/lib/server/capability-router.test.ts +0 -2
  63. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +14 -14
  64. package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
  65. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -2
  66. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
  67. package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
  68. package/src/lib/server/chat-execution/chat-turn-preparation.ts +2 -22
  69. package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
  70. package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
  71. package/src/lib/server/chat-execution/message-classifier.ts +1 -16
  72. package/src/lib/server/chat-execution/prompt-builder.test.ts +0 -1
  73. package/src/lib/server/chat-execution/prompt-builder.ts +0 -30
  74. package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
  75. package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
  76. package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
  77. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +8 -123
  78. package/src/lib/server/chat-execution/stream-agent-chat.ts +1 -5
  79. package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
  80. package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
  81. package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
  82. package/src/lib/server/chats/chat-session-service.ts +3 -5
  83. package/src/lib/server/connectors/connector-inbound.ts +0 -1
  84. package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
  85. package/src/lib/server/connectors/connector-service.ts +39 -9
  86. package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
  87. package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
  88. package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
  89. package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
  90. package/src/lib/server/connectors/swarmdock-tasks.ts +127 -0
  91. package/src/lib/server/connectors/swarmdock.ts +285 -0
  92. package/src/lib/server/execution-brief.test.ts +2 -25
  93. package/src/lib/server/execution-brief.ts +30 -35
  94. package/src/lib/server/execution-engine/task-attempt.ts +0 -1
  95. package/src/lib/server/goals/goal-repository.ts +19 -0
  96. package/src/lib/server/goals/goal-service.ts +143 -0
  97. package/src/lib/server/persistence/storage-context.ts +0 -5
  98. package/src/lib/server/portability/export.ts +109 -0
  99. package/src/lib/server/portability/import.ts +159 -0
  100. package/src/lib/server/protocols/protocol-normalization.ts +0 -4
  101. package/src/lib/server/protocols/protocol-queries.ts +0 -6
  102. package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
  103. package/src/lib/server/protocols/protocol-service.ts +0 -1
  104. package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
  105. package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
  106. package/src/lib/server/protocols/protocol-swarm.ts +0 -2
  107. package/src/lib/server/protocols/protocol-types.ts +0 -2
  108. package/src/lib/server/provider-health.ts +0 -9
  109. package/src/lib/server/runtime/daemon-state/core.ts +0 -9
  110. package/src/lib/server/runtime/daemon-state.test.ts +0 -35
  111. package/src/lib/server/runtime/heartbeat-service.ts +3 -23
  112. package/src/lib/server/runtime/queue/core.ts +11 -33
  113. package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
  114. package/src/lib/server/runtime/scheduler.ts +0 -13
  115. package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
  116. package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
  117. package/src/lib/server/runtime/session-run-manager/queries.ts +0 -1
  118. package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
  119. package/src/lib/server/runtime/session-run-manager.test.ts +0 -28
  120. package/src/lib/server/session-tools/crud.ts +0 -14
  121. package/src/lib/server/session-tools/delegate.ts +0 -4
  122. package/src/lib/server/session-tools/index.ts +0 -4
  123. package/src/lib/server/session-tools/team-context.ts +0 -3
  124. package/src/lib/server/storage-normalization.ts +13 -0
  125. package/src/lib/server/storage.ts +75 -45
  126. package/src/lib/server/tasks/task-checkout.ts +59 -0
  127. package/src/lib/server/tasks/task-lifecycle.ts +2 -0
  128. package/src/lib/server/tasks/task-route-service.ts +4 -26
  129. package/src/lib/server/tasks/task-service.ts +0 -7
  130. package/src/lib/server/tool-aliases.ts +0 -1
  131. package/src/lib/server/tool-capability-policy-advanced.test.ts +4 -4
  132. package/src/lib/server/tool-capability-policy.ts +0 -2
  133. package/src/lib/server/tool-planning.ts +0 -12
  134. package/src/lib/server/universal-tool-access.ts +0 -1
  135. package/src/lib/server/usage/cost-rollup.ts +124 -0
  136. package/src/lib/server/usage/usage-repository.ts +6 -0
  137. package/src/lib/server/wallets/wallet-crypto.ts +33 -0
  138. package/src/lib/server/wallets/wallet-repository.ts +24 -0
  139. package/src/lib/server/wallets/wallet-service.ts +119 -0
  140. package/src/lib/server/working-state/extraction.ts +8 -42
  141. package/src/lib/server/working-state/normalization.ts +10 -103
  142. package/src/lib/server/working-state/service.ts +12 -21
  143. package/src/lib/strip-internal-metadata.test.ts +1 -1
  144. package/src/lib/strip-internal-metadata.ts +1 -1
  145. package/src/lib/tool-definitions.ts +0 -1
  146. package/src/lib/validation/schemas.ts +36 -32
  147. package/src/lib/validation/server-schemas.ts +35 -0
  148. package/src/stores/slices/data-slice.ts +5 -1
  149. package/src/stores/slices/ui-slice.ts +0 -4
  150. package/src/types/agent.ts +10 -84
  151. package/src/types/app-settings.ts +6 -2
  152. package/src/types/approval.ts +3 -2
  153. package/src/types/connector.ts +1 -0
  154. package/src/types/goal.ts +30 -0
  155. package/src/types/index.ts +2 -1
  156. package/src/types/message.ts +0 -1
  157. package/src/types/misc.ts +2 -4
  158. package/src/types/protocol.ts +0 -2
  159. package/src/types/run.ts +0 -3
  160. package/src/types/session.ts +1 -51
  161. package/src/types/swarmdock.ts +29 -0
  162. package/src/types/task.ts +9 -3
  163. package/src/types/working-state.ts +2 -9
  164. package/src/views/settings/section-runtime-loop.tsx +0 -14
  165. package/src/app/api/canvas/[sessionId]/route.ts +0 -35
  166. package/src/app/api/missions/[id]/actions/route.ts +0 -31
  167. package/src/app/api/missions/[id]/events/route.ts +0 -14
  168. package/src/app/api/missions/[id]/route.ts +0 -10
  169. package/src/app/api/missions/route.test.ts +0 -244
  170. package/src/app/api/missions/route.ts +0 -57
  171. package/src/app/api/wallets/[id]/approve/route.ts +0 -79
  172. package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
  173. package/src/app/api/wallets/[id]/send/route.ts +0 -113
  174. package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
  175. package/src/app/missions/[id]/page.tsx +0 -3
  176. package/src/app/missions/page.tsx +0 -685
  177. package/src/components/canvas/canvas-panel.tsx +0 -267
  178. package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
  179. package/src/components/wallets/wallet-panel.tsx +0 -1010
  180. package/src/components/wallets/wallet-section.tsx +0 -260
  181. package/src/features/missions/queries.ts +0 -23
  182. package/src/lib/canvas-content.test.ts +0 -360
  183. package/src/lib/canvas-content.ts +0 -198
  184. package/src/lib/server/canvas-content.test.ts +0 -32
  185. package/src/lib/server/canvas-content.ts +0 -6
  186. package/src/lib/server/ethereum.ts +0 -591
  187. package/src/lib/server/evm-swap.ts +0 -476
  188. package/src/lib/server/missions/mission-intent.test.ts +0 -63
  189. package/src/lib/server/missions/mission-intent.ts +0 -569
  190. package/src/lib/server/missions/mission-repository.ts +0 -74
  191. package/src/lib/server/missions/mission-service/actions.ts +0 -6
  192. package/src/lib/server/missions/mission-service/bindings.ts +0 -9
  193. package/src/lib/server/missions/mission-service/context.ts +0 -4
  194. package/src/lib/server/missions/mission-service/core.ts +0 -2271
  195. package/src/lib/server/missions/mission-service/queries.ts +0 -12
  196. package/src/lib/server/missions/mission-service/recovery.ts +0 -5
  197. package/src/lib/server/missions/mission-service/ticks.ts +0 -9
  198. package/src/lib/server/missions/mission-service.test.ts +0 -888
  199. package/src/lib/server/missions/mission-service.ts +0 -6
  200. package/src/lib/server/session-tools/canvas.ts +0 -105
  201. package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
  202. package/src/lib/server/session-tools/wallet.ts +0 -1287
  203. package/src/lib/server/solana.ts +0 -327
  204. package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
  205. package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
  206. package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
  207. package/src/lib/server/wallet/wallet-service.test.ts +0 -81
  208. package/src/lib/server/wallet/wallet-service.ts +0 -225
  209. package/src/lib/wallet/wallet-transactions.test.ts +0 -75
  210. package/src/lib/wallet/wallet-transactions.ts +0 -43
  211. package/src/lib/wallet/wallet.test.ts +0 -333
  212. package/src/lib/wallet/wallet.ts +0 -183
  213. package/src/types/mission.ts +0 -185
  214. package/src/views/settings/section-wallets.tsx +0 -35
@@ -1,772 +0,0 @@
1
- import { Contract, Interface, JsonRpcProvider, getAddress, zeroPadValue } from 'ethers'
2
- import { PublicKey } from '@solana/web3.js'
3
-
4
- import { formatAtomicAmount, getWalletAssetSymbol } from '@/lib/wallet/wallet'
5
- import type { AgentWallet, WalletAssetBalance, WalletPortfolioSummary } from '@/types'
6
-
7
- import { getProvider as getEthereumProvider } from '@/lib/server/ethereum'
8
- import { getConnection as getSolanaConnection } from '@/lib/server/solana'
9
-
10
- const TOKEN_PROGRAM_IDS = [
11
- new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'),
12
- new PublicKey('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'),
13
- ] as const
14
- const METAPLEX_METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s')
15
-
16
- const SOLSCAN_ACCOUNT_BASE = 'https://solscan.io/account/'
17
- const SOLSCAN_TOKEN_BASE = 'https://solscan.io/token/'
18
- const ERC20_TRANSFER_IFACE = new Interface([
19
- 'event Transfer(address indexed from, address indexed to, uint256 value)',
20
- ])
21
- const ERC20_TRANSFER_TOPIC = ERC20_TRANSFER_IFACE.getEvent('Transfer')?.topicHash || ''
22
- const ERC20_SYMBOL_BYTES32_IFACE = new Interface(['function symbol() view returns (bytes32)'])
23
- const ERC20_NAME_BYTES32_IFACE = new Interface(['function name() view returns (bytes32)'])
24
- const ERC20_ABI = [
25
- 'function balanceOf(address owner) view returns (uint256)',
26
- 'function decimals() view returns (uint8)',
27
- 'function symbol() view returns (string)',
28
- 'function name() view returns (string)',
29
- ] as const
30
- const PORTFOLIO_CACHE_TTL_MS = 20_000
31
- const SOLANA_RPC_MIN_INTERVAL_MS = 500
32
- let lastSolanaRpcCall = 0
33
- const EVM_CONTRACT_CACHE_TTL_MS = 10 * 60 * 1000
34
- const SOLANA_METADATA_BATCH_SIZE = 100
35
- const TOKEN_METADATA_NAME_OFFSET = 1 + 32 + 32
36
- const TOKEN_METADATA_NAME_LENGTH = 32
37
- const TOKEN_METADATA_SYMBOL_OFFSET = TOKEN_METADATA_NAME_OFFSET + TOKEN_METADATA_NAME_LENGTH
38
- const TOKEN_METADATA_SYMBOL_LENGTH = 10
39
-
40
- interface WalletPortfolioCacheEntry {
41
- expiresAt: number
42
- portfolio: WalletPortfolio
43
- }
44
-
45
- interface EvmContractDiscoveryCacheEntry {
46
- expiresAt: number
47
- walletCreatedAt: number
48
- scannedToBlock: number
49
- contractAddresses: string[]
50
- }
51
-
52
- interface EvmNetworkConfig {
53
- id: string
54
- label: string
55
- rpcUrl: string
56
- addressExplorerBaseUrl: string
57
- tokenExplorerBaseUrl: string
58
- avgBlockMs: number
59
- maxDiscoveryBlocks: number
60
- maxLogRange?: number
61
- knownTokens?: KnownEvmTokenConfig[]
62
- }
63
-
64
- interface KnownEvmTokenConfig {
65
- address: string
66
- symbol: string
67
- name: string
68
- decimals: number
69
- }
70
-
71
- export interface WalletPortfolio {
72
- balanceAtomic: string
73
- balanceFormatted: string
74
- balanceSymbol: string
75
- balanceDisplay: string
76
- balanceLamports?: number
77
- balanceSol?: number
78
- assets: WalletAssetBalance[]
79
- summary: WalletPortfolioSummary
80
- }
81
-
82
- export interface GetWalletPortfolioOptions {
83
- timeoutMs?: number
84
- allowStale?: boolean
85
- }
86
-
87
- const PORTFOLIO_CACHE_MAX = 200
88
- const portfolioCache = new Map<string, WalletPortfolioCacheEntry>()
89
- const evmContractDiscoveryCache = new Map<string, EvmContractDiscoveryCacheEntry>()
90
-
91
- function pruneExpiredPortfolioCaches(): void {
92
- const now = Date.now()
93
- for (const [key, entry] of portfolioCache) {
94
- if (entry.expiresAt <= now) portfolioCache.delete(key)
95
- }
96
- for (const [key, entry] of evmContractDiscoveryCache) {
97
- if (entry.expiresAt <= now) evmContractDiscoveryCache.delete(key)
98
- }
99
- // Hard cap as safety net
100
- if (portfolioCache.size > PORTFOLIO_CACHE_MAX) {
101
- const excess = portfolioCache.size - PORTFOLIO_CACHE_MAX
102
- const iter = portfolioCache.keys()
103
- for (let i = 0; i < excess; i++) {
104
- const k = iter.next().value
105
- if (k !== undefined) portfolioCache.delete(k)
106
- }
107
- }
108
- if (evmContractDiscoveryCache.size > PORTFOLIO_CACHE_MAX) {
109
- const excess = evmContractDiscoveryCache.size - PORTFOLIO_CACHE_MAX
110
- const iter = evmContractDiscoveryCache.keys()
111
- for (let i = 0; i < excess; i++) {
112
- const k = iter.next().value
113
- if (k !== undefined) evmContractDiscoveryCache.delete(k)
114
- }
115
- }
116
- }
117
-
118
- const KNOWN_SOLANA_TOKENS: Record<string, { symbol: string; name: string }> = {
119
- EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: { symbol: 'USDC', name: 'USD Coin' },
120
- }
121
-
122
- const KNOWN_EVM_TOKENS: Record<string, KnownEvmTokenConfig[]> = {
123
- ethereum: [
124
- {
125
- address: getAddress('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'),
126
- symbol: 'USDC',
127
- name: 'USD Coin',
128
- decimals: 6,
129
- },
130
- ],
131
- arbitrum: [
132
- {
133
- address: getAddress('0xaf88d065e77c8cC2239327C5EDb3A432268e5831'),
134
- symbol: 'USDC',
135
- name: 'USD Coin',
136
- decimals: 6,
137
- },
138
- ],
139
- base: [
140
- {
141
- address: getAddress('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'),
142
- symbol: 'USDC',
143
- name: 'USD Coin',
144
- decimals: 6,
145
- },
146
- ],
147
- }
148
-
149
- function getEnabledEvmNetworks(): EvmNetworkConfig[] {
150
- return [
151
- {
152
- id: 'ethereum',
153
- label: 'Ethereum',
154
- rpcUrl: process.env.ETHEREUM_RPC_URL || process.env.EVM_RPC_URL || 'https://ethereum-rpc.publicnode.com',
155
- addressExplorerBaseUrl: 'https://etherscan.io/address/',
156
- tokenExplorerBaseUrl: 'https://etherscan.io/token/',
157
- avgBlockMs: 12_000,
158
- maxDiscoveryBlocks: 300_000,
159
- maxLogRange: 50_000,
160
- knownTokens: KNOWN_EVM_TOKENS.ethereum,
161
- },
162
- {
163
- id: 'arbitrum',
164
- label: 'Arbitrum',
165
- rpcUrl: process.env.ARBITRUM_RPC_URL || 'https://arbitrum-one-rpc.publicnode.com',
166
- addressExplorerBaseUrl: 'https://arbiscan.io/address/',
167
- tokenExplorerBaseUrl: 'https://arbiscan.io/token/',
168
- avgBlockMs: 250,
169
- maxDiscoveryBlocks: 2_000_000,
170
- maxLogRange: 50_000,
171
- knownTokens: KNOWN_EVM_TOKENS.arbitrum,
172
- },
173
- {
174
- id: 'base',
175
- label: 'Base',
176
- rpcUrl: process.env.BASE_RPC_URL || 'https://base-rpc.publicnode.com',
177
- addressExplorerBaseUrl: 'https://basescan.org/address/',
178
- tokenExplorerBaseUrl: 'https://basescan.org/token/',
179
- avgBlockMs: 2_000,
180
- maxDiscoveryBlocks: 800_000,
181
- maxLogRange: 50_000,
182
- knownTokens: KNOWN_EVM_TOKENS.base,
183
- },
184
- ].filter((network) => Boolean(network.rpcUrl))
185
- }
186
-
187
- export function getKnownEvmTokenContracts(networkId: string): string[] {
188
- return (KNOWN_EVM_TOKENS[networkId] || []).map((token) => token.address)
189
- }
190
-
191
- export function estimateDiscoveryStartBlock(input: {
192
- latestBlock: number
193
- walletCreatedAt: number
194
- avgBlockMs: number
195
- maxDiscoveryBlocks: number
196
- now?: number
197
- safetyBlocks?: number
198
- }): number {
199
- const now = input.now ?? Date.now()
200
- const safetyBlocks = input.safetyBlocks ?? 5_000
201
- const ageMs = Math.max(0, now - input.walletCreatedAt)
202
- const estimatedBlocks = Math.min(
203
- input.maxDiscoveryBlocks,
204
- Math.max(safetyBlocks, Math.ceil(ageMs / input.avgBlockMs) + safetyBlocks),
205
- )
206
- return Math.max(0, input.latestBlock - estimatedBlocks)
207
- }
208
-
209
- export function buildLogDiscoveryRanges(
210
- fromBlock: number,
211
- toBlock: number,
212
- maxLogRange?: number,
213
- ): Array<{ fromBlock: number; toBlock: number }> {
214
- if (toBlock < fromBlock) return []
215
- if (!maxLogRange || maxLogRange < 1 || toBlock - fromBlock + 1 <= maxLogRange) {
216
- return [{ fromBlock, toBlock }]
217
- }
218
-
219
- const ranges: Array<{ fromBlock: number; toBlock: number }> = []
220
- for (let start = fromBlock; start <= toBlock; start += maxLogRange) {
221
- ranges.push({
222
- fromBlock: start,
223
- toBlock: Math.min(toBlock, start + maxLogRange - 1),
224
- })
225
- }
226
- return ranges
227
- }
228
-
229
- function shortId(value: string): string {
230
- return value.length <= 10 ? value : `${value.slice(0, 4)}...${value.slice(-4)}`
231
- }
232
-
233
- function readFixedUtf8String(data: Uint8Array, offset: number, length: number): string {
234
- if (data.length < offset + length) return ''
235
- return Buffer.from(data.subarray(offset, offset + length))
236
- .toString('utf8')
237
- .replace(/\0/g, '')
238
- .trim()
239
- }
240
-
241
- export function parseMetaplexMetadataFields(data: Uint8Array): { name?: string; symbol?: string } | null {
242
- const name = readFixedUtf8String(data, TOKEN_METADATA_NAME_OFFSET, TOKEN_METADATA_NAME_LENGTH)
243
- const symbol = readFixedUtf8String(data, TOKEN_METADATA_SYMBOL_OFFSET, TOKEN_METADATA_SYMBOL_LENGTH)
244
- if (!name && !symbol) return null
245
- return {
246
- name: name || undefined,
247
- symbol: symbol || undefined,
248
- }
249
- }
250
-
251
- function getSolanaMetadataPda(mint: string): PublicKey | null {
252
- try {
253
- const mintKey = new PublicKey(mint)
254
- return PublicKey.findProgramAddressSync(
255
- [
256
- Buffer.from('metadata'),
257
- METAPLEX_METADATA_PROGRAM_ID.toBuffer(),
258
- mintKey.toBuffer(),
259
- ],
260
- METAPLEX_METADATA_PROGRAM_ID,
261
- )[0]
262
- } catch {
263
- return null
264
- }
265
- }
266
-
267
- async function resolveSolanaMetadata(mints: string[]): Promise<Map<string, { name?: string; symbol?: string }>> {
268
- const result = new Map<string, { name?: string; symbol?: string }>()
269
- if (mints.length === 0) return result
270
-
271
- const connection = getSolanaConnection()
272
- const entries = mints
273
- .map((mint) => ({ mint, pda: getSolanaMetadataPda(mint) }))
274
- .filter((entry): entry is { mint: string; pda: PublicKey } => Boolean(entry.pda))
275
-
276
- for (let index = 0; index < entries.length; index += SOLANA_METADATA_BATCH_SIZE) {
277
- const batch = entries.slice(index, index + SOLANA_METADATA_BATCH_SIZE)
278
- try {
279
- const accounts = await rateLimitedSolanaCall(() => connection.getMultipleAccountsInfo(batch.map((entry) => entry.pda)))
280
- for (let accountIndex = 0; accountIndex < batch.length; accountIndex += 1) {
281
- const account = accounts[accountIndex]
282
- if (!account?.data) continue
283
- const parsed = parseMetaplexMetadataFields(account.data)
284
- if (!parsed) continue
285
- result.set(batch[accountIndex].mint, parsed)
286
- }
287
- } catch {
288
- continue
289
- }
290
- }
291
-
292
- return result
293
- }
294
-
295
- function normalizeAssetDisplay(
296
- symbol: string,
297
- balanceAtomic: string,
298
- decimals: number,
299
- opts?: { minFractionDigits?: number; maxFractionDigits?: number },
300
- ): { balanceFormatted: string; balanceDisplay: string } {
301
- const balanceFormatted = formatAtomicAmount(balanceAtomic, decimals, {
302
- minFractionDigits: opts?.minFractionDigits ?? 0,
303
- maxFractionDigits: opts?.maxFractionDigits ?? 6,
304
- })
305
- return {
306
- balanceFormatted,
307
- balanceDisplay: `${balanceFormatted} ${symbol}`,
308
- }
309
- }
310
-
311
- function buildPortfolioSummary(assets: WalletAssetBalance[]): WalletPortfolioSummary {
312
- const nonZeroAssets = assets.filter((asset) => BigInt(asset.balanceAtomic) > BigInt(0))
313
- return {
314
- totalAssets: assets.length,
315
- nonZeroAssets: nonZeroAssets.length,
316
- tokenAssets: assets.filter((asset) => !asset.isNative && BigInt(asset.balanceAtomic) > BigInt(0)).length,
317
- networkCount: new Set(nonZeroAssets.map((asset) => asset.networkId)).size,
318
- }
319
- }
320
-
321
- function sortAssets(assets: WalletAssetBalance[]): WalletAssetBalance[] {
322
- return [...assets].sort((left, right) => {
323
- const leftNonZero = BigInt(left.balanceAtomic) > BigInt(0)
324
- const rightNonZero = BigInt(right.balanceAtomic) > BigInt(0)
325
- if (leftNonZero !== rightNonZero) return leftNonZero ? -1 : 1
326
- if (left.isNative !== right.isNative) return left.isNative ? -1 : 1
327
- const networkCompare = left.networkLabel.localeCompare(right.networkLabel)
328
- if (networkCompare !== 0) return networkCompare
329
- return left.symbol.localeCompare(right.symbol)
330
- })
331
- }
332
-
333
- function buildPortfolioCacheKey(wallet: Pick<AgentWallet, 'id' | 'updatedAt'>): string {
334
- return `${wallet.id}:${wallet.updatedAt}`
335
- }
336
-
337
- function getCurrentCachedPortfolioEntry(wallet: Pick<AgentWallet, 'id' | 'updatedAt'>): WalletPortfolioCacheEntry | null {
338
- return portfolioCache.get(buildPortfolioCacheKey(wallet)) || null
339
- }
340
-
341
- function getLatestCachedPortfolioEntry(walletId: string): WalletPortfolioCacheEntry | null {
342
- let latest: WalletPortfolioCacheEntry | null = null
343
- for (const [key, entry] of portfolioCache.entries()) {
344
- if (!key.startsWith(`${walletId}:`)) continue
345
- if (!latest || entry.expiresAt > latest.expiresAt) latest = entry
346
- }
347
- return latest
348
- }
349
-
350
- export function getCachedWalletPortfolio(wallet: Pick<AgentWallet, 'id' | 'updatedAt'>): WalletPortfolio | null {
351
- return getCurrentCachedPortfolioEntry(wallet)?.portfolio
352
- || getLatestCachedPortfolioEntry(wallet.id)?.portfolio
353
- || null
354
- }
355
-
356
- async function withWalletPortfolioTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
357
- let timer: ReturnType<typeof setTimeout> | null = null
358
- try {
359
- return await Promise.race([
360
- promise,
361
- new Promise<never>((_, reject) => {
362
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)
363
- }),
364
- ])
365
- } finally {
366
- if (timer) clearTimeout(timer)
367
- }
368
- }
369
-
370
- export function buildEmptyWalletPortfolio(wallet: Pick<AgentWallet, 'chain' | 'publicKey'>): WalletPortfolio {
371
- const balanceSymbol = wallet.chain === 'ethereum' ? 'ETH' : 'SOL'
372
- const balanceDisplay = `0.0000 ${balanceSymbol}`
373
- return {
374
- balanceAtomic: '0',
375
- balanceFormatted: '0.0000',
376
- balanceDisplay,
377
- balanceSymbol,
378
- assets: [],
379
- summary: { totalAssets: 0, nonZeroAssets: 0, tokenAssets: 0, networkCount: 0 },
380
- }
381
- }
382
-
383
- export async function resolveWalletPortfolioWithTimeout<T>(
384
- params: {
385
- load: () => Promise<T>
386
- timeoutMs?: number
387
- stale?: T | null
388
- label: string
389
- },
390
- ): Promise<T> {
391
- try {
392
- if (typeof params.timeoutMs === 'number' && params.timeoutMs > 0) {
393
- return await withWalletPortfolioTimeout(params.load(), params.timeoutMs, params.label)
394
- }
395
- return await params.load()
396
- } catch (err) {
397
- if (params.stale != null) return params.stale
398
- throw err
399
- }
400
- }
401
-
402
- async function rateLimitedSolanaCall<T>(fn: () => Promise<T>): Promise<T> {
403
- const now = Date.now()
404
- const elapsed = now - lastSolanaRpcCall
405
- if (elapsed < SOLANA_RPC_MIN_INTERVAL_MS) {
406
- await new Promise((resolve) => setTimeout(resolve, SOLANA_RPC_MIN_INTERVAL_MS - elapsed))
407
- }
408
- lastSolanaRpcCall = Date.now()
409
- return fn()
410
- }
411
-
412
- async function fetchSolanaAssets(wallet: AgentWallet): Promise<WalletPortfolio> {
413
- const connection = getSolanaConnection()
414
- const publicKey = new PublicKey(wallet.publicKey)
415
- const nativeBalanceAtomic = String(await rateLimitedSolanaCall(() => connection.getBalance(publicKey)))
416
- const nativeDisplay = normalizeAssetDisplay('SOL', nativeBalanceAtomic, 9, { minFractionDigits: 4 })
417
- const assets: WalletAssetBalance[] = [{
418
- id: `solana:mainnet:native`,
419
- chain: 'solana',
420
- networkId: 'solana-mainnet',
421
- networkLabel: 'Solana',
422
- symbol: 'SOL',
423
- name: 'Solana',
424
- decimals: 9,
425
- balanceAtomic: nativeBalanceAtomic,
426
- ...nativeDisplay,
427
- isNative: true,
428
- explorerUrl: `${SOLSCAN_ACCOUNT_BASE}${wallet.publicKey}`,
429
- }]
430
-
431
- const tokenAccounts = []
432
- for (const programId of TOKEN_PROGRAM_IDS) {
433
- tokenAccounts.push(await rateLimitedSolanaCall(() => connection.getParsedTokenAccountsByOwner(publicKey, { programId })))
434
- }
435
-
436
- const tokensByMint = new Map<string, WalletAssetBalance>()
437
- for (const result of tokenAccounts) {
438
- for (const account of result.value) {
439
- const parsed = (account.account.data as { parsed?: { info?: Record<string, unknown> } }).parsed
440
- const info = parsed?.info || {}
441
- const mint = typeof info.mint === 'string' ? info.mint : ''
442
- const tokenAmount = info.tokenAmount as { amount?: string; decimals?: number } | undefined
443
- const amountAtomic = String(tokenAmount?.amount || '0')
444
- if (!mint || BigInt(amountAtomic) <= BigInt(0)) continue
445
- const decimals = typeof tokenAmount?.decimals === 'number' ? tokenAmount.decimals : 0
446
- const known = KNOWN_SOLANA_TOKENS[mint]
447
- const symbol = known?.symbol || shortId(mint)
448
- const name = known?.name || `SPL Token ${shortId(mint)}`
449
- const display = normalizeAssetDisplay(symbol, amountAtomic, decimals)
450
- const existing = tokensByMint.get(mint)
451
- if (existing) {
452
- const nextAtomic = (BigInt(existing.balanceAtomic) + BigInt(amountAtomic)).toString()
453
- tokensByMint.set(mint, {
454
- ...existing,
455
- balanceAtomic: nextAtomic,
456
- ...normalizeAssetDisplay(existing.symbol, nextAtomic, existing.decimals),
457
- })
458
- continue
459
- }
460
- tokensByMint.set(mint, {
461
- id: `solana:mainnet:${mint}`,
462
- chain: 'solana',
463
- networkId: 'solana-mainnet',
464
- networkLabel: 'Solana',
465
- symbol,
466
- name,
467
- decimals,
468
- balanceAtomic: amountAtomic,
469
- ...display,
470
- isNative: false,
471
- tokenMint: mint,
472
- explorerUrl: `${SOLSCAN_TOKEN_BASE}${mint}`,
473
- })
474
- }
475
- }
476
-
477
- const unknownMints = [...tokensByMint.keys()].filter((mint) => !KNOWN_SOLANA_TOKENS[mint])
478
- const metadataByMint = await resolveSolanaMetadata(unknownMints)
479
- for (const [mint, metadata] of metadataByMint.entries()) {
480
- const existing = tokensByMint.get(mint)
481
- if (!existing) continue
482
- const symbol = metadata.symbol?.trim() || existing.symbol
483
- const name = metadata.name?.trim() || existing.name
484
- tokensByMint.set(mint, {
485
- ...existing,
486
- symbol,
487
- name,
488
- ...normalizeAssetDisplay(symbol, existing.balanceAtomic, existing.decimals),
489
- })
490
- }
491
-
492
- assets.push(...tokensByMint.values())
493
- const sortedAssets = sortAssets(assets)
494
- return {
495
- balanceAtomic: nativeBalanceAtomic,
496
- balanceFormatted: nativeDisplay.balanceFormatted,
497
- balanceDisplay: nativeDisplay.balanceDisplay,
498
- balanceSymbol: 'SOL',
499
- balanceLamports: Number.parseInt(nativeBalanceAtomic, 10),
500
- balanceSol: Number.parseFloat(nativeDisplay.balanceFormatted),
501
- assets: sortedAssets,
502
- summary: buildPortfolioSummary(sortedAssets),
503
- }
504
- }
505
-
506
- async function fetchAlchemyTokenContracts(provider: JsonRpcProvider, address: string): Promise<string[] | null> {
507
- try {
508
- const response = await provider.send('alchemy_getTokenBalances', [address, 'erc20']) as {
509
- tokenBalances?: Array<{ contractAddress?: string; tokenBalance?: string }>
510
- }
511
- const balances = Array.isArray(response?.tokenBalances) ? response.tokenBalances : []
512
- return balances
513
- .map((entry) => String(entry?.contractAddress || '').trim())
514
- .filter((value) => value && value !== '0x0000000000000000000000000000000000000000')
515
- } catch {
516
- return null
517
- }
518
- }
519
-
520
- async function readErc20Bytes32Metadata(
521
- provider: JsonRpcProvider,
522
- contractAddress: string,
523
- method: 'symbol' | 'name',
524
- ): Promise<string> {
525
- try {
526
- const iface = method === 'symbol' ? ERC20_SYMBOL_BYTES32_IFACE : ERC20_NAME_BYTES32_IFACE
527
- const raw = await provider.call({
528
- to: contractAddress,
529
- data: iface.encodeFunctionData(method, []),
530
- })
531
- if (!raw || raw === '0x') return ''
532
- const decoded = iface.decodeFunctionResult(method, raw)[0]
533
- if (typeof decoded !== 'string') return ''
534
- return readFixedUtf8String(Buffer.from(decoded.slice(2), 'hex'), 0, 32)
535
- } catch {
536
- return ''
537
- }
538
- }
539
-
540
- async function getLogsWithChunking(
541
- provider: JsonRpcProvider,
542
- filter: { fromBlock: number; toBlock: number; topics: Array<string | null> },
543
- maxLogRange?: number,
544
- ) {
545
- const ranges = buildLogDiscoveryRanges(filter.fromBlock, filter.toBlock, maxLogRange)
546
- const logs = []
547
- for (const range of ranges) {
548
- logs.push(...await provider.getLogs({
549
- ...filter,
550
- fromBlock: range.fromBlock,
551
- toBlock: range.toBlock,
552
- }))
553
- }
554
- return logs
555
- }
556
-
557
- async function discoverErc20ContractsFromLogs(
558
- provider: JsonRpcProvider,
559
- address: string,
560
- walletId: string,
561
- walletCreatedAt: number,
562
- network: EvmNetworkConfig,
563
- ): Promise<string[]> {
564
- const cacheKey = `${walletId}:${network.id}:${address.toLowerCase()}`
565
- const cached = evmContractDiscoveryCache.get(cacheKey)
566
- try {
567
- const latestBlock = await provider.getBlockNumber()
568
- const contractSet = new Set(cached?.contractAddresses || [])
569
- const fallbackFromBlock = estimateDiscoveryStartBlock({
570
- latestBlock,
571
- walletCreatedAt,
572
- avgBlockMs: network.avgBlockMs,
573
- maxDiscoveryBlocks: network.maxDiscoveryBlocks,
574
- })
575
- const fromBlock = cached && cached.walletCreatedAt === walletCreatedAt
576
- ? Math.max(fallbackFromBlock, cached.scannedToBlock + 1)
577
- : fallbackFromBlock
578
- if (fromBlock > latestBlock) return [...contractSet]
579
-
580
- const paddedAddress = zeroPadValue(getAddress(address), 32)
581
- const [incoming, outgoing] = await Promise.all([
582
- getLogsWithChunking(provider, {
583
- fromBlock,
584
- toBlock: latestBlock,
585
- topics: [ERC20_TRANSFER_TOPIC, null, paddedAddress],
586
- }, network.maxLogRange),
587
- getLogsWithChunking(provider, {
588
- fromBlock,
589
- toBlock: latestBlock,
590
- topics: [ERC20_TRANSFER_TOPIC, paddedAddress],
591
- }, network.maxLogRange),
592
- ])
593
- for (const contractAddress of [...incoming, ...outgoing]
594
- .map((log) => {
595
- try {
596
- return getAddress(log.address)
597
- } catch {
598
- return ''
599
- }
600
- })
601
- .filter(Boolean)) {
602
- contractSet.add(contractAddress)
603
- }
604
- evmContractDiscoveryCache.set(cacheKey, {
605
- expiresAt: Date.now() + EVM_CONTRACT_CACHE_TTL_MS,
606
- walletCreatedAt,
607
- scannedToBlock: latestBlock,
608
- contractAddresses: [...contractSet],
609
- })
610
- return [...contractSet]
611
- } catch {
612
- return cached?.contractAddresses || []
613
- }
614
- }
615
-
616
- async function resolveAlchemyMetadata(
617
- provider: JsonRpcProvider,
618
- contractAddress: string,
619
- ): Promise<{ symbol?: string; name?: string; decimals?: number } | null> {
620
- try {
621
- const metadata = await provider.send('alchemy_getTokenMetadata', [contractAddress]) as {
622
- symbol?: string
623
- name?: string
624
- decimals?: number
625
- }
626
- return metadata || null
627
- } catch {
628
- return null
629
- }
630
- }
631
-
632
- async function resolveErc20Asset(
633
- provider: JsonRpcProvider,
634
- address: string,
635
- contractAddress: string,
636
- network: EvmNetworkConfig,
637
- ): Promise<WalletAssetBalance | null> {
638
- try {
639
- const normalizedContractAddress = getAddress(contractAddress)
640
- const contract = new Contract(normalizedContractAddress, ERC20_ABI, provider)
641
- const metadata = await resolveAlchemyMetadata(provider, contractAddress)
642
- const knownToken = network.knownTokens?.find((token) => token.address.toLowerCase() === normalizedContractAddress.toLowerCase())
643
- const [balanceRaw, decimalsRaw, symbolRaw, nameRaw] = await Promise.all([
644
- contract.balanceOf(address).catch(() => BigInt(0)),
645
- metadata?.decimals != null
646
- ? Promise.resolve(metadata.decimals)
647
- : knownToken?.decimals != null
648
- ? Promise.resolve(knownToken.decimals)
649
- : contract.decimals().catch(() => 18),
650
- metadata?.symbol
651
- ? Promise.resolve(metadata.symbol)
652
- : knownToken?.symbol
653
- ? Promise.resolve(knownToken.symbol)
654
- : contract.symbol().catch(() => readErc20Bytes32Metadata(provider, normalizedContractAddress, 'symbol')),
655
- metadata?.name
656
- ? Promise.resolve(metadata.name)
657
- : knownToken?.name
658
- ? Promise.resolve(knownToken.name)
659
- : contract.name().catch(() => readErc20Bytes32Metadata(provider, normalizedContractAddress, 'name')),
660
- ])
661
- const balanceAtomic = balanceRaw.toString()
662
- if (BigInt(balanceAtomic) <= BigInt(0)) return null
663
- const decimals = typeof decimalsRaw === 'number' ? decimalsRaw : Number(decimalsRaw ?? 18)
664
- const symbol = typeof symbolRaw === 'string' && symbolRaw.trim() ? symbolRaw.trim() : shortId(normalizedContractAddress)
665
- const name = typeof nameRaw === 'string' && nameRaw.trim() ? nameRaw.trim() : `ERC-20 ${shortId(normalizedContractAddress)}`
666
- const display = normalizeAssetDisplay(symbol, balanceAtomic, decimals)
667
- return {
668
- id: `${network.id}:${normalizedContractAddress.toLowerCase()}`,
669
- chain: 'ethereum',
670
- networkId: network.id,
671
- networkLabel: network.label,
672
- symbol,
673
- name,
674
- decimals,
675
- balanceAtomic,
676
- ...display,
677
- isNative: false,
678
- contractAddress: normalizedContractAddress,
679
- explorerUrl: `${network.tokenExplorerBaseUrl}${normalizedContractAddress}`,
680
- }
681
- } catch {
682
- return null
683
- }
684
- }
685
-
686
- async function fetchEvmAssets(wallet: AgentWallet): Promise<WalletPortfolio> {
687
- const assets: WalletAssetBalance[] = []
688
- let totalNativeAtomic = BigInt(0)
689
-
690
- for (const network of getEnabledEvmNetworks()) {
691
- const provider = getEthereumProvider(network.rpcUrl)
692
- let nativeBalanceAtomic = '0'
693
- try {
694
- nativeBalanceAtomic = (await provider.getBalance(wallet.publicKey)).toString()
695
- } catch {
696
- nativeBalanceAtomic = '0'
697
- }
698
- totalNativeAtomic += BigInt(nativeBalanceAtomic)
699
- assets.push({
700
- id: `${network.id}:native`,
701
- chain: 'ethereum',
702
- networkId: network.id,
703
- networkLabel: network.label,
704
- symbol: 'ETH',
705
- name: `${network.label} ETH`,
706
- decimals: 18,
707
- balanceAtomic: nativeBalanceAtomic,
708
- ...normalizeAssetDisplay('ETH', nativeBalanceAtomic, 18, { minFractionDigits: 4 }),
709
- isNative: true,
710
- explorerUrl: `${network.addressExplorerBaseUrl}${wallet.publicKey}`,
711
- })
712
-
713
- const contractSet = new Set<string>()
714
- for (const knownToken of network.knownTokens || []) contractSet.add(knownToken.address)
715
- const alchemyContracts = await fetchAlchemyTokenContracts(provider, wallet.publicKey)
716
- for (const contractAddress of alchemyContracts || []) contractSet.add(getAddress(contractAddress))
717
- const discoveredContracts = await discoverErc20ContractsFromLogs(provider, wallet.publicKey, wallet.id, wallet.createdAt, network)
718
- for (const contractAddress of discoveredContracts) contractSet.add(contractAddress)
719
-
720
- const tokenAssets = await Promise.all(
721
- [...contractSet].map((contractAddress) => resolveErc20Asset(provider, wallet.publicKey, contractAddress, network)),
722
- )
723
- assets.push(...tokenAssets.filter((asset): asset is WalletAssetBalance => Boolean(asset)))
724
- }
725
-
726
- const nativeBalanceAtomic = totalNativeAtomic.toString()
727
- const nativeDisplay = normalizeAssetDisplay('ETH', nativeBalanceAtomic, 18, { minFractionDigits: 4 })
728
- const sortedAssets = sortAssets(assets)
729
- return {
730
- balanceAtomic: nativeBalanceAtomic,
731
- balanceFormatted: nativeDisplay.balanceFormatted,
732
- balanceDisplay: nativeDisplay.balanceDisplay,
733
- balanceSymbol: getWalletAssetSymbol('ethereum'),
734
- assets: sortedAssets,
735
- summary: buildPortfolioSummary(sortedAssets),
736
- }
737
- }
738
-
739
- export async function getWalletPortfolio(wallet: AgentWallet, options?: GetWalletPortfolioOptions): Promise<WalletPortfolio> {
740
- const cacheKey = buildPortfolioCacheKey(wallet)
741
- const cached = getCurrentCachedPortfolioEntry(wallet)
742
- if (cached && cached.expiresAt > Date.now()) return cached.portfolio
743
-
744
- const stale = options?.allowStale ? getLatestCachedPortfolioEntry(wallet.id)?.portfolio || null : null
745
- const portfolio = await resolveWalletPortfolioWithTimeout({
746
- load: () => (wallet.chain === 'ethereum' ? fetchEvmAssets(wallet) : fetchSolanaAssets(wallet)),
747
- timeoutMs: options?.timeoutMs,
748
- stale,
749
- label: `wallet portfolio ${wallet.id}`,
750
- })
751
-
752
- pruneExpiredPortfolioCaches()
753
- portfolioCache.set(cacheKey, {
754
- expiresAt: Date.now() + PORTFOLIO_CACHE_TTL_MS,
755
- portfolio,
756
- })
757
- return portfolio
758
- }
759
-
760
- export function clearWalletPortfolioCache(walletId?: string) {
761
- if (!walletId) {
762
- portfolioCache.clear()
763
- evmContractDiscoveryCache.clear()
764
- return
765
- }
766
- for (const key of portfolioCache.keys()) {
767
- if (key.startsWith(`${walletId}:`)) portfolioCache.delete(key)
768
- }
769
- for (const [key, entry] of evmContractDiscoveryCache.entries()) {
770
- if (entry.expiresAt <= Date.now() || key.startsWith(`${walletId}:`)) evmContractDiscoveryCache.delete(key)
771
- }
772
- }