@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,443 @@
1
+ /**
2
+ * Curator sub-vault read helpers for the MoreVaults ethers.js v6 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 Multicall3 for
8
+ * batched RPC efficiency.
9
+ */
10
+
11
+ import { Contract, Interface, ethers } from "ethers";
12
+ import type { Provider } from "ethers";
13
+ import {
14
+ SUB_VAULT_ABI,
15
+ ERC20_ABI,
16
+ METADATA_ABI,
17
+ VAULT_ABI,
18
+ VAULT_ANALYSIS_ABI,
19
+ REGISTRY_ABI,
20
+ } from "./abis";
21
+ import type {
22
+ SubVaultPosition,
23
+ SubVaultInfo,
24
+ ERC7540RequestStatus,
25
+ VaultPortfolio,
26
+ AssetBalance,
27
+ } from "./types";
28
+
29
+ // Multicall3 — deployed at the same address on every EVM chain
30
+ const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
31
+ const MULTICALL3_ABI = [
32
+ "function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)",
33
+ ] as const;
34
+
35
+ /** keccak256("ERC4626_ID") — type ID for synchronous ERC4626 sub-vaults */
36
+ const ERC4626_ID = ethers.keccak256(ethers.toUtf8Bytes("ERC4626_ID"));
37
+
38
+ /** keccak256("ERC7540_ID") — type ID for asynchronous ERC7540 sub-vaults */
39
+ const ERC7540_ID = ethers.keccak256(ethers.toUtf8Bytes("ERC7540_ID"));
40
+
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+
43
+ /**
44
+ * Get active sub-vault positions held by the vault.
45
+ *
46
+ * Queries the vault's `tokensHeld` for ERC4626 and ERC7540 type IDs, then
47
+ * fetches balances, underlying values, and token metadata for each sub-vault.
48
+ * Sub-vaults with zero share balance are excluded.
49
+ *
50
+ * @param provider Read-only provider (must be on the vault's chain)
51
+ * @param vault Vault address (diamond proxy)
52
+ * @returns Array of active SubVaultPosition objects
53
+ */
54
+ export async function getSubVaultPositions(
55
+ provider: Provider,
56
+ vault: string
57
+ ): Promise<SubVaultPosition[]> {
58
+ const subVaultIface = new Interface(SUB_VAULT_ABI as unknown as string[]);
59
+ const vaultContract = new Contract(vault, SUB_VAULT_ABI as unknown as string[], provider);
60
+
61
+ // Step 1: fetch sub-vault lists by type in parallel
62
+ const [erc4626Raw, erc7540Raw] = await Promise.all([
63
+ (vaultContract.tokensHeld(ERC4626_ID) as Promise<string[]>).catch(() => [] as string[]),
64
+ (vaultContract.tokensHeld(ERC7540_ID) as Promise<string[]>).catch(() => [] as string[]),
65
+ ]);
66
+
67
+ const allSubVaults: Array<{ address: string; type: "erc4626" | "erc7540" }> = [
68
+ ...erc4626Raw.map((a) => ({ address: a, type: "erc4626" as const })),
69
+ ...erc7540Raw.map((a) => ({ address: a, type: "erc7540" as const })),
70
+ ];
71
+
72
+ if (allSubVaults.length === 0) return [];
73
+
74
+ // Step 2: multicall — balanceOf(vault), asset(), name(), symbol(), decimals() per sub-vault
75
+ const mc = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
76
+ const erc20Iface = new Interface(ERC20_ABI as unknown as string[]);
77
+ const metaIface = new Interface(METADATA_ABI as unknown as string[]);
78
+ const vaultIface = new Interface(VAULT_ABI as unknown as string[]);
79
+
80
+ const PER_SV = 5;
81
+ const subVaultCalls = allSubVaults.flatMap(({ address: sv }) => [
82
+ { target: sv, allowFailure: true, callData: erc20Iface.encodeFunctionData("balanceOf", [vault]) },
83
+ { target: sv, allowFailure: true, callData: vaultIface.encodeFunctionData("asset") },
84
+ { target: sv, allowFailure: true, callData: metaIface.encodeFunctionData("name") },
85
+ { target: sv, allowFailure: true, callData: metaIface.encodeFunctionData("symbol") },
86
+ { target: sv, allowFailure: true, callData: metaIface.encodeFunctionData("decimals") },
87
+ ]);
88
+
89
+ const subVaultResults: { success: boolean; returnData: string }[] =
90
+ await mc.aggregate3.staticCall(subVaultCalls);
91
+
92
+ interface PartialSV {
93
+ address: string;
94
+ type: "erc4626" | "erc7540";
95
+ sharesBalance: bigint;
96
+ underlyingAsset: string;
97
+ name: string;
98
+ symbol: string;
99
+ decimals: number;
100
+ }
101
+
102
+ const partials: PartialSV[] = allSubVaults.map(({ address: sv, type }, i) => {
103
+ const base = i * PER_SV;
104
+ const sharesBalance = subVaultResults[base].success
105
+ ? (erc20Iface.decodeFunctionResult("balanceOf", subVaultResults[base].returnData)[0] as bigint)
106
+ : 0n;
107
+ const underlyingAsset = subVaultResults[base + 1].success
108
+ ? (vaultIface.decodeFunctionResult("asset", subVaultResults[base + 1].returnData)[0] as string)
109
+ : ethers.ZeroAddress;
110
+ const name = subVaultResults[base + 2].success ? (metaIface.decodeFunctionResult("name", subVaultResults[base + 2].returnData)[0] as string) : "";
111
+ const symbol = subVaultResults[base + 3].success ? (metaIface.decodeFunctionResult("symbol", subVaultResults[base + 3].returnData)[0] as string) : "";
112
+ const decimals = subVaultResults[base + 4].success ? (Number(metaIface.decodeFunctionResult("decimals", subVaultResults[base + 4].returnData)[0])) : 18;
113
+
114
+ return { address: sv, type, sharesBalance, underlyingAsset, name, symbol, decimals };
115
+ });
116
+
117
+ // Filter to positions with non-zero balance
118
+ const active = partials.filter((p) => p.sharesBalance > 0n);
119
+ if (active.length === 0) return [];
120
+
121
+ // Step 3: multicall — convertToAssets(shares) + underlying metadata per active position
122
+ const PER_ACTIVE = 4;
123
+ const activeCalls = active.flatMap(({ address: sv, sharesBalance, underlyingAsset }) => [
124
+ { target: sv, allowFailure: true, callData: subVaultIface.encodeFunctionData("convertToAssets", [sharesBalance]) },
125
+ { target: underlyingAsset, allowFailure: true, callData: metaIface.encodeFunctionData("name") },
126
+ { target: underlyingAsset, allowFailure: true, callData: metaIface.encodeFunctionData("symbol") },
127
+ { target: underlyingAsset, allowFailure: true, callData: metaIface.encodeFunctionData("decimals") },
128
+ ]);
129
+
130
+ const activeResults: { success: boolean; returnData: string }[] =
131
+ await mc.aggregate3.staticCall(activeCalls);
132
+
133
+ return active.map((p, i): SubVaultPosition => {
134
+ const base = i * PER_ACTIVE;
135
+ const underlyingValue = activeResults[base].success
136
+ ? (subVaultIface.decodeFunctionResult("convertToAssets", activeResults[base].returnData)[0] as bigint)
137
+ : 0n;
138
+ const underlyingSymbol = activeResults[base + 2].success ? (metaIface.decodeFunctionResult("symbol", activeResults[base + 2].returnData)[0] as string) : "";
139
+ const underlyingDecimals = activeResults[base + 3].success ? (Number(metaIface.decodeFunctionResult("decimals", activeResults[base + 3].returnData)[0])) : 18;
140
+
141
+ return {
142
+ address: p.address,
143
+ type: p.type,
144
+ name: p.name,
145
+ symbol: p.symbol,
146
+ decimals: p.decimals,
147
+ sharesBalance: p.sharesBalance,
148
+ underlyingValue,
149
+ underlyingAsset: p.underlyingAsset,
150
+ underlyingSymbol,
151
+ underlyingDecimals,
152
+ };
153
+ });
154
+ }
155
+
156
+ // ─────────────────────────────────────────────────────────────────────────────
157
+
158
+ /**
159
+ * Detect whether a contract is an ERC7540, ERC4626, or unknown vault type.
160
+ *
161
+ * Tries to call ERC7540-specific functions first (pendingDepositRequest).
162
+ * Falls back to ERC4626 convertToAssets(0). Returns null if neither succeeds.
163
+ *
164
+ * @param provider Read-only provider (must be on the same chain as subVault)
165
+ * @param subVault Sub-vault contract address to probe
166
+ * @returns 'erc7540' | 'erc4626' | null
167
+ */
168
+ export async function detectSubVaultType(
169
+ provider: Provider,
170
+ subVault: string
171
+ ): Promise<"erc4626" | "erc7540" | null> {
172
+ const mc = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
173
+ const subVaultIface = new Interface(SUB_VAULT_ABI as unknown as string[]);
174
+
175
+ const calls = [
176
+ {
177
+ target: subVault,
178
+ allowFailure: true,
179
+ callData: subVaultIface.encodeFunctionData("pendingDepositRequest", [0n, ethers.ZeroAddress]),
180
+ },
181
+ {
182
+ target: subVault,
183
+ allowFailure: true,
184
+ callData: subVaultIface.encodeFunctionData("convertToAssets", [0n]),
185
+ },
186
+ ];
187
+
188
+ const results: { success: boolean; returnData: string }[] =
189
+ await mc.aggregate3.staticCall(calls);
190
+
191
+ if (results[0].success) return "erc7540";
192
+ if (results[1].success) return "erc4626";
193
+ return null;
194
+ }
195
+
196
+ // ─────────────────────────────────────────────────────────────────────────────
197
+
198
+ /**
199
+ * Analyse a specific sub-vault to understand deposit limits, metadata, type,
200
+ * and global-registry whitelist status.
201
+ *
202
+ * @param provider Read-only provider (must be on the vault's chain)
203
+ * @param vault Vault address (diamond proxy) — used to check maxDeposit
204
+ * @param subVault Sub-vault address to analyse
205
+ * @returns SubVaultInfo snapshot
206
+ */
207
+ export async function getSubVaultInfo(
208
+ provider: Provider,
209
+ vault: string,
210
+ subVault: string
211
+ ): Promise<SubVaultInfo> {
212
+ const mc = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
213
+ const subVaultIface = new Interface(SUB_VAULT_ABI as unknown as string[]);
214
+ const metaIface = new Interface(METADATA_ABI as unknown as string[]);
215
+ const vaultIface = new Interface(VAULT_ABI as unknown as string[]);
216
+ const registryIface = new Interface(REGISTRY_ABI as unknown as string[]);
217
+ const vaultAnalysisIface = new Interface(VAULT_ANALYSIS_ABI as unknown as string[]);
218
+
219
+ // Detect type and fetch basic metadata in parallel
220
+ const [type, basicResults] = await Promise.all([
221
+ detectSubVaultType(provider, subVault),
222
+ mc.aggregate3.staticCall([
223
+ { target: subVault, allowFailure: true, callData: metaIface.encodeFunctionData("name") },
224
+ { target: subVault, allowFailure: true, callData: metaIface.encodeFunctionData("symbol") },
225
+ { target: subVault, allowFailure: true, callData: metaIface.encodeFunctionData("decimals") },
226
+ { target: subVault, allowFailure: true, callData: vaultIface.encodeFunctionData("asset") },
227
+ { target: subVault, allowFailure: true, callData: subVaultIface.encodeFunctionData("maxDeposit", [vault]) },
228
+ ]) as Promise<{ success: boolean; returnData: string }[]>,
229
+ ]);
230
+
231
+ const name = basicResults[0].success ? (metaIface.decodeFunctionResult("name", basicResults[0].returnData)[0] as string) : "";
232
+ const symbol = basicResults[1].success ? (metaIface.decodeFunctionResult("symbol", basicResults[1].returnData)[0] as string) : "";
233
+ const decimals = basicResults[2].success ? (Number(metaIface.decodeFunctionResult("decimals", basicResults[2].returnData)[0])) : 18;
234
+ const underlying = basicResults[3].success ? (vaultIface.decodeFunctionResult("asset", basicResults[3].returnData)[0] as string) : ethers.ZeroAddress;
235
+ const maxDeposit = basicResults[4].success ? (subVaultIface.decodeFunctionResult("maxDeposit", basicResults[4].returnData)[0] as bigint) : 0n;
236
+
237
+ // Fetch underlying metadata and registry in parallel
238
+ const [underlyingResults, registryRaw] = await Promise.all([
239
+ mc.aggregate3.staticCall([
240
+ { target: underlying, allowFailure: true, callData: metaIface.encodeFunctionData("symbol") },
241
+ { target: underlying, allowFailure: true, callData: metaIface.encodeFunctionData("decimals") },
242
+ ]) as Promise<{ success: boolean; returnData: string }[]>,
243
+ new Contract(vault, VAULT_ANALYSIS_ABI as unknown as string[], provider)
244
+ .moreVaultsRegistry()
245
+ .catch(() => null) as Promise<string | null>,
246
+ ]);
247
+
248
+ const underlyingSymbol = underlyingResults[0].success ? (metaIface.decodeFunctionResult("symbol", underlyingResults[0].returnData)[0] as string) : "";
249
+ const underlyingDecimals = underlyingResults[1].success ? (Number(metaIface.decodeFunctionResult("decimals", underlyingResults[1].returnData)[0])) : 18;
250
+
251
+ let isWhitelisted = false;
252
+ if (registryRaw) {
253
+ const whitelistResult = await (new Contract(registryRaw, REGISTRY_ABI as unknown as string[], provider)
254
+ .isWhitelisted(subVault) as Promise<boolean>)
255
+ .catch(() => false);
256
+ isWhitelisted = whitelistResult;
257
+ }
258
+
259
+ return {
260
+ address: subVault,
261
+ type: type ?? "erc4626",
262
+ name,
263
+ symbol,
264
+ decimals,
265
+ underlyingAsset: underlying,
266
+ underlyingSymbol,
267
+ underlyingDecimals,
268
+ maxDeposit,
269
+ isWhitelisted,
270
+ };
271
+ }
272
+
273
+ // ─────────────────────────────────────────────────────────────────────────────
274
+
275
+ /**
276
+ * Get the ERC7540 async request status for a specific sub-vault and vault controller.
277
+ *
278
+ * @param provider Read-only provider (must be on the vault's chain)
279
+ * @param vault Vault address acting as controller in the sub-vault
280
+ * @param subVault ERC7540 sub-vault address
281
+ * @returns ERC7540RequestStatus with canFinalize flags
282
+ */
283
+ export async function getERC7540RequestStatus(
284
+ provider: Provider,
285
+ vault: string,
286
+ subVault: string
287
+ ): Promise<ERC7540RequestStatus> {
288
+ const mc = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
289
+ const subVaultIface = new Interface(SUB_VAULT_ABI as unknown as string[]);
290
+
291
+ const calls = [
292
+ { target: subVault, allowFailure: true, callData: subVaultIface.encodeFunctionData("pendingDepositRequest", [0n, vault]) },
293
+ { target: subVault, allowFailure: true, callData: subVaultIface.encodeFunctionData("claimableDepositRequest", [0n, vault]) },
294
+ { target: subVault, allowFailure: true, callData: subVaultIface.encodeFunctionData("pendingRedeemRequest", [0n, vault]) },
295
+ { target: subVault, allowFailure: true, callData: subVaultIface.encodeFunctionData("claimableRedeemRequest", [0n, vault]) },
296
+ ];
297
+
298
+ const results: { success: boolean; returnData: string }[] =
299
+ await mc.aggregate3.staticCall(calls);
300
+
301
+ const pendingDeposit = results[0].success ? (subVaultIface.decodeFunctionResult("pendingDepositRequest", results[0].returnData)[0] as bigint) : 0n;
302
+ const claimableDeposit = results[1].success ? (subVaultIface.decodeFunctionResult("claimableDepositRequest", results[1].returnData)[0] as bigint) : 0n;
303
+ const pendingRedeem = results[2].success ? (subVaultIface.decodeFunctionResult("pendingRedeemRequest", results[2].returnData)[0] as bigint) : 0n;
304
+ const claimableRedeem = results[3].success ? (subVaultIface.decodeFunctionResult("claimableRedeemRequest", results[3].returnData)[0] as bigint) : 0n;
305
+
306
+ return {
307
+ subVault,
308
+ pendingDeposit,
309
+ claimableDeposit,
310
+ pendingRedeem,
311
+ claimableRedeem,
312
+ canFinalizeDeposit: claimableDeposit > 0n,
313
+ canFinalizeRedeem: claimableRedeem > 0n,
314
+ };
315
+ }
316
+
317
+ // ─────────────────────────────────────────────────────────────────────────────
318
+
319
+ /**
320
+ * Preview how many shares the vault would receive for a given asset deposit.
321
+ *
322
+ * @param provider Read-only provider
323
+ * @param subVault Sub-vault address (ERC4626 or ERC7540)
324
+ * @param assets Amount of underlying assets to preview
325
+ * @returns Expected shares to be minted
326
+ */
327
+ export async function previewSubVaultDeposit(
328
+ provider: Provider,
329
+ subVault: string,
330
+ assets: bigint
331
+ ): Promise<bigint> {
332
+ const contract = new Contract(subVault, SUB_VAULT_ABI as unknown as string[], provider);
333
+ return (await contract.previewDeposit(assets)) as bigint;
334
+ }
335
+
336
+ // ─────────────────────────────────────────────────────────────────────────────
337
+
338
+ /**
339
+ * Preview how many underlying assets would be returned for redeeming shares.
340
+ *
341
+ * @param provider Read-only provider
342
+ * @param subVault Sub-vault address (ERC4626 or ERC7540)
343
+ * @param shares Number of shares to preview redemption for
344
+ * @returns Expected underlying assets to be returned
345
+ */
346
+ export async function previewSubVaultRedeem(
347
+ provider: Provider,
348
+ subVault: string,
349
+ shares: bigint
350
+ ): Promise<bigint> {
351
+ const contract = new Contract(subVault, SUB_VAULT_ABI as unknown as string[], provider);
352
+ return (await contract.previewRedeem(shares)) as bigint;
353
+ }
354
+
355
+ // ─────────────────────────────────────────────────────────────────────────────
356
+
357
+ /**
358
+ * Get the complete portfolio view for a vault, combining liquid asset balances
359
+ * with active sub-vault positions and locked ERC7540 assets.
360
+ *
361
+ * @param provider Read-only provider (must be on the vault's hub chain)
362
+ * @param vault Vault address (diamond proxy)
363
+ * @returns VaultPortfolio with full breakdown
364
+ */
365
+ export async function getVaultPortfolio(
366
+ provider: Provider,
367
+ vault: string
368
+ ): Promise<VaultPortfolio> {
369
+ const mc = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
370
+ const subVaultIface = new Interface(SUB_VAULT_ABI as unknown as string[]);
371
+ const vaultAnalysisIface = new Interface(VAULT_ANALYSIS_ABI as unknown as string[]);
372
+ const vaultIface = new Interface(VAULT_ABI as unknown as string[]);
373
+ const erc20Iface = new Interface(ERC20_ABI as unknown as string[]);
374
+ const metaIface = new Interface(METADATA_ABI as unknown as string[]);
375
+
376
+ // Step 1: get available assets, sub-vault positions, and vault totals in parallel
377
+ const [availableRaw, subVaultPositions, vaultTotals] = await Promise.all([
378
+ (new Contract(vault, VAULT_ANALYSIS_ABI as unknown as string[], provider).getAvailableAssets() as Promise<string[]>)
379
+ .catch(() => [] as string[]),
380
+ getSubVaultPositions(provider, vault),
381
+ mc.aggregate3.staticCall([
382
+ { target: vault, allowFailure: true, callData: vaultIface.encodeFunctionData("totalAssets") },
383
+ { target: vault, allowFailure: true, callData: vaultIface.encodeFunctionData("totalSupply") },
384
+ { target: vault, allowFailure: true, callData: vaultIface.encodeFunctionData("asset") },
385
+ ]) as Promise<{ success: boolean; returnData: string }[]>,
386
+ ]);
387
+
388
+ const totalAssets = vaultTotals[0].success ? (vaultIface.decodeFunctionResult("totalAssets", vaultTotals[0].returnData)[0] as bigint) : 0n;
389
+ const totalSupply = vaultTotals[1].success ? (vaultIface.decodeFunctionResult("totalSupply", vaultTotals[1].returnData)[0] as bigint) : 0n;
390
+ const underlyingAsset = vaultTotals[2].success ? (vaultIface.decodeFunctionResult("asset", vaultTotals[2].returnData)[0] as string) : ethers.ZeroAddress;
391
+
392
+ // Exclude sub-vault share addresses from liquid asset list
393
+ const subVaultAddressSet = new Set(subVaultPositions.map((p) => p.address.toLowerCase()));
394
+ const liquidAddresses = (availableRaw as string[]).filter(
395
+ (addr) => !subVaultAddressSet.has(addr.toLowerCase()),
396
+ );
397
+
398
+ // Step 2: fetch balances + metadata for liquid assets
399
+ const PER_ASSET = 4;
400
+ let liquidAssets: AssetBalance[] = [];
401
+
402
+ if (liquidAddresses.length > 0) {
403
+ const liquidCalls = liquidAddresses.flatMap((addr) => [
404
+ { target: addr, allowFailure: true, callData: erc20Iface.encodeFunctionData("balanceOf", [vault]) },
405
+ { target: addr, allowFailure: true, callData: metaIface.encodeFunctionData("name") },
406
+ { target: addr, allowFailure: true, callData: metaIface.encodeFunctionData("symbol") },
407
+ { target: addr, allowFailure: true, callData: metaIface.encodeFunctionData("decimals") },
408
+ ]);
409
+
410
+ const liquidResults: { success: boolean; returnData: string }[] =
411
+ await mc.aggregate3.staticCall(liquidCalls);
412
+
413
+ liquidAssets = liquidAddresses.map((addr, i): AssetBalance => {
414
+ const base = i * PER_ASSET;
415
+ const balance = liquidResults[base].success ? (erc20Iface.decodeFunctionResult("balanceOf", liquidResults[base].returnData)[0] as bigint) : 0n;
416
+ const name = liquidResults[base + 1].success ? (metaIface.decodeFunctionResult("name", liquidResults[base + 1].returnData)[0] as string) : "";
417
+ const symbol = liquidResults[base + 2].success ? (metaIface.decodeFunctionResult("symbol", liquidResults[base + 2].returnData)[0] as string) : "";
418
+ const decimals = liquidResults[base + 3].success ? (Number(metaIface.decodeFunctionResult("decimals", liquidResults[base + 3].returnData)[0])) : 18;
419
+ return { address: addr, name, symbol, decimals, balance };
420
+ });
421
+ }
422
+
423
+ // Step 3: locked assets for the vault's underlying (ERC7540 pending requests)
424
+ const lockedAssets = await (new Contract(vault, SUB_VAULT_ABI as unknown as string[], provider)
425
+ .lockedTokensAmountOfAsset(underlyingAsset) as Promise<bigint>)
426
+ .catch(() => 0n);
427
+
428
+ // Step 4: compute approximate total value
429
+ const subVaultTotal = subVaultPositions.reduce((sum, p) => sum + p.underlyingValue, 0n);
430
+ const underlyingBalance = liquidAssets.find(
431
+ (a) => a.address.toLowerCase() === underlyingAsset.toLowerCase(),
432
+ )?.balance ?? 0n;
433
+ const totalValue = underlyingBalance + subVaultTotal;
434
+
435
+ return {
436
+ liquidAssets,
437
+ subVaultPositions,
438
+ totalValue,
439
+ totalAssets,
440
+ totalSupply,
441
+ lockedAssets,
442
+ };
443
+ }
@@ -27,6 +27,11 @@ export type {
27
27
  VaultAnalysis,
28
28
  AssetBalance,
29
29
  VaultAssetBreakdown,
30
+ // Sub-vault types (Phase 5)
31
+ SubVaultPosition,
32
+ SubVaultInfo,
33
+ ERC7540RequestStatus,
34
+ VaultPortfolio,
30
35
  } from "./types";
