@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
@@ -15,6 +15,18 @@ export type {
15
15
  Signer,
16
16
  Provider,
17
17
  ContractTransactionReceipt,
18
+ // Curator types
19
+ SwapParams,
20
+ BatchSwapParams,
21
+ BridgeParams,
22
+ PendingAction,
23
+ SubmitActionsResult,
24
+ CuratorAction,
25
+ CuratorVaultStatus,
26
+ AssetInfo,
27
+ VaultAnalysis,
28
+ AssetBalance,
29
+ VaultAssetBreakdown,
18
30
  } from "./types";
19
31
  export { ActionType } from "./types";
20
32
 
@@ -27,6 +39,16 @@ export {
27
39
  OFT_ABI,
28
40
  METADATA_ABI,
29
41
  LZ_ENDPOINT_ABI,
42
+ // Curator ABIs
43
+ MULTICALL_ABI,
44
+ DEX_ABI,
45
+ BRIDGE_FACET_ABI,
46
+ ERC7540_FACET_ABI,
47
+ ERC4626_FACET_ABI,
48
+ CURATOR_CONFIG_ABI,
49
+ LZ_ADAPTER_ABI,
50
+ VAULT_ANALYSIS_ABI,
51
+ REGISTRY_ABI,
30
52
  } from "./abis";
31
53
 
32
54
  // --- Errors ---
@@ -60,6 +82,7 @@ export {
60
82
  quoteDepositFromSpokeFee,
61
83
  quoteComposeFee,
62
84
  executeCompose,
85
+ waitForCompose,
63
86
  } from "./crossChainFlows";
64
87
 
65
88
  // --- Redeem flows ---
@@ -72,7 +95,10 @@ export {
72
95
  smartRedeem,
73
96
  bridgeSharesToHub,
74
97
  bridgeAssetsToSpoke,
98
+ resolveRedeemAddresses,
99
+ quoteShareBridgeFee,
75
100
  } from "./redeemFlows";
101
+ export type { SpokeRedeemRoute } from "./redeemFlows";
76
102
 
77
103
  // --- Utilities ---
