@exponent-labs/exponent-vaults-fetcher 0.9.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/build/constants.d.ts +9 -0
- package/build/constants.js +29 -0
- package/build/constants.js.map +1 -0
- package/build/fetcher.d.ts +441 -0
- package/build/fetcher.js +351 -0
- package/build/fetcher.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.js +23 -0
- package/build/index.js.map +1 -0
- package/build/typeUtils.d.ts +19 -0
- package/build/typeUtils.js +54 -0
- package/build/typeUtils.js.map +1 -0
- package/build/types.d.ts +148 -0
- package/build/types.js +5 -0
- package/build/types.js.map +1 -0
- package/package.json +19 -0
- package/src/constants.ts +33 -0
- package/src/fetcher.ts +436 -0
- package/src/index.ts +4 -0
- package/src/typeUtils.ts +108 -0
- package/src/types.ts +160 -0
- package/tsconfig.json +22 -0
package/src/constants.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { web3 } from "@coral-xyz/anchor"
|
|
2
|
+
|
|
3
|
+
import { PROGRAM_ID } from "@exponent-labs/exponent-vaults-idl"
|
|
4
|
+
|
|
5
|
+
const EXPONENT_PRICES_SEED = new TextEncoder().encode("exponent_prices")
|
|
6
|
+
|
|
7
|
+
/** Seed used for withdrawal PDA derivation on-chain. */
|
|
8
|
+
export const WITHDRAWAL_ACCOUNT_SEED = "withdrawal" as const
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Set of vault addresses to ignore during fetching.
|
|
12
|
+
* These accounts cannot be deserialized due to program updates and should be skipped.
|
|
13
|
+
*/
|
|
14
|
+
export const IGNORED_VAULTS = new Set([
|
|
15
|
+
"Fo7vvPsSqNAYy2S7CpQoVFP8Ygnr22xB3fJVhde9GjcB",
|
|
16
|
+
"7apbV7QJHFhh5Ueczg35n6StyNdqeMmDdRwKDNCSRm7i",
|
|
17
|
+
"GsT257ZWjLboYAbWE1qDX2irHSyvCviQatyu3g2DSqmb",
|
|
18
|
+
"3KUQ8H5v4pLthqCsdPZuMnwjHVJcdkiFeWZGdX3Mr7MX",
|
|
19
|
+
"79aYbxCzLNFiF3tibivW6kva8B5qcFUAPAGweDuAyfMY",
|
|
20
|
+
"99RpUEbUMe1chVgpJNV16iXaJqdfvm459siFX9SA75nq",
|
|
21
|
+
"E957xxdpgsCbmqHo9AyHkLRTvgppxgQbZ9V8QXpQegg5",
|
|
22
|
+
"F9Tv2qBbS1YnsR4hYyaq6ztPUcTpht1ZT5Gb22DFwyg1",
|
|
23
|
+
"FKif7QNqt6cSaFj9cGL8eCz9cuSncKvbVNHxzd92r2hY",
|
|
24
|
+
"GfT6RcToh5mmkJ5DpPSBsBSJzun2WyVcsWXzNCtEfVUa",
|
|
25
|
+
"HUXvqaGbjX8kZ65ZhMhJgzfrjEX5By2gA8x9Z9mEFfQH",
|
|
26
|
+
"Hyi6LoTzcHpaQueSz4qqFdWeBydihpkRR8CqFvLWspqH",
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
/** Singleton ExponentPrices account address */
|
|
30
|
+
export const EXPONENT_PRICES_ADDRESS = web3.PublicKey.findProgramAddressSync(
|
|
31
|
+
[EXPONENT_PRICES_SEED],
|
|
32
|
+
new web3.PublicKey(PROGRAM_ID),
|
|
33
|
+
)[0].toBase58()
|
package/src/fetcher.ts
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { AnchorProvider, Program, Wallet, web3 } from "@coral-xyz/anchor"
|
|
2
|
+
|
|
3
|
+
import { ExponentVaults, IDL, PROGRAM_ID } from "@exponent-labs/exponent-vaults-idl"
|
|
4
|
+
import { ExponentVaultsPDA } from "@exponent-labs/exponent-vaults-pda"
|
|
5
|
+
|
|
6
|
+
import { IGNORED_VAULTS } from "./constants"
|
|
7
|
+
import { transformAnchorData } from "./typeUtils"
|
|
8
|
+
import { ActionProposal, ExponentPrices, ExponentVault, WithdrawalAccount } from "./types"
|
|
9
|
+
|
|
10
|
+
const EXPONENT_PRICES_META_DISCRIMINATOR = Buffer.from([188, 97, 196, 52, 192, 41, 211, 196])
|
|
11
|
+
const MAX_PRICES = 32
|
|
12
|
+
const MAX_PRICE_MANAGERS = 8
|
|
13
|
+
const PRICE_NODE_REGISTERS = 4
|
|
14
|
+
const PRICE_USAGE_AMOUNT_REGISTER = 1
|
|
15
|
+
|
|
16
|
+
const PUBKEY_BYTES = 32
|
|
17
|
+
const U32_BYTES = 4
|
|
18
|
+
const U64_BYTES = 8
|
|
19
|
+
const ZERO_PUBKEY = new web3.PublicKey(new Uint8Array(PUBKEY_BYTES))
|
|
20
|
+
|
|
21
|
+
const EXPONENT_PRICE_SIZE = 224
|
|
22
|
+
const PRICE_NODE_SIZE = EXPONENT_PRICE_SIZE
|
|
23
|
+
const NODE_REGISTERS_SIZE = PRICE_NODE_REGISTERS * U32_BYTES
|
|
24
|
+
const PRICE_NODE_WRAPPED_SIZE = NODE_REGISTERS_SIZE + PRICE_NODE_SIZE
|
|
25
|
+
|
|
26
|
+
const EXPONENT_PRICES_MIN_SIZE =
|
|
27
|
+
8 + // discriminator
|
|
28
|
+
4 + // price_managers_len
|
|
29
|
+
4 + // padding
|
|
30
|
+
MAX_PRICE_MANAGERS * PUBKEY_BYTES + // price_managers
|
|
31
|
+
8 + // allocator.size
|
|
32
|
+
4 + // allocator.bump_index
|
|
33
|
+
4 + // allocator.free_list_head
|
|
34
|
+
MAX_PRICES * PRICE_NODE_WRAPPED_SIZE + // allocator nodes
|
|
35
|
+
128 // prices_options
|
|
36
|
+
|
|
37
|
+
class MockWallet implements Wallet {
|
|
38
|
+
constructor(readonly payer: web3.Keypair) {
|
|
39
|
+
this.payer = payer
|
|
40
|
+
}
|
|
41
|
+
async signTransaction<T extends web3.Transaction | web3.VersionedTransaction>(tx: T): Promise<T> {
|
|
42
|
+
return tx
|
|
43
|
+
}
|
|
44
|
+
async signAllTransactions<T extends web3.Transaction | web3.VersionedTransaction>(txs: T[]): Promise<T[]> {
|
|
45
|
+
return txs
|
|
46
|
+
}
|
|
47
|
+
get publicKey(): web3.PublicKey {
|
|
48
|
+
return this.payer.publicKey
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ExponentVaultsFetcher {
|
|
53
|
+
public readonly program: Program<ExponentVaults>
|
|
54
|
+
public readonly programId: web3.PublicKey
|
|
55
|
+
|
|
56
|
+
constructor(connection: web3.Connection, programId: web3.PublicKey | string = PROGRAM_ID) {
|
|
57
|
+
this.programId = new web3.PublicKey(programId)
|
|
58
|
+
const provider = new AnchorProvider(connection, new MockWallet(web3.Keypair.generate()))
|
|
59
|
+
const programIdl = {
|
|
60
|
+
...(IDL as ExponentVaults),
|
|
61
|
+
address: this.programId.toBase58(),
|
|
62
|
+
} as ExponentVaults
|
|
63
|
+
|
|
64
|
+
this.program = new Program<ExponentVaults>(programIdl, provider)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fetchVault(address: web3.PublicKey) {
|
|
68
|
+
return this.program.account.exponentStrategyVault.fetch(address)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fetchWithdrawalAccount(address: web3.PublicKey) {
|
|
72
|
+
return this.program.account.withdrawalAccount.fetch(address)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fetchActionProposal(address: web3.PublicKey) {
|
|
76
|
+
return this.program.account.actionProposal.fetch(address)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async fetchAllVaults(filters?: web3.GetProgramAccountsFilter[]) {
|
|
80
|
+
const discriminator = this.program.account.exponentStrategyVault.coder.accounts.memcmp("exponentStrategyVault")
|
|
81
|
+
const baseFilters: web3.GetProgramAccountsFilter[] = discriminator
|
|
82
|
+
? [{ memcmp: { offset: discriminator.offset, bytes: discriminator.bytes } }]
|
|
83
|
+
: []
|
|
84
|
+
const allFilters = filters ? [...baseFilters, ...filters] : baseFilters
|
|
85
|
+
|
|
86
|
+
const rawAccounts = await this.program.provider.connection.getProgramAccounts(this.programId, {
|
|
87
|
+
filters: allFilters,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const decoded: { publicKey: web3.PublicKey; account: any }[] = []
|
|
91
|
+
for (const { pubkey, account } of rawAccounts) {
|
|
92
|
+
if (IGNORED_VAULTS.has(pubkey.toBase58())) continue
|
|
93
|
+
// Guard against old/mismatched vault layouts: Borsh may read garbage vec lengths
|
|
94
|
+
// from misaligned data, causing uncatchable OOM via massive array allocation.
|
|
95
|
+
// The first vec (token_entries) is at byte offset 104 (8 discriminator + 3×32 pubkeys).
|
|
96
|
+
// If its u32 length is unreasonably large, the layout is wrong — skip to prevent OOM.
|
|
97
|
+
const TOKEN_ENTRIES_VEC_OFFSET = 104
|
|
98
|
+
if (account.data.length < TOKEN_ENTRIES_VEC_OFFSET + 4) continue
|
|
99
|
+
const tokenEntriesLen = account.data.readUInt32LE(TOKEN_ENTRIES_VEC_OFFSET)
|
|
100
|
+
if (tokenEntriesLen > 64) continue
|
|
101
|
+
try {
|
|
102
|
+
const data = this.program.account.exponentStrategyVault.coder.accounts.decode(
|
|
103
|
+
"exponentStrategyVault",
|
|
104
|
+
account.data,
|
|
105
|
+
)
|
|
106
|
+
decoded.push({ publicKey: pubkey, account: data })
|
|
107
|
+
} catch {
|
|
108
|
+
// skip accounts that fail to deserialize
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return decoded
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async fetchAllActionProposals(filters?: web3.GetProgramAccountsFilter[]) {
|
|
115
|
+
const discriminator = this.program.account.actionProposal.coder.accounts.memcmp("actionProposal")
|
|
116
|
+
const baseFilters: web3.GetProgramAccountsFilter[] = discriminator
|
|
117
|
+
? [{ memcmp: { offset: discriminator.offset, bytes: discriminator.bytes } }]
|
|
118
|
+
: []
|
|
119
|
+
const allFilters = filters ? [...baseFilters, ...filters] : baseFilters
|
|
120
|
+
|
|
121
|
+
const rawAccounts = await this.program.provider.connection.getProgramAccounts(this.programId, {
|
|
122
|
+
filters: allFilters,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const decoded: { publicKey: web3.PublicKey; account: any }[] = []
|
|
126
|
+
for (const { pubkey, account } of rawAccounts) {
|
|
127
|
+
try {
|
|
128
|
+
const data = this.program.account.actionProposal.coder.accounts.decode("actionProposal", account.data)
|
|
129
|
+
decoded.push({ publicKey: pubkey, account: data })
|
|
130
|
+
} catch {
|
|
131
|
+
// skip accounts that fail to deserialize
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return decoded
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fetchAllWithdrawalAccounts(filters?: web3.GetProgramAccountsFilter[]) {
|
|
139
|
+
return this.program.account.withdrawalAccount.all(filters)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async fetchAllActionProposalsForVault(vaultAddress: web3.PublicKey, filters?: web3.GetProgramAccountsFilter[]) {
|
|
143
|
+
const vaultFilter: web3.GetProgramAccountsFilter = {
|
|
144
|
+
memcmp: {
|
|
145
|
+
offset: 8, // discriminator
|
|
146
|
+
bytes: vaultAddress.toBase58(),
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return this.fetchAllActionProposals(filters ? [vaultFilter, ...filters] : [vaultFilter])
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async fetchVaultDeserialized(address: web3.PublicKey): Promise<ExponentVault> {
|
|
154
|
+
if (IGNORED_VAULTS.has(address.toBase58())) {
|
|
155
|
+
throw new Error(`Vault ${address.toBase58()} is in the ignore list and cannot be fetched`)
|
|
156
|
+
}
|
|
157
|
+
const vault = await this.fetchVault(address)
|
|
158
|
+
return transformAnchorData(vault) as ExponentVault
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async fetchActionProposalDeserialized(address: web3.PublicKey): Promise<ActionProposal> {
|
|
162
|
+
const proposal = await this.fetchActionProposal(address)
|
|
163
|
+
return transformAnchorData(proposal) as unknown as ActionProposal
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async fetchWithdrawalAccountDeserialized(address: web3.PublicKey): Promise<WithdrawalAccount> {
|
|
167
|
+
const withdrawal = await this.fetchWithdrawalAccount(address)
|
|
168
|
+
return transformAnchorData(withdrawal)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async fetchAllVaultsDeserialized(filters?: web3.GetProgramAccountsFilter[]): Promise<
|
|
172
|
+
{
|
|
173
|
+
publicKey: web3.PublicKey
|
|
174
|
+
account: ExponentVault
|
|
175
|
+
}[]
|
|
176
|
+
> {
|
|
177
|
+
const accounts = await this.fetchAllVaults(filters)
|
|
178
|
+
return accounts.map(({ publicKey, account }) => ({
|
|
179
|
+
publicKey,
|
|
180
|
+
account: transformAnchorData(account) as ExponentVault,
|
|
181
|
+
}))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async fetchAllActionProposalsDeserialized(filters?: web3.GetProgramAccountsFilter[]): Promise<
|
|
185
|
+
{
|
|
186
|
+
publicKey: web3.PublicKey
|
|
187
|
+
account: ActionProposal
|
|
188
|
+
}[]
|
|
189
|
+
> {
|
|
190
|
+
const accounts = await this.fetchAllActionProposals(filters)
|
|
191
|
+
return accounts.map(({ publicKey, account }) => ({
|
|
192
|
+
publicKey,
|
|
193
|
+
account: transformAnchorData(account) as unknown as ActionProposal,
|
|
194
|
+
}))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async fetchAllActionProposalsForVaultDeserialized(
|
|
198
|
+
vaultAddress: web3.PublicKey,
|
|
199
|
+
filters?: web3.GetProgramAccountsFilter[],
|
|
200
|
+
): Promise<
|
|
201
|
+
{
|
|
202
|
+
publicKey: web3.PublicKey
|
|
203
|
+
account: ActionProposal
|
|
204
|
+
}[]
|
|
205
|
+
> {
|
|
206
|
+
const accounts = await this.fetchAllActionProposalsForVault(vaultAddress, filters)
|
|
207
|
+
return accounts.map(({ publicKey, account }) => ({
|
|
208
|
+
publicKey,
|
|
209
|
+
account: transformAnchorData(account) as unknown as ActionProposal,
|
|
210
|
+
}))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async fetchAllWithdrawalAccountsDeserialized(filters?: web3.GetProgramAccountsFilter[]) {
|
|
214
|
+
const accounts = await this.fetchAllWithdrawalAccounts(filters)
|
|
215
|
+
return accounts.map(({ publicKey, account }) => ({
|
|
216
|
+
publicKey,
|
|
217
|
+
account: transformAnchorData(account),
|
|
218
|
+
}))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Fetch the singleton ExponentPrices account.
|
|
223
|
+
*/
|
|
224
|
+
async fetchExponentPrices(): Promise<ExponentPrices> {
|
|
225
|
+
const address = new ExponentVaultsPDA(this.programId).exponentPrices()[0]
|
|
226
|
+
const accountInfo = await this.program.provider.connection.getAccountInfo(address)
|
|
227
|
+
|
|
228
|
+
if (!accountInfo) {
|
|
229
|
+
throw new Error(`ExponentPrices account not found: ${address.toBase58()}`)
|
|
230
|
+
}
|
|
231
|
+
if (!accountInfo.owner.equals(this.programId)) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`ExponentPrices owner mismatch: expected ${this.programId.toBase58()}, got ${accountInfo.owner.toBase58()}`,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const data = accountInfo.data
|
|
238
|
+
if (data.length < EXPONENT_PRICES_MIN_SIZE) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`ExponentPrices account too small: got ${data.length} bytes, expected at least ${EXPONENT_PRICES_MIN_SIZE}`,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const discriminator = data.subarray(0, 8)
|
|
245
|
+
if (!discriminator.equals(EXPONENT_PRICES_META_DISCRIMINATOR)) {
|
|
246
|
+
throw new Error("Invalid ExponentPrices discriminator")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let offset = 8
|
|
250
|
+
|
|
251
|
+
const readU32 = () => {
|
|
252
|
+
const value = data.readUInt32LE(offset)
|
|
253
|
+
offset += U32_BYTES
|
|
254
|
+
return value
|
|
255
|
+
}
|
|
256
|
+
const readU64 = () => {
|
|
257
|
+
const value = data.readBigUInt64LE(offset)
|
|
258
|
+
offset += U64_BYTES
|
|
259
|
+
return value
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const managersLen = readU32()
|
|
263
|
+
offset += U32_BYTES // _padding0
|
|
264
|
+
if (managersLen > MAX_PRICE_MANAGERS) {
|
|
265
|
+
throw new Error(`Invalid ExponentPrices managers length: ${managersLen}`)
|
|
266
|
+
}
|
|
267
|
+
const managers: web3.PublicKey[] = []
|
|
268
|
+
for (let i = 0; i < managersLen; i++) {
|
|
269
|
+
const managerOffset = offset + i * PUBKEY_BYTES
|
|
270
|
+
managers.push(new web3.PublicKey(data.subarray(managerOffset, managerOffset + PUBKEY_BYTES)))
|
|
271
|
+
}
|
|
272
|
+
offset += MAX_PRICE_MANAGERS * PUBKEY_BYTES // price_managers
|
|
273
|
+
|
|
274
|
+
const allocatorSize = Number(readU64())
|
|
275
|
+
const bumpIndex = readU32()
|
|
276
|
+
const freeListHead = readU32()
|
|
277
|
+
if (allocatorSize > MAX_PRICES) {
|
|
278
|
+
throw new Error(`Invalid ExponentPrices allocator size: ${allocatorSize}`)
|
|
279
|
+
}
|
|
280
|
+
if (bumpIndex > MAX_PRICES + 1) {
|
|
281
|
+
throw new Error(`Invalid ExponentPrices bump index: ${bumpIndex}`)
|
|
282
|
+
}
|
|
283
|
+
if (freeListHead > MAX_PRICES + 1) {
|
|
284
|
+
throw new Error(`Invalid ExponentPrices free list head: ${freeListHead}`)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const nodesOffset = offset
|
|
288
|
+
|
|
289
|
+
const readNodeAt = (index: number) => {
|
|
290
|
+
if (index < 1 || index > MAX_PRICES) {
|
|
291
|
+
throw new Error(`Invalid ExponentPrices node index: ${index}`)
|
|
292
|
+
}
|
|
293
|
+
const nodeOffset = nodesOffset + (index - 1) * PRICE_NODE_WRAPPED_SIZE
|
|
294
|
+
|
|
295
|
+
const usageAmount = data.readUInt32LE(nodeOffset + PRICE_USAGE_AMOUNT_REGISTER * U32_BYTES)
|
|
296
|
+
const valueOffset = nodeOffset + NODE_REGISTERS_SIZE
|
|
297
|
+
|
|
298
|
+
const priceMintBytes = data.subarray(valueOffset, valueOffset + PUBKEY_BYTES)
|
|
299
|
+
const priceMint = new web3.PublicKey(priceMintBytes)
|
|
300
|
+
const active = priceMintBytes.some((b: number) => b !== 0)
|
|
301
|
+
const underlyingMint = new web3.PublicKey(
|
|
302
|
+
data.subarray(valueOffset + PUBKEY_BYTES, valueOffset + PUBKEY_BYTES * 2),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
const priceOffset = valueOffset + PUBKEY_BYTES * 2
|
|
306
|
+
const n0 = data.readBigUInt64LE(priceOffset)
|
|
307
|
+
const n1 = data.readBigUInt64LE(priceOffset + 8)
|
|
308
|
+
const n2 = data.readBigUInt64LE(priceOffset + 16)
|
|
309
|
+
const n3 = data.readBigUInt64LE(priceOffset + 24)
|
|
310
|
+
|
|
311
|
+
const positionsAmount = data.readBigUInt64LE(valueOffset + 96)
|
|
312
|
+
const lastUpdatedAt = data.readBigUInt64LE(valueOffset + 104)
|
|
313
|
+
const lastUpdatedSlot = data.readBigUInt64LE(valueOffset + 112)
|
|
314
|
+
const priceType = data.readUInt8(valueOffset + 120)
|
|
315
|
+
const impliedApyBps = priceType === 3 ? data.readUInt32LE(valueOffset + 121) : null
|
|
316
|
+
const priceInterfaceAccounts = new web3.PublicKey(
|
|
317
|
+
data.subarray(valueOffset + 128, valueOffset + 128 + PUBKEY_BYTES),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
active,
|
|
322
|
+
usageAmount,
|
|
323
|
+
price: {
|
|
324
|
+
priceId: BigInt(index),
|
|
325
|
+
priceMint,
|
|
326
|
+
underlyingMint,
|
|
327
|
+
price: [[n0, n1, n2, n3]],
|
|
328
|
+
positionsAmount,
|
|
329
|
+
lastUpdatedAt,
|
|
330
|
+
lastUpdatedSlot,
|
|
331
|
+
priceType,
|
|
332
|
+
impliedApyBps,
|
|
333
|
+
impliedApy: impliedApyBps === null ? null : impliedApyBps / 1_000_000,
|
|
334
|
+
priceInterfaceAccounts,
|
|
335
|
+
interfaceAccounts: [],
|
|
336
|
+
},
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const prices: ExponentPrices["prices"] = Array.from({ length: MAX_PRICES + 1 }, () => null)
|
|
341
|
+
let activePrices = 0
|
|
342
|
+
for (let nodeIndex = 1; nodeIndex <= MAX_PRICES; nodeIndex++) {
|
|
343
|
+
const parsed = readNodeAt(nodeIndex)
|
|
344
|
+
if (parsed.active) {
|
|
345
|
+
prices[nodeIndex] = parsed.price
|
|
346
|
+
activePrices += 1
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (activePrices !== allocatorSize) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`Corrupted ExponentPrices allocator: parsed ${activePrices} active entries, allocator has ${allocatorSize}`,
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const activePriceEntries = prices.filter((entry) => entry !== null) as NonNullable<
|
|
357
|
+
ExponentPrices["prices"][number]
|
|
358
|
+
>[]
|
|
359
|
+
const uniqueInterfaceAccounts = [
|
|
360
|
+
...new Map(
|
|
361
|
+
activePriceEntries.map((entry) => [entry.priceInterfaceAccounts.toBase58(), entry.priceInterfaceAccounts]),
|
|
362
|
+
).values(),
|
|
363
|
+
].filter((pubkey) => !pubkey.equals(ZERO_PUBKEY))
|
|
364
|
+
|
|
365
|
+
if (uniqueInterfaceAccounts.length > 0) {
|
|
366
|
+
const infos = await this.program.provider.connection.getMultipleAccountsInfo(uniqueInterfaceAccounts)
|
|
367
|
+
const decodedInterfaceAccounts = new Map<string, web3.PublicKey[]>()
|
|
368
|
+
|
|
369
|
+
for (let i = 0; i < uniqueInterfaceAccounts.length; i++) {
|
|
370
|
+
const key = uniqueInterfaceAccounts[i]
|
|
371
|
+
const info = infos[i]
|
|
372
|
+
if (!info) {
|
|
373
|
+
throw new Error(`PriceInterfaceAccounts account not found: ${key.toBase58()}`)
|
|
374
|
+
}
|
|
375
|
+
if (!info.owner.equals(this.programId)) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`PriceInterfaceAccounts owner mismatch: expected ${this.programId.toBase58()}, got ${info.owner.toBase58()}`,
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
decodedInterfaceAccounts.set(key.toBase58(), decodePriceInterfaceAccounts(info.data))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (const entry of activePriceEntries) {
|
|
384
|
+
entry.interfaceAccounts = decodedInterfaceAccounts.get(entry.priceInterfaceAccounts.toBase58()) ?? []
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { managers, prices }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get all unique price IDs required by a vault's token account positions.
|
|
393
|
+
* Returns a set of price IDs that should exist in an ExponentPrices account.
|
|
394
|
+
*/
|
|
395
|
+
getRequiredPriceIds(vault: ExponentVault): Set<bigint> {
|
|
396
|
+
const priceIds = new Set<bigint>()
|
|
397
|
+
|
|
398
|
+
for (const position of vault.strategyPositions) {
|
|
399
|
+
if ("tokenAccount" in position) {
|
|
400
|
+
const entry = position.tokenAccount[0]
|
|
401
|
+
for (const balance of entry.balances) {
|
|
402
|
+
if ("simple" in balance.priceId) {
|
|
403
|
+
priceIds.add(balance.priceId.simple.priceId)
|
|
404
|
+
} else if ("multiply" in balance.priceId) {
|
|
405
|
+
for (const id of balance.priceId.multiply.priceIds) {
|
|
406
|
+
priceIds.add(id)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return priceIds
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function decodePriceInterfaceAccounts(data: Buffer): web3.PublicKey[] {
|
|
418
|
+
if (data.length < 12) {
|
|
419
|
+
throw new Error(`PriceInterfaceAccounts account too small: ${data.length} bytes`)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const len = data.readUInt32LE(8)
|
|
423
|
+
const expectedSize = 8 + 4 + len * PUBKEY_BYTES
|
|
424
|
+
if (data.length < expectedSize) {
|
|
425
|
+
throw new Error(`PriceInterfaceAccounts account truncated: expected ${expectedSize} bytes, got ${data.length}`)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const interfaceAccounts: web3.PublicKey[] = []
|
|
429
|
+
let offset = 12
|
|
430
|
+
for (let i = 0; i < len; i++) {
|
|
431
|
+
interfaceAccounts.push(new web3.PublicKey(data.subarray(offset, offset + PUBKEY_BYTES)))
|
|
432
|
+
offset += PUBKEY_BYTES
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return interfaceAccounts
|
|
436
|
+
}
|
package/src/index.ts
ADDED
package/src/typeUtils.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { BN, web3 } from "@coral-xyz/anchor"
|
|
2
|
+
|
|
3
|
+
// Extract enum discriminant keys
|
|
4
|
+
type AnchorEnumKey<T> =
|
|
5
|
+
T extends Record<string, any>
|
|
6
|
+
? {
|
|
7
|
+
[K in keyof T]: T[K] extends Record<string, never> ? K : never
|
|
8
|
+
}[keyof T]
|
|
9
|
+
: never
|
|
10
|
+
|
|
11
|
+
// Check if type is an Anchor enum
|
|
12
|
+
type IsAnchorEnum<T> =
|
|
13
|
+
T extends Record<string, any>
|
|
14
|
+
? {
|
|
15
|
+
[K in keyof T]: T[K] extends Record<string, never> ? true : false
|
|
16
|
+
}[keyof T] extends true
|
|
17
|
+
? true
|
|
18
|
+
: false
|
|
19
|
+
: false
|
|
20
|
+
|
|
21
|
+
// Check if type is a PublicKey (has toBase58, equals, etc.)
|
|
22
|
+
type IsPublicKey<T> = T extends { toBase58: any; equals: any; toBuffer: any } ? true : false
|
|
23
|
+
|
|
24
|
+
// Main recursive transformation with PublicKey preservation
|
|
25
|
+
type Transform<T> =
|
|
26
|
+
// Preserve PublicKey as-is
|
|
27
|
+
IsPublicKey<T> extends true
|
|
28
|
+
? web3.PublicKey
|
|
29
|
+
: T extends BN
|
|
30
|
+
? bigint
|
|
31
|
+
: T extends Array<infer U>
|
|
32
|
+
? Array<Transform<U>>
|
|
33
|
+
: T extends Map<infer K, infer V>
|
|
34
|
+
? Map<Transform<K>, Transform<V>>
|
|
35
|
+
: T extends Set<infer U>
|
|
36
|
+
? Set<Transform<U>>
|
|
37
|
+
: T extends Uint8Array
|
|
38
|
+
? Uint8Array
|
|
39
|
+
: T extends Date
|
|
40
|
+
? Date
|
|
41
|
+
: T extends object
|
|
42
|
+
? IsAnchorEnum<T> extends true
|
|
43
|
+
? AnchorEnumKey<T>
|
|
44
|
+
: {
|
|
45
|
+
[K in keyof T]: Transform<T[K]>
|
|
46
|
+
}
|
|
47
|
+
: T
|
|
48
|
+
|
|
49
|
+
// Export the main utility
|
|
50
|
+
export type TransformedAnchorType<T> = Transform<T>
|
|
51
|
+
|
|
52
|
+
// Helper function to convert at runtime
|
|
53
|
+
export function transformAnchorData<T>(data: T): TransformedAnchorType<T> {
|
|
54
|
+
// Preserve PublicKey instances
|
|
55
|
+
if (data instanceof web3.PublicKey) {
|
|
56
|
+
return data as any
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (data instanceof BN) {
|
|
60
|
+
return BigInt(data.toString()) as any
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (data instanceof Date) {
|
|
64
|
+
return data as any
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (data instanceof Uint8Array) {
|
|
68
|
+
return data as any
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (Array.isArray(data)) {
|
|
72
|
+
return data.map(transformAnchorData) as any
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (data instanceof Map) {
|
|
76
|
+
const result = new Map()
|
|
77
|
+
data.forEach((value, key) => {
|
|
78
|
+
result.set(transformAnchorData(key), transformAnchorData(value))
|
|
79
|
+
})
|
|
80
|
+
return result as any
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (data instanceof Set) {
|
|
84
|
+
return new Set([...data].map(transformAnchorData)) as any
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (data && typeof data === "object") {
|
|
88
|
+
// Check if it's an Anchor enum
|
|
89
|
+
const keys = Object.keys(data)
|
|
90
|
+
if (keys.length === 1) {
|
|
91
|
+
const value = data[keys[0] as keyof typeof data]
|
|
92
|
+
if (value && typeof value === "object" && Object.keys(value).length === 0) {
|
|
93
|
+
return keys[0] as any
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Regular object - recursively transform
|
|
98
|
+
const result: any = {}
|
|
99
|
+
for (const key in data) {
|
|
100
|
+
if (Object.prototype.hasOwnProperty.call(data, key)) {
|
|
101
|
+
result[key] = transformAnchorData(data[key])
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return data as any
|
|
108
|
+
}
|