31
36
  export { ActionType } from "./types";
32
37
 
@@ -49,6 +54,7 @@ export {
49
54
  LZ_ADAPTER_ABI,
50
55
  VAULT_ANALYSIS_ABI,
51
56
  REGISTRY_ABI,
57
+ SUB_VAULT_ABI,
52
58
  } from "./abis";
53
59
 
54
60
  // --- Errors ---
@@ -171,6 +177,26 @@ export {
171
177
  encodeUniswapV3SwapCalldata,
172
178
  } from "./curatorSwaps";
173
179
 
180
+ // --- Curator bridge helpers ---
181
+ export {
182
+ encodeBridgeParams,
183
+ quoteCuratorBridgeFee,
184
+ executeCuratorBridge,
185
+ findBridgeRoute,
186
+ } from "./curatorBridge";
187
+ export type { CuratorBridgeParams } from "./curatorBridge";
188
+
189
+ // --- Curator Sub-Vault Operations (Phase 5) ---
190
+ export {
191
+ getSubVaultPositions,
192
+ detectSubVaultType,
193
+ getSubVaultInfo,
194
+ getERC7540RequestStatus,
195
+ previewSubVaultDeposit,
196
+ previewSubVaultRedeem,
197
+ getVaultPortfolio,
198
+ } from "./curatorSubVaults";
199
+
174
200
  // --- Topology ---
