@w3lib/solana-batch-call 0.6.0 → 0.7.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/CHANGELOG.md +33 -0
- package/README.md +16 -8
- package/dist/accountReader.d.ts +57 -0
- package/dist/accountReader.d.ts.map +1 -0
- package/dist/accountReader.js +149 -0
- package/dist/accountReader.js.map +1 -0
- package/dist/address.d.ts +9 -0
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +36 -6
- package/dist/address.js.map +1 -1
- package/dist/approvals.d.ts +5 -3
- package/dist/approvals.d.ts.map +1 -1
- package/dist/approvals.js +25 -36
- package/dist/approvals.js.map +1 -1
- package/dist/balances.d.ts +178 -0
- package/dist/balances.d.ts.map +1 -0
- package/dist/balances.js +524 -0
- package/dist/balances.js.map +1 -0
- package/dist/batchPlan.d.ts +2 -2
- package/dist/batchPlan.d.ts.map +1 -1
- package/dist/batchPlan.js +5 -26
- package/dist/batchPlan.js.map +1 -1
- package/dist/bytes.d.ts +31 -0
- package/dist/bytes.d.ts.map +1 -0
- package/dist/bytes.js +74 -0
- package/dist/bytes.js.map +1 -0
- package/dist/claim.d.ts +5 -3
- package/dist/claim.d.ts.map +1 -1
- package/dist/claim.js +10 -10
- package/dist/claim.js.map +1 -1
- package/dist/client.d.ts +26 -41
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +41 -986
- package/dist/client.js.map +1 -1
- package/dist/clientConfig.d.ts +11 -0
- package/dist/clientConfig.d.ts.map +1 -0
- package/dist/clientConfig.js +3 -0
- package/dist/clientConfig.js.map +1 -0
- package/dist/decode.d.ts.map +1 -1
- package/dist/decode.js +2 -29
- package/dist/decode.js.map +1 -1
- package/dist/fees.d.ts.map +1 -1
- package/dist/fees.js +2 -12
- package/dist/fees.js.map +1 -1
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +4 -15
- package/dist/history.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +21 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +50 -69
- package/dist/layout.js.map +1 -1
- package/dist/liquidity/common.js +2 -2
- package/dist/liquidity/common.js.map +1 -1
- package/dist/lp.d.ts +9 -13
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +67 -55
- package/dist/lp.js.map +1 -1
- package/dist/portfolio.d.ts.map +1 -1
- package/dist/portfolio.js +3 -8
- package/dist/portfolio.js.map +1 -1
- package/dist/reclaim.d.ts +4 -2
- package/dist/reclaim.d.ts.map +1 -1
- package/dist/reclaim.js +16 -16
- package/dist/reclaim.js.map +1 -1
- package/dist/risk.js +2 -2
- package/dist/risk.js.map +1 -1
- package/dist/rpc.d.ts +30 -7
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +400 -109
- package/dist/rpc.js.map +1 -1
- package/dist/simulate.d.ts.map +1 -1
- package/dist/simulate.js +3 -26
- package/dist/simulate.js.map +1 -1
- package/dist/source.d.ts +8 -4
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +11 -4
- package/dist/source.js.map +1 -1
- package/dist/subpaths/approvals.d.ts +3 -3
- package/dist/subpaths/approvals.d.ts.map +1 -1
- package/dist/subpaths/approvals.js.map +1 -1
- package/dist/subpaths/claim.d.ts +3 -3
- package/dist/subpaths/claim.d.ts.map +1 -1
- package/dist/subpaths/claim.js.map +1 -1
- package/dist/subpaths/reclaim.d.ts +3 -3
- package/dist/subpaths/reclaim.d.ts.map +1 -1
- package/dist/subpaths/reclaim.js.map +1 -1
- package/dist/swap/index.js +3 -3
- package/dist/swap/index.js.map +1 -1
- package/dist/tokenDetails.d.ts +12 -0
- package/dist/tokenDetails.d.ts.map +1 -0
- package/dist/tokenDetails.js +196 -0
- package/dist/tokenDetails.js.map +1 -0
- package/dist/tokenMeta.d.ts +63 -0
- package/dist/tokenMeta.d.ts.map +1 -0
- package/dist/tokenMeta.js +226 -0
- package/dist/tokenMeta.js.map +1 -0
- package/dist/tradeCheck.js +3 -3
- package/dist/tradeCheck.js.map +1 -1
- package/dist/util.d.ts +22 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +107 -0
- package/dist/util.js.map +1 -0
- package/dist/write/chain.d.ts +51 -0
- package/dist/write/chain.d.ts.map +1 -0
- package/dist/write/chain.js +184 -0
- package/dist/write/chain.js.map +1 -0
- package/dist/write/collect.js +2 -2
- package/dist/write/collect.js.map +1 -1
- package/dist/write/errors.d.ts +10 -0
- package/dist/write/errors.d.ts.map +1 -0
- package/dist/write/errors.js +77 -0
- package/dist/write/errors.js.map +1 -0
- package/dist/write/pack.d.ts +28 -0
- package/dist/write/pack.d.ts.map +1 -0
- package/dist/write/pack.js +125 -0
- package/dist/write/pack.js.map +1 -0
- package/dist/write/send.d.ts +23 -9
- package/dist/write/send.d.ts.map +1 -1
- package/dist/write/send.js +53 -351
- package/dist/write/send.js.map +1 -1
- package/dist/write/tracker.d.ts +24 -0
- package/dist/write/tracker.d.ts.map +1 -0
- package/dist/write/tracker.js +129 -0
- package/dist/write/tracker.js.map +1 -0
- package/package.json +1 -1
- package/src/accountReader.ts +187 -0
- package/src/address.ts +41 -6
- package/src/approvals.ts +36 -44
- package/src/balances.ts +678 -0
- package/src/batchPlan.ts +6 -26
- package/src/bytes.ts +89 -0
- package/src/claim.ts +15 -11
- package/src/client.ts +59 -1239
- package/src/clientConfig.ts +12 -0
- package/src/decode.ts +4 -30
- package/src/fees.ts +2 -9
- package/src/history.ts +4 -15
- package/src/index.ts +13 -1
- package/src/layout.ts +63 -82
- package/src/liquidity/common.ts +2 -2
- package/src/lp.ts +86 -60
- package/src/portfolio.ts +8 -11
- package/src/reclaim.ts +20 -17
- package/src/risk.ts +2 -2
- package/src/rpc.ts +472 -116
- package/src/simulate.ts +3 -21
- package/src/source.ts +14 -5
- package/src/subpaths/approvals.ts +3 -2
- package/src/subpaths/claim.ts +3 -2
- package/src/subpaths/reclaim.ts +3 -2
- package/src/swap/index.ts +3 -3
- package/src/tokenDetails.ts +240 -0
- package/src/tokenMeta.ts +270 -0
- package/src/tradeCheck.ts +3 -3
- package/src/util.ts +110 -0
- package/src/write/chain.ts +218 -0
- package/src/write/collect.ts +2 -2
- package/src/write/errors.ts +82 -0
- package/src/write/pack.ts +142 -0
- package/src/write/send.ts +74 -407
- package/src/write/tracker.ts +152 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Cluster } from './constants.js'
|
|
2
|
+
import type { SourceOptions } from './source.js'
|
|
3
|
+
|
|
4
|
+
/** SolanaClient 的节点配置(网络、节点选项、期望的创世哈希),按客户端登记:写操作的 sendRpc 按同样的配置解析、校验网络 */
|
|
5
|
+
export interface ClientSourceConfig {
|
|
6
|
+
cluster: Cluster | undefined
|
|
7
|
+
options: SourceOptions
|
|
8
|
+
genesis: () => Promise<string>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 单独成模块:/write 只需查表,不用静态带上 source.ts(sendRpc 解析时才按需加载) */
|
|
12
|
+
export const clientSourceConfigs = new WeakMap<object, ClientSourceConfig>()
|
package/src/decode.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 指令解码(/simulate 和 /history 共用):System、SPL Token / Token-2022、关联代币账户、计算预算、Memo、升级程序加载器,
|
|
3
3
|
* 以及 programs.ts 里认得的 DEX / 聚合器(协议 + 指令名 + 大类)。其他程序只给程序名(认得时)和大类 contract / unknown
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { Bytes } from './bytes.js'
|
|
6
6
|
import { ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from './constants.js'
|
|
7
7
|
import { BPF_LOADER_UPGRADEABLE, COMPUTE_BUDGET, MEMO_V1, MEMO_V2, PROGRAMS, knownMethod, type IxType } from './programs.js'
|
|
8
8
|
|
|
@@ -28,32 +28,6 @@ export const AUTHORITY_TYPES = [
|
|
|
28
28
|
|
|
29
29
|
const short = (a: string | undefined) => (a ? `${a.slice(0, 4)}…${a.slice(-4)}` : '?')
|
|
30
30
|
|
|
31
|
-
class Data {
|
|
32
|
-
readonly view: DataView
|
|
33
|
-
offset = 0
|
|
34
|
-
constructor(readonly bytes: Uint8Array) {
|
|
35
|
-
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
36
|
-
}
|
|
37
|
-
u8() {
|
|
38
|
-
return this.view.getUint8(this.offset++)
|
|
39
|
-
}
|
|
40
|
-
u32() {
|
|
41
|
-
const v = this.view.getUint32(this.offset, true)
|
|
42
|
-
this.offset += 4
|
|
43
|
-
return v
|
|
44
|
-
}
|
|
45
|
-
u64() {
|
|
46
|
-
const v = this.view.getBigUint64(this.offset, true)
|
|
47
|
-
this.offset += 8
|
|
48
|
-
return v
|
|
49
|
-
}
|
|
50
|
-
key() {
|
|
51
|
-
const v = encodeAddress(this.bytes.slice(this.offset, this.offset + 32))
|
|
52
|
-
this.offset += 32
|
|
53
|
-
return v
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
31
|
/**
|
|
58
32
|
* 解码一条指令。accounts 是指令用到的账户地址(已按指令里的下标解析,包含地址查找表里的),data 是指令数据。
|
|
59
33
|
* 数据格式不对时不抛错,按“不认识的指令”返回
|
|
@@ -78,7 +52,7 @@ export function decodeInstruction(index: number, programId: string, accounts: re
|
|
|
78
52
|
type Partial_ = Pick<DecodedInstruction, 'type' | 'summary'> & Partial<Pick<DecodedInstruction, 'method' | 'args' | 'protocol'>>
|
|
79
53
|
|
|
80
54
|
function decodeKnown(programId: string, a: readonly string[], data: Uint8Array): Partial_ | null {
|
|
81
|
-
const d = new
|
|
55
|
+
const d = new Bytes(data)
|
|
82
56
|
switch (programId) {
|
|
83
57
|
case SYSTEM_PROGRAM_ID:
|
|
84
58
|
return decodeSystem(d, a)
|
|
@@ -108,7 +82,7 @@ function decodeKnown(programId: string, a: readonly string[], data: Uint8Array):
|
|
|
108
82
|
}
|
|
109
83
|
}
|
|
110
84
|
|
|
111
|
-
function decodeSystem(d:
|
|
85
|
+
function decodeSystem(d: Bytes, a: readonly string[]): Partial_ {
|
|
112
86
|
const tag = d.u32()
|
|
113
87
|
switch (tag) {
|
|
114
88
|
case 0: {
|
|
@@ -139,7 +113,7 @@ function decodeSystem(d: Data, a: readonly string[]): Partial_ {
|
|
|
139
113
|
}
|
|
140
114
|
}
|
|
141
115
|
|
|
142
|
-
function decodeToken(d:
|
|
116
|
+
function decodeToken(d: Bytes, a: readonly string[]): Partial_ {
|
|
143
117
|
const tag = d.u8()
|
|
144
118
|
const amountArgs = (extra: Record<string, unknown>, amount: bigint) => ({ ...extra, amount: amount.toString() })
|
|
145
119
|
switch (tag) {
|
package/src/fees.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { base64 } from '@scure/base'
|
|
|
7
7
|
import { encodeAddress } from './address.js'
|
|
8
8
|
import type { RequestOptions, SolanaClient } from './client.js'
|
|
9
9
|
import { SOL_DECIMALS, SYSTEM_PROGRAM_ID } from './constants.js'
|
|
10
|
-
import { formatUnits } from './layout.js'
|
|
10
|
+
import { computeBudgetOf, formatUnits } from './layout.js'
|
|
11
11
|
import type { InstructionInput } from './write/transaction.js'
|
|
12
12
|
|
|
13
13
|
export type FeeLevel = 'slow' | 'normal' | 'fast'
|
|
@@ -156,14 +156,7 @@ export async function estimateCost(client: SolanaClient, input: CostInput, optio
|
|
|
156
156
|
raw = base64.encode(bytes)
|
|
157
157
|
signatures = message.signatures
|
|
158
158
|
writable = [...message.writable, ...(await lookupWritable(client, message.lookups, signal))]
|
|
159
|
-
|
|
160
|
-
let price = 0n
|
|
161
|
-
for (const ix of message.instructions) {
|
|
162
|
-
if (ix.programId !== COMPUTE_BUDGET_PROGRAM_ID) continue
|
|
163
|
-
const view = new DataView(ix.data.buffer, ix.data.byteOffset, ix.data.byteLength)
|
|
164
|
-
if (ix.data[0] === 2 && ix.data.length >= 5) limit = view.getUint32(1, true)
|
|
165
|
-
if (ix.data[0] === 3 && ix.data.length >= 9) price = view.getBigUint64(1, true)
|
|
166
|
-
}
|
|
159
|
+
const { limit, price } = computeBudgetOf(message.instructions)
|
|
167
160
|
const others = message.instructions.filter((ix) => ix.programId !== COMPUTE_BUDGET_PROGRAM_ID).length
|
|
168
161
|
budget = { limit: Math.min(MAX_COMPUTE_UNITS, limit ?? DEFAULT_UNITS_PER_INSTRUCTION * others), price }
|
|
169
162
|
} else {
|
package/src/history.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { base58 } from '@scure/base'
|
|
7
7
|
|
|
8
|
-
import { isAddress } from './address.js'
|
|
8
|
+
import { InvalidAddressError, isAddress } from './address.js'
|
|
9
9
|
import type { RequestOptions, SolanaClient } from './client.js'
|
|
10
10
|
import { NATIVE_MINT, SOL_DECIMALS, SYSTEM_PROGRAM_ID } from './constants.js'
|
|
11
11
|
import { decodeInstruction, type DecodedInstruction } from './decode.js'
|
|
12
12
|
import { classify, describe, type AssetDelta, type Classified } from './describe.js'
|
|
13
13
|
import { formatUnits } from './layout.js'
|
|
14
|
+
import { mapPool } from './util.js'
|
|
14
15
|
|
|
15
16
|
export type ActivityType = 'transfer' | 'swap' | 'approve' | 'liquidity' | 'mint' | 'contract' | 'other'
|
|
16
17
|
|
|
@@ -95,7 +96,7 @@ const short = (a: string) => `${a.slice(0, 4)}…${a.slice(-4)}`
|
|
|
95
96
|
* ```
|
|
96
97
|
*/
|
|
97
98
|
export async function history(client: SolanaClient, address: string, options: HistoryOptions = {}): Promise<HistoryPage> {
|
|
98
|
-
if (!isAddress(address)) throw new
|
|
99
|
+
if (!isAddress(address)) throw new InvalidAddressError(`Invalid address: ${address}`, address)
|
|
99
100
|
const limit = options.limit ?? 20
|
|
100
101
|
if (!Number.isInteger(limit) || limit < 1 || limit > MAX_HISTORY_LIMIT) throw new RangeError(`limit must be an integer between 1 and ${MAX_HISTORY_LIMIT}`)
|
|
101
102
|
const sources = options.source === undefined ? [rpcHistory()] : Array.isArray(options.source) ? options.source : [options.source as HistorySource]
|
|
@@ -185,18 +186,6 @@ interface SignatureInfo {
|
|
|
185
186
|
blockTime: number | null
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
async function pool<T, R>(items: readonly T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {
|
|
189
|
-
const results = new Array<R>(items.length)
|
|
190
|
-
let next = 0
|
|
191
|
-
const worker = async () => {
|
|
192
|
-
while (next < items.length) {
|
|
193
|
-
const i = next++
|
|
194
|
-
results[i] = await fn(items[i] as T)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
await Promise.all(Array.from({ length: Math.min(Math.max(1, limit), items.length) }, worker))
|
|
198
|
-
return results
|
|
199
|
-
}
|
|
200
189
|
|
|
201
190
|
/**
|
|
202
191
|
* 默认数据源:getSignaturesForAddress(用 before 翻页)+ getTransaction(逐笔,json 编码、支持 v0)。
|
|
@@ -228,7 +217,7 @@ export function rpcHistory(options: RpcHistoryOptions = {}): HistorySource {
|
|
|
228
217
|
let i = 0
|
|
229
218
|
while (i < rows.length && items.length < limit && scanned < maxScan) {
|
|
230
219
|
const chunk = rows.slice(i, i + Math.max(1, Math.min(limit - items.length, maxScan - scanned)))
|
|
231
|
-
const txs = await
|
|
220
|
+
const txs = await mapPool(chunk, concurrency, (row) => client.request<RawTransaction | null>('getTransaction', [row.signature, { encoding: 'json', maxSupportedTransactionVersion: MAX_TX_VERSION, commitment }], { signal }))
|
|
232
221
|
for (const [j, tx] of txs.entries()) {
|
|
233
222
|
i++
|
|
234
223
|
scanned++
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export {
|
|
|
6
6
|
type AccountsOptions,
|
|
7
7
|
type BalanceQuery,
|
|
8
8
|
type BalancesOptions,
|
|
9
|
+
type BalancesQueryOptions,
|
|
9
10
|
type ClientConfig,
|
|
10
11
|
type OwnerTokensOptions,
|
|
11
12
|
type PersistTokenMetaOptions,
|
|
@@ -64,7 +65,17 @@ export {
|
|
|
64
65
|
clusterOfGenesis,
|
|
65
66
|
type Cluster,
|
|
66
67
|
} from './constants.js'
|
|
67
|
-
export {
|
|
68
|
+
export {
|
|
69
|
+
InvalidAddressError,
|
|
70
|
+
createProgramAddress,
|
|
71
|
+
decodeAddress,
|
|
72
|
+
encodeAddress,
|
|
73
|
+
findProgramAddress,
|
|
74
|
+
getAssociatedTokenAddress,
|
|
75
|
+
getMetadataAddress,
|
|
76
|
+
isAddress,
|
|
77
|
+
isOnCurve,
|
|
78
|
+
} from './address.js'
|
|
68
79
|
export {
|
|
69
80
|
formatAmount,
|
|
70
81
|
formatUnits,
|
|
@@ -94,6 +105,7 @@ export {
|
|
|
94
105
|
type FallbackOptions,
|
|
95
106
|
type HttpRpcOptions,
|
|
96
107
|
type RequestEvent,
|
|
108
|
+
type RpcRequestOptions,
|
|
97
109
|
type RpcTransport,
|
|
98
110
|
} from './rpc.js'
|
|
99
111
|
export { NetworkCheckedRpc, NetworkMismatchError, type RpcSource, type SourceOptions } from './source.js'
|
package/src/layout.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Bytes } from './bytes.js'
|
|
2
2
|
|
|
3
3
|
/** getMultipleAccounts 解析后的账户 */
|
|
4
4
|
export interface AccountInfo {
|
|
@@ -10,80 +10,6 @@ export interface AccountInfo {
|
|
|
10
10
|
executable: boolean
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
class Reader {
|
|
14
|
-
#offset: number
|
|
15
|
-
readonly #view: DataView
|
|
16
|
-
|
|
17
|
-
constructor(
|
|
18
|
-
readonly bytes: Uint8Array,
|
|
19
|
-
offset = 0,
|
|
20
|
-
) {
|
|
21
|
-
this.#offset = offset
|
|
22
|
-
this.#view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
get offset(): number {
|
|
26
|
-
return this.#offset
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
#need(n: number): void {
|
|
30
|
-
if (this.#offset + n > this.bytes.length) {
|
|
31
|
-
throw new RangeError('Account data is shorter than expected')
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
skip(n: number): void {
|
|
36
|
-
this.#need(n)
|
|
37
|
-
this.#offset += n
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
u8(): number {
|
|
41
|
-
this.#need(1)
|
|
42
|
-
return this.bytes[this.#offset++] as number
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
u16(): number {
|
|
46
|
-
this.#need(2)
|
|
47
|
-
const v = this.#view.getUint16(this.#offset, true)
|
|
48
|
-
this.#offset += 2
|
|
49
|
-
return v
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
u32(): number {
|
|
53
|
-
this.#need(4)
|
|
54
|
-
const v = this.#view.getUint32(this.#offset, true)
|
|
55
|
-
this.#offset += 4
|
|
56
|
-
return v
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
u64(): bigint {
|
|
60
|
-
this.#need(8)
|
|
61
|
-
const v = this.#view.getBigUint64(this.#offset, true)
|
|
62
|
-
this.#offset += 8
|
|
63
|
-
return v
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
pubkey(): string {
|
|
67
|
-
this.#need(32)
|
|
68
|
-
const v = encodeAddress(this.bytes.slice(this.#offset, this.#offset + 32))
|
|
69
|
-
this.#offset += 32
|
|
70
|
-
return v
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** borsh string:u32 长度 + UTF-8;去掉 Metaplex 用来补齐长度的 \0 */
|
|
74
|
-
string(): string {
|
|
75
|
-
const len = this.u32()
|
|
76
|
-
this.#need(len)
|
|
77
|
-
const v = new TextDecoder().decode(this.bytes.slice(this.#offset, this.#offset + len))
|
|
78
|
-
this.#offset += len
|
|
79
|
-
return v.replace(/\0+$/, '').trim()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
option<T>(read: () => T): T | null {
|
|
83
|
-
return this.u8() === 1 ? read() : null
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
13
|
export interface MintInfo {
|
|
88
14
|
mintAuthority: string | null
|
|
89
15
|
supply: bigint
|
|
@@ -116,7 +42,7 @@ export function parseMint(data: Uint8Array): MintInfo {
|
|
|
116
42
|
if (data.length !== MINT_SIZE && !(data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_MINT)) {
|
|
117
43
|
throw new RangeError('Not a mint account')
|
|
118
44
|
}
|
|
119
|
-
const r = new
|
|
45
|
+
const r = new Bytes(data)
|
|
120
46
|
const hasMintAuthority = r.u32() === 1
|
|
121
47
|
const mintAuthority = r.pubkey()
|
|
122
48
|
const supply = r.u64()
|
|
@@ -140,7 +66,7 @@ export function findExtension(data: Uint8Array, extensionType: number): Uint8Arr
|
|
|
140
66
|
return null
|
|
141
67
|
}
|
|
142
68
|
try {
|
|
143
|
-
const r = new
|
|
69
|
+
const r = new Bytes(data, EXTENSIONS_OFFSET)
|
|
144
70
|
while (r.offset + 4 <= data.length) {
|
|
145
71
|
const type = r.u16()
|
|
146
72
|
const length = r.u16()
|
|
@@ -167,7 +93,7 @@ function parseTokenMetadataExtension(data: Uint8Array): TokenMetadata | null {
|
|
|
167
93
|
return null
|
|
168
94
|
}
|
|
169
95
|
try {
|
|
170
|
-
const m = new
|
|
96
|
+
const m = new Bytes(ext)
|
|
171
97
|
m.pubkey() // updateAuthority
|
|
172
98
|
m.pubkey() // mint
|
|
173
99
|
return { name: m.string(), symbol: m.string(), uri: m.string() }
|
|
@@ -212,7 +138,7 @@ export function parseTransferFeeConfig(data: Uint8Array): TransferFeeConfigInfo
|
|
|
212
138
|
return null
|
|
213
139
|
}
|
|
214
140
|
try {
|
|
215
|
-
const r = new
|
|
141
|
+
const r = new Bytes(ext)
|
|
216
142
|
// OptionalNonZeroPubkey:全 0 表示没有
|
|
217
143
|
const configAuthority = r.pubkey()
|
|
218
144
|
const withdrawAuthority = r.pubkey()
|
|
@@ -238,15 +164,70 @@ export interface TokenAccountInfo {
|
|
|
238
164
|
amount: bigint
|
|
239
165
|
}
|
|
240
166
|
|
|
167
|
+
function isTokenAccountData(data: Uint8Array): boolean {
|
|
168
|
+
return data.length === ACCOUNT_SIZE || (data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_ACCOUNT)
|
|
169
|
+
}
|
|
170
|
+
|
|
241
171
|
/** 解析代币账户(SPL Token / Token-2022 前 72 字节相同) */
|
|
242
172
|
export function parseTokenAccount(data: Uint8Array): TokenAccountInfo {
|
|
243
|
-
if (
|
|
173
|
+
if (!isTokenAccountData(data)) {
|
|
244
174
|
throw new RangeError('Not a token account')
|
|
245
175
|
}
|
|
246
|
-
const r = new
|
|
176
|
+
const r = new Bytes(data)
|
|
247
177
|
return { mint: r.pubkey(), owner: r.pubkey(), amount: r.u64() }
|
|
248
178
|
}
|
|
249
179
|
|
|
180
|
+
/** 代币账户的完整基础布局(前 165 字节,SPL Token / Token-2022 相同) */
|
|
181
|
+
export interface TokenAccountState extends TokenAccountInfo {
|
|
182
|
+
delegate: string | null
|
|
183
|
+
/** 0 未初始化、1 正常、2 冻结 */
|
|
184
|
+
state: number
|
|
185
|
+
frozen: boolean
|
|
186
|
+
/** Wrapped SOL 账户(is_native 有值) */
|
|
187
|
+
native: boolean
|
|
188
|
+
delegatedAmount: bigint
|
|
189
|
+
closeAuthority: string | null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 解析代币账户的完整基础布局;不是代币账户或未初始化时为 null(包内使用:/approvals、/simulate) */
|
|
193
|
+
export function parseTokenAccountState(data: Uint8Array): TokenAccountState | null {
|
|
194
|
+
if (!isTokenAccountData(data)) {
|
|
195
|
+
return null
|
|
196
|
+
}
|
|
197
|
+
const b = new Bytes(data)
|
|
198
|
+
const state = b.u8(108)
|
|
199
|
+
if (state === 0) {
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
// COption:u32 标记(1 有、0 没有)+ 值
|
|
203
|
+
return {
|
|
204
|
+
mint: b.key(0),
|
|
205
|
+
owner: b.key(32),
|
|
206
|
+
amount: b.u64(64),
|
|
207
|
+
delegate: b.u32(72) === 1 ? b.key(76) : null,
|
|
208
|
+
state,
|
|
209
|
+
frozen: state === 2,
|
|
210
|
+
native: b.u32(109) === 1,
|
|
211
|
+
delegatedAmount: b.u64(121),
|
|
212
|
+
closeAuthority: b.u32(129) === 1 ? b.key(133) : null,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 计算预算指令里的计算单元上限(SetComputeUnitLimit)和单价(SetComputeUnitPrice,micro-lamports);没有时 limit 为 null、price 为 0 */
|
|
217
|
+
export function computeBudgetOf(instructions: Iterable<{ programId: string; data: Uint8Array }>): { limit: number | null; price: bigint } {
|
|
218
|
+
let limit: number | null = null
|
|
219
|
+
let price = 0n
|
|
220
|
+
for (const ix of instructions) {
|
|
221
|
+
if (ix.programId !== COMPUTE_BUDGET_PROGRAM) continue
|
|
222
|
+
const b = new Bytes(ix.data)
|
|
223
|
+
if (ix.data[0] === 2 && ix.data.length >= 5) limit = b.u32(1)
|
|
224
|
+
if (ix.data[0] === 3 && ix.data.length >= 9) price = b.u64(1)
|
|
225
|
+
}
|
|
226
|
+
return { limit, price }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111'
|
|
230
|
+
|
|
250
231
|
export interface MetaplexMetadata extends TokenMetadata {
|
|
251
232
|
updateAuthority: string
|
|
252
233
|
mint: string
|
|
@@ -261,7 +242,7 @@ export interface MetaplexMetadata extends TokenMetadata {
|
|
|
261
242
|
|
|
262
243
|
/** 解析 Metaplex Token Metadata 账户;老版本账户缺少后面的字段时,缺的部分为默认值 */
|
|
263
244
|
export function parseMetaplexMetadata(data: Uint8Array): MetaplexMetadata {
|
|
264
|
-
const r = new
|
|
245
|
+
const r = new Bytes(data)
|
|
265
246
|
if (r.u8() !== 4) {
|
|
266
247
|
throw new RangeError('Not a Metaplex metadata account') // Key::MetadataV1 = 4
|
|
267
248
|
}
|
package/src/liquidity/common.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha2.js'
|
|
2
2
|
|
|
3
|
-
import { encodeAddress, getAssociatedTokenAddress, isAddress } from '../address.js'
|
|
3
|
+
import { encodeAddress, getAssociatedTokenAddress, InvalidAddressError, isAddress } from '../address.js'
|
|
4
4
|
import type { SolanaClient } from '../client.js'
|
|
5
5
|
import { NATIVE_MINT, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '../constants.js'
|
|
6
6
|
import { formatUnits, parseMint, parseTokenAccount, parseTransferFeeConfig, parseUnits, type AccountInfo } from '../layout.js'
|
|
@@ -25,7 +25,7 @@ export interface Ctx {
|
|
|
25
25
|
export function createCtx(client: SolanaClient, owner: string, options: SolanaLiquidityOptions): Ctx {
|
|
26
26
|
for (const address of [owner, options.recipient ?? owner]) {
|
|
27
27
|
if (!isAddress(address)) {
|
|
28
|
-
throw new
|
|
28
|
+
throw new InvalidAddressError(`Invalid address: ${address}`, address)
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
const slippage = options.slippage ?? 0.005
|
package/src/lp.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { sha256 } from '@noble/hashes/sha2.js'
|
|
2
2
|
import { base64 } from '@scure/base'
|
|
3
3
|
|
|
4
|
-
import { decodeAddress,
|
|
4
|
+
import { decodeAddress, findProgramAddress, InvalidAddressError, isAddress } from './address.js'
|
|
5
5
|
import type { RequestOptions, SolanaClient } from './client.js'
|
|
6
6
|
import { formatUnits, parseMint, parseTokenAccount, type AccountInfo } from './layout.js'
|
|
7
7
|
import { AllNodesFailedError, HttpError, RpcError } from './rpc.js'
|
|
8
|
+
import { boundedSet, mapPool } from './util.js'
|
|
9
|
+
import { Bytes } from './bytes.js'
|
|
8
10
|
import {
|
|
9
11
|
getAmountsForLiquidity,
|
|
10
12
|
getFeeGrowthInside,
|
|
@@ -168,34 +170,8 @@ export interface LpPositionsOptions extends RequestOptions {
|
|
|
168
170
|
// 账户布局(各程序的 Anchor / 原生账户,偏移量含 8 字节判别符)
|
|
169
171
|
// ---------------------------------------------------------------------------
|
|
170
172
|
|
|
171
|
-
|
|
172
|
-
export
|
|
173
|
-
readonly #view: DataView
|
|
174
|
-
constructor(readonly data: Uint8Array) {
|
|
175
|
-
this.#view = new DataView(data.buffer, data.byteOffset, data.byteLength)
|
|
176
|
-
}
|
|
177
|
-
u8(o: number): number {
|
|
178
|
-
return this.#view.getUint8(o)
|
|
179
|
-
}
|
|
180
|
-
u16(o: number): number {
|
|
181
|
-
return this.#view.getUint16(o, true)
|
|
182
|
-
}
|
|
183
|
-
u32(o: number): number {
|
|
184
|
-
return this.#view.getUint32(o, true)
|
|
185
|
-
}
|
|
186
|
-
i32(o: number): number {
|
|
187
|
-
return this.#view.getInt32(o, true)
|
|
188
|
-
}
|
|
189
|
-
u64(o: number): bigint {
|
|
190
|
-
return this.#view.getBigUint64(o, true)
|
|
191
|
-
}
|
|
192
|
-
u128(o: number): bigint {
|
|
193
|
-
return (this.#view.getBigUint64(o + 8, true) << 64n) | this.#view.getBigUint64(o, true)
|
|
194
|
-
}
|
|
195
|
-
key(o: number): string {
|
|
196
|
-
return encodeAddress(this.data.subarray(o, o + 32))
|
|
197
|
-
}
|
|
198
|
-
}
|
|
173
|
+
// 小端读取账户数据:公共的 Bytes(添加 / 移除流动性也从这里引用)
|
|
174
|
+
export { Bytes }
|
|
199
175
|
|
|
200
176
|
const ZERO_ADDRESS = '11111111111111111111111111111111'
|
|
201
177
|
const RAYDIUM_POSITION_SIZE = 281
|
|
@@ -544,7 +520,7 @@ interface RawAmm {
|
|
|
544
520
|
*/
|
|
545
521
|
export async function lpPositions(client: SolanaClient, owner: string, options: LpPositionsOptions = {}): Promise<SolanaLpPosition[]> {
|
|
546
522
|
if (!isAddress(owner)) {
|
|
547
|
-
throw new
|
|
523
|
+
throw new InvalidAddressError(`Invalid owner address: ${owner}`, owner)
|
|
548
524
|
}
|
|
549
525
|
const protocols = new Set<SolanaLpProtocol>(options.protocols ?? (Object.keys(SOLANA_LP_PROTOCOLS) as SolanaLpProtocol[]))
|
|
550
526
|
for (const p of protocols) {
|
|
@@ -554,7 +530,7 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
554
530
|
}
|
|
555
531
|
for (const address of [...(options.mints ?? []), ...(options.pools ?? [])]) {
|
|
556
532
|
if (!isAddress(address)) {
|
|
557
|
-
throw new
|
|
533
|
+
throw new InvalidAddressError(`Invalid address: ${address}`, address)
|
|
558
534
|
}
|
|
559
535
|
}
|
|
560
536
|
const { signal } = options
|
|
@@ -621,13 +597,30 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
621
597
|
}
|
|
622
598
|
})
|
|
623
599
|
|
|
624
|
-
// 3. 池子:集中流动性的池子账户;LP 代币按 LP mint
|
|
600
|
+
// 3. 池子:集中流动性的池子账户;LP 代币按 LP mint 找池子(之前找到过的直接读池子账户,和集中流动性的池子一起读)
|
|
625
601
|
const poolAddresses = [...new Set(positions.map((p) => p.pool))]
|
|
602
|
+
const cached = lookups.flatMap((l) => {
|
|
603
|
+
const address = ammPoolByLpMint.get(lpPoolKey(l.protocol, l.lpMint))
|
|
604
|
+
return address ? [{ ...l, address }] : []
|
|
605
|
+
})
|
|
606
|
+
const uncached = lookups.filter((l) => !ammPoolByLpMint.has(lpPoolKey(l.protocol, l.lpMint)))
|
|
626
607
|
const [poolAccounts, found] = await Promise.all([
|
|
627
|
-
client.accounts(poolAddresses, { signal }),
|
|
628
|
-
|
|
608
|
+
client.accounts([...poolAddresses, ...cached.map((c) => c.address)], { signal }),
|
|
609
|
+
findAmmPools(client, uncached, signal),
|
|
629
610
|
])
|
|
630
|
-
|
|
611
|
+
// 缓存的池子核对一下(地址不对、账户读不到时重新找)
|
|
612
|
+
const stale: typeof lookups = []
|
|
613
|
+
cached.forEach((c, i) => {
|
|
614
|
+
const account = poolAccounts[poolAddresses.length + i]
|
|
615
|
+
const pool = account ? parseAmmPool(account) : null
|
|
616
|
+
if (pool && pool.protocol === c.protocol && pool.lpMint === c.lpMint) {
|
|
617
|
+
poolByLp.set(pool.lpMint, pool)
|
|
618
|
+
} else {
|
|
619
|
+
ammPoolByLpMint.delete(lpPoolKey(c.protocol, c.lpMint))
|
|
620
|
+
stale.push({ protocol: c.protocol, lpMint: c.lpMint })
|
|
621
|
+
}
|
|
622
|
+
})
|
|
623
|
+
for (const pool of [...found, ...(stale.length ? await findAmmPools(client, stale, signal) : [])]) {
|
|
631
624
|
if (pool) {
|
|
632
625
|
poolByLp.set(pool.lpMint, pool)
|
|
633
626
|
}
|
|
@@ -751,6 +744,19 @@ export async function lpPositions(client: SolanaClient, owner: string, options:
|
|
|
751
744
|
return finalize(client, raw, options)
|
|
752
745
|
}
|
|
753
746
|
|
|
747
|
+
/**
|
|
748
|
+
* 方法不可用(见 methodUnavailable)时把错误换成说明怎么绕过的 Error(cause 为原错误,hint 拿到原错误的 message);
|
|
749
|
+
* 取消、限频、超时等原样抛出(保留错误类型)。/lp、/approvals、资产总值共用
|
|
750
|
+
*/
|
|
751
|
+
export function withMethodHint<T>(promise: Promise<T>, signal: AbortSignal | undefined, hint: (detail: string) => string): Promise<T> {
|
|
752
|
+
return promise.catch((err: unknown) => {
|
|
753
|
+
if (signal?.aborted || !methodUnavailable(err)) {
|
|
754
|
+
throw err
|
|
755
|
+
}
|
|
756
|
+
throw new Error(hint(err instanceof Error ? err.message : String(err)), { cause: err })
|
|
757
|
+
})
|
|
758
|
+
}
|
|
759
|
+
|
|
754
760
|
/**
|
|
755
761
|
* 节点不提供这个方法(方法不存在、被拦截、需要付费 / Token、403 等):这时报错里说明怎么绕过。
|
|
756
762
|
* 多节点时所有节点都是这个原因才算;限频、超时、网络不对等原样抛出(资产总值 portfolio 也用)
|
|
@@ -779,43 +785,63 @@ async function listHoldings(client: SolanaClient, owner: string, mints: readonly
|
|
|
779
785
|
const balances = list.length ? await client.balances(owner, list, { signal }) : []
|
|
780
786
|
return balances.filter((b) => b.success && !b.native && b.balance !== '0').map((b) => ({ mint: b.token, amount: BigInt(b.balance), decimals: b.decimals }))
|
|
781
787
|
}
|
|
782
|
-
const tokens = await
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
{ cause: err },
|
|
790
|
-
)
|
|
791
|
-
})
|
|
788
|
+
const tokens = await withMethodHint(
|
|
789
|
+
client.ownerTokens(owner, { signal, metadata: false, includeNative: false, includeNfts: true }),
|
|
790
|
+
signal,
|
|
791
|
+
(detail) =>
|
|
792
|
+
`Scanning the token accounts of ${owner} failed (${detail}). ` +
|
|
793
|
+
'Scanning needs getTokenAccountsByOwner (most free public nodes do not support it): pass the position NFT / LP mints in `mints`, or use a node that supports it',
|
|
794
|
+
)
|
|
792
795
|
return tokens.map((t) => ({ mint: t.token, amount: BigInt(t.balance), decimals: t.decimals }))
|
|
793
796
|
}
|
|
794
797
|
|
|
795
|
-
/**
|
|
798
|
+
/**
|
|
799
|
+
* LP mint → 池子地址(AMM v4 / CPMM):池子地址不会变,找到后永久缓存(条目数有上限,超出淘汰最早写入的),
|
|
800
|
+
* 之后不再 getProgramAccounts,用时读池子账户核对
|
|
801
|
+
*/
|
|
802
|
+
const ammPoolByLpMint = new Map<string, string>()
|
|
803
|
+
const MAX_CACHED_LP_POOLS = 10_000
|
|
804
|
+
/** 同时进行的 getProgramAccounts 数(每个都要节点扫描整个程序的账户,很重) */
|
|
805
|
+
const MAX_PARALLEL_POOL_LOOKUPS = 4
|
|
806
|
+
|
|
807
|
+
function lpPoolKey(protocol: 'raydium-amm' | 'raydium-cpmm', lpMint: string): string {
|
|
808
|
+
return `${protocol}:${lpMint}`
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** 测试用:清空 LP mint → 池子地址的缓存 */
|
|
812
|
+
export function resetLpPoolCache(): void {
|
|
813
|
+
ammPoolByLpMint.clear()
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** 按 LP mint 找池子,限制并发;结果与 lookups 一一对应 */
|
|
817
|
+
function findAmmPools(
|
|
818
|
+
client: SolanaClient,
|
|
819
|
+
lookups: ReadonlyArray<{ protocol: 'raydium-amm' | 'raydium-cpmm'; lpMint: string }>,
|
|
820
|
+
signal: AbortSignal | undefined,
|
|
821
|
+
): Promise<Array<AmmPoolState | null>> {
|
|
822
|
+
return mapPool(lookups, MAX_PARALLEL_POOL_LOOKUPS, (l) => findAmmPool(client, l.protocol, l.lpMint, signal))
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** 按 LP mint 找 AMM v4 / CPMM 的池子(getProgramAccounts + memcmp),找不到时为 null;找到的池子地址写入缓存 */
|
|
796
826
|
async function findAmmPool(client: SolanaClient, protocol: 'raydium-amm' | 'raydium-cpmm', lpMint: string, signal: AbortSignal | undefined): Promise<AmmPoolState | null> {
|
|
797
827
|
const [programId, size, offset] = protocol === 'raydium-amm' ? [RAYDIUM_AMM_PROGRAM_ID, AMM_POOL_SIZE, 464] : [RAYDIUM_CPMM_PROGRAM_ID, CPMM_POOL_SIZE, 136]
|
|
798
|
-
|
|
799
|
-
|
|
828
|
+
// 免费公共节点大多不支持 getProgramAccounts:说明怎么绕过
|
|
829
|
+
const res = await withMethodHint(
|
|
830
|
+
client.request<{ pubkey: string; account: { data: [string, string]; owner: string } }[]>(
|
|
800
831
|
'getProgramAccounts',
|
|
801
832
|
[programId, { encoding: 'base64', commitment: client.commitment, filters: [{ dataSize: size }, { memcmp: { offset, bytes: lpMint } }] }],
|
|
802
833
|
{ signal },
|
|
803
|
-
)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
throw new Error(
|
|
810
|
-
`Finding the ${SOLANA_LP_PROTOCOLS[protocol].name} pool of LP token ${lpMint} uses getProgramAccounts, which failed (${err instanceof Error ? err.message : String(err)}; most free public nodes do not support it). ` +
|
|
811
|
-
"Pass the pool address in `pools`, use a node that supports getProgramAccounts, or skip LP tokens with `protocols: ['raydium-clmm', 'orca-whirlpool']`",
|
|
812
|
-
{ cause: err },
|
|
813
|
-
)
|
|
814
|
-
})
|
|
834
|
+
),
|
|
835
|
+
signal,
|
|
836
|
+
(detail) =>
|
|
837
|
+
`Finding the ${SOLANA_LP_PROTOCOLS[protocol].name} pool of LP token ${lpMint} uses getProgramAccounts, which failed (${detail}; most free public nodes do not support it). ` +
|
|
838
|
+
"Pass the pool address in `pools`, use a node that supports getProgramAccounts, or skip LP tokens with `protocols: ['raydium-clmm', 'orca-whirlpool']`",
|
|
839
|
+
)
|
|
815
840
|
for (const item of res ?? []) {
|
|
816
841
|
const data = base64.decode(item.account.data[0])
|
|
817
842
|
const pool = parseAmmPool({ address: item.pubkey, owner: item.account.owner, data, lamports: 0n, executable: false })
|
|
818
843
|
if (pool?.lpMint === lpMint) {
|
|
844
|
+
boundedSet(ammPoolByLpMint, lpPoolKey(protocol, lpMint), pool.address, MAX_CACHED_LP_POOLS)
|
|
819
845
|
return pool
|
|
820
846
|
}
|
|
821
847
|
}
|
package/src/portfolio.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 资产总值(子路径 `@w3lib/solana-batch-call/price`):代币余额 × 价格,可选加上 LP 仓位的价值
|
|
3
3
|
*/
|
|
4
4
|
import type { RequestOptions, SolanaClient } from './client.js'
|
|
5
|
-
import { lpPositions,
|
|
5
|
+
import { lpPositions, withMethodHint, type LpPositionsOptions, type SolanaLpPosition } from './lp.js'
|
|
6
6
|
import { combinePrices, defillamaPrices, onchainPrices, tokenPrices, type SolanaPriceSource } from './price.js'
|
|
7
7
|
|
|
8
8
|
export interface PortfolioOptions extends RequestOptions {
|
|
@@ -110,15 +110,12 @@ async function holdings(client: SolanaClient, owner: string, tokens: readonly st
|
|
|
110
110
|
const balances = tokens.length ? await client.balances(owner, tokens, { symbol: true, signal }) : []
|
|
111
111
|
return balances.filter((b) => b.success).map((b) => ({ token: b.token, native: b.native, symbol: b.symbol ?? null, balance: b.balance, decimals: b.decimals, formatted: b.formatted }))
|
|
112
112
|
}
|
|
113
|
-
const owned = await
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
{ cause: err },
|
|
121
|
-
)
|
|
122
|
-
})
|
|
113
|
+
const owned = await withMethodHint(
|
|
114
|
+
client.ownerTokens(owner, { signal }),
|
|
115
|
+
signal,
|
|
116
|
+
(detail) =>
|
|
117
|
+
`Listing the tokens of ${owner} failed (${detail}). ` +
|
|
118
|
+
'Listing needs getTokenAccountsByOwner (most free public nodes do not support it): pass the mints in `tokens`, or use a node that supports it',
|
|
119
|
+
)
|
|
123
120
|
return owned.map((t) => ({ token: t.token, native: t.native, symbol: t.symbol ?? null, balance: t.balance, decimals: t.decimals, formatted: t.formatted }))
|
|
124
121
|
}
|