@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.
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Utility helpers for the MoreVaults ethers.js v6 SDK.
3
+ *
4
+ * All reads use Provider (read-only). Writes use Signer.
5
+ */
6
+
7
+ import { Contract, Interface, ZeroAddress } from "ethers";
8
+ import type { Provider, Signer } from "ethers";
9
+ import { BRIDGE_ABI, CONFIG_ABI, ERC20_ABI, VAULT_ABI } from "./abis";
10
+
11
+ // Multicall3 — deployed at the same address on every EVM chain
12
+ const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
13
+ const MULTICALL3_ABI = [
14
+ "function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)",
15
+ ] as const;
16
+ import type { CrossChainRequestInfo } from "./types";
17
+
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+
20
+ export type VaultMode =
21
+ | "local" // single-chain vault, no cross-chain
22
+ | "cross-chain-oracle" // hub with oracle-based accounting (sync)
23
+ | "cross-chain-async" // hub with off-chain accounting (async, D4/D5/R5)
24
+ | "paused" // vault is paused
25
+ | "full"; // deposit capacity reached
26
+
27
+ export interface VaultStatus {
28
+ /** Vault operating mode — determines which SDK flow to use */
29
+ mode: VaultMode;
30
+ /** Which deposit function to call given the current configuration */
31
+ recommendedDepositFlow: "depositSimple" | "depositAsync" | "mintAsync" | "none";
32
+ /** Which redeem function to call given the current configuration */
33
+ recommendedRedeemFlow: "redeemShares" | "redeemAsync" | "none";
34
+
35
+ // ── Configuration ──────────────────────────────────────────────────────────
36
+ isHub: boolean;
37
+ isPaused: boolean;
38
+ oracleAccountingEnabled: boolean;
39
+
40
+ /** address(0) means CCManager is not set — async flows will fail */
41
+ ccManager: string;
42
+ /** address(0) means escrow is not configured in the registry */
43
+ escrow: string;
44
+
45
+ // ── Withdrawal queue ───────────────────────────────────────────────────────
46
+ withdrawalQueueEnabled: boolean;
47
+ /** Timelock duration in seconds (0 = no timelock) */
48
+ withdrawalTimelockSeconds: bigint;
49
+
50
+ // ── Capacity ───────────────────────────────────────────────────────────────
51
+ /**
52
+ * Remaining deposit capacity in underlying token decimals.
53
+ * `type(uint256).max` = no cap configured (unlimited).
54
+ * `0n` = vault is full — no more deposits accepted.
55
+ * If `depositAccessRestricted = true`, this value is `type(uint256).max` but
56
+ * deposits are still gated by whitelist or other access control.
57
+ */
58
+ remainingDepositCapacity: bigint;
59
+ /**
60
+ * True when `maxDeposit(address(0))` reverted, indicating the vault uses
61
+ * whitelist or other access control to restrict who can deposit.
62
+ */
63
+ depositAccessRestricted: boolean;
64
+
65
+ // ── Vault metrics ──────────────────────────────────────────────────────────
66
+ underlying: string;
67
+ totalAssets: bigint;
68
+ totalSupply: bigint;
69
+ /** Vault share token decimals. Use this for display — never hardcode 18. */
70
+ decimals: number;
71
+ /**
72
+ * Price of 1 full share expressed in underlying token units.
73
+ * = convertToAssets(10^decimals). Grows over time as the vault earns yield.
74
+ */
75
+ sharePrice: bigint;
76
+ /**
77
+ * Underlying token balance held directly on the hub chain.
78
+ * This is the only portion that can be paid out to redeeming users immediately.
79
+ * (= ERC-20.balanceOf(vault) on the hub)
80
+ */
81
+ hubLiquidBalance: bigint;
82
+ /**
83
+ * Approximate value deployed to spoke chains (totalAssets − hubLiquidBalance).
84
+ * These funds are NOT immediately redeemable — the vault curator must
85
+ * call executeBridging to repatriate them before large redeems can succeed.
86
+ */
87
+ spokesDeployedBalance: bigint;
88
+ /**
89
+ * Maximum assets that can be redeemed right now without curator intervention.
90
+ * - For hub vaults: equals `hubLiquidBalance`.
91
+ * - For local/oracle vaults: equals `totalAssets`.
92
+ */
93
+ maxImmediateRedeemAssets: bigint;
94
+
95
+ // ── Issues — empty when everything is correctly configured ─────────────────
96
+ /**
97
+ * Human-readable list of configuration problems that would cause transactions
98
+ * to fail. Empty array = vault is ready to use.
99
+ */
100
+ issues: string[];
101
+ }
102
+
103
+ /**
104
+ * Ensure the spender has sufficient ERC-20 allowance; approve if not.
105
+ *
106
+ * @param signer Wallet signer with account attached
107
+ * @param provider Read-only provider for allowance checks
108
+ * @param token ERC-20 token address
109
+ * @param spender Address to approve
110
+ * @param amount Minimum required allowance
111
+ */
112
+ export async function ensureAllowance(
113
+ signer: Signer,
114
+ provider: Provider,
115
+ token: string,
116
+ spender: string,
117
+ amount: bigint
118
+ ): Promise<void> {
119
+ const owner = await signer.getAddress();
120
+ const erc20Read = new Contract(token, ERC20_ABI, provider);
121
+ const current: bigint = await erc20Read.allowance(owner, spender);
122
+ if (current < amount) {
123
+ const erc20Write = new Contract(token, ERC20_ABI, signer);
124
+ const tx = await erc20Write.approve(spender, amount);
125
+ await tx.wait();
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Quote the LayerZero native fee required for async vault actions.
131
+ *
132
+ * @param provider Read-only provider
133
+ * @param vault Vault address (diamond proxy)
134
+ * @param extraOptions Optional LZ extra options bytes (default 0x)
135
+ * @returns Required native fee in wei
136
+ */
137
+ export async function quoteLzFee(
138
+ provider: Provider,
139
+ vault: string,
140
+ extraOptions: string = "0x"
141
+ ): Promise<bigint> {
142
+ const bridge = new Contract(vault, BRIDGE_ABI, provider);
143
+ const fee: bigint = await bridge.quoteAccountingFee(extraOptions);
144
+ return fee;
145
+ }
146
+
147
+ /**
148
+ * Check if a vault is operating in async mode (cross-chain hub with oracle OFF).
149
+ *
150
+ * @param provider Read-only provider
151
+ * @param vault Vault address
152
+ * @returns true if the vault requires async cross-chain flows
153
+ */
154
+ export async function isAsyncMode(
155
+ provider: Provider,
156
+ vault: string
157
+ ): Promise<boolean> {
158
+ const config = new Contract(vault, CONFIG_ABI, provider);
159
+ const bridge = new Contract(vault, BRIDGE_ABI, provider);
160
+
161
+ const [isHub, oraclesEnabled]: [boolean, boolean] = await Promise.all([
162
+ config.isHub(),
163
+ bridge.oraclesCrossChainAccounting(),
164
+ ]);
165
+
166
+ if (!isHub) return false;
167
+ return !oraclesEnabled;
168
+ }
169
+
170
+ /**
171
+ * Poll for async request completion status.
172
+ *
173
+ * @param provider Read-only provider
174
+ * @param vault Vault address
175
+ * @param guid Request GUID returned by the async flow
176
+ * @returns Whether the request is fulfilled, finalized, and the result
177
+ */
178
+ export async function getAsyncRequestStatus(
179
+ provider: Provider,
180
+ vault: string,
181
+ guid: string
182
+ ): Promise<{ fulfilled: boolean; finalized: boolean; result: bigint }> {
183
+ const bridge = new Contract(vault, BRIDGE_ABI, provider);
184
+
185
+ const [info, finalizationResult]: [CrossChainRequestInfo, bigint] =
186
+ await Promise.all([
187
+ bridge.getRequestInfo(guid),
188
+ bridge.getFinalizationResult(guid),
189
+ ]);
190
+
191
+ return {
192
+ fulfilled: info.fulfilled,
193
+ finalized: info.finalized,
194
+ result: finalizationResult,
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Read the full configuration and operational status of a vault.
200
+ *
201
+ * All independent reads are fired in parallel.
202
+ *
203
+ * @param provider Read-only provider
204
+ * @param vault Vault address (diamond proxy)
205
+ * @returns Full vault status snapshot
206
+ */
207
+ export async function getVaultStatus(
208
+ provider: Provider,
209
+ vault: string
210
+ ): Promise<VaultStatus> {
211
+ const mc = new Contract(MULTICALL3_ADDRESS, MULTICALL3_ABI, provider);
212
+ const configIface = new Interface(CONFIG_ABI as unknown as string[]);
213
+ const bridgeIface = new Interface(BRIDGE_ABI as unknown as string[]);
214
+ const vaultIface = new Interface(VAULT_ABI as unknown as string[]);
215
+ const decimalsIface = new Interface(["function decimals() view returns (uint8)"]);
216
+
217
+ // ── Batch 1: 12 calls → 1 eth_call via Multicall3.aggregate3 ─────────────
218
+ const b1Calls = [
219
+ { target: vault, allowFailure: false, callData: configIface.encodeFunctionData("isHub") },
220
+ { target: vault, allowFailure: false, callData: configIface.encodeFunctionData("paused") },
221
+ { target: vault, allowFailure: false, callData: bridgeIface.encodeFunctionData("oraclesCrossChainAccounting") },
222
+ { target: vault, allowFailure: false, callData: configIface.encodeFunctionData("getCrossChainAccountingManager") },
223
+ { target: vault, allowFailure: false, callData: configIface.encodeFunctionData("getEscrow") },
224
+ { target: vault, allowFailure: false, callData: configIface.encodeFunctionData("getWithdrawalQueueStatus") },
225
+ { target: vault, allowFailure: false, callData: configIface.encodeFunctionData("getWithdrawalTimelock") },
226
+ // allowFailure=true: maxDeposit reverts on whitelisted vaults with address(0)
227
+ { target: vault, allowFailure: true, callData: configIface.encodeFunctionData("maxDeposit", [ZeroAddress]) },
228
+ { target: vault, allowFailure: false, callData: vaultIface.encodeFunctionData("asset") },
229
+ { target: vault, allowFailure: false, callData: vaultIface.encodeFunctionData("totalAssets") },
230
+ { target: vault, allowFailure: false, callData: vaultIface.encodeFunctionData("totalSupply") },
231
+ { target: vault, allowFailure: false, callData: decimalsIface.encodeFunctionData("decimals") },
232
+ ];
233
+
234
+ const b1: { success: boolean; returnData: string }[] = await mc.aggregate3.staticCall(b1Calls);
235
+
236
+ const isHub = configIface.decodeFunctionResult("isHub", b1[0].returnData)[0] as boolean;
237
+ const isPaused = configIface.decodeFunctionResult("paused", b1[1].returnData)[0] as boolean;
238
+ const oraclesEnabled = bridgeIface.decodeFunctionResult("oraclesCrossChainAccounting", b1[2].returnData)[0] as boolean;
239
+ const ccManager = configIface.decodeFunctionResult("getCrossChainAccountingManager", b1[3].returnData)[0] as string;
240
+ const escrow = configIface.decodeFunctionResult("getEscrow", b1[4].returnData)[0] as string;
241
+ const withdrawalQueueEnabled = configIface.decodeFunctionResult("getWithdrawalQueueStatus", b1[5].returnData)[0] as boolean;
242
+ const withdrawalTimelockSeconds = configIface.decodeFunctionResult("getWithdrawalTimelock", b1[6].returnData)[0] as bigint;
243
+ // null sentinel: reverted means whitelist/ACL
244
+ const maxDepositRaw = b1[7].success
245
+ ? configIface.decodeFunctionResult("maxDeposit", b1[7].returnData)[0] as bigint
246
+ : null;
247
+ const underlying = vaultIface.decodeFunctionResult("asset", b1[8].returnData)[0] as string;
248
+ const totalAssets = vaultIface.decodeFunctionResult("totalAssets", b1[9].returnData)[0] as bigint;
249
+ const totalSupply = vaultIface.decodeFunctionResult("totalSupply", b1[10].returnData)[0] as bigint;
250
+ const decimalsRaw = decimalsIface.decodeFunctionResult("decimals", b1[11].returnData)[0];
251
+ const decimalsNum = Number(decimalsRaw);
252
+ const oneShare = 10n ** BigInt(decimalsNum);
253
+
254
+ // ── Batch 2: 2 calls → 1 eth_call (depends on underlying + decimals) ─────
255
+ const erc20Iface = new Interface(ERC20_ABI as unknown as string[]);
256
+ const b2Calls = [
257
+ { target: underlying, allowFailure: false, callData: erc20Iface.encodeFunctionData("balanceOf", [vault]) },
258
+ { target: vault, allowFailure: false, callData: vaultIface.encodeFunctionData("convertToAssets", [oneShare]) },
259
+ ];
260
+
261
+ const b2: { success: boolean; returnData: string }[] = await mc.aggregate3.staticCall(b2Calls);
262
+
263
+ const hubLiquidBalance = erc20Iface.decodeFunctionResult("balanceOf", b2[0].returnData)[0] as bigint;
264
+ const sharePrice = vaultIface.decodeFunctionResult("convertToAssets", b2[1].returnData)[0] as bigint;
265
+
266
+ const spokesDeployedBalance: bigint = totalAssets > hubLiquidBalance ? totalAssets - hubLiquidBalance : 0n;
267
+
268
+ // null = maxDeposit reverted → whitelist/ACL vault
269
+ const MAX_UINT256 = BigInt('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff');
270
+ const depositAccessRestricted = maxDepositRaw === null;
271
+ const effectiveCapacity: bigint = depositAccessRestricted ? MAX_UINT256 : maxDepositRaw!;
272
+
273
+ // ── Derive mode ────────────────────────────────────────────────────────────
274
+ let mode: VaultMode;
275
+ if (isPaused) {
276
+ mode = "paused";
277
+ } else if (effectiveCapacity === 0n) {
278
+ mode = "full";
279
+ } else if (!isHub) {
280
+ mode = "local";
281
+ } else if (oraclesEnabled) {
282
+ mode = "cross-chain-oracle";
283
+ } else {
284
+ mode = "cross-chain-async";
285
+ }
286
+
287
+ // ── Recommended flows ──────────────────────────────────────────────────────
288
+ let recommendedDepositFlow: VaultStatus["recommendedDepositFlow"];
289
+ let recommendedRedeemFlow: VaultStatus["recommendedRedeemFlow"];
290
+
291
+ if (mode === "paused" || mode === "full") {
292
+ recommendedDepositFlow = "none";
293
+ recommendedRedeemFlow = mode === "paused" ? "none" : "redeemShares";
294
+ } else if (mode === "cross-chain-async") {
295
+ recommendedDepositFlow = "depositAsync";
296
+ recommendedRedeemFlow = "redeemAsync";
297
+ } else {
298
+ // local or cross-chain-oracle
299
+ recommendedDepositFlow = "depositSimple";
300
+ recommendedRedeemFlow = "redeemShares";
301
+ }
302
+
303
+ // ── Issues ─────────────────────────────────────────────────────────────────
304
+ const issues: string[] = [];
305
+
306
+ if (isPaused) {
307
+ issues.push("Vault is paused — no deposits or redeems are possible.");
308
+ }
309
+ if (effectiveCapacity === 0n && !isPaused) {
310
+ issues.push(
311
+ "Deposit capacity is full — increase depositCapacity via setDepositCapacity()."
312
+ );
313
+ }
314
+ if (isHub && !oraclesEnabled && ccManager === ZeroAddress) {
315
+ issues.push(
316
+ "CCManager not configured — async flows will revert. Call setCrossChainAccountingManager(address) as vault owner."
317
+ );
318
+ }
319
+ if (isHub && !oraclesEnabled && escrow === ZeroAddress) {
320
+ issues.push(
321
+ "Escrow not configured in registry — async flows will revert. Set the escrow via the MoreVaultsRegistry."
322
+ );
323
+ }
324
+ if (depositAccessRestricted) {
325
+ issues.push("Deposit access is restricted (whitelist or other access control). Only approved addresses can deposit.");
326
+ }
327
+
328
+ // ── maxImmediateRedeemAssets ────────────────────────────────────────────────
329
+ const maxImmediateRedeemAssets: bigint = isHub && !oraclesEnabled ? hubLiquidBalance : totalAssets;
330
+
331
+ if (isHub) {
332
+ if (hubLiquidBalance === 0n) {
333
+ issues.push(
334
+ `Hub has no liquid assets (hubLiquidBalance = 0). All redeems will be auto-refunded until the curator repatriates funds from spokes via executeBridging().`
335
+ );
336
+ } else if (totalAssets > 0n && hubLiquidBalance * 10n < totalAssets) {
337
+ const pct = Number((hubLiquidBalance * 10000n) / totalAssets) / 100;
338
+ issues.push(
339
+ `Low hub liquidity: ${hubLiquidBalance} units liquid on hub (${pct.toFixed(1)}% of TVL). ` +
340
+ `Redeems above ${hubLiquidBalance} underlying units will be auto-refunded. ` +
341
+ `Curator must call executeBridging() to repatriate from spokes.`
342
+ );
343
+ }
344
+ if (spokesDeployedBalance > 0n) {
345
+ const total = totalAssets;
346
+ issues.push(
347
+ `${spokesDeployedBalance} units (~${((Number(spokesDeployedBalance) / Number(total || 1n)) * 100).toFixed(1)}% of TVL) ` +
348
+ `are deployed on spoke chains earning yield. These are NOT immediately redeemable — ` +
349
+ `they require a curator repatriation (executeBridging) before users can withdraw them.`
350
+ );
351
+ }
352
+ }
353
+
354
+ return {
355
+ mode,
356
+ recommendedDepositFlow,
357
+ recommendedRedeemFlow,
358
+ isHub,
359
+ isPaused,
360
+ oracleAccountingEnabled: oraclesEnabled,
361
+ ccManager,
362
+ escrow,
363
+ withdrawalQueueEnabled,
364
+ withdrawalTimelockSeconds: BigInt(withdrawalTimelockSeconds),
365
+ remainingDepositCapacity: effectiveCapacity,
366
+ depositAccessRestricted,
367
+ underlying,
368
+ totalAssets,
369
+ totalSupply,
370
+ decimals: decimalsNum,
371
+ sharePrice,
372
+ hubLiquidBalance,
373
+ spokesDeployedBalance,
374
+ maxImmediateRedeemAssets,
375
+ issues,
376
+ };
377
+ }