@oydual31/more-vaults-sdk 0.2.2 → 0.2.4

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.2",
3
+ "version": "0.2.4",
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
+ }
@@ -1,12 +1,11 @@
1
1
  import { useMemo } from 'react'
2
2
  import { useQuery } from '@tanstack/react-query'
3
- import { useChainId, usePublicClient } from 'wagmi'
4
3
  import type { Address, PublicClient } from 'viem'
5
- import { asSdkClient } from '../viem/wagmiCompat.js'
6
4
  import { getVaultDistribution } from '../viem/distribution.js'
7
5
  import type { VaultDistribution } from '../viem/distribution.js'
8
6
  import { createChainClient } from '../viem/spokeRoutes.js'
9
- import { useVaultTopology } from './useVaultTopology.js'
7
+ import { discoverVaultTopology } from '../viem/topology.js'
8
+ import type { VaultTopology } from '../viem/topology.js'
10
9
 
11
10
  export type { VaultDistribution }
12
11
 
@@ -18,9 +17,9 @@ interface UseVaultDistributionReturn {
18
17
  /**
19
18
  * Read the full cross-chain capital distribution of a vault.
20
19
  *
21
- * Uses the connected wallet's chain as the hub client (via wagmi),
22
- * discovers spoke chains via topology, and creates ephemeral public
23
- * clients with fallback RPCs for spoke reads (all supported chains covered).
20
+ * Discovers the vault topology automatically via `discoverVaultTopology`
21
+ * (works without a connected wallet), then creates hub and spoke clients
22
+ * via public RPCs.
24
23
  *
25
24
  * Spoke reads that fail (bad RPC, timeout) degrade gracefully —
26
25
  * those spokes will appear with `isReachable: false`.
@@ -37,13 +36,15 @@ interface UseVaultDistributionReturn {
37
36
  export function useVaultDistribution(
38
37
  vault: Address | undefined,
39
38
  ): UseVaultDistributionReturn {
40
- const chainId = useChainId()
41
- const publicClient = usePublicClient()
42
- const { topology } = useVaultTopology(vault)
39
+ // Step 1: discover topology (wallet-independent)
40
+ const { data: topology } = useQuery<VaultTopology>({
41
+ queryKey: ['vaultTopology', vault],
42
+ queryFn: () => discoverVaultTopology(vault!),
43
+ enabled: !!vault,
44
+ staleTime: 5 * 60 * 1000,
45
+ })
43
46
 
44
- // Build spoke clients using the shared fallback-RPC factory from spokeRoutes.
45
- // Covers all supported chains (Eth, Arb, Op, BSC, Sonic, Flow) with multiple
46
- // fallback endpoints each — spokes without a known RPC degrade to isReachable: false.
47
+ // Build spoke clients from topology
47
48
  const spokeClients = useMemo((): Record<number, PublicClient> => {
48
49
  if (!topology) return {}
49
50
  const clients: Record<number, PublicClient> = {}
@@ -54,15 +55,15 @@ export function useVaultDistribution(
54
55
  return clients
55
56
  }, [topology])
56
57
 
58
+ // Step 2: fetch distribution using hub-chain client (not wallet client)
57
59
  const { data: distribution, isLoading } = useQuery<VaultDistribution>({
58
- queryKey: ['vaultDistribution', vault, chainId],
59
- queryFn: () =>
60
- getVaultDistribution(
61
- asSdkClient(publicClient),
62
- vault!,
63
- spokeClients,
64
- ),
65
- enabled: !!vault && !!publicClient,
60
+ queryKey: ['vaultDistribution', vault, topology?.hubChainId],
61
+ queryFn: () => {
62
+ const hubClient = createChainClient(topology!.hubChainId)
63
+ if (!hubClient) throw new Error(`No public RPC for hub chainId ${topology!.hubChainId}`)
64
+ return getVaultDistribution(hubClient as PublicClient, vault!, spokeClients)
65
+ },
66
+ enabled: !!vault && !!topology && topology.role !== 'local',
66
67
  staleTime: 30_000,
67
68
  })
68
69
 
package/src/viem/index.ts CHANGED
@@ -101,6 +101,7 @@ export {
101
101
  getUserBalances,
102
102
  getMaxWithdrawable,
103
103
  getVaultSummary,
104
+ getUserPositionMultiChain,
104
105
  } from './userHelpers'
105
106
  export type {
106
107
  UserPosition,
@@ -112,6 +113,7 @@ export type {
112
113
  UserBalances,
113
114
  MaxWithdrawable,
114
115
  VaultSummary,
116
+ MultiChainUserPosition,
115
117
  } from './userHelpers'
116
118
 
117
119
  // --- 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
+ }