@oydual31/more-vaults-sdk 0.3.2 → 0.4.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.
- package/dist/ethers/index.cjs +1794 -315
- package/dist/ethers/index.cjs.map +1 -1
- package/dist/ethers/index.d.cts +1147 -1
- package/dist/ethers/index.d.ts +1147 -1
- package/dist/ethers/index.js +1752 -317
- package/dist/ethers/index.js.map +1 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/{spokeRoutes-DK7cIW4z.d.cts → spokeRoutes-BIafSbQ3.d.cts} +13 -2
- package/dist/{spokeRoutes-DK7cIW4z.d.ts → spokeRoutes-BIafSbQ3.d.ts} +13 -2
- package/dist/viem/index.cjs +34 -28
- package/dist/viem/index.cjs.map +1 -1
- package/dist/viem/index.d.cts +1 -1
- package/dist/viem/index.d.ts +1 -1
- package/dist/viem/index.js +34 -29
- package/dist/viem/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ethers/abis.ts +92 -0
- package/src/ethers/chains.ts +191 -0
- package/src/ethers/crossChainFlows.ts +208 -0
- package/src/ethers/curatorMulticall.ts +195 -0
- package/src/ethers/curatorStatus.ts +319 -0
- package/src/ethers/curatorSwaps.ts +192 -0
- package/src/ethers/distribution.ts +156 -0
- package/src/ethers/index.ts +96 -1
- package/src/ethers/preflight.ts +225 -1
- package/src/ethers/redeemFlows.ts +160 -1
- package/src/ethers/spokeRoutes.ts +361 -0
- package/src/ethers/topology.ts +240 -0
- package/src/ethers/types.ts +95 -0
- package/src/ethers/userHelpers.ts +193 -0
- package/src/ethers/utils.ts +28 -0
- package/src/viem/crossChainFlows.ts +12 -22
- package/src/viem/index.ts +1 -0
- package/src/viem/preflight.ts +3 -9
- package/src/viem/redeemFlows.ts +4 -5
- package/src/viem/utils.ts +40 -0
|
@@ -10,6 +10,8 @@ import { BRIDGE_ABI, CONFIG_ABI, ERC20_ABI, VAULT_ABI, METADATA_ABI } from "./ab
|
|
|
10
10
|
import type { CrossChainRequestInfo } from "./types";
|
|
11
11
|
import { getVaultStatus } from "./utils";
|
|
12
12
|
import type { VaultStatus } from "./utils";
|
|
13
|
+
import { CHAIN_ID_TO_EID, OFT_ROUTES, createChainProvider } from "./chains";
|
|
14
|
+
import { discoverVaultTopology, OMNI_FACTORY_ADDRESS } from "./topology";
|
|
13
15
|
|
|
14
16
|
// Multicall3 — deployed at the same address on every EVM chain
|
|
15
17
|
const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
@@ -478,3 +480,194 @@ export async function getVaultSummary(
|
|
|
478
480
|
]);
|
|
479
481
|
return { ...status, ...metadata };
|
|
480
482
|
}
|
|
483
|
+
|
|
484
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
/** Minimal ABIs for SHARE_OFT discovery in getUserPositionMultiChain */
|
|
487
|
+
const FACTORY_COMPOSER_ABI_UH = [
|
|
488
|
+
"function vaultComposer(address _vault) view returns (address)",
|
|
489
|
+
] as const;
|
|
490
|
+
|
|
491
|
+
const COMPOSER_SHARE_OFT_ABI_UH = [
|
|
492
|
+
"function SHARE_OFT() view returns (address)",
|
|
493
|
+
] as const;
|
|
494
|
+
|
|
495
|
+
const OFT_PEERS_ABI_UH = [
|
|
496
|
+
"function peers(uint32 eid) view returns (bytes32)",
|
|
497
|
+
] as const;
|
|
498
|
+
|
|
499
|
+
export interface MultiChainUserPosition {
|
|
500
|
+
/** Shares held directly on the hub vault (vault.balanceOf) */
|
|
501
|
+
hubShares: bigint;
|
|
502
|
+
/** Per-spoke SHARE_OFT balances normalized to vault decimals: { [chainId]: bigint } */
|
|
503
|
+
spokeShares: Record<number, bigint>;
|
|
504
|
+
/** Per-spoke SHARE_OFT raw balances in OFT native decimals: { [chainId]: bigint }
|
|
505
|
+
* Use these for bridgeSharesToHub() and quoteShareBridgeFee() */
|
|
506
|
+
rawSpokeShares: Record<number, bigint>;
|
|
507
|
+
/** hubShares + sum of all spokeShares (in vault decimals) */
|
|
508
|
+
totalShares: bigint;
|
|
509
|
+
/** convertToAssets(totalShares) on the hub */
|
|
510
|
+
estimatedAssets: bigint;
|
|
511
|
+
/** Share price: convertToAssets(10^decimals) */
|
|
512
|
+
sharePrice: bigint;
|
|
513
|
+
/** Vault decimals */
|
|
514
|
+
decimals: number;
|
|
515
|
+
/** Pending async withdrawal request on hub, or null */
|
|
516
|
+
pendingWithdrawal: {
|
|
517
|
+
shares: bigint;
|
|
518
|
+
timelockEndsAt: bigint;
|
|
519
|
+
canRedeemNow: boolean;
|
|
520
|
+
} | null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Read the user's position across all chains of an omni vault.
|
|
525
|
+
*
|
|
526
|
+
* Discovers topology automatically, reads hub shares + pending withdrawal,
|
|
527
|
+
* then reads SHARE_OFT balances on each spoke chain in parallel.
|
|
528
|
+
*
|
|
529
|
+
* For local (single-chain) vaults, spokeShares will be empty and this
|
|
530
|
+
* behaves identically to getUserPosition.
|
|
531
|
+
*
|
|
532
|
+
* @param vault Vault address (same on all chains via CREATE3)
|
|
533
|
+
* @param user User wallet address
|
|
534
|
+
* @returns Aggregated position across all chains
|
|
535
|
+
*/
|
|
536
|
+
export async function getUserPositionMultiChain(
|
|
537
|
+
vault: string,
|
|
538
|
+
user: string,
|
|
539
|
+
): Promise<MultiChainUserPosition> {
|
|
540
|
+
// Step 1: discover topology
|
|
541
|
+
const topo = await discoverVaultTopology(vault);
|
|
542
|
+
const hubProvider = createChainProvider(topo.hubChainId);
|
|
543
|
+
if (!hubProvider) throw new Error(`No public RPC for hub chainId ${topo.hubChainId}`);
|
|
544
|
+
|
|
545
|
+
// Step 2: read hub data (shares, decimals, withdrawal request)
|
|
546
|
+
const mc = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, hubProvider);
|
|
547
|
+
const vaultIface = new Interface(VAULT_ABI as unknown as string[]);
|
|
548
|
+
const decimalsIface = new Interface(["function decimals() view returns (uint8)"]);
|
|
549
|
+
|
|
550
|
+
const b1Calls = [
|
|
551
|
+
{ target: vault, allowFailure: false, callData: vaultIface.encodeFunctionData("balanceOf", [user]) },
|
|
552
|
+
{ target: vault, allowFailure: false, callData: decimalsIface.encodeFunctionData("decimals") },
|
|
553
|
+
{ target: vault, allowFailure: false, callData: vaultIface.encodeFunctionData("getWithdrawalRequest", [user]) },
|
|
554
|
+
];
|
|
555
|
+
|
|
556
|
+
const [b1Raw, block] = await Promise.all([
|
|
557
|
+
mc.aggregate3.staticCall(b1Calls) as Promise<{ success: boolean; returnData: string }[]>,
|
|
558
|
+
hubProvider.getBlock("latest"),
|
|
559
|
+
]);
|
|
560
|
+
|
|
561
|
+
const hubShares = vaultIface.decodeFunctionResult("balanceOf", b1Raw[0].returnData)[0] as bigint;
|
|
562
|
+
const decimals = Number(decimalsIface.decodeFunctionResult("decimals", b1Raw[1].returnData)[0]);
|
|
563
|
+
const withdrawalResult = vaultIface.decodeFunctionResult("getWithdrawalRequest", b1Raw[2].returnData);
|
|
564
|
+
const withdrawShares = withdrawalResult[0] as bigint;
|
|
565
|
+
const timelockEndsAt = withdrawalResult[1] as bigint;
|
|
566
|
+
|
|
567
|
+
// Step 3: resolve SHARE_OFT addresses for spokes (if any)
|
|
568
|
+
const spokeShares: Record<number, bigint> = {};
|
|
569
|
+
const rawSpokeShares: Record<number, bigint> = {};
|
|
570
|
+
|
|
571
|
+
if (topo.spokeChainIds.length > 0) {
|
|
572
|
+
let hubShareOft: string | null = null;
|
|
573
|
+
try {
|
|
574
|
+
const factory = new Contract(OMNI_FACTORY_ADDRESS, FACTORY_COMPOSER_ABI_UH, hubProvider);
|
|
575
|
+
const composerAddress: string = await factory.vaultComposer(vault);
|
|
576
|
+
|
|
577
|
+
if (composerAddress !== "0x0000000000000000000000000000000000000000") {
|
|
578
|
+
const composer = new Contract(composerAddress, COMPOSER_SHARE_OFT_ABI_UH, hubProvider);
|
|
579
|
+
hubShareOft = await composer.SHARE_OFT();
|
|
580
|
+
}
|
|
581
|
+
} catch { /* no composer — skip spoke reads */ }
|
|
582
|
+
|
|
583
|
+
if (hubShareOft) {
|
|
584
|
+
const hubShareOftContract = new Contract(hubShareOft, OFT_PEERS_ABI_UH, hubProvider);
|
|
585
|
+
|
|
586
|
+
const spokePromises = topo.spokeChainIds.map(async (spokeChainId) => {
|
|
587
|
+
try {
|
|
588
|
+
const spokeEid = CHAIN_ID_TO_EID[spokeChainId];
|
|
589
|
+
if (!spokeEid) return { chainId: spokeChainId, balance: 0n, rawBalance: 0n };
|
|
590
|
+
|
|
591
|
+
const spokeOftBytes32: string = await hubShareOftContract.peers(spokeEid);
|
|
592
|
+
const spokeOft = `0x${spokeOftBytes32.slice(-40)}`;
|
|
593
|
+
|
|
594
|
+
if (spokeOft === "0x0000000000000000000000000000000000000000") {
|
|
595
|
+
return { chainId: spokeChainId, balance: 0n, rawBalance: 0n };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const spokeProvider = createChainProvider(spokeChainId);
|
|
599
|
+
if (!spokeProvider) return { chainId: spokeChainId, balance: 0n, rawBalance: 0n };
|
|
600
|
+
|
|
601
|
+
// Read balance + decimals on spoke chain via Multicall3
|
|
602
|
+
const spokeMc = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, spokeProvider);
|
|
603
|
+
const erc20Iface = new Interface(ERC20_ABI as unknown as string[]);
|
|
604
|
+
const spokeDecimalsIface = new Interface(["function decimals() view returns (uint8)"]);
|
|
605
|
+
|
|
606
|
+
const spokeCalls = [
|
|
607
|
+
{ target: spokeOft, allowFailure: false, callData: erc20Iface.encodeFunctionData("balanceOf", [user]) },
|
|
608
|
+
{ target: spokeOft, allowFailure: false, callData: spokeDecimalsIface.encodeFunctionData("decimals") },
|
|
609
|
+
];
|
|
610
|
+
const spokeRaw: { success: boolean; returnData: string }[] =
|
|
611
|
+
await spokeMc.aggregate3.staticCall(spokeCalls);
|
|
612
|
+
|
|
613
|
+
const rawBalance = erc20Iface.decodeFunctionResult("balanceOf", spokeRaw[0].returnData)[0] as bigint;
|
|
614
|
+
const spokeOftDecimals = Number(spokeDecimalsIface.decodeFunctionResult("decimals", spokeRaw[1].returnData)[0]);
|
|
615
|
+
|
|
616
|
+
// Normalize to vault decimals
|
|
617
|
+
let balance: bigint;
|
|
618
|
+
if (spokeOftDecimals > decimals) {
|
|
619
|
+
balance = rawBalance / (10n ** BigInt(spokeOftDecimals - decimals));
|
|
620
|
+
} else if (spokeOftDecimals < decimals) {
|
|
621
|
+
balance = rawBalance * (10n ** BigInt(decimals - spokeOftDecimals));
|
|
622
|
+
} else {
|
|
623
|
+
balance = rawBalance;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return { chainId: spokeChainId, balance, rawBalance };
|
|
627
|
+
} catch {
|
|
628
|
+
return { chainId: spokeChainId, balance: 0n, rawBalance: 0n };
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
const results = await Promise.all(spokePromises);
|
|
633
|
+
for (const { chainId, balance, rawBalance } of results) {
|
|
634
|
+
spokeShares[chainId] = balance;
|
|
635
|
+
rawSpokeShares[chainId] = rawBalance;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Step 4: compute totals
|
|
641
|
+
const totalSpokeShares = Object.values(spokeShares).reduce((sum, b) => sum + b, 0n);
|
|
642
|
+
const totalShares = hubShares + totalSpokeShares;
|
|
643
|
+
|
|
644
|
+
const oneShare = 10n ** BigInt(decimals);
|
|
645
|
+
const vaultContract = new Contract(vault, VAULT_ABI, hubProvider);
|
|
646
|
+
const [estimatedAssets, sharePrice]: [bigint, bigint] = await Promise.all([
|
|
647
|
+
totalShares === 0n
|
|
648
|
+
? Promise.resolve(0n)
|
|
649
|
+
: (vaultContract.convertToAssets(totalShares) as Promise<bigint>),
|
|
650
|
+
vaultContract.convertToAssets(oneShare) as Promise<bigint>,
|
|
651
|
+
]);
|
|
652
|
+
|
|
653
|
+
// Step 5: pending withdrawal
|
|
654
|
+
const currentTimestamp = BigInt(block?.timestamp ?? 0);
|
|
655
|
+
const pendingWithdrawal = withdrawShares === 0n
|
|
656
|
+
? null
|
|
657
|
+
: {
|
|
658
|
+
shares: withdrawShares,
|
|
659
|
+
timelockEndsAt,
|
|
660
|
+
canRedeemNow: timelockEndsAt === 0n || currentTimestamp >= timelockEndsAt,
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
hubShares,
|
|
665
|
+
spokeShares,
|
|
666
|
+
rawSpokeShares,
|
|
667
|
+
totalShares,
|
|
668
|
+
estimatedAssets,
|
|
669
|
+
sharePrice,
|
|
670
|
+
decimals,
|
|
671
|
+
pendingWithdrawal,
|
|
672
|
+
};
|
|
673
|
+
}
|
package/src/ethers/utils.ts
CHANGED
|
@@ -8,6 +8,11 @@ import { Contract, Interface, ZeroAddress } from "ethers";
|
|
|
8
8
|
import type { Provider, Signer } from "ethers";
|
|
9
9
|
import { BRIDGE_ABI, CONFIG_ABI, ERC20_ABI, VAULT_ABI } from "./abis";
|
|
10
10
|
|
|
11
|
+
// Minimal ABI for Stargate type detection
|
|
12
|
+
const STARGATE_TYPE_ABI = [
|
|
13
|
+
"function stargateType() view returns (uint8)",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
11
16
|
// Multicall3 — deployed at the same address on every EVM chain
|
|
12
17
|
const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
13
18
|
const MULTICALL3_ABI = [
|
|
@@ -379,3 +384,26 @@ export async function getVaultStatus(
|
|
|
379
384
|
issues,
|
|
380
385
|
};
|
|
381
386
|
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Detect whether an OFT address is a Stargate V2 pool by calling `stargateType()`.
|
|
390
|
+
*
|
|
391
|
+
* Stargate pools implement this function (returns 0=Pool, 1=OFT).
|
|
392
|
+
* Standard OFTs revert because they don't have it.
|
|
393
|
+
*
|
|
394
|
+
* @param provider Read-only provider on the OFT's chain
|
|
395
|
+
* @param oft OFT contract address
|
|
396
|
+
* @returns true if the contract is a Stargate V2 pool/OFT
|
|
397
|
+
*/
|
|
398
|
+
export async function detectStargateOft(
|
|
399
|
+
provider: Provider,
|
|
400
|
+
oft: string
|
|
401
|
+
): Promise<boolean> {
|
|
402
|
+
try {
|
|
403
|
+
const contract = new Contract(oft, STARGATE_TYPE_ABI, provider);
|
|
404
|
+
await contract.stargateType();
|
|
405
|
+
return true;
|
|
406
|
+
} catch {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from 'viem'
|
|
11
11
|
import { OFT_ABI, BRIDGE_ABI, LZ_ENDPOINT_ABI } from './abis'
|
|
12
12
|
import type { ComposeData, SpokeDepositResult } from './types'
|
|
13
|
-
import { ensureAllowance } from './utils'
|
|
13
|
+
import { ensureAllowance, detectStargateOft } from './utils'
|
|
14
14
|
import { OFT_ROUTES, EID_TO_CHAIN_ID } from './chains'
|
|
15
15
|
import { OMNI_FACTORY_ADDRESS } from './topology'
|
|
16
16
|
import { createChainClient } from './spokeRoutes'
|
|
@@ -38,8 +38,6 @@ const COMPOSER_ABI = [
|
|
|
38
38
|
},
|
|
39
39
|
] as const
|
|
40
40
|
|
|
41
|
-
const STARGATE_ASSETS = new Set(['stgUSDC', 'USDT', 'WETH'])
|
|
42
|
-
|
|
43
41
|
/**
|
|
44
42
|
* Build a LZ V2 TYPE_3 executor option that forwards native ETH to the lzCompose call.
|
|
45
43
|
*
|
|
@@ -61,17 +59,6 @@ function buildLzComposeOption(gas: bigint, nativeValue: bigint): `0x${string}` {
|
|
|
61
59
|
return `0x00030300220000${gasHex}${valueHex}` as `0x${string}`
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
/** Returns true if the OFT is a Stargate V2 pool (bus/taxi architecture). */
|
|
65
|
-
function isStargateOft(oft: Address): boolean {
|
|
66
|
-
for (const [symbol, chainMap] of Object.entries(OFT_ROUTES)) {
|
|
67
|
-
if (!STARGATE_ASSETS.has(symbol)) continue
|
|
68
|
-
for (const entry of Object.values(chainMap as Record<number, { oft: string; token: string }>)) {
|
|
69
|
-
if (getAddress(entry.oft) === oft) return true
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return false
|
|
73
|
-
}
|
|
74
|
-
|
|
75
62
|
/**
|
|
76
63
|
* Resolve the native ETH value that MoreVaultsComposer needs to receive via lzCompose.
|
|
77
64
|
*
|
|
@@ -263,7 +250,7 @@ export async function depositFromSpoke(
|
|
|
263
250
|
|
|
264
251
|
// For Stargate OFTs: extraOptions must be '0x' (rejects LZCOMPOSE type-3 options).
|
|
265
252
|
// For standard OFTs: inject LZCOMPOSE option with native ETH for readFee + share send.
|
|
266
|
-
const isStargate =
|
|
253
|
+
const isStargate = await detectStargateOft(publicClient, oft)
|
|
267
254
|
let resolvedExtraOptions: `0x${string}`
|
|
268
255
|
if (extraOptions !== '0x') {
|
|
269
256
|
resolvedExtraOptions = extraOptions
|
|
@@ -377,7 +364,7 @@ export async function depositFromSpoke(
|
|
|
377
364
|
// The compose message is NOT available yet — it's emitted as ComposeSent on the hub
|
|
378
365
|
// after LZ delivers the message. The user must call waitForCompose() to get it,
|
|
379
366
|
// then executeCompose() to execute it.
|
|
380
|
-
const stargate =
|
|
367
|
+
const stargate = isStargate
|
|
381
368
|
let composeData: ComposeData | undefined
|
|
382
369
|
if (stargate) {
|
|
383
370
|
// Snapshot current hub block BEFORE waiting — this is exactly where we start
|
|
@@ -454,7 +441,7 @@ export async function quoteDepositFromSpokeFee(
|
|
|
454
441
|
const composerBytes32 = pad(composerAddress, { size: 32 })
|
|
455
442
|
|
|
456
443
|
// Match depositFromSpoke: resolve extraOptions the same way
|
|
457
|
-
const isStargate =
|
|
444
|
+
const isStargate = await detectStargateOft(publicClient, oft)
|
|
458
445
|
let resolvedExtraOptions: `0x${string}`
|
|
459
446
|
if (extraOptions !== '0x') {
|
|
460
447
|
resolvedExtraOptions = extraOptions
|
|
@@ -560,14 +547,17 @@ export async function waitForCompose(
|
|
|
560
547
|
const receiverNeedle = getAddress(receiver).slice(2).toLowerCase()
|
|
561
548
|
const startBlock = composeData.hubBlockStart
|
|
562
549
|
|
|
563
|
-
//
|
|
564
|
-
const knownFromAddresses: Address[] = []
|
|
550
|
+
// Collect all OFT addresses on the hub chain, then filter to Stargate pools on-chain
|
|
565
551
|
const hubChainId = composeData.hubChainId
|
|
566
|
-
|
|
567
|
-
|
|
552
|
+
const candidateAddresses: Address[] = []
|
|
553
|
+
for (const chainMap of Object.values(OFT_ROUTES)) {
|
|
568
554
|
const entry = (chainMap as Record<number, { oft: string; token: string }>)[hubChainId]
|
|
569
|
-
if (entry)
|
|
555
|
+
if (entry) candidateAddresses.push(getAddress(entry.oft) as Address)
|
|
570
556
|
}
|
|
557
|
+
const stargateChecks = await Promise.all(
|
|
558
|
+
candidateAddresses.map(async (addr) => ({ addr, isSg: await detectStargateOft(hubPublicClient, addr) })),
|
|
559
|
+
)
|
|
560
|
+
const knownFromAddresses = stargateChecks.filter((c) => c.isSg).map((c) => c.addr)
|
|
571
561
|
|
|
572
562
|
let attempt = 0
|
|
573
563
|
// Track the highest block we've already scanned to avoid re-scanning
|
package/src/viem/index.ts
CHANGED
package/src/viem/preflight.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { CONFIG_ABI, BRIDGE_ABI, VAULT_ABI, ERC20_ABI, OFT_ABI } from './abis'
|
|
|
11
11
|
import { InsufficientLiquidityError } from './errors'
|
|
12
12
|
import { quoteComposeFee } from './crossChainFlows'
|
|
13
13
|
import { createChainClient } from './spokeRoutes'
|
|
14
|
-
import { EID_TO_CHAIN_ID
|
|
14
|
+
import { EID_TO_CHAIN_ID } from './chains'
|
|
15
|
+
import { detectStargateOft } from './utils'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Pre-flight checks for async cross-chain flows (D4 / D5 / R5).
|
|
@@ -305,14 +306,7 @@ export async function preflightSpokeDeposit(
|
|
|
305
306
|
}
|
|
306
307
|
|
|
307
308
|
// 3. For Stargate OFTs: check ETH on hub for TX2 (compose retry)
|
|
308
|
-
const
|
|
309
|
-
let isStargate = false
|
|
310
|
-
for (const [symbol, chainMap] of Object.entries(OFT_ROUTES)) {
|
|
311
|
-
if (!STARGATE_ASSETS.has(symbol)) continue
|
|
312
|
-
for (const entry of Object.values(chainMap as Record<number, { oft: string; token: string }>)) {
|
|
313
|
-
if (getAddress(entry.oft) === oft) isStargate = true
|
|
314
|
-
}
|
|
315
|
-
}
|
|
309
|
+
const isStargate = await detectStargateOft(spokePublicClient, oft)
|
|
316
310
|
|
|
317
311
|
let hubNativeBalance = 0n
|
|
318
312
|
let estimatedComposeFee = 0n
|
package/src/viem/redeemFlows.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
AsyncRequestResult,
|
|
16
16
|
} from './types'
|
|
17
17
|
import { ActionType } from './types'
|
|
18
|
-
import { ensureAllowance, getVaultStatus, quoteLzFee } from './utils'
|
|
18
|
+
import { ensureAllowance, getVaultStatus, quoteLzFee, detectStargateOft } from './utils'
|
|
19
19
|
import { preflightAsync, preflightRedeemLiquidity } from './preflight'
|
|
20
20
|
import { EscrowNotConfiguredError } from './errors'
|
|
21
21
|
import { validateWalletChain } from './chainValidation'
|
|
@@ -581,8 +581,6 @@ export interface SpokeRedeemRoute {
|
|
|
581
581
|
symbol: string
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
-
const STARGATE_ASSETS = new Set(['stgUSDC', 'USDT', 'WETH'])
|
|
585
|
-
|
|
586
584
|
const FACTORY_COMPOSER_ABI = [
|
|
587
585
|
{
|
|
588
586
|
type: 'function' as const,
|
|
@@ -664,7 +662,6 @@ export async function resolveRedeemAddresses(
|
|
|
664
662
|
// Find matching OFT route for the vault's asset on the hub chain
|
|
665
663
|
let hubAssetOft: Address | null = null
|
|
666
664
|
let spokeAsset: Address | null = null
|
|
667
|
-
let isStargate = false
|
|
668
665
|
let symbol = ''
|
|
669
666
|
|
|
670
667
|
for (const [sym, chainMap] of Object.entries(OFT_ROUTES)) {
|
|
@@ -675,7 +672,6 @@ export async function resolveRedeemAddresses(
|
|
|
675
672
|
if (getAddress(hubEntry.token) === getAddress(hubAsset)) {
|
|
676
673
|
hubAssetOft = getAddress(hubEntry.oft) as Address
|
|
677
674
|
spokeAsset = getAddress(spokeEntry.token) as Address
|
|
678
|
-
isStargate = STARGATE_ASSETS.has(sym)
|
|
679
675
|
symbol = sym
|
|
680
676
|
break
|
|
681
677
|
}
|
|
@@ -688,6 +684,9 @@ export async function resolveRedeemAddresses(
|
|
|
688
684
|
)
|
|
689
685
|
}
|
|
690
686
|
|
|
687
|
+
// On-chain detection: Stargate pools implement stargateType(), standard OFTs revert
|
|
688
|
+
const isStargate = await detectStargateOft(hubPublicClient, hubAssetOft)
|
|
689
|
+
|
|
691
690
|
return {
|
|
692
691
|
hubChainId,
|
|
693
692
|
spokeChainId,
|
package/src/viem/utils.ts
CHANGED
|
@@ -522,3 +522,43 @@ export async function waitForAsyncRequest(
|
|
|
522
522
|
`The request may still complete — check https://layerzeroscan.com/tx/${guid}`,
|
|
523
523
|
)
|
|
524
524
|
}
|
|
525
|
+
|
|
526
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
527
|
+
// Stargate detection — on-chain probe
|
|
528
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
const STARGATE_TYPE_ABI = [
|
|
531
|
+
{
|
|
532
|
+
type: 'function' as const,
|
|
533
|
+
name: 'stargateType' as const,
|
|
534
|
+
inputs: [] as const,
|
|
535
|
+
outputs: [{ type: 'uint8' as const }] as const,
|
|
536
|
+
stateMutability: 'view' as const,
|
|
537
|
+
},
|
|
538
|
+
] as const
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Detect whether an OFT address is a Stargate V2 pool by calling `stargateType()`.
|
|
542
|
+
*
|
|
543
|
+
* Stargate pools implement this function (returns 0=Pool, 1=OFT).
|
|
544
|
+
* Standard OFTs revert because they don't have it.
|
|
545
|
+
*
|
|
546
|
+
* @param publicClient Viem public client on the OFT's chain
|
|
547
|
+
* @param oft OFT contract address
|
|
548
|
+
* @returns true if the contract is a Stargate V2 pool/OFT
|
|
549
|
+
*/
|
|
550
|
+
export async function detectStargateOft(
|
|
551
|
+
publicClient: PublicClient,
|
|
552
|
+
oft: Address,
|
|
553
|
+
): Promise<boolean> {
|
|
554
|
+
try {
|
|
555
|
+
await publicClient.readContract({
|
|
556
|
+
address: getAddress(oft),
|
|
557
|
+
abi: STARGATE_TYPE_ABI,
|
|
558
|
+
functionName: 'stargateType',
|
|
559
|
+
})
|
|
560
|
+
return true
|
|
561
|
+
} catch {
|
|
562
|
+
return false
|
|
563
|
+
}
|
|
564
|
+
}
|