78
104
  export {
@@ -81,11 +107,18 @@ export {
81
107
  isAsyncMode,
82
108
  getAsyncRequestStatus,
83
109
  getVaultStatus,
110
+ detectStargateOft,
84
111
  } from "./utils";
85
112
  export type { VaultStatus, VaultMode } from "./utils";
86
113
 
87
114
  // --- Pre-flight validation ---
88
- export { preflightSync, preflightAsync, preflightRedeemLiquidity } from "./preflight";
115
+ export {
116
+ preflightSync,
117
+ preflightAsync,
118
+ preflightRedeemLiquidity,
119
+ preflightSpokeDeposit,
120
+ preflightSpokeRedeem,
121
+ } from "./preflight";
89
122
 
90
123
  // --- User Helpers ---
91
124
  export {
@@ -98,6 +131,7 @@ export {
98
131
  getUserBalances,
99
132
  getMaxWithdrawable,
100
133
  getVaultSummary,
134
+ getUserPositionMultiChain,
101
135
  } from "./userHelpers";
102
136
  export type {
103
137
  UserPosition,
@@ -109,7 +143,68 @@ export type {
109
143
  UserBalances,
110
144
  MaxWithdrawable,
111
145
  VaultSummary,
146
+ MultiChainUserPosition,
112
147
  } from "./userHelpers";
113
148
 
149
+ // --- Curator status reads ---
150
+ export {
151
+ getCuratorVaultStatus,
152
+ getPendingActions,
153
+ isCurator,
154
+ getVaultAnalysis,
155
+ checkProtocolWhitelist,
156
+ getVaultAssetBreakdown,
157
+ } from "./curatorStatus";
158
+
159
+ // --- Curator multicall writes ---
160
+ export {
161
+ encodeCuratorAction,
162
+ buildCuratorBatch,
163
+ submitActions,
164
+ executeActions,
165
+ vetoActions,
166
+ } from "./curatorMulticall";
167
+
168
+ // --- Curator swap helpers ---
169
+ export {
170
+ buildUniswapV3Swap,
171
+ encodeUniswapV3SwapCalldata,
172
+ } from "./curatorSwaps";
173
+
174
+ // --- Topology ---
175
+ export {
176
+ getVaultTopology,
177
+ getFullVaultTopology,
178
+ discoverVaultTopology,
179
+ isOnHubChain,
180
+ getAllVaultChainIds,
181
+ OMNI_FACTORY_ADDRESS,
182
+ } from "./topology";
183
+ export type { VaultTopology } from "./topology";
184
+
185
+ // --- Distribution ---
186
+ export {
187
+ getVaultDistribution,
188
+ getVaultDistributionWithTopology,
189
+ } from "./distribution";
190
+ export type { VaultDistribution, SpokeBalance } from "./distribution";
191
+
192
+ // --- Spoke routes ---
193
+ export {
194
+ getInboundRoutes,
195
+ getUserBalancesForRoutes,
196
+ getOutboundRoutes,
197
+ quoteRouteDepositFee,
198
+ NATIVE_SYMBOL,
199
+ } from "./spokeRoutes";
200
+ export type {
201
+ InboundRoute,
202
+ InboundRouteWithBalance,
203
+ OutboundRoute,
204
+ } from "./spokeRoutes";
205
+
206
+ // --- Chains ---
207
+ export { UNISWAP_V3_ROUTERS, OFT_ROUTES } from "./chains";
208
+
114
209
  // --- wagmi / ethers adapter compatibility ---
115
210
  export { asSdkSigner } from "./wagmiCompat";
@@ -8,8 +8,11 @@
8
8
 
9
9
  import { Contract, ZeroAddress } from "ethers";
10
10
  import type { Provider } from "ethers";
11
- import { CONFIG_ABI, BRIDGE_ABI, VAULT_ABI, ERC20_ABI } from "./abis";
11
+ import { CONFIG_ABI, BRIDGE_ABI, VAULT_ABI, ERC20_ABI, OFT_ABI } from "./abis";
12
12
  import { InsufficientLiquidityError } from "./errors";
13
+ import { detectStargateOft } from "./utils";
14
+ import { EID_TO_CHAIN_ID, createChainProvider } from "./chains";
15
+ import { quoteComposeFee } from "./crossChainFlows";
13
16
 
14
17
  /**
15
18
  * Pre-flight checks for async cross-chain flows (D4 / D5 / R5).
@@ -164,3 +167,224 @@ export async function preflightSync(
164
167
  );
165
168
  }
166
169
  }
170
+
171
+ /**
172
+ * Pre-flight checks for spoke-to-hub deposits (D6 / D7 via OFT Compose).
173
+ *
174
+ * Validates that:
175
+ * 1. User has enough tokens on the spoke chain to deposit.
176
+ * 2. User has enough native gas on the spoke chain for TX1 (OFT.send).
177
+ * 3. For Stargate OFTs (2-TX flow): user has enough ETH on the hub chain for TX2.
178
+ *
179
+ * @param spokeProvider Read-only provider on the SPOKE chain
180
+ * @param vault Vault address
181
+ * @param spokeOFT OFT contract address on the spoke chain
182
+ * @param hubEid LZ EID for the hub chain
183
+ * @param spokeEid LZ EID for the spoke chain
184
+ * @param amount Amount of tokens to deposit
185
+ * @param userAddress User's wallet address
186
+ * @param lzFee LZ fee for TX1 (from quoteDepositFromSpokeFee)
187
+ * @returns Object with validated balances for UI display
188
+ */
189
+ export async function preflightSpokeDeposit(
190
+ spokeProvider: Provider,
191
+ vault: string,
192
+ spokeOFT: string,
193
+ hubEid: number,
194
+ spokeEid: number,
195
+ amount: bigint,
196
+ userAddress: string,
197
+ lzFee: bigint,
198
+ ): Promise<{
199
+ spokeTokenBalance: bigint
200
+ spokeNativeBalance: bigint
201
+ hubNativeBalance: bigint
202
+ estimatedComposeFee: bigint
203
+ isStargate: boolean
204
+ }> {
205
+ // Read the underlying token address from the OFT
206
+ const OFT_TOKEN_ABI = ["function token() view returns (address)"] as const;
207
+ const oftContract = new Contract(spokeOFT, OFT_TOKEN_ABI, spokeProvider);
208
+ const spokeToken: string = await oftContract.token();
209
+
210
+ // Check token balance + native balance on spoke in parallel
211
+ const tokenContract = new Contract(spokeToken, ERC20_ABI, spokeProvider);
212
+ const [spokeTokenBalance, spokeNativeBalance]: [bigint, bigint] = await Promise.all([
213
+ tokenContract.balanceOf(userAddress),
214
+ spokeProvider.getBalance(userAddress),
215
+ ]);
216
+
217
+ // 1. Check token balance
218
+ if (spokeTokenBalance < amount) {
219
+ throw new Error(
220
+ `[MoreVaults] Insufficient token balance on spoke chain.\n` +
221
+ ` Need: ${amount}\n` +
222
+ ` Have: ${spokeTokenBalance}\n` +
223
+ ` Token: ${spokeToken}`,
224
+ );
225
+ }
226
+
227
+ // 2. Check native gas for TX1 (lzFee + gas buffer)
228
+ const gasBuffer = 500_000_000_000_000n; // 0.0005 ETH
229
+ if (spokeNativeBalance < lzFee + gasBuffer) {
230
+ throw new Error(
231
+ `[MoreVaults] Insufficient native gas on spoke chain for TX1.\n` +
232
+ ` Need: ~${lzFee + gasBuffer} wei (LZ fee + gas)\n` +
233
+ ` Have: ${spokeNativeBalance} wei`,
234
+ );
235
+ }
236
+
237
+ // 3. For Stargate OFTs: check ETH on hub for TX2 (compose retry)
238
+ const isStargate = await detectStargateOft(spokeProvider, spokeOFT);
239
+
240
+ let hubNativeBalance = 0n;
241
+ let estimatedComposeFee = 0n;
242
+
243
+ if (isStargate) {
244
+ const hubChainId = EID_TO_CHAIN_ID[hubEid];
245
+ const hubProvider = createChainProvider(hubChainId);
246
+ if (hubProvider) {
247
+ [hubNativeBalance, estimatedComposeFee] = await Promise.all([
248
+ hubProvider.getBalance(userAddress),
249
+ quoteComposeFee(hubProvider, vault, spokeEid, userAddress),
250
+ ]);
251
+
252
+ const hubGasBuffer = 300_000_000_000_000n; // 0.0003 ETH
253
+ const totalNeeded = estimatedComposeFee + hubGasBuffer;
254
+
255
+ if (hubNativeBalance < totalNeeded) {
256
+ throw new Error(
257
+ `[MoreVaults] Insufficient ETH on hub chain for TX2 (compose retry).\n` +
258
+ ` This is a Stargate 2-TX flow — TX2 requires ETH on the hub chain.\n` +
259
+ ` Need: ~${totalNeeded} wei (compose fee ${estimatedComposeFee} + gas)\n` +
260
+ ` Have: ${hubNativeBalance} wei\n` +
261
+ ` Short: ${totalNeeded - hubNativeBalance} wei\n` +
262
+ ` Send ETH to ${userAddress} on chainId ${hubChainId} before depositing.`,
263
+ );
264
+ }
265
+ }
266
+ }
267
+
268
+ return {
269
+ spokeTokenBalance,
270
+ spokeNativeBalance,
271
+ hubNativeBalance,
272
+ estimatedComposeFee,
273
+ isStargate,
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Pre-flight checks for spoke→hub→spoke redeem (R6 + R1 + R7).
279
+ *
280
+ * Validates that:
281
+ * 1. User has shares on the spoke chain.
282
+ * 2. User has enough native gas on the spoke for TX1.
283
+ * 3. User has enough native gas on the hub for TX2 (redeem) + TX3 (asset bridge).
284
+ *
285
+ * @param route SpokeRedeemRoute from resolveRedeemAddresses
286
+ * @param shares Shares the user intends to redeem
287
+ * @param userAddress User's wallet address
288
+ * @param shareBridgeFee LZ fee for share bridge (TX1)
289
+ * @returns Validated balances for UI display
290
+ */
291
+ export async function preflightSpokeRedeem(
292
+ route: {
293
+ hubChainId: number
294
+ spokeChainId: number
295
+ hubEid: number
296
+ spokeEid: number
297
+ hubAsset: string
298
+ spokeShareOft: string
299
+ hubAssetOft: string
300
+ spokeAsset: string
301
+ isStargate: boolean
302
+ },
303
+ shares: bigint,
304
+ userAddress: string,
305
+ shareBridgeFee: bigint,
306
+ ): Promise<{
307
+ sharesOnSpoke: bigint
308
+ spokeNativeBalance: bigint
309
+ hubNativeBalance: bigint
310
+ estimatedAssetBridgeFee: bigint
311
+ estimatedAssetsOut: bigint
312
+ hubLiquidBalance: bigint
313
+ }> {
314
+ const spokeProvider = createChainProvider(route.spokeChainId);
315
+ const hubProvider = createChainProvider(route.hubChainId);
316
+ if (!spokeProvider) throw new Error(`No public RPC for spoke chainId ${route.spokeChainId}`);
317
+ if (!hubProvider) throw new Error(`No public RPC for hub chainId ${route.hubChainId}`);
318
+
319
+ // Parallel reads: shares on spoke, native balances
320
+ const spokeShareContract = new Contract(route.spokeShareOft, ERC20_ABI, spokeProvider);
321
+ const [sharesOnSpoke, spokeNativeBalance, hubNativeBalance]: [bigint, bigint, bigint] =
322
+ await Promise.all([
323
+ spokeShareContract.balanceOf(userAddress),
324
+ spokeProvider.getBalance(userAddress),
325
+ hubProvider.getBalance(userAddress),
326
+ ]);
327
+
328
+ // 1. Check shares
329
+ if (sharesOnSpoke < shares) {
330
+ throw new Error(
331
+ `[MoreVaults] Insufficient shares on spoke chain.\n` +
332
+ ` Need: ${shares}\n` +
333
+ ` Have: ${sharesOnSpoke}\n` +
334
+ ` Token: ${route.spokeShareOft}`,
335
+ );
336
+ }
337
+
338
+ // 2. Check spoke gas for TX1
339
+ const spokeGasBuffer = 500_000_000_000_000n; // 0.0005 ETH
340
+ if (spokeNativeBalance < shareBridgeFee + spokeGasBuffer) {
341
+ throw new Error(
342
+ `[MoreVaults] Insufficient native gas on spoke for TX1 (share bridge).\n` +
343
+ ` Need: ~${shareBridgeFee + spokeGasBuffer} wei (LZ fee + gas)\n` +
344
+ ` Have: ${spokeNativeBalance} wei`,
345
+ );
346
+ }
347
+
348
+ // 3. Estimate asset bridge fee (TX3) and check hub gas
349
+ let estimatedAssetBridgeFee = 0n;
350
+
351
+ try {
352
+ const toBytes32 = `0x${userAddress.replace(/^0x/, '').padStart(64, '0')}`;
353
+ const dummyAmount = 1_000_000n; // 1 USDC for fee estimation
354
+ const hubOft = new Contract(route.hubAssetOft, OFT_ABI, hubProvider);
355
+ const feeResult = await hubOft.quoteSend({
356
+ dstEid: route.spokeEid,
357
+ to: toBytes32,
358
+ amountLD: dummyAmount,
359
+ minAmountLD: dummyAmount * 99n / 100n,
360
+ extraOptions: "0x",
361
+ composeMsg: "0x",
362
+ oftCmd: route.isStargate ? "0x01" : "0x",
363
+ }, false);
364
+ estimatedAssetBridgeFee = feeResult.nativeFee as bigint;
365
+ } catch {
366
+ estimatedAssetBridgeFee = 300_000_000_000_000n; // 0.0003 ETH fallback
367
+ }
368
+
369
+ const hubGasBuffer = 300_000_000_000_000n; // 0.0003 ETH for gas (TX2 + TX3)
370
+ const totalHubNeeded = estimatedAssetBridgeFee + hubGasBuffer;
371
+
372
+ if (hubNativeBalance < totalHubNeeded) {
373
+ throw new Error(
374
+ `[MoreVaults] Insufficient ETH on hub chain for TX2 (redeem) + TX3 (asset bridge).\n` +
375
+ ` Need: ~${totalHubNeeded} wei (LZ fee ${estimatedAssetBridgeFee} + gas)\n` +
376
+ ` Have: ${hubNativeBalance} wei\n` +
377
+ ` Short: ${totalHubNeeded - hubNativeBalance} wei\n` +
378
+ ` Send ETH to ${userAddress} on chainId ${route.hubChainId} before redeeming.`,
379
+ );
380
+ }
381
+
382
+ return {
383
+ sharesOnSpoke,
384
+ spokeNativeBalance,
385
+ hubNativeBalance,
386
+ estimatedAssetBridgeFee,
387
+ estimatedAssetsOut: 0n,
388
+ hubLiquidBalance: 0n,
389
+ };
390
+ }
@@ -15,7 +15,9 @@ import type { ContractTransactionReceipt } from "ethers";
15
15
  import { preflightAsync, preflightRedeemLiquidity } from "./preflight";
16
16
  import { EscrowNotConfiguredError } from "./errors";
17
17
  import { validateWalletChain } from "./chainValidation";
18
- import { getVaultStatus, quoteLzFee } from "./utils";
18
+ import { getVaultStatus, quoteLzFee, detectStargateOft } from "./utils";
19
+ import { CHAIN_ID_TO_EID, OFT_ROUTES, createChainProvider } from "./chains";
20
+ import { OMNI_FACTORY_ADDRESS } from "./topology";
19
21
 
20
22
  /**
21
23
  * Ensure `spender` has at least `amount` allowance from `owner`.
@@ -407,3 +409,160 @@ export async function bridgeAssetsToSpoke(
407
409
 
408
410
  return { receipt };
409
411
  }
412
+
413
+ // ---------------------------------------------------------------------------
414
+ // Spoke redeem helpers
415
+ // ---------------------------------------------------------------------------
416
+
417
+ /** Minimal ABIs used only within redeemFlows */
418
+ const FACTORY_COMPOSER_ABI_RF = [
419
+ "function vaultComposer(address _vault) view returns (address)",
420
+ ] as const;
421
+
422
+ const REDEEM_COMPOSER_ABI = [
423
+ "function SHARE_OFT() view returns (address)",
424
+ ] as const;
425
+
426
+ const OFT_PEERS_ABI_RF = [
427
+ "function peers(uint32 eid) view returns (bytes32)",
428
+ ] as const;
429
+
430
+ export interface SpokeRedeemRoute {
431
+ /** Hub chain ID */
432
+ hubChainId: number
433
+ /** Spoke chain ID */
434
+ spokeChainId: number
435
+ /** LZ EID for the hub */
436
+ hubEid: number
437
+ /** LZ EID for the spoke */
438
+ spokeEid: number
439
+ /** Vault underlying asset address on hub */
440
+ hubAsset: string
441
+ /** SHARE_OFT on spoke chain */
442
+ spokeShareOft: string
443
+ /** Asset OFT on hub for bridging back */
444
+ hubAssetOft: string
445
+ /** Underlying asset address on spoke chain */
446
+ spokeAsset: string
447
+ /** Whether the asset OFT is a Stargate pool */
448
+ isStargate: boolean
449
+ /** OFT route symbol (e.g. 'stgUSDC') */
450
+ symbol: string
451
+ }
452
+
453
+ /**
454
+ * Quote the LZ fee for bridging shares from spoke to hub via SHARE_OFT.
455
+ *
456
+ * IMPORTANT: `amountLD` must be in SHARE_OFT native decimals (e.g. 18),
457
+ * NOT vault decimals (e.g. 8). Use the raw `SHARE_OFT.balanceOf(user)` value.
458
+ *
459
+ * @param spokeProvider Read-only provider on the SPOKE chain
460
+ * @param shareOFT SHARE_OFT address on the spoke chain
461
+ * @param hubChainEid LayerZero Endpoint ID for the hub chain
462
+ * @param amountLD Shares in SHARE_OFT native decimals (raw balanceOf)
463
+ * @param receiver Receiver address on the hub chain
464
+ * @returns LZ native fee in wei
465
+ */
466
+ export async function quoteShareBridgeFee(
467
+ spokeProvider: Provider,
468
+ shareOFT: string,
469
+ hubChainEid: number,
470
+ amountLD: bigint,
471
+ receiver: string,
472
+ ): Promise<bigint> {
473
+ const toBytes32 = zeroPadValue(receiver, 32);
474
+ const sendParam = {
475
+ dstEid: hubChainEid,
476
+ to: toBytes32,
477
+ amountLD,
478
+ minAmountLD: amountLD,
479
+ extraOptions: "0x",
480
+ composeMsg: "0x",
481
+ oftCmd: "0x",
482
+ };
483
+
484
+ const oft = new Contract(shareOFT, OFT_ABI, spokeProvider);
485
+ const fee = await oft.quoteSend(sendParam, false);
486
+ return fee.nativeFee as bigint;
487
+ }
488
+
489
+ /**
490
+ * Resolve all addresses needed for a full spoke→hub→spoke redeem flow.
491
+ *
492
+ * @param hubProvider Read-only provider on the HUB chain
493
+ * @param vault Vault address
494
+ * @param hubChainId Hub chain ID
495
+ * @param spokeChainId Spoke chain ID where user has shares
496
+ * @returns All addresses needed for bridgeSharesToHub + redeemShares + bridgeAssetsToSpoke
497
+ */
498
+ export async function resolveRedeemAddresses(
499
+ hubProvider: Provider,
500
+ vault: string,
501
+ hubChainId: number,
502
+ spokeChainId: number,
503
+ ): Promise<SpokeRedeemRoute> {
504
+ const hubEid = CHAIN_ID_TO_EID[hubChainId];
505
+ const spokeEid = CHAIN_ID_TO_EID[spokeChainId];
506
+ if (!hubEid || !spokeEid) {
507
+ throw new Error(`No LZ EID for chainId ${!hubEid ? hubChainId : spokeChainId}`);
508
+ }
509
+
510
+ const vaultContract = new Contract(vault, VAULT_ABI, hubProvider);
511
+ const factory = new Contract(OMNI_FACTORY_ADDRESS, FACTORY_COMPOSER_ABI_RF, hubProvider);
512
+ const [hubAsset, composerAddress]: [string, string] = await Promise.all([
513
+ vaultContract.asset(),
514
+ factory.vaultComposer(vault),
515
+ ]);
516
+
517
+ if (composerAddress === ZeroAddress) {
518
+ throw new Error(`[MoreVaults] No composer registered for vault ${vault} on hub chain ${hubChainId}`);
519
+ }
520
+
521
+ const composer = new Contract(composerAddress, REDEEM_COMPOSER_ABI, hubProvider);
522
+ const hubShareOft: string = await composer.SHARE_OFT();
523
+
524
+ const hubShareOftContract = new Contract(hubShareOft, OFT_PEERS_ABI_RF, hubProvider);
525
+ const spokeShareOftBytes32: string = await hubShareOftContract.peers(spokeEid);
526
+
527
+ // Convert bytes32 to address (last 20 bytes = last 40 hex chars)
528
+ const spokeShareOft = `0x${spokeShareOftBytes32.slice(-40)}`;
529
+
530
+ let hubAssetOft: string | null = null;
531
+ let spokeAsset: string | null = null;
532
+ let symbol = '';
533
+
534
+ for (const [sym, chainMap] of Object.entries(OFT_ROUTES)) {
535
+ const hubEntry = (chainMap as Record<number, { oft: string; token: string }>)[hubChainId];
536
+ const spokeEntry = (chainMap as Record<number, { oft: string; token: string }>)[spokeChainId];
537
+ if (!hubEntry || !spokeEntry) continue;
538
+
539
+ if (hubEntry.token.toLowerCase() === hubAsset.toLowerCase()) {
540
+ hubAssetOft = hubEntry.oft;
541
+ spokeAsset = spokeEntry.token;
542
+ symbol = sym;
543
+ break;
544
+ }
545
+ }
546
+
547
+ if (!hubAssetOft || !spokeAsset) {
548
+ throw new Error(
549
+ `[MoreVaults] No OFT route found for vault asset ${hubAsset} ` +
550
+ `between hub chain ${hubChainId} and spoke chain ${spokeChainId}`,
551
+ );
552
+ }
553
+
554
+ const isStargate = await detectStargateOft(hubProvider, hubAssetOft);
555
+
556
+ return {
557
+ hubChainId,
558
+ spokeChainId,
559
+ hubEid,
560
+ spokeEid,
561
+ hubAsset,
562
+ spokeShareOft,
563
+ hubAssetOft,
564
+ spokeAsset,
565
+ isStargate,
566
+ symbol,
567
+ };
568
+ }