@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/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 +197 -28
- package/dist/viem/index.cjs.map +1 -1
- package/dist/viem/index.d.cts +129 -2
- package/dist/viem/index.d.ts +129 -2
- package/dist/viem/index.js +193 -29
- package/dist/viem/index.js.map +1 -1
- package/package.json +1 -1
- package/src/viem/chains.ts +17 -0
- package/src/viem/crossChainFlows.ts +12 -22
- package/src/viem/curatorStatus.ts +66 -2
- package/src/viem/curatorSwaps.ts +239 -0
- package/src/viem/index.ts +9 -1
- package/src/viem/preflight.ts +3 -9
- package/src/viem/redeemFlows.ts +4 -5
- package/src/viem/types.ts +16 -0
- package/src/viem/utils.ts +40 -0
package/package.json
CHANGED
package/src/viem/chains.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
@@ -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.
|
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/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
|
+
}
|