@oydual31/more-vaults-sdk 0.3.1 → 0.3.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oydual31/more-vaults-sdk",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "TypeScript SDK for MoreVaults protocol — viem/wagmi and ethers.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -261,6 +261,23 @@ export const LZ_TIMEOUTS = {
261
261
  FULL_SPOKE_REDEEM: 3_600_000, // 60 min
262
262
  } as const
263
263
 
264
+ /**
265
+ * Uniswap V3 SwapRouter addresses per chain.
266
+ * Used by curator swap helpers to build calldata for on-chain swaps.
267
+ *
268
+ * Note on struct differences:
269
+ * - SwapRouter (Eth/Arb/Op, 0xE592...): exactInputSingle struct includes `deadline`
270
+ * - SwapRouter02 (Base, 0x2626...): exactInputSingle struct does NOT include `deadline`
271
+ * - FlowSwap V3 (Flow EVM, 0xeEDC...): derived from original UniV3, includes `deadline`
272
+ */
273
+ export const UNISWAP_V3_ROUTERS: Record<number, `0x${string}`> = {
274
+ [8453]: '0x2626664c2603336E57B271c5C0b26F421741e481', // Base — SwapRouter02 (no deadline)
275
+ [1]: '0xE592427A0AEce92De3Edee1F18E0157C05861564', // Ethereum — SwapRouter
276
+ [42161]: '0xE592427A0AEce92De3Edee1F18E0157C05861564', // Arbitrum — SwapRouter
277
+ [10]: '0xE592427A0AEce92De3Edee1F18E0157C05861564', // Optimism — SwapRouter
278
+ [747]: '0xeEDC6Ff75e1b10B903D9013c358e446a73d35341', // Flow EVM — FlowSwap V3 SwapRouter
279
+ }
280
+
264
281
  // ---------------------------------------------------------------------------
265
282
  // Legacy flat exports — kept for backwards compat, prefer OFT_ROUTES
266
283
  // ---------------------------------------------------------------------------
@@ -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 = isStargateOft(oft)
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 = isStargateOft(getAddress(spokeOFT))
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 = isStargateOft(oft)
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
- // Known Stargate pool addresses on hub for composeQueue checks
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
- for (const [symbol, chainMap] of Object.entries(OFT_ROUTES)) {
567
- if (!STARGATE_ASSETS.has(symbol)) continue
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) knownFromAddresses.push(getAddress(entry.oft) as Address)
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
@@ -6,8 +6,8 @@
6
6
  */
7
7
 
8
8
  import { type Address, type PublicClient, getAddress } from 'viem'
9
- import { MULTICALL_ABI, CURATOR_CONFIG_ABI, VAULT_ANALYSIS_ABI, REGISTRY_ABI, METADATA_ABI } from './abis.js'
10
- import type { CuratorVaultStatus, PendingAction, VaultAnalysis, AssetInfo } from './types.js'
9
+ import { MULTICALL_ABI, CURATOR_CONFIG_ABI, VAULT_ANALYSIS_ABI, REGISTRY_ABI, METADATA_ABI, ERC20_ABI, VAULT_ABI } from './abis.js'
10
+ import type { CuratorVaultStatus, PendingAction, VaultAnalysis, AssetInfo, AssetBalance, VaultAssetBreakdown } from './types.js'
11
11
 
12
12
  // ─────────────────────────────────────────────────────────────────────────────
13
13
 
@@ -253,3 +253,67 @@ export async function checkProtocolWhitelist(
253
253
  return out
254
254
  }
255
255
 
