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