@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/dist/react/index.cjs +132 -10
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +22 -5
- package/dist/react/index.d.ts +22 -5
- package/dist/react/index.js +132 -11
- package/dist/react/index.js.map +1 -1
- package/dist/{spokeRoutes-CK5NSOOF.d.cts → spokeRoutes-CXMG7nLp.d.cts} +35 -1
- package/dist/{spokeRoutes-CK5NSOOF.d.ts → spokeRoutes-CXMG7nLp.d.ts} +35 -1
- package/dist/viem/index.cjs +110 -0
- package/dist/viem/index.cjs.map +1 -1
- package/dist/viem/index.d.cts +1 -1
- package/dist/viem/index.d.ts +1 -1
- package/dist/viem/index.js +110 -1
- package/dist/viem/index.js.map +1 -1
- package/package.json +1 -1
- package/src/react/index.ts +3 -0
- package/src/react/useUserPositionMultiChain.ts +33 -0
- package/src/react/useVaultDistribution.ts +21 -20
- package/src/viem/index.ts +2 -0
- package/src/viem/userHelpers.ts +184 -1
package/package.json
CHANGED
package/src/react/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
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
|
|
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,
|
|
59
|
-
queryFn: () =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 ---
|
package/src/viem/userHelpers.ts
CHANGED
|
@@ -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
|
+
}
|