256
+ // ─────────────────────────────────────────────────────────────────────────────
257
+
258
+ /**
259
+ * Get the vault's per-asset balance breakdown on the hub chain.
260
+ *
261
+ * Returns the balance of every available asset held by the vault, plus
262
+ * totalAssets and totalSupply for context. Useful for portfolio views
263
+ * that need to show individual holdings rather than a single USD-denominated total.
264
+ *
265
+ * @param publicClient Viem public client (must be on the vault's hub chain)
266
+ * @param vault Vault address (diamond proxy)
267
+ * @returns VaultAssetBreakdown with per-asset balances
268
+ */
269
+ export async function getVaultAssetBreakdown(
270
+ publicClient: PublicClient,
271
+ vault: Address,
272
+ ): Promise<VaultAssetBreakdown> {
273
+ const v = getAddress(vault)
274
+
275
+ // Step 1: get available assets list
276
+ const availableRaw = await publicClient.readContract({
277
+ address: v,
278
+ abi: VAULT_ANALYSIS_ABI,
279
+ functionName: 'getAvailableAssets',
280
+ }) as Address[]
281
+
282
+ const addresses = availableRaw.map(getAddress)
283
+
284
+ // Step 2: multicall — balanceOf + metadata for each asset + totalAssets + totalSupply + vault decimals
285
+ const results = await publicClient.multicall({
286
+ contracts: [
287
+ // Per-asset: balanceOf, name, symbol, decimals
288
+ ...addresses.flatMap((addr) => [
289
+ { address: addr, abi: ERC20_ABI, functionName: 'balanceOf' as const, args: [v] as [Address] },
290
+ { address: addr, abi: METADATA_ABI, functionName: 'name' as const },
291
+ { address: addr, abi: METADATA_ABI, functionName: 'symbol' as const },
292
+ { address: addr, abi: METADATA_ABI, functionName: 'decimals' as const },
293
+ ]),
294
+ // Vault totals
295
+ { address: v, abi: VAULT_ABI, functionName: 'totalAssets' as const },
296
+ { address: v, abi: VAULT_ABI, functionName: 'totalSupply' as const },
297
+ { address: v, abi: METADATA_ABI, functionName: 'decimals' as const },
298
+ ],
299
+ allowFailure: true,
300
+ })
301
+
302
+ const perAssetFields = 4 // balanceOf, name, symbol, decimals
303
+ const assets: AssetBalance[] = addresses.map((addr, i) => {
304
+ const base = i * perAssetFields
305
+ const balance = results[base]?.status === 'success' ? (results[base].result as bigint) : 0n
306
+ const name = results[base + 1]?.status === 'success' ? (results[base + 1].result as string) : ''
307
+ const symbol = results[base + 2]?.status === 'success' ? (results[base + 2].result as string) : ''
308
+ const decimals = results[base + 3]?.status === 'success' ? (results[base + 3].result as number) : 18
309
+
310
+ return { address: addr, name, symbol, decimals, balance }
311
+ })
312
+
313
+ const totalsBase = addresses.length * perAssetFields
314
+ const totalAssets = results[totalsBase]?.status === 'success' ? (results[totalsBase].result as bigint) : 0n
315
+ const totalSupply = results[totalsBase + 1]?.status === 'success' ? (results[totalsBase + 1].result as bigint) : 0n
316
+ const underlyingDecimals = results[totalsBase + 2]?.status === 'success' ? (results[totalsBase + 2].result as number) : 6
317
+
318
+ return { assets, totalAssets, totalSupply, underlyingDecimals }
319
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Curator swap helpers for Uniswap V3-compatible DEXes.
3
+ *
4
+ * Provides typed helpers to build CuratorAction objects and raw calldata for
5
+ * Uniswap V3 exactInputSingle swaps, automatically resolving the correct router
6
+ * and ABI variant (SwapRouter vs SwapRouter02) per chain.
7
+ *
8
+ * Supported chains and routers:
9
+ * - Base (8453): SwapRouter02 0x2626... — NO deadline field
10
+ * - Ethereum (1): SwapRouter 0xE592... — HAS deadline field
11
+ * - Arbitrum (42161): SwapRouter 0xE592... — HAS deadline field
12
+ * - Optimism (10): SwapRouter 0xE592... — HAS deadline field
13
+ * - Flow EVM (747): FlowSwap V3 0xeEDC... — HAS deadline field
14
+ *
15
+ * @module curatorSwaps
16
+ */
17
+
18
+ import { type Address, encodeFunctionData } from 'viem'
19
+ import { UNISWAP_V3_ROUTERS } from './chains.js'
20
+ import type { CuratorAction } from './types.js'
21
+
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // ABI constants
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Uniswap V3 SwapRouter exactInputSingle ABI.
28
+ * Used for: Ethereum (1), Arbitrum (42161), Optimism (10), Flow EVM (747).
29
+ * Struct includes `deadline` field.
30
+ */
31
+ const UNISWAP_V3_SWAP_ROUTER_ABI = [
32
+ {
33
+ type: 'function',
34
+ name: 'exactInputSingle',
35
+ inputs: [
36
+ {
37
+ type: 'tuple',
38
+ name: 'params',
39
+ components: [
40
+ { name: 'tokenIn', type: 'address' },
41
+ { name: 'tokenOut', type: 'address' },
42
+ { name: 'fee', type: 'uint24' },
43
+ { name: 'recipient', type: 'address' },
44
+ { name: 'deadline', type: 'uint256' },
45
+ { name: 'amountIn', type: 'uint256' },
46
+ { name: 'amountOutMinimum', type: 'uint256' },
47
+ { name: 'sqrtPriceLimitX96', type: 'uint160' },
48
+ ],
49
+ },
50
+ ],
51
+ outputs: [{ name: 'amountOut', type: 'uint256' }],
52
+ stateMutability: 'payable',
53
+ },
54
+ ] as const
55
+
56
+ /**
57
+ * Uniswap V3 SwapRouter02 exactInputSingle ABI.
58
+ * Used for: Base (8453).
59
+ * Struct does NOT include `deadline` field — SwapRouter02 removed it.
60
+ */
61
+ const UNISWAP_V3_SWAP_ROUTER02_ABI = [
62
+ {
63
+ type: 'function',
64
+ name: 'exactInputSingle',
65
+ inputs: [
66
+ {
67
+ type: 'tuple',
68
+ name: 'params',
69
+ components: [
70
+ { name: 'tokenIn', type: 'address' },
71
+ { name: 'tokenOut', type: 'address' },
72
+ { name: 'fee', type: 'uint24' },
73
+ { name: 'recipient', type: 'address' },
74
+ { name: 'amountIn', type: 'uint256' },
75
+ { name: 'amountOutMinimum', type: 'uint256' },
76
+ { name: 'sqrtPriceLimitX96', type: 'uint160' },
77
+ ],
78
+ },
79
+ ],
80
+ outputs: [{ name: 'amountOut', type: 'uint256' }],
81
+ stateMutability: 'payable',
82
+ },
83
+ ] as const
84
+
85
+ // ─────────────────────────────────────────────────────────────────────────────
86
+ // Chain variant detection
87
+ // ─────────────────────────────────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Chains that use SwapRouter02 (no deadline in struct).
91
+ * All other chains in UNISWAP_V3_ROUTERS use the original SwapRouter.
92
+ */
93
+ const SWAP_ROUTER02_CHAINS = new Set([8453])
94
+
95
+ // ─────────────────────────────────────────────────────────────────────────────
96
+ // Calldata encoding
97
+ // ─────────────────────────────────────────────────────────────────────────────
98
+
99
+ /**
100
+ * Encode Uniswap V3 exactInputSingle calldata directly.
101
+ * For curators who want raw calldata without the CuratorAction wrapper.
102
+ *
103
+ * Automatically selects the correct ABI variant (SwapRouter vs SwapRouter02)
104
+ * based on the chainId. The deadline (for SwapRouter chains) is set to
105
+ * `now + 20 minutes` to prevent stale transactions from executing.
106
+ *
107
+ * @param params.chainId EVM chain ID — must be present in UNISWAP_V3_ROUTERS
108
+ * @param params.tokenIn Input token address
109
+ * @param params.tokenOut Output token address
110
+ * @param params.fee Pool fee tier: 100, 500, 3000, or 10000
111
+ * @param params.amountIn Exact input amount (in tokenIn units)
112
+ * @param params.minAmountOut Minimum acceptable output (slippage protection)
113
+ * @param params.recipient Address to receive the output tokens (usually the vault)
114
+ * @returns The router contract address and ABI-encoded calldata
115
+ * @throws If no router is configured for the given chainId
116
+ */
117
+ export function encodeUniswapV3SwapCalldata(params: {
118
+ chainId: number
119
+ tokenIn: Address
120
+ tokenOut: Address
121
+ fee: number
122
+ amountIn: bigint
123
+ minAmountOut: bigint
124
+ recipient: Address
125
+ }): { targetContract: Address; swapCallData: `0x${string}` } {
126
+ const { chainId, tokenIn, tokenOut, fee, amountIn, minAmountOut, recipient } = params
127
+
128
+ const router = UNISWAP_V3_ROUTERS[chainId]
129
+ if (!router) {
130
+ throw new Error(
131
+ `[MoreVaults] No Uniswap V3 router configured for chainId ${chainId}. ` +
132
+ `Supported chains: ${Object.keys(UNISWAP_V3_ROUTERS).join(', ')}`
133
+ )
134
+ }
135
+
136
+ let swapCallData: `0x${string}`
137
+
138
+ if (SWAP_ROUTER02_CHAINS.has(chainId)) {
139
+ // SwapRouter02 (Base) — no deadline field
140
+ swapCallData = encodeFunctionData({
141
+ abi: UNISWAP_V3_SWAP_ROUTER02_ABI,
142
+ functionName: 'exactInputSingle',
143
+ args: [
144
+ {
145
+ tokenIn,
146
+ tokenOut,
147
+ fee,
148
+ recipient,
149
+ amountIn,
150
+ amountOutMinimum: minAmountOut,
151
+ sqrtPriceLimitX96: 0n,
152
+ },
153
+ ],
154
+ })
155
+ } else {
156
+ // Original SwapRouter (Eth/Arb/Op/Flow EVM) — has deadline field
157
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 1200) // now + 20 minutes
158
+ swapCallData = encodeFunctionData({
159
+ abi: UNISWAP_V3_SWAP_ROUTER_ABI,
160
+ functionName: 'exactInputSingle',
161
+ args: [
162
+ {
163
+ tokenIn,
164
+ tokenOut,
165
+ fee,
166
+ recipient,
167
+ deadline,
168
+ amountIn,
169
+ amountOutMinimum: minAmountOut,
170
+ sqrtPriceLimitX96: 0n,
171
+ },
172
+ ],
173
+ })
174
+ }
175
+
176
+ return { targetContract: router, swapCallData }
177
+ }
178
+
179
+ // ─────────────────────────────────────────────────────────────────────────────
180
+ // CuratorAction builder
181
+ // ─────────────────────────────────────────────────────────────────────────────
182
+
183
+ /**
184
+ * Build a CuratorAction for a Uniswap V3 exactInputSingle swap.
185
+ *
186
+ * Automatically resolves the router address from UNISWAP_V3_ROUTERS and
187
+ * selects the correct ABI struct (with or without deadline) based on chainId.
188
+ *
189
+ * The returned action is a `swap` variant ready to be passed to
190
+ * `buildCuratorBatch` and then `submitActions`.
191
+ *
192
+ * @param params.chainId EVM chain ID — must be present in UNISWAP_V3_ROUTERS
193
+ * @param params.tokenIn Input token address
194
+ * @param params.tokenOut Output token address
195
+ * @param params.fee Pool fee tier: 100, 500, 3000, or 10000
196
+ * @param params.amountIn Exact input amount (in tokenIn units)
197
+ * @param params.minAmountOut Minimum acceptable output (slippage protection)
198
+ * @param params.recipient Address to receive output tokens (usually the vault)
199
+ * @returns A typed CuratorAction ready for buildCuratorBatch
200
+ * @throws If no router is configured for the given chainId
201
+ *
202
+ * @example
203
+ * ```typescript
204
+ * const action = buildUniswapV3Swap({
205
+ * chainId: 8453,
206
+ * tokenIn: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC
207
+ * tokenOut: '0x4200000000000000000000000000000000000006', // WETH
208
+ * fee: 500, // 0.05% pool
209
+ * amountIn: 150_000n, // 0.15 USDC (6 decimals)
210
+ * minAmountOut: 1n, // accept any amount (set properly in production)
211
+ * recipient: VAULT,
212
+ * })
213
+ * const batch = buildCuratorBatch([action])
214
+ * await submitActions(walletClient, publicClient, vault, batch)
215
+ * ```
216
+ */
217
+ export function buildUniswapV3Swap(params: {
218
+ chainId: number
219
+ tokenIn: Address
220
+ tokenOut: Address
221
+ fee: number
222
+ amountIn: bigint
223
+ minAmountOut: bigint
224
+ recipient: Address
225
+ }): CuratorAction {
226
+ const { targetContract, swapCallData } = encodeUniswapV3SwapCalldata(params)
227
+
228
+ return {
229
+ type: 'swap',
230
+ params: {
231
+ targetContract,
232
+ tokenIn: params.tokenIn,
233
+ tokenOut: params.tokenOut,
234
+ maxAmountIn: params.amountIn,
235
+ minAmountOut: params.minAmountOut,
236
+ swapCallData,
237
+ },
238
+ }
239
+ }
package/src/viem/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Provides typed helpers for all deposit, redeem, and cross-chain vault flows.
3
3
 
