@oydual31/more-vaults-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,283 @@
1
+ # @more-vaults/sdk
2
+
3
+ TypeScript SDK for the MoreVaults protocol. Supports **viem/wagmi** and **ethers.js v6**.
4
+
5
+ ---
6
+
7
+ ## What is MoreVaults
8
+
9
+ 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.
10
+
11
+ 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.
12
+
13
+ ---
14
+
15
+ ## Core concepts
16
+
17
+ ### Assets and shares
18
+
19
+ - **Asset** (or underlying): the token users deposit — e.g. USDC. Always the same token in and out.
20
+ - **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.
21
+ - **Share price**: how many assets one share is worth right now. Starts at 1:1 and grows over time.
22
+
23
+ ```
24
+ Deposit 100 USDC → receive 100 shares (at launch, price = 1)
25
+ Wait 1 year → share price = 1.05
26
+ Redeem 100 shares → receive 105 USDC
27
+ ```
28
+
29
+ > Vault shares use more decimals than the underlying token. A vault over USDC (6 decimals) will typically have 8 decimals for shares. Always read `vault.decimals()` — never hardcode it.
30
+
31
+ ### Hub and spoke
32
+
33
+ MoreVaults uses a **hub-and-spoke** model for cross-chain yield:
34
+
35
+ - **Hub** (`isHub = true`): the chain where the vault does its accounting — mints/burns shares, accepts deposits and redemptions. All SDK flows target the hub. Flow EVM is the current reference deployment, but any EVM chain can be the hub.
36
+ - **Spoke**: a position on another chain (Arbitrum, Base, etc.) where the vault has deployed funds for yield. Users on spoke chains bridge tokens to the hub via LayerZero OFT — they never interact with the spoke contracts directly.
37
+
38
+ If a vault has `isHub = false`, it is a single-chain vault — no cross-chain flows apply, use D1/R1.
39
+
40
+ ### Vault modes
41
+
42
+ A vault is always in one of these modes. Use `getVaultStatus()` to read it:
43
+
44
+ | Mode | `isHub` | Oracle | What it means | Which flows |
45
+ |------|---------|--------|---------------|-------------|
46
+ | `local` | false | — | Single-chain vault. No cross-chain. | D1, D2, R1, R2 |
47
+ | `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 |
48
+ | `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 |
49
+ | `paused` | — | — | No deposits or redeems accepted. | None |
50
+ | `full` | — | — | Deposit capacity reached. Redeems still work. | R1, R2 only |
51
+
52
+ ### Oracle ON vs OFF
53
+
54
+ 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).
55
+
56
+ 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.
57
+
58
+ ### Hub liquidity and repatriation
59
+
60
+ 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.
61
+
62
+ This means:
63
+
64
+ - **`totalAssets()`** = hub liquid balance + value of all spoke positions (reported by oracle or LZ Read).
65
+ - **Redeemable now** = hub liquid balance only. If a user tries to redeem more than the hub holds, the call fails.
66
+ - 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.
67
+
68
+ **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.
69
+
70
+ If a redeem fails because the hub is under-funded:
71
+ 1. The user's shares are returned automatically (or the tx reverts before any state change for R1).
72
+ 2. The vault curator must repatriate liquidity from the spokes.
73
+ 3. The user retries the redeem once sufficient liquidity is available on the hub.
74
+
75
+ ### Withdrawal queue and timelock
76
+
77
+ Some vaults require shares to be "queued" before redemption:
78
+
79
+ - **`withdrawalQueueEnabled = true`**: users must call `requestRedeem` first, then `redeemShares` separately.
80
+ - **`withdrawalTimelockSeconds > 0`**: there is a mandatory waiting period between `requestRedeem` and `redeemShares`. Useful for vaults that need time to rebalance liquidity.
81
+
82
+ If neither is set, `redeemShares` works in a single call.
83
+
84
+ ### Escrow
85
+
86
+ 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.
87
+
88
+ **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`.
89
+
90
+ To get the escrow address: read it from the vault itself:
91
+ ```ts
92
+ const status = await getVaultStatus(publicClient, VAULT_ADDRESS)
93
+ const escrow = status.escrow // address(0) if not configured
94
+ ```
95
+
96
+ ### Same address on every chain (CREATE3)
97
+
98
+ 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 Flow EVM is `0xABC...`, the corresponding escrow and spoke-side contracts are also at predictable, identical addresses across Arbitrum, Base, etc.
99
+
100
+ This simplifies the frontend significantly — you don't need a separate address map per chain. One address identifies the vault everywhere.
101
+
102
+ ### VaultAddresses
103
+
104
+ Every flow function takes a `VaultAddresses` object:
105
+
106
+ ```ts
107
+ interface VaultAddresses {
108
+ vault: Address // Vault address — same on every chain (CREATE3)
109
+ escrow: Address // MoreVaultsEscrow — same address as vault on each chain, required for D4, D5, R5
110
+ shareOFT?: Address // OFTAdapter for vault shares — required for R6 (spoke redeem)
111
+ usdcOFT?: Address // OFT for the underlying token on the spoke — required for D6/D7
112
+ }
113
+ ```
114
+
115
+ For simple hub flows (D1, R1) you only need `vault`. For async flows you also need `escrow` — get it from `getVaultStatus(publicClient, vault).escrow`. For cross-chain flows from a spoke you also need the OFT addresses for that specific spoke chain.
116
+
117
+ ### LayerZero EID
118
+
119
+ 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):
120
+
121
+ | Chain | Chain ID | LayerZero EID |
122
+ |-------|----------|---------------|
123
+ | Flow EVM | 747 | 30332 |
124
+ | Arbitrum | 42161 | 30110 |
125
+ | Base | 8453 | 30184 |
126
+ | Ethereum | 1 | 30101 |
127
+
128
+ ### GUID (async request ID)
129
+
130
+ 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:
131
+
132
+ ```ts
133
+ const { guid } = await depositAsync(...)
134
+
135
+ // Poll status
136
+ const info = await getAsyncRequestStatusLabel(publicClient, vault, guid)
137
+ // info.status: 'pending' | 'ready-to-execute' | 'completed' | 'refunded'
138
+ // info.result: shares minted or assets received once completed
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Clients
144
+
145
+ Every SDK function takes one or two "clients" as its first arguments — the objects that talk to the blockchain.
146
+
147
+ **viem** uses two separate objects:
148
+
149
+ | Client | Role | How to create |
150
+ |--------|------|--------------|
151
+ | `publicClient` | Read-only — calls `eth_call`, reads state, simulates txs. No wallet needed. | `createPublicClient({ chain, transport: http(RPC_URL) })` |
152
+ | `walletClient` | Signs and sends transactions. Needs a connected account. | `createWalletClient({ account, chain, transport: http(RPC_URL) })` |
153
+
154
+ In React with wagmi:
155
+ ```ts
156
+ import { usePublicClient, useWalletClient } from 'wagmi'
157
+ const publicClient = usePublicClient()
158
+ const { data: walletClient } = useWalletClient()
159
+ ```
160
+
161
+ **ethers.js** uses a single `Signer` for both reading and signing:
162
+
163
+ | How to get it | When to use |
164
+ |---------------|-------------|
165
+ | `new BrowserProvider(window.ethereum).getSigner()` | Browser — MetaMask or any injected wallet |
166
+ | `new Wallet(PRIVATE_KEY, new JsonRpcProvider(RPC_URL))` | Node.js — scripts, bots, backends |
167
+
168
+ Read-only helpers (`getUserPosition`, `previewDeposit`, etc.) accept a bare `Provider` in the ethers version — no signer needed.
169
+
170
+ > 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.
171
+
172
+ ---
173
+
174
+ ## Quick start
175
+
176
+ ### viem / wagmi
177
+
178
+ ```ts
179
+ import { getVaultStatus, depositSimple, getUserPosition } from './src/viem/index.js'
180
+ import { createPublicClient, createWalletClient, http, parseUnits } from 'viem'
181
+
182
+ const publicClient = createPublicClient({ chain: flowEvm, transport: http(RPC_URL) })
183
+ const walletClient = createWalletClient({ account, chain: flowEvm, transport: http(RPC_URL) })
184
+
185
+ // 1. Check vault status to know which flow to use
186
+ const status = await getVaultStatus(publicClient, VAULT_ADDRESS)
187
+ // status.mode → 'local' | 'cross-chain-oracle' | 'cross-chain-async' | 'paused' | 'full'
188
+ // status.recommendedDepositFlow → 'depositSimple' | 'depositAsync' | 'none'
189
+ // status.escrow → escrow address (needed for async flows)
190
+
191
+ const addresses = { vault: VAULT_ADDRESS, escrow: status.escrow }
192
+
193
+ // 2. Deposit
194
+ const { txHash, shares } = await depositSimple(
195
+ walletClient, publicClient, addresses,
196
+ parseUnits('100', 6), // 100 USDC
197
+ account.address,
198
+ )
199
+
200
+ // 3. Read position
201
+ const position = await getUserPosition(publicClient, VAULT_ADDRESS, account.address)
202
+ console.log('Shares:', position.shares)
203
+ console.log('Value:', position.estimatedAssets)
204
+ ```
205
+
206
+ ### ethers.js
207
+
208
+ ```ts
209
+ import { getVaultStatus, depositSimple, getUserPosition } from './src/ethers/index.js'
210
+ import { BrowserProvider, parseUnits } from 'ethers'
211
+
212
+ const provider = new BrowserProvider(window.ethereum)
213
+ const signer = await provider.getSigner()
214
+
215
+ const status = await getVaultStatus(provider, VAULT_ADDRESS)
216
+ const addresses = { vault: VAULT_ADDRESS, escrow: status.escrow }
217
+
218
+ const { txHash, shares } = await depositSimple(
219
+ signer, addresses,
220
+ parseUnits('100', 6),
221
+ await signer.getAddress(),
222
+ )
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Flows
228
+
229
+ ### Deposit
230
+
231
+ | ID | Function | When to use | Doc |
232
+ |----|----------|-------------|-----|
233
+ | D1 | `depositSimple` | User on hub chain, oracle ON or local vault | [→](./docs/flows/D1-deposit-simple.md) |
234
+ | D2 | `depositMultiAsset` | Deposit multiple tokens in one call | [→](./docs/flows/D2-deposit-multi-asset.md) |
235
+ | D3 | `depositCrossChainOracleOn` | Alias for D1 — hub with oracle ON, explicit naming | [→](./docs/flows/D3-deposit-oracle-on.md) |
236
+ | D4 | `depositAsync` | Hub with oracle OFF — async LZ Read, shares arrive in ~1–5 min | [→](./docs/flows/D4-deposit-async.md) |
237
+ | D5 | `mintAsync` | Same as D4 but user specifies exact share amount | [→](./docs/flows/D5-mint-async.md) |
238
+ | D6/D7 | `depositFromSpoke` | User on another chain — tokens bridge via LZ OFT | [→](./docs/flows/D6-D7-deposit-from-spoke.md) |
239
+
240
+ ### Redeem
241
+
242
+ | ID | Function | When to use | Doc |
243
+ |----|----------|-------------|-----|
244
+ | R1 | `redeemShares` | Standard redeem, hub chain, no queue | [→](./docs/flows/R1-redeem-shares.md) |
245
+ | R2 | `withdrawAssets` | Specify exact asset amount to receive | [→](./docs/flows/R2-withdraw-assets.md) |
246
+ | R3 | `requestRedeem` | Withdrawal queue enabled, no timelock | [→](./docs/flows/R3-R4-request-redeem.md) |
247
+ | R4 | `requestRedeem` | Withdrawal queue + mandatory wait period | [→](./docs/flows/R3-R4-request-redeem.md) |
248
+ | R5 | `redeemAsync` | Hub with oracle OFF — async LZ Read | [→](./docs/flows/R5-redeem-async.md) |
249
+ | R6 | `bridgeSharesToHub` | User on spoke — step 1: bridge shares to hub | [→](./docs/flows/R6-bridge-shares-to-hub.md) |
250
+
251
+ ### User helpers (read-only, no gas)
252
+
253
+ Full reference: [docs/user-helpers.md](./docs/user-helpers.md)
254
+
255
+ | Function | What it returns |
256
+ |----------|----------------|
257
+ | `getUserPosition` | shares, asset value, share price, pending withdrawal |
258
+ | `previewDeposit` | estimated shares for a given asset amount |
259
+ | `previewRedeem` | estimated assets for a given share amount |
260
+ | `canDeposit` | `{ allowed, reason }` — paused / cap-full / ok |
261
+ | `getVaultMetadata` | name, symbol, decimals, underlying, TVL, capacity |
262
+ | `getVaultStatus` | full config snapshot + recommended flow + issues list |
263
+ | `quoteLzFee` | native fee required for D4, D5, R5 |
264
+ | `getAsyncRequestStatusLabel` | pending / ready-to-execute / completed / refunded |
265
+
266
+ ---
267
+
268
+ ## Repo structure
269
+
270
+ ```
271
+ more-vaults-sdk/
272
+ ├── src/
273
+ │ ├── viem/ ← viem/wagmi SDK
274
+ │ └── ethers/ ← ethers.js v6 SDK
275
+ ├── docs/
276
+ │ ├── flows/ ← one .md per flow with detailed examples
277
+ │ ├── user-helpers.md
278
+ │ └── testing.md
279
+ ├── tests/ ← integration tests (require Foundry + Anvil)
280
+ └── contracts/ ← protocol Solidity source + mocks (for running tests)
281
+ ```
282
+
283
+ Integration tests: [docs/testing.md](./docs/testing.md) — `bash tests/run.sh` runs all 43 tests.