@oydual31/more-vaults-sdk 0.4.2 → 0.6.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 (38) hide show
  1. package/README.md +94 -0
  2. package/dist/{spokeRoutes-B8Lnk-t4.d.cts → curatorBridge-CNs59kT9.d.cts} +222 -1
  3. package/dist/{spokeRoutes-B8Lnk-t4.d.ts → curatorBridge-CNs59kT9.d.ts} +222 -1
  4. package/dist/ethers/index.cjs +328 -3
  5. package/dist/ethers/index.cjs.map +1 -1
  6. package/dist/ethers/index.d.cts +279 -1
  7. package/dist/ethers/index.d.ts +279 -1
  8. package/dist/ethers/index.js +318 -5
  9. package/dist/ethers/index.js.map +1 -1
  10. package/dist/react/index.cjs +375 -0
  11. package/dist/react/index.cjs.map +1 -1
  12. package/dist/react/index.d.cts +266 -2
  13. package/dist/react/index.d.ts +266 -2
  14. package/dist/react/index.js +372 -2
  15. package/dist/react/index.js.map +1 -1
  16. package/dist/viem/index.cjs +377 -0
  17. package/dist/viem/index.cjs.map +1 -1
  18. package/dist/viem/index.d.cts +261 -3
  19. package/dist/viem/index.d.ts +261 -3
  20. package/dist/viem/index.js +367 -2
  21. package/dist/viem/index.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/ethers/abis.ts +24 -0
  24. package/src/ethers/curatorBridge.ts +235 -0
  25. package/src/ethers/curatorSubVaults.ts +443 -0
  26. package/src/ethers/index.ts +26 -0
  27. package/src/ethers/types.ts +99 -0
  28. package/src/react/index.ts +14 -0
  29. package/src/react/useCuratorBridgeQuote.ts +43 -0
  30. package/src/react/useERC7540RequestStatus.ts +43 -0
  31. package/src/react/useExecuteBridge.ts +50 -0
  32. package/src/react/useSubVaultPositions.ts +35 -0
  33. package/src/react/useVaultPortfolio.ts +35 -0
  34. package/src/viem/abis.ts +24 -0
  35. package/src/viem/curatorBridge.ts +288 -0
  36. package/src/viem/curatorSubVaults.ts +514 -0
  37. package/src/viem/index.ts +23 -0
  38. package/src/viem/types.ts +100 -0
