@oydual31/more-vaults-sdk 0.3.0 → 0.3.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oydual31/more-vaults-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "TypeScript SDK for MoreVaults protocol — viem/wagmi and ethers.js",
5
5
  "type": "module",
6
6
  "exports": {
package/src/viem/abis.ts CHANGED
@@ -729,6 +729,57 @@ export const LZ_ADAPTER_ABI = [
729
729
  },
730
730
  ] as const
731
731
 
732
+ /**
733
+ * ERC4626Facet ABI — synchronous deposit and redeem into whitelisted ERC-4626 vaults.
734
+ */
735
+ export const ERC4626_FACET_ABI = [
736
+ {
737
+ type: 'function',
738
+ name: 'erc4626Deposit',
739
+ inputs: [
740
+ { name: 'vault', type: 'address' },
741
+ { name: 'assets', type: 'uint256' },
742
+ ],
743
+ outputs: [{ name: 'shares', type: 'uint256' }],
744
+ stateMutability: 'nonpayable',
745
+ },
746
+ {
747
+ type: 'function',
748
+ name: 'erc4626Redeem',
749
+ inputs: [
750
+ { name: 'vault', type: 'address' },
751
+ { name: 'shares', type: 'uint256' },
752
+ ],
753
+ outputs: [{ name: 'assets', type: 'uint256' }],
754
+ stateMutability: 'nonpayable',
755
+ },
756
+ ] as const
757
+
758
+ /**
759
+ * Vault analysis ABIs — per-vault whitelist and registry reads.
760
+ */
761
+ export const VAULT_ANALYSIS_ABI = [
762
+ // Asset management reads
763
+ { type: 'function', name: 'getAvailableAssets', inputs: [], outputs: [{ type: 'address[]' }], stateMutability: 'view' },
764
+ { type: 'function', name: 'getDepositableAssets', inputs: [], outputs: [{ type: 'address[]' }], stateMutability: 'view' },
765
+ { type: 'function', name: 'isAssetAvailable', inputs: [{ name: 'asset', type: 'address' }], outputs: [{ type: 'bool' }], stateMutability: 'view' },
766
+ { type: 'function', name: 'isAssetDepositable', inputs: [{ name: 'asset', type: 'address' }], outputs: [{ type: 'bool' }], stateMutability: 'view' },
767
+ // Deposit whitelist
768
+ { type: 'function', name: 'isDepositWhitelistEnabled', inputs: [], outputs: [{ type: 'bool' }], stateMutability: 'view' },
769
+ { type: 'function', name: 'getAvailableToDeposit', inputs: [{ name: 'depositor', type: 'address' }], outputs: [{ type: 'uint256' }], stateMutability: 'view' },
770
+ // Registry
771
+ { type: 'function', name: 'moreVaultsRegistry', inputs: [], outputs: [{ type: 'address' }], stateMutability: 'view' },
772
+ ] as const
773
+
774
+ /**
775
+ * MoreVaultsRegistry ABI — global protocol and bridge whitelist checks.
776
+ */
777
+ export const REGISTRY_ABI = [
778
+ { type: 'function', name: 'isWhitelisted', inputs: [{ name: 'protocol', type: 'address' }], outputs: [{ type: 'bool' }], stateMutability: 'view' },
779
+ { type: 'function', name: 'isBridgeAllowed', inputs: [{ name: 'bridge', type: 'address' }], outputs: [{ type: 'bool' }], stateMutability: 'view' },
780
+ { type: 'function', name: 'getAllowedFacets', inputs: [], outputs: [{ type: 'address[]' }], stateMutability: 'view' },
781
+ ] as const
782
+
732
783
  /**
733
784
  * Minimal LZ Endpoint V2 ABI for compose queue management.
734
785
  * Used by the Stargate 2-TX flow to check compose status and execute pending composes.
@@ -261,6 +261,23 @@ export const LZ_TIMEOUTS = {
261
261
  FULL_SPOKE_REDEEM: 3_600_000, // 60 min
262
262
  } as const
263
263
 
264
+ /**
265
+ * Uniswap V3 SwapRouter addresses per chain.
266
+ * Used by curator swap helpers to build calldata for on-chain swaps.
267
+ *
268
+ * Note on struct differences:
269
+ * - SwapRouter (Eth/Arb/Op, 0xE592...): exactInputSingle struct includes `deadline`
270
+ * - SwapRouter02 (Base, 0x2626...): exactInputSingle struct does NOT include `deadline`
271
+ * - FlowSwap V3 (Flow EVM, 0xeEDC...): derived from original UniV3, includes `deadline`
272
+ */
273
+ export const UNISWAP_V3_ROUTERS: Record<number, `0x${string}`> = {
274
+ [8453]: '0x2626664c2603336E57B271c5C0b26F421741e481', // Base — SwapRouter02 (no deadline)
275
+ [1]: '0xE592427A0AEce92De3Edee1F18E0157C05861564', // Ethereum — SwapRouter
276
+ [42161]: '0xE592427A0AEce92De3Edee1F18E0157C05861564', // Arbitrum — SwapRouter
277
+ [10]: '0xE592427A0AEce92De3Edee1F18E0157C05861564', // Optimism — SwapRouter
278
+ [747]: '0xeEDC6Ff75e1b10B903D9013c358e446a73d35341', // Flow EVM — FlowSwap V3 SwapRouter
279
+ }
280
+
264
281
  // ---------------------------------------------------------------------------
