@w3lib/solana-batch-call 0.0.0-stage → 0.1.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 +12 -0
- package/LICENSE +21 -0
- package/README.md +169 -2
- package/dist/address.d.ts +14 -0
- package/dist/address.d.ts.map +1 -0
- package/dist/address.js +81 -0
- package/dist/address.js.map +1 -0
- package/dist/client.d.ts +96 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +575 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +35 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +55 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +177 -0
- package/dist/layout.js.map +1 -0
- package/dist/rpc.d.ts +59 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +295 -0
- package/dist/rpc.js.map +1 -0
- package/dist/shortcuts.d.ts +60 -0
- package/dist/shortcuts.d.ts.map +1 -0
- package/dist/shortcuts.js +136 -0
- package/dist/shortcuts.js.map +1 -0
- package/dist/source.d.ts +35 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +126 -0
- package/dist/source.js.map +1 -0
- package/dist/types.d.ts +104 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -3
- package/src/address.ts +92 -0
- package/src/client.ts +753 -0
- package/src/constants.ts +41 -0
- package/src/index.ts +54 -0
- package/src/layout.ts +238 -0
- package/src/rpc.ts +349 -0
- package/src/shortcuts.ts +173 -0
- package/src/source.ts +151 -0
- package/src/types.ts +108 -0
package/src/shortcuts.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { SolanaClient, type BalancesOptions, type ClientConfig, type OwnerTokensOptions, type TokensOptions } from './client.js'
|
|
2
|
+
import type { RpcSource } from './source.js'
|
|
3
|
+
import type { DefaultTokenField, NftDetails, NftOwner, OwnedToken, SolBalance, TokenBalance, TokenDetails, TokenField } from './types.js'
|
|
4
|
+
|
|
5
|
+
/** 各函数共用的节点参数 */
|
|
6
|
+
export interface ShortcutOptions extends ClientConfig {
|
|
7
|
+
/**
|
|
8
|
+
* 节点:RPC URL、URL 数组(主节点 + 备用节点)、web3.js Connection、自定义传输(request(method, params))。
|
|
9
|
+
* 不传则使用 cluster(默认 mainnet)的内置公共节点
|
|
10
|
+
*/
|
|
11
|
+
provider?: RpcSource | readonly RpcSource[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 复用客户端:多次调用共享连接、批量合并与故障切换状态。按 “配置内容” 分组,组内 URL 按字符串、对象按引用(WeakMap)。
|
|
15
|
+
// 配置里有函数(如自定义 fetch)或节点数组含对象时不缓存。
|
|
16
|
+
const MAX_CACHED = 32
|
|
17
|
+
|
|
18
|
+
interface ClientGroup {
|
|
19
|
+
byString: Map<string, SolanaClient>
|
|
20
|
+
byObject: WeakMap<object, SolanaClient>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const clientCache = new Map<string, ClientGroup>()
|
|
24
|
+
|
|
25
|
+
function setBounded<K, V>(map: Map<K, V>, key: K, value: V): void {
|
|
26
|
+
map.set(key, value)
|
|
27
|
+
if (map.size > MAX_CACHED) {
|
|
28
|
+
map.delete(map.keys().next().value as K)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function configKey(config: ClientConfig): string | null {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(config, (_key, value: unknown) => {
|
|
35
|
+
if (typeof value === 'function') {
|
|
36
|
+
throw new Error('not comparable')
|
|
37
|
+
}
|
|
38
|
+
return value
|
|
39
|
+
})
|
|
40
|
+
} catch {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sourceKey(source: ShortcutOptions['provider']): string | object | null {
|
|
46
|
+
if (source === undefined) {
|
|
47
|
+
return 'default'
|
|
48
|
+
}
|
|
49
|
+
if (typeof source === 'string') {
|
|
50
|
+
return `url:${source}`
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(source)) {
|
|
53
|
+
return source.every((item) => typeof item === 'string') ? `urls:${source.join('\n')}` : null
|
|
54
|
+
}
|
|
55
|
+
return source as object
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 拆出节点参数和函数自己的参数(ownKeys),其余作为客户端配置;值为 undefined 的参数忽略 */
|
|
59
|
+
function resolve<T extends ShortcutOptions, K extends keyof T & string = never>(
|
|
60
|
+
options: T,
|
|
61
|
+
ownKeys: readonly K[] = [],
|
|
62
|
+
): { client: SolanaClient; own: Pick<T, K> } {
|
|
63
|
+
const { provider, ...others } = options
|
|
64
|
+
const own: Record<string, unknown> = {}
|
|
65
|
+
const config: Record<string, unknown> = {}
|
|
66
|
+
for (const [key, value] of Object.entries(others)) {
|
|
67
|
+
if (value !== undefined) {
|
|
68
|
+
;((ownKeys as readonly string[]).includes(key) ? own : config)[key] = value
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { client: getClient(provider, config as ClientConfig), own: own as Pick<T, K> }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getClient(source: ShortcutOptions['provider'], config: ClientConfig): SolanaClient {
|
|
75
|
+
const cfg = configKey(config)
|
|
76
|
+
const key = sourceKey(source)
|
|
77
|
+
if (cfg === null || key === null) {
|
|
78
|
+
return new SolanaClient(source, config)
|
|
79
|
+
}
|
|
80
|
+
let group = clientCache.get(cfg)
|
|
81
|
+
if (!group) {
|
|
82
|
+
group = { byString: new Map(), byObject: new WeakMap() }
|
|
83
|
+
setBounded(clientCache, cfg, group)
|
|
84
|
+
}
|
|
85
|
+
let client = typeof key === 'string' ? group.byString.get(key) : group.byObject.get(key)
|
|
86
|
+
if (!client) {
|
|
87
|
+
client = new SolanaClient(source, config)
|
|
88
|
+
if (typeof key === 'string') {
|
|
89
|
+
setBounded(group.byString, key, client)
|
|
90
|
+
} else {
|
|
91
|
+
group.byObject.set(key, client)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return client
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface GetBalancesOptions extends BalancesOptions, ShortcutOptions {}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 批量查余额(SOL + SPL Token + Token-2022),返回原始余额、decimals 和换算后的数值。
|
|
101
|
+
*
|
|
102
|
+
* ```ts
|
|
103
|
+
* import { NATIVE_MINT, getBalances } from '@w3lib/solana-batch-call'
|
|
104
|
+
*
|
|
105
|
+
* await getBalances(owner, [NATIVE_MINT, USDC]) // 指定代币:免费公共节点也能用
|
|
106
|
+
* await getBalances(owner, [NATIVE_MINT, USDC], { symbol: true })
|
|
107
|
+
* await getBalances(owner, undefined, { provider: 'https://my-rpc…' }) // 全部持仓:需要支持 getTokenAccountsByOwner 的节点
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export function getBalances(owner: string, mints?: readonly string[], options: GetBalancesOptions = {}): Promise<TokenBalance[]> {
|
|
111
|
+
const { client, own } = resolve(options, ['symbol', 'scan'])
|
|
112
|
+
return client.balances(owner, mints, own)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 批量查多个地址的 SOL 余额(一次 getMultipleAccounts) */
|
|
116
|
+
export function getSolBalances(addresses: readonly string[], options: ShortcutOptions = {}): Promise<SolBalance[]> {
|
|
117
|
+
return resolve(options).client.solBalances(addresses)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface GetTokensOptions<F extends TokenField = DefaultTokenField> extends TokensOptions<F>, ShortcutOptions {}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 批量查代币详情,字段可选(默认 name / symbol / decimals)。
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* await getTokens([USDC, PYUSD], { fields: ['name', 'symbol', 'decimals', 'supply', 'tokenProgram'] })
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
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, { fields: own.fields })
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 批量查 NFT 元数据(名称、uri、所属集合、版税、创作者等) */
|
|
135
|
+
export function getNfts(mints: readonly string[], options: ShortcutOptions = {}): Promise<NftDetails[]> {
|
|
136
|
+
return resolve(options).client.nfts(mints)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 批量查 NFT 持有人 */
|
|
140
|
+
export function getNftOwners(mints: readonly string[], options: ShortcutOptions = {}): Promise<NftOwner[]> {
|
|
141
|
+
return resolve(options).client.nftOwners(mints)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 查某地址持有的全部 NFT(需要支持 getTokenAccountsByOwner 的节点) */
|
|
145
|
+
export function getOwnerNfts(owner: string, options: ShortcutOptions = {}): Promise<NftDetails[]> {
|
|
146
|
+
return resolve(options).client.ownerNfts(owner)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface GetOwnerTokensOptions extends OwnerTokensOptions, ShortcutOptions {}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 列出持有人拥有的全部代币(SOL 在第一位),带 name / symbol,适合做资产列表。
|
|
153
|
+
* 需要支持 getTokenAccountsByOwner 的节点。
|
|
154
|
+
*
|
|
155
|
+
* ```ts
|
|
156
|
+
* await getOwnerTokens(owner, { provider: 'https://my-rpc…' })
|
|
157
|
+
* // [{ token: NATIVE_MINT, native: true, name: 'Solana', symbol: 'SOL', formatted: '1.5', … }, { token: USDC, name: 'USD Coin', … }]
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function getOwnerTokens(owner: string, options: GetOwnerTokensOptions = {}): Promise<OwnedToken[]> {
|
|
161
|
+
const { client, own } = resolve(options, ['metadata', 'includeNative', 'includeZero', 'includeNfts'])
|
|
162
|
+
return client.ownerTokens(owner, own)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 测试用:按参数取(或创建)客户端,用于验证缓存复用(不在包的公开导出里) */
|
|
166
|
+
export function resolveClientForTest(options: ShortcutOptions): SolanaClient {
|
|
167
|
+
return resolve(options).client
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 测试用:清空客户端缓存 */
|
|
171
|
+
export function resetClientCache(): void {
|
|
172
|
+
clientCache.clear()
|
|
173
|
+
}
|
package/src/source.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
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'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 节点来源(也可以传数组:主节点 + 备用节点):
|
|
6
|
+
* - RPC URL 字符串
|
|
7
|
+
* - @solana/web3.js 的 Connection(使用其 rpcEndpoint)
|
|
8
|
+
* - 自定义传输:实现 `request(method, params)` 的对象(如包一层 @solana/kit 的 rpc)
|
|
9
|
+
*/
|
|
10
|
+
export type RpcSource = string | RpcTransport | { rpcEndpoint: string }
|
|
11
|
+
|
|
12
|
+
export interface SourceOptions extends HttpRpcOptions, FallbackOptions {}
|
|
13
|
+
|
|
14
|
+
/** @param failFast 多个自定义节点时为 true:不在单节点内重试 429,直接切到下一个节点 */
|
|
15
|
+
function toTransport(source: RpcSource, options: SourceOptions, failFast: boolean): RpcTransport {
|
|
16
|
+
if (typeof source === 'string') {
|
|
17
|
+
return new HttpRpc(source, { ...options, retries: failFast ? 0 : options.retries })
|
|
18
|
+
}
|
|
19
|
+
if (typeof (source as RpcTransport).request === 'function') {
|
|
20
|
+
return source as RpcTransport
|
|
21
|
+
}
|
|
22
|
+
const endpoint = (source as { rpcEndpoint?: unknown }).rpcEndpoint
|
|
23
|
+
if (typeof endpoint === 'string') {
|
|
24
|
+
return new HttpRpc(endpoint, { ...options, retries: failFast ? 0 : options.retries })
|
|
25
|
+
}
|
|
26
|
+
throw new Error('Unsupported RPC source: expected a URL, a web3.js Connection or an object with request(method, params)')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 各节点的创世区块哈希缓存:失败(含超时)时删除,下次重试
|
|
30
|
+
const genesisCache = new WeakMap<RpcTransport, Promise<string>>()
|
|
31
|
+
|
|
32
|
+
export function genesisOf(transport: RpcTransport): Promise<string> {
|
|
33
|
+
let cached = genesisCache.get(transport)
|
|
34
|
+
if (!cached) {
|
|
35
|
+
const promise = transport.request<string>('getGenesisHash')
|
|
36
|
+
genesisCache.set(transport, promise)
|
|
37
|
+
promise.catch(() => {
|
|
38
|
+
if (genesisCache.get(transport) === promise) {
|
|
39
|
+
genesisCache.delete(transport)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
cached = promise
|
|
43
|
+
}
|
|
44
|
+
return cached
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 识别失败后多久内不再重试(期间请求照常发出,不做校验) */
|
|
48
|
+
const CHECK_RETRY_INTERVAL = 60_000
|
|
49
|
+
|
|
50
|
+
export class NetworkMismatchError extends Error {
|
|
51
|
+
constructor(expected: string, actual: string) {
|
|
52
|
+
super(`Network mismatch: expected genesis ${expected}, but the node is on ${actual}`)
|
|
53
|
+
this.name = 'NetworkMismatchError'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 校验节点所在的网络(创世区块哈希),避免把 devnet 的数据当成 mainnet 的返回。
|
|
59
|
+
* 校验与请求并行发出,不增加延迟;结果缓存,每个节点只多一次请求;识别失败时不拦截,并在一段时间内不再重试。
|
|
60
|
+
*/
|
|
61
|
+
export class NetworkCheckedRpc implements RpcTransport {
|
|
62
|
+
#checkFailedAt = 0
|
|
63
|
+
|
|
64
|
+
constructor(
|
|
65
|
+
readonly inner: RpcTransport,
|
|
66
|
+
readonly expected: () => Promise<string>,
|
|
67
|
+
) {}
|
|
68
|
+
|
|
69
|
+
async #check(): Promise<void> {
|
|
70
|
+
if (this.#checkFailedAt && Date.now() - this.#checkFailedAt < CHECK_RETRY_INTERVAL) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
let expected: string
|
|
74
|
+
let actual: string
|
|
75
|
+
try {
|
|
76
|
+
;[expected, actual] = await Promise.all([this.expected(), genesisOf(this.inner)])
|
|
77
|
+
} catch {
|
|
78
|
+
this.#checkFailedAt = Date.now()
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
this.#checkFailedAt = 0
|
|
82
|
+
if (expected !== actual) {
|
|
83
|
+
throw new NetworkMismatchError(expected, actual)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
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)])
|
|
89
|
+
if (checked.status === 'rejected') {
|
|
90
|
+
throw checked.reason
|
|
91
|
+
}
|
|
92
|
+
if (result.status === 'rejected') {
|
|
93
|
+
throw result.reason
|
|
94
|
+
}
|
|
95
|
+
return result.value
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ResolvedSource {
|
|
100
|
+
transport: RpcTransport
|
|
101
|
+
/** 期望的创世区块哈希:指定了 cluster 时取对应值,否则取第一个响应节点的 */
|
|
102
|
+
genesis: () => Promise<string>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function resolveSource(
|
|
106
|
+
source: RpcSource | readonly RpcSource[] | undefined,
|
|
107
|
+
cluster: Cluster | undefined,
|
|
108
|
+
options: SourceOptions = {},
|
|
109
|
+
): ResolvedSource {
|
|
110
|
+
const isDefault = source === undefined
|
|
111
|
+
const list: readonly RpcSource[] = isDefault ? DEFAULT_RPC_URLS[cluster ?? 'mainnet'] : Array.isArray(source) ? source : [source as RpcSource]
|
|
112
|
+
if (!list.length) {
|
|
113
|
+
throw new Error('RPC source list is empty')
|
|
114
|
+
}
|
|
115
|
+
const multiple = list.length > 1
|
|
116
|
+
// 内置节点保留 429 重试:浏览器里官方节点总是 403,实际只有 publicnode 一个可用节点
|
|
117
|
+
const raw = list.map((item) => toTransport(item, options, multiple && !isDefault))
|
|
118
|
+
|
|
119
|
+
let detecting: Promise<string> | null = null
|
|
120
|
+
const genesis = (): Promise<string> => {
|
|
121
|
+
if (cluster) {
|
|
122
|
+
return Promise.resolve(GENESIS_HASHES[cluster])
|
|
123
|
+
}
|
|
124
|
+
if (!detecting) {
|
|
125
|
+
const promise = (async () => {
|
|
126
|
+
let lastError: unknown
|
|
127
|
+
for (const node of raw) {
|
|
128
|
+
try {
|
|
129
|
+
return await genesisOf(node)
|
|
130
|
+
} catch (err) {
|
|
131
|
+
lastError = err
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw lastError
|
|
135
|
+
})()
|
|
136
|
+
detecting = promise
|
|
137
|
+
promise.catch(() => {
|
|
138
|
+
if (detecting === promise) detecting = null
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
return detecting
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 内置节点本来就是按 cluster 选的,不校验;单个节点且没指定 cluster 时,期望值就是它自己,也不需要校验
|
|
145
|
+
const check = !isDefault && (multiple || cluster !== undefined)
|
|
146
|
+
const nodes = check ? raw.map((node) => new NetworkCheckedRpc(node, genesis)) : raw
|
|
147
|
+
return {
|
|
148
|
+
transport: multiple ? new FallbackRpc(nodes, options) : (nodes[0] as RpcTransport),
|
|
149
|
+
genesis,
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export type Commitment = 'processed' | 'confirmed' | 'finalized'
|
|
2
|
+
|
|
3
|
+
/** balances() 的单项结果,字段都是字符串 / 数字 / 布尔 / null,可以直接 JSON.stringify */
|
|
4
|
+
export interface TokenBalance {
|
|
5
|
+
/** 代币的 mint(原样) */
|
|
6
|
+
token: string
|
|
7
|
+
/** 是否按原生 SOL 查询 */
|
|
8
|
+
native: boolean
|
|
9
|
+
/** 余额(最小单位的十进制字符串);需要计算时用 BigInt(balance) */
|
|
10
|
+
balance: string
|
|
11
|
+
decimals: number
|
|
12
|
+
/** 按 decimals 换算后的十进制字符串,如 "1234.5";整数不带小数点 */
|
|
13
|
+
formatted: string
|
|
14
|
+
/** 代币所属程序(SPL Token / Token-2022);主币或未知时为 null */
|
|
15
|
+
tokenProgram: string | null
|
|
16
|
+
/** 仅在 symbol: true 时返回;读不到时为 null */
|
|
17
|
+
symbol?: string | null
|
|
18
|
+
/** 查询失败(mint 不存在、地址非法等)时为 false,balance / formatted 为 "0" */
|
|
19
|
+
success: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** ownerTokens() / getOwnerTokens() 的单项结果 */
|
|
23
|
+
export interface OwnedToken extends Omit<TokenBalance, 'symbol'> {
|
|
24
|
+
/** 该代币的代币账户数量;大于 1 时 balance 为所有账户的合计 */
|
|
25
|
+
accounts: number
|
|
26
|
+
/** metadata 为 true(默认)时返回;没有元数据或读取失败时为 null(见 metadataStatus) */
|
|
27
|
+
name?: string | null
|
|
28
|
+
symbol?: string | null
|
|
29
|
+
/** metadata 为 true 时返回:ok 读到了;missing 确认没有元数据;failed 节点问题没能读到(可以稍后单独用 getTokens 重查) */
|
|
30
|
+
metadataStatus?: 'ok' | 'missing' | 'failed'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SolBalance {
|
|
34
|
+
address: string
|
|
35
|
+
/** lamports(十进制字符串) */
|
|
36
|
+
balance: string
|
|
37
|
+
/** 按 9 位精度换算后的 SOL */
|
|
38
|
+
formatted: string
|
|
39
|
+
success: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** tokens() / getTokens() 可选的字段 */
|
|
43
|
+
export type TokenField = 'name' | 'symbol' | 'uri' | 'decimals' | 'supply' | 'tokenProgram' | 'mintAuthority' | 'freezeAuthority'
|
|
44
|
+
|
|
45
|
+
export const DEFAULT_TOKEN_FIELDS = ['name', 'symbol', 'decimals'] as const satisfies readonly TokenField[]
|
|
46
|
+
|
|
47
|
+
export type DefaultTokenField = (typeof DEFAULT_TOKEN_FIELDS)[number]
|
|
48
|
+
|
|
49
|
+
interface TokenFieldTypes {
|
|
50
|
+
/** 优先取 Token-2022 TokenMetadata 扩展,其次 Metaplex 元数据 */
|
|
51
|
+
name: string | null
|
|
52
|
+
symbol: string | null
|
|
53
|
+
uri: string | null
|
|
54
|
+
decimals: number | null
|
|
55
|
+
/** 最小单位的十进制字符串 */
|
|
56
|
+
supply: string | null
|
|
57
|
+
tokenProgram: string | null
|
|
58
|
+
/** 没有 mint 权限(已放弃)时为 null */
|
|
59
|
+
mintAuthority: string | null
|
|
60
|
+
freezeAuthority: string | null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* tokens() / getTokens() 的单项结果,只包含请求的字段。
|
|
65
|
+
* 请求 supply 时额外带 supplyFormatted(按 decimals 换算)。
|
|
66
|
+
*/
|
|
67
|
+
export type TokenDetails<F extends TokenField = DefaultTokenField> = {
|
|
68
|
+
/** mint 地址 */
|
|
69
|
+
address: string
|
|
70
|
+
native: boolean
|
|
71
|
+
/** mint 存在且请求的 name / symbol / uri / decimals / supply 都读到时为 true */
|
|
72
|
+
success: boolean
|
|
73
|
+
} & { [K in F]: TokenFieldTypes[K] } & ('supply' extends F ? { supplyFormatted: string | null } : unknown)
|
|
74
|
+
|
|
75
|
+
export type TokenStandard =
|
|
76
|
+
| 'NonFungible'
|
|
77
|
+
| 'FungibleAsset'
|
|
78
|
+
| 'Fungible'
|
|
79
|
+
| 'NonFungibleEdition'
|
|
80
|
+
| 'ProgrammableNonFungible'
|
|
81
|
+
| 'ProgrammableNonFungibleEdition'
|
|
82
|
+
|
|
83
|
+
/** nfts() / getNfts() 的单项结果(Metaplex 元数据;Token-2022 NFT 只有 name / symbol / uri) */
|
|
84
|
+
export interface NftDetails {
|
|
85
|
+
mint: string
|
|
86
|
+
name: string | null
|
|
87
|
+
symbol: string | null
|
|
88
|
+
/** 链下元数据 JSON 的地址 */
|
|
89
|
+
uri: string | null
|
|
90
|
+
/** 所属集合;verified 为 false 表示未经集合方签名确认 */
|
|
91
|
+
collection: { address: string; verified: boolean } | null
|
|
92
|
+
creators: Array<{ address: string; verified: boolean; share: number }>
|
|
93
|
+
/** 版税,万分比 */
|
|
94
|
+
sellerFeeBasisPoints: number | null
|
|
95
|
+
tokenStandard: TokenStandard | null
|
|
96
|
+
isMutable: boolean | null
|
|
97
|
+
updateAuthority: string | null
|
|
98
|
+
success: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface NftOwner {
|
|
102
|
+
mint: string
|
|
103
|
+
/** 持有人钱包地址 */
|
|
104
|
+
owner: string | null
|
|
105
|
+
/** 持有该 NFT 的代币账户 */
|
|
106
|
+
tokenAccount: string | null
|
|
107
|
+
success: boolean
|
|
108
|
+
}
|