@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 CHANGED
@@ -1,26 +1,185 @@
1
1
  # @oydual31/more-vaults-sdk
2
2
 
3
- TypeScript SDK for the MoreVaults protocol. Supports **viem/wagmi** and **ethers.js v6**.
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
- // viem / wagmi
11
- import { smartDeposit, smartRedeem, getVaultStatus } from '@oydual31/more-vaults-sdk/viem'
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
- // ethers.js v6
14
- import { getVaultStatus, depositSimple } from '@oydual31/more-vaults-sdk/ethers'
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
- ## What is MoreVaults
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
- MoreVaults is a cross-chain yield vault protocol. Users deposit tokens and receive **shares** that represent their proportional stake. The vault deploys those funds across multiple chains to earn yield. When a user redeems their shares, they get back the underlying tokens plus any accrued yield.
126
+ All three modules expose the same logical features. Choose based on your stack.
127
+
128
+ ---
22
129
 
23
- Each vault is a **diamond proxy** (EIP-2535) — a single address that routes calls to multiple facets. From the SDK's perspective, it's just an address.
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** (or underlying): the token users deposit e.g. USDC. Always the same token in and out.
32
- - **Shares**: what the vault mints when you deposit. They represent your ownership percentage. As the vault earns yield, each share becomes worth more assets. Shares are ERC-20 tokens — they live at the vault address itself.
33
- - **Share price**: how many assets one share is worth right now. Starts at 1:1 and grows over time.
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 for cross-chain yield:
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. All SDK flows target the hub.
48
- - **Spoke**: a position on another chain (Arbitrum, Base, Ethereum, etc.) where the vault has deployed funds for yield. Users on spoke chains bridge tokens to the hub via LayerZero OFT.
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 a vault has `isHub = false`, it is a single-chain vault — no cross-chain flows apply, use D1/R1.
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
- A vault is always in one of these modes. Use `getVaultStatus()` to read it:
213
+ Use `getVaultStatus()` to read the current mode:
55
214
 
56
- | Mode | `isHub` | Oracle | What it means | Which flows |
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 cross-chain positions. Oracle feeds aggregate spoke balances synchronously. From the user's perspective, identical to local. | D1/D3, D2, R1, R2 |
60
- | `cross-chain-async` | true | OFF | Hub where spoke balances are NOT available synchronously. Every deposit/redeem triggers a LayerZero Read to query spokes before the vault can calculate share prices. Slower, requires a keeper. | D4, D5, R5 |
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 feed that knows the current value of funds deployed to spoke chains. `totalAssets()` resolves instantly in the same block — flows are synchronous (D1/R1).
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 it's `false`, the vault must query the spokes via **LayerZero Read** to get accurate accounting. This takes 1–5 minutes for a round-trip. Deposits and redeems are **async** — the user locks funds, waits for the oracle response, and a keeper finalizes.
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 **small fraction of TVL as liquid assets**. The rest is deployed to spoke chains where it earns yield — locked in positions on Morpho, Aave, or other protocols.
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
- This means:
75
-
76
- - **`totalAssets()`** = hub liquid balance + value of all spoke positions (reported by oracle or LZ Read).
77
- - **Redeemable now** = hub liquid balance only. If a user tries to redeem more than the hub holds, the call fails.
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 "queued" before redemption:
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
- If neither is set, `redeemShares` works in a single call.
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` is a contract that temporarily holds user funds during async flows (D4, D5, R5). When a user calls `depositAsync`, their tokens go to the escrow — not the vault — while the LayerZero Read resolves. After the keeper finalizes, the escrow releases the funds to the vault.
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 chain's actual chain ID. You need the EID when calling cross-chain flows (D6/D7, R6):
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 specific cross-chain request. Use it to track status:
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
- // Option 1: Wait for finalization (recommended)
140
- const final = await waitForAsyncRequest(publicClient, vault, guid)
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
- // Option 2: Check status once
145
- const info = await getAsyncRequestStatusLabel(publicClient, vault, guid)
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 | How to create |
158
- |--------|------|--------------|
159
- | `publicClient` | Read-only — calls `eth_call`, reads state, simulates txs. No wallet needed. | `createPublicClient({ chain, transport: http(RPC_URL) })` |
160
- | `walletClient` | Signs and sends transactions. Needs a connected account. | `createWalletClient({ account, chain, transport: http(RPC_URL) })` |
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` for both reading and signing:
311
+ **ethers.js** uses a single `Signer`:
170
312
 
