@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
package/src/simulate.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { RequestOptions, SolanaClient } from './client.js'
|
|
|
9
9
|
import { NATIVE_MINT, SOL_DECIMALS, SYSTEM_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from './constants.js'
|
|
10
10
|
import { decodeInstruction, type DecodedInstruction } from './decode.js'
|
|
11
11
|
import { classify, describe, type AssetDelta, type TxKind } from './describe.js'
|
|
12
|
-
import { formatUnits, parseMint, type AccountInfo } from './layout.js'
|
|
12
|
+
import { computeBudgetOf, formatUnits, parseMint, parseTokenAccountState, type AccountInfo } from './layout.js'
|
|
13
13
|
import { BPF_LOADER_UPGRADEABLE, COMPUTE_BUDGET, PROGRAMS, programErrorName } from './programs.js'
|
|
14
14
|
import type { SolanaPriceSource } from './price.js'
|
|
15
15
|
import {
|
|
@@ -222,18 +222,7 @@ interface TokenState {
|
|
|
222
222
|
function tokenState(snap: Snap | null): TokenState | null {
|
|
223
223
|
const d = snap?.data
|
|
224
224
|
if (!snap || !d || (snap.owner !== TOKEN_PROGRAM_ID && snap.owner !== TOKEN_2022_PROGRAM_ID)) return null
|
|
225
|
-
|
|
226
|
-
const view = new DataView(d.buffer, d.byteOffset, d.byteLength)
|
|
227
|
-
const key = (o: number) => encodeAddress(d.slice(o, o + 32))
|
|
228
|
-
if (d[108] === 0) return null
|
|
229
|
-
return {
|
|
230
|
-
mint: key(0),
|
|
231
|
-
owner: key(32),
|
|
232
|
-
amount: view.getBigUint64(64, true),
|
|
233
|
-
delegate: view.getUint32(72, true) === 1 ? key(76) : null,
|
|
234
|
-
delegatedAmount: view.getBigUint64(121, true),
|
|
235
|
-
closeAuthority: view.getUint32(129, true) === 1 ? key(133) : null,
|
|
236
|
-
}
|
|
225
|
+
return parseTokenAccountState(d)
|
|
237
226
|
}
|
|
238
227
|
|
|
239
228
|
function mintAuthorities(snap: Snap | null): { mintAuthority: string | null; freezeAuthority: string | null } | null {
|
|
@@ -338,14 +327,7 @@ export async function simulate(client: SolanaClient, input: SimulateInput, optio
|
|
|
338
327
|
}
|
|
339
328
|
|
|
340
329
|
// 手续费:签名费 + 计算单元上限 × 单价
|
|
341
|
-
|
|
342
|
-
let price = 0n
|
|
343
|
-
for (const ix of tx.instructions) {
|
|
344
|
-
if (ix.programId !== COMPUTE_BUDGET) continue
|
|
345
|
-
const view = new DataView(ix.data.buffer, ix.data.byteOffset, ix.data.byteLength)
|
|
346
|
-
if (ix.data[0] === 2 && ix.data.length >= 5) limit = BigInt(view.getUint32(1, true))
|
|
347
|
-
if (ix.data[0] === 3 && ix.data.length >= 9) price = view.getBigUint64(1, true)
|
|
348
|
-
}
|
|
330
|
+
const { limit, price } = computeBudgetOf(tx.instructions)
|
|
349
331
|
const nonBudget = tx.instructions.filter((ix) => ix.programId !== COMPUTE_BUDGET).length
|
|
350
332
|
const units = BigInt(Math.min(1_400_000, limit !== null ? Number(limit) : 200_000 * nonBudget))
|
|
351
333
|
const fee = 5000n * BigInt(tx.signers.length) + (price * units + 999_999n) / 1_000_000n
|
package/src/source.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { DEFAULT_RPC_URLS, GENESIS_HASHES, type Cluster } from './constants.js'
|
|
2
|
-
import { FallbackRpc, HttpRpc, type FallbackOptions, type HttpRpcOptions, type RpcTransport } from './rpc.js'
|
|
2
|
+
import { FallbackRpc, HttpRpc, type FallbackOptions, type HttpRpcOptions, type RpcRequestOptions, type RpcTransport } from './rpc.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* 节点来源(也可以传数组:主节点 + 备用节点):
|
|
6
6
|
* - RPC URL 字符串
|
|
7
7
|
* - @solana/web3.js 的 Connection(使用其 rpcEndpoint)
|
|
8
|
-
* - 自定义传输:实现 `request(method, params)` 的对象(如包一层 @solana/kit 的 rpc
|
|
8
|
+
* - 自定义传输:实现 `request(method, params)` 的对象(如包一层 @solana/kit 的 rpc);可选的第三个参数 `{ signal }` 用于取消
|
|
9
9
|
*/
|
|
10
10
|
export type RpcSource = string | RpcTransport | { rpcEndpoint: string }
|
|
11
11
|
|
|
@@ -84,8 +84,8 @@ export class NetworkCheckedRpc implements RpcTransport {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
async request<T = unknown>(method: string, params?: readonly unknown[]): Promise<T> {
|
|
88
|
-
const [checked, result] = await Promise.allSettled([this.#check(), this.inner.request<T>(method, params)])
|
|
87
|
+
async request<T = unknown>(method: string, params?: readonly unknown[], options?: RpcRequestOptions): Promise<T> {
|
|
88
|
+
const [checked, result] = await Promise.allSettled([this.#check(), options ? this.inner.request<T>(method, params, options) : this.inner.request<T>(method, params)])
|
|
89
89
|
if (checked.status === 'rejected') {
|
|
90
90
|
throw checked.reason
|
|
91
91
|
}
|
|
@@ -102,10 +102,16 @@ export interface ResolvedSource {
|
|
|
102
102
|
genesis: () => Promise<string>
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
export { clientSourceConfigs, type ClientSourceConfig } from './clientConfig.js'
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param expectedGenesis 期望的创世哈希(如 sendRpc 要和 client 在同一个网络):传了时总是校验节点所在网络,不一致时报 NetworkMismatchError
|
|
109
|
+
*/
|
|
105
110
|
export function resolveSource(
|
|
106
111
|
source: RpcSource | readonly RpcSource[] | undefined,
|
|
107
112
|
cluster: Cluster | undefined,
|
|
108
113
|
options: SourceOptions = {},
|
|
114
|
+
expectedGenesis?: () => Promise<string>,
|
|
109
115
|
): ResolvedSource {
|
|
110
116
|
const isDefault = source === undefined
|
|
111
117
|
const list: readonly RpcSource[] = isDefault ? DEFAULT_RPC_URLS[cluster ?? 'mainnet'] : Array.isArray(source) ? source : [source as RpcSource]
|
|
@@ -118,6 +124,9 @@ export function resolveSource(
|
|
|
118
124
|
|
|
119
125
|
let detecting: Promise<string> | null = null
|
|
120
126
|
const genesis = (): Promise<string> => {
|
|
127
|
+
if (expectedGenesis) {
|
|
128
|
+
return expectedGenesis()
|
|
129
|
+
}
|
|
121
130
|
if (cluster) {
|
|
122
131
|
return Promise.resolve(GENESIS_HASHES[cluster])
|
|
123
132
|
}
|
|
@@ -142,7 +151,7 @@ export function resolveSource(
|
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
// 内置节点本来就是按 cluster 选的,不校验;单个节点且没指定 cluster 时,期望值就是它自己,也不需要校验
|
|
145
|
-
const check = !isDefault && (multiple || cluster !== undefined)
|
|
154
|
+
const check = !isDefault && (multiple || cluster !== undefined || expectedGenesis !== undefined)
|
|
146
155
|
const nodes = check ? raw.map((node) => new NetworkCheckedRpc(node, genesis)) : raw
|
|
147
156
|
// 单个节点也包一层 FallbackRpc:统一 onRequest 事件和健康状态(单节点失败时抛原始错误)
|
|
148
157
|
return { transport: new FallbackRpc(nodes, options), genesis }
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* await closeTokenAccounts(client, key, list.filter((a) => a.amount === '0'))
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
|
13
|
-
import { approvals, type
|
|
13
|
+
import { approvals, type ApprovalsOptions, type TokenAccountApproval } from '../approvals.js'
|
|
14
14
|
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
15
15
|
|
|
16
16
|
export {
|
|
@@ -22,6 +22,7 @@ export {
|
|
|
22
22
|
revokeApprovals,
|
|
23
23
|
type ApprovalCall,
|
|
24
24
|
type ApprovalResult,
|
|
25
|
+
type ApprovalsOptions,
|
|
25
26
|
type ApprovalsQuery,
|
|
26
27
|
type BuildClosesOptions,
|
|
27
28
|
type CloseAccountsOptions,
|
|
@@ -30,7 +31,7 @@ export {
|
|
|
30
31
|
} from '../approvals.js'
|
|
31
32
|
export { closeAccount, harvestWithheldTokensToMint, revoke } from '../write/instructions.js'
|
|
32
33
|
|
|
33
|
-
export interface GetApprovalsOptions extends
|
|
34
|
+
export interface GetApprovalsOptions extends ApprovalsOptions, ShortcutOptions {}
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* 查地址的代币账户给出的授权和可以关闭的空账户,节点参数与 getBalances 相同。
|
package/src/subpaths/claim.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* await claimAll(client, key, c.positions) // 批量领取
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
|
-
import { claimable, type Claimable, type
|
|
14
|
+
import { claimable, type Claimable, type ClaimableOptions } from '../claim.js'
|
|
15
15
|
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
16
16
|
|
|
17
17
|
export {
|
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
type ClaimOptions,
|
|
26
26
|
type ClaimPlan,
|
|
27
27
|
type ClaimProtocol,
|
|
28
|
+
type ClaimableOptions,
|
|
28
29
|
type ClaimQuery,
|
|
29
30
|
type ClaimResult,
|
|
30
31
|
type ClaimSimulation,
|
|
@@ -35,7 +36,7 @@ export {
|
|
|
35
36
|
} from '../claim.js'
|
|
36
37
|
export type { PlanSimulation } from '../batchPlan.js'
|
|
37
38
|
|
|
38
|
-
export interface GetClaimableOptions extends
|
|
39
|
+
export interface GetClaimableOptions extends ClaimableOptions, ShortcutOptions {}
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* 预览一个地址能领取的手续费和挖矿奖励(只读取),节点参数与 getBalances 相同。默认列出全部代币账户(需要支持 getTokenAccountsByOwner 的节点),
|
package/src/subpaths/reclaim.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* const { simulation, results } = await reclaim(client, key, r.items.filter((i) => i.selected))
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
|
-
import { reclaimable, type Reclaimable, type
|
|
14
|
+
import { reclaimable, type Reclaimable, type ReclaimableOptions } from '../reclaim.js'
|
|
15
15
|
import { resolve, type ShortcutOptions } from '../shortcuts.js'
|
|
16
16
|
|
|
17
17
|
export {
|
|
@@ -28,6 +28,7 @@ export {
|
|
|
28
28
|
type ReclaimKind,
|
|
29
29
|
type ReclaimOptions,
|
|
30
30
|
type ReclaimPlan,
|
|
31
|
+
type ReclaimableOptions,
|
|
31
32
|
type ReclaimQuery,
|
|
32
33
|
type ReclaimResult,
|
|
33
34
|
type ReclaimSimulation,
|
|
@@ -37,7 +38,7 @@ export {
|
|
|
37
38
|
export type { PlanSimulation } from '../batchPlan.js'
|
|
38
39
|
export { burnChecked, closeAccount, harvestWithheldTokensToMint } from '../write/instructions.js'
|
|
39
40
|
|
|
40
|
-
export interface GetReclaimableOptions extends
|
|
41
|
+
export interface GetReclaimableOptions extends ReclaimableOptions, ShortcutOptions {}
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* 扫描钱包里能回收租金的账户,节点参数与 getBalances 相同。默认列出全部代币账户(需要支持 getTokenAccountsByOwner 的节点),
|
package/src/swap/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (直接兑换,或经过 SOL / USDC / USDT 的两跳),按链上状态在本地报价(照搬各程序的计算),选输出最多的;
|
|
4
4
|
* 按滑点算最少收到的数量,构建指令(SOL 自动包装 / 解包、缺少的代币账户自动创建),发送同 /write
|
|
5
5
|
*/
|
|
6
|
-
import { getAssociatedTokenAddress, isAddress } from '../address.js'
|
|
6
|
+
import { getAssociatedTokenAddress, InvalidAddressError, isAddress } from '../address.js'
|
|
7
7
|
import type { SolanaClient } from '../client.js'
|
|
8
8
|
import { NATIVE_MINT, TOKEN_PROGRAM_ID } from '../constants.js'
|
|
9
9
|
import { formatUnits, parseTokenAccount, type AccountInfo } from '../layout.js'
|
|
@@ -180,7 +180,7 @@ export async function buildSwap(client: SolanaClient, owner: string, params: Sol
|
|
|
180
180
|
const tokenIn = nativeIn ? NATIVE_MINT : params.tokenIn
|
|
181
181
|
const tokenOut = nativeOut ? NATIVE_MINT : params.tokenOut
|
|
182
182
|
for (const mint of [tokenIn, tokenOut]) {
|
|
183
|
-
if (!isAddress(mint)) throw new
|
|
183
|
+
if (!isAddress(mint)) throw new InvalidAddressError(`Invalid mint address: ${mint}`, mint)
|
|
184
184
|
}
|
|
185
185
|
if (tokenIn === tokenOut) throw new Error(nativeIn || nativeOut ? 'Wrapping / unwrapping SOL is not a swap' : 'tokenIn and tokenOut are the same')
|
|
186
186
|
const protocols = new Set(options.protocols ?? SWAP_PROTOCOLS)
|
|
@@ -192,7 +192,7 @@ export async function buildSwap(client: SolanaClient, owner: string, params: Sol
|
|
|
192
192
|
if (!isAddress(m)) throw new Error(`Invalid intermediate mint: ${m}`)
|
|
193
193
|
}
|
|
194
194
|
for (const p of options.pools ?? []) {
|
|
195
|
-
if (!isAddress(p)) throw new
|
|
195
|
+
if (!isAddress(p)) throw new InvalidAddressError(`Invalid pool address: ${p}`, p)
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
// 1. 一次读:候选池子(PDA 推导 + 传入的)、Clock、各代币的 mint
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// 代币详情(tokens)、NFT 元数据(nfts)、NFT 持有人(nftOwners)
|
|
2
|
+
|
|
3
|
+
import type { AccountInfo, TransferFeeInfo } from './layout.js'
|
|
4
|
+
import { getMetadataAddress, isAddress } from './address.js'
|
|
5
|
+
import type { RequestOptions, TokensOptions } from './client.js'
|
|
6
|
+
import { SOL_DECIMALS, TOKEN_2022_PROGRAM_ID } from './constants.js'
|
|
7
|
+
import { formatUnits, parseTokenAccount, parseTransferFeeConfig } from './layout.js'
|
|
8
|
+
import { RpcError } from './rpc.js'
|
|
9
|
+
import type { ReadContext } from './balances.js'
|
|
10
|
+
import type { QueryContext } from './accountReader.js'
|
|
11
|
+
import { getMeta, learnMeta, safeParseMetaplex, safeParseMint } from './tokenMeta.js'
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_TOKEN_FIELDS,
|
|
14
|
+
type FailureReason,
|
|
15
|
+
type NftDetails,
|
|
16
|
+
type NftOwner,
|
|
17
|
+
type TokenDetails,
|
|
18
|
+
type TokenField,
|
|
19
|
+
type TokenStandard,
|
|
20
|
+
type TransferFee,
|
|
21
|
+
type TransferFeeConfig,
|
|
22
|
+
} from './types.js'
|
|
23
|
+
|
|
24
|
+
const TOKEN_STANDARDS: readonly TokenStandard[] = [
|
|
25
|
+
'NonFungible',
|
|
26
|
+
'FungibleAsset',
|
|
27
|
+
'Fungible',
|
|
28
|
+
'NonFungibleEdition',
|
|
29
|
+
'ProgrammableNonFungible',
|
|
30
|
+
'ProgrammableNonFungibleEdition',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
export async function tokensOf<F extends TokenField>(rc: ReadContext, mints: readonly string[], options: TokensOptions<F>): Promise<TokenDetails<F>[]> {
|
|
34
|
+
const fields = (options.fields ?? DEFAULT_TOKEN_FIELDS) as readonly F[]
|
|
35
|
+
const wanted = new Set<TokenField>(fields)
|
|
36
|
+
const scope = rc.scope
|
|
37
|
+
const ctx: QueryContext = { signal: options.signal }
|
|
38
|
+
const needsNames = wanted.has('name') || wanted.has('symbol') || wanted.has('uri')
|
|
39
|
+
const addresses: string[] = []
|
|
40
|
+
const plan = mints.map((mint) => {
|
|
41
|
+
if (rc.nativeMints.has(mint) || !isAddress(mint)) {
|
|
42
|
+
return { mint, mintIndex: -1, metadataIndex: -1 }
|
|
43
|
+
}
|
|
44
|
+
const meta = getMeta(scope, mint)
|
|
45
|
+
// uri / supply / 权限 / 手续费不缓存,需要时总要读 mint;name / symbol 命中缓存时可以省掉元数据账户
|
|
46
|
+
const cachedOnly =
|
|
47
|
+
!wanted.has('uri') &&
|
|
48
|
+
!wanted.has('supply') &&
|
|
49
|
+
!wanted.has('mintAuthority') &&
|
|
50
|
+
!wanted.has('freezeAuthority') &&
|
|
51
|
+
!wanted.has('transferFee') &&
|
|
52
|
+
(!wanted.has('decimals') || meta.decimals !== undefined) &&
|
|
53
|
+
(!wanted.has('tokenProgram') || meta.tokenProgram !== undefined) &&
|
|
54
|
+
(!wanted.has('name') || meta.name !== undefined) &&
|
|
55
|
+
(!wanted.has('symbol') || meta.symbol !== undefined)
|
|
56
|
+
if (cachedOnly) {
|
|
57
|
+
return { mint, mintIndex: -1, metadataIndex: -1 }
|
|
58
|
+
}
|
|
59
|
+
const namesCached = (!wanted.has('name') || meta.name !== undefined) && (!wanted.has('symbol') || meta.symbol !== undefined) && !wanted.has('uri')
|
|
60
|
+
return {
|
|
61
|
+
mint,
|
|
62
|
+
mintIndex: addresses.push(mint) - 1,
|
|
63
|
+
metadataIndex: needsNames && !namesCached ? addresses.push(getMetadataAddress(mint)) - 1 : -1,
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
const [accounts, epoch] = await Promise.all([
|
|
67
|
+
addresses.length ? rc.reader.accounts(addresses, ctx) : Promise.resolve([] as (AccountInfo | null)[]),
|
|
68
|
+
// 手续费按当前 epoch 选新 / 旧配置;和账户读取在同一个批量请求里
|
|
69
|
+
wanted.has('transferFee') && addresses.length
|
|
70
|
+
? rc.reader.call<{ epoch: number | string }>(ctx, 'getEpochInfo', [{ commitment: rc.commitment }]).then((res) => BigInt(res.epoch))
|
|
71
|
+
: Promise.resolve(null),
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
return plan.map(({ mint, mintIndex, metadataIndex }) => {
|
|
75
|
+
const native = rc.nativeMints.has(mint)
|
|
76
|
+
const values: Record<TokenField, unknown> & { exists: boolean } = native
|
|
77
|
+
? { exists: true, name: 'Solana', symbol: 'SOL', uri: null, decimals: SOL_DECIMALS, supply: null, tokenProgram: null, mintAuthority: null, freezeAuthority: null, transferFee: null }
|
|
78
|
+
: tokenValues(
|
|
79
|
+
rc,
|
|
80
|
+
mint,
|
|
81
|
+
mintIndex === -1 ? null : (accounts[mintIndex] ?? null),
|
|
82
|
+
metadataIndex === -1 ? null : (accounts[metadataIndex] ?? null),
|
|
83
|
+
mintIndex !== -1,
|
|
84
|
+
mintIndex !== -1 && metadataIndex !== -1,
|
|
85
|
+
epoch,
|
|
86
|
+
)
|
|
87
|
+
const out: Record<string, unknown> = { address: mint, native }
|
|
88
|
+
let missing: F | undefined
|
|
89
|
+
for (const field of fields) {
|
|
90
|
+
out[field] = values[field] ?? null
|
|
91
|
+
// 权限为 null 表示没有权限、手续费为 null 表示没有手续费,都是有效值;主币没有 uri / supply / tokenProgram
|
|
92
|
+
const nullable =
|
|
93
|
+
field === 'mintAuthority' || field === 'freezeAuthority' || field === 'transferFee' || (native && (field === 'uri' || field === 'supply' || field === 'tokenProgram'))
|
|
94
|
+
if (out[field] === null && !nullable) {
|
|
95
|
+
missing ??= field
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (wanted.has('supply')) {
|
|
99
|
+
const supply = values.supply as string | null
|
|
100
|
+
const decimals = values.decimals as number | null
|
|
101
|
+
out.supplyFormatted = supply === null || decimals === null ? null : formatUnits(BigInt(supply), decimals)
|
|
102
|
+
}
|
|
103
|
+
const error: FailureReason | undefined = native
|
|
104
|
+
? undefined
|
|
105
|
+
: !isAddress(mint)
|
|
106
|
+
? 'invalid-address'
|
|
107
|
+
: !values.exists
|
|
108
|
+
? accounts[mintIndex]
|
|
109
|
+
? 'not-token'
|
|
110
|
+
: 'not-found'
|
|
111
|
+
: missing
|
|
112
|
+
? 'missing-field'
|
|
113
|
+
: undefined
|
|
114
|
+
out.success = error === undefined
|
|
115
|
+
if (error) {
|
|
116
|
+
out.error = error
|
|
117
|
+
if (error === 'missing-field') {
|
|
118
|
+
out.errorField = missing
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return out as TokenDetails<F>
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function tokenValues(
|
|
126
|
+
rc: ReadContext,
|
|
127
|
+
mint: string,
|
|
128
|
+
mintAccount: AccountInfo | null,
|
|
129
|
+
metadataAccount: AccountInfo | null,
|
|
130
|
+
fetchedMint: boolean,
|
|
131
|
+
lookedForNames: boolean,
|
|
132
|
+
epoch: bigint | null,
|
|
133
|
+
): Record<TokenField, unknown> & { exists: boolean } {
|
|
134
|
+
const scope = rc.scope
|
|
135
|
+
const meta = learnMeta(scope, mint, mintAccount, metadataAccount, lookedForNames)
|
|
136
|
+
const info = mintAccount ? safeParseMint(mintAccount) : null
|
|
137
|
+
const uri = info?.metadata?.uri ?? (metadataAccount ? (safeParseMetaplex(metadataAccount)?.uri ?? null) : null)
|
|
138
|
+
const feeConfig = info && mintAccount?.owner === TOKEN_2022_PROGRAM_ID ? parseTransferFeeConfig(mintAccount.data) : null
|
|
139
|
+
return {
|
|
140
|
+
// 读了 mint 账户就以它为准(不存在则 false);没读说明请求的字段全部命中缓存
|
|
141
|
+
exists: fetchedMint ? info !== null : true,
|
|
142
|
+
name: meta.name ?? null,
|
|
143
|
+
symbol: meta.symbol ?? null,
|
|
144
|
+
uri,
|
|
145
|
+
decimals: meta.decimals ?? null,
|
|
146
|
+
supply: info ? info.supply.toString() : null,
|
|
147
|
+
tokenProgram: meta.tokenProgram ?? null,
|
|
148
|
+
mintAuthority: info?.mintAuthority ?? null,
|
|
149
|
+
freezeAuthority: info?.freezeAuthority ?? null,
|
|
150
|
+
transferFee: feeConfig ? toTransferFeeConfig(feeConfig.older, feeConfig.newer, epoch) : null,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function toTransferFee(fee: TransferFeeInfo): TransferFee {
|
|
155
|
+
return { basisPoints: fee.basisPoints, maximumFee: fee.maximumFee.toString(), epoch: fee.epoch.toString() }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 当前生效的配置:epoch >= newer.epoch 用 newer,否则 older;不知道当前 epoch 时按 newer */
|
|
159
|
+
function toTransferFeeConfig(older: TransferFeeInfo, newer: TransferFeeInfo, epoch: bigint | null): TransferFeeConfig {
|
|
160
|
+
const current = epoch === null || epoch >= newer.epoch ? newer : older
|
|
161
|
+
return { ...toTransferFee(current), older: toTransferFee(older), newer: toTransferFee(newer) }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 批量查 NFT 元数据(Metaplex;Token-2022 NFT 取 TokenMetadata 扩展),一次 getMultipleAccounts */
|
|
165
|
+
export async function nftsOf(rc: ReadContext, mints: readonly string[], options: RequestOptions): Promise<NftDetails[]> {
|
|
166
|
+
const scope = rc.scope
|
|
167
|
+
const addresses: string[] = []
|
|
168
|
+
const plan = mints.map((mint) =>
|
|
169
|
+
isAddress(mint) ? { mint, metadataIndex: addresses.push(getMetadataAddress(mint)) - 1, mintIndex: addresses.push(mint) - 1 } : { mint, metadataIndex: -1, mintIndex: -1 },
|
|
170
|
+
)
|
|
171
|
+
const accounts = addresses.length ? await rc.reader.accounts(addresses, { signal: options.signal }) : []
|
|
172
|
+
return plan.map(({ mint, metadataIndex, mintIndex }): NftDetails => {
|
|
173
|
+
const metadataAccount = metadataIndex === -1 ? null : (accounts[metadataIndex] ?? null)
|
|
174
|
+
const mintAccount = mintIndex === -1 ? null : (accounts[mintIndex] ?? null)
|
|
175
|
+
learnMeta(scope, mint, mintAccount, metadataAccount, mintIndex !== -1)
|
|
176
|
+
const metaplex = metadataAccount ? safeParseMetaplex(metadataAccount) : null
|
|
177
|
+
const ext = mintAccount ? (safeParseMint(mintAccount)?.metadata ?? null) : null
|
|
178
|
+
if (!metaplex && !ext) {
|
|
179
|
+
const error: FailureReason = mintIndex === -1 ? 'invalid-address' : !mintAccount ? 'not-found' : !safeParseMint(mintAccount) ? 'not-token' : 'no-metadata'
|
|
180
|
+
return { mint, name: null, symbol: null, uri: null, collection: null, creators: [], sellerFeeBasisPoints: null, tokenStandard: null, isMutable: null, updateAuthority: null, success: false, error }
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
mint,
|
|
184
|
+
name: ext?.name ?? metaplex?.name ?? null,
|
|
185
|
+
symbol: ext?.symbol ?? metaplex?.symbol ?? null,
|
|
186
|
+
uri: ext?.uri ?? metaplex?.uri ?? null,
|
|
187
|
+
collection: metaplex?.collection ?? null,
|
|
188
|
+
creators: metaplex?.creators ?? [],
|
|
189
|
+
sellerFeeBasisPoints: metaplex?.sellerFeeBasisPoints ?? null,
|
|
190
|
+
tokenStandard: metaplex?.tokenStandard == null ? null : (TOKEN_STANDARDS[metaplex.tokenStandard] ?? null),
|
|
191
|
+
isMutable: metaplex?.isMutable ?? null,
|
|
192
|
+
updateAuthority: metaplex?.updateAuthority ?? null,
|
|
193
|
+
success: true,
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 批量查 NFT 持有人:getTokenLargestAccounts(合并成一个批量请求)+ 一次 getMultipleAccounts。
|
|
200
|
+
* 节点拒绝的 mint(不存在 / 不是代币)在同一次 getMultipleAccounts 里读 mint 账户区分原因
|
|
201
|
+
*/
|
|
202
|
+
export async function nftOwnersOf(rc: ReadContext, mints: readonly string[], options: RequestOptions): Promise<NftOwner[]> {
|
|
203
|
+
const ctx: QueryContext = { signal: options.signal }
|
|
204
|
+
const largest = await Promise.all(
|
|
205
|
+
mints.map((mint): Promise<{ tokenAccount: string | null; rejected: boolean }> =>
|
|
206
|
+
isAddress(mint)
|
|
207
|
+
? rc.reader.call<{ value: Array<{ address: string; amount: string }> }>(ctx, 'getTokenLargestAccounts', [mint, { commitment: rc.commitment }])
|
|
208
|
+
.then((res) => ({ tokenAccount: res.value.find((a) => a.amount !== '0')?.address ?? null, rejected: false }))
|
|
209
|
+
.catch((err: unknown) => {
|
|
210
|
+
// mint 不存在 / 不是代币:该项失败;节点问题照常抛出
|
|
211
|
+
if (err instanceof RpcError && !err.nodeFault) return { tokenAccount: null, rejected: true }
|
|
212
|
+
throw err
|
|
213
|
+
})
|
|
214
|
+
: Promise.resolve({ tokenAccount: null, rejected: false }),
|
|
215
|
+
),
|
|
216
|
+
)
|
|
217
|
+
const toRead = largest.flatMap(({ tokenAccount, rejected }, i) => (tokenAccount ? [tokenAccount] : rejected ? [mints[i] as string] : []))
|
|
218
|
+
const accounts = toRead.length ? await rc.reader.accounts(toRead, ctx) : []
|
|
219
|
+
const byAddress = new Map(toRead.map((a, i) => [a, accounts[i] ?? null]))
|
|
220
|
+
return mints.map((mint, i): NftOwner => {
|
|
221
|
+
const { tokenAccount, rejected } = largest[i] ?? { tokenAccount: null, rejected: false }
|
|
222
|
+
if (!isAddress(mint)) {
|
|
223
|
+
return { mint, owner: null, tokenAccount: null, success: false, error: 'invalid-address' }
|
|
224
|
+
}
|
|
225
|
+
if (rejected) {
|
|
226
|
+
return { mint, owner: null, tokenAccount: null, success: false, error: byAddress.get(mint) ? 'not-token' : 'not-found' }
|
|
227
|
+
}
|
|
228
|
+
const account = tokenAccount ? byAddress.get(tokenAccount) : null
|
|
229
|
+
let owner: string | null = null
|
|
230
|
+
if (account) {
|
|
231
|
+
try {
|
|
232
|
+
owner = parseTokenAccount(account.data).owner
|
|
233
|
+
} catch {
|
|
234
|
+
owner = null
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// 没有余额大于 0 的代币账户(供应量为 0 / 已销毁),或代币账户刚被关闭
|
|
238
|
+
return owner ? { mint, owner, tokenAccount, success: true } : { mint, owner: null, tokenAccount: null, success: false, error: 'no-holder' }
|
|
239
|
+
})
|
|
240
|
+
}
|