@oydual31/more-vaults-sdk 0.1.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/README.md +283 -0
- package/dist/ethers/index.cjs +973 -0
- package/dist/ethers/index.cjs.map +1 -0
- package/dist/ethers/index.d.cts +671 -0
- package/dist/ethers/index.d.ts +671 -0
- package/dist/ethers/index.js +924 -0
- package/dist/ethers/index.js.map +1 -0
- package/dist/viem/index.cjs +1426 -0
- package/dist/viem/index.cjs.map +1 -0
- package/dist/viem/index.d.cts +1291 -0
- package/dist/viem/index.d.ts +1291 -0
- package/dist/viem/index.js +1377 -0
- package/dist/viem/index.js.map +1 -0
- package/package.json +46 -0
- package/src/ethers/abis.ts +82 -0
- package/src/ethers/crossChainFlows.ts +206 -0
- package/src/ethers/depositFlows.ts +347 -0
- package/src/ethers/errors.ts +81 -0
- package/src/ethers/index.ts +103 -0
- package/src/ethers/preflight.ts +156 -0
- package/src/ethers/redeemFlows.ts +286 -0
- package/src/ethers/types.ts +67 -0
- package/src/ethers/userHelpers.ts +480 -0
- package/src/ethers/utils.ts +377 -0
- package/src/viem/abis.ts +392 -0
- package/src/viem/crossChainFlows.ts +220 -0
- package/src/viem/depositFlows.ts +331 -0
- package/src/viem/errors.ts +81 -0
- package/src/viem/index.ts +100 -0
- package/src/viem/preflight.ts +204 -0
- package/src/viem/redeemFlows.ts +337 -0
- package/src/viem/types.ts +56 -0
- package/src/viem/userHelpers.ts +489 -0
- package/src/viem/utils.ts +421 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flight validation helpers for MoreVaults SDK flows.
|
|
3
|
+
*
|
|
4
|
+
* Each function reads on-chain state and throws a descriptive error BEFORE
|
|
5
|
+
* the actual contract call, so developers see a clear, actionable message
|
|
6
|
+
* instead of a raw VM revert.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { type Address, type PublicClient, getAddress, zeroAddress } from 'viem'
|
|
10
|
+
import { CONFIG_ABI, BRIDGE_ABI, VAULT_ABI, ERC20_ABI } from './abis'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pre-flight checks for async cross-chain flows (D4 / D5 / R5).
|
|
14
|
+
*
|
|
15
|
+
* Validates that:
|
|
16
|
+
* 1. The CCManager is configured on the vault.
|
|
17
|
+
* 2. An escrow is registered in the vault's registry.
|
|
18
|
+
* 3. The vault is a hub (required for async flows).
|
|
19
|
+
* 4. The vault does NOT have oracle-based cross-chain accounting enabled
|
|
20
|
+
* (oracle-on vaults should use depositSimple / depositCrossChainOracleOn).
|
|
21
|
+
* 5. The vault is not paused.
|
|
22
|
+
*
|
|
23
|
+
* All reads that are independent of each other are executed in parallel via
|
|
24
|
+
* Promise.all to minimise latency.
|
|
25
|
+
*
|
|
26
|
+
* @param publicClient Public client for contract reads
|
|
27
|
+
* @param vault Vault address (diamond proxy)
|
|
28
|
+
* @param escrow Escrow address from VaultAddresses
|
|
29
|
+
*/
|
|
30
|
+
export async function preflightAsync(
|
|
31
|
+
publicClient: PublicClient,
|
|
32
|
+
vault: Address,
|
|
33
|
+
escrow: Address,
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
const v = getAddress(vault)
|
|
36
|
+
|
|
37
|
+
// Parallel read: ccManager, escrow, isHub, oraclesCrossChainAccounting, paused
|
|
38
|
+
const [ccManager, registeredEscrow, isHub, oraclesEnabled, isPaused] =
|
|
39
|
+
await Promise.all([
|
|
40
|
+
publicClient.readContract({
|
|
41
|
+
address: v,
|
|
42
|
+
abi: CONFIG_ABI,
|
|
43
|
+
functionName: 'getCrossChainAccountingManager',
|
|
44
|
+
}),
|
|
45
|
+
publicClient.readContract({
|
|
46
|
+
address: v,
|
|
47
|
+
abi: CONFIG_ABI,
|
|
48
|
+
functionName: 'getEscrow',
|
|
49
|
+
}),
|
|
50
|
+
publicClient.readContract({
|
|
51
|
+
address: v,
|
|
52
|
+
abi: CONFIG_ABI,
|
|
53
|
+
functionName: 'isHub',
|
|
54
|
+
}),
|
|
55
|
+
publicClient.readContract({
|
|
56
|
+
address: v,
|
|
57
|
+
abi: BRIDGE_ABI,
|
|
58
|
+
functionName: 'oraclesCrossChainAccounting',
|
|
59
|
+
}),
|
|
60
|
+
publicClient.readContract({
|
|
61
|
+
address: v,
|
|
62
|
+
abi: CONFIG_ABI,
|
|
63
|
+
functionName: 'paused',
|
|
64
|
+
}),
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
if (ccManager === zeroAddress) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`[MoreVaults] CCManager not configured on vault ${vault}. Call setCrossChainAccountingManager(ccManagerAddress) as vault owner first.`,
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (registeredEscrow === zeroAddress) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`[MoreVaults] Escrow not configured for vault ${vault}. The registry must have an escrow set for this vault.`,
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!isHub) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`[MoreVaults] Vault ${vault} is not a hub vault. Async flows (D4/D5/R5) only work on hub vaults.`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (oraclesEnabled) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`[MoreVaults] Vault ${vault} has oracle-based cross-chain accounting enabled. Use depositSimple/depositCrossChainOracleOn instead of async flows.`,
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isPaused) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`[MoreVaults] Vault ${vault} is paused. Cannot perform any actions.`,
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pre-flight liquidity check for async redeem (R5).
|
|
100
|
+
*
|
|
101
|
+
* Reads the hub's liquid balance of the underlying token and compares it
|
|
102
|
+
* against the assets the user expects to receive. If the hub does not hold
|
|
103
|
+
* enough liquid assets the redeem will be auto-refunded after the LZ round-trip,
|
|
104
|
+
* wasting the LayerZero fee.
|
|
105
|
+
*
|
|
106
|
+
* This check is best-effort: liquidity could change in the 1-5 minutes between
|
|
107
|
+
* submission and execution. But it catches the common case where the hub is
|
|
108
|
+
* already under-funded at the time of submission.
|
|
109
|
+
*
|
|
110
|
+
* @param publicClient Public client for contract reads
|
|
111
|
+
* @param vault Vault address (diamond proxy)
|
|
112
|
+
* @param shares Shares the user intends to redeem
|
|
113
|
+
*/
|
|
114
|
+
export async function preflightRedeemLiquidity(
|
|
115
|
+
publicClient: PublicClient,
|
|
116
|
+
vault: Address,
|
|
117
|
+
shares: bigint,
|
|
118
|
+
): Promise<void> {
|
|
119
|
+
const v = getAddress(vault)
|
|
120
|
+
|
|
121
|
+
// Need underlying address first
|
|
122
|
+
const underlying = await publicClient.readContract({
|
|
123
|
+
address: v,
|
|
124
|
+
abi: VAULT_ABI,
|
|
125
|
+
functionName: 'asset',
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Parallel: hub liquid balance + estimated assets for these shares
|
|
129
|
+
const [hubLiquid, assetsNeeded] = await Promise.all([
|
|
130
|
+
publicClient.readContract({
|
|
131
|
+
address: getAddress(underlying as Address),
|
|
132
|
+
abi: ERC20_ABI,
|
|
133
|
+
functionName: 'balanceOf',
|
|
134
|
+
args: [v],
|
|
135
|
+
}),
|
|
136
|
+
publicClient.readContract({
|
|
137
|
+
address: v,
|
|
138
|
+
abi: VAULT_ABI,
|
|
139
|
+
functionName: 'convertToAssets',
|
|
140
|
+
args: [shares],
|
|
141
|
+
}),
|
|
142
|
+
])
|
|
143
|
+
|
|
144
|
+
if ((hubLiquid as bigint) < (assetsNeeded as bigint)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`[MoreVaults] Insufficient hub liquidity for redeem.\n` +
|
|
147
|
+
` Hub liquid balance : ${hubLiquid}\n` +
|
|
148
|
+
` Estimated required : ${assetsNeeded}\n` +
|
|
149
|
+
`Submitting this redeem will waste the LayerZero fee — the request will be auto-refunded.\n` +
|
|
150
|
+
`Ask the vault curator to repatriate liquidity from spoke chains first.`,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Pre-flight checks for synchronous deposit flows (D1 / D3).
|
|
157
|
+
*
|
|
158
|
+
* Validates that:
|
|
159
|
+
* 1. The vault is not paused.
|
|
160
|
+
* 2. The vault still has deposit capacity (maxDeposit > 0).
|
|
161
|
+
*
|
|
162
|
+
* Both reads are executed in parallel.
|
|
163
|
+
*
|
|
164
|
+
* @param publicClient Public client for contract reads
|
|
165
|
+
* @param vault Vault address (diamond proxy)
|
|
166
|
+
*/
|
|
167
|
+
export async function preflightSync(
|
|
168
|
+
publicClient: PublicClient,
|
|
169
|
+
vault: Address,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
const v = getAddress(vault)
|
|
172
|
+
|
|
173
|
+
// Run paused and maxDeposit in parallel.
|
|
174
|
+
// maxDeposit(address(0)) may REVERT on whitelisted vaults — catch separately.
|
|
175
|
+
const [isPaused, depositCapResult] = await Promise.all([
|
|
176
|
+
publicClient.readContract({
|
|
177
|
+
address: v,
|
|
178
|
+
abi: CONFIG_ABI,
|
|
179
|
+
functionName: 'paused',
|
|
180
|
+
}),
|
|
181
|
+
publicClient
|
|
182
|
+
.readContract({
|
|
183
|
+
address: v,
|
|
184
|
+
abi: CONFIG_ABI,
|
|
185
|
+
functionName: 'maxDeposit',
|
|
186
|
+
args: [zeroAddress],
|
|
187
|
+
})
|
|
188
|
+
.catch(() => null as null),
|
|
189
|
+
])
|
|
190
|
+
|
|
191
|
+
if (isPaused) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`[MoreVaults] Vault ${vault} is paused. Cannot perform any actions.`,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// null means maxDeposit reverted → whitelist vault — skip capacity check
|
|
198
|
+
// (the user may still be whitelisted; canDeposit will do user-specific check)
|
|
199
|
+
if (depositCapResult !== null && depositCapResult === 0n) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`[MoreVaults] Vault ${vault} has reached deposit capacity. No more deposits accepted.`,
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Address,
|
|
3
|
+
type Hash,
|
|
4
|
+
type PublicClient,
|
|
5
|
+
type WalletClient,
|
|
6
|
+
encodeAbiParameters,
|
|
7
|
+
getAddress,
|
|
8
|
+
pad,
|
|
9
|
+
} from 'viem'
|
|
10
|
+
import { VAULT_ABI, BRIDGE_ABI, OFT_ABI } from './abis'
|
|
11
|
+
import type {
|
|
12
|
+
VaultAddresses,
|
|
13
|
+
RedeemResult,
|
|
14
|
+
AsyncRequestResult,
|
|
15
|
+
} from './types'
|
|
16
|
+
import { ActionType } from './types'
|
|
17
|
+
import { ensureAllowance } from './utils'
|
|
18
|
+
import { preflightAsync, preflightRedeemLiquidity } from './preflight'
|
|
19
|
+
import { MissingEscrowAddressError } from './errors'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* R1 — Simple share redemption (ERC-4626 standard).
|
|
23
|
+
*
|
|
24
|
+
* Burns `shares` and returns the proportional amount of underlying assets.
|
|
25
|
+
* If a withdrawal queue is enabled, the caller must have previously called
|
|
26
|
+
* `requestRedeem` and waited for the timelock to expire.
|
|
27
|
+
*
|
|
28
|
+
* **User transactions**: 1 redeem call.
|
|
29
|
+
*
|
|
30
|
+
* @param walletClient Wallet client with account attached
|
|
31
|
+
* @param publicClient Public client for reads and simulation
|
|
32
|
+
* @param addresses Vault address set (only `vault` is used)
|
|
33
|
+
* @param shares Amount of vault shares to redeem
|
|
34
|
+
* @param receiver Address that will receive the underlying assets
|
|
35
|
+
* @param owner Owner of the shares being redeemed
|
|
36
|
+
* @returns Transaction hash and amount of assets received
|
|
37
|
+
*/
|
|
38
|
+
export async function redeemShares(
|
|
39
|
+
walletClient: WalletClient,
|
|
40
|
+
publicClient: PublicClient,
|
|
41
|
+
addresses: VaultAddresses,
|
|
42
|
+
shares: bigint,
|
|
43
|
+
receiver: Address,
|
|
44
|
+
owner: Address,
|
|
45
|
+
): Promise<RedeemResult> {
|
|
46
|
+
const account = walletClient.account!
|
|
47
|
+
const vault = getAddress(addresses.vault)
|
|
48
|
+
|
|
49
|
+
const { result: assets } = await publicClient.simulateContract({
|
|
50
|
+
address: vault,
|
|
51
|
+
abi: VAULT_ABI,
|
|
52
|
+
functionName: 'redeem',
|
|
53
|
+
args: [shares, getAddress(receiver), getAddress(owner)],
|
|
54
|
+
account: account.address,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const txHash = await walletClient.writeContract({
|
|
58
|
+
address: vault,
|
|
59
|
+
abi: VAULT_ABI,
|
|
60
|
+
functionName: 'redeem',
|
|
61
|
+
args: [shares, getAddress(receiver), getAddress(owner)],
|
|
62
|
+
account,
|
|
63
|
+
chain: walletClient.chain,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return { txHash, assets }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* R2 — Withdraw by specifying the exact assets amount.
|
|
71
|
+
*
|
|
72
|
+
* Burns the necessary shares to withdraw exactly `assets` amount of underlying.
|
|
73
|
+
* If a withdrawal queue is enabled, the caller must have previously called
|
|
74
|
+
* `requestWithdraw` and waited for the timelock.
|
|
75
|
+
*
|
|
76
|
+
* **User transactions**: 1 withdraw call.
|
|
77
|
+
*
|
|
78
|
+
* @param walletClient Wallet client with account attached
|
|
79
|
+
* @param publicClient Public client for reads and simulation
|
|
80
|
+
* @param addresses Vault address set (only `vault` is used)
|
|
81
|
+
* @param assets Exact amount of underlying assets to withdraw
|
|
82
|
+
* @param receiver Address that will receive the assets
|
|
83
|
+
* @param owner Owner of the shares being burned
|
|
84
|
+
* @returns Transaction hash and the actual assets withdrawn
|
|
85
|
+
*/
|
|
86
|
+
export async function withdrawAssets(
|
|
87
|
+
walletClient: WalletClient,
|
|
88
|
+
publicClient: PublicClient,
|
|
89
|
+
addresses: VaultAddresses,
|
|
90
|
+
assets: bigint,
|
|
91
|
+
receiver: Address,
|
|
92
|
+
owner: Address,
|
|
93
|
+
): Promise<RedeemResult> {
|
|
94
|
+
const account = walletClient.account!
|
|
95
|
+
const vault = getAddress(addresses.vault)
|
|
96
|
+
|
|
97
|
+
const { result: sharesBurned } = await publicClient.simulateContract({
|
|
98
|
+
address: vault,
|
|
99
|
+
abi: VAULT_ABI,
|
|
100
|
+
functionName: 'withdraw',
|
|
101
|
+
args: [assets, getAddress(receiver), getAddress(owner)],
|
|
102
|
+
account: account.address,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const txHash = await walletClient.writeContract({
|
|
106
|
+
address: vault,
|
|
107
|
+
abi: VAULT_ABI,
|
|
108
|
+
functionName: 'withdraw',
|
|
109
|
+
args: [assets, getAddress(receiver), getAddress(owner)],
|
|
110
|
+
account,
|
|
111
|
+
chain: walletClient.chain,
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// For withdraw, the return is shares burned; assets is what was requested
|
|
115
|
+
return { txHash, assets }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* R3 / R4 — Request redeem (queue shares for withdrawal).
|
|
120
|
+
*
|
|
121
|
+
* Places `shares` into the withdrawal queue. If a timelock is configured (R4),
|
|
122
|
+
* the user must wait until `timelockEndsAt` before calling `redeemShares`.
|
|
123
|
+
* If no timelock (R3), `redeemShares` can be called immediately after.
|
|
124
|
+
*
|
|
125
|
+
* Use `getWithdrawalRequest` to check the timelock status.
|
|
126
|
+
*
|
|
127
|
+
* **User transactions**: 1 requestRedeem call, then later 1 redeemShares call.
|
|
128
|
+
*
|
|
129
|
+
* @param walletClient Wallet client with account attached
|
|
130
|
+
* @param publicClient Public client for simulation
|
|
131
|
+
* @param addresses Vault address set (only `vault` is used)
|
|
132
|
+
* @param shares Amount of shares to queue for redemption
|
|
133
|
+
* @param owner The address on behalf of which the request is made
|
|
134
|
+
* @returns Transaction hash of the request
|
|
135
|
+
*/
|
|
136
|
+
export async function requestRedeem(
|
|
137
|
+
walletClient: WalletClient,
|
|
138
|
+
publicClient: PublicClient,
|
|
139
|
+
addresses: VaultAddresses,
|
|
140
|
+
shares: bigint,
|
|
141
|
+
owner: Address,
|
|
142
|
+
): Promise<{ txHash: Hash }> {
|
|
143
|
+
const account = walletClient.account!
|
|
144
|
+
const vault = getAddress(addresses.vault)
|
|
145
|
+
|
|
146
|
+
await publicClient.simulateContract({
|
|
147
|
+
address: vault,
|
|
148
|
+
abi: VAULT_ABI,
|
|
149
|
+
functionName: 'requestRedeem',
|
|
150
|
+
args: [shares, getAddress(owner)],
|
|
151
|
+
account: account.address,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const txHash = await walletClient.writeContract({
|
|
155
|
+
address: vault,
|
|
156
|
+
abi: VAULT_ABI,
|
|
157
|
+
functionName: 'requestRedeem',
|
|
158
|
+
args: [shares, getAddress(owner)],
|
|
159
|
+
account,
|
|
160
|
+
chain: walletClient.chain,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return { txHash }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Helper — Get the current withdrawal request for an owner.
|
|
168
|
+
*
|
|
169
|
+
* Returns the queued shares and timelock end timestamp.
|
|
170
|
+
* Useful for showing a countdown timer in the UI (R4 flow).
|
|
171
|
+
*
|
|
172
|
+
* @param publicClient Public client for reading state
|
|
173
|
+
* @param vault Vault address
|
|
174
|
+
* @param owner Owner whose request to query
|
|
175
|
+
* @returns Request info or null if no active request
|
|
176
|
+
*/
|
|
177
|
+
export async function getWithdrawalRequest(
|
|
178
|
+
publicClient: PublicClient,
|
|
179
|
+
vault: Address,
|
|
180
|
+
owner: Address,
|
|
181
|
+
): Promise<{ shares: bigint; timelockEndsAt: bigint } | null> {
|
|
182
|
+
const [shares, timelockEndsAt] = await publicClient.readContract({
|
|
183
|
+
address: getAddress(vault),
|
|
184
|
+
abi: VAULT_ABI,
|
|
185
|
+
functionName: 'getWithdrawalRequest',
|
|
186
|
+
args: [getAddress(owner)],
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
if (shares === 0n) return null
|
|
190
|
+
|
|
191
|
+
return { shares, timelockEndsAt }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* R5 — Async redeem (cross-chain hub, oracle OFF).
|
|
196
|
+
*
|
|
197
|
+
* Initiates an async redeem via `initVaultActionRequest(REDEEM, ...)`.
|
|
198
|
+
* Shares are locked in the escrow while the LZ Read resolves accounting.
|
|
199
|
+
* After `executeRequest`, assets are sent to the receiver.
|
|
200
|
+
*
|
|
201
|
+
* **IMPORTANT**: `amountLimit` MUST be 0 for REDEEM actions (not shares).
|
|
202
|
+
* The shares amount is encoded in the actionCallData. Setting amountLimit to
|
|
203
|
+
* a non-zero value would be interpreted as "max assets to receive" (inverted
|
|
204
|
+
* slippage check), which is almost never what the user wants.
|
|
205
|
+
*
|
|
206
|
+
* **User transactions**: 1 approve (shares to ESCROW) + 1 initVaultActionRequest.
|
|
207
|
+
* **Wait**: Assets arrive after LZ callback + executeRequest.
|
|
208
|
+
*
|
|
209
|
+
* @param walletClient Wallet client with account attached
|
|
210
|
+
* @param publicClient Public client for reads and simulation
|
|
211
|
+
* @param addresses Vault address set (`vault` + `escrow` required)
|
|
212
|
+
* @param shares Amount of shares to redeem
|
|
213
|
+
* @param receiver Address that will receive the underlying assets
|
|
214
|
+
* @param owner Owner of the shares (must match the initiator)
|
|
215
|
+
* @param lzFee msg.value for LZ Read fee
|
|
216
|
+
* @param extraOptions Optional LZ extra options bytes
|
|
217
|
+
* @returns Transaction hash and GUID for tracking
|
|
218
|
+
*/
|
|
219
|
+
export async function redeemAsync(
|
|
220
|
+
walletClient: WalletClient,
|
|
221
|
+
publicClient: PublicClient,
|
|
222
|
+
addresses: VaultAddresses,
|
|
223
|
+
shares: bigint,
|
|
224
|
+
receiver: Address,
|
|
225
|
+
owner: Address,
|
|
226
|
+
lzFee: bigint,
|
|
227
|
+
extraOptions: `0x${string}` = '0x',
|
|
228
|
+
): Promise<AsyncRequestResult> {
|
|
229
|
+
const account = walletClient.account!
|
|
230
|
+
const vault = getAddress(addresses.vault)
|
|
231
|
+
if (!addresses.escrow) throw new MissingEscrowAddressError()
|
|
232
|
+
const escrow = getAddress(addresses.escrow)
|
|
233
|
+
|
|
234
|
+
// Pre-flight: validate async cross-chain setup before sending any transaction
|
|
235
|
+
await preflightAsync(publicClient, vault, escrow)
|
|
236
|
+
|
|
237
|
+
// Pre-flight: check hub has enough liquid assets — avoids wasting LZ fee on a guaranteed refund
|
|
238
|
+
await preflightRedeemLiquidity(publicClient, vault, shares)
|
|
239
|
+
|
|
240
|
+
// Approve ESCROW for shares (vault share token is the vault itself for ERC-4626)
|
|
241
|
+
await ensureAllowance(walletClient, publicClient, vault, escrow, shares)
|
|
242
|
+
|
|
243
|
+
// Encode parameters only (no selector) — contracts use abi.decode on these bytes
|
|
244
|
+
const actionCallData = encodeAbiParameters(
|
|
245
|
+
[{ type: 'uint256', name: 'shares' }, { type: 'address', name: 'receiver' }, { type: 'address', name: 'owner' }],
|
|
246
|
+
[shares, getAddress(receiver), getAddress(owner)],
|
|
247
|
+
) as `0x${string}`
|
|
248
|
+
|
|
249
|
+
// amountLimit MUST be 0 for REDEEM — see JSDoc above
|
|
250
|
+
const { result: guid } = await publicClient.simulateContract({
|
|
251
|
+
address: vault,
|
|
252
|
+
abi: BRIDGE_ABI,
|
|
253
|
+
functionName: 'initVaultActionRequest',
|
|
254
|
+
args: [ActionType.REDEEM, actionCallData, 0n, extraOptions],
|
|
255
|
+
value: lzFee,
|
|
256
|
+
account: account.address,
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
const txHash = await walletClient.writeContract({
|
|
260
|
+
address: vault,
|
|
261
|
+
abi: BRIDGE_ABI,
|
|
262
|
+
functionName: 'initVaultActionRequest',
|
|
263
|
+
args: [ActionType.REDEEM, actionCallData, 0n, extraOptions],
|
|
264
|
+
value: lzFee,
|
|
265
|
+
account,
|
|
266
|
+
chain: walletClient.chain,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
return { txHash, guid: guid as `0x${string}` }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* R6 — Bridge shares from spoke to hub chain via OFT.
|
|
274
|
+
*
|
|
275
|
+
* This is step 1 of a 2-step spoke redeem flow:
|
|
276
|
+
* 1. `bridgeSharesToHub()` — send shares from spoke to hub via OFT
|
|
277
|
+
* 2. `redeemShares()` — call redeem on the hub vault (after shares arrive)
|
|
278
|
+
*
|
|
279
|
+
* The two steps happen on different chains and cannot be combined into a single
|
|
280
|
+
* SDK call. The frontend must switch chains between steps.
|
|
281
|
+
*
|
|
282
|
+
* **User transactions on spoke chain**: 1 approve (shares to shareOFT) + 1 OFT.send().
|
|
283
|
+
* **Gas**: Requires native token on spoke for LZ fees, and gas on hub for step 2.
|
|
284
|
+
*
|
|
285
|
+
* @param walletClient Wallet client on the SPOKE chain
|
|
286
|
+
* @param publicClient Public client on the SPOKE chain
|
|
287
|
+
* @param shareOFT OFTAdapter address for vault shares on the spoke chain
|
|
288
|
+
* @param hubChainEid LayerZero Endpoint ID for the hub chain (Flow EVM = 30332)
|
|
289
|
+
* @param shares Amount of vault shares to bridge
|
|
290
|
+
* @param receiver Receiver address on the HUB chain
|
|
291
|
+
* @param lzFee msg.value for OFT send (quote via OFT.quoteSend)
|
|
292
|
+
* @returns Transaction hash of the OFT.send() call
|
|
293
|
+
*/
|
|
294
|
+
export async function bridgeSharesToHub(
|
|
295
|
+
walletClient: WalletClient,
|
|
296
|
+
publicClient: PublicClient,
|
|
297
|
+
shareOFT: Address,
|
|
298
|
+
hubChainEid: number,
|
|
299
|
+
shares: bigint,
|
|
300
|
+
receiver: Address,
|
|
301
|
+
lzFee: bigint,
|
|
302
|
+
): Promise<{ txHash: Hash }> {
|
|
303
|
+
const account = walletClient.account!
|
|
304
|
+
const oft = getAddress(shareOFT)
|
|
305
|
+
|
|
306
|
+
// Approve OFT for share transfer
|
|
307
|
+
await ensureAllowance(walletClient, publicClient, oft, oft, shares)
|
|
308
|
+
|
|
309
|
+
const toBytes32 = pad(getAddress(receiver), { size: 32 })
|
|
310
|
+
|
|
311
|
+
const sendParam = {
|
|
312
|
+
dstEid: hubChainEid,
|
|
313
|
+
to: toBytes32,
|
|
314
|
+
amountLD: shares,
|
|
315
|
+
minAmountLD: shares, // shares should bridge 1:1
|
|
316
|
+
extraOptions: '0x' as `0x${string}`,
|
|
317
|
+
composeMsg: '0x' as `0x${string}`,
|
|
318
|
+
oftCmd: '0x' as `0x${string}`,
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const fee = {
|
|
322
|
+
nativeFee: lzFee,
|
|
323
|
+
lzTokenFee: 0n,
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const txHash = await walletClient.writeContract({
|
|
327
|
+
address: oft,
|
|
328
|
+
abi: OFT_ABI,
|
|
329
|
+
functionName: 'send',
|
|
330
|
+
args: [sendParam, fee, account.address],
|
|
331
|
+
value: lzFee,
|
|
332
|
+
account,
|
|
333
|
+
chain: walletClient.chain,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
return { txHash }
|
|
337
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Address, Hash, PublicClient, WalletClient } from 'viem'
|
|
2
|
+
|
|
3
|
+
export interface VaultAddresses {
|
|
4
|
+
/** Hub vault address (diamond proxy) */
|
|
5
|
+
vault: Address
|
|
6
|
+
/** MoreVaultsEscrow — holds locked tokens during async cross-chain flows */
|
|
7
|
+
escrow?: Address
|
|
8
|
+
/** OFTAdapter for vault shares (cross-chain redeem only) */
|
|
9
|
+
shareOFT?: Address
|
|
10
|
+
/** OFT for USDC bridging (cross-chain deposits from spoke) */
|
|
11
|
+
usdcOFT?: Address
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DepositResult {
|
|
15
|
+
txHash: Hash
|
|
16
|
+
shares: bigint
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RedeemResult {
|
|
20
|
+
txHash: Hash
|
|
21
|
+
assets: bigint
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AsyncRequestResult {
|
|
25
|
+
txHash: Hash
|
|
26
|
+
/** Cross-chain request GUID to track via getRequestInfo / getFinalizationResult */
|
|
27
|
+
guid: `0x${string}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ActionType enum values matching MoreVaultsLib.ActionType on-chain.
|
|
32
|
+
* DEPOSIT=0, MINT=1, WITHDRAW=2, REDEEM=3, MULTI_ASSETS_DEPOSIT=4, ACCRUE_FEES=5
|
|
33
|
+
*/
|
|
34
|
+
export const ActionType = {
|
|
35
|
+
DEPOSIT: 0,
|
|
36
|
+
MINT: 1,
|
|
37
|
+
WITHDRAW: 2,
|
|
38
|
+
REDEEM: 3,
|
|
39
|
+
MULTI_ASSETS_DEPOSIT: 4,
|
|
40
|
+
ACCRUE_FEES: 5,
|
|
41
|
+
} as const
|
|
42
|
+
|
|
43
|
+
export type ActionTypeValue = (typeof ActionType)[keyof typeof ActionType]
|
|
44
|
+
|
|
45
|
+
export interface CrossChainRequestInfo {
|
|
46
|
+
initiator: Address
|
|
47
|
+
timestamp: bigint
|
|
48
|
+
actionType: number
|
|
49
|
+
actionCallData: `0x${string}`
|
|
50
|
+
fulfilled: boolean
|
|
51
|
+
finalized: boolean
|
|
52
|
+
refunded: boolean
|
|
53
|
+
totalAssets: bigint
|
|
54
|
+
finalizationResult: bigint
|
|
55
|
+
amountLimit: bigint
|
|
56
|
+
}
|