4
4
  // --- Chain constants ---
5
- export { CHAIN_IDS, LZ_EIDS, EID_TO_CHAIN_ID, CHAIN_ID_TO_EID, OFT_ROUTES, STARGATE_TAXI_CMD, USDC_STARGATE_OFT, USDC_TOKEN, LZ_TIMEOUTS } from './chains'
5
+ export { CHAIN_IDS, LZ_EIDS, EID_TO_CHAIN_ID, CHAIN_ID_TO_EID, OFT_ROUTES, STARGATE_TAXI_CMD, USDC_STARGATE_OFT, USDC_TOKEN, LZ_TIMEOUTS, UNISWAP_V3_ROUTERS } from './chains'
6
6
 
7
7
  // --- ABIs ---
8
8
  export {
@@ -42,7 +42,9 @@ export type {
42
42
  CuratorAction,
43
43
  CuratorVaultStatus,
44
44
  AssetInfo,
45
+ AssetBalance,
45
46
  VaultAnalysis,
47
+ VaultAssetBreakdown,
46
48
  } from './types'
47
49
  export { ActionType } from './types'
48
50
 
@@ -104,6 +106,7 @@ export {
104
106
  getAsyncRequestStatus,
105
107
  waitForAsyncRequest,
106
108
  getVaultStatus,
109
+ detectStargateOft,
107
110
  } from './utils'
108
111
  export type { VaultStatus, VaultMode, AsyncRequestFinalResult } from './utils'
109
112
 
@@ -162,6 +165,7 @@ export {
162
165
  isCurator,
163
166
  getVaultAnalysis,
164
167
  checkProtocolWhitelist,
168
+ getVaultAssetBreakdown,
165
169
  } from './curatorStatus'
166
170
  export {
167
171
  encodeCuratorAction,
@@ -170,6 +174,10 @@ export {
170
174
  executeActions,
171
175
  vetoActions,
172
176
  } from './curatorMulticall'
177
+ export {
178
+ buildUniswapV3Swap,
179
+ encodeUniswapV3SwapCalldata,
180
+ } from './curatorSwaps'
173
181
 
174
182
  // --- wagmi compatibility ---
175
183
  // Re-export viem's PublicClient type for wagmi compatibility.
@@ -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, OFT_ROUTES } from './chains'
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 STARGATE_ASSETS = new Set(['stgUSDC', 'USDT', 'WETH'])
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
@@ -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/types.ts CHANGED
@@ -177,3 +177,19 @@ export interface VaultAnalysis {
177
177
  /** Registry address for global protocol whitelist checks */
178
178
  registryAddress: Address | null
179
179
  }
180
+
181
+ export interface AssetBalance extends AssetInfo {
182
+ /** Raw balance held by the vault */
183
+ balance: bigint
184
+ }
185
+
186
+ export interface VaultAssetBreakdown {
187
+ /** Per-asset balances held by the vault on the hub chain */
188
+ assets: AssetBalance[]
189
+ /** totalAssets() as reported by the vault (all positions converted to underlying) */
190
+ totalAssets: bigint
191
+ /** totalSupply() of vault shares */
192
+ totalSupply: bigint
193
+ /** Vault underlying token decimals */
194
+ underlyingDecimals: number
195
+ }
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
+ }