@oydual31/more-vaults-sdk 0.4.0 → 0.4.2

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