@@ -0,0 +1,514 @@
1
+ /**
2
+ * Curator sub-vault read helpers for the MoreVaults SDK (Phase 5).
3
+ *
4
+ * Provides portfolio views and sub-vault analysis for curator dashboards.
5
+ * Supports both ERC4626 (synchronous) and ERC7540 (asynchronous) sub-vaults.
6
+ *
7
+ * All functions are read-only (no wallet needed) and use multicall for
8
+ * batched RPC efficiency.
9
+ */
10
+
11
+ import {
12
+ type Address,
13
+ type PublicClient,
14
+ getAddress,
15
+ keccak256,
16
+ toHex,
17
+ zeroAddress,
18
+ } from 'viem'
19
+ import {
20
+ SUB_VAULT_ABI,
21
+ ERC20_ABI,
22
+ METADATA_ABI,
23
+ VAULT_ABI,
24
+ VAULT_ANALYSIS_ABI,
25
+ REGISTRY_ABI,
26
+ } from './abis.js'
27
+ import type {
28
+ SubVaultPosition,
29
+ SubVaultInfo,
30
+ ERC7540RequestStatus,
31
+ VaultPortfolio,
32
+ } from './types.js'
33
+ import type { AssetBalance } from './types.js'
34
+
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+ // Internal constants
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+
39
+ /** keccak256("ERC4626_ID") — type ID for synchronous ERC4626 sub-vaults */
40
+ const ERC4626_ID = keccak256(toHex('ERC4626_ID')) as `0x${string}`
41
+
42
+ /** keccak256("ERC7540_ID") — type ID for asynchronous ERC7540 sub-vaults */
43
+ const ERC7540_ID = keccak256(toHex('ERC7540_ID')) as `0x${string}`
44
+
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Get active sub-vault positions held by the vault.
49
+ *
50
+ * Queries the vault's `tokensHeld` for ERC4626 and ERC7540 type IDs, then
51
+ * fetches balances, underlying values, and token metadata for each sub-vault
52
+ * in a single multicall round. Sub-vaults with zero share balance are excluded.
53
+ *
54
+ * @param publicClient Viem public client (must be on the vault's chain)
55
+ * @param vault Vault address (diamond proxy)
56
+ * @returns Array of active SubVaultPosition objects
57
+ */
58
+ export async function getSubVaultPositions(
59
+ publicClient: PublicClient,
60
+ vault: Address,
61
+ ): Promise<SubVaultPosition[]> {
62
+ const v = getAddress(vault)
63
+
64
+ // Step 1: fetch the list of sub-vaults by type in parallel
65
+ const [erc4626Raw, erc7540Raw] = await Promise.all([
66
+ publicClient
67
+ .readContract({
68
+ address: v,
69
+ abi: SUB_VAULT_ABI,
70
+ functionName: 'tokensHeld',
71
+ args: [ERC4626_ID],
72
+ })
73
+ .catch(() => [] as Address[]),
74
+ publicClient
75
+ .readContract({
76
+ address: v,
77
+ abi: SUB_VAULT_ABI,
78
+ functionName: 'tokensHeld',
79
+ args: [ERC7540_ID],
80
+ })
81
+ .catch(() => [] as Address[]),
82
+ ])
83
+
84
+ const erc4626Vaults = (erc4626Raw as Address[]).map(getAddress)
85
+ const erc7540Vaults = (erc7540Raw as Address[]).map(getAddress)
86
+
87
+ const allSubVaults: Array<{ address: Address; type: 'erc4626' | 'erc7540' }> = [
88
+ ...erc4626Vaults.map((a) => ({ address: a, type: 'erc4626' as const })),
89
+ ...erc7540Vaults.map((a) => ({ address: a, type: 'erc7540' as const })),
90
+ ]
91
+
92
+ if (allSubVaults.length === 0) return []
93
+
94
+ // Step 2: multicall — for each sub-vault:
95
+ // balanceOf(vault), asset(), name(), symbol(), decimals()
96
+ // That's 5 calls per sub-vault = slots 0..4, 5..9, etc.
97
+ const PER_SV = 5
98
+ const subVaultCalls = allSubVaults.flatMap(({ address: sv }) => [
99
+ { address: sv, abi: ERC20_ABI, functionName: 'balanceOf' as const, args: [v] as [Address] },
100
+ { address: sv, abi: VAULT_ABI, functionName: 'asset' as const },
101
+ { address: sv, abi: METADATA_ABI, functionName: 'name' as const },
102
+ { address: sv, abi: METADATA_ABI, functionName: 'symbol' as const },
103
+ { address: sv, abi: METADATA_ABI, functionName: 'decimals' as const },
104
+ ])
105
+
106
+ const subVaultResults = await publicClient.multicall({
107
+ contracts: subVaultCalls,
108
+ allowFailure: true,
109
+ })
110
+
111
+ // Parse per-sub-vault results and collect underlying asset addresses
112
+ interface PartialSV {
113
+ address: Address
114
+ type: 'erc4626' | 'erc7540'
115
+ sharesBalance: bigint
116
+ underlyingAsset: Address
117
+ name: string
118
+ symbol: string
119
+ decimals: number
120
+ }
121
+
122
+ const partials: PartialSV[] = allSubVaults.map(({ address: sv, type }, i) => {
123
+ const base = i * PER_SV
124
+ const sharesBalance = subVaultResults[base]?.status === 'success'
125
+ ? (subVaultResults[base].result as bigint)
126
+ : 0n
127
+ const underlyingAsset = subVaultResults[base + 1]?.status === 'success'
128
+ ? getAddress(subVaultResults[base + 1].result as Address)
129
+ : zeroAddress
130
+ const name = subVaultResults[base + 2]?.status === 'success' ? (subVaultResults[base + 2].result as string) : ''
131
+ const symbol = subVaultResults[base + 3]?.status === 'success' ? (subVaultResults[base + 3].result as string) : ''
132
+ const decimals = subVaultResults[base + 4]?.status === 'success' ? (subVaultResults[base + 4].result as number) : 18
133
+
134
+ return { address: sv, type, sharesBalance, underlyingAsset, name, symbol, decimals }
135
+ })
136
+
137
+ // Filter out sub-vaults with no position
138
+ const active = partials.filter((p) => p.sharesBalance > 0n)
139
+ if (active.length === 0) return []
140
+
141
+ // Step 3: multicall — for each active sub-vault:
142
+ // convertToAssets(sharesBalance) on the sub-vault
143
+ // name(), symbol(), decimals() on the underlying asset
144
+ // That's 4 calls per active sub-vault = slots 0..3, 4..7, etc.
145
+ const PER_ACTIVE = 4
146
+ const activeCalls = active.flatMap(({ address: sv, sharesBalance, underlyingAsset }) => [
147
+ { address: sv, abi: SUB_VAULT_ABI, functionName: 'convertToAssets' as const, args: [sharesBalance] as [bigint] },
148
+ { address: underlyingAsset, abi: METADATA_ABI, functionName: 'name' as const },
149
+ { address: underlyingAsset, abi: METADATA_ABI, functionName: 'symbol' as const },
150
+ { address: underlyingAsset, abi: METADATA_ABI, functionName: 'decimals' as const },
151
+ ])
152
+
153
+ const activeResults = await publicClient.multicall({
154
+ contracts: activeCalls,
155
+ allowFailure: true,
156
+ })
157
+
158
+ return active.map((p, i) => {
159
+ const base = i * PER_ACTIVE
160
+ const underlyingValue = activeResults[base]?.status === 'success' ? (activeResults[base].result as bigint) : 0n
161
+ const underlyingSymbol = activeResults[base + 2]?.status === 'success' ? (activeResults[base + 2].result as string) : ''
162
+ const underlyingDecimals = activeResults[base + 3]?.status === 'success' ? (activeResults[base + 3].result as number) : 18
163
+
164
+ return {
165
+ address: p.address,
166
+ type: p.type,
167
+ name: p.name,
168
+ symbol: p.symbol,
169
+ decimals: p.decimals,
170
+ sharesBalance: p.sharesBalance,
171
+ underlyingValue,
172
+ underlyingAsset: p.underlyingAsset,
173
+ underlyingSymbol,
174
+ underlyingDecimals,
175
+ } satisfies SubVaultPosition
176
+ })
177
+ }
178
+
179
+ // ─────────────────────────────────────────────────────────────────────────────
180
+
181
+ /**
182
+ * Detect whether a contract is an ERC7540, ERC4626, or unknown vault type.
183
+ *
184
+ * Tries to call ERC7540-specific functions first (pendingDepositRequest /
185
+ * claimableDepositRequest). If those revert, falls back to ERC4626
186
+ * convertToAssets(0). Returns null if neither succeeds.
187
+ *
188
+ * @param publicClient Viem public client (must be on the same chain as subVault)
189
+ * @param subVault Sub-vault contract address to probe
190
+ * @returns 'erc7540' | 'erc4626' | null
191
+ */
192
+ export async function detectSubVaultType(
193
+ publicClient: PublicClient,
194
+ subVault: Address,
195
+ ): Promise<'erc4626' | 'erc7540' | null> {
196
+ const sv = getAddress(subVault)
197
+
198
+ const [erc7540Result, erc4626Result] = await publicClient.multicall({
199
+ contracts: [
200
+ {
201
+ address: sv,
202
+ abi: SUB_VAULT_ABI,
203
+ functionName: 'pendingDepositRequest',
204
+ args: [0n, zeroAddress],
205
+ },
206
+ {
207
+ address: sv,
208
+ abi: SUB_VAULT_ABI,
209
+ functionName: 'convertToAssets',
210
+ args: [0n],
211
+ },
212
+ ],
213
+ allowFailure: true,
214
+ })
215
+
216
+ if (erc7540Result?.status === 'success') return 'erc7540'
217
+ if (erc4626Result?.status === 'success') return 'erc4626'
218
+ return null
219
+ }
220
+
221
+ // ─────────────────────────────────────────────────────────────────────────────
222
+
223
+ /**
224
+ * Analyse a specific sub-vault to understand deposit limits, metadata, type,
225
+ * and global-registry whitelist status.
226
+ *
227
+ * Useful for curators deciding whether to invest vault funds into a given
228
+ * ERC4626 or ERC7540 protocol.
229
+ *
230
+ * @param publicClient Viem public client (must be on the vault's chain)
231
+ * @param vault Vault address (diamond proxy) — used to check maxDeposit
232
+ * @param subVault Sub-vault address to analyse
233
+ * @returns SubVaultInfo snapshot
234
+ */
235
+ export async function getSubVaultInfo(
236
+ publicClient: PublicClient,
237
+ vault: Address,
238
+ subVault: Address,
239
+ ): Promise<SubVaultInfo> {
240
+ const v = getAddress(vault)
241
+ const sv = getAddress(subVault)
242
+
243
+ // Detect type and fetch basic metadata in parallel
244
+ const [type, basicResults] = await Promise.all([
245
+ detectSubVaultType(publicClient, sv),
246
+ publicClient.multicall({
247
+ contracts: [
248
+ { address: sv, abi: METADATA_ABI, functionName: 'name' as const },
249
+ { address: sv, abi: METADATA_ABI, functionName: 'symbol' as const },
250
+ { address: sv, abi: METADATA_ABI, functionName: 'decimals' as const },
251
+ { address: sv, abi: VAULT_ABI, functionName: 'asset' as const },
252
+ { address: sv, abi: SUB_VAULT_ABI, functionName: 'maxDeposit' as const, args: [v] as [Address] },
253
+ ],
254
+ allowFailure: true,
255
+ }),
256
+ ])
257
+
258
+ const name = basicResults[0]?.status === 'success' ? (basicResults[0].result as string) : ''
259
+ const symbol = basicResults[1]?.status === 'success' ? (basicResults[1].result as string) : ''
260
+ const decimals = basicResults[2]?.status === 'success' ? (basicResults[2].result as number) : 18
261
+ const underlying = basicResults[3]?.status === 'success'
262
+ ? getAddress(basicResults[3].result as Address)
263
+ : zeroAddress
264
+ const maxDeposit = basicResults[4]?.status === 'success' ? (basicResults[4].result as bigint) : 0n
265
+
266
+ // Fetch underlying asset metadata and whitelist status in parallel
267
+ const [underlyingResults, registryRaw] = await Promise.all([
268
+ publicClient.multicall({
269
+ contracts: [
270
+ { address: underlying, abi: METADATA_ABI, functionName: 'name' as const },
271
+ { address: underlying, abi: METADATA_ABI, functionName: 'symbol' as const },
272
+ { address: underlying, abi: METADATA_ABI, functionName: 'decimals' as const },
273
+ ],
274
+ allowFailure: true,
275
+ }),
276
+ publicClient
277
+ .readContract({ address: v, abi: VAULT_ANALYSIS_ABI, functionName: 'moreVaultsRegistry' })
278
+ .catch(() => null),
279
+ ])
280
+
281
+ const underlyingSymbol = underlyingResults[1]?.status === 'success' ? (underlyingResults[1].result as string) : ''
282
+ const underlyingDecimals = underlyingResults[2]?.status === 'success' ? (underlyingResults[2].result as number) : 18
283
+
284
+ let isWhitelisted = false
285
+ if (registryRaw) {
286
+ const registry = getAddress(registryRaw as Address)
287
+ const whitelistResult = await publicClient
288
+ .readContract({
289
+ address: registry,
290
+ abi: REGISTRY_ABI,
291
+ functionName: 'isWhitelisted',
292
+ args: [sv],
293
+ })
294
+ .catch(() => false)
295
+ isWhitelisted = whitelistResult as boolean
296
+ }
297
+
298
+ return {
299
+ address: sv,
300
+ type: type ?? 'erc4626',
301
+ name,
302
+ symbol,
303
+ decimals,
304
+ underlyingAsset: underlying,
305
+ underlyingSymbol,
306
+ underlyingDecimals,
307
+ maxDeposit,
308
+ isWhitelisted,
309
+ }
310
+ }
311
+
312
+ // ─────────────────────────────────────────────────────────────────────────────
313
+
314
+ /**
315
+ * Get the ERC7540 async request status for a specific sub-vault and vault controller.
316
+ *
317
+ * Queries pendingDepositRequest, claimableDepositRequest, pendingRedeemRequest,
318
+ * and claimableRedeemRequest using requestId = 0 (the standard default).
319
+ *
320
+ * @param publicClient Viem public client (must be on the vault's chain)
321
+ * @param vault Vault address acting as controller in the sub-vault
322
+ * @param subVault ERC7540 sub-vault address
323
+ * @returns ERC7540RequestStatus with canFinalize flags
324
+ */
325
+ export async function getERC7540RequestStatus(
326
+ publicClient: PublicClient,
327
+ vault: Address,
328
+ subVault: Address,
329
+ ): Promise<ERC7540RequestStatus> {
330
+ const v = getAddress(vault)
331
+ const sv = getAddress(subVault)
332
+
333
+ const results = await publicClient.multicall({
334
+ contracts: [
335
+ { address: sv, abi: SUB_VAULT_ABI, functionName: 'pendingDepositRequest' as const, args: [0n, v] as [bigint, Address] },
336
+ { address: sv, abi: SUB_VAULT_ABI, functionName: 'claimableDepositRequest' as const, args: [0n, v] as [bigint, Address] },
337
+ { address: sv, abi: SUB_VAULT_ABI, functionName: 'pendingRedeemRequest' as const, args: [0n, v] as [bigint, Address] },
338
+ { address: sv, abi: SUB_VAULT_ABI, functionName: 'claimableRedeemRequest' as const, args: [0n, v] as [bigint, Address] },
339
+ ],
340
+ allowFailure: true,
341
+ })
342
+
343
+ const pendingDeposit = results[0]?.status === 'success' ? (results[0].result as bigint) : 0n
344
+ const claimableDeposit = results[1]?.status === 'success' ? (results[1].result as bigint) : 0n
345
+ const pendingRedeem = results[2]?.status === 'success' ? (results[2].result as bigint) : 0n
346
+ const claimableRedeem = results[3]?.status === 'success' ? (results[3].result as bigint) : 0n
347
+
348
+ return {
349
+ subVault: sv,
350
+ pendingDeposit,
351
+ claimableDeposit,
352
+ pendingRedeem,
353
+ claimableRedeem,
354
+ canFinalizeDeposit: claimableDeposit > 0n,
355
+ canFinalizeRedeem: claimableRedeem > 0n,
356
+ }
357
+ }
358
+
359
+ // ─────────────────────────────────────────────────────────────────────────────
360
+
361
+ /**
362
+ * Preview how many shares the vault would receive for a given asset deposit
363
+ * into a sub-vault.
364
+ *
365
+ * Calls `previewDeposit(assets)` on the sub-vault contract.
366
+ *
367
+ * @param publicClient Viem public client
368
+ * @param subVault Sub-vault address (ERC4626 or ERC7540)
369
+ * @param assets Amount of underlying assets to preview
370
+ * @returns Expected shares to be minted
371
+ */
372
+ export async function previewSubVaultDeposit(
373
+ publicClient: PublicClient,
374
+ subVault: Address,
375
+ assets: bigint,
376
+ ): Promise<bigint> {
377
+ const result = await publicClient.readContract({
378
+ address: getAddress(subVault),
379
+ abi: SUB_VAULT_ABI,
380
+ functionName: 'previewDeposit',
381
+ args: [assets],
382
+ })
383
+ return result as bigint
384
+ }
385
+
386
+ // ─────────────────────────────────────────────────────────────────────────────
387
+
388
+ /**
389
+ * Preview how many underlying assets the vault would receive for redeeming
390
+ * a given number of shares from a sub-vault.
391
+ *
392
+ * Calls `previewRedeem(shares)` on the sub-vault contract.
393
+ *
394
+ * @param publicClient Viem public client
395
+ * @param subVault Sub-vault address (ERC4626 or ERC7540)
396
+ * @param shares Number of shares to preview redemption for
397
+ * @returns Expected underlying assets to be returned
398
+ */
399
+ export async function previewSubVaultRedeem(
400
+ publicClient: PublicClient,
401
+ subVault: Address,
402
+ shares: bigint,
403
+ ): Promise<bigint> {
404
+ const result = await publicClient.readContract({
405
+ address: getAddress(subVault),
406
+ abi: SUB_VAULT_ABI,
407
+ functionName: 'previewRedeem',
408
+ args: [shares],
409
+ })
410
+ return result as bigint
411
+ }
412
+
413
+ // ─────────────────────────────────────────────────────────────────────────────
414
+
415
+ /**
416
+ * Get the complete portfolio view for a vault, combining liquid asset balances
417
+ * with active sub-vault positions and locked ERC7540 assets.
418
+ *
419
+ * Liquid assets that are also sub-vault share tokens are deduplicated to avoid
420
+ * double-counting (the sub-vault's underlying value is already captured via
421
+ * convertToAssets).
422
+ *
423
+ * @param publicClient Viem public client (must be on the vault's hub chain)
424
+ * @param vault Vault address (diamond proxy)
425
+ * @returns VaultPortfolio with full breakdown
426
+ */
427
+ export async function getVaultPortfolio(
428
+ publicClient: PublicClient,
429
+ vault: Address,
430
+ ): Promise<VaultPortfolio> {
431
+ const v = getAddress(vault)
432
+
433
+ // Step 1: get available assets, sub-vault positions, totalAssets, totalSupply, underlying in parallel
434
+ const [availableRaw, subVaultPositions, vaultTotals] = await Promise.all([
435
+ publicClient
436
+ .readContract({ address: v, abi: VAULT_ANALYSIS_ABI, functionName: 'getAvailableAssets' })
437
+ .catch(() => [] as Address[]),
438
+ getSubVaultPositions(publicClient, v),
439
+ publicClient.multicall({
440
+ contracts: [
441
+ { address: v, abi: VAULT_ABI, functionName: 'totalAssets' as const },
442
+ { address: v, abi: VAULT_ABI, functionName: 'totalSupply' as const },
443
+ { address: v, abi: VAULT_ABI, functionName: 'asset' as const },
444
+ ],
445
+ allowFailure: true,
446
+ }),
447
+ ])
448
+
449
+ const totalAssets = vaultTotals[0]?.status === 'success' ? (vaultTotals[0].result as bigint) : 0n
450
+ const totalSupply = vaultTotals[1]?.status === 'success' ? (vaultTotals[1].result as bigint) : 0n
451
+ const underlyingAsset = vaultTotals[2]?.status === 'success'
452
+ ? getAddress(vaultTotals[2].result as Address)
453
+ : zeroAddress
454
+
455
+ const availableAddresses = (availableRaw as Address[]).map(getAddress)
456
+
457
+ // Sub-vault share addresses to exclude from liquid assets (avoid double-counting)
458
+ const subVaultAddressSet = new Set(subVaultPositions.map((p) => p.address.toLowerCase()))
459
+
460
+ // Filter liquid asset addresses: exclude sub-vault share tokens
461
+ const liquidAddresses = availableAddresses.filter(
462
+ (addr) => !subVaultAddressSet.has(addr.toLowerCase()),
463
+ )
464
+
465
+ // Step 2: fetch balances + metadata for liquid assets in one multicall
466
+ const PER_ASSET = 4 // balanceOf, name, symbol, decimals
467
+ const liquidCalls = liquidAddresses.flatMap((addr) => [
468
+ { address: addr, abi: ERC20_ABI, functionName: 'balanceOf' as const, args: [v] as [Address] },
469
+ { address: addr, abi: METADATA_ABI, functionName: 'name' as const },
470
+ { address: addr, abi: METADATA_ABI, functionName: 'symbol' as const },
471
+ { address: addr, abi: METADATA_ABI, functionName: 'decimals' as const },
472
+ ])
473
+
474
+ const liquidResults = liquidAddresses.length > 0
475
+ ? await publicClient.multicall({ contracts: liquidCalls, allowFailure: true })
476
+ : []
477
+
478
+ const liquidAssets: AssetBalance[] = liquidAddresses.map((addr, i) => {
479
+ const base = i * PER_ASSET
480
+ const balance = liquidResults[base]?.status === 'success' ? (liquidResults[base].result as bigint) : 0n
481
+ const name = liquidResults[base + 1]?.status === 'success' ? (liquidResults[base + 1].result as string) : ''
482
+ const symbol = liquidResults[base + 2]?.status === 'success' ? (liquidResults[base + 2].result as string) : ''
483
+ const decimals = liquidResults[base + 3]?.status === 'success' ? (liquidResults[base + 3].result as number) : 18
484
+ return { address: addr, name, symbol, decimals, balance }
485
+ })
486
+
487
+ // Step 3: fetch locked assets for the vault's underlying (ERC7540 pending requests)
488
+ const lockedAssets = await publicClient
489
+ .readContract({
490
+ address: v,
491
+ abi: SUB_VAULT_ABI,
492
+ functionName: 'lockedTokensAmountOfAsset',
493
+ args: [underlyingAsset],
494
+ })
495
+ .catch(() => 0n) as bigint
496
+
497
+ // Step 4: compute total value = liquid assets (underlying only) + sub-vault underlying values
498
+ // totalAssets from the vault already accounts for all positions, so we use it directly.
499
+ // We also provide a manual sum of sub-vault values for reference.
500
+ const subVaultTotal = subVaultPositions.reduce((sum, p) => sum + p.underlyingValue, 0n)
501
+ const underlyingBalance = liquidAssets.find(
502
+ (a) => a.address.toLowerCase() === underlyingAsset.toLowerCase(),
503
+ )?.balance ?? 0n
504
+ const totalValue = underlyingBalance + subVaultTotal
505
+
506
+ return {
507
+ liquidAssets,
508
+ subVaultPositions,
509
+ totalValue,
510
+ totalAssets,
511
+ totalSupply,
512
+ lockedAssets,
513
+ }
514
+ }
package/src/viem/index.ts CHANGED
@@ -22,6 +22,7 @@ export {
22
22
  LZ_ADAPTER_ABI,
23
23
  VAULT_ANALYSIS_ABI,
24
24
  REGISTRY_ABI,
25
+ SUB_VAULT_ABI,
25
26
  } from './abis'
26
27
 
27
28
  // --- Types ---
@@ -45,6 +46,10 @@ export type {
45
46
  AssetBalance,
46
47
  VaultAnalysis,
47
48
  VaultAssetBreakdown,
49
+ SubVaultPosition,
50
+ SubVaultInfo,
51
+ ERC7540RequestStatus,
52
+ VaultPortfolio,
48
53
  } from './types'
49
54
  export { ActionType } from './types'
50
55
 
@@ -178,6 +183,24 @@ export {
178
183
  buildUniswapV3Swap,
179
184
  encodeUniswapV3SwapCalldata,
180
185
  } from './curatorSwaps'
186
+ export {
187
+ encodeBridgeParams,
188
+ quoteCuratorBridgeFee,
189
+ executeCuratorBridge,
190
+ findBridgeRoute,
191
+ } from './curatorBridge'
192
+ export type { CuratorBridgeParams } from './curatorBridge'
193
+
194
+ // --- Curator Sub-Vault Operations ---
195
+ export {
196
+ getSubVaultPositions,
197
+ detectSubVaultType,
198
+ getSubVaultInfo,
199
+ getERC7540RequestStatus,
200
+ previewSubVaultDeposit,
201
+ previewSubVaultRedeem,
202
+ getVaultPortfolio,
203
+ } from './curatorSubVaults'
181
204
 
182
205
  // --- wagmi compatibility ---
183
206
  // Re-export viem's PublicClient type for wagmi compatibility.
package/src/viem/types.ts CHANGED
@@ -193,3 +193,103 @@ export interface VaultAssetBreakdown {
193
193
  /** Vault underlying token decimals */
194
194
  underlyingDecimals: number
195
195
  }
196
+
197
+ // ─────────────────────────────────────────────────────────────────────────────
198
+ // Sub-vault Types (Phase 5)
199
+ // ─────────────────────────────────────────────────────────────────────────────
200
+
201
+ /**
202
+ * A single active sub-vault position held by the curator vault.
203
+ * Covers both ERC4626 (synchronous) and ERC7540 (asynchronous) sub-vaults.
204
+ */
205
+ export interface SubVaultPosition {
206
+ /** Sub-vault contract address */
207
+ address: Address
208
+ /** Protocol type of the sub-vault */
209
+ type: 'erc4626' | 'erc7540'
210
+ /** Name of the sub-vault share token */
211
+ name: string
212
+ /** Symbol of the sub-vault share token */
213
+ symbol: string
214
+ /** Decimals of the sub-vault share token */
215
+ decimals: number
216
+ /** Shares of the sub-vault held by the curator vault */
217
+ sharesBalance: bigint
218
+ /** Current value of the shares in terms of the sub-vault's underlying asset */
219
+ underlyingValue: bigint
220
+ /** Underlying asset address of the sub-vault */
221
+ underlyingAsset: Address
222
+ /** Symbol of the sub-vault's underlying asset */
223
+ underlyingSymbol: string
224
+ /** Decimals of the sub-vault's underlying asset */
225
+ underlyingDecimals: number
226
+ }
227
+
228
+ /**
229
+ * Metadata and capability snapshot for a potential sub-vault investment target.
230
+ */
231
+ export interface SubVaultInfo {
232
+ /** Sub-vault contract address */
233
+ address: Address
234
+ /** Protocol type: ERC4626 (sync) or ERC7540 (async) */
235
+ type: 'erc4626' | 'erc7540'
236
+ /** Sub-vault share token name */
237
+ name: string
238
+ /** Sub-vault share token symbol */
239
+ symbol: string
240
+ /** Sub-vault share token decimals */
241
+ decimals: number
242
+ /** Underlying asset address */
243
+ underlyingAsset: Address
244
+ /** Underlying asset symbol */
245
+ underlyingSymbol: string
246
+ /** Underlying asset decimals */
247
+ underlyingDecimals: number
248
+ /** Maximum amount the curator vault can deposit (from maxDeposit(vault)) */
249
+ maxDeposit: bigint
250
+ /** Whether the sub-vault is whitelisted in the global MoreVaults registry */
251
+ isWhitelisted: boolean
252
+ }
253
+
254
+ /**
255
+ * Status of pending and claimable ERC7540 async requests for a sub-vault.
256
+ * Uses requestId = 0 (the standard default for non-batch ERC7540 vaults).
257
+ */
258
+ export interface ERC7540RequestStatus {
259
+ /** Sub-vault address these statuses belong to */
260
+ subVault: Address
261
+ /** Assets in a pending deposit request (not yet claimable) */
262
+ pendingDeposit: bigint
263
+ /** Assets ready to be claimed/finalized as shares */
264
+ claimableDeposit: bigint
265
+ /** Shares in a pending redeem request (not yet claimable) */
266
+ pendingRedeem: bigint
267
+ /** Assets ready to be claimed after redeem fulfillment */
268
+ claimableRedeem: bigint
269
+ /** True if claimableDeposit > 0 — vault can call erc7540Deposit to finalize */
270
+ canFinalizeDeposit: boolean
271
+ /** True if claimableRedeem > 0 — vault can call erc7540Redeem to finalize */
272
+ canFinalizeRedeem: boolean
273
+ }
274
+
275
+ /**
276
+ * Full portfolio view for a curator vault combining liquid and invested assets.
277
+ */
278
+ export interface VaultPortfolio {
279
+ /** Liquid ERC20 asset balances held directly by the vault (excludes sub-vault share tokens) */
280
+ liquidAssets: AssetBalance[]
281
+ /** Active positions in ERC4626/ERC7540 sub-vaults */
282
+ subVaultPositions: SubVaultPosition[]
283
+ /**
284
+ * Approximate total value in underlying terms:
285
+ * liquid underlying balance + sum of sub-vault convertToAssets values.
286
+ * For the authoritative total, use `totalAssets` from the vault contract.
287
+ */
288
+ totalValue: bigint
289
+ /** totalAssets() from the vault — authoritative AUM figure */
290
+ totalAssets: bigint
291
+ /** totalSupply() of vault shares */
292
+ totalSupply: bigint
293
+ /** Assets locked in pending ERC7540 requests (lockedTokensAmountOfAsset) */
294
+ lockedAssets: bigint
295
+ }