265
282
  // Legacy flat exports — kept for backwards compat, prefer OFT_ROUTES
266
283
  // ---------------------------------------------------------------------------
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Curator MulticallFacet write operations for the MoreVaults SDK.
3
+ *
4
+ * Provides typed helpers to submit, execute, and veto curator action batches
5
+ * on any MoreVaults diamond that has the MulticallFacet installed.
6
+ *
7
+ * All write functions use the simulate-then-write pattern:
8
+ * 1. `publicClient.simulateContract` — validates on-chain, catches reverts early
9
+ * 2. `walletClient.writeContract` — sends the actual transaction
10
+ *
11
+ * @module curatorMulticall
12
+ */
13
+
14
+ import {
15
+ type Address,
16
+ type PublicClient,
17
+ type WalletClient,
18
+ encodeFunctionData,
19
+ getAddress,
20
+ } from 'viem'
21
+ import {
22
+ MULTICALL_ABI,
23
+ DEX_ABI,
24
+ ERC7540_FACET_ABI,
25
+ ERC4626_FACET_ABI,
26
+ } from './abis.js'
27
+ import type {
28
+ CuratorAction,
29
+ SubmitActionsResult,
30
+ } from './types.js'
31
+
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ // Encoding helpers
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Encode a single typed CuratorAction into raw calldata bytes suitable for
38
+ * passing into `submitActions(bytes[] actionsData)`.
39
+ *
40
+ * The encoded bytes are the full ABI-encoded function call (4-byte selector +
41
+ * arguments) targeting the vault diamond itself — the MulticallFacet will
42
+ * call `address(this).call(actionsData[i])` for each entry.
43
+ *
44
+ * @param action A discriminated-union CuratorAction describing what to do
45
+ * @returns ABI-encoded calldata bytes (`0x`-prefixed hex string)
46
+ */
47
+ export function encodeCuratorAction(action: CuratorAction): `0x${string}` {
48
+ switch (action.type) {
49
+ case 'swap':
50
+ return encodeFunctionData({
51
+ abi: DEX_ABI,
52
+ functionName: 'executeSwap',
53
+ args: [
54
+ {
55
+ targetContract: getAddress(action.params.targetContract),
56
+ tokenIn: getAddress(action.params.tokenIn),
57
+ tokenOut: getAddress(action.params.tokenOut),
58
+ maxAmountIn: action.params.maxAmountIn,
59
+ minAmountOut: action.params.minAmountOut,
60
+ swapCallData: action.params.swapCallData,
61
+ },
62
+ ],
63
+ })
64
+
65
+ case 'batchSwap':
66
+ return encodeFunctionData({
67
+ abi: DEX_ABI,
68
+ functionName: 'executeBatchSwap',
69
+ args: [
70
+ {
71
+ swaps: action.params.swaps.map((s) => ({
72
+ targetContract: getAddress(s.targetContract),
73
+ tokenIn: getAddress(s.tokenIn),
74
+ tokenOut: getAddress(s.tokenOut),
75
+ maxAmountIn: s.maxAmountIn,
76
+ minAmountOut: s.minAmountOut,
77
+ swapCallData: s.swapCallData,
78
+ })),
79
+ },
80
+ ],
81
+ })
82
+
83
+ case 'erc4626Deposit':
84
+ return encodeFunctionData({
85
+ abi: ERC4626_FACET_ABI,
86
+ functionName: 'erc4626Deposit',
87
+ args: [getAddress(action.vault), action.assets],
88
+ })
89
+
90
+ case 'erc4626Redeem':
91
+ return encodeFunctionData({
92
+ abi: ERC4626_FACET_ABI,
93
+ functionName: 'erc4626Redeem',
94
+ args: [getAddress(action.vault), action.shares],
95
+ })
96
+
97
+ case 'erc7540RequestDeposit':
98
+ return encodeFunctionData({
99
+ abi: ERC7540_FACET_ABI,
100
+ functionName: 'erc7540RequestDeposit',
101
+ args: [getAddress(action.vault), action.assets],
102
+ })
103
+
104
+ case 'erc7540Deposit':
105
+ return encodeFunctionData({
106
+ abi: ERC7540_FACET_ABI,
107
+ functionName: 'erc7540Deposit',
108
+ args: [getAddress(action.vault), action.assets],
109
+ })
110
+
111
+ case 'erc7540RequestRedeem':
112
+ return encodeFunctionData({
113
+ abi: ERC7540_FACET_ABI,
114
+ functionName: 'erc7540RequestRedeem',
115
+ args: [getAddress(action.vault), action.shares],
116
+ })
117
+
118
+ case 'erc7540Redeem':
119
+ return encodeFunctionData({
120
+ abi: ERC7540_FACET_ABI,
121
+ functionName: 'erc7540Redeem',
122
+ args: [getAddress(action.vault), action.shares],
123
+ })
124
+
125
+ default: {
126
+ // TypeScript exhaustiveness check — this branch is never reached at runtime
127
+ const _exhaustive: never = action
128
+ throw new Error(`[MoreVaults] Unknown CuratorAction type: ${(_exhaustive as any).type}`)
129
+ }
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Encode an array of CuratorActions into a calldata array ready for
135
+ * `submitActions`.
136
+ *
137
+ * @param actions Array of typed CuratorAction objects
138
+ * @returns Array of ABI-encoded calldata hex strings
139
+ */
140
+ export function buildCuratorBatch(actions: CuratorAction[]): `0x${string}`[] {
141
+ return actions.map(encodeCuratorAction)
142
+ }
143
+
144
+ // ─────────────────────────────────────────────────────────────────────────────
145
+ // Write operations
146
+ // ─────────────────────────────────────────────────────────────────────────────
147
+
148
+ /**
149
+ * Submit a batch of curator actions to the vault's MulticallFacet.
150
+ *
151
+ * When `timeLockPeriod == 0` the contract immediately executes the actions
152
+ * inside `submitActions` itself. When a timelock is configured the actions
153
+ * are queued and must be executed later with `executeActions`.
154
+ *
155
+ * Uses the simulate-then-write pattern: simulation runs first so any on-chain
156
+ * revert (wrong curator, bad selector, slippage limit, etc.) surfaces before
157
+ * any gas is spent on a failing transaction.
158
+ *
159
+ * After the write succeeds, the function reads `getCurrentNonce` to determine
160
+ * which nonce was assigned to this batch (nonce - 1 after the submit increments it).
161
+ *
162
+ * @param walletClient Wallet client with curator account attached
163
+ * @param publicClient Public client for reads and simulation
164
+ * @param vault Vault address (diamond proxy)
165
+ * @param actions Array of raw calldata bytes — use `buildCuratorBatch` to build
166
+ * @returns Transaction hash and the nonce assigned to this batch
167
+ * @throws If the caller is not the curator, or any action selector is
168
+ * not allowed, or any action would revert
169
+ */
170
+ export async function submitActions(
171
+ walletClient: WalletClient,
172
+ publicClient: PublicClient,
173
+ vault: Address,
174
+ actions: `0x${string}`[],
175
+ ): Promise<SubmitActionsResult> {
176
+ const account = walletClient.account!
177
+ const v = getAddress(vault)
178
+
179
+ // Simulate first — catches permission errors and reverts before spending gas
180
+ await publicClient.simulateContract({
181
+ address: v,
182
+ abi: MULTICALL_ABI,
183
+ functionName: 'submitActions',
184
+ args: [actions],
185
+ account: account.address,
186
+ })
187
+
188
+ const txHash = await walletClient.writeContract({
189
+ address: v,
190
+ abi: MULTICALL_ABI,
191
+ functionName: 'submitActions',
192
+ args: [actions],
193
+ account,
194
+ chain: walletClient.chain,
195
+ })
196
+
197
+ // Read the nonce that was assigned: the contract increments actionNonce after storing,
198
+ // so getCurrentNonce now returns (assignedNonce + 1). Subtract 1 to recover it.
199
+ const nextNonce = await publicClient.readContract({
200
+ address: v,
201
+ abi: MULTICALL_ABI,
202
+ functionName: 'getCurrentNonce',
203
+ })
204
+
205
+ const nonce = nextNonce - 1n
206
+
207
+ return { txHash, nonce }
208
+ }
209
+
210
+ /**
211
+ * Execute pending actions after their timelock period has expired.
212
+ *
213
+ * Can only be called when `block.timestamp >= pendingUntil`. The contract
214
+ * reverts with `ActionsStillPending` if the timelock has not expired.
215
+ *
216
+ * Caller must be the curator (or any address when timeLockPeriod == 0, since
217
+ * in that case `submitActions` auto-executes and there is nothing to execute here).
218
+ *
219
+ * Uses simulate-then-write to surface on-chain reverts early.
220
+ *
221
+ * @param walletClient Wallet client with curator account attached
222
+ * @param publicClient Public client for reads and simulation
223
+ * @param vault Vault address (diamond proxy)
224
+ * @param nonce The action batch nonce to execute
225
+ * @returns Transaction hash
226
+ */
227
+ export async function executeActions(
228
+ walletClient: WalletClient,
229
+ publicClient: PublicClient,
230
+ vault: Address,
231
+ nonce: bigint,
232
+ ): Promise<{ txHash: `0x${string}` }> {
233
+ const account = walletClient.account!
234
+ const v = getAddress(vault)
235
+
236
+ // Simulate to surface reverts (NoSuchActions, ActionsStillPending, slippage)
237
+ await publicClient.simulateContract({
238
+ address: v,
239
+ abi: MULTICALL_ABI,
240
+ functionName: 'executeActions',
241
+ args: [nonce],
242
+ account: account.address,
243
+ })
244
+
245
+ const txHash = await walletClient.writeContract({
246
+ address: v,
247
+ abi: MULTICALL_ABI,
248
+ functionName: 'executeActions',
249
+ args: [nonce],
250
+ account,
251
+ chain: walletClient.chain,
252
+ })
253
+
254
+ return { txHash }
255
+ }
256
+
257
+ /**
258
+ * Guardian-only: cancel (veto) one or more pending action batches.
259
+ *
260
+ * Deletes the pending actions from storage, preventing them from ever being
261
+ * executed. Only the vault guardian can call this.
262
+ *
263
+ * Uses simulate-then-write to catch `NoSuchActions` and permission errors early.
264
+ *
265
+ * @param walletClient Wallet client with guardian account attached
266
+ * @param publicClient Public client for reads and simulation
267
+ * @param vault Vault address (diamond proxy)
268
+ * @param nonces Array of action nonces to cancel
269
+ * @returns Transaction hash
270
+ */
271
+ export async function vetoActions(
272
+ walletClient: WalletClient,
273
+ publicClient: PublicClient,
274
+ vault: Address,
275
+ nonces: bigint[],
276
+ ): Promise<{ txHash: `0x${string}` }> {
277
+ const account = walletClient.account!
278
+ const v = getAddress(vault)
279
+
280
+ // Simulate to catch NotGuardian, NoSuchActions, etc.
281
+ await publicClient.simulateContract({
282
+ address: v,
283
+ abi: MULTICALL_ABI,
284
+ functionName: 'vetoActions',
285
+ args: [nonces],
286
+ account: account.address,
287
+ })
288
+
289
+ const txHash = await walletClient.writeContract({
290
+ address: v,
291
+ abi: MULTICALL_ABI,
292
+ functionName: 'vetoActions',
293
+ args: [nonces],
294
+ account,
295
+ chain: walletClient.chain,
296
+ })
297
+
298
+ return { txHash }
299
+ }
@@ -6,8 +6,8 @@
6
6
  */
7
7
 
8
8
  import { type Address, type PublicClient, getAddress } from 'viem'
9
- import { MULTICALL_ABI, CURATOR_CONFIG_ABI } from './abis.js'
10
- import type { CuratorVaultStatus, PendingAction } from './types.js'
9
+ import { MULTICALL_ABI, CURATOR_CONFIG_ABI, VAULT_ANALYSIS_ABI, REGISTRY_ABI, METADATA_ABI, ERC20_ABI, VAULT_ABI } from './abis.js'
10
+ import type { CuratorVaultStatus, PendingAction, VaultAnalysis, AssetInfo, AssetBalance, VaultAssetBreakdown } from './types.js'
11
11
 
12
12
  // ─────────────────────────────────────────────────────────────────────────────
13
13
 
@@ -122,3 +122,198 @@ export async function isCurator(
122
122
 
123
123
  return getAddress(curatorAddress as Address) === getAddress(address)
124
124
  }
125
+
126
+ // ─────────────────────────────────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Full vault analysis — available assets with metadata, depositable assets, whitelist config.
130
+ * Useful for curator dashboards to understand what the vault can do.
131
+ *
132
+ * @param publicClient Viem public client (must be on the vault's chain)
133
+ * @param vault Vault address (diamond proxy)
134
+ * @returns VaultAnalysis snapshot
135
+ */
136
+ export async function getVaultAnalysis(
137
+ publicClient: PublicClient,
138
+ vault: Address,
139
+ ): Promise<VaultAnalysis> {
140
+ const v = getAddress(vault)
141
+
142
+ // Batch 1: fetch asset lists, whitelist flag, and registry address in parallel
143
+ const [availableRaw, depositableRaw, depositWhitelistEnabled, registryResult] =
144
+ await Promise.all([
145
+ publicClient.readContract({
146
+ address: v,
147
+ abi: VAULT_ANALYSIS_ABI,
148
+ functionName: 'getAvailableAssets',
149
+ }),
150
+ publicClient.readContract({
151
+ address: v,
152
+ abi: VAULT_ANALYSIS_ABI,
153
+ functionName: 'getDepositableAssets',
154
+ }),
155
+ publicClient.readContract({
156
+ address: v,
157
+ abi: VAULT_ANALYSIS_ABI,
158
+ functionName: 'isDepositWhitelistEnabled',
159
+ }),
160
+ publicClient.readContract({
161
+ address: v,
162
+ abi: VAULT_ANALYSIS_ABI,
163
+ functionName: 'moreVaultsRegistry',
164
+ }).catch(() => null),
165
+ ])
166
+
167
+ const availableAddresses = (availableRaw as Address[]).map(getAddress)
168
+ const depositableAddresses = (depositableRaw as Address[]).map(getAddress)
169
+
170
+ // Deduplicated set of all asset addresses we need metadata for
171
+ const allAddresses = Array.from(new Set([...availableAddresses, ...depositableAddresses]))
172
+
173
+ // Batch 2: multicall for name/symbol/decimals on all unique assets
174
+ const metadataCalls = allAddresses.flatMap((addr) => [
175
+ { address: addr, abi: METADATA_ABI, functionName: 'name' as const },
176
+ { address: addr, abi: METADATA_ABI, functionName: 'symbol' as const },
177
+ { address: addr, abi: METADATA_ABI, functionName: 'decimals' as const },
178
+ ])
179
+
180
+ const metadataResults = allAddresses.length > 0
181
+ ? await publicClient.multicall({ contracts: metadataCalls, allowFailure: true })
182
+ : []
183
+
184
+ // Map address → AssetInfo
185
+ const assetInfoMap = new Map<Address, AssetInfo>()
186
+ for (let i = 0; i < allAddresses.length; i++) {
187
+ const addr = allAddresses[i]
188
+ const nameResult = metadataResults[i * 3]
189
+ const symbolResult = metadataResults[i * 3 + 1]
190
+ const decimalsResult = metadataResults[i * 3 + 2]
191
+
192
+ assetInfoMap.set(addr, {
193
+ address: addr,
194
+ name: nameResult?.status === 'success' ? (nameResult.result as string) : '',
195
+ symbol: symbolResult?.status === 'success' ? (symbolResult.result as string) : '',
196
+ decimals: decimalsResult?.status === 'success' ? (decimalsResult.result as number) : 18,
197
+ })
198
+ }
199
+
200
+ const registryAddress = registryResult ? getAddress(registryResult as Address) : null
201
+
202
+ return {
203
+ availableAssets: availableAddresses.map((a) => assetInfoMap.get(a)!),
204
+ depositableAssets: depositableAddresses.map((a) => assetInfoMap.get(a)!),
205
+ depositWhitelistEnabled: depositWhitelistEnabled as boolean,
206
+ registryAddress,
207
+ }
208
+ }
209
+
210
+ // ─────────────────────────────────────────────────────────────────────────────
211
+
212
+ /**
213
+ * Check if specific protocol addresses are whitelisted in the global registry.
214
+ * Useful for curators to verify DEX routers before building swap calldata.
215
+ *
216
+ * @param publicClient Viem public client (must be on the vault's chain)
217
+ * @param vault Vault address (diamond proxy)
218
+ * @param protocols Protocol addresses to check
219
+ * @returns Record mapping address → whitelisted boolean
220
+ */
221
+ export async function checkProtocolWhitelist(
222
+ publicClient: PublicClient,
223
+ vault: Address,
224
+ protocols: Address[],
225
+ ): Promise<Record<string, boolean>> {
226
+ const v = getAddress(vault)
227
+
228
+ const registryRaw = await publicClient.readContract({
229
+ address: v,
230
+ abi: VAULT_ANALYSIS_ABI,
231
+ functionName: 'moreVaultsRegistry',
232
+ })
233
+
234
+ const registry = getAddress(registryRaw as Address)
235
+
236
+ if (protocols.length === 0) return {}
237
+
238
+ const results = await publicClient.multicall({
239
+ contracts: protocols.map((protocol) => ({
240
+ address: registry,
241
+ abi: REGISTRY_ABI,
242
+ functionName: 'isWhitelisted' as const,
243
+ args: [getAddress(protocol)] as [Address],
244
+ })),
245
+ allowFailure: true,
246
+ })
247
+
248
+ const out: Record<string, boolean> = {}
249
+ for (let i = 0; i < protocols.length; i++) {
250
+ const r = results[i]
251
+ out[getAddress(protocols[i])] = r?.status === 'success' ? (r.result as boolean) : false
252
+ }
253
+ return out
254
+ }
255
+
256
+ // ─────────────────────────────────────────────────────────────────────────────
257
+
258
+ /**
259
+ * Get the vault's per-asset balance breakdown on the hub chain.
260
+ *
261
+ * Returns the balance of every available asset held by the vault, plus
262
+ * totalAssets and totalSupply for context. Useful for portfolio views
263
+ * that need to show individual holdings rather than a single USD-denominated total.
264
+ *
265
+ * @param publicClient Viem public client (must be on the vault's hub chain)
266
+ * @param vault Vault address (diamond proxy)
267
+ * @returns VaultAssetBreakdown with per-asset balances
268
+ */
269
+ export async function getVaultAssetBreakdown(
270
+ publicClient: PublicClient,
271
+ vault: Address,
272
+ ): Promise<VaultAssetBreakdown> {
273
+ const v = getAddress(vault)
274
+
275
+ // Step 1: get available assets list
276
+ const availableRaw = await publicClient.readContract({
277
+ address: v,
278
+ abi: VAULT_ANALYSIS_ABI,
279
+ functionName: 'getAvailableAssets',
280
+ }) as Address[]
281
+
282
+ const addresses = availableRaw.map(getAddress)
283
+
284
+ // Step 2: multicall — balanceOf + metadata for each asset + totalAssets + totalSupply + vault decimals
285
+ const results = await publicClient.multicall({
286
+ contracts: [
287
+ // Per-asset: balanceOf, name, symbol, decimals
288
+ ...addresses.flatMap((addr) => [
289
+ { address: addr, abi: ERC20_ABI, functionName: 'balanceOf' as const, args: [v] as [Address] },
290
+ { address: addr, abi: METADATA_ABI, functionName: 'name' as const },
291
+ { address: addr, abi: METADATA_ABI, functionName: 'symbol' as const },
292
+ { address: addr, abi: METADATA_ABI, functionName: 'decimals' as const },
293
+ ]),
294
+ // Vault totals
295
+ { address: v, abi: VAULT_ABI, functionName: 'totalAssets' as const },
296
+ { address: v, abi: VAULT_ABI, functionName: 'totalSupply' as const },
297
+ { address: v, abi: METADATA_ABI, functionName: 'decimals' as const },
298
+ ],
299
+ allowFailure: true,
300
+ })
301
+
302
+ const perAssetFields = 4 // balanceOf, name, symbol, decimals
303
+ const assets: AssetBalance[] = addresses.map((addr, i) => {
304
+ const base = i * perAssetFields
305
+ const balance = results[base]?.status === 'success' ? (results[base].result as bigint) : 0n
306
+ const name = results[base + 1]?.status === 'success' ? (results[base + 1].result as string) : ''
307
+ const symbol = results[base + 2]?.status === 'success' ? (results[base + 2].result as string) : ''
308
+ const decimals = results[base + 3]?.status === 'success' ? (results[base + 3].result as number) : 18
309
+
310
+ return { address: addr, name, symbol, decimals, balance }
311
+ })
312
+
313
+ const totalsBase = addresses.length * perAssetFields
314
+ const totalAssets = results[totalsBase]?.status === 'success' ? (results[totalsBase].result as bigint) : 0n
315
+ const totalSupply = results[totalsBase + 1]?.status === 'success' ? (results[totalsBase + 1].result as bigint) : 0n
316
+ const underlyingDecimals = results[totalsBase + 2]?.status === 'success' ? (results[totalsBase + 2].result as number) : 6
317
+
318
+ return { assets, totalAssets, totalSupply, underlyingDecimals }
319
+ }