171
- | How to get it | When to use |
172
- |---------------|-------------|
173
- | `new BrowserProvider(window.ethereum).getSigner()` | Browser — MetaMask or any injected wallet |
174
- | `new Wallet(PRIVATE_KEY, new JsonRpcProvider(RPC_URL))` | Node.js — scripts, bots, backends |
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 the hub chain. Spoke deposit/redeem (D6/D7/R6) the spoke chain.
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
- ## Quick start — Smart flows (recommended)
324
+ ## Deposit flows
181
325
 
182
- The simplest way to use the SDK. `smartDeposit` and `smartRedeem` auto-detect the vault type and use the correct flow.
326
+ ### Smart flows (recommended)
183
327
 
184
- ### viem / wagmi
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, smartRedeem, waitForAsyncRequest, getVaultStatus, LZ_TIMEOUTS } from '@oydual31/more-vaults-sdk/viem'
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 publicClient = createPublicClient({ chain: base, transport: http(RPC_URL) })
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
- const VAULT = '0x8f740aba022b3fcc934ab75c581c04b75e72aba6'
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
- // --- Deposit ---
197
- const depositResult = await smartDeposit(
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), // 100 USDC
367
+ parseUnits('100', 6),
201
368
  account.address,
202
369
  )
370
+ ```
203
371
 
204
- // Check if async (LZ Read callback needed)
205
- if ('guid' in depositResult) {
206
- console.log('Async deposit waiting for LZ callback (~5 min)')
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
- // Wait for finalization by GUID — returns exact shares minted
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
- // --- Redeem ---
219
- const redeemResult = await smartRedeem(
378
+ const { txHash, guid } = await depositAsync(
220
379
  walletClient, publicClient,
221
380
  { vault: VAULT },
222
- shares,
223
- account.address,
381
+ parseUnits('100', 6),
224
382
  account.address,
383
+ lzFee,
225
384
  )
226
385
 
227
- if ('guid' in redeemResult) {
228
- console.log('Async redeem — waiting for LZ callback (~5 min)')
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
- ## Flows
393
+ ## Redeem flows
240
394
 
241
- ### Smart flows (auto-detection)
395
+ ### Smart flows (recommended)
242
396
 
243
- | Function | Description |
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
- ### Hub-chain deposit
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
- | ID | Function | When to use | Doc |
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
- ### Cross-chain deposit (spoke -> hub)
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
- | ID | Function | When to use | Doc |
262
- |----|----------|-------------|-----|
263
- | D6/D7 | `depositFromSpoke` | User on spoke chain — tokens bridge via LZ OFT | [->](./docs/flows/D6-D7-deposit-from-spoke.md) |
415
+ **R1 Simple redeem:**
416
+ ```ts
417
+ import { redeemShares } from '@oydual31/more-vaults-sdk/viem'
264
418
 
265
- For Stargate OFTs (stgUSDC, USDT, WETH), `depositFromSpoke` returns `composeData` — the user must execute a 2nd TX on the hub via `waitForCompose` + `executeCompose`. For standard OFTs, compose auto-executes in 1 TX.
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
- ### Hub-chain redeem
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
- | ID | Function | When to use | Doc |
270
- |----|----------|-------------|-----|
271
- | — | `smartRedeem` | **Recommended.** Auto-detects vault type. | — |
272
- | R1 | `redeemShares` | Standard redeem, hub chain, no queue | [->](./docs/flows/R1-redeem-shares.md) |
273
- | R2 | `withdrawAssets` | Specify exact asset amount to receive | [->](./docs/flows/R2-withdraw-assets.md) |
274
- | R3 | `requestRedeem` | Withdrawal queue enabled, no timelock | [->](./docs/flows/R3-R4-request-redeem.md) |
275
- | R4 | `requestRedeem` | Withdrawal queue + mandatory wait period | [->](./docs/flows/R3-R4-request-redeem.md) |
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
- ### Cross-chain redeem (spoke -> hub -> spoke)
447
+ ### Spoke deposit (D6 / D7)
279
448
 
