@oydual31/more-vaults-sdk 0.4.1 → 0.5.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 +903 -215
- package/dist/{spokeRoutes-B8Lnk-t4.d.cts → curatorBridge-DTW1lPF7.d.cts} +130 -1
- package/dist/{spokeRoutes-B8Lnk-t4.d.ts → curatorBridge-DTW1lPF7.d.ts} +130 -1
- package/dist/ethers/index.cjs +73 -0
- package/dist/ethers/index.cjs.map +1 -1
- package/dist/ethers/index.d.cts +103 -1
- package/dist/ethers/index.d.ts +103 -1
- package/dist/ethers/index.js +71 -2
- package/dist/ethers/index.js.map +1 -1
- package/dist/react/index.cjs +163 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +214 -2
- package/dist/react/index.d.ts +214 -2
- package/dist/react/index.js +162 -1
- package/dist/react/index.js.map +1 -1
- package/dist/viem/index.cjs +91 -0
- package/dist/viem/index.cjs.map +1 -1
- package/dist/viem/index.d.cts +2 -2
- package/dist/viem/index.d.ts +2 -2
- package/dist/viem/index.js +88 -1
- package/dist/viem/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ethers/curatorBridge.ts +235 -0
- package/src/ethers/index.ts +9 -0
- package/src/react/index.ts +4 -0
- package/src/react/useCuratorBridgeQuote.ts +43 -0
- package/src/react/useExecuteBridge.ts +50 -0
- package/src/viem/curatorBridge.ts +288 -0
- package/src/viem/index.ts +7 -0
package/README.md
CHANGED
|
@@ -1,26 +1,185 @@
|
|
|
1
1
|
# @oydual31/more-vaults-sdk
|
|
2
2
|
|
|
3
|
-
TypeScript SDK for the MoreVaults protocol. Supports **viem/wagmi
|
|
3
|
+
TypeScript SDK for the MoreVaults protocol. Supports **viem/wagmi**, **ethers.js v6**, and **React hooks**.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install @oydual31/more-vaults-sdk
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Table of contents
|
|
12
|
+
|
|
13
|
+
1. [Installation](#installation)
|
|
14
|
+
2. [Quick start](#quick-start)
|
|
15
|
+
3. [Module overview](#module-overview)
|
|
16
|
+
4. [Feature parity table](#feature-parity-table)
|
|
17
|
+
5. [Core concepts](#core-concepts)
|
|
18
|
+
6. [Deposit flows (D1–D7)](#deposit-flows)
|
|
19
|
+
7. [Redeem flows (R1–R5)](#redeem-flows)
|
|
20
|
+
8. [Cross-chain flows](#cross-chain-flows)
|
|
21
|
+
9. [Curator operations](#curator-operations)
|
|
22
|
+
10. [Vault topology & distribution](#vault-topology--distribution)
|
|
23
|
+
11. [Spoke routes](#spoke-routes)
|
|
24
|
+
12. [React hooks reference](#react-hooks-reference)
|
|
25
|
+
13. [Stargate vs Standard OFT handling](#stargate-vs-standard-oft-handling)
|
|
26
|
+
14. [Supported chains](#supported-chains)
|
|
27
|
+
15. [LZ timeouts](#lz-timeouts)
|
|
28
|
+
16. [Pre-flight validation](#pre-flight-validation)
|
|
29
|
+
17. [Error types](#error-types)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @oydual31/more-vaults-sdk
|
|
37
|
+
# or
|
|
38
|
+
yarn add @oydual31/more-vaults-sdk
|
|
39
|
+
# or
|
|
40
|
+
pnpm add @oydual31/more-vaults-sdk
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Peer dependencies** (install only what you use — all are optional):
|
|
44
|
+
|
|
45
|
+
| Package | Version |
|
|
46
|
+
|---------|---------|
|
|
47
|
+
| `viem` | `>=2` |
|
|
48
|
+
| `ethers` | `>=6` |
|
|
49
|
+
| `react` | `>=18` |
|
|
50
|
+
| `wagmi` | `>=2` |
|
|
51
|
+
| `@tanstack/react-query` | `>=5` |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
### viem
|
|
58
|
+
|
|
9
59
|
```ts
|
|
10
|
-
|
|
11
|
-
import {
|
|
60
|
+
import { smartDeposit, smartRedeem, getVaultStatus, waitForAsyncRequest, LZ_TIMEOUTS } from '@oydual31/more-vaults-sdk/viem'
|
|
61
|
+
import { createPublicClient, createWalletClient, http, parseUnits } from 'viem'
|
|
62
|
+
import { base } from 'viem/chains'
|
|
63
|
+
|
|
64
|
+
const VAULT = '0x8f740aba022b3fcc934ab75c581c04b75e72aba6'
|
|
65
|
+
const RPC = 'https://mainnet.base.org'
|
|
66
|
+
|
|
67
|
+
const publicClient = createPublicClient({ chain: base, transport: http(RPC) })
|
|
68
|
+
const walletClient = createWalletClient({ account, chain: base, transport: http(RPC) })
|
|
12
69
|
|
|
13
|
-
//
|
|
14
|
-
|
|
70
|
+
// --- Deposit 100 USDC ---
|
|
71
|
+
const depositResult = await smartDeposit(
|
|
72
|
+
walletClient, publicClient,
|
|
73
|
+
{ vault: VAULT },
|
|
74
|
+
parseUnits('100', 6), // 100 USDC
|
|
75
|
+
account.address,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if ('guid' in depositResult) {
|
|
79
|
+
// Async vault — wait for LZ Read callback (~5 min)
|
|
80
|
+
const final = await waitForAsyncRequest(publicClient, VAULT, depositResult.guid)
|
|
81
|
+
console.log('Shares minted:', final.result)
|
|
82
|
+
} else {
|
|
83
|
+
console.log('Shares minted:', depositResult.shares)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Redeem shares ---
|
|
87
|
+
const redeemResult = await smartRedeem(
|
|
88
|
+
walletClient, publicClient,
|
|
89
|
+
{ vault: VAULT },
|
|
90
|
+
shares,
|
|
91
|
+
account.address,
|
|
92
|
+
account.address,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if ('guid' in redeemResult) {
|
|
96
|
+
const final = await waitForAsyncRequest(publicClient, VAULT, redeemResult.guid)
|
|
97
|
+
console.log('Assets received:', final.result)
|
|
98
|
+
} else {
|
|
99
|
+
console.log('Assets received:', redeemResult.assets)
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### ethers.js
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { smartDeposit, smartRedeem, getVaultStatus } from '@oydual31/more-vaults-sdk/ethers'
|
|
107
|
+
import { Wallet, JsonRpcProvider, parseUnits } from 'ethers'
|
|
108
|
+
|
|
109
|
+
const provider = new JsonRpcProvider('https://mainnet.base.org')
|
|
110
|
+
const signer = new Wallet(PRIVATE_KEY, provider)
|
|
111
|
+
const VAULT = '0x8f740aba022b3fcc934ab75c581c04b75e72aba6'
|
|
112
|
+
|
|
113
|
+
const result = await smartDeposit(signer, { vault: VAULT }, parseUnits('100', 6), signer.address)
|
|
15
114
|
```
|
|
16
115
|
|
|
17
116
|
---
|
|
18
117
|
|
|
19
|
-
##
|
|
118
|
+
## Module overview
|
|
119
|
+
|
|
120
|
+
| Import path | Description | Dependencies |
|
|
121
|
+
|-------------|-------------|--------------|
|
|
122
|
+
| `@oydual31/more-vaults-sdk/viem` | Full-featured SDK — all flows, curator, topology | `viem` |
|
|
123
|
+
| `@oydual31/more-vaults-sdk/ethers` | Same feature set, ethers.js v6 API | `ethers` |
|
|
124
|
+
| `@oydual31/more-vaults-sdk/react` | React hooks built on wagmi + @tanstack/react-query | `react`, `wagmi`, `@tanstack/react-query` |
|
|
20
125
|
|
|
21
|
-
|
|
126
|
+
All three modules expose the same logical features. Choose based on your stack.
|
|
127
|
+
|
|
128
|
+
---
|
|
22
129
|
|
|
23
|
-
|
|
130
|
+
## Feature parity table
|
|
131
|
+
|
|
132
|
+
| Feature | viem | ethers | react |
|
|
133
|
+
|---------|------|--------|-------|
|
|
134
|
+
| `smartDeposit` / `useSmartDeposit` | Yes | Yes | Yes |
|
|
135
|
+
| `smartRedeem` / `useSmartRedeem` | Yes | Yes | Yes |
|
|
136
|
+
| `depositSimple` / `useDepositSimple` | Yes | Yes | Yes |
|
|
137
|
+
| `redeemShares` / `useRedeemShares` | Yes | Yes | Yes |
|
|
138
|
+
| `depositAsync`, `mintAsync` | Yes | Yes | — |
|
|
139
|
+
| `redeemAsync` | Yes | Yes | — |
|
|
140
|
+
| `depositMultiAsset` | Yes | Yes | — |
|
|
141
|
+
| `requestRedeem`, `getWithdrawalRequest` | Yes | Yes | — |
|
|
142
|
+
| `withdrawAssets` | Yes | Yes | — |
|
|
143
|
+
| `depositFromSpoke`, `depositFromSpokeAsync` | Yes | Yes | — |
|
|
144
|
+
| `quoteDepositFromSpokeFee` | Yes | Yes | — |
|
|
145
|
+
| `waitForCompose`, `quoteComposeFee`, `executeCompose` | Yes | Yes | — |
|
|
146
|
+
| `bridgeSharesToHub`, `bridgeAssetsToSpoke` | Yes | Yes | — |
|
|
147
|
+
| `resolveRedeemAddresses`, `quoteShareBridgeFee` | Yes | Yes | — |
|
|
148
|
+
| `getVaultStatus` | Yes | Yes | `useVaultStatus` |
|
|
149
|
+
| `getVaultMetadata` | Yes | Yes | `useVaultMetadata` |
|
|
150
|
+
| `getUserPosition` | Yes | Yes | `useUserPosition` |
|
|
151
|
+
| `getUserPositionMultiChain` | Yes | Yes | `useUserPositionMultiChain` |
|
|
152
|
+
| `previewDeposit`, `previewRedeem` | Yes | Yes | — |
|
|
153
|
+
| `canDeposit` | Yes | Yes | — |
|
|
154
|
+
| `getUserBalances`, `getMaxWithdrawable` | Yes | Yes | — |
|
|
155
|
+
| `getVaultSummary` | Yes | Yes | — |
|
|
156
|
+
| `quoteLzFee` | Yes | Yes | `useLzFee` |
|
|
157
|
+
| `getAsyncRequestStatusLabel` | Yes | Yes | `useAsyncRequestStatus` |
|
|
158
|
+
| `waitForAsyncRequest` | Yes | — | — |
|
|
159
|
+
| `getVaultTopology`, `getFullVaultTopology`, `discoverVaultTopology` | Yes | Yes | `useVaultTopology` |
|
|
160
|
+
| `isOnHubChain`, `getAllVaultChainIds` | Yes | Yes | — |
|
|
161
|
+
| `getVaultDistribution`, `getVaultDistributionWithTopology` | Yes | Yes | `useVaultDistribution` |
|
|
162
|
+
| `getInboundRoutes` | Yes | Yes | `useInboundRoutes` |
|
|
163
|
+
| `getUserBalancesForRoutes` | Yes | Yes | — |
|
|
164
|
+
| `getOutboundRoutes`, `quoteRouteDepositFee` | Yes | Yes | — |
|
|
165
|
+
| `getCuratorVaultStatus` | Yes | Yes | `useCuratorVaultStatus` |
|
|
166
|
+
| `getPendingActions` | Yes | Yes | `usePendingActions` |
|
|
167
|
+
| `isCurator` | Yes | Yes | `useIsCurator` |
|
|
168
|
+
| `getVaultAnalysis` | Yes | Yes | `useVaultAnalysis` |
|
|
169
|
+
| `getVaultAssetBreakdown` | Yes | Yes | `useVaultAssetBreakdown` |
|
|
170
|
+
| `checkProtocolWhitelist` | Yes | Yes | `useProtocolWhitelist` |
|
|
171
|
+
| `encodeCuratorAction`, `buildCuratorBatch` | Yes | Yes | — |
|
|
172
|
+
| `submitActions` | Yes | Yes | `useSubmitActions` |
|
|
173
|
+
| `executeActions` | Yes | Yes | `useExecuteActions` |
|
|
174
|
+
| `vetoActions` | Yes | Yes | `useVetoActions` |
|
|
175
|
+
| `buildUniswapV3Swap`, `encodeUniswapV3SwapCalldata` | Yes | Yes | — |
|
|
176
|
+
| `quoteCuratorBridgeFee`, `executeCuratorBridge` | Yes | Yes | `useCuratorBridgeQuote`, `useExecuteBridge` |
|
|
177
|
+
| `findBridgeRoute`, `encodeBridgeParams` | Yes | Yes | — |
|
|
178
|
+
| `detectStargateOft` | Yes | Yes | — |
|
|
179
|
+
| `preflightSync`, `preflightAsync` | Yes | Yes | — |
|
|
180
|
+
| `preflightSpokeDeposit`, `preflightSpokeRedeem` | Yes | Yes | — |
|
|
181
|
+
| `preflightRedeemLiquidity` | Yes | Yes | — |
|
|
182
|
+
| Chain constants, ABIs, error types | Yes | Yes | — |
|
|
24
183
|
|
|
25
184
|
---
|
|
26
185
|
|
|
@@ -28,9 +187,9 @@ Each vault is a **diamond proxy** (EIP-2535) — a single address that routes ca
|
|
|
28
187
|
|
|
29
188
|
### Assets and shares
|
|
30
189
|
|
|
31
|
-
- **Asset
|
|
32
|
-
- **Shares**: what the vault mints when you deposit.
|
|
33
|
-
- **Share price**: how many assets one share is worth
|
|
190
|
+
- **Asset**: the token users deposit (e.g. USDC). Always the same token in and out.
|
|
191
|
+
- **Shares**: what the vault mints when you deposit. Represent your ownership percentage. As the vault earns yield, each share becomes worth more assets. Shares are ERC-20 tokens at the vault address.
|
|
192
|
+
- **Share price**: how many assets one share is worth. Starts at 1:1 and grows over time.
|
|
34
193
|
|
|
35
194
|
```
|
|
36
195
|
Deposit 100 USDC → receive 100 shares (at launch, price = 1)
|
|
@@ -42,122 +201,105 @@ Redeem 100 shares → receive 105 USDC
|
|
|
42
201
|
|
|
43
202
|
### Hub and spoke
|
|
44
203
|
|
|
45
|
-
MoreVaults uses a **hub-and-spoke** model
|
|
204
|
+
MoreVaults uses a **hub-and-spoke** model:
|
|
46
205
|
|
|
47
|
-
- **Hub** (`isHub = true`): the chain where the vault does its accounting — mints/burns shares, accepts deposits and redemptions.
|
|
48
|
-
- **Spoke**: a
|
|
206
|
+
- **Hub** (`isHub = true`): the chain where the vault does its accounting — mints/burns shares, accepts deposits and redemptions.
|
|
207
|
+
- **Spoke**: a chain where the vault has deployed funds for yield. Users on spoke chains bridge tokens to the hub via LayerZero OFT.
|
|
49
208
|
|
|
50
|
-
If
|
|
209
|
+
If `isHub = false`, the vault is a single-chain vault — no cross-chain flows apply, use D1/R1.
|
|
51
210
|
|
|
52
211
|
### Vault modes
|
|
53
212
|
|
|
54
|
-
|
|
213
|
+
Use `getVaultStatus()` to read the current mode:
|
|
55
214
|
|
|
56
|
-
| Mode | `isHub` | Oracle |
|
|
57
|
-
|
|
215
|
+
| Mode | `isHub` | Oracle | Description | Applicable flows |
|
|
216
|
+
|------|---------|--------|-------------|-----------------|
|
|
58
217
|
| `local` | false | — | Single-chain vault. No cross-chain. | D1, D2, R1, R2 |
|
|
59
|
-
| `cross-chain-oracle` | true | ON | Hub with
|
|
60
|
-
| `cross-chain-async` | true | OFF | Hub where spoke balances
|
|
218
|
+
| `cross-chain-oracle` | true | ON | Hub with oracle-fed spoke balances. Synchronous like `local`. | D1/D3, D2, R1, R2 |
|
|
219
|
+
| `cross-chain-async` | true | OFF | Hub where spoke balances require a LZ Read query. Async deposits/redeems. | D4, D5, R5 |
|
|
61
220
|
| `paused` | — | — | No deposits or redeems accepted. | None |
|
|
62
221
|
| `full` | — | — | Deposit capacity reached. Redeems still work. | R1, R2 only |
|
|
63
222
|
|
|
64
223
|
### Oracle ON vs OFF
|
|
65
224
|
|
|
66
|
-
When `oraclesCrossChainAccounting = true`, the vault has a configured oracle
|
|
225
|
+
When `oraclesCrossChainAccounting = true`, the vault has a configured oracle that knows the current value of spoke deployments. `totalAssets()` resolves instantly — flows are synchronous.
|
|
67
226
|
|
|
68
|
-
When
|
|
227
|
+
When `false`, the vault must query spokes via **LayerZero Read** to calculate share prices. Deposits and redeems are **async** — the user locks funds, waits for the oracle response (~1–5 min), and a keeper finalizes.
|
|
69
228
|
|
|
70
229
|
### Hub liquidity and repatriation
|
|
71
230
|
|
|
72
|
-
In a cross-chain vault, the hub typically holds only a
|
|
231
|
+
In a cross-chain vault, the hub typically holds only a fraction of TVL as liquid assets. The rest is deployed to spoke chains.
|
|
73
232
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
-
|
|
77
|
-
- **
|
|
78
|
-
- For async redeems (R5), a failed `executeRequest` causes the vault to **auto-refund shares** back to the user — no assets are lost, but the redeem did not complete.
|
|
79
|
-
|
|
80
|
-
**Repatriation** is the process of moving funds from spokes back to the hub so they become liquid again. This is a **manual, curator-only operation** (`executeBridging`). There is no automatic mechanism — the protocol does not pull funds from spokes on behalf of users.
|
|
233
|
+
- `totalAssets()` = hub liquid balance + value of all spoke positions.
|
|
234
|
+
- **Redeemable now** = hub liquid balance only. Attempting to redeem more than the hub holds fails.
|
|
235
|
+
- For async redeems (R5), a failed `executeRequest` causes the vault to refund shares — no assets are lost.
|
|
236
|
+
- **Repatriation** (moving funds from spokes to the hub) is a curator-only operation.
|
|
81
237
|
|
|
82
238
|
### Withdrawal queue and timelock
|
|
83
239
|
|
|
84
|
-
Some vaults require shares to be
|
|
85
|
-
|
|
86
|
-
- **`withdrawalQueueEnabled = true`**: users must call `requestRedeem` first, then `redeemShares` separately.
|
|
87
|
-
- **`withdrawalTimelockSeconds > 0`**: there is a mandatory waiting period between `requestRedeem` and `redeemShares`. Useful for vaults that need time to rebalance liquidity.
|
|
240
|
+
Some vaults require shares to be queued before redemption:
|
|
88
241
|
|
|
89
|
-
|
|
242
|
+
- `withdrawalQueueEnabled = true`: users must call `requestRedeem` first, then `redeemShares` separately.
|
|
243
|
+
- `withdrawalTimelockSeconds > 0`: mandatory waiting period between `requestRedeem` and `redeemShares`.
|
|
90
244
|
|
|
91
245
|
### Escrow
|
|
92
246
|
|
|
93
|
-
The `MoreVaultsEscrow`
|
|
247
|
+
The `MoreVaultsEscrow` temporarily holds user funds during async flows (D4, D5, R5). Tokens go to the escrow while the LZ Read resolves. The SDK handles the approve-to-escrow step internally.
|
|
94
248
|
|
|
95
|
-
**You never interact with the escrow directly.** The SDK handles the approve-to-escrow step internally. You just need to pass its address in `VaultAddresses.escrow`.
|
|
96
|
-
|
|
97
|
-
To get the escrow address: read it from the vault itself:
|
|
98
249
|
```ts
|
|
99
250
|
const status = await getVaultStatus(publicClient, VAULT_ADDRESS)
|
|
100
251
|
const escrow = status.escrow // address(0) if not configured
|
|
101
252
|
```
|
|
102
253
|
|
|
103
|
-
### Same address on every chain (CREATE3)
|
|
104
|
-
|
|
105
|
-
MoreVaults deploys all contracts using **CREATE3**, which means a vault has the **same address on every chain** where it exists. If the hub vault on Base is `0xABC...`, the corresponding escrow and spoke-side contracts are also at predictable, identical addresses across Arbitrum, Ethereum, etc.
|
|
106
|
-
|
|
107
254
|
### VaultAddresses
|
|
108
255
|
|
|
109
|
-
Every flow function takes a `VaultAddresses` object:
|
|
110
|
-
|
|
111
256
|
```ts
|
|
112
257
|
interface VaultAddresses {
|
|
113
258
|
vault: Address // Vault address — same on every chain (CREATE3)
|
|
114
259
|
escrow?: Address // MoreVaultsEscrow — required for D4, D5, R5 (auto-resolved if omitted)
|
|
115
|
-
hubChainId?: number // Optional chain validation
|
|
260
|
+
hubChainId?: number // Optional chain validation guard
|
|
116
261
|
}
|
|
117
262
|
```
|
|
118
263
|
|
|
119
|
-
For simple hub flows (D1, R1) you only need `vault`. For async flows the SDK auto-resolves the escrow from the vault if not provided.
|
|
120
|
-
|
|
121
264
|
### LayerZero EID
|
|
122
265
|
|
|
123
|
-
LayerZero identifies chains by an **Endpoint ID (EID)** — different from the
|
|
266
|
+
LayerZero identifies chains by an **Endpoint ID (EID)** — different from the EVM chain ID:
|
|
124
267
|
|
|
125
268
|
| Chain | Chain ID | LayerZero EID |
|
|
126
269
|
|-------|----------|---------------|
|
|
127
270
|
| Ethereum | 1 | 30101 |
|
|
128
271
|
| Arbitrum | 42161 | 30110 |
|
|
272
|
+
| Optimism | 10 | 30111 |
|
|
129
273
|
| Base | 8453 | 30184 |
|
|
274
|
+
| BNB Chain | 56 | 30102 |
|
|
275
|
+
| Sonic | 146 | 30332 |
|
|
130
276
|
| Flow EVM | 747 | 30336 |
|
|
131
277
|
|
|
132
278
|
### GUID (async request ID)
|
|
133
279
|
|
|
134
|
-
When you call `depositAsync`, `mintAsync`, or `redeemAsync`, the function returns a `guid` — a `bytes32` identifier for that
|
|
280
|
+
When you call `depositAsync`, `mintAsync`, or `redeemAsync`, the function returns a `guid` — a `bytes32` identifier for that cross-chain request:
|
|
135
281
|
|
|
136
282
|
```ts
|
|
137
283
|
const { guid } = await depositAsync(...)
|
|
138
284
|
|
|
139
|
-
//
|
|
140
|
-
const final = await waitForAsyncRequest(publicClient,
|
|
285
|
+
// Wait for finalization (recommended)
|
|
286
|
+
const final = await waitForAsyncRequest(publicClient, VAULT, guid)
|
|
141
287
|
// final.status: 'completed' | 'refunded'
|
|
142
288
|
// final.result: exact shares minted or assets received (bigint)
|
|
143
289
|
|
|
144
|
-
//
|
|
145
|
-
const info = await getAsyncRequestStatusLabel(publicClient,
|
|
290
|
+
// Check status once
|
|
291
|
+
const info = await getAsyncRequestStatusLabel(publicClient, VAULT, guid)
|
|
146
292
|
// info.label: 'pending' | 'fulfilled' | 'finalized' | 'refunded'
|
|
147
293
|
```
|
|
148
294
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
## Clients
|
|
152
|
-
|
|
153
|
-
Every SDK function takes one or two "clients" as its first arguments — the objects that talk to the blockchain.
|
|
295
|
+
### Clients
|
|
154
296
|
|
|
155
297
|
**viem** uses two separate objects:
|
|
156
298
|
|
|
157
|
-
| Client | Role |
|
|
158
|
-
|
|
159
|
-
| `publicClient` | Read-only
|
|
160
|
-
| `walletClient` | Signs and sends transactions.
|
|
299
|
+
| Client | Role |
|
|
300
|
+
|--------|------|
|
|
301
|
+
| `publicClient` | Read-only. `createPublicClient({ chain, transport: http(RPC_URL) })` |
|
|
302
|
+
| `walletClient` | Signs and sends transactions. `createWalletClient({ account, chain, transport: http(RPC_URL) })` |
|
|
161
303
|
|
|
162
304
|
In React with wagmi:
|
|
163
305
|
```ts
|
|
@@ -166,257 +308,803 @@ const publicClient = usePublicClient()
|
|
|
166
308
|
const { data: walletClient } = useWalletClient()
|
|
167
309
|
```
|
|
168
310
|
|
|
169
|
-
**ethers.js** uses a single `Signer
|
|
311
|
+
**ethers.js** uses a single `Signer`:
|
|
170
312
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
313
|
+
```ts
|
|
314
|
+
// Browser
|
|
315
|
+
const signer = await new BrowserProvider(window.ethereum).getSigner()
|
|
316
|
+
// Node.js
|
|
317
|
+
const signer = new Wallet(PRIVATE_KEY, new JsonRpcProvider(RPC_URL))
|
|
318
|
+
```
|
|
175
319
|
|
|
176
|
-
> The client's chain must match the chain where the vault lives. Hub flows
|
|
320
|
+
> The client's chain must match the chain where the vault lives. Hub flows use the hub chain client. Spoke flows (D6/D7) use the spoke chain client.
|
|
177
321
|
|
|
178
322
|
---
|
|
179
323
|
|
|
180
|
-
##
|
|
324
|
+
## Deposit flows
|
|
181
325
|
|
|
182
|
-
|
|
326
|
+
### Smart flows (recommended)
|
|
183
327
|
|
|
184
|
-
|
|
328
|
+
`smartDeposit` auto-detects the vault type and routes to the correct flow:
|
|
329
|
+
|
|
330
|
+
| Vault mode | What `smartDeposit` calls |
|
|
331
|
+
|------------|--------------------------|
|
|
332
|
+
| `local` or `cross-chain-oracle` | `depositSimple` (synchronous) |
|
|
333
|
+
| `cross-chain-async` | `depositAsync` (async, returns `guid`) |
|
|
185
334
|
|
|
186
335
|
```ts
|
|
187
|
-
import { smartDeposit
|
|
188
|
-
import { createPublicClient, createWalletClient, http, parseUnits } from 'viem'
|
|
189
|
-
import { base } from 'viem/chains'
|
|
336
|
+
import { smartDeposit } from '@oydual31/more-vaults-sdk/viem'
|
|
190
337
|
|
|
191
|
-
const
|
|
192
|
-
const walletClient = createWalletClient({ account, chain: base, transport: http(RPC_URL) })
|
|
338
|
+
const result = await smartDeposit(walletClient, publicClient, { vault: VAULT }, amount, receiver)
|
|
193
339
|
|
|
194
|
-
|
|
340
|
+
if ('guid' in result) {
|
|
341
|
+
// Async vault — poll for finalization
|
|
342
|
+
console.log(result.guid)
|
|
343
|
+
} else {
|
|
344
|
+
// Sync vault — shares available immediately
|
|
345
|
+
console.log(result.shares)
|
|
346
|
+
}
|
|
347
|
+
```
|
|
195
348
|
|
|
196
|
-
|
|
197
|
-
|
|
349
|
+
### Hub-chain deposit flows
|
|
350
|
+
|
|
351
|
+
| ID | Function | When to use |
|
|
352
|
+
|----|----------|-------------|
|
|
353
|
+
| — | `smartDeposit` | Recommended. Auto-detects vault type. |
|
|
354
|
+
| D1 | `depositSimple` | User on hub chain, oracle ON or local vault |
|
|
355
|
+
| D2 | `depositMultiAsset` | Deposit multiple tokens in one call |
|
|
356
|
+
| D3 | `depositCrossChainOracleOn` | Alias for D1 — hub with oracle ON |
|
|
357
|
+
| D4 | `depositAsync` | Hub with oracle OFF — async LZ Read. Returns `guid`. |
|
|
358
|
+
| D5 | `mintAsync` | Same as D4 but user specifies exact share amount |
|
|
359
|
+
|
|
360
|
+
**D1 — Simple deposit:**
|
|
361
|
+
```ts
|
|
362
|
+
import { depositSimple } from '@oydual31/more-vaults-sdk/viem'
|
|
363
|
+
|
|
364
|
+
const { txHash, shares } = await depositSimple(
|
|
198
365
|
walletClient, publicClient,
|
|
199
366
|
{ vault: VAULT },
|
|
200
|
-
parseUnits('100', 6),
|
|
367
|
+
parseUnits('100', 6),
|
|
201
368
|
account.address,
|
|
202
369
|
)
|
|
370
|
+
```
|
|
203
371
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
console.log('GUID:', depositResult.guid)
|
|
372
|
+
**D4 — Async deposit (oracle OFF):**
|
|
373
|
+
```ts
|
|
374
|
+
import { depositAsync, waitForAsyncRequest, quoteLzFee } from '@oydual31/more-vaults-sdk/viem'
|
|
208
375
|
|
|
209
|
-
|
|
210
|
-
const final = await waitForAsyncRequest(publicClient, VAULT, depositResult.guid)
|
|
211
|
-
// final.status: 'completed' | 'refunded'
|
|
212
|
-
// final.result: shares minted (bigint)
|
|
213
|
-
console.log('Shares minted:', final.result)
|
|
214
|
-
} else {
|
|
215
|
-
console.log('Sync deposit — shares:', depositResult.shares)
|
|
216
|
-
}
|
|
376
|
+
const lzFee = await quoteLzFee(publicClient, VAULT)
|
|
217
377
|
|
|
218
|
-
|
|
219
|
-
const redeemResult = await smartRedeem(
|
|
378
|
+
const { txHash, guid } = await depositAsync(
|
|
220
379
|
walletClient, publicClient,
|
|
221
380
|
{ vault: VAULT },
|
|
222
|
-
|
|
223
|
-
account.address,
|
|
381
|
+
parseUnits('100', 6),
|
|
224
382
|
account.address,
|
|
383
|
+
lzFee,
|
|
225
384
|
)
|
|
226
385
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const final = await waitForAsyncRequest(publicClient, VAULT, redeemResult.guid)
|
|
231
|
-
console.log('Assets received:', final.result)
|
|
232
|
-
} else {
|
|
233
|
-
console.log('Sync redeem — assets:', redeemResult.assets)
|
|
234
|
-
}
|
|
386
|
+
const final = await waitForAsyncRequest(publicClient, VAULT, guid)
|
|
387
|
+
// final.status: 'completed' | 'refunded'
|
|
388
|
+
// final.result: shares minted (bigint)
|
|
235
389
|
```
|
|
236
390
|
|
|
237
391
|
---
|
|
238
392
|
|
|
239
|
-
##
|
|
393
|
+
## Redeem flows
|
|
240
394
|
|
|
241
|
-
### Smart flows (
|
|
395
|
+
### Smart flows (recommended)
|
|
242
396
|
|
|
243
|
-
|
|
244
|
-
|----------|-------------|
|
|
245
|
-
| `smartDeposit` | Auto-detects vault mode → `depositSimple` (sync) or `depositAsync` (async) |
|
|
246
|
-
| `smartRedeem` | Auto-detects vault mode → `redeemShares` (sync) or `redeemAsync` (async) |
|
|
397
|
+
`smartRedeem` auto-detects the vault type and routes to the correct flow:
|
|
247
398
|
|
|
248
|
-
|
|
399
|
+
| Vault mode | What `smartRedeem` calls |
|
|
400
|
+
|------------|------------------------|
|
|
401
|
+
| `local` or `cross-chain-oracle` | `redeemShares` (synchronous) |
|
|
402
|
+
| `cross-chain-async` | `redeemAsync` (async, returns `guid`) |
|
|
249
403
|
|
|
250
|
-
|
|
251
|
-
|----|----------|-------------|-----|
|
|
252
|
-
| — | `smartDeposit` | **Recommended.** Auto-detects vault type. | — |
|
|
253
|
-
| D1 | `depositSimple` | User on hub chain, oracle ON or local vault | [->](./docs/flows/D1-deposit-simple.md) |
|
|
254
|
-
| D2 | `depositMultiAsset` | Deposit multiple tokens in one call | [->](./docs/flows/D2-deposit-multi-asset.md) |
|
|
255
|
-
| D3 | `depositCrossChainOracleOn` | Alias for D1 — hub with oracle ON | [->](./docs/flows/D3-deposit-oracle-on.md) |
|
|
256
|
-
| D4 | `depositAsync` | Hub with oracle OFF — async LZ Read | [->](./docs/flows/D4-deposit-async.md) |
|
|
257
|
-
| D5 | `mintAsync` | Same as D4 but user specifies exact share amount | [->](./docs/flows/D5-mint-async.md) |
|
|
404
|
+
### Hub-chain redeem flows
|
|
258
405
|
|
|
259
|
-
|
|
406
|
+
| ID | Function | When to use |
|
|
407
|
+
|----|----------|-------------|
|
|
408
|
+
| — | `smartRedeem` | Recommended. Auto-detects vault type. |
|
|
409
|
+
| R1 | `redeemShares` | Standard redeem, hub chain, no queue |
|
|
410
|
+
| R2 | `withdrawAssets` | Specify exact asset amount to receive |
|
|
411
|
+
| R3 | `requestRedeem` | Withdrawal queue enabled, no timelock |
|
|
412
|
+
| R4 | `requestRedeem` | Withdrawal queue + mandatory wait period |
|
|
413
|
+
| R5 | `redeemAsync` | Hub with oracle OFF — async LZ Read. Returns `guid`. |
|
|
260
414
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
415
|
+
**R1 — Simple redeem:**
|
|
416
|
+
```ts
|
|
417
|
+
import { redeemShares } from '@oydual31/more-vaults-sdk/viem'
|
|
264
418
|
|
|
265
|
-
|
|
419
|
+
const { txHash, assets } = await redeemShares(
|
|
420
|
+
walletClient, publicClient,
|
|
421
|
+
{ vault: VAULT },
|
|
422
|
+
shares,
|
|
423
|
+
account.address, // receiver
|
|
424
|
+
account.address, // owner
|
|
425
|
+
)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**R3/R4 — Queued redeem:**
|
|
429
|
+
```ts
|
|
430
|
+
import { requestRedeem, redeemShares, getWithdrawalRequest } from '@oydual31/more-vaults-sdk/viem'
|
|
431
|
+
|
|
432
|
+
// Step 1: queue the request
|
|
433
|
+
await requestRedeem(walletClient, publicClient, { vault: VAULT }, shares, account.address)
|
|
266
434
|
|
|
267
|
-
|
|
435
|
+
// Step 2: wait for timelock to expire (if configured), then redeem
|
|
436
|
+
// Check status
|
|
437
|
+
const request = await getWithdrawalRequest(publicClient, VAULT, account.address)
|
|
268
438
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
| R5 | `redeemAsync` | Hub with oracle OFF — async LZ Read | [->](./docs/flows/R5-redeem-async.md) |
|
|
439
|
+
// Step 3: execute redeem
|
|
440
|
+
await redeemShares(walletClient, publicClient, { vault: VAULT }, shares, account.address, account.address)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Cross-chain flows
|
|
277
446
|
|
|
278
|
-
###
|
|
447
|
+
### Spoke deposit (D6 / D7)
|
|
279
448
|
|
|
280
|
-
|
|
449
|
+
Deposits from a spoke chain to the hub vault via LayerZero OFT Compose:
|
|
281
450
|
|
|
451
|
+
- **D6 (oracle ON)**: composer calls `_depositAndSend` — shares arrive on spoke in ~1 LZ round-trip.
|
|
452
|
+
- **D7 (oracle OFF)**: composer calls `_initDeposit` — requires an additional LZ Read round-trip.
|
|
453
|
+
|
|
454
|
+
The interface is identical for both. The SDK detects which path the composer takes.
|
|
455
|
+
|
|
456
|
+
```ts
|
|
457
|
+
import {
|
|
458
|
+
getInboundRoutes,
|
|
459
|
+
quoteDepositFromSpokeFee,
|
|
460
|
+
depositFromSpoke,
|
|
461
|
+
waitForCompose,
|
|
462
|
+
quoteComposeFee,
|
|
463
|
+
executeCompose,
|
|
464
|
+
} from '@oydual31/more-vaults-sdk/viem'
|
|
465
|
+
import { LZ_EIDS } from '@oydual31/more-vaults-sdk/viem'
|
|
466
|
+
|
|
467
|
+
// 1. Discover available routes
|
|
468
|
+
const routes = await getInboundRoutes(hubChainId, VAULT, vaultAsset, userAddress)
|
|
469
|
+
|
|
470
|
+
// 2. Quote the LZ fee for the chosen route
|
|
471
|
+
const lzFee = await quoteDepositFromSpokeFee(
|
|
472
|
+
spokePublicClient,
|
|
473
|
+
VAULT,
|
|
474
|
+
route.spokeOft,
|
|
475
|
+
LZ_EIDS.BASE, // hubEid
|
|
476
|
+
LZ_EIDS.ETH, // spokeEid
|
|
477
|
+
amount,
|
|
478
|
+
account.address,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
// 3. Send from spoke chain
|
|
482
|
+
const { txHash, guid, composeData } = await depositFromSpoke(
|
|
483
|
+
spokeWalletClient, spokePublicClient,
|
|
484
|
+
VAULT,
|
|
485
|
+
route.spokeOft,
|
|
486
|
+
LZ_EIDS.BASE, // hubEid
|
|
487
|
+
LZ_EIDS.ETH, // spokeEid
|
|
488
|
+
amount,
|
|
489
|
+
account.address,
|
|
490
|
+
lzFee,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
// 4. For Stargate OFTs: execute the pending compose on the hub (2-TX flow)
|
|
494
|
+
if (composeData) {
|
|
495
|
+
const fullComposeData = await waitForCompose(hubPublicClient, composeData, account.address)
|
|
496
|
+
const composeFee = await quoteComposeFee(hubPublicClient, VAULT, LZ_EIDS.ETH, account.address)
|
|
497
|
+
const { txHash: composeTxHash, guid: asyncGuid } = await executeCompose(
|
|
498
|
+
hubWalletClient, hubPublicClient, fullComposeData, composeFee,
|
|
499
|
+
)
|
|
500
|
+
// For D7 vaults, asyncGuid is present — poll finalization
|
|
501
|
+
if (asyncGuid) {
|
|
502
|
+
const final = await waitForAsyncRequest(hubPublicClient, VAULT, asyncGuid)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// For standard OFTs: no action needed — compose executes automatically in 1 TX.
|
|
282
506
|
```
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
507
|
+
|
|
508
|
+
### Spoke redeem (3-step flow)
|
|
509
|
+
|
|
510
|
+
Full spoke redeem moves shares from spoke to hub, redeems, then bridges assets back:
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
Step 1 (Spoke): bridgeSharesToHub() — bridge shares spoke→hub via SHARE_OFT (~7 min)
|
|
514
|
+
Step 2 (Hub): smartRedeem() — redeem on hub (auto-detects async, ~5 min callback)
|
|
515
|
+
Step 3 (Hub): bridgeAssetsToSpoke() — bridge assets hub→spoke via Stargate/OFT (~13 min)
|
|
286
516
|
```
|
|
287
517
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
518
|
+
```ts
|
|
519
|
+
import {
|
|
520
|
+
resolveRedeemAddresses,
|
|
521
|
+
preflightSpokeRedeem,
|
|
522
|
+
bridgeSharesToHub,
|
|
523
|
+
quoteShareBridgeFee,
|
|
524
|
+
smartRedeem,
|
|
525
|
+
bridgeAssetsToSpoke,
|
|
526
|
+
} from '@oydual31/more-vaults-sdk/viem'
|
|
527
|
+
|
|
528
|
+
// Pre-step: resolve all contract addresses dynamically
|
|
529
|
+
const addresses = await resolveRedeemAddresses(publicClient, VAULT, spokeChainId)
|
|
530
|
+
|
|
531
|
+
// Pre-step: validate balances and gas
|
|
532
|
+
const check = await preflightSpokeRedeem(route, shares, userAddress, shareBridgeFee)
|
|
295
533
|
|
|
296
|
-
|
|
534
|
+
// Step 1: bridge shares to hub
|
|
535
|
+
const shareFee = await quoteShareBridgeFee(spokePublicClient, VAULT, hubEid, account.address)
|
|
536
|
+
const { txHash } = await bridgeSharesToHub(spokeWalletClient, spokePublicClient, route, shares, account.address, shareFee)
|
|
297
537
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
###
|
|
306
|
-
|
|
307
|
-
Full reference: [docs/user-helpers.md](./docs/user-helpers.md)
|
|
308
|
-
|
|
309
|
-
| Function | What it returns |
|
|
310
|
-
|----------|----------------|
|
|
311
|
-
| `getUserPosition` | shares, asset value, share price, pending withdrawal (single chain) |
|
|
312
|
-
| `getUserPositionMultiChain` | shares across hub + all spokes, total shares, estimated assets |
|
|
313
|
-
| `previewDeposit` | estimated shares for a given asset amount |
|
|
314
|
-
| `previewRedeem` | estimated assets for a given share amount |
|
|
315
|
-
| `canDeposit` | `{ allowed, reason }` — paused / cap-full / ok |
|
|
316
|
-
| `getVaultMetadata` | name, symbol, decimals, underlying, TVL, capacity |
|
|
317
|
-
| `getVaultStatus` | full config snapshot + recommended flow + issues list |
|
|
318
|
-
| `quoteLzFee` | native fee required for D4, D5, R5 |
|
|
319
|
-
| `getAsyncRequestStatusLabel` | pending / ready-to-execute / completed / refunded |
|
|
320
|
-
| `getUserBalances` | shares + underlying balance in one call |
|
|
321
|
-
| `getMaxWithdrawable` | max assets redeemable given hub liquidity |
|
|
322
|
-
| `getVaultSummary` | metadata + status + user position combined |
|
|
323
|
-
|
|
324
|
-
### Spoke route discovery
|
|
538
|
+
// Step 2: redeem on hub (after shares arrive ~7 min)
|
|
539
|
+
const redeemResult = await smartRedeem(hubWalletClient, hubPublicClient, { vault: VAULT }, shares, account.address, account.address)
|
|
540
|
+
|
|
541
|
+
// Step 3: bridge assets back to spoke
|
|
542
|
+
await bridgeAssetsToSpoke(hubWalletClient, hubPublicClient, route, assets, account.address, bridgeFee)
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### Compose helpers
|
|
325
546
|
|
|
326
547
|
| Function | Description |
|
|
327
548
|
|----------|-------------|
|
|
328
|
-
| `
|
|
329
|
-
| `
|
|
330
|
-
| `
|
|
331
|
-
| `quoteRouteDepositFee` | LZ fee for a specific inbound route |
|
|
332
|
-
| `resolveRedeemAddresses` | Discover SHARE_OFT, asset OFT, spoke asset for a redeem route |
|
|
549
|
+
| `waitForCompose` | Poll for pending compose in LZ Endpoint's `composeQueue`. Scans `ComposeSent` events from hub block captured at TX1. |
|
|
550
|
+
| `quoteComposeFee` | Quote ETH needed for `executeCompose` (readFee + shareSendFee + 10% buffer) |
|
|
551
|
+
| `executeCompose` | Execute pending compose on hub chain. Returns `{ txHash, guid? }` — `guid` present for async D7 vaults |
|
|
333
552
|
|
|
334
|
-
|
|
553
|
+
---
|
|
335
554
|
|
|
336
|
-
|
|
555
|
+
## Curator operations
|
|
337
556
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
| `getPendingActions` | Pending actions for a given nonce, with `isExecutable` flag (timelock check) |
|
|
342
|
-
| `isCurator` | Check if an address is the vault's curator |
|
|
557
|
+
Curator operations are for vault managers, not end users. All reads are multicall-batched. All writes use the simulate-then-write pattern.
|
|
558
|
+
|
|
559
|
+
### Status reads
|
|
343
560
|
|
|
344
561
|
```ts
|
|
345
|
-
import { getCuratorVaultStatus, isCurator } from '@oydual31/more-vaults-sdk/viem'
|
|
562
|
+
import { getCuratorVaultStatus, getPendingActions, isCurator, getVaultAnalysis, getVaultAssetBreakdown, checkProtocolWhitelist } from '@oydual31/more-vaults-sdk/viem'
|
|
346
563
|
|
|
347
564
|
const status = await getCuratorVaultStatus(publicClient, VAULT)
|
|
348
565
|
// status.curator — curator address
|
|
349
566
|
// status.timeLockPeriod — seconds (0 = immediate execution)
|
|
567
|
+
// status.maxSlippagePercent — slippage limit for swaps
|
|
350
568
|
// status.currentNonce — latest action nonce
|
|
351
|
-
// status.availableAssets — whitelisted
|
|
352
|
-
// status.lzAdapter — cross-chain accounting manager
|
|
569
|
+
// status.availableAssets — whitelisted token addresses
|
|
570
|
+
// status.lzAdapter — cross-chain accounting manager address
|
|
353
571
|
// status.paused — vault paused state
|
|
354
572
|
|
|
355
573
|
const isManager = await isCurator(publicClient, VAULT, myAddress)
|
|
574
|
+
|
|
575
|
+
// Full analysis — available assets with name/symbol/decimals, depositable assets, whitelist config
|
|
576
|
+
const analysis = await getVaultAnalysis(publicClient, VAULT)
|
|
577
|
+
// analysis.availableAssets — AssetInfo[] with metadata
|
|
578
|
+
// analysis.depositableAssets — AssetInfo[]
|
|
579
|
+
// analysis.depositWhitelistEnabled
|
|
580
|
+
// analysis.registryAddress
|
|
581
|
+
|
|
582
|
+
// Per-asset balance breakdown on the hub
|
|
583
|
+
const breakdown = await getVaultAssetBreakdown(publicClient, VAULT)
|
|
584
|
+
// breakdown.assets — AssetBalance[] (address, name, symbol, decimals, balance)
|
|
585
|
+
// breakdown.totalAssets
|
|
586
|
+
// breakdown.totalSupply
|
|
587
|
+
|
|
588
|
+
// Check pending actions for a nonce
|
|
589
|
+
const pending = await getPendingActions(publicClient, VAULT, nonce)
|
|
590
|
+
// pending.actionsData — raw calldata bytes[]
|
|
591
|
+
// pending.pendingUntil — timestamp when executable
|
|
592
|
+
// pending.isExecutable — boolean (timelock expired)
|
|
593
|
+
|
|
594
|
+
// Check protocol whitelist
|
|
595
|
+
const whitelist = await checkProtocolWhitelist(publicClient, VAULT, [routerAddress])
|
|
596
|
+
// { '0xRouter...': true }
|
|
356
597
|
```
|
|
357
598
|
|
|
358
|
-
###
|
|
599
|
+
### Batch actions
|
|
359
600
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
601
|
+
Curator actions are encoded and submitted as a batch. When `timeLockPeriod == 0`, actions execute immediately on submission. With a timelock, they queue and must be executed separately.
|
|
602
|
+
|
|
603
|
+
```ts
|
|
604
|
+
import {
|
|
605
|
+
buildUniswapV3Swap,
|
|
606
|
+
encodeCuratorAction,
|
|
607
|
+
buildCuratorBatch,
|
|
608
|
+
submitActions,
|
|
609
|
+
executeActions,
|
|
610
|
+
vetoActions,
|
|
611
|
+
} from '@oydual31/more-vaults-sdk/viem'
|
|
612
|
+
|
|
613
|
+
// Build a Uniswap V3 swap action (router auto-resolved per chainId)
|
|
614
|
+
const swapAction = buildUniswapV3Swap({
|
|
615
|
+
chainId: 8453, // Base — uses SwapRouter02 (no deadline)
|
|
616
|
+
tokenIn: USDC_ADDRESS,
|
|
617
|
+
tokenOut: WETH_ADDRESS,
|
|
618
|
+
fee: 500, // 0.05% pool
|
|
619
|
+
amountIn: parseUnits('1000', 6),
|
|
620
|
+
minAmountOut: parseUnits('0.39', 18),
|
|
621
|
+
recipient: VAULT,
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
// Build additional actions using the discriminated union type
|
|
625
|
+
const depositAction: CuratorAction = {
|
|
626
|
+
type: 'erc4626Deposit',
|
|
627
|
+
vault: MORPHO_VAULT,
|
|
628
|
+
assets: parseUnits('500', 6),
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Encode and submit the batch
|
|
632
|
+
const batch = buildCuratorBatch([swapAction, depositAction])
|
|
633
|
+
const { txHash, nonce } = await submitActions(walletClient, publicClient, VAULT, batch)
|
|
634
|
+
|
|
635
|
+
// If timeLockPeriod > 0: wait for timelock, then execute
|
|
636
|
+
await executeActions(walletClient, publicClient, VAULT, nonce)
|
|
637
|
+
|
|
638
|
+
// Guardian: cancel pending actions
|
|
639
|
+
await vetoActions(guardianWalletClient, publicClient, VAULT, [nonce])
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
### Supported CuratorAction types
|
|
643
|
+
|
|
644
|
+
| Type | Description |
|
|
645
|
+
|------|-------------|
|
|
646
|
+
| `swap` | Single Uniswap V3 exactInputSingle swap |
|
|
647
|
+
| `batchSwap` | Multiple swaps in one action |
|
|
648
|
+
| `erc4626Deposit` | Deposit assets into an ERC-4626 vault |
|
|
649
|
+
| `erc4626Redeem` | Redeem shares from an ERC-4626 vault |
|
|
650
|
+
| `erc7540RequestDeposit` | Request deposit into an ERC-7540 async vault |
|
|
651
|
+
| `erc7540Deposit` | Finalize ERC-7540 deposit |
|
|
652
|
+
| `erc7540RequestRedeem` | Request redeem from an ERC-7540 async vault |
|
|
653
|
+
| `erc7540Redeem` | Finalize ERC-7540 redeem |
|
|
654
|
+
|
|
655
|
+
### Swap helpers
|
|
656
|
+
|
|
657
|
+
`buildUniswapV3Swap` automatically selects the correct router and ABI variant per chain:
|
|
658
|
+
|
|
659
|
+
| Chain | Router | ABI variant |
|
|
660
|
+
|-------|--------|-------------|
|
|
661
|
+
| Base (8453) | SwapRouter02 `0x2626...` | No `deadline` field |
|
|
662
|
+
| Ethereum (1) | SwapRouter `0xE592...` | Has `deadline` field |
|
|
663
|
+
| Arbitrum (42161) | SwapRouter `0xE592...` | Has `deadline` field |
|
|
664
|
+
| Optimism (10) | SwapRouter `0xE592...` | Has `deadline` field |
|
|
665
|
+
| Flow EVM (747) | FlowSwap V3 `0xeEDC...` | Has `deadline` field |
|
|
666
|
+
|
|
667
|
+
To get raw calldata without wrapping in a `CuratorAction`:
|
|
668
|
+
```ts
|
|
669
|
+
const { targetContract, swapCallData } = encodeUniswapV3SwapCalldata({
|
|
670
|
+
chainId: 8453,
|
|
671
|
+
tokenIn: USDC_ADDRESS,
|
|
672
|
+
tokenOut: WETH_ADDRESS,
|
|
673
|
+
fee: 500,
|
|
674
|
+
amountIn: parseUnits('1000', 6),
|
|
675
|
+
minAmountOut: 0n,
|
|
676
|
+
recipient: VAULT,
|
|
677
|
+
})
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Bridge operations
|
|
681
|
+
|
|
682
|
+
Curators can bridge assets between hub and spoke vaults via LayerZero. This is a direct curator call (not via multicall) — the vault pauses during bridging for security.
|
|
683
|
+
|
|
684
|
+
```ts
|
|
685
|
+
import {
|
|
686
|
+
quoteCuratorBridgeFee,
|
|
687
|
+
executeCuratorBridge,
|
|
688
|
+
findBridgeRoute,
|
|
689
|
+
} from '@oydual31/more-vaults-sdk/viem'
|
|
690
|
+
|
|
691
|
+
// Find the OFT route for USDC between Base and Arbitrum
|
|
692
|
+
const route = findBridgeRoute(8453, 42161, USDC_ADDRESS)
|
|
693
|
+
// route.oftSrc — OFT on source chain (Stargate USDC on Base)
|
|
694
|
+
// route.oftDst — OFT on destination chain
|
|
695
|
+
// route.symbol — 'stgUSDC'
|
|
696
|
+
|
|
697
|
+
// Quote the LayerZero fee
|
|
698
|
+
const fee = await quoteCuratorBridgeFee(publicClient, VAULT, {
|
|
699
|
+
oftToken: route.oftSrc,
|
|
700
|
+
dstEid: 30110, // Arbitrum LZ EID
|
|
701
|
+
amount: parseUnits('1000', 6), // 1000 USDC
|
|
702
|
+
dstVault: SPOKE_VAULT_ADDRESS,
|
|
703
|
+
refundAddress: curatorAddress,
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
// Execute the bridge (curator only)
|
|
707
|
+
const txHash = await executeCuratorBridge(
|
|
708
|
+
walletClient, publicClient, VAULT,
|
|
709
|
+
USDC_ADDRESS, // underlying ERC-20 token
|
|
710
|
+
{
|
|
711
|
+
oftToken: route.oftSrc,
|
|
712
|
+
dstEid: 30110,
|
|
713
|
+
amount: parseUnits('1000', 6),
|
|
714
|
+
dstVault: SPOKE_VAULT_ADDRESS,
|
|
715
|
+
refundAddress: curatorAddress,
|
|
716
|
+
},
|
|
717
|
+
)
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
## Vault topology & distribution
|
|
723
|
+
|
|
724
|
+
### Topology
|
|
725
|
+
|
|
726
|
+
Resolve the hub/spoke structure of any vault:
|
|
727
|
+
|
|
728
|
+
```ts
|
|
729
|
+
import {
|
|
730
|
+
getVaultTopology,
|
|
731
|
+
getFullVaultTopology,
|
|
732
|
+
discoverVaultTopology,
|
|
733
|
+
isOnHubChain,
|
|
734
|
+
getAllVaultChainIds,
|
|
735
|
+
OMNI_FACTORY_ADDRESS,
|
|
736
|
+
} from '@oydual31/more-vaults-sdk/viem'
|
|
737
|
+
|
|
738
|
+
// Query from a known chain
|
|
739
|
+
const topo = await getVaultTopology(baseClient, VAULT)
|
|
740
|
+
// { role: 'hub', hubChainId: 8453, spokeChainIds: [1, 42161] }
|
|
741
|
+
|
|
742
|
+
// Query from any chain — same vault is a spoke on Ethereum
|
|
743
|
+
const topo2 = await getVaultTopology(ethClient, VAULT)
|
|
744
|
+
// { role: 'spoke', hubChainId: 8453, spokeChainIds: [1] }
|
|
745
|
+
|
|
746
|
+
// Auto-discover across all supported chains (no wallet needed)
|
|
747
|
+
const topo3 = await discoverVaultTopology(VAULT)
|
|
748
|
+
// Iterates all supported chains, finds the hub, returns full topology
|
|
749
|
+
|
|
750
|
+
// Get full spoke list — must use hub-chain client
|
|
751
|
+
const fullTopo = await getFullVaultTopology(baseClient, VAULT)
|
|
752
|
+
|
|
753
|
+
// Helpers
|
|
754
|
+
const onHub = isOnHubChain(walletChainId, topo) // boolean
|
|
755
|
+
const allChains = getAllVaultChainIds(topo) // [8453, 1, 42161]
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
`VaultTopology` shape:
|
|
759
|
+
```ts
|
|
760
|
+
interface VaultTopology {
|
|
761
|
+
role: 'hub' | 'spoke' | 'local'
|
|
762
|
+
hubChainId: number
|
|
763
|
+
spokeChainIds: number[]
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
### Distribution
|
|
768
|
+
|
|
769
|
+
Read the cross-chain capital distribution (hub liquid, hub strategies, spoke balances):
|
|
770
|
+
|
|
771
|
+
```ts
|
|
772
|
+
import { getVaultDistribution, getVaultDistributionWithTopology } from '@oydual31/more-vaults-sdk/viem'
|
|
773
|
+
|
|
774
|
+
// With explicit spoke clients — reads spoke balances in parallel
|
|
775
|
+
const dist = await getVaultDistribution(baseClient, VAULT, {
|
|
776
|
+
[1]: ethClient,
|
|
777
|
+
[42161]: arbClient,
|
|
778
|
+
})
|
|
779
|
+
// dist.hubLiquidBalance — idle on hub (not deployed)
|
|
780
|
+
// dist.hubStrategyBalance — deployed to hub-side strategies (Morpho, Aave, etc.)
|
|
781
|
+
// dist.hubTotalAssets — hubLiquidBalance + hubStrategyBalance
|
|
782
|
+
// dist.spokesDeployedBalance — what hub accounting thinks is on spokes
|
|
783
|
+
// dist.spokeBalances — SpokeBalance[] { chainId, totalAssets, isReachable }
|
|
784
|
+
// dist.totalActual — hub + reachable spoke totals
|
|
785
|
+
// dist.oracleAccountingEnabled
|
|
786
|
+
|
|
787
|
+
// Hub-only — discovers spoke chain IDs but does not read them
|
|
788
|
+
const dist2 = await getVaultDistributionWithTopology(baseClient, VAULT)
|
|
789
|
+
// dist2.spokeChainIds — list of spoke chain IDs to query if needed
|
|
790
|
+
// dist2.spokeBalances === [] (empty — no spoke clients provided)
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
---
|
|
794
|
+
|
|
795
|
+
## Spoke routes
|
|
796
|
+
|
|
797
|
+
Discover available deposit and redeem routes across chains:
|
|
798
|
+
|
|
799
|
+
```ts
|
|
800
|
+
import {
|
|
801
|
+
getInboundRoutes,
|
|
802
|
+
getUserBalancesForRoutes,
|
|
803
|
+
getOutboundRoutes,
|
|
804
|
+
quoteRouteDepositFee,
|
|
805
|
+
NATIVE_SYMBOL,
|
|
806
|
+
} from '@oydual31/more-vaults-sdk/viem'
|
|
807
|
+
|
|
808
|
+
// All routes a user can deposit from
|
|
809
|
+
const inbound = await getInboundRoutes(hubChainId, VAULT, vaultAsset, userAddress)
|
|
810
|
+
// Returns InboundRoute[]:
|
|
811
|
+
// - depositType: 'direct' | 'direct-async' | 'oft-compose'
|
|
812
|
+
// - spokeChainId, spokeOft, spokeToken, hubOft
|
|
813
|
+
// - sourceTokenSymbol — display this to users (e.g. 'USDC', 'weETH')
|
|
814
|
+
// - lzFeeEstimate (using 1 USDC placeholder amount)
|
|
815
|
+
// - nativeSymbol — gas token for the spoke chain
|
|
816
|
+
|
|
817
|
+
// Fetch user balances for each route
|
|
818
|
+
const withBalances = await getUserBalancesForRoutes(inbound, userAddress)
|
|
819
|
+
// Adds userBalance: bigint to each route
|
|
820
|
+
|
|
821
|
+
// Precise fee quote for a real deposit amount
|
|
822
|
+
const fee = await quoteRouteDepositFee(route, hubChainId, amount, userAddress)
|
|
823
|
+
// Returns 0n for 'direct' routes (no LZ fee needed)
|
|
824
|
+
|
|
825
|
+
// All chains a user can receive assets when redeeming
|
|
826
|
+
const outbound = await getOutboundRoutes(hubChainId, VAULT)
|
|
827
|
+
// Returns OutboundRoute[]:
|
|
828
|
+
// - chainId, routeType: 'hub' | 'spoke', eid, nativeSymbol
|
|
829
|
+
|
|
830
|
+
// Native gas symbol per chain
|
|
831
|
+
NATIVE_SYMBOL[8453] // 'ETH'
|
|
832
|
+
NATIVE_SYMBOL[747] // 'FLOW'
|
|
833
|
+
NATIVE_SYMBOL[146] // 'S'
|
|
834
|
+
NATIVE_SYMBOL[56] // 'BNB'
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
**InboundRoute deposit types:**
|
|
838
|
+
|
|
839
|
+
| `depositType` | User location | LZ fee | What happens |
|
|
840
|
+
|---------------|--------------|--------|--------------|
|
|
841
|
+
| `direct` | Hub chain, sync vault | None | Standard ERC-4626 `deposit()` |
|
|
842
|
+
| `direct-async` | Hub chain, async vault | Yes | `depositAsync()` with LZ Read |
|
|
843
|
+
| `oft-compose` | Spoke chain | Yes | OFT bridge + composer on hub |
|
|
844
|
+
|
|
845
|
+
---
|
|
846
|
+
|
|
847
|
+
## React hooks reference
|
|
848
|
+
|
|
849
|
+
Import from `@oydual31/more-vaults-sdk/react`. Requires wagmi v2 + @tanstack/react-query v5.
|
|
850
|
+
|
|
851
|
+
### Read hooks
|
|
852
|
+
|
|
853
|
+
| Hook | Returns | Description |
|
|
854
|
+
|------|---------|-------------|
|
|
855
|
+
| `useVaultStatus(vault)` | `VaultStatus` | Full config snapshot + recommended flow |
|
|
856
|
+
| `useVaultMetadata(vault)` | `VaultMetadata` | name, symbol, decimals, underlying, TVL, capacity |
|
|
857
|
+
| `useUserPosition(vault, user)` | `UserPosition` | shares, asset value, share price, pending withdrawal |
|
|
858
|
+
| `useUserPositionMultiChain(vault, user)` | `MultiChainUserPosition` | shares across hub + all spokes |
|
|
859
|
+
| `useLzFee(vault)` | `bigint` | Native fee required for async flows |
|
|
860
|
+
| `useAsyncRequestStatus(vault, guid)` | `AsyncRequestStatusInfo` | Status label for async request |
|
|
861
|
+
| `useVaultTopology(vault)` | `VaultTopology` | Hub/spoke chain structure |
|
|
862
|
+
| `useVaultDistribution(vault)` | `VaultDistribution` | TVL breakdown across chains |
|
|
863
|
+
| `useInboundRoutes(hubChainId, vault, asset, user)` | `InboundRoute[]` | Available deposit routes |
|
|
864
|
+
|
|
865
|
+
### Action hooks
|
|
866
|
+
|
|
867
|
+
| Hook | Description |
|
|
868
|
+
|------|-------------|
|
|
869
|
+
| `useSmartDeposit()` | Auto-routing deposit (sync or async) |
|
|
870
|
+
| `useSmartRedeem()` | Auto-routing redeem (sync or async) |
|
|
871
|
+
| `useDepositSimple()` | D1 — simple hub deposit |
|
|
872
|
+
| `useRedeemShares()` | R1 — standard hub redeem |
|
|
873
|
+
| `useOmniDeposit()` | Full omni-chain deposit with routing |
|
|
874
|
+
| `useOmniRedeem()` | Full omni-chain redeem with routing |
|
|
875
|
+
|
|
876
|
+
### Curator read hooks
|
|
877
|
+
|
|
878
|
+
| Hook | Returns | Description |
|
|
879
|
+
|------|---------|-------------|
|
|
880
|
+
| `useCuratorVaultStatus(vault)` | `CuratorVaultStatus` | Curator, timelock, nonce, assets, LZ adapter |
|
|
881
|
+
| `useVaultAnalysis(vault)` | `VaultAnalysis` | Available/depositable assets with metadata |
|
|
882
|
+
| `useVaultAssetBreakdown(vault)` | `VaultAssetBreakdown` | Per-asset balance breakdown |
|
|
883
|
+
| `usePendingActions(vault, nonce)` | `PendingAction` | Pending action batch with `isExecutable` flag |
|
|
884
|
+
| `useIsCurator(vault, address)` | `boolean` | Whether address is the current curator |
|
|
885
|
+
| `useProtocolWhitelist(vault, protocols)` | `Record<string, boolean>` | Protocol whitelist status |
|
|
886
|
+
|
|
887
|
+
### Curator write hooks
|
|
888
|
+
|
|
889
|
+
| Hook | Description |
|
|
890
|
+
|------|-------------|
|
|
891
|
+
| `useSubmitActions()` | Submit a batch of curator actions |
|
|
892
|
+
| `useExecuteActions()` | Execute queued actions after timelock |
|
|
893
|
+
| `useVetoActions()` | Guardian: cancel pending actions |
|
|
894
|
+
| `useCuratorBridgeQuote()` | Quote LayerZero fee for curator bridge |
|
|
895
|
+
| `useExecuteBridge()` | Execute curator bridge operation |
|
|
896
|
+
|
|
897
|
+
### React example
|
|
898
|
+
|
|
899
|
+
```tsx
|
|
900
|
+
import {
|
|
901
|
+
useVaultStatus,
|
|
902
|
+
useUserPosition,
|
|
903
|
+
useSmartDeposit,
|
|
904
|
+
} from '@oydual31/more-vaults-sdk/react'
|
|
905
|
+
import { parseUnits } from 'viem'
|
|
906
|
+
|
|
907
|
+
const VAULT = '0x8f740aba022b3fcc934ab75c581c04b75e72aba6'
|
|
908
|
+
|
|
909
|
+
function VaultDashboard() {
|
|
910
|
+
const { data: status } = useVaultStatus(VAULT)
|
|
911
|
+
const { data: position } = useUserPosition(VAULT, userAddress)
|
|
912
|
+
const { deposit, isPending } = useSmartDeposit()
|
|
913
|
+
|
|
914
|
+
const handleDeposit = () =>
|
|
915
|
+
deposit({ vault: VAULT }, parseUnits('100', 6), userAddress)
|
|
916
|
+
|
|
917
|
+
return (
|
|
918
|
+
<div>
|
|
919
|
+
<p>Mode: {status?.mode}</p>
|
|
920
|
+
<p>Your shares: {position?.shares?.toString()}</p>
|
|
921
|
+
<button onClick={handleDeposit} disabled={isPending}>Deposit 100 USDC</button>
|
|
922
|
+
</div>
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
|
|
929
|
+
## Stargate vs Standard OFT handling
|
|
930
|
+
|
|
931
|
+
The SDK auto-detects the OFT type via `detectStargateOft()`:
|
|
932
|
+
|
|
933
|
+
| OFT type | Examples | `extraOptions` | Compose delivery | User action after TX1 |
|
|
934
|
+
|----------|----------|---------------|-----------------|----------------------|
|
|
935
|
+
| **Stargate OFT** | stgUSDC, USDT, WETH | `'0x'` (empty) | Compose stays pending in LZ Endpoint `composeQueue` | Must execute TX2 on hub: `waitForCompose` → `executeCompose` |
|
|
936
|
+
| **Standard OFT** | Custom OFT adapters | LZCOMPOSE type-3 option injected with native ETH | LZ executor forwards ETH, compose auto-executes | No action needed |
|
|
937
|
+
|
|
938
|
+
Stargate's `TokenMessaging` contract rejects LZCOMPOSE type-3 executor options (`InvalidExecutorOption(3)`). The SDK handles this transparently — `depositFromSpoke` returns `composeData` when a 2nd TX is required.
|
|
939
|
+
|
|
940
|
+
**Detecting Stargate OFTs:**
|
|
941
|
+
```ts
|
|
942
|
+
import { detectStargateOft } from '@oydual31/more-vaults-sdk/viem'
|
|
943
|
+
|
|
944
|
+
const isStargate = await detectStargateOft(publicClient, oftAddress)
|
|
945
|
+
```
|
|
367
946
|
|
|
368
947
|
---
|
|
369
948
|
|
|
370
|
-
##
|
|
949
|
+
## Supported chains
|
|
950
|
+
|
|
951
|
+
Chains where the MoreVaults OMNI factory is deployed (`OMNI_FACTORY_ADDRESS = 0x7bDB8B17604b03125eFAED33cA0c55FBf856BB0C`):
|
|
952
|
+
|
|
953
|
+
| Chain | Chain ID | LZ EID | Native gas |
|
|
954
|
+
|-------|----------|--------|------------|
|
|
955
|
+
| Ethereum | 1 | 30101 | ETH |
|
|
956
|
+
| Arbitrum | 42161 | 30110 | ETH |
|
|
957
|
+
| Optimism | 10 | 30111 | ETH |
|
|
958
|
+
| Base | 8453 | 30184 | ETH |
|
|
959
|
+
| BNB Chain | 56 | 30102 | BNB |
|
|
960
|
+
| Sonic | 146 | 30332 | S |
|
|
961
|
+
| Flow EVM | 747 | 30336 | FLOW |
|
|
962
|
+
|
|
963
|
+
```ts
|
|
964
|
+
import { CHAIN_IDS, LZ_EIDS, EID_TO_CHAIN_ID, CHAIN_ID_TO_EID } from '@oydual31/more-vaults-sdk/viem'
|
|
965
|
+
|
|
966
|
+
CHAIN_IDS.BASE // 8453
|
|
967
|
+
LZ_EIDS.BASE // 30184
|
|
968
|
+
EID_TO_CHAIN_ID[30184] // 8453
|
|
969
|
+
CHAIN_ID_TO_EID[8453] // 30184
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
The `createChainTransport` and `createChainClient` helpers (exported from viem) build public-RPC clients for all supported chains using fallback transports:
|
|
973
|
+
|
|
974
|
+
```ts
|
|
975
|
+
import { createChainTransport } from '@oydual31/more-vaults-sdk/viem'
|
|
976
|
+
|
|
977
|
+
// Use with your own wallet client — useful for cross-chain flows
|
|
978
|
+
const transport = createChainTransport(8453)
|
|
979
|
+
const walletClient = createWalletClient({ account, chain: base, transport })
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
---
|
|
983
|
+
|
|
984
|
+
## LZ timeouts
|
|
985
|
+
|
|
986
|
+
Use these constants as timeout values in UI progress indicators:
|
|
371
987
|
|
|
372
988
|
```ts
|
|
373
989
|
import { LZ_TIMEOUTS } from '@oydual31/more-vaults-sdk/viem'
|
|
374
990
|
|
|
375
|
-
LZ_TIMEOUTS.POLL_INTERVAL //
|
|
991
|
+
LZ_TIMEOUTS.POLL_INTERVAL // 30 s — balance poll interval
|
|
376
992
|
LZ_TIMEOUTS.OFT_BRIDGE // 15 min — standard OFT bridge (shares or assets)
|
|
377
|
-
LZ_TIMEOUTS.STARGATE_BRIDGE // 30 min — Stargate bridge
|
|
993
|
+
LZ_TIMEOUTS.STARGATE_BRIDGE // 30 min — Stargate bridge
|
|
378
994
|
LZ_TIMEOUTS.LZ_READ_CALLBACK // 15 min — async deposit/redeem LZ Read callback
|
|
379
995
|
LZ_TIMEOUTS.COMPOSE_DELIVERY // 45 min — compose delivery to hub (spoke deposit)
|
|
380
|
-
LZ_TIMEOUTS.FULL_SPOKE_REDEEM // 60 min — full spoke
|
|
996
|
+
LZ_TIMEOUTS.FULL_SPOKE_REDEEM // 60 min — full spoke→hub→spoke redeem
|
|
381
997
|
```
|
|
382
998
|
|
|
383
|
-
|
|
999
|
+
Do not timeout before these values — cross-chain operations can legitimately take this long under network congestion.
|
|
384
1000
|
|
|
385
1001
|
---
|
|
386
1002
|
|
|
387
1003
|
## Pre-flight validation
|
|
388
1004
|
|
|
389
|
-
Run pre-flight checks before submitting transactions to
|
|
1005
|
+
Run pre-flight checks before submitting transactions to surface issues early with clear error messages:
|
|
390
1006
|
|
|
391
1007
|
```ts
|
|
392
|
-
import {
|
|
1008
|
+
import {
|
|
1009
|
+
preflightSync,
|
|
1010
|
+
preflightAsync,
|
|
1011
|
+
preflightRedeemLiquidity,
|
|
1012
|
+
preflightSpokeDeposit,
|
|
1013
|
+
preflightSpokeRedeem,
|
|
1014
|
+
} from '@oydual31/more-vaults-sdk/viem'
|
|
1015
|
+
|
|
1016
|
+
// Before D1/D3 — sync hub deposit
|
|
1017
|
+
await preflightSync(publicClient, vault, escrow)
|
|
1018
|
+
// Validates: vault not paused, not full
|
|
1019
|
+
|
|
1020
|
+
// Before D4/D5/R5 — async flow
|
|
1021
|
+
await preflightAsync(publicClient, vault, escrow)
|
|
1022
|
+
// Validates: CCManager configured, escrow registered, isHub, oracle OFF, not paused
|
|
1023
|
+
|
|
1024
|
+
// Before R1/R2 — check hub has enough liquidity
|
|
1025
|
+
await preflightRedeemLiquidity(publicClient, vault, assets)
|
|
1026
|
+
// Throws InsufficientLiquidityError if hub liquid balance < assets
|
|
393
1027
|
|
|
394
1028
|
// Before spoke deposit
|
|
395
|
-
|
|
396
|
-
// Validates: spoke balance, gas, hub composer setup
|
|
1029
|
+
await preflightSpokeDeposit(...)
|
|
1030
|
+
// Validates: spoke balance, spoke gas (LZ fee), hub composer setup
|
|
397
1031
|
|
|
398
1032
|
// Before spoke redeem
|
|
399
1033
|
const check = await preflightSpokeRedeem(route, shares, userAddress, shareBridgeFee)
|
|
400
|
-
// Validates: shares on spoke, spoke gas
|
|
401
|
-
// Returns: estimatedAssetBridgeFee, hubLiquidBalance
|
|
1034
|
+
// Validates: shares on spoke, spoke gas, hub gas
|
|
1035
|
+
// Returns: estimatedAssetBridgeFee, hubLiquidBalance
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
---
|
|
1039
|
+
|
|
1040
|
+
## Error types
|
|
1041
|
+
|
|
1042
|
+
All SDK errors extend `MoreVaultsError`. Import typed errors for `instanceof` checks:
|
|
1043
|
+
|
|
1044
|
+
```ts
|
|
1045
|
+
import {
|
|
1046
|
+
MoreVaultsError,
|
|
1047
|
+
VaultPausedError,
|
|
1048
|
+
CapacityFullError,
|
|
1049
|
+
NotWhitelistedError,
|
|
1050
|
+
InsufficientLiquidityError,
|
|
1051
|
+
CCManagerNotConfiguredError,
|
|
1052
|
+
EscrowNotConfiguredError,
|
|
1053
|
+
NotHubVaultError,
|
|
1054
|
+
MissingEscrowAddressError,
|
|
1055
|
+
WrongChainError,
|
|
1056
|
+
} from '@oydual31/more-vaults-sdk/viem'
|
|
1057
|
+
|
|
1058
|
+
try {
|
|
1059
|
+
await smartDeposit(...)
|
|
1060
|
+
} catch (err) {
|
|
1061
|
+
if (err instanceof VaultPausedError) {
|
|
1062
|
+
// vault is paused
|
|
1063
|
+
} else if (err instanceof CapacityFullError) {
|
|
1064
|
+
// deposit capacity reached
|
|
1065
|
+
} else if (err instanceof InsufficientLiquidityError) {
|
|
1066
|
+
// hub doesn't have enough liquid assets to cover the redeem
|
|
1067
|
+
} else if (err instanceof WrongChainError) {
|
|
1068
|
+
// wallet is on the wrong chain
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
402
1071
|
```
|
|
403
1072
|
|
|
404
1073
|
---
|
|
405
1074
|
|
|
1075
|
+
## User helpers reference
|
|
1076
|
+
|
|
1077
|
+
| Function | Returns |
|
|
1078
|
+
|----------|---------|
|
|
1079
|
+
| `getUserPosition(publicClient, vault, user)` | `UserPosition` — shares, asset value, share price, pending withdrawal |
|
|
1080
|
+
| `getUserPositionMultiChain(hubClient, vault, user)` | `MultiChainUserPosition` — shares across hub + all spokes |
|
|
1081
|
+
| `previewDeposit(publicClient, vault, assets)` | `bigint` — estimated shares |
|
|
1082
|
+
| `previewRedeem(publicClient, vault, shares)` | `bigint` — estimated assets |
|
|
1083
|
+
| `canDeposit(publicClient, vault, user)` | `DepositEligibility` — `{ allowed, reason }` |
|
|
1084
|
+
| `getVaultMetadata(publicClient, vault)` | `VaultMetadata` — name, symbol, decimals, underlying, TVL, capacity |
|
|
1085
|
+
| `getVaultStatus(publicClient, vault)` | `VaultStatus` — full config + mode + recommended flow |
|
|
1086
|
+
| `quoteLzFee(publicClient, vault)` | `bigint` — native fee for D4/D5/R5 |
|
|
1087
|
+
| `getAsyncRequestStatusLabel(publicClient, vault, guid)` | `AsyncRequestStatusInfo` |
|
|
1088
|
+
| `getUserBalances(publicClient, vault, user)` | `UserBalances` — shares + underlying in one call |
|
|
1089
|
+
| `getMaxWithdrawable(publicClient, vault, user)` | `MaxWithdrawable` — max assets given hub liquidity |
|
|
1090
|
+
| `getVaultSummary(publicClient, vault, user)` | `VaultSummary` — metadata + status + position combined |
|
|
1091
|
+
|
|
1092
|
+
---
|
|
1093
|
+
|
|
406
1094
|
## Repo structure
|
|
407
1095
|
|
|
408
1096
|
```
|
|
409
1097
|
more-vaults-sdk/
|
|
410
1098
|
├── src/
|
|
411
|
-
│ ├── viem/
|
|
412
|
-
│ ├── ethers/
|
|
413
|
-
│ └── react/
|
|
1099
|
+
│ ├── viem/ — viem/wagmi SDK
|
|
1100
|
+
│ ├── ethers/ — ethers.js v6 SDK
|
|
1101
|
+
│ └── react/ — React hooks (wagmi)
|
|
414
1102
|
├── docs/
|
|
415
|
-
│ ├── flows/
|
|
1103
|
+
│ ├── flows/ — per-flow detailed documentation
|
|
416
1104
|
│ ├── user-helpers.md
|
|
417
1105
|
│ └── testing.md
|
|
418
|
-
├── scripts/
|
|
419
|
-
└── tests/
|
|
1106
|
+
├── scripts/ — E2E test scripts (mainnet)
|
|
1107
|
+
└── tests/ — integration tests (require Foundry + Anvil)
|
|
420
1108
|
```
|
|
421
1109
|
|
|
422
|
-
Integration tests:
|
|
1110
|
+
Integration tests: `bash tests/run.sh` — runs the full test suite against a forked mainnet.
|