@oydual31/more-vaults-sdk 0.3.3 → 0.4.1

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 (42) hide show
  1. package/dist/ethers/index.cjs +1794 -315
  2. package/dist/ethers/index.cjs.map +1 -1
  3. package/dist/ethers/index.d.cts +1147 -1
  4. package/dist/ethers/index.d.ts +1147 -1
  5. package/dist/ethers/index.js +1752 -317
  6. package/dist/ethers/index.js.map +1 -1
  7. package/dist/react/index.cjs +644 -0
  8. package/dist/react/index.cjs.map +1 -1
  9. package/dist/react/index.d.cts +111 -2
  10. package/dist/react/index.d.ts +111 -2
  11. package/dist/react/index.js +638 -3
  12. package/dist/react/index.js.map +1 -1
  13. package/dist/{spokeRoutes-BIafSbQ3.d.cts → spokeRoutes-B8Lnk-t4.d.cts} +191 -2
  14. package/dist/{spokeRoutes-BIafSbQ3.d.ts → spokeRoutes-B8Lnk-t4.d.ts} +191 -2
  15. package/dist/viem/index.d.cts +4 -192
  16. package/dist/viem/index.d.ts +4 -192
  17. package/package.json +1 -1
  18. package/src/ethers/abis.ts +92 -0
  19. package/src/ethers/chains.ts +191 -0
  20. package/src/ethers/crossChainFlows.ts +208 -0
  21. package/src/ethers/curatorMulticall.ts +195 -0
  22. package/src/ethers/curatorStatus.ts +319 -0
  23. package/src/ethers/curatorSwaps.ts +192 -0
  24. package/src/ethers/distribution.ts +156 -0
  25. package/src/ethers/index.ts +96 -1
  26. package/src/ethers/preflight.ts +225 -1
  27. package/src/ethers/redeemFlows.ts +160 -1
  28. package/src/ethers/spokeRoutes.ts +361 -0
  29. package/src/ethers/topology.ts +240 -0
  30. package/src/ethers/types.ts +95 -0
  31. package/src/ethers/userHelpers.ts +193 -0
  32. package/src/ethers/utils.ts +28 -0
  33. package/src/react/index.ts +25 -0
  34. package/src/react/useCuratorVaultStatus.ts +32 -0
  35. package/src/react/useExecuteActions.ts +23 -0
  36. package/src/react/useIsCurator.ts +30 -0
  37. package/src/react/usePendingActions.ts +33 -0
  38. package/src/react/useProtocolWhitelist.ts +30 -0
  39. package/src/react/useSubmitActions.ts +27 -0
  40. package/src/react/useVaultAnalysis.ts +32 -0
  41. package/src/react/useVaultAssetBreakdown.ts +32 -0
  42. package/src/react/useVetoActions.ts +23 -0
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Spoke route helpers for the MoreVaults ethers.js v6 SDK.
3
+ *
4
+ * Provides functions to discover inbound/outbound cross-chain deposit and
5
+ * redemption routes, and to quote LayerZero fees for those routes.
6
+ */
7
+
8
+ import { Contract, ZeroAddress } from "ethers";
9
+ import type { Provider } from "ethers";
10
+ import { OFT_ROUTES, CHAIN_ID_TO_EID, createChainProvider } from "./chains";
11
+ import { OFT_ABI, ERC20_ABI } from "./abis";
12
+ import { isAsyncMode, quoteLzFee } from "./utils";
13
+ import { getVaultTopology } from "./topology";
14
+
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+
17
+ export interface OutboundRoute {
18
+ /** Chain ID where user can receive shares/assets */
19
+ chainId: number;
20
+ /** Whether this chain is the hub (direct redeem) or a spoke (shares bridged back) */
21
+ routeType: "hub" | "spoke";
22
+ /** LZ EID for this chain */
23
+ eid: number;
24
+ /** Native gas symbol */
25
+ nativeSymbol: string;
26
+ }
27
+
28
+ export interface InboundRoute {
29
+ /** Internal route identifier from OFT_ROUTES (e.g. 'stgUSDC') — do NOT show to users */
30
+ symbol: string;
31
+ /** Chain ID where user sends from */
32
+ spokeChainId: number;
33
+ /**
34
+ * How the deposit is executed:
35
+ * - 'direct' → user is on the hub chain, vault uses standard ERC-4626 (depositSimple). No LZ fee.
36
+ * - 'direct-async' → user is on the hub chain, vault uses async accounting (depositAsync). LZ fee required.
37
+ * - 'oft-compose' → user is on a spoke chain, use depositFromSpoke via OFT compose. LZ fee required.
38
+ */
39
+ depositType: "direct" | "direct-async" | "oft-compose";
40
+ /** OFT contract on spoke chain — pass as `spokeOFT` to depositFromSpoke. Null for direct deposits. */
41
+ spokeOft: string | null;
42
+ /** Token user must approve on spoke chain (ZeroAddress = native ETH) */
43
+ spokeToken: string;
44
+ /**
45
+ * Human-readable symbol of the token the user needs to hold on the spoke chain.
46
+ * For OFTAdapters this is the underlying token symbol (e.g. 'USDC', 'weETH').
47
+ * For pure OFTs this is the OFT's own symbol (e.g. 'sUSDe', 'USDe').
48
+ * Use this — not `symbol` — when displaying the token name to users.
49
+ */
50
+ sourceTokenSymbol: string;
51
+ /** OFT contract on hub chain — receives tokens for the composer. Null for direct deposits. */
52
+ hubOft: string | null;
53
+ /** oftCmd to use in SendParam (0x01 for Stargate taxi, 0x for standard OFT) */
54
+ oftCmd: string;
55
+ /** LZ fee estimate in native wei of the SPOKE chain (not always ETH — e.g. FLOW on Flow EVM) */
56
+ lzFeeEstimate: bigint;
57
+ /** Native gas token symbol for the spoke chain — use this when displaying the fee */
58
+ nativeSymbol: string;
59
+ }
60
+
61
+ export interface InboundRouteWithBalance extends InboundRoute {
62
+ /** User's token balance on the spoke chain */
63
+ userBalance: bigint;
64
+ }
65
+
66
+ // ─────────────────────────────────────────────────────────────────────────────
67
+
68
+ /** Native gas token symbol per chain ID — lzFeeEstimate is denominated in this token */
69
+ export const NATIVE_SYMBOL: Partial<Record<number, string>> = {
70
+ 1: "ETH",
71
+ 10: "ETH",
72
+ 42161: "ETH",
73
+ 8453: "ETH",
74
+ 747: "FLOW",
75
+ 146: "S",
76
+ 56: "BNB",
77
+ };
78
+
79
+ // ─────────────────────────────────────────────────────────────────────────────
80
+
81
+ const SYMBOL_ABI = [
82
+ "function symbol() view returns (string)",
83
+ ] as const;
84
+
85
+ /** Read ERC20 symbol() on-chain. Falls back to `fallbackSymbol` if the call fails. */
86
+ async function readTokenSymbol(
87
+ provider: Provider | null,
88
+ token: string,
89
+ fallbackSymbol: string,
90
+ ): Promise<string> {
91
+ if (!provider) return fallbackSymbol;
92
+ try {
93
+ const contract = new Contract(token, SYMBOL_ABI, provider);
94
+ return await (contract.symbol() as Promise<string>);
95
+ } catch {
96
+ return fallbackSymbol;
97
+ }
98
+ }
99
+
100
+ // ─────────────────────────────────────────────────────────────────────────────
101
+
102
+ /**
103
+ * Find all valid OFT inbound routes for a vault.
104
+ *
105
+ * Only returns routes for chains where the vault has a registered spoke —
106
+ * this is required so the composer can send shares back to the user's chain.
107
+ * The hub chain is always included as a 'direct' deposit option.
108
+ *
109
+ * Routes that revert on quoteSend() (no liquidity, no peer) are excluded.
110
+ *
111
+ * @param hubChainId Chain ID of the vault hub (e.g. 8453 for Base)
112
+ * @param vault Vault address (to resolve registered spoke chains)
113
+ * @param vaultAsset vault.asset() address on the hub chain
114
+ * @param userAddress User address (used as receiver for fee quote)
115
+ */
116
+ export async function getInboundRoutes(
117
+ hubChainId: number,
118
+ vault: string,
119
+ vaultAsset: string,
120
+ userAddress: string,
121
+ ): Promise<InboundRoute[]> {
122
+ const hubEid = CHAIN_ID_TO_EID[hubChainId];
123
+ if (!hubEid) throw new Error(`No LZ EID for hub chainId ${hubChainId}`);
124
+
125
+ // Fetch vault topology to get registered spoke chains
126
+ const hubProvider = createChainProvider(hubChainId);
127
+ if (!hubProvider) throw new Error(`No public RPC for hub chainId ${hubChainId}`);
128
+ const topology = await getVaultTopology(hubProvider, vault);
129
+ const registeredSpokes = new Set(topology.spokeChainIds);
130
+
131
+ const results: InboundRoute[] = [];
132
+
133
+ const vaultAssetNorm = vaultAsset.toLowerCase();
134
+
135
+ for (const [symbol, chainMap] of Object.entries(OFT_ROUTES)) {
136
+ const hubEntry = (chainMap as Record<number, { oft: string; token: string }>)[hubChainId];
137
+ if (!hubEntry) continue;
138
+
139
+ // Does this OFT deliver the right asset to the hub?
140
+ if (hubEntry.token.toLowerCase() !== vaultAssetNorm) continue;
141
+
142
+ // oftCmd for OFT compose deposits: always '0x' (TAXI mode = immediate delivery with composeMsg).
143
+ const oftCmd = "0x";
144
+
145
+ // Only check chains where the vault has a registered spoke
146
+ const spokesToCheck = Object.keys(chainMap)
147
+ .map(Number)
148
+ .filter((id) => id !== hubChainId && registeredSpokes.has(id));
149
+
150
+ await Promise.allSettled(
151
+ spokesToCheck.map(async (spokeChainId) => {
152
+ const spokeEntry = (chainMap as Record<number, { oft: string; token: string }>)[spokeChainId];
153
+ if (!spokeEntry) return;
154
+
155
+ const spokeProvider = createChainProvider(spokeChainId);
156
+ if (!spokeProvider) return;
157
+
158
+ // Validate route via quoteSend — if it reverts, skip
159
+ try {
160
+ const receiverBytes32 =
161
+ "0x" + userAddress.replace(/^0x/, "").toLowerCase().padStart(64, "0");
162
+
163
+ const spokeOft = new Contract(spokeEntry.oft, OFT_ABI, spokeProvider);
164
+ const [feeResult, sourceTokenSymbol] = await Promise.all([
165
+ spokeOft.quoteSend(
166
+ {
167
+ dstEid: hubEid,
168
+ to: receiverBytes32,
169
+ amountLD: 1_000_000n,
170
+ minAmountLD: 0n,
171
+ extraOptions: "0x",
172
+ composeMsg: "0x",
173
+ oftCmd,
174
+ },
175
+ false,
176
+ ),
177
+ readTokenSymbol(spokeProvider, spokeEntry.token, symbol),
178
+ ]);
179
+
180
+ results.push({
181
+ symbol,
182
+ spokeChainId,
183
+ depositType: "oft-compose",
184
+ spokeOft: spokeEntry.oft,
185
+ spokeToken: spokeEntry.token,
186
+ sourceTokenSymbol,
187
+ hubOft: hubEntry.oft,
188
+ oftCmd,
189
+ lzFeeEstimate: feeResult.nativeFee as bigint,
190
+ nativeSymbol: NATIVE_SYMBOL[spokeChainId] ?? "ETH",
191
+ });
192
+ } catch {
193
+ // Route not available — skip silently
194
+ }
195
+ }),
196
+ );
197
+ }
198
+
199
+ // Add the hub chain itself as a deposit option.
200
+ // For async vaults the vault uses depositAsync which requires a LZ fee even on the hub chain.
201
+ const [asyncMode, ...hubOftEntries] = await Promise.all([
202
+ isAsyncMode(hubProvider, vault),
203
+ ...Object.entries(OFT_ROUTES).map(async ([sym, chainMap]) => {
204
+ const hubEntry = (chainMap as Record<number, { oft: string; token: string }>)[hubChainId];
205
+ if (!hubEntry || hubEntry.token.toLowerCase() !== vaultAssetNorm) return null;
206
+ return { symbol: sym, hubEntry };
207
+ }),
208
+ ]);
209
+
210
+ const hubOftEntry = hubOftEntries.find((e) => e !== null) ?? null;
211
+
212
+ if (hubOftEntry) {
213
+ const { symbol, hubEntry } = hubOftEntry as { symbol: string; hubEntry: { oft: string; token: string } };
214
+ const [sourceTokenSymbol, lzFeeEstimate] = await Promise.all([
215
+ readTokenSymbol(hubProvider, hubEntry.token, symbol),
216
+ asyncMode ? quoteLzFee(hubProvider, vault) : Promise.resolve(0n),
217
+ ]);
218
+ results.unshift({
219
+ symbol,
220
+ spokeChainId: hubChainId,
221
+ depositType: asyncMode ? "direct-async" : "direct",
222
+ spokeOft: null,
223
+ spokeToken: hubEntry.token,
224
+ sourceTokenSymbol,
225
+ hubOft: null,
226
+ oftCmd: "0x",
227
+ lzFeeEstimate,
228
+ nativeSymbol: NATIVE_SYMBOL[hubChainId] ?? "ETH",
229
+ });
230
+ }
231
+
232
+ return results;
233
+ }
234
+
235
+ /**
236
+ * Fetch user token balances for each inbound route in parallel.
237
+ * Routes with native ETH as token (ZeroAddress) return the chain's ETH balance.
238
+ *
239
+ * @param routes Inbound routes from getInboundRoutes()
240
+ * @param userAddress User wallet address
241
+ */
242
+ export async function getUserBalancesForRoutes(
243
+ routes: InboundRoute[],
244
+ userAddress: string,
245
+ ): Promise<InboundRouteWithBalance[]> {
246
+ return Promise.all(
247
+ routes.map(async (route) => {
248
+ const provider = createChainProvider(route.spokeChainId);
249
+ if (!provider) return { ...route, userBalance: 0n };
250
+
251
+ try {
252
+ let userBalance: bigint;
253
+
254
+ if (route.spokeToken.toLowerCase() === ZeroAddress.toLowerCase()) {
255
+ userBalance = await provider.getBalance(userAddress);
256
+ } else {
257
+ const erc20 = new Contract(route.spokeToken, ERC20_ABI, provider);
258
+ userBalance = await (erc20.balanceOf(userAddress) as Promise<bigint>);
259
+ }
260
+
261
+ return { ...route, userBalance };
262
+ } catch {
263
+ return { ...route, userBalance: 0n };
264
+ }
265
+ }),
266
+ );
267
+ }
268
+
269
+ /**
270
+ * Find all outbound routes for a vault — chains where a user can receive
271
+ * shares/assets when redeeming.
272
+ *
273
+ * The hub chain is always first (direct redeem). Spoke chains follow
274
+ * (shares are bridged back via the composer).
275
+ *
276
+ * @param hubChainId Chain ID of the vault hub (e.g. 8453 for Base)
277
+ * @param vault Vault address (to resolve registered spoke chains)
278
+ */
279
+ export async function getOutboundRoutes(
280
+ hubChainId: number,
281
+ vault: string,
282
+ ): Promise<OutboundRoute[]> {
283
+ const hubEid = CHAIN_ID_TO_EID[hubChainId];
284
+ if (!hubEid) throw new Error(`No LZ EID for hub chainId ${hubChainId}`);
285
+
286
+ const hubProvider = createChainProvider(hubChainId);
287
+ if (!hubProvider) throw new Error(`No public RPC for hub chainId ${hubChainId}`);
288
+
289
+ const topology = await getVaultTopology(hubProvider, vault);
290
+
291
+ const routes: OutboundRoute[] = [
292
+ {
293
+ chainId: hubChainId,
294
+ routeType: "hub",
295
+ eid: hubEid,
296
+ nativeSymbol: NATIVE_SYMBOL[hubChainId] ?? "ETH",
297
+ },
298
+ ];
299
+
300
+ for (const spokeChainId of topology.spokeChainIds) {
301
+ const eid = CHAIN_ID_TO_EID[spokeChainId];
302
+ if (!eid) continue;
303
+
304
+ routes.push({
305
+ chainId: spokeChainId,
306
+ routeType: "spoke",
307
+ eid,
308
+ nativeSymbol: NATIVE_SYMBOL[spokeChainId] ?? "ETH",
309
+ });
310
+ }
311
+
312
+ return routes;
313
+ }
314
+
315
+ /**
316
+ * Quote the LayerZero native fee for a cross-chain deposit with a real amount.
317
+ *
318
+ * More precise than the `lzFeeEstimate` field on `InboundRoute`, which uses
319
+ * a dummy 1 USDC amount.
320
+ *
321
+ * @param route An InboundRoute from `getInboundRoutes()`
322
+ * @param hubChainId Chain ID of the vault hub (needed for LZ destination EID)
323
+ * @param amount Real deposit amount in token decimals
324
+ * @param userAddress User address (used as receiver for fee quote)
325
+ * @returns Native fee in wei of the spoke chain's gas token, or 0n for direct deposits
326
+ */
327
+ export async function quoteRouteDepositFee(
328
+ route: InboundRoute,
329
+ hubChainId: number,
330
+ amount: bigint,
331
+ userAddress: string,
332
+ ): Promise<bigint> {
333
+ if (route.depositType === "direct") return 0n;
334
+
335
+ const hubEid = CHAIN_ID_TO_EID[hubChainId];
336
+ if (!hubEid) throw new Error(`No LZ EID for hub chainId ${hubChainId}`);
337
+
338
+ if (!route.spokeOft) throw new Error("Route is oft-compose but spokeOft is null");
339
+
340
+ const spokeProvider = createChainProvider(route.spokeChainId);
341
+ if (!spokeProvider) throw new Error(`No public RPC for spoke chainId ${route.spokeChainId}`);
342
+
343
+ const receiverBytes32 =
344
+ "0x" + userAddress.replace(/^0x/, "").toLowerCase().padStart(64, "0");
345
+
346
+ const spokeOft = new Contract(route.spokeOft, OFT_ABI, spokeProvider);
347
+ const feeResult = await spokeOft.quoteSend(
348
+ {
349
+ dstEid: hubEid,
350
+ to: receiverBytes32,
351
+ amountLD: amount,
352
+ minAmountLD: 0n,
353
+ extraOptions: "0x",
354
+ composeMsg: "0x",
355
+ oftCmd: route.oftCmd,
356
+ },
357
+ false,
358
+ );
359
+
360
+ return feeResult.nativeFee as bigint;
361
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Vault topology helpers for the MoreVaults ethers.js v6 SDK.
3
+ *
4
+ * Resolves the cross-chain hub/spoke structure of a vault using the
5
+ * MoreVaults OmniFactory contract (same address on every supported chain via CREATE3).
6
+ */
7
+
8
+ import { Contract } from "ethers";
9
+ import type { Provider } from "ethers";
10
+ import { EID_TO_CHAIN_ID, CHAIN_IDS, createChainProvider } from "./chains";
11
+
12
+ // MoreVaults OMNI factory — same address on every supported chain (CREATE3)
13
+ export const OMNI_FACTORY_ADDRESS = "0x7bDB8B17604b03125eFAED33cA0c55FBf856BB0C";
14
+
15
+ const FACTORY_ABI = [
16
+ "function localEid() view returns (uint32)",
17
+ "function isCrossChainVault(uint32 __eid, address _vault) view returns (bool)",
18
+ "function hubToSpokes(uint32 __eid, address _hubVault) view returns (uint32[] eids, address[] vaults)",
19
+ "function spokeToHub(uint32 __eid, address _spokeVault) view returns (uint32 eid, address vault)",
20
+ ] as const;
21
+
22
+ export interface VaultTopology {
23
+ /**
24
+ * Role of this vault on the chain you queried:
25
+ * - 'hub' → this chain holds the TVL, users deposit here
26
+ * - 'spoke' → this chain is a yield deployment; deposits go to the hub
27
+ * - 'local' → single-chain vault, no cross-chain setup
28
+ */
29
+ role: "hub" | "spoke" | "local";
30
+
31
+ /** Chain ID where the hub lives. Same as the queried chain when role='hub'. */
32
+ hubChainId: number;
33
+
34
+ /**
35
+ * All spoke chain IDs registered under this hub.
36
+ * Empty when role='local'.
37
+ * Since vaults are CREATE3-deployed, the vault address is the same on all chains.
38
+ */
39
+ spokeChainIds: number[];
40
+ }
41
+
42
+ /** All mainnet chain IDs where the OMNI_FACTORY is deployed */
43
+ const DISCOVERY_CHAIN_IDS = Object.values(CHAIN_IDS).filter(
44
+ (id) => id !== 545, // exclude testnet
45
+ ) as number[];
46
+
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Resolve the cross-chain topology of a vault: hub chain + all spoke chains.
51
+ *
52
+ * Works for hub vaults, spoke vaults, and local (single-chain) vaults.
53
+ * Because MoreVaults uses CREATE3, the vault address is identical on every chain —
54
+ * only the chain IDs differ.
55
+ *
56
+ * @param provider Connected to the chain you want to inspect
57
+ * @param vault Vault address (same on all chains)
58
+ * @param factoryAddress MoreVaults factory (defaults to OMNI_FACTORY_ADDRESS)
59
+ *
60
+ * @example
61
+ * // Querying from Base — will detect hub + Ethereum/Arbitrum spokes
62
+ * const topo = await getVaultTopology(baseProvider, '0x8f740...')
63
+ * // { role: 'hub', hubChainId: 8453, spokeChainIds: [1, 42161] }
64
+ *
65
+ * // Querying from Ethereum — same vault is a spoke there
66
+ * const topo = await getVaultTopology(ethProvider, '0x8f740...')
67
+ * // { role: 'spoke', hubChainId: 8453, spokeChainIds: [1, 42161] }
68
+ */
69
+ export async function getVaultTopology(
70
+ provider: Provider,
71
+ vault: string,
72
+ factoryAddress: string = OMNI_FACTORY_ADDRESS,
73
+ ): Promise<VaultTopology> {
74
+ const factory = new Contract(factoryAddress, FACTORY_ABI, provider);
75
+
76
+ // Get local EID from the factory on the queried chain
77
+ const localEid: number = Number(await factory.localEid());
78
+
79
+ // Check if this vault is a hub on the current chain
80
+ const isHub: boolean = await factory.isCrossChainVault(localEid, vault);
81
+
82
+ if (isHub) {
83
+ // Hub: get all registered spokes
84
+ const result = await factory.hubToSpokes(localEid, vault);
85
+ const spokeEids: bigint[] = result[0];
86
+
87
+ const localChainId = EID_TO_CHAIN_ID[localEid] ?? 0;
88
+ const spokeChainIds = spokeEids
89
+ .map((eid) => EID_TO_CHAIN_ID[Number(eid)])
90
+ .filter((id): id is number => id !== undefined);
91
+
92
+ return { role: "hub", hubChainId: localChainId, spokeChainIds };
93
+ }
94
+
95
+ // Check if this vault is a spoke on the current chain
96
+ const spokeResult = await factory.spokeToHub(localEid, vault);
97
+ const hubEid: number = Number(spokeResult[0]);
98
+ const hubVault: string = spokeResult[1];
99
+
100
+ if (hubEid !== 0 && hubVault !== "0x0000000000000000000000000000000000000000") {
101
+ // Spoke: resolve hub chain
102
+ const hubChainId = EID_TO_CHAIN_ID[hubEid] ?? 0;
103
+
104
+ // We only have the current chain's provider — return what we know.
105
+ // The hub's full spoke list requires a separate provider for the hub chain.
106
+ const spokeChainIds: number[] = [];
107
+
108
+ // If we happen to know the local chain mapping, include it as a spoke
109
+ const localChainId = EID_TO_CHAIN_ID[localEid];
110
+ if (localChainId !== undefined) spokeChainIds.push(localChainId);
111
+
112
+ return { role: "spoke", hubChainId, spokeChainIds };
113
+ }
114
+
115
+ // Local vault — no cross-chain setup
116
+ const localChainId = EID_TO_CHAIN_ID[localEid] ?? 0;
117
+ return { role: "local", hubChainId: localChainId, spokeChainIds: [] };
118
+ }
119
+
120
+ /**
121
+ * Resolve the FULL topology of a vault by querying the hub chain directly.
122
+ *
123
+ * Provide a provider connected to the hub chain for complete spoke data.
124
+ * If you don't know which chain is the hub, call `getVaultTopology` first
125
+ * from any chain and use the returned `hubChainId` to create the hub provider.
126
+ *
127
+ * @param hubChainProvider Provider connected to the hub chain
128
+ * @param vault Vault address
129
+ * @param factoryAddress MoreVaults factory (defaults to OMNI_FACTORY_ADDRESS)
130
+ */
131
+ export async function getFullVaultTopology(
132
+ hubChainProvider: Provider,
133
+ vault: string,
134
+ factoryAddress: string = OMNI_FACTORY_ADDRESS,
135
+ ): Promise<VaultTopology> {
136
+ const topo = await getVaultTopology(hubChainProvider, vault, factoryAddress);
137
+ if (topo.role !== "hub") {
138
+ throw new Error(
139
+ `getFullVaultTopology: provider must be connected to the hub chain (${topo.hubChainId}), ` +
140
+ `but got role="${topo.role}". Connect to chainId ${topo.hubChainId} instead.`,
141
+ );
142
+ }
143
+ return topo;
144
+ }
145
+
146
+ /**
147
+ * Discover a vault's topology across all supported chains.
148
+ *
149
+ * Unlike `getVaultTopology` (which queries a single chain), this function
150
+ * automatically iterates all supported chains when the initial query returns
151
+ * `role: "local"`. This handles the case where the caller doesn't know which
152
+ * chain the vault is deployed on, or when no provider is connected.
153
+ *
154
+ * If a `provider` is provided, it's tried first. If that returns "local",
155
+ * every other supported chain is probed via public RPCs.
156
+ * If no `provider` is provided, all chains are probed.
157
+ *
158
+ * Once a hub is found, `getFullVaultTopology` is called to get the complete
159
+ * spoke list.
160
+ *
161
+ * @param vault Vault address (same on all chains via CREATE3)
162
+ * @param provider Optional — provider for the "preferred" chain to try first
163
+ * @param factoryAddress MoreVaults factory (defaults to OMNI_FACTORY_ADDRESS)
164
+ *
165
+ * @example
166
+ * // No wallet connected — discovers that 0x8f74... is hub on Base
167
+ * const topo = await discoverVaultTopology('0x8f740...')
168
+ * // { role: 'hub', hubChainId: 8453, spokeChainIds: [1, 42161] }
169
+ */
170
+ export async function discoverVaultTopology(
171
+ vault: string,
172
+ provider?: Provider | null,
173
+ factoryAddress: string = OMNI_FACTORY_ADDRESS,
174
+ ): Promise<VaultTopology> {
175
+ // 1. Try the provided provider first (fast path — avoids extra RPC calls)
176
+ let triedChainId: number | undefined;
177
+ if (provider) {
178
+ try {
179
+ const topo = await getVaultTopology(provider, vault, factoryAddress);
180
+ if (topo.role !== "local") {
181
+ // Found hub or spoke — if spoke, resolve full topology from hub
182
+ if (topo.role === "spoke") {
183
+ const hubProvider = createChainProvider(topo.hubChainId);
184
+ if (hubProvider) {
185
+ try {
186
+ return await getFullVaultTopology(hubProvider, vault, factoryAddress);
187
+ } catch { /* fall through to return partial */ }
188
+ }
189
+ }
190
+ return topo;
191
+ }
192
+ // Determine which chainId we just tried
193
+ const network = await provider.getNetwork();
194
+ triedChainId = Number(network.chainId);
195
+ } catch { /* provider failed — continue with discovery */ }
196
+ }
197
+
198
+ // 2. Iterate all supported chains
199
+ for (const chainId of DISCOVERY_CHAIN_IDS) {
200
+ if (chainId === triedChainId) continue;
201
+ const chainProvider = createChainProvider(chainId);
202
+ if (!chainProvider) continue;
203
+
204
+ try {
205
+ const topo = await getVaultTopology(chainProvider, vault, factoryAddress);
206
+ if (topo.role === "hub") return topo;
207
+ if (topo.role === "spoke") {
208
+ // Found spoke — get full topology from hub
209
+ const hubProvider = createChainProvider(topo.hubChainId);
210
+ if (hubProvider) {
211
+ try {
212
+ return await getFullVaultTopology(hubProvider, vault, factoryAddress);
213
+ } catch { return topo; }
214
+ }
215
+ return topo;
216
+ }
217
+ } catch { /* this chain doesn't have the factory or vault — skip */ }
218
+ }
219
+
220
+ // 3. Not found on any chain — return local with chainId 0
221
+ return { role: "local", hubChainId: 0, spokeChainIds: [] };
222
+ }
223
+
224
+ /**
225
+ * Check if a wallet is connected to the hub chain for a given vault.
226
+ * Useful for showing a "Switch to Base" prompt before deposit.
227
+ *
228
+ * @param currentChainId Chain ID the wallet is currently connected to
229
+ * @param topology Result of getVaultTopology
230
+ */
231
+ export function isOnHubChain(currentChainId: number, topology: VaultTopology): boolean {
232
+ return currentChainId === topology.hubChainId;
233
+ }
234
+
235
+ /**
236
+ * Get all chain IDs where this vault is deployed (hub + all spokes).
237
+ */
238
+ export function getAllVaultChainIds(topology: VaultTopology): number[] {
239
+ return [topology.hubChainId, ...topology.spokeChainIds];
240
+ }
@@ -70,4 +70,99 @@ export interface CrossChainRequestInfo {
70
70
  amountLimit: bigint;
71
71
  }
72
72
 
73
+ // ─────────────────────────────────────────────────────────────────────────────
74
+ // Curator Operations Types
75
+ // ─────────────────────────────────────────────────────────────────────────────
76
+
77
+ export interface SwapParams {
78
+ targetContract: string;
79
+ tokenIn: string;
80
+ tokenOut: string;
81
+ maxAmountIn: bigint;
82
+ minAmountOut: bigint;
83
+ swapCallData: string;
84
+ }
85
+
86
+ export interface BatchSwapParams {
87
+ swaps: SwapParams[];
88
+ }
89
+
90
+ export interface BridgeParams {
91
+ oftToken: string;
92
+ dstEid: number;
93
+ amount: bigint;
94
+ dstVault: string;
95
+ refundAddress: string;
96
+ }
97
+
98
+ export interface PendingAction {
99
+ nonce: bigint;
100
+ actionsData: string[];
101
+ pendingUntil: bigint;
102
+ isExecutable: boolean;
103
+ }
104
+
105
+ export interface SubmitActionsResult {
106
+ receipt: ContractTransactionReceipt;
107
+ nonce: bigint;
108
+ }
109
+
110
+ export type CuratorAction =
111
+ | { type: 'swap'; params: SwapParams }
112
+ | { type: 'batchSwap'; params: BatchSwapParams }
113
+ | { type: 'erc4626Deposit'; vault: string; assets: bigint }
114
+ | { type: 'erc4626Redeem'; vault: string; shares: bigint }
115
+ | { type: 'erc7540RequestDeposit'; vault: string; assets: bigint }
116
+ | { type: 'erc7540Deposit'; vault: string; assets: bigint }
117
+ | { type: 'erc7540RequestRedeem'; vault: string; shares: bigint }
118
+ | { type: 'erc7540Redeem'; vault: string; shares: bigint };
119
+
120
+ export interface CuratorVaultStatus {
121
+ curator: string;
122
+ timeLockPeriod: bigint;
123
+ maxSlippagePercent: bigint;
124
+ currentNonce: bigint;
125
+ availableAssets: string[];
126
+ lzAdapter: string;
127
+ paused: boolean;
128
+ }
129
+
130
+ // ─────────────────────────────────────────────────────────────────────────────
131
+ // Vault Analysis Types
132
+ // ─────────────────────────────────────────────────────────────────────────────
133
+
134
+ export interface AssetInfo {
135
+ address: string;
136
+ symbol: string;
137
+ name: string;
138
+ decimals: number;
139
+ }
140
+
141
+ export interface VaultAnalysis {
142
+ /** All tokens the vault can hold/swap (curator-managed) */
143
+ availableAssets: AssetInfo[];
144
+ /** Tokens users can deposit */
145
+ depositableAssets: AssetInfo[];
146
+ /** Whether deposit whitelist is enabled (restricts who can deposit) */
147
+ depositWhitelistEnabled: boolean;
148
+ /** Registry address for global protocol whitelist checks */
149
+ registryAddress: string | null;
150
+ }
151
+
152
+ export interface AssetBalance extends AssetInfo {
153
+ /** Raw balance held by the vault */
154
+ balance: bigint;
155
+ }
156
+
157
+ export interface VaultAssetBreakdown {
158
+ /** Per-asset balances held by the vault on the hub chain */
159
+ assets: AssetBalance[];
160
+ /** totalAssets() as reported by the vault (all positions converted to underlying) */
161
+ totalAssets: bigint;
162
+ /** totalSupply() of vault shares */
163
+ totalSupply: bigint;
164
+ /** Vault underlying token decimals */
165
+ underlyingDecimals: number;
166
+ }
167
+
73
168
  export type { Signer, Provider, ContractTransactionReceipt };