280
- Full spoke redeem is a 3-step flow across two chains:
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
- Step 1 (Spoke): bridgeSharesToHub() — shares spoke->hub via SHARE_OFT (~7 min)
284
- Step 2 (Hub): smartRedeem() — redeem on hub (auto-detects async, ~5 min callback)
285
- Step 3 (Hub): bridgeAssetsToSpoke() — assets hub->spoke via Stargate/OFT (~13 min)
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
- | Function | Step | Doc |
289
- |----------|------|-----|
290
- | `resolveRedeemAddresses` | Pre-step: discover all addresses dynamically | — |
291
- | `preflightSpokeRedeem` | Pre-step: validate balances + gas on both chains | — |
292
- | `bridgeSharesToHub` | Step 1: bridge shares spoke->hub | [->](./docs/flows/R6-bridge-shares-to-hub.md) |
293
- | `smartRedeem` | Step 2: redeem on hub | — |
294
- | `bridgeAssetsToSpoke` | Step 3: bridge assets hub->spoke | [->](./docs/flows/R7-bridge-assets-to-spoke.md) |
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
- ### Compose helpers (Stargate 2-TX flow)
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
- | Function | Description |
299
- |----------|-------------|
300
- | `waitForCompose` | Wait for pending compose in LZ Endpoint's composeQueue |
301
- | `quoteComposeFee` | Quote ETH needed for `executeCompose` (readFee + shareSendFee) |
302
- | `executeCompose` | Execute pending compose on hub chain. Returns `{ txHash, guid? }` — GUID present for async vaults |
303
- | `waitForAsyncRequest` | Poll async request by GUID until finalized. Returns `{ status, result }` with exact amounts |
304
-
305
- ### User helpers (read-only, no gas)
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
- | `getInboundRoutes` | All routes a user can deposit from (which chains/tokens) |
329
- | `getUserBalancesForRoutes` | User balances for all inbound routes |
330
- | `getOutboundRoutes` | All routes for spoke redeem (which chains to bridge back to) |
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
- ### Curator operations (vault manager)
553
+ ---
335
554
 
336
- Full reference: [docs/curator-operations.md](./docs/curator-operations.md)
555
+ ## Curator operations
337
556
 
338
- | Function | Description |
339
- |----------|-------------|
340
- | `getCuratorVaultStatus` | Full status snapshot: curator address, timelock, slippage, nonce, available assets, LZ adapter |
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 tokens
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
- ### Topology & distribution
599
+ ### Batch actions
359
600
 
360
- | Function | Description |
361
- |----------|-------------|
362
- | `discoverVaultTopology` | Auto-discover hub/spoke topology across all chains (no wallet needed) |
363
- | `getVaultTopology` | Hub/spoke chain IDs, OFT routes, composer addresses (requires correct chain client) |
364
- | `getFullVaultTopology` | Topology + all on-chain config |
365
- | `getVaultDistribution` | TVL breakdown across hub + all spokes |
366
- | `isOnHubChain` | Check if user is on the hub chain |
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
- ## LZ Timeouts (for UI integration)
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 // 30sinterval between balance polls
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 (slower, pool mechanics)
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->hub->spoke redeem
996
+ LZ_TIMEOUTS.FULL_SPOKE_REDEEM // 60 min — full spokehubspoke redeem
381
997
  ```
382
998
 
383
- Use these as timeout values in UI progress indicators. Do NOT timeout before these values — cross-chain operations can legitimately take this long.
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 catch issues early:
1005
+ Run pre-flight checks before submitting transactions to surface issues early with clear error messages:
390
1006
 
391
1007
  ```ts
392
- import { preflightSpokeDeposit, preflightSpokeRedeem } from '@oydual31/more-vaults-sdk/viem'
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
- const check = await preflightSpokeDeposit(...)
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 (LZ fee + buffer), hub gas (asset bridge fee)
401
- // Returns: estimatedAssetBridgeFee, hubLiquidBalance, etc.
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/ <- viem/wagmi SDK
412
- │ ├── ethers/ <- ethers.js v6 SDK
413
- │ └── react/ <- React hooks (wagmi)
1099
+ │ ├── viem/ viem/wagmi SDK
1100
+ │ ├── ethers/ ethers.js v6 SDK
1101
+ │ └── react/ React hooks (wagmi)
414
1102
  ├── docs/
415
- │ ├── flows/ <- one .md per flow with detailed examples
1103
+ │ ├── flows/ per-flow detailed documentation
416
1104
  │ ├── user-helpers.md
417
1105
  │ └── testing.md
418
- ├── scripts/ <- E2E test scripts (mainnet)
419
- └── tests/ <- integration tests (require Foundry + Anvil)
1106
+ ├── scripts/ E2E test scripts (mainnet)
1107
+ └── tests/ integration tests (require Foundry + Anvil)
420
1108
  ```
421
1109
 
422
- Integration tests: [docs/testing.md](./docs/testing.md) — `bash tests/run.sh` runs all 43 tests.
1110
+ Integration tests: `bash tests/run.sh` runs the full test suite against a forked mainnet.