175
201
  export {
176
202
  getVaultTopology,
@@ -165,4 +165,103 @@ export interface VaultAssetBreakdown {
165
165
  underlyingDecimals: number;
166
166
  }
167
167
 
168
+ // ─────────────────────────────────────────────────────────────────────────────
169
+ // Sub-vault Types (Phase 5)
170
+ // ─────────────────────────────────────────────────────────────────────────────
171
+
172
+ /**
173
+ * A single active sub-vault position held by the curator vault.
174
+ * Covers both ERC4626 (synchronous) and ERC7540 (asynchronous) sub-vaults.
175
+ */
176
+ export interface SubVaultPosition {
177
+ /** Sub-vault contract address */
178
+ address: string;
179
+ /** Protocol type of the sub-vault */
180
+ type: "erc4626" | "erc7540";
181
+ /** Name of the sub-vault share token */
182
+ name: string;
183
+ /** Symbol of the sub-vault share token */
184
+ symbol: string;
185
+ /** Decimals of the sub-vault share token */
186
+ decimals: number;
187
+ /** Shares of the sub-vault held by the curator vault */
188
+ sharesBalance: bigint;
189
+ /** Current value of the shares in terms of the sub-vault's underlying asset */
190
+ underlyingValue: bigint;
191
+ /** Underlying asset address of the sub-vault */
192
+ underlyingAsset: string;
193
+ /** Symbol of the sub-vault's underlying asset */
194
+ underlyingSymbol: string;
195
+ /** Decimals of the sub-vault's underlying asset */
196
+ underlyingDecimals: number;
197
+ }
198
+
199
+ /**
200
+ * Metadata and capability snapshot for a potential sub-vault investment target.
201
+ */
202
+ export interface SubVaultInfo {
203
+ /** Sub-vault contract address */
204
+ address: string;
205
+ /** Protocol type: ERC4626 (sync) or ERC7540 (async) */
206
+ type: "erc4626" | "erc7540";
207
+ /** Sub-vault share token name */
208
+ name: string;
209
+ /** Sub-vault share token symbol */
210
+ symbol: string;
211
+ /** Sub-vault share token decimals */
212
+ decimals: number;
213
+ /** Underlying asset address */
214
+ underlyingAsset: string;
215
+ /** Underlying asset symbol */
216
+ underlyingSymbol: string;
217
+ /** Underlying asset decimals */
218
+ underlyingDecimals: number;
219
+ /** Maximum amount the curator vault can deposit (from maxDeposit(vault)) */
220
+ maxDeposit: bigint;
221
+ /** Whether the sub-vault is whitelisted in the global MoreVaults registry */
222
+ isWhitelisted: boolean;
223
+ }
224
+
225
+ /**
226
+ * Status of pending and claimable ERC7540 async requests for a sub-vault.
227
+ * Uses requestId = 0 (the standard default for non-batch ERC7540 vaults).
228
+ */
229
+ export interface ERC7540RequestStatus {
230
+ /** Sub-vault address these statuses belong to */
231
+ subVault: string;
232
+ /** Assets in a pending deposit request (not yet claimable) */
233
+ pendingDeposit: bigint;
234
+ /** Assets ready to be claimed/finalized as shares */
235
+ claimableDeposit: bigint;
236
+ /** Shares in a pending redeem request (not yet claimable) */
237
+ pendingRedeem: bigint;
238
+ /** Assets ready to be claimed after redeem fulfillment */
239
+ claimableRedeem: bigint;
240
+ /** True if claimableDeposit > 0 — vault can call erc7540Deposit to finalize */
241
+ canFinalizeDeposit: boolean;
242
+ /** True if claimableRedeem > 0 — vault can call erc7540Redeem to finalize */
243
+ canFinalizeRedeem: boolean;
244
+ }
245
+
246
+ /**
247
+ * Full portfolio view for a curator vault combining liquid and invested assets.
248
+ */
249
+ export interface VaultPortfolio {
250
+ /** Liquid ERC20 asset balances held directly by the vault (excludes sub-vault share tokens) */
251
+ liquidAssets: AssetBalance[];
252
+ /** Active positions in ERC4626/ERC7540 sub-vaults */
253
+ subVaultPositions: SubVaultPosition[];
254
+ /**
255
+ * Approximate total value in underlying terms:
256
+ * liquid underlying balance + sum of sub-vault convertToAssets values.
257
+ */
258
+ totalValue: bigint;
259
+ /** totalAssets() from the vault — authoritative AUM figure */
260
+ totalAssets: bigint;
261
+ /** totalSupply() of vault shares */
262
+ totalSupply: bigint;
263
+ /** Assets locked in pending ERC7540 requests (lockedTokensAmountOfAsset) */
264
+ lockedAssets: bigint;
265
+ }
266
+
168
267
  export type { Signer, Provider, ContractTransactionReceipt };
@@ -62,3 +62,17 @@ export type { CuratorAction } from './useSubmitActions.js'
62
62
  export { useExecuteActions } from './useExecuteActions.js'
63
63
 
64
64
  export { useVetoActions } from './useVetoActions.js'
65
+
66
+ // --- Curator Bridge hooks ---
67
+ export { useCuratorBridgeQuote } from './useCuratorBridgeQuote.js'
68
+ export { useExecuteBridge } from './useExecuteBridge.js'
69
+
70
+ // --- Curator Sub-Vault hooks (Phase 5) ---
71
+ export { useSubVaultPositions } from './useSubVaultPositions.js'
72
+ export type { SubVaultPosition } from './useSubVaultPositions.js'
73
+
74
+ export { useVaultPortfolio } from './useVaultPortfolio.js'
75
+ export type { VaultPortfolio } from './useVaultPortfolio.js'
76
+
77
+ export { useERC7540RequestStatus } from './useERC7540RequestStatus.js'
78
+ export type { ERC7540RequestStatus } from './useERC7540RequestStatus.js'
@@ -0,0 +1,43 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { usePublicClient } from 'wagmi'
3
+ import { asSdkClient, quoteCuratorBridgeFee } from '../viem/index.js'
4
+ import type { CuratorBridgeParams } from '../viem/index.js'
5
+
6
+ export type { CuratorBridgeParams }
7
+
8
+ /**
9
+ * Quote the native fee required to bridge assets from the hub vault via LzAdapter.
10
+ *
11
+ * Refreshes every 60s — bridge fees fluctuate with LayerZero network demand.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * const { fee, isLoading } = useCuratorBridgeQuote('0xVAULT', 8453, {
16
+ * oftToken: '0x27a16dc786820B16E5c9028b75B99F6f604b5d26',
17
+ * dstEid: 30101,
18
+ * amount: 1_000_000n,
19
+ * dstVault: '0xSpokeVault...',
20
+ * refundAddress: '0xCurator...',
21
+ * })
22
+ * ```
23
+ */
24
+ export function useCuratorBridgeQuote(
25
+ vault: `0x${string}` | undefined,
26
+ chainId: number,
27
+ params: CuratorBridgeParams | undefined,
28
+ ) {
29
+ const publicClient = usePublicClient({ chainId })
30
+
31
+ const query = useQuery({
32
+ queryKey: ['curatorBridgeQuote', vault, chainId, params],
33
+ queryFn: () => quoteCuratorBridgeFee(asSdkClient(publicClient), vault!, params!),
34
+ enabled: !!vault && !!publicClient && !!params,
35
+ refetchInterval: 60_000,
36
+ staleTime: 30_000,
37
+ })
38
+
39
+ return {
40
+ ...query,
41
+ fee: query.data,
42
+ }
43
+ }