@w3lib/solana-batch-call 0.1.0 → 0.2.1
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 +41 -0
- package/README.md +120 -18
- package/dist/address.d.ts +7 -1
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +56 -8
- package/dist/address.js.map +1 -1
- package/dist/client.d.ts +117 -23
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +756 -247
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +31 -2
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +68 -8
- package/dist/layout.js.map +1 -1
- package/dist/rpc.d.ts +65 -4
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +320 -42
- package/dist/rpc.js.map +1 -1
- package/dist/shortcuts.d.ts +38 -5
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +169 -12
- package/dist/shortcuts.js.map +1 -1
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +2 -4
- package/dist/source.js.map +1 -1
- package/dist/types.d.ts +54 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +13 -10
- package/src/address.ts +60 -8
- package/src/client.ts +974 -270
- package/src/index.ts +47 -3
- package/src/layout.ts +90 -8
- package/src/rpc.ts +368 -42
- package/src/shortcuts.ts +223 -16
- package/src/source.ts +2 -4
- package/src/types.ts +59 -3
package/src/shortcuts.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
SolanaClient,
|
|
3
|
+
type BalanceQuery,
|
|
4
|
+
type BalancesOptions,
|
|
5
|
+
type ClientConfig,
|
|
6
|
+
type OwnerTokensOptions,
|
|
7
|
+
type RequestOptions,
|
|
8
|
+
type SolBalancesOptions,
|
|
9
|
+
type TokensOptions,
|
|
10
|
+
} from './client.js'
|
|
2
11
|
import type { RpcSource } from './source.js'
|
|
3
12
|
import type { DefaultTokenField, NftDetails, NftOwner, OwnedToken, SolBalance, TokenBalance, TokenDetails, TokenField } from './types.js'
|
|
4
13
|
|
|
@@ -29,11 +38,24 @@ function setBounded<K, V>(map: Map<K, V>, key: K, value: V): void {
|
|
|
29
38
|
}
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
// 配置里的函数(如自定义 fetch)按对象编号参与缓存键:同一个函数对象复用同一个客户端(WeakMap 不阻止回收)
|
|
42
|
+
const functionIds = new WeakMap<object, number>()
|
|
43
|
+
let nextFunctionId = 0
|
|
44
|
+
|
|
45
|
+
/** 配置的缓存键:对象的键按字母排序(写法顺序不同视为同一份配置),函数按对象编号;无法序列化时返回 null(不缓存) */
|
|
32
46
|
function configKey(config: ClientConfig): string | null {
|
|
33
47
|
try {
|
|
34
48
|
return JSON.stringify(config, (_key, value: unknown) => {
|
|
35
49
|
if (typeof value === 'function') {
|
|
36
|
-
|
|
50
|
+
let id = functionIds.get(value)
|
|
51
|
+
if (id === undefined) {
|
|
52
|
+
id = ++nextFunctionId
|
|
53
|
+
functionIds.set(value, id)
|
|
54
|
+
}
|
|
55
|
+
return `function#${id}`
|
|
56
|
+
}
|
|
57
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
58
|
+
return Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
37
59
|
}
|
|
38
60
|
return value
|
|
39
61
|
})
|
|
@@ -55,7 +77,10 @@ function sourceKey(source: ShortcutOptions['provider']): string | object | null
|
|
|
55
77
|
return source as object
|
|
56
78
|
}
|
|
57
79
|
|
|
58
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* 拆出节点参数和函数自己的参数(ownKeys),其余作为客户端配置;值为 undefined 的参数忽略。
|
|
82
|
+
* signal 总是作为函数自己的参数(放进客户端配置会让每次调用都新建客户端)
|
|
83
|
+
*/
|
|
59
84
|
function resolve<T extends ShortcutOptions, K extends keyof T & string = never>(
|
|
60
85
|
options: T,
|
|
61
86
|
ownKeys: readonly K[] = [],
|
|
@@ -65,7 +90,7 @@ function resolve<T extends ShortcutOptions, K extends keyof T & string = never>(
|
|
|
65
90
|
const config: Record<string, unknown> = {}
|
|
66
91
|
for (const [key, value] of Object.entries(others)) {
|
|
67
92
|
if (value !== undefined) {
|
|
68
|
-
;((ownKeys as readonly string[]).includes(key) ? own : config)[key] = value
|
|
93
|
+
;((ownKeys as readonly string[]).includes(key) ? own : key === 'signal' ? {} : config)[key] = value
|
|
69
94
|
}
|
|
70
95
|
}
|
|
71
96
|
return { client: getClient(provider, config as ClientConfig), own: own as Pick<T, K> }
|
|
@@ -104,17 +129,36 @@ export interface GetBalancesOptions extends BalancesOptions, ShortcutOptions {}
|
|
|
104
129
|
*
|
|
105
130
|
* await getBalances(owner, [NATIVE_MINT, USDC]) // 指定代币:免费公共节点也能用
|
|
106
131
|
* await getBalances(owner, [NATIVE_MINT, USDC], { symbol: true })
|
|
132
|
+
* await getBalances(owner, undefined, { tokenPrograms: ['spl'] }) // 不含 Token-2022 代币
|
|
133
|
+
* await getBalances(owner, [USDC], { accounts: 'all' }) // 算上非 ATA 账户:需要支持 getTokenAccountsByOwner 的节点
|
|
107
134
|
* await getBalances(owner, undefined, { provider: 'https://my-rpc…' }) // 全部持仓:需要支持 getTokenAccountsByOwner 的节点
|
|
108
135
|
* ```
|
|
109
136
|
*/
|
|
110
137
|
export function getBalances(owner: string, mints?: readonly string[], options: GetBalancesOptions = {}): Promise<TokenBalance[]> {
|
|
111
|
-
const { client, own } = resolve(options,
|
|
138
|
+
const { client, own } = resolve(options, BALANCE_KEYS)
|
|
112
139
|
return client.balances(owner, mints, own)
|
|
113
140
|
}
|
|
114
141
|
|
|
142
|
+
const BALANCE_KEYS = ['symbol', 'scan', 'accounts', 'tokenPrograms', 'signal', 'minContextSlot', 'withSlot'] as const
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 多个钱包一次查,结果与 queries 一一对应;ATA 模式下所有钱包合并成一次 getMultipleAccounts。参数同 getBalances。
|
|
146
|
+
*
|
|
147
|
+
* ```ts
|
|
148
|
+
* await getMultiBalances([{ owner: a, mints: [NATIVE_MINT, USDC] }, { owner: b, mints: [USDC] }])
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export function getMultiBalances(queries: readonly BalanceQuery[], options: GetBalancesOptions = {}): Promise<TokenBalance[][]> {
|
|
152
|
+
const { client, own } = resolve(options, BALANCE_KEYS)
|
|
153
|
+
return client.multiBalances(queries, own)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface GetSolBalancesOptions extends SolBalancesOptions, ShortcutOptions {}
|
|
157
|
+
|
|
115
158
|
/** 批量查多个地址的 SOL 余额(一次 getMultipleAccounts) */
|
|
116
|
-
export function getSolBalances(addresses: readonly string[], options:
|
|
117
|
-
|
|
159
|
+
export function getSolBalances(addresses: readonly string[], options: GetSolBalancesOptions = {}): Promise<SolBalance[]> {
|
|
160
|
+
const { client, own } = resolve(options, ['signal', 'minContextSlot', 'withSlot'])
|
|
161
|
+
return client.solBalances(addresses, own)
|
|
118
162
|
}
|
|
119
163
|
|
|
120
164
|
export interface GetTokensOptions<F extends TokenField = DefaultTokenField> extends TokensOptions<F>, ShortcutOptions {}
|
|
@@ -127,23 +171,28 @@ export interface GetTokensOptions<F extends TokenField = DefaultTokenField> exte
|
|
|
127
171
|
* ```
|
|
128
172
|
*/
|
|
129
173
|
export function getTokens<const F extends TokenField = DefaultTokenField>(mints: readonly string[], options: GetTokensOptions<F> = {}): Promise<TokenDetails<F>[]> {
|
|
130
|
-
const { client, own } = resolve(options, ['fields'])
|
|
131
|
-
return client.tokens<F>(mints,
|
|
174
|
+
const { client, own } = resolve(options, ['fields', 'signal'])
|
|
175
|
+
return client.tokens<F>(mints, own)
|
|
132
176
|
}
|
|
133
177
|
|
|
178
|
+
export interface GetNftsOptions extends RequestOptions, ShortcutOptions {}
|
|
179
|
+
|
|
134
180
|
/** 批量查 NFT 元数据(名称、uri、所属集合、版税、创作者等) */
|
|
135
|
-
export function getNfts(mints: readonly string[], options:
|
|
136
|
-
|
|
181
|
+
export function getNfts(mints: readonly string[], options: GetNftsOptions = {}): Promise<NftDetails[]> {
|
|
182
|
+
const { client, own } = resolve(options, ['signal'])
|
|
183
|
+
return client.nfts(mints, own)
|
|
137
184
|
}
|
|
138
185
|
|
|
139
186
|
/** 批量查 NFT 持有人 */
|
|
140
|
-
export function getNftOwners(mints: readonly string[], options:
|
|
141
|
-
|
|
187
|
+
export function getNftOwners(mints: readonly string[], options: GetNftsOptions = {}): Promise<NftOwner[]> {
|
|
188
|
+
const { client, own } = resolve(options, ['signal'])
|
|
189
|
+
return client.nftOwners(mints, own)
|
|
142
190
|
}
|
|
143
191
|
|
|
144
192
|
/** 查某地址持有的全部 NFT(需要支持 getTokenAccountsByOwner 的节点) */
|
|
145
|
-
export function getOwnerNfts(owner: string, options:
|
|
146
|
-
|
|
193
|
+
export function getOwnerNfts(owner: string, options: GetNftsOptions = {}): Promise<NftDetails[]> {
|
|
194
|
+
const { client, own } = resolve(options, ['signal'])
|
|
195
|
+
return client.ownerNfts(owner, own)
|
|
147
196
|
}
|
|
148
197
|
|
|
149
198
|
export interface GetOwnerTokensOptions extends OwnerTokensOptions, ShortcutOptions {}
|
|
@@ -158,10 +207,168 @@ export interface GetOwnerTokensOptions extends OwnerTokensOptions, ShortcutOptio
|
|
|
158
207
|
* ```
|
|
159
208
|
*/
|
|
160
209
|
export function getOwnerTokens(owner: string, options: GetOwnerTokensOptions = {}): Promise<OwnedToken[]> {
|
|
161
|
-
const { client, own } = resolve(options, ['metadata', 'includeNative', 'includeZero', 'includeNfts'])
|
|
210
|
+
const { client, own } = resolve(options, ['metadata', 'includeNative', 'includeZero', 'includeNfts', 'tokenPrograms', 'signal', 'minContextSlot', 'withSlot'])
|
|
162
211
|
return client.ownerTokens(owner, own)
|
|
163
212
|
}
|
|
164
213
|
|
|
214
|
+
export interface WatchBalancesOptions extends Omit<GetBalancesOptions, 'signal' | 'minContextSlot'> {
|
|
215
|
+
/** 轮询间隔(毫秒),默认 15000;多处订阅同一份时取最小的 */
|
|
216
|
+
interval?: number
|
|
217
|
+
/** 第一次拿到结果、以及之后余额(balance)变化时调用;previous 是上一次通知的结果(第一次为 null) */
|
|
218
|
+
onChange: (balances: TokenBalance[], previous: TokenBalance[] | null) => void
|
|
219
|
+
/** 某次轮询失败时调用(之后照常继续轮询) */
|
|
220
|
+
onError?: (err: unknown) => void
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface Watcher {
|
|
224
|
+
interval: number
|
|
225
|
+
onChange: WatchBalancesOptions['onChange']
|
|
226
|
+
onError?: WatchBalancesOptions['onError']
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface Watch {
|
|
230
|
+
client: SolanaClient
|
|
231
|
+
owner: string
|
|
232
|
+
mints: readonly string[] | undefined
|
|
233
|
+
options: BalancesOptions
|
|
234
|
+
watchers: Set<Watcher>
|
|
235
|
+
timer: ReturnType<typeof setTimeout> | null
|
|
236
|
+
running: boolean
|
|
237
|
+
stopped: boolean
|
|
238
|
+
last: TokenBalance[] | null
|
|
239
|
+
lastKey: string | null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 共享轮询:按客户端(同样的节点配置会复用同一个客户端)+ owner + mints(顺序无关)+ 影响结果的选项
|
|
243
|
+
const watches = new WeakMap<SolanaClient, Map<string, Watch>>()
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 轮询余额,多处订阅同一个钱包时合并成一份轮询,只在余额变化时通知。返回取消订阅的函数;最后一个订阅者取消时停止轮询。
|
|
247
|
+
* 参数同 getBalances(不支持 signal / minContextSlot);上一次请求没完成时不会发起下一次。
|
|
248
|
+
* 结果顺序按第一个订阅者传的 mints;新订阅者加入时如果已有结果,立即收到一次(previous 为 null)
|
|
249
|
+
*
|
|
250
|
+
* ```ts
|
|
251
|
+
* const stop = watchBalances(owner, [NATIVE_MINT, USDC], { interval: 10_000, onChange: (list) => render(list) })
|
|
252
|
+
* stop()
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
export function watchBalances(owner: string, mints: readonly string[] | undefined, options: WatchBalancesOptions): () => void {
|
|
256
|
+
const { onChange, onError, interval = 15_000, ...rest } = options
|
|
257
|
+
if (!(interval > 0)) {
|
|
258
|
+
throw new Error(`Invalid interval: ${interval}`)
|
|
259
|
+
}
|
|
260
|
+
const { client, own } = resolve(rest, ['symbol', 'scan', 'accounts', 'tokenPrograms', 'withSlot'])
|
|
261
|
+
const key = JSON.stringify([
|
|
262
|
+
owner,
|
|
263
|
+
mints === undefined ? null : [...new Set(mints)].sort(),
|
|
264
|
+
own.symbol ?? false,
|
|
265
|
+
own.scan ?? false,
|
|
266
|
+
own.accounts ?? 'ata',
|
|
267
|
+
own.tokenPrograms === undefined ? null : [...own.tokenPrograms].sort(),
|
|
268
|
+
own.withSlot ?? false,
|
|
269
|
+
])
|
|
270
|
+
let group = watches.get(client)
|
|
271
|
+
if (!group) {
|
|
272
|
+
group = new Map()
|
|
273
|
+
watches.set(client, group)
|
|
274
|
+
}
|
|
275
|
+
let watch = group.get(key)
|
|
276
|
+
const watcher: Watcher = { interval, onChange, onError }
|
|
277
|
+
if (!watch) {
|
|
278
|
+
watch = { client, owner, mints, options: own, watchers: new Set([watcher]), timer: null, running: false, stopped: false, last: null, lastKey: null }
|
|
279
|
+
group.set(key, watch)
|
|
280
|
+
poll(watch)
|
|
281
|
+
} else {
|
|
282
|
+
watch.watchers.add(watcher)
|
|
283
|
+
const current = watch
|
|
284
|
+
if (current.last) {
|
|
285
|
+
const last = current.last
|
|
286
|
+
queueMicrotask(() => current.watchers.has(watcher) && safely(() => watcher.onChange(last, null)))
|
|
287
|
+
}
|
|
288
|
+
// 间隔变小:按新的间隔重新安排下一次
|
|
289
|
+
if (current.timer && interval < nextInterval(current, watcher)) {
|
|
290
|
+
clearTimeout(current.timer)
|
|
291
|
+
current.timer = null
|
|
292
|
+
schedule(current)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const owned = watch
|
|
296
|
+
const ownedGroup = group
|
|
297
|
+
return () => {
|
|
298
|
+
if (!owned.watchers.delete(watcher) || owned.watchers.size) {
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
owned.stopped = true
|
|
302
|
+
if (owned.timer) {
|
|
303
|
+
clearTimeout(owned.timer)
|
|
304
|
+
owned.timer = null
|
|
305
|
+
}
|
|
306
|
+
if (ownedGroup.get(key) === owned) {
|
|
307
|
+
ownedGroup.delete(key)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** 除了 except 以外订阅者的最小间隔(用于判断新订阅者是否让间隔变小) */
|
|
313
|
+
function nextInterval(watch: Watch, except?: Watcher): number {
|
|
314
|
+
let min = Number.POSITIVE_INFINITY
|
|
315
|
+
for (const w of watch.watchers) {
|
|
316
|
+
if (w !== except) min = Math.min(min, w.interval)
|
|
317
|
+
}
|
|
318
|
+
return min
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function schedule(watch: Watch): void {
|
|
322
|
+
if (watch.stopped || watch.timer || watch.running) {
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
watch.timer = setTimeout(() => poll(watch), nextInterval(watch))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function poll(watch: Watch): void {
|
|
329
|
+
watch.timer = null
|
|
330
|
+
if (watch.running || watch.stopped) {
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
watch.running = true
|
|
334
|
+
watch.client
|
|
335
|
+
.balances(watch.owner, watch.mints, watch.options)
|
|
336
|
+
.then(
|
|
337
|
+
(balances) => {
|
|
338
|
+
// 只比较余额(slot 每次都会变)
|
|
339
|
+
const key = JSON.stringify(balances.map((b) => [b.token, b.balance, b.success]))
|
|
340
|
+
if (watch.stopped || key === watch.lastKey) {
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
const previous = watch.last
|
|
344
|
+
watch.last = balances
|
|
345
|
+
watch.lastKey = key
|
|
346
|
+
for (const w of [...watch.watchers]) {
|
|
347
|
+
safely(() => w.onChange(balances, previous))
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
(err: unknown) => {
|
|
351
|
+
if (!watch.stopped) {
|
|
352
|
+
for (const w of [...watch.watchers]) {
|
|
353
|
+
safely(() => w.onError?.(err))
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
)
|
|
358
|
+
.finally(() => {
|
|
359
|
+
watch.running = false
|
|
360
|
+
schedule(watch)
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function safely(fn: () => void): void {
|
|
365
|
+
try {
|
|
366
|
+
fn()
|
|
367
|
+
} catch {
|
|
368
|
+
// 订阅者的回调出错不影响轮询
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
165
372
|
/** 测试用:按参数取(或创建)客户端,用于验证缓存复用(不在包的公开导出里) */
|
|
166
373
|
export function resolveClientForTest(options: ShortcutOptions): SolanaClient {
|
|
167
374
|
return resolve(options).client
|
package/src/source.ts
CHANGED
|
@@ -144,8 +144,6 @@ export function resolveSource(
|
|
|
144
144
|
// 内置节点本来就是按 cluster 选的,不校验;单个节点且没指定 cluster 时,期望值就是它自己,也不需要校验
|
|
145
145
|
const check = !isDefault && (multiple || cluster !== undefined)
|
|
146
146
|
const nodes = check ? raw.map((node) => new NetworkCheckedRpc(node, genesis)) : raw
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
genesis,
|
|
150
|
-
}
|
|
147
|
+
// 单个节点也包一层 FallbackRpc:统一 onRequest 事件和健康状态(单节点失败时抛原始错误)
|
|
148
|
+
return { transport: new FallbackRpc(nodes, options), genesis }
|
|
151
149
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
export type Commitment = 'processed' | 'confirmed' | 'finalized'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* tokenPrograms 参数的取值:简写 'spl'(SPL Token)/ 'token-2022',也可以直接传程序地址
|
|
5
|
+
* (TOKEN_PROGRAM_ID / TOKEN_2022_PROGRAM_ID)
|
|
6
|
+
*/
|
|
7
|
+
export type TokenProgramName = 'spl' | 'token-2022'
|
|
8
|
+
// (string & {}):保留 'spl' / 'token-2022' 的自动补全
|
|
9
|
+
export type TokenProgramOption = TokenProgramName | (string & {})
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 单项失败(success: false)的原因:
|
|
13
|
+
* - invalid-address:地址格式非法(不是 base58 的 32 字节)
|
|
14
|
+
* - not-found:地址合法,但链上没有这个账户(如 mint 不存在)
|
|
15
|
+
* - not-token:账户存在,但不是代币 mint(或代币账户)
|
|
16
|
+
* - no-metadata:代币存在,但没有元数据(nfts)
|
|
17
|
+
* - no-holder:没有持有人(nftOwners:供应量为 0 或已销毁)
|
|
18
|
+
* - missing-field:代币存在,但请求的字段读不到(tokens),见 errorField
|
|
19
|
+
*/
|
|
20
|
+
export type FailureReason = 'invalid-address' | 'not-found' | 'not-token' | 'no-metadata' | 'no-holder' | 'missing-field'
|
|
21
|
+
|
|
3
22
|
/** balances() 的单项结果,字段都是字符串 / 数字 / 布尔 / null,可以直接 JSON.stringify */
|
|
4
23
|
export interface TokenBalance {
|
|
5
24
|
/** 代币的 mint(原样) */
|
|
@@ -17,10 +36,14 @@ export interface TokenBalance {
|
|
|
17
36
|
symbol?: string | null
|
|
18
37
|
/** 查询失败(mint 不存在、地址非法等)时为 false,balance / formatted 为 "0" */
|
|
19
38
|
success: boolean
|
|
39
|
+
/** 失败原因,仅 success 为 false 时返回 */
|
|
40
|
+
error?: FailureReason
|
|
41
|
+
/** 传了 withSlot / minContextSlot 时返回:读取时节点的 slot(一次查询发了多个请求时取最小的) */
|
|
42
|
+
slot?: number
|
|
20
43
|
}
|
|
21
44
|
|
|
22
|
-
/** ownerTokens() / getOwnerTokens()
|
|
23
|
-
export interface OwnedToken extends Omit<TokenBalance, 'symbol'> {
|
|
45
|
+
/** ownerTokens() / getOwnerTokens() 的单项结果(总是 success: true) */
|
|
46
|
+
export interface OwnedToken extends Omit<TokenBalance, 'symbol' | 'error'> {
|
|
24
47
|
/** 该代币的代币账户数量;大于 1 时 balance 为所有账户的合计 */
|
|
25
48
|
accounts: number
|
|
26
49
|
/** metadata 为 true(默认)时返回;没有元数据或读取失败时为 null(见 metadataStatus) */
|
|
@@ -37,10 +60,33 @@ export interface SolBalance {
|
|
|
37
60
|
/** 按 9 位精度换算后的 SOL */
|
|
38
61
|
formatted: string
|
|
39
62
|
success: boolean
|
|
63
|
+
/** 失败原因(只有 invalid-address),仅 success 为 false 时返回 */
|
|
64
|
+
error?: FailureReason
|
|
65
|
+
/** 传了 withSlot / minContextSlot 时返回:读取时节点的 slot(多个请求时取最小的);没有发请求(地址全部非法)时不返回 */
|
|
66
|
+
slot?: number
|
|
40
67
|
}
|
|
41
68
|
|
|
42
69
|
/** tokens() / getTokens() 可选的字段 */
|
|
43
|
-
export type TokenField = 'name' | 'symbol' | 'uri' | 'decimals' | 'supply' | 'tokenProgram' | 'mintAuthority' | 'freezeAuthority'
|
|
70
|
+
export type TokenField = 'name' | 'symbol' | 'uri' | 'decimals' | 'supply' | 'tokenProgram' | 'mintAuthority' | 'freezeAuthority' | 'transferFee'
|
|
71
|
+
|
|
72
|
+
/** Token-2022 转账手续费的一套配置 */
|
|
73
|
+
export interface TransferFee {
|
|
74
|
+
/** 费率,万分比(100 = 1%) */
|
|
75
|
+
basisPoints: number
|
|
76
|
+
/** 单笔最多收取的手续费(最小单位的十进制字符串) */
|
|
77
|
+
maximumFee: string
|
|
78
|
+
/** 从这个 epoch 开始生效 */
|
|
79
|
+
epoch: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* tokens() 的 transferFee 字段:顶层是**当前 epoch** 生效的配置(按 getEpochInfo 选 newer / older),
|
|
84
|
+
* older / newer 是扩展里的两套原始配置。收到的数量 = 转账数量 - min(数量 × basisPoints / 10000 向上取整, maximumFee)
|
|
85
|
+
*/
|
|
86
|
+
export interface TransferFeeConfig extends TransferFee {
|
|
87
|
+
older: TransferFee
|
|
88
|
+
newer: TransferFee
|
|
89
|
+
}
|
|
44
90
|
|
|
45
91
|
export const DEFAULT_TOKEN_FIELDS = ['name', 'symbol', 'decimals'] as const satisfies readonly TokenField[]
|
|
46
92
|
|
|
@@ -58,6 +104,8 @@ interface TokenFieldTypes {
|
|
|
58
104
|
/** 没有 mint 权限(已放弃)时为 null */
|
|
59
105
|
mintAuthority: string | null
|
|
60
106
|
freezeAuthority: string | null
|
|
107
|
+
/** Token-2022 转账手续费;SPL Token、没有这个扩展、主币时为 null(不算失败) */
|
|
108
|
+
transferFee: TransferFeeConfig | null
|
|
61
109
|
}
|
|
62
110
|
|
|
63
111
|
/**
|
|
@@ -70,6 +118,10 @@ export type TokenDetails<F extends TokenField = DefaultTokenField> = {
|
|
|
70
118
|
native: boolean
|
|
71
119
|
/** mint 存在且请求的 name / symbol / uri / decimals / supply 都读到时为 true */
|
|
72
120
|
success: boolean
|
|
121
|
+
/** 失败原因,仅 success 为 false 时返回 */
|
|
122
|
+
error?: FailureReason
|
|
123
|
+
/** error 为 missing-field 时:第一个读不到的字段 */
|
|
124
|
+
errorField?: F
|
|
73
125
|
} & { [K in F]: TokenFieldTypes[K] } & ('supply' extends F ? { supplyFormatted: string | null } : unknown)
|
|
74
126
|
|
|
75
127
|
export type TokenStandard =
|
|
@@ -96,6 +148,8 @@ export interface NftDetails {
|
|
|
96
148
|
isMutable: boolean | null
|
|
97
149
|
updateAuthority: string | null
|
|
98
150
|
success: boolean
|
|
151
|
+
/** 失败原因,仅 success 为 false 时返回 */
|
|
152
|
+
error?: FailureReason
|
|
99
153
|
}
|
|
100
154
|
|
|
101
155
|
export interface NftOwner {
|
|
@@ -105,4 +159,6 @@ export interface NftOwner {
|
|
|
105
159
|
/** 持有该 NFT 的代币账户 */
|
|
106
160
|
tokenAccount: string | null
|
|
107
161
|
success: boolean
|
|
162
|
+
/** 失败原因,仅 success 为 false 时返回 */
|
|
163
|
+
error?: FailureReason
|
|
108
164
|
}
|