@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,489 @@
1
+ import { type Address, type PublicClient, getAddress } from 'viem'
2
+ import { BRIDGE_ABI, CONFIG_ABI, ERC20_ABI, VAULT_ABI, METADATA_ABI } from './abis'
3
+ import type { CrossChainRequestInfo } from './types'
4
+ import { getVaultStatus } from './utils'
5
+ import type { VaultStatus } from './utils'
6
+
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ export interface UserPosition {
10
+ /** Vault share balance */
11
+ shares: bigint
12
+ /** convertToAssets(shares) — what they'd get if they redeemed now */
13
+ estimatedAssets: bigint
14
+ /** Price of 1 full share in underlying (convertToAssets(10n ** decimals)) */
15
+ sharePrice: bigint
16
+ /** Vault decimals (for display) */
17
+ decimals: number
18
+ pendingWithdrawal: {
19
+ shares: bigint
20
+ timelockEndsAt: bigint
21
+ /** block.timestamp >= timelockEndsAt (or timelockEndsAt === 0n) */
22
+ canRedeemNow: boolean
23
+ } | null // null if no pending withdrawal request
24
+ }
25
+
26
+ /**
27
+ * Read the user's current position in the vault.
28
+ *
29
+ * @param publicClient Public client for reads
30
+ * @param vault Vault address (diamond proxy)
31
+ * @param user User wallet address
32
+ * @returns Full user position snapshot
33
+ */
34
+ export async function getUserPosition(
35
+ publicClient: PublicClient,
36
+ vault: Address,
37
+ user: Address,
38
+ ): Promise<UserPosition> {
39
+ const v = getAddress(vault)
40
+ const u = getAddress(user)
41
+
42
+ // First batch: balance, decimals, withdrawal request — via multicall
43
+ const [sharesResult, decimalsResult, withdrawalRequestResult] = await publicClient.multicall({
44
+ contracts: [
45
+ { address: v, abi: VAULT_ABI, functionName: 'balanceOf', args: [u] },
46
+ { address: v, abi: METADATA_ABI, functionName: 'decimals' },
47
+ { address: v, abi: VAULT_ABI, functionName: 'getWithdrawalRequest', args: [u] },
48
+ ],
49
+ allowFailure: false,
50
+ })
51
+ const block = await publicClient.getBlock()
52
+ const shares = sharesResult
53
+ const decimals = decimalsResult
54
+ const withdrawalRequest = withdrawalRequestResult
55
+
56
+ const [withdrawShares, timelockEndsAt] = withdrawalRequest as unknown as [bigint, bigint]
57
+
58
+ // Second batch: convertToAssets calls (need shares and decimals from first batch)
59
+ const oneShare = 10n ** BigInt(decimals)
60
+ const [estimatedAssets, sharePrice] = await Promise.all([
61
+ shares === 0n
62
+ ? Promise.resolve(0n)
63
+ : publicClient.readContract({ address: v, abi: VAULT_ABI, functionName: 'convertToAssets', args: [shares] }),
64
+ publicClient.readContract({ address: v, abi: VAULT_ABI, functionName: 'convertToAssets', args: [oneShare] }),
65
+ ])
66
+
67
+ const currentTimestamp = block.timestamp
68
+
69
+ const pendingWithdrawal =
70
+ withdrawShares === 0n
71
+ ? null
72
+ : {
73
+ shares: withdrawShares,
74
+ timelockEndsAt,
75
+ canRedeemNow: timelockEndsAt === 0n || currentTimestamp >= timelockEndsAt,
76
+ }
77
+
78
+ return {
79
+ shares,
80
+ estimatedAssets,
81
+ sharePrice,
82
+ decimals,
83
+ pendingWithdrawal,
84
+ }
85
+ }
86
+
87
+ // ─────────────────────────────────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Preview how many shares a given asset amount would mint.
91
+ *
92
+ * @param publicClient Public client for reads
93
+ * @param vault Vault address
94
+ * @param assets Amount of underlying tokens to deposit
95
+ * @returns Estimated shares to be minted
96
+ */
97
+ export async function previewDeposit(
98
+ publicClient: PublicClient,
99
+ vault: Address,
100
+ assets: bigint,
101
+ ): Promise<bigint> {
102
+ return publicClient.readContract({
103
+ address: getAddress(vault),
104
+ abi: VAULT_ABI,
105
+ functionName: 'previewDeposit',
106
+ args: [assets],
107
+ })
108
+ }
109
+
110
+ // ─────────────────────────────────────────────────────────────────────────────
111
+
112
+ /**
113
+ * Preview how many underlying assets a given share amount would redeem.
114
+ *
115
+ * @param publicClient Public client for reads
116
+ * @param vault Vault address
117
+ * @param shares Amount of vault shares to redeem
118
+ * @returns Estimated assets to be returned
119
+ */
120
+ export async function previewRedeem(
121
+ publicClient: PublicClient,
122
+ vault: Address,
123
+ shares: bigint,
124
+ ): Promise<bigint> {
125
+ return publicClient.readContract({
126
+ address: getAddress(vault),
127
+ abi: VAULT_ABI,
128
+ functionName: 'previewRedeem',
129
+ args: [shares],
130
+ })
131
+ }
132
+
133
+ // ─────────────────────────────────────────────────────────────────────────────
134
+
135
+ export type DepositBlockReason = 'paused' | 'capacity-full' | 'not-whitelisted' | 'ok'
136
+
137
+ export interface DepositEligibility {
138
+ allowed: boolean
139
+ reason: DepositBlockReason
140
+ }
141
+
142
+ /**
143
+ * Check whether a user is eligible to deposit into the vault right now.
144
+ *
145
+ * @param publicClient Public client for reads
146
+ * @param vault Vault address
147
+ * @param user User wallet address
148
+ * @returns Eligibility result with reason
149
+ */
150
+ export async function canDeposit(
151
+ publicClient: PublicClient,
152
+ vault: Address,
153
+ user: Address,
154
+ ): Promise<DepositEligibility> {
155
+ const v = getAddress(vault)
156
+
157
+ const isPaused = await publicClient.readContract({
158
+ address: v,
159
+ abi: CONFIG_ABI,
160
+ functionName: 'paused',
161
+ })
162
+
163
+ if (isPaused) {
164
+ return { allowed: false, reason: 'paused' }
165
+ }
166
+
167
+ // maxDeposit(user) can REVERT on vaults with whitelist/ACL
168
+ let maxDepositAmount: bigint
169
+ try {
170
+ maxDepositAmount = await publicClient.readContract({
171
+ address: v,
172
+ abi: CONFIG_ABI,
173
+ functionName: 'maxDeposit',
174
+ args: [getAddress(user)],
175
+ })
176
+ } catch {
177
+ // Revert means the vault has whitelist/ACL and this user is not approved
178
+ return { allowed: false, reason: 'not-whitelisted' }
179
+ }
180
+
181
+ if (maxDepositAmount === 0n) {
182
+ return { allowed: false, reason: 'capacity-full' }
183
+ }
184
+ return { allowed: true, reason: 'ok' }
185
+ }
186
+
187
+ // ─────────────────────────────────────────────────────────────────────────────
188
+
189
+ export interface VaultMetadata {
190
+ name: string
191
+ symbol: string
192
+ decimals: number
193
+ underlying: Address
194
+ underlyingSymbol: string
195
+ underlyingDecimals: number
196
+ }
197
+
198
+ /**
199
+ * Read display metadata for a vault and its underlying token.
200
+ *
201
+ * @param publicClient Public client for reads
202
+ * @param vault Vault address
203
+ * @returns Vault and underlying token metadata
204
+ */
205
+ export async function getVaultMetadata(
206
+ publicClient: PublicClient,
207
+ vault: Address,
208
+ ): Promise<VaultMetadata> {
209
+ const v = getAddress(vault)
210
+
211
+ // Batch 1: vault name, symbol, decimals, underlying — 1 eth_call via multicall
212
+ const b1 = await publicClient.multicall({
213
+ contracts: [
214
+ { address: v, abi: METADATA_ABI, functionName: 'name' },
215
+ { address: v, abi: METADATA_ABI, functionName: 'symbol' },
216
+ { address: v, abi: METADATA_ABI, functionName: 'decimals' },
217
+ { address: v, abi: VAULT_ABI, functionName: 'asset' },
218
+ ] as const,
219
+ allowFailure: false,
220
+ })
221
+
222
+ const [name, symbol, decimals, underlying] = b1
223
+ const underlyingAddr = getAddress(underlying as Address)
224
+
225
+ // Batch 2: underlying symbol + decimals — 1 eth_call via multicall
226
+ const b2 = await publicClient.multicall({
227
+ contracts: [
228
+ { address: underlyingAddr, abi: METADATA_ABI, functionName: 'symbol' },
229
+ { address: underlyingAddr, abi: METADATA_ABI, functionName: 'decimals' },
230
+ ] as const,
231
+ allowFailure: false,
232
+ })
233
+
234
+ const [underlyingSymbol, underlyingDecimals] = b2
235
+
236
+ return {
237
+ name,
238
+ symbol,
239
+ decimals,
240
+ underlying: underlyingAddr,
241
+ underlyingSymbol,
242
+ underlyingDecimals,
243
+ }
244
+ }
245
+
246
+ // ─────────────────────────────────────────────────────────────────────────────
247
+
248
+ export type AsyncRequestStatus = 'pending' | 'ready-to-execute' | 'completed' | 'refunded'
249
+
250
+ export interface AsyncRequestStatusInfo {
251
+ status: AsyncRequestStatus
252
+ /** Human-readable description */
253
+ label: string
254
+ /** Shares minted or assets returned (0 if still pending) */
255
+ result: bigint
256
+ }
257
+
258
+ /**
259
+ * Get the human-readable status of an async cross-chain request.
260
+ *
261
+ * @param publicClient Public client for reads
262
+ * @param vault Vault address
263
+ * @param guid Request GUID returned by depositAsync / mintAsync / redeemAsync
264
+ * @returns Status info with label and result
265
+ */
266
+ export async function getAsyncRequestStatusLabel(
267
+ publicClient: PublicClient,
268
+ vault: Address,
269
+ guid: `0x${string}`,
270
+ ): Promise<AsyncRequestStatusInfo> {
271
+ const v = getAddress(vault)
272
+
273
+ const [info, finalizationResult] = await Promise.all([
274
+ publicClient.readContract({
275
+ address: v,
276
+ abi: BRIDGE_ABI,
277
+ functionName: 'getRequestInfo',
278
+ args: [guid],
279
+ }) as Promise<CrossChainRequestInfo>,
280
+ publicClient.readContract({
281
+ address: v,
282
+ abi: BRIDGE_ABI,
283
+ functionName: 'getFinalizationResult',
284
+ args: [guid],
285
+ }),
286
+ ])
287
+
288
+ if (info.refunded) {
289
+ return {
290
+ status: 'refunded',
291
+ label: 'Request refunded — tokens returned to initiator',
292
+ result: 0n,
293
+ }
294
+ }
295
+ if (info.finalized) {
296
+ return {
297
+ status: 'completed',
298
+ label: 'Completed',
299
+ result: finalizationResult,
300
+ }
301
+ }
302
+ if (info.fulfilled) {
303
+ return {
304
+ status: 'ready-to-execute',
305
+ label: 'Oracle responded — ready to execute',
306
+ result: 0n,
307
+ }
308
+ }
309
+ return {
310
+ status: 'pending',
311
+ label: 'Waiting for cross-chain oracle response...',
312
+ result: 0n,
313
+ }
314
+ }
315
+
316
+ // ─────────────────────────────────────────────────────────────────────────────
317
+
318
+ export interface UserBalances {
319
+ /** Vault shares the user holds */
320
+ shareBalance: bigint
321
+ /** Underlying token balance in wallet (for deposit input) */
322
+ underlyingBalance: bigint
323
+ /** convertToAssets(shareBalance) — vault position value */
324
+ estimatedAssets: bigint
325
+ }
326
+
327
+ /**
328
+ * Read the user's token balances relevant to a vault.
329
+ *
330
+ * @param publicClient Public client for reads
331
+ * @param vault Vault address
332
+ * @param user User wallet address
333
+ * @returns Share balance, underlying wallet balance, and estimated assets
334
+ */
335
+ export async function getUserBalances(
336
+ publicClient: PublicClient,
337
+ vault: Address,
338
+ user: Address,
339
+ ): Promise<UserBalances> {
340
+ const v = getAddress(vault)
341
+ const u = getAddress(user)
342
+
343
+ // Batch 1: get underlying address, share balance, decimals
344
+ const [shareBalance, , underlying] = await publicClient.multicall({
345
+ contracts: [
346
+ { address: v, abi: VAULT_ABI, functionName: 'balanceOf', args: [u] },
347
+ { address: v, abi: METADATA_ABI, functionName: 'decimals' },
348
+ { address: v, abi: VAULT_ABI, functionName: 'asset' },
349
+ ],
350
+ allowFailure: false,
351
+ })
352
+
353
+ const underlyingAddr = getAddress(underlying)
354
+
355
+ // Batch 2: underlying balance + estimated assets (skip convertToAssets if no shares)
356
+ const [underlyingBalance, estimatedAssets] = await Promise.all([
357
+ publicClient.readContract({
358
+ address: underlyingAddr,
359
+ abi: ERC20_ABI,
360
+ functionName: 'balanceOf',
361
+ args: [u],
362
+ }),
363
+ shareBalance === 0n
364
+ ? Promise.resolve(0n)
365
+ : publicClient.readContract({
366
+ address: v,
367
+ abi: VAULT_ABI,
368
+ functionName: 'convertToAssets',
369
+ args: [shareBalance],
370
+ }),
371
+ ])
372
+
373
+ return {
374
+ shareBalance,
375
+ underlyingBalance,
376
+ estimatedAssets,
377
+ }
378
+ }
379
+
380
+ // ─────────────────────────────────────────────────────────────────────────────
381
+
382
+ export interface MaxWithdrawable {
383
+ /** How many shares can be redeemed right now */
384
+ shares: bigint
385
+ /** How many underlying assets that corresponds to */
386
+ assets: bigint
387
+ }
388
+
389
+ /**
390
+ * Calculate the maximum amount a user can withdraw from a vault right now.
391
+ *
392
+ * For hub vaults without oracle accounting, this is limited by hub liquidity.
393
+ * For local and oracle vaults, all assets are immediately redeemable.
394
+ *
395
+ * @param publicClient Public client for reads
396
+ * @param vault Vault address
397
+ * @param user User wallet address
398
+ * @returns Maximum withdrawable shares and assets
399
+ */
400
+ export async function getMaxWithdrawable(
401
+ publicClient: PublicClient,
402
+ vault: Address,
403
+ user: Address,
404
+ ): Promise<MaxWithdrawable> {
405
+ const v = getAddress(vault)
406
+ const u = getAddress(user)
407
+
408
+ // Batch 1: isHub, oraclesCrossChainAccounting, user share balance, underlying address
409
+ const [isHub, oraclesEnabled, userShares, underlying] = await publicClient.multicall({
410
+ contracts: [
411
+ { address: v, abi: CONFIG_ABI, functionName: 'isHub' },
412
+ { address: v, abi: BRIDGE_ABI, functionName: 'oraclesCrossChainAccounting' },
413
+ { address: v, abi: VAULT_ABI, functionName: 'balanceOf', args: [u] },
414
+ { address: v, abi: VAULT_ABI, functionName: 'asset' },
415
+ ],
416
+ allowFailure: false,
417
+ })
418
+
419
+ if (userShares === 0n) {
420
+ return { shares: 0n, assets: 0n }
421
+ }
422
+
423
+ const underlyingAddr = getAddress(underlying)
424
+
425
+ // Batch 2: estimated assets for user shares + hub liquid balance
426
+ const [estimatedAssets, hubLiquidBalance] = await Promise.all([
427
+ publicClient.readContract({
428
+ address: v,
429
+ abi: VAULT_ABI,
430
+ functionName: 'convertToAssets',
431
+ args: [userShares],
432
+ }),
433
+ publicClient.readContract({
434
+ address: underlyingAddr,
435
+ abi: ERC20_ABI,
436
+ functionName: 'balanceOf',
437
+ args: [v],
438
+ }),
439
+ ])
440
+
441
+ let maxAssets: bigint
442
+ if (isHub && !oraclesEnabled) {
443
+ // Hub vault: limited by hub liquidity
444
+ maxAssets = estimatedAssets < hubLiquidBalance ? estimatedAssets : hubLiquidBalance
445
+ } else {
446
+ // Local or oracle vault: all assets redeemable
447
+ maxAssets = estimatedAssets
448
+ }
449
+
450
+ // Convert back to shares if limited by hub liquidity
451
+ let maxShares: bigint
452
+ if (maxAssets < estimatedAssets) {
453
+ maxShares = await publicClient.readContract({
454
+ address: v,
455
+ abi: VAULT_ABI,
456
+ functionName: 'convertToShares',
457
+ args: [maxAssets],
458
+ })
459
+ } else {
460
+ maxShares = userShares
461
+ }
462
+
463
+ return {
464
+ shares: maxShares,
465
+ assets: maxAssets,
466
+ }
467
+ }
468
+
469
+ // ─────────────────────────────────────────────────────────────────────────────
470
+
471
+ export type VaultSummary = VaultStatus & VaultMetadata
472
+
473
+ /**
474
+ * Get a combined snapshot of vault status and metadata in one call.
475
+ *
476
+ * @param publicClient Public client for reads
477
+ * @param vault Vault address
478
+ * @returns Merged VaultStatus and VaultMetadata
479
+ */
480
+ export async function getVaultSummary(
481
+ publicClient: PublicClient,
482
+ vault: Address,
483
+ ): Promise<VaultSummary> {
484
+ const [status, metadata] = await Promise.all([
485
+ getVaultStatus(publicClient, vault),
486
+ getVaultMetadata(publicClient, vault),
487
+ ])
488
+ return { ...status, ...metadata }
489
+ }