@oydual31/more-vaults-sdk 0.4.2 → 0.6.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.
- package/README.md +94 -0
- package/dist/{spokeRoutes-B8Lnk-t4.d.cts → curatorBridge-CNs59kT9.d.cts} +222 -1
- package/dist/{spokeRoutes-B8Lnk-t4.d.ts → curatorBridge-CNs59kT9.d.ts} +222 -1
- package/dist/ethers/index.cjs +328 -3
- package/dist/ethers/index.cjs.map +1 -1
- package/dist/ethers/index.d.cts +279 -1
- package/dist/ethers/index.d.ts +279 -1
- package/dist/ethers/index.js +318 -5
- package/dist/ethers/index.js.map +1 -1
- package/dist/react/index.cjs +375 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +266 -2
- package/dist/react/index.d.ts +266 -2
- package/dist/react/index.js +372 -2
- package/dist/react/index.js.map +1 -1
- package/dist/viem/index.cjs +377 -0
- package/dist/viem/index.cjs.map +1 -1
- package/dist/viem/index.d.cts +261 -3
- package/dist/viem/index.d.ts +261 -3
- package/dist/viem/index.js +367 -2
- package/dist/viem/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ethers/abis.ts +24 -0
- package/src/ethers/curatorBridge.ts +235 -0
- package/src/ethers/curatorSubVaults.ts +443 -0
- package/src/ethers/index.ts +26 -0
- package/src/ethers/types.ts +99 -0
- package/src/react/index.ts +14 -0
- package/src/react/useCuratorBridgeQuote.ts +43 -0
- package/src/react/useERC7540RequestStatus.ts +43 -0
- package/src/react/useExecuteBridge.ts +50 -0
- package/src/react/useSubVaultPositions.ts +35 -0
- package/src/react/useVaultPortfolio.ts +35 -0
- package/src/viem/abis.ts +24 -0
- package/src/viem/curatorBridge.ts +288 -0
- package/src/viem/curatorSubVaults.ts +514 -0
- package/src/viem/index.ts +23 -0
- package/src/viem/types.ts +100 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curator sub-vault read helpers for the MoreVaults SDK (Phase 5).
|
|
3
|
+
*
|
|
4
|
+
* Provides portfolio views and sub-vault analysis for curator dashboards.
|
|
5
|
+
* Supports both ERC4626 (synchronous) and ERC7540 (asynchronous) sub-vaults.
|
|
6
|
+
*
|
|
7
|
+
* All functions are read-only (no wallet needed) and use multicall for
|
|
8
|
+
* batched RPC efficiency.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type Address,
|
|
13
|
+
type PublicClient,
|
|
14
|
+
getAddress,
|
|
15
|
+
keccak256,
|
|
16
|
+
toHex,
|
|
17
|
+
zeroAddress,
|
|
18
|
+
} from 'viem'
|
|
19
|
+
import {
|
|
20
|
+
SUB_VAULT_ABI,
|
|
21
|
+
ERC20_ABI,
|
|
22
|
+
METADATA_ABI,
|
|
23
|
+
VAULT_ABI,
|
|
24
|
+
VAULT_ANALYSIS_ABI,
|
|
25
|
+
REGISTRY_ABI,
|
|
26
|
+
} from './abis.js'
|
|
27
|
+
import type {
|
|
28
|
+
SubVaultPosition,
|
|
29
|
+
SubVaultInfo,
|
|
30
|
+
ERC7540RequestStatus,
|
|
31
|
+
VaultPortfolio,
|
|
32
|
+
} from './types.js'
|
|
33
|
+
import type { AssetBalance } from './types.js'
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// Internal constants
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** keccak256("ERC4626_ID") — type ID for synchronous ERC4626 sub-vaults */
|
|
40
|
+
const ERC4626_ID = keccak256(toHex('ERC4626_ID')) as `0x${string}`
|
|
41
|
+
|
|
42
|
+
/** keccak256("ERC7540_ID") — type ID for asynchronous ERC7540 sub-vaults */
|
|
43
|
+
const ERC7540_ID = keccak256(toHex('ERC7540_ID')) as `0x${string}`
|
|
44
|
+
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get active sub-vault positions held by the vault.
|
|
49
|
+
*
|
|
50
|
+
* Queries the vault's `tokensHeld` for ERC4626 and ERC7540 type IDs, then
|
|
51
|
+
* fetches balances, underlying values, and token metadata for each sub-vault
|
|
52
|
+
* in a single multicall round. Sub-vaults with zero share balance are excluded.
|
|
53
|
+
*
|
|
54
|
+
* @param publicClient Viem public client (must be on the vault's chain)
|
|
55
|
+
* @param vault Vault address (diamond proxy)
|
|
56
|
+
* @returns Array of active SubVaultPosition objects
|
|
57
|
+
*/
|
|
58
|
+
export async function getSubVaultPositions(
|
|
59
|
+
publicClient: PublicClient,
|
|
60
|
+
vault: Address,
|
|
61
|
+
): Promise<SubVaultPosition[]> {
|
|
62
|
+
const v = getAddress(vault)
|
|
63
|
+
|
|
64
|
+
// Step 1: fetch the list of sub-vaults by type in parallel
|
|
65
|
+
const [erc4626Raw, erc7540Raw] = await Promise.all([
|
|
66
|
+
publicClient
|
|
67
|
+
.readContract({
|
|
68
|
+
address: v,
|
|
69
|
+
abi: SUB_VAULT_ABI,
|
|
70
|
+
functionName: 'tokensHeld',
|
|
71
|
+
args: [ERC4626_ID],
|
|
72
|
+
})
|
|
73
|
+
.catch(() => [] as Address[]),
|
|
74
|
+
publicClient
|
|
75
|
+
.readContract({
|
|
76
|
+
address: v,
|
|
77
|
+
abi: SUB_VAULT_ABI,
|
|
78
|
+
functionName: 'tokensHeld',
|
|
79
|
+
args: [ERC7540_ID],
|
|
80
|
+
})
|
|
81
|
+
.catch(() => [] as Address[]),
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
const erc4626Vaults = (erc4626Raw as Address[]).map(getAddress)
|
|
85
|
+
const erc7540Vaults = (erc7540Raw as Address[]).map(getAddress)
|
|
86
|
+
|
|
87
|
+
const allSubVaults: Array<{ address: Address; type: 'erc4626' | 'erc7540' }> = [
|
|
88
|
+
...erc4626Vaults.map((a) => ({ address: a, type: 'erc4626' as const })),
|
|
89
|
+
...erc7540Vaults.map((a) => ({ address: a, type: 'erc7540' as const })),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
if (allSubVaults.length === 0) return []
|
|
93
|
+
|
|
94
|
+
// Step 2: multicall — for each sub-vault:
|
|
95
|
+
// balanceOf(vault), asset(), name(), symbol(), decimals()
|
|
96
|
+
// That's 5 calls per sub-vault = slots 0..4, 5..9, etc.
|
|
97
|
+
const PER_SV = 5
|
|
98
|
+
const subVaultCalls = allSubVaults.flatMap(({ address: sv }) => [
|
|
99
|
+
{ address: sv, abi: ERC20_ABI, functionName: 'balanceOf' as const, args: [v] as [Address] },
|
|
100
|
+
{ address: sv, abi: VAULT_ABI, functionName: 'asset' as const },
|
|
101
|
+
{ address: sv, abi: METADATA_ABI, functionName: 'name' as const },
|
|
102
|
+
{ address: sv, abi: METADATA_ABI, functionName: 'symbol' as const },
|
|
103
|
+
{ address: sv, abi: METADATA_ABI, functionName: 'decimals' as const },
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
const subVaultResults = await publicClient.multicall({
|
|
107
|
+
contracts: subVaultCalls,
|
|
108
|
+
allowFailure: true,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Parse per-sub-vault results and collect underlying asset addresses
|
|
112
|
+
interface PartialSV {
|
|
113
|
+
address: Address
|
|
114
|
+
type: 'erc4626' | 'erc7540'
|
|
115
|
+
sharesBalance: bigint
|
|
116
|
+
underlyingAsset: Address
|
|
117
|
+
name: string
|
|
118
|
+
symbol: string
|
|
119
|
+
decimals: number
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const partials: PartialSV[] = allSubVaults.map(({ address: sv, type }, i) => {
|
|
123
|
+
const base = i * PER_SV
|
|
124
|
+
const sharesBalance = subVaultResults[base]?.status === 'success'
|
|
125
|
+
? (subVaultResults[base].result as bigint)
|
|
126
|
+
: 0n
|
|
127
|
+
const underlyingAsset = subVaultResults[base + 1]?.status === 'success'
|
|
128
|
+
? getAddress(subVaultResults[base + 1].result as Address)
|
|
129
|
+
: zeroAddress
|
|
130
|
+
const name = subVaultResults[base + 2]?.status === 'success' ? (subVaultResults[base + 2].result as string) : ''
|
|
131
|
+
const symbol = subVaultResults[base + 3]?.status === 'success' ? (subVaultResults[base + 3].result as string) : ''
|
|
132
|
+
const decimals = subVaultResults[base + 4]?.status === 'success' ? (subVaultResults[base + 4].result as number) : 18
|
|
133
|
+
|
|
134
|
+
return { address: sv, type, sharesBalance, underlyingAsset, name, symbol, decimals }
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Filter out sub-vaults with no position
|
|
138
|
+
const active = partials.filter((p) => p.sharesBalance > 0n)
|
|
139
|
+
if (active.length === 0) return []
|
|
140
|
+
|
|
141
|
+
// Step 3: multicall — for each active sub-vault:
|
|
142
|
+
// convertToAssets(sharesBalance) on the sub-vault
|
|
143
|
+
// name(), symbol(), decimals() on the underlying asset
|
|
144
|
+
// That's 4 calls per active sub-vault = slots 0..3, 4..7, etc.
|
|
145
|
+
const PER_ACTIVE = 4
|
|
146
|
+
const activeCalls = active.flatMap(({ address: sv, sharesBalance, underlyingAsset }) => [
|
|
147
|
+
{ address: sv, abi: SUB_VAULT_ABI, functionName: 'convertToAssets' as const, args: [sharesBalance] as [bigint] },
|
|
148
|
+
{ address: underlyingAsset, abi: METADATA_ABI, functionName: 'name' as const },
|
|
149
|
+
{ address: underlyingAsset, abi: METADATA_ABI, functionName: 'symbol' as const },
|
|
150
|
+
{ address: underlyingAsset, abi: METADATA_ABI, functionName: 'decimals' as const },
|
|
151
|
+
])
|
|
152
|
+
|
|
153
|
+
const activeResults = await publicClient.multicall({
|
|
154
|
+
contracts: activeCalls,
|
|
155
|
+
allowFailure: true,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
return active.map((p, i) => {
|
|
159
|
+
const base = i * PER_ACTIVE
|
|
160
|
+
const underlyingValue = activeResults[base]?.status === 'success' ? (activeResults[base].result as bigint) : 0n
|
|
161
|
+
const underlyingSymbol = activeResults[base + 2]?.status === 'success' ? (activeResults[base + 2].result as string) : ''
|
|
162
|
+
const underlyingDecimals = activeResults[base + 3]?.status === 'success' ? (activeResults[base + 3].result as number) : 18
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
address: p.address,
|
|
166
|
+
type: p.type,
|
|
167
|
+
name: p.name,
|
|
168
|
+
symbol: p.symbol,
|
|
169
|
+
decimals: p.decimals,
|
|
170
|
+
sharesBalance: p.sharesBalance,
|
|
171
|
+
underlyingValue,
|
|
172
|
+
underlyingAsset: p.underlyingAsset,
|
|
173
|
+
underlyingSymbol,
|
|
174
|
+
underlyingDecimals,
|
|
175
|
+
} satisfies SubVaultPosition
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Detect whether a contract is an ERC7540, ERC4626, or unknown vault type.
|
|
183
|
+
*
|
|
184
|
+
* Tries to call ERC7540-specific functions first (pendingDepositRequest /
|
|
185
|
+
* claimableDepositRequest). If those revert, falls back to ERC4626
|
|
186
|
+
* convertToAssets(0). Returns null if neither succeeds.
|
|
187
|
+
*
|
|
188
|
+
* @param publicClient Viem public client (must be on the same chain as subVault)
|
|
189
|
+
* @param subVault Sub-vault contract address to probe
|
|
190
|
+
* @returns 'erc7540' | 'erc4626' | null
|
|
191
|
+
*/
|
|
192
|
+
export async function detectSubVaultType(
|
|
193
|
+
publicClient: PublicClient,
|
|
194
|
+
subVault: Address,
|
|
195
|
+
): Promise<'erc4626' | 'erc7540' | null> {
|
|
196
|
+
const sv = getAddress(subVault)
|
|
197
|
+
|
|
198
|
+
const [erc7540Result, erc4626Result] = await publicClient.multicall({
|
|
199
|
+
contracts: [
|
|
200
|
+
{
|
|
201
|
+
address: sv,
|
|
202
|
+
abi: SUB_VAULT_ABI,
|
|
203
|
+
functionName: 'pendingDepositRequest',
|
|
204
|
+
args: [0n, zeroAddress],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
address: sv,
|
|
208
|
+
abi: SUB_VAULT_ABI,
|
|
209
|
+
functionName: 'convertToAssets',
|
|
210
|
+
args: [0n],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
allowFailure: true,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
if (erc7540Result?.status === 'success') return 'erc7540'
|
|
217
|
+
if (erc4626Result?.status === 'success') return 'erc4626'
|
|
218
|
+
return null
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Analyse a specific sub-vault to understand deposit limits, metadata, type,
|
|
225
|
+
* and global-registry whitelist status.
|
|
226
|
+
*
|
|
227
|
+
* Useful for curators deciding whether to invest vault funds into a given
|
|
228
|
+
* ERC4626 or ERC7540 protocol.
|
|
229
|
+
*
|
|
230
|
+
* @param publicClient Viem public client (must be on the vault's chain)
|
|
231
|
+
* @param vault Vault address (diamond proxy) — used to check maxDeposit
|
|
232
|
+
* @param subVault Sub-vault address to analyse
|
|
233
|
+
* @returns SubVaultInfo snapshot
|
|
234
|
+
*/
|
|
235
|
+
export async function getSubVaultInfo(
|
|
236
|
+
publicClient: PublicClient,
|
|
237
|
+
vault: Address,
|
|
238
|
+
subVault: Address,
|
|
239
|
+
): Promise<SubVaultInfo> {
|
|
240
|
+
const v = getAddress(vault)
|
|
241
|
+
const sv = getAddress(subVault)
|
|
242
|
+
|
|
243
|
+
// Detect type and fetch basic metadata in parallel
|
|
244
|
+
const [type, basicResults] = await Promise.all([
|
|
245
|
+
detectSubVaultType(publicClient, sv),
|
|
246
|
+
publicClient.multicall({
|
|
247
|
+
contracts: [
|
|
248
|
+
{ address: sv, abi: METADATA_ABI, functionName: 'name' as const },
|
|
249
|
+
{ address: sv, abi: METADATA_ABI, functionName: 'symbol' as const },
|
|
250
|
+
{ address: sv, abi: METADATA_ABI, functionName: 'decimals' as const },
|
|
251
|
+
{ address: sv, abi: VAULT_ABI, functionName: 'asset' as const },
|
|
252
|
+
{ address: sv, abi: SUB_VAULT_ABI, functionName: 'maxDeposit' as const, args: [v] as [Address] },
|
|
253
|
+
],
|
|
254
|
+
allowFailure: true,
|
|
255
|
+
}),
|
|
256
|
+
])
|
|
257
|
+
|
|
258
|
+
const name = basicResults[0]?.status === 'success' ? (basicResults[0].result as string) : ''
|
|
259
|
+
const symbol = basicResults[1]?.status === 'success' ? (basicResults[1].result as string) : ''
|
|
260
|
+
const decimals = basicResults[2]?.status === 'success' ? (basicResults[2].result as number) : 18
|
|
261
|
+
const underlying = basicResults[3]?.status === 'success'
|
|
262
|
+
? getAddress(basicResults[3].result as Address)
|
|
263
|
+
: zeroAddress
|
|
264
|
+
const maxDeposit = basicResults[4]?.status === 'success' ? (basicResults[4].result as bigint) : 0n
|
|
265
|
+
|
|
266
|
+
// Fetch underlying asset metadata and whitelist status in parallel
|
|
267
|
+
const [underlyingResults, registryRaw] = await Promise.all([
|
|
268
|
+
publicClient.multicall({
|
|
269
|
+
contracts: [
|
|
270
|
+
{ address: underlying, abi: METADATA_ABI, functionName: 'name' as const },
|
|
271
|
+
{ address: underlying, abi: METADATA_ABI, functionName: 'symbol' as const },
|
|
272
|
+
{ address: underlying, abi: METADATA_ABI, functionName: 'decimals' as const },
|
|
273
|
+
],
|
|
274
|
+
allowFailure: true,
|
|
275
|
+
}),
|
|
276
|
+
publicClient
|
|
277
|
+
.readContract({ address: v, abi: VAULT_ANALYSIS_ABI, functionName: 'moreVaultsRegistry' })
|
|
278
|
+
.catch(() => null),
|
|
279
|
+
])
|
|
280
|
+
|
|
281
|
+
const underlyingSymbol = underlyingResults[1]?.status === 'success' ? (underlyingResults[1].result as string) : ''
|
|
282
|
+
const underlyingDecimals = underlyingResults[2]?.status === 'success' ? (underlyingResults[2].result as number) : 18
|
|
283
|
+
|
|
284
|
+
let isWhitelisted = false
|
|
285
|
+
if (registryRaw) {
|
|
286
|
+
const registry = getAddress(registryRaw as Address)
|
|
287
|
+
const whitelistResult = await publicClient
|
|
288
|
+
.readContract({
|
|
289
|
+
address: registry,
|
|
290
|
+
abi: REGISTRY_ABI,
|
|
291
|
+
functionName: 'isWhitelisted',
|
|
292
|
+
args: [sv],
|
|
293
|
+
})
|
|
294
|
+
.catch(() => false)
|
|
295
|
+
isWhitelisted = whitelistResult as boolean
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
address: sv,
|
|
300
|
+
type: type ?? 'erc4626',
|
|
301
|
+
name,
|
|
302
|
+
symbol,
|
|
303
|
+
decimals,
|
|
304
|
+
underlyingAsset: underlying,
|
|
305
|
+
underlyingSymbol,
|
|
306
|
+
underlyingDecimals,
|
|
307
|
+
maxDeposit,
|
|
308
|
+
isWhitelisted,
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get the ERC7540 async request status for a specific sub-vault and vault controller.
|
|
316
|
+
*
|
|
317
|
+
* Queries pendingDepositRequest, claimableDepositRequest, pendingRedeemRequest,
|
|
318
|
+
* and claimableRedeemRequest using requestId = 0 (the standard default).
|
|
319
|
+
*
|
|
320
|
+
* @param publicClient Viem public client (must be on the vault's chain)
|
|
321
|
+
* @param vault Vault address acting as controller in the sub-vault
|
|
322
|
+
* @param subVault ERC7540 sub-vault address
|
|
323
|
+
* @returns ERC7540RequestStatus with canFinalize flags
|
|
324
|
+
*/
|
|
325
|
+
export async function getERC7540RequestStatus(
|
|
326
|
+
publicClient: PublicClient,
|
|
327
|
+
vault: Address,
|
|
328
|
+
subVault: Address,
|
|
329
|
+
): Promise<ERC7540RequestStatus> {
|
|
330
|
+
const v = getAddress(vault)
|
|
331
|
+
const sv = getAddress(subVault)
|
|
332
|
+
|
|
333
|
+
const results = await publicClient.multicall({
|
|
334
|
+
contracts: [
|
|
335
|
+
{ address: sv, abi: SUB_VAULT_ABI, functionName: 'pendingDepositRequest' as const, args: [0n, v] as [bigint, Address] },
|
|
336
|
+
{ address: sv, abi: SUB_VAULT_ABI, functionName: 'claimableDepositRequest' as const, args: [0n, v] as [bigint, Address] },
|
|
337
|
+
{ address: sv, abi: SUB_VAULT_ABI, functionName: 'pendingRedeemRequest' as const, args: [0n, v] as [bigint, Address] },
|
|
338
|
+
{ address: sv, abi: SUB_VAULT_ABI, functionName: 'claimableRedeemRequest' as const, args: [0n, v] as [bigint, Address] },
|
|
339
|
+
],
|
|
340
|
+
allowFailure: true,
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const pendingDeposit = results[0]?.status === 'success' ? (results[0].result as bigint) : 0n
|
|
344
|
+
const claimableDeposit = results[1]?.status === 'success' ? (results[1].result as bigint) : 0n
|
|
345
|
+
const pendingRedeem = results[2]?.status === 'success' ? (results[2].result as bigint) : 0n
|
|
346
|
+
const claimableRedeem = results[3]?.status === 'success' ? (results[3].result as bigint) : 0n
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
subVault: sv,
|
|
350
|
+
pendingDeposit,
|
|
351
|
+
claimableDeposit,
|
|
352
|
+
pendingRedeem,
|
|
353
|
+
claimableRedeem,
|
|
354
|
+
canFinalizeDeposit: claimableDeposit > 0n,
|
|
355
|
+
canFinalizeRedeem: claimableRedeem > 0n,
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Preview how many shares the vault would receive for a given asset deposit
|
|
363
|
+
* into a sub-vault.
|
|
364
|
+
*
|
|
365
|
+
* Calls `previewDeposit(assets)` on the sub-vault contract.
|
|
366
|
+
*
|
|
367
|
+
* @param publicClient Viem public client
|
|
368
|
+
* @param subVault Sub-vault address (ERC4626 or ERC7540)
|
|
369
|
+
* @param assets Amount of underlying assets to preview
|
|
370
|
+
* @returns Expected shares to be minted
|
|
371
|
+
*/
|
|
372
|
+
export async function previewSubVaultDeposit(
|
|
373
|
+
publicClient: PublicClient,
|
|
374
|
+
subVault: Address,
|
|
375
|
+
assets: bigint,
|
|
376
|
+
): Promise<bigint> {
|
|
377
|
+
const result = await publicClient.readContract({
|
|
378
|
+
address: getAddress(subVault),
|
|
379
|
+
abi: SUB_VAULT_ABI,
|
|
380
|
+
functionName: 'previewDeposit',
|
|
381
|
+
args: [assets],
|
|
382
|
+
})
|
|
383
|
+
return result as bigint
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Preview how many underlying assets the vault would receive for redeeming
|
|
390
|
+
* a given number of shares from a sub-vault.
|
|
391
|
+
*
|
|
392
|
+
* Calls `previewRedeem(shares)` on the sub-vault contract.
|
|
393
|
+
*
|
|
394
|
+
* @param publicClient Viem public client
|
|
395
|
+
* @param subVault Sub-vault address (ERC4626 or ERC7540)
|
|
396
|
+
* @param shares Number of shares to preview redemption for
|
|
397
|
+
* @returns Expected underlying assets to be returned
|
|
398
|
+
*/
|
|
399
|
+
export async function previewSubVaultRedeem(
|
|
400
|
+
publicClient: PublicClient,
|
|
401
|
+
subVault: Address,
|
|
402
|
+
shares: bigint,
|
|
403
|
+
): Promise<bigint> {
|
|
404
|
+
const result = await publicClient.readContract({
|
|
405
|
+
address: getAddress(subVault),
|
|
406
|
+
abi: SUB_VAULT_ABI,
|
|
407
|
+
functionName: 'previewRedeem',
|
|
408
|
+
args: [shares],
|
|
409
|
+
})
|
|
410
|
+
return result as bigint
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get the complete portfolio view for a vault, combining liquid asset balances
|
|
417
|
+
* with active sub-vault positions and locked ERC7540 assets.
|
|
418
|
+
*
|
|
419
|
+
* Liquid assets that are also sub-vault share tokens are deduplicated to avoid
|
|
420
|
+
* double-counting (the sub-vault's underlying value is already captured via
|
|
421
|
+
* convertToAssets).
|
|
422
|
+
*
|
|
423
|
+
* @param publicClient Viem public client (must be on the vault's hub chain)
|
|
424
|
+
* @param vault Vault address (diamond proxy)
|
|
425
|
+
* @returns VaultPortfolio with full breakdown
|
|
426
|
+
*/
|
|
427
|
+
export async function getVaultPortfolio(
|
|
428
|
+
publicClient: PublicClient,
|
|
429
|
+
vault: Address,
|
|
430
|
+
): Promise<VaultPortfolio> {
|
|
431
|
+
const v = getAddress(vault)
|
|
432
|
+
|
|
433
|
+
// Step 1: get available assets, sub-vault positions, totalAssets, totalSupply, underlying in parallel
|
|
434
|
+
const [availableRaw, subVaultPositions, vaultTotals] = await Promise.all([
|
|
435
|
+
publicClient
|
|
436
|
+
.readContract({ address: v, abi: VAULT_ANALYSIS_ABI, functionName: 'getAvailableAssets' })
|
|
437
|
+
.catch(() => [] as Address[]),
|
|
438
|
+
getSubVaultPositions(publicClient, v),
|
|
439
|
+
publicClient.multicall({
|
|
440
|
+
contracts: [
|
|
441
|
+
{ address: v, abi: VAULT_ABI, functionName: 'totalAssets' as const },
|
|
442
|
+
{ address: v, abi: VAULT_ABI, functionName: 'totalSupply' as const },
|
|
443
|
+
{ address: v, abi: VAULT_ABI, functionName: 'asset' as const },
|
|
444
|
+
],
|
|
445
|
+
allowFailure: true,
|
|
446
|
+
}),
|
|
447
|
+
])
|
|
448
|
+
|
|
449
|
+
const totalAssets = vaultTotals[0]?.status === 'success' ? (vaultTotals[0].result as bigint) : 0n
|
|
450
|
+
const totalSupply = vaultTotals[1]?.status === 'success' ? (vaultTotals[1].result as bigint) : 0n
|
|
451
|
+
const underlyingAsset = vaultTotals[2]?.status === 'success'
|
|
452
|
+
? getAddress(vaultTotals[2].result as Address)
|
|
453
|
+
: zeroAddress
|
|
454
|
+
|
|
455
|
+
const availableAddresses = (availableRaw as Address[]).map(getAddress)
|
|
456
|
+
|
|
457
|
+
// Sub-vault share addresses to exclude from liquid assets (avoid double-counting)
|
|
458
|
+
const subVaultAddressSet = new Set(subVaultPositions.map((p) => p.address.toLowerCase()))
|
|
459
|
+
|
|
460
|
+
// Filter liquid asset addresses: exclude sub-vault share tokens
|
|
461
|
+
const liquidAddresses = availableAddresses.filter(
|
|
462
|
+
(addr) => !subVaultAddressSet.has(addr.toLowerCase()),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
// Step 2: fetch balances + metadata for liquid assets in one multicall
|
|
466
|
+
const PER_ASSET = 4 // balanceOf, name, symbol, decimals
|
|
467
|
+
const liquidCalls = liquidAddresses.flatMap((addr) => [
|
|
468
|
+
{ address: addr, abi: ERC20_ABI, functionName: 'balanceOf' as const, args: [v] as [Address] },
|
|
469
|
+
{ address: addr, abi: METADATA_ABI, functionName: 'name' as const },
|
|
470
|
+
{ address: addr, abi: METADATA_ABI, functionName: 'symbol' as const },
|
|
471
|
+
{ address: addr, abi: METADATA_ABI, functionName: 'decimals' as const },
|
|
472
|
+
])
|
|
473
|
+
|
|
474
|
+
const liquidResults = liquidAddresses.length > 0
|
|
475
|
+
? await publicClient.multicall({ contracts: liquidCalls, allowFailure: true })
|
|
476
|
+
: []
|
|
477
|
+
|
|
478
|
+
const liquidAssets: AssetBalance[] = liquidAddresses.map((addr, i) => {
|
|
479
|
+
const base = i * PER_ASSET
|
|
480
|
+
const balance = liquidResults[base]?.status === 'success' ? (liquidResults[base].result as bigint) : 0n
|
|
481
|
+
const name = liquidResults[base + 1]?.status === 'success' ? (liquidResults[base + 1].result as string) : ''
|
|
482
|
+
const symbol = liquidResults[base + 2]?.status === 'success' ? (liquidResults[base + 2].result as string) : ''
|
|
483
|
+
const decimals = liquidResults[base + 3]?.status === 'success' ? (liquidResults[base + 3].result as number) : 18
|
|
484
|
+
return { address: addr, name, symbol, decimals, balance }
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
// Step 3: fetch locked assets for the vault's underlying (ERC7540 pending requests)
|
|
488
|
+
const lockedAssets = await publicClient
|
|
489
|
+
.readContract({
|
|
490
|
+
address: v,
|
|
491
|
+
abi: SUB_VAULT_ABI,
|
|
492
|
+
functionName: 'lockedTokensAmountOfAsset',
|
|
493
|
+
args: [underlyingAsset],
|
|
494
|
+
})
|
|
495
|
+
.catch(() => 0n) as bigint
|
|
496
|
+
|
|
497
|
+
// Step 4: compute total value = liquid assets (underlying only) + sub-vault underlying values
|
|
498
|
+
// totalAssets from the vault already accounts for all positions, so we use it directly.
|
|
499
|
+
// We also provide a manual sum of sub-vault values for reference.
|
|
500
|
+
const subVaultTotal = subVaultPositions.reduce((sum, p) => sum + p.underlyingValue, 0n)
|
|
501
|
+
const underlyingBalance = liquidAssets.find(
|
|
502
|
+
(a) => a.address.toLowerCase() === underlyingAsset.toLowerCase(),
|
|
503
|
+
)?.balance ?? 0n
|
|
504
|
+
const totalValue = underlyingBalance + subVaultTotal
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
liquidAssets,
|
|
508
|
+
subVaultPositions,
|
|
509
|
+
totalValue,
|
|
510
|
+
totalAssets,
|
|
511
|
+
totalSupply,
|
|
512
|
+
lockedAssets,
|
|
513
|
+
}
|
|
514
|
+
}
|
package/src/viem/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export {
|
|
|
22
22
|
LZ_ADAPTER_ABI,
|
|
23
23
|
VAULT_ANALYSIS_ABI,
|
|
24
24
|
REGISTRY_ABI,
|
|
25
|
+
SUB_VAULT_ABI,
|
|
25
26
|
} from './abis'
|
|
26
27
|
|
|
27
28
|
// --- Types ---
|
|
@@ -45,6 +46,10 @@ export type {
|
|
|
45
46
|
AssetBalance,
|
|
46
47
|
VaultAnalysis,
|
|
47
48
|
VaultAssetBreakdown,
|
|
49
|
+
SubVaultPosition,
|
|
50
|
+
SubVaultInfo,
|
|
51
|
+
ERC7540RequestStatus,
|
|
52
|
+
VaultPortfolio,
|
|
48
53
|
} from './types'
|
|
49
54
|
export { ActionType } from './types'
|
|
50
55
|
|
|
@@ -178,6 +183,24 @@ export {
|
|
|
178
183
|
buildUniswapV3Swap,
|
|
179
184
|
encodeUniswapV3SwapCalldata,
|
|
180
185
|
} from './curatorSwaps'
|
|
186
|
+
export {
|
|
187
|
+
encodeBridgeParams,
|
|
188
|
+
quoteCuratorBridgeFee,
|
|
189
|
+
executeCuratorBridge,
|
|
190
|
+
findBridgeRoute,
|
|
191
|
+
} from './curatorBridge'
|
|
192
|
+
export type { CuratorBridgeParams } from './curatorBridge'
|
|
193
|
+
|
|
194
|
+
// --- Curator Sub-Vault Operations ---
|
|
195
|
+
export {
|
|
196
|
+
getSubVaultPositions,
|
|
197
|
+
detectSubVaultType,
|
|
198
|
+
getSubVaultInfo,
|
|
199
|
+
getERC7540RequestStatus,
|
|
200
|
+
previewSubVaultDeposit,
|
|
201
|
+
previewSubVaultRedeem,
|
|
202
|
+
getVaultPortfolio,
|
|
203
|
+
} from './curatorSubVaults'
|
|
181
204
|
|
|
182
205
|
// --- wagmi compatibility ---
|
|
183
206
|
// Re-export viem's PublicClient type for wagmi compatibility.
|
package/src/viem/types.ts
CHANGED
|
@@ -193,3 +193,103 @@ export interface VaultAssetBreakdown {
|
|
|
193
193
|
/** Vault underlying token decimals */
|
|
194
194
|
underlyingDecimals: number
|
|
195
195
|
}
|
|
196
|
+
|
|
197
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
198
|
+
// Sub-vault Types (Phase 5)
|
|
199
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* A single active sub-vault position held by the curator vault.
|
|
203
|
+
* Covers both ERC4626 (synchronous) and ERC7540 (asynchronous) sub-vaults.
|
|
204
|
+
*/
|
|
205
|
+
export interface SubVaultPosition {
|
|
206
|
+
/** Sub-vault contract address */
|
|
207
|
+
address: Address
|
|
208
|
+
/** Protocol type of the sub-vault */
|
|
209
|
+
type: 'erc4626' | 'erc7540'
|
|
210
|
+
/** Name of the sub-vault share token */
|
|
211
|
+
name: string
|
|
212
|
+
/** Symbol of the sub-vault share token */
|
|
213
|
+
symbol: string
|
|
214
|
+
/** Decimals of the sub-vault share token */
|
|
215
|
+
decimals: number
|
|
216
|
+
/** Shares of the sub-vault held by the curator vault */
|
|
217
|
+
sharesBalance: bigint
|
|
218
|
+
/** Current value of the shares in terms of the sub-vault's underlying asset */
|
|
219
|
+
underlyingValue: bigint
|
|
220
|
+
/** Underlying asset address of the sub-vault */
|
|
221
|
+
underlyingAsset: Address
|
|
222
|
+
/** Symbol of the sub-vault's underlying asset */
|
|
223
|
+
underlyingSymbol: string
|
|
224
|
+
/** Decimals of the sub-vault's underlying asset */
|
|
225
|
+
underlyingDecimals: number
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Metadata and capability snapshot for a potential sub-vault investment target.
|
|
230
|
+
*/
|
|
231
|
+
export interface SubVaultInfo {
|
|
232
|
+
/** Sub-vault contract address */
|
|
233
|
+
address: Address
|
|
234
|
+
/** Protocol type: ERC4626 (sync) or ERC7540 (async) */
|
|
235
|
+
type: 'erc4626' | 'erc7540'
|
|
236
|
+
/** Sub-vault share token name */
|
|
237
|
+
name: string
|
|
238
|
+
/** Sub-vault share token symbol */
|
|
239
|
+
symbol: string
|
|
240
|
+
/** Sub-vault share token decimals */
|
|
241
|
+
decimals: number
|
|
242
|
+
/** Underlying asset address */
|
|
243
|
+
underlyingAsset: Address
|
|
244
|
+
/** Underlying asset symbol */
|
|
245
|
+
underlyingSymbol: string
|
|
246
|
+
/** Underlying asset decimals */
|
|
247
|
+
underlyingDecimals: number
|
|
248
|
+
/** Maximum amount the curator vault can deposit (from maxDeposit(vault)) */
|
|
249
|
+
maxDeposit: bigint
|
|
250
|
+
/** Whether the sub-vault is whitelisted in the global MoreVaults registry */
|
|
251
|
+
isWhitelisted: boolean
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Status of pending and claimable ERC7540 async requests for a sub-vault.
|
|
256
|
+
* Uses requestId = 0 (the standard default for non-batch ERC7540 vaults).
|
|
257
|
+
*/
|
|
258
|
+
export interface ERC7540RequestStatus {
|
|
259
|
+
/** Sub-vault address these statuses belong to */
|
|
260
|
+
subVault: Address
|
|
261
|
+
/** Assets in a pending deposit request (not yet claimable) */
|
|
262
|
+
pendingDeposit: bigint
|
|
263
|
+
/** Assets ready to be claimed/finalized as shares */
|
|
264
|
+
claimableDeposit: bigint
|
|
265
|
+
/** Shares in a pending redeem request (not yet claimable) */
|
|
266
|
+
pendingRedeem: bigint
|
|
267
|
+
/** Assets ready to be claimed after redeem fulfillment */
|
|
268
|
+
claimableRedeem: bigint
|
|
269
|
+
/** True if claimableDeposit > 0 — vault can call erc7540Deposit to finalize */
|
|
270
|
+
canFinalizeDeposit: boolean
|
|
271
|
+
/** True if claimableRedeem > 0 — vault can call erc7540Redeem to finalize */
|
|
272
|
+
canFinalizeRedeem: boolean
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Full portfolio view for a curator vault combining liquid and invested assets.
|
|
277
|
+
*/
|
|
278
|
+
export interface VaultPortfolio {
|
|
279
|
+
/** Liquid ERC20 asset balances held directly by the vault (excludes sub-vault share tokens) */
|
|
280
|
+
liquidAssets: AssetBalance[]
|
|
281
|
+
/** Active positions in ERC4626/ERC7540 sub-vaults */
|
|
282
|
+
subVaultPositions: SubVaultPosition[]
|
|
283
|
+
/**
|
|
284
|
+
* Approximate total value in underlying terms:
|
|
285
|
+
* liquid underlying balance + sum of sub-vault convertToAssets values.
|
|
286
|
+
* For the authoritative total, use `totalAssets` from the vault contract.
|
|
287
|
+
*/
|
|
288
|
+
totalValue: bigint
|
|
289
|
+
/** totalAssets() from the vault — authoritative AUM figure */
|
|
290
|
+
totalAssets: bigint
|
|
291
|
+
/** totalSupply() of vault shares */
|
|
292
|
+
totalSupply: bigint
|
|
293
|
+
/** Assets locked in pending ERC7540 requests (lockedTokensAmountOfAsset) */
|
|
294
|
+
lockedAssets: bigint
|
|
295
|
+
}
|