@oydual31/more-vaults-sdk 0.2.3 → 0.2.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oydual31/more-vaults-sdk",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "TypeScript SDK for MoreVaults protocol — viem/wagmi and ethers.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -11,6 +11,9 @@ export type { VaultMetadata } from './useVaultMetadata.js'
11
11
  export { useUserPosition } from './useUserPosition.js'
12
12
  export type { UserPosition } from './useUserPosition.js'
13
13
 
14
+ export { useUserPositionMultiChain } from './useUserPositionMultiChain.js'
15
+ export type { MultiChainUserPosition } from './useUserPositionMultiChain.js'
16
+
14
17
  export { useLzFee } from './useLzFee.js'
15
18
 
16
19
  export { useAsyncRequestStatus } from './useAsyncRequestStatus.js'
@@ -0,0 +1,33 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import { getUserPositionMultiChain } from '../viem/userHelpers.js'
3
+ import type { MultiChainUserPosition } from '../viem/userHelpers.js'
4
+
5
+ export type { MultiChainUserPosition }
6
+
7
+ /**
8
+ * Read the user's position across all chains of an omni vault.
9
+ *
10
+ * Discovers topology automatically, reads hub shares + pending withdrawal,
11
+ * then reads SHARE_OFT balances on each spoke chain in parallel.
12
+ * Works without a connected wallet (uses public RPCs).
13
+ *
14
+ * @example
15
+ * const { data: position } = useUserPositionMultiChain('0xVAULT', '0xUSER')
16
+ * // position.hubShares — shares on hub (Base)
17
+ * // position.spokeShares — { 1: 500n, 42161: 0n } per spoke
18
+ * // position.totalShares — hub + all spokes
19
+ * // position.estimatedAssets — convertToAssets(totalShares)
20
+ * // position.pendingWithdrawal — async withdrawal if any
21
+ */
22
+ export function useUserPositionMultiChain(
23
+ vault: `0x${string}` | undefined,
24
+ user: `0x${string}` | undefined,
25
+ ) {
26
+ return useQuery<MultiChainUserPosition>({
27
+ queryKey: ['userPositionMultiChain', vault, user],
28
+ queryFn: () => getUserPositionMultiChain(vault!, user!),
29
+ enabled: !!vault && !!user,
30
+ refetchInterval: 30_000,
31
+ staleTime: 15_000,
32
+ })
33
+ }
package/src/viem/index.ts CHANGED
@@ -83,9 +83,10 @@ export {
83
83
  quoteLzFee,
84
84
  isAsyncMode,
85
85
  getAsyncRequestStatus,
86
+ waitForAsyncRequest,
86
87
  getVaultStatus,
87
88
  } from './utils'
88
- export type { VaultStatus, VaultMode } from './utils'
89
+ export type { VaultStatus, VaultMode, AsyncRequestFinalResult } from './utils'
89
90
 
90
91
  // --- Pre-flight validation ---
91
92
  export { preflightSync, preflightAsync, preflightRedeemLiquidity, preflightSpokeDeposit, preflightSpokeRedeem } from './preflight'
@@ -101,6 +102,7 @@ export {
101
102
  getUserBalances,
102
103
  getMaxWithdrawable,
103
104
  getVaultSummary,
105
+ getUserPositionMultiChain,
104
106
  } from './userHelpers'
105
107
  export type {
106
108
  UserPosition,
@@ -112,6 +114,7 @@ export type {
112
114
  UserBalances,
113
115
  MaxWithdrawable,
114
116
  VaultSummary,
117
+ MultiChainUserPosition,
115
118
  } from './userHelpers'
116
119
 
117
120
  // --- Topology ---
@@ -1,8 +1,11 @@
1
1
  import { type Address, type PublicClient, getAddress } from 'viem'
2
- import { BRIDGE_ABI, CONFIG_ABI, ERC20_ABI, VAULT_ABI, METADATA_ABI } from './abis'
2
+ import { BRIDGE_ABI, CONFIG_ABI, ERC20_ABI, VAULT_ABI, METADATA_ABI, OFT_ABI } from './abis'
3
3
  import type { CrossChainRequestInfo } from './types'
4
4
  import { getVaultStatus } from './utils'
5
5
  import type { VaultStatus } from './utils'
6
+ import { discoverVaultTopology, OMNI_FACTORY_ADDRESS } from './topology'
7
+ import { createChainClient } from './spokeRoutes'
8
+ import { CHAIN_ID_TO_EID } from './chains'
6
9
 
7
10
  // ─────────────────────────────────────────────────────────────────────────────
8
11
 
@@ -498,3 +501,183 @@ export async function getVaultSummary(
498
501
  ])
499
502
  return { ...status, ...metadata }
500
503
  }
504
+
505
+ // ─────────────────────────────────────────────────────────────────────────────
506
+
507
+ const FACTORY_COMPOSER_ABI = [
508
+ {
509
+ type: 'function' as const,
510
+ name: 'vaultComposer',
511
+ inputs: [{ name: '_vault', type: 'address' }],
512
+ outputs: [{ name: '', type: 'address' }],
513
+ stateMutability: 'view' as const,
514
+ },
515
+ ] as const
516
+
517
+ const COMPOSER_SHARE_OFT_ABI = [
518
+ {
519
+ type: 'function' as const,
520
+ name: 'SHARE_OFT',
521
+ inputs: [],
522
+ outputs: [{ name: '', type: 'address' }],
523
+ stateMutability: 'view' as const,
524
+ },
525
+ ] as const
526
+
527
+ export interface MultiChainUserPosition {
528
+ /** Shares held directly on the hub vault (vault.balanceOf) */
529
+ hubShares: bigint
530
+ /** Per-spoke SHARE_OFT balances: { [chainId]: bigint } */
531
+ spokeShares: Record<number, bigint>
532
+ /** hubShares + sum of all spokeShares */
533
+ totalShares: bigint
534
+ /** convertToAssets(totalShares) on the hub */
535
+ estimatedAssets: bigint
536
+ /** Share price: convertToAssets(10^decimals) */
537
+ sharePrice: bigint
538
+ /** Vault decimals */
539
+ decimals: number
540
+ /** Pending async withdrawal request on hub, or null */
541
+ pendingWithdrawal: {
542
+ shares: bigint
543
+ timelockEndsAt: bigint
544
+ canRedeemNow: boolean
545
+ } | null
546
+ }
547
+
548
+ /**
549
+ * Read the user's position across all chains of an omni vault.
550
+ *
551
+ * Discovers topology automatically, reads hub shares + pending withdrawal,
552
+ * then reads SHARE_OFT balances on each spoke chain in parallel.
553
+ *
554
+ * For local (single-chain) vaults, spokeShares will be empty and this
555
+ * behaves identically to getUserPosition.
556
+ *
557
+ * @param vault Vault address (same on all chains via CREATE3)
558
+ * @param user User wallet address
559
+ * @returns Aggregated position across all chains
560
+ */
561
+ export async function getUserPositionMultiChain(
562
+ vault: Address,
563
+ user: Address,
564
+ ): Promise<MultiChainUserPosition> {
565
+ const v = getAddress(vault)
566
+ const u = getAddress(user)
567
+
568
+ // Step 1: discover topology
569
+ const topo = await discoverVaultTopology(vault)
570
+ const hubClient = createChainClient(topo.hubChainId)
571
+ if (!hubClient) throw new Error(`No public RPC for hub chainId ${topo.hubChainId}`)
572
+
573
+ // Step 2: read hub data (shares, decimals, withdrawal request)
574
+ const [hubShares, decimals, withdrawalRequest] = await (hubClient as PublicClient).multicall({
575
+ contracts: [
576
+ { address: v, abi: VAULT_ABI, functionName: 'balanceOf', args: [u] },
577
+ { address: v, abi: METADATA_ABI, functionName: 'decimals' },
578
+ { address: v, abi: VAULT_ABI, functionName: 'getWithdrawalRequest', args: [u] },
579
+ ],
580
+ allowFailure: false,
581
+ })
582
+
583
+ const [withdrawShares, timelockEndsAt] = withdrawalRequest as unknown as [bigint, bigint]
584
+
585
+ // Step 3: resolve SHARE_OFT addresses for spokes (if any)
586
+ const spokeShares: Record<number, bigint> = {}
587
+
588
+ if (topo.spokeChainIds.length > 0) {
589
+ // Get hub SHARE_OFT via factory → composer → SHARE_OFT
590
+ let hubShareOft: Address | null = null
591
+ try {
592
+ const composerAddress = await (hubClient as PublicClient).readContract({
593
+ address: OMNI_FACTORY_ADDRESS,
594
+ abi: FACTORY_COMPOSER_ABI,
595
+ functionName: 'vaultComposer',
596
+ args: [v],
597
+ }) as Address
598
+
599
+ if (composerAddress !== '0x0000000000000000000000000000000000000000') {
600
+ hubShareOft = await (hubClient as PublicClient).readContract({
601
+ address: composerAddress,
602
+ abi: COMPOSER_SHARE_OFT_ABI,
603
+ functionName: 'SHARE_OFT',
604
+ }) as Address
605
+ }
606
+ } catch { /* no composer — skip spoke reads */ }
607
+
608
+ if (hubShareOft) {
609
+ // Read spoke SHARE_OFT addresses via peers() and balances in parallel
610
+ const spokePromises = topo.spokeChainIds.map(async (spokeChainId) => {
611
+ try {
612
+ const spokeEid = CHAIN_ID_TO_EID[spokeChainId]
613
+ if (!spokeEid) return { chainId: spokeChainId, balance: 0n }
614
+
615
+ // Get spoke SHARE_OFT address from hub peers()
616
+ const spokeOftBytes32 = await (hubClient as PublicClient).readContract({
617
+ address: hubShareOft!,
618
+ abi: OFT_ABI,
619
+ functionName: 'peers',
620
+ args: [spokeEid],
621
+ }) as `0x${string}`
622
+
623
+ const spokeOft = getAddress(`0x${spokeOftBytes32.slice(-40)}`) as Address
624
+ if (spokeOft === '0x0000000000000000000000000000000000000000') {
625
+ return { chainId: spokeChainId, balance: 0n }
626
+ }
627
+
628
+ // Read balance on spoke chain
629
+ const spokeClient = createChainClient(spokeChainId)
630
+ if (!spokeClient) return { chainId: spokeChainId, balance: 0n }
631
+
632
+ const balance = await (spokeClient as PublicClient).readContract({
633
+ address: spokeOft,
634
+ abi: ERC20_ABI,
635
+ functionName: 'balanceOf',
636
+ args: [u],
637
+ })
638
+
639
+ return { chainId: spokeChainId, balance }
640
+ } catch {
641
+ return { chainId: spokeChainId, balance: 0n }
642
+ }
643
+ })
644
+
645
+ const results = await Promise.all(spokePromises)
646
+ for (const { chainId, balance } of results) {
647
+ spokeShares[chainId] = balance
648
+ }
649
+ }
650
+ }
651
+
652
+ // Step 4: compute totals
653
+ const totalSpokeShares = Object.values(spokeShares).reduce((sum, b) => sum + b, 0n)
654
+ const totalShares = hubShares + totalSpokeShares
655
+
656
+ const oneShare = 10n ** BigInt(decimals)
657
+ const [estimatedAssets, sharePrice] = await Promise.all([
658
+ totalShares === 0n
659
+ ? Promise.resolve(0n)
660
+ : (hubClient as PublicClient).readContract({ address: v, abi: VAULT_ABI, functionName: 'convertToAssets', args: [totalShares] }),
661
+ (hubClient as PublicClient).readContract({ address: v, abi: VAULT_ABI, functionName: 'convertToAssets', args: [oneShare] }),
662
+ ])
663
+
664
+ // Step 5: pending withdrawal
665
+ const block = await (hubClient as PublicClient).getBlock()
666
+ const pendingWithdrawal = withdrawShares === 0n
667
+ ? null
668
+ : {
669
+ shares: withdrawShares,
670
+ timelockEndsAt,
671
+ canRedeemNow: timelockEndsAt === 0n || block.timestamp >= timelockEndsAt,
672
+ }
673
+
674
+ return {
675
+ hubShares,
676
+ spokeShares,
677
+ totalShares,
678
+ estimatedAssets,
679
+ sharePrice,
680
+ decimals,
681
+ pendingWithdrawal,
682
+ }
683
+ }
package/src/viem/utils.ts CHANGED
@@ -437,7 +437,7 @@ export async function getAsyncRequestStatus(
437
437
  publicClient: PublicClient,
438
438
  vault: Address,
439
439
  guid: `0x${string}`,
440
- ): Promise<{ fulfilled: boolean; finalized: boolean; result: bigint }> {
440
+ ): Promise<{ fulfilled: boolean; finalized: boolean; refunded: boolean; result: bigint }> {
441
441
  const info = (await publicClient.readContract({
442
442
  address: getAddress(vault),
443
443
  abi: BRIDGE_ABI,
@@ -455,6 +455,70 @@ export async function getAsyncRequestStatus(
455
455
  return {
456
456
  fulfilled: info.fulfilled,
457
457
  finalized: info.finalized,
458
+ refunded: info.refunded,
458
459
  result: finalizationResult,
459
460
  }
460
461
  }
462
+
463
+ export interface AsyncRequestFinalResult {
464
+ /** 'completed' = assets/shares received, 'refunded' = tokens returned to user */
465
+ status: 'completed' | 'refunded'
466
+ /** For deposit: shares minted. For redeem: assets returned. 0 if refunded. */
467
+ result: bigint
468
+ }
469
+
470
+ /**
471
+ * Poll an async cross-chain request until it finalizes or times out.
472
+ *
473
+ * This is the correct way to wait for smartDeposit/smartRedeem results on
474
+ * async vaults. Do NOT poll balance — use this instead, which reads the
475
+ * on-chain request state by GUID.
476
+ *
477
+ * @param publicClient Public client on the hub chain
478
+ * @param vault Vault address
479
+ * @param guid GUID from smartDeposit/smartRedeem result
480
+ * @param pollInterval Milliseconds between polls (default: 30_000)
481
+ * @param timeout Max wait time in milliseconds (default: 900_000 = 15 min)
482
+ * @param onPoll Optional callback invoked after each poll with current status
483
+ * @returns Final result with status and amount
484
+ * @throws Error if timeout is reached
485
+ *
486
+ * @example
487
+ * const depositResult = await smartDeposit(walletClient, publicClient, { vault }, amount, receiver)
488
+ * if ('guid' in depositResult) {
489
+ * const final = await waitForAsyncRequest(publicClient, vault, depositResult.guid, 30_000, 900_000, (s) => {
490
+ * console.log(`Status: fulfilled=${s.fulfilled}, finalized=${s.finalized}`)
491
+ * })
492
+ * console.log(`Done: ${final.status}, result: ${final.result}`)
493
+ * }
494
+ */
495
+ export async function waitForAsyncRequest(
496
+ publicClient: PublicClient,
497
+ vault: Address,
498
+ guid: `0x${string}`,
499
+ pollInterval: number = 30_000,
500
+ timeout: number = 900_000,
501
+ onPoll?: (status: { fulfilled: boolean; finalized: boolean; refunded: boolean; result: bigint }) => void,
502
+ ): Promise<AsyncRequestFinalResult> {
503
+ const deadline = Date.now() + timeout
504
+
505
+ while (Date.now() < deadline) {
506
+ const status = await getAsyncRequestStatus(publicClient, vault, guid)
507
+
508
+ if (onPoll) onPoll(status)
509
+
510
+ if (status.finalized) {
511
+ return { status: 'completed', result: status.result }
512
+ }
513
+ if (status.refunded) {
514
+ return { status: 'refunded', result: 0n }
515
+ }
516
+
517
+ await new Promise(r => setTimeout(r, pollInterval))
518
+ }
519
+
520
+ throw new Error(
521
+ `[MoreVaults] Async request ${guid} did not finalize within ${timeout / 1000}s. ` +
522
+ `The request may still complete — check https://layerzeroscan.com/tx/${guid}`,
523
+ )
524
+ }