@w3lib/solana-batch-call 0.0.0-stage → 0.2.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 +35 -0
- package/LICENSE +21 -0
- package/README.md +270 -2
- package/dist/address.d.ts +20 -0
- package/dist/address.d.ts.map +1 -0
- package/dist/address.js +120 -0
- package/dist/address.js.map +1 -0
- package/dist/client.d.ts +190 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +971 -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 +84 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +237 -0
- package/dist/layout.js.map +1 -0
- package/dist/rpc.d.ts +115 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +458 -0
- package/dist/rpc.js.map +1 -0
- package/dist/shortcuts.d.ts +93 -0
- package/dist/shortcuts.d.ts.map +1 -0
- package/dist/shortcuts.js +281 -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 +124 -0
- package/dist/source.js.map +1 -0
- package/dist/types.d.ts +155 -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 +60 -4
- package/src/address.ts +134 -0
- package/src/client.ts +1298 -0
- package/src/constants.ts +41 -0
- package/src/index.ts +98 -0
- package/src/layout.ts +320 -0
- package/src/rpc.ts +549 -0
- package/src/shortcuts.ts +367 -0
- package/src/source.ts +149 -0
- package/src/types.ts +164 -0
package/src/constants.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111'
|
|
2
|
+
export const TOKEN_PROGRAM_ID = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
|
3
|
+
export const TOKEN_2022_PROGRAM_ID = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'
|
|
4
|
+
export const ASSOCIATED_TOKEN_PROGRAM_ID = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL'
|
|
5
|
+
export const METADATA_PROGRAM_ID = 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'
|
|
6
|
+
|
|
7
|
+
/** Wrapped SOL 的 mint,按惯例也用来表示原生 SOL */
|
|
8
|
+
export const NATIVE_MINT = 'So11111111111111111111111111111111111111112'
|
|
9
|
+
|
|
10
|
+
/** 原生 SOL 的精度(lamports) */
|
|
11
|
+
export const SOL_DECIMALS = 9
|
|
12
|
+
|
|
13
|
+
export type Cluster = 'mainnet' | 'devnet' | 'testnet'
|
|
14
|
+
|
|
15
|
+
/** 各网络的创世区块哈希(getGenesisHash),用于识别节点所在网络 */
|
|
16
|
+
export const GENESIS_HASHES: Readonly<Record<Cluster, string>> = {
|
|
17
|
+
mainnet: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d',
|
|
18
|
+
devnet: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG',
|
|
19
|
+
testnet: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function clusterOfGenesis(hash: string): Cluster | null {
|
|
23
|
+
for (const [cluster, genesis] of Object.entries(GENESIS_HASHES)) {
|
|
24
|
+
if (genesis === hash) {
|
|
25
|
+
return cluster as Cluster
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 内置公共节点(按优先级排序,2026-10 实测):
|
|
33
|
+
* - publicnode:放行跨域、支持批量请求,但不支持 getTokenAccountsByOwner 等需要索引的方法
|
|
34
|
+
* - 官方节点:对带浏览器 Origin 的请求返回 403,只能在服务端使用
|
|
35
|
+
* 公共节点限频严,生产环境建议传入自己的节点(Helius / QuickNode / Triton 等)。
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_RPC_URLS: Readonly<Record<Cluster, readonly string[]>> = {
|
|
38
|
+
mainnet: ['https://solana-rpc.publicnode.com', 'https://api.mainnet-beta.solana.com'],
|
|
39
|
+
devnet: ['https://api.devnet.solana.com'],
|
|
40
|
+
testnet: ['https://api.testnet.solana.com'],
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export {
|
|
2
|
+
SolanaClient,
|
|
3
|
+
exportTokenMetaCache,
|
|
4
|
+
importTokenMetaCache,
|
|
5
|
+
persistTokenMetaCache,
|
|
6
|
+
type AccountsOptions,
|
|
7
|
+
type BalanceQuery,
|
|
8
|
+
type BalancesOptions,
|
|
9
|
+
type ClientConfig,
|
|
10
|
+
type OwnerTokensOptions,
|
|
11
|
+
type PersistTokenMetaOptions,
|
|
12
|
+
type RequestOptions,
|
|
13
|
+
type SlotOptions,
|
|
14
|
+
type SolBalancesOptions,
|
|
15
|
+
type TokenMetaSnapshot,
|
|
16
|
+
type TokensOptions,
|
|
17
|
+
} from './client.js'
|
|
18
|
+
export {
|
|
19
|
+
getBalances,
|
|
20
|
+
getMultiBalances,
|
|
21
|
+
getNftOwners,
|
|
22
|
+
getNfts,
|
|
23
|
+
getOwnerNfts,
|
|
24
|
+
getOwnerTokens,
|
|
25
|
+
getSolBalances,
|
|
26
|
+
getTokens,
|
|
27
|
+
watchBalances,
|
|
28
|
+
type GetBalancesOptions,
|
|
29
|
+
type GetNftsOptions,
|
|
30
|
+
type GetOwnerTokensOptions,
|
|
31
|
+
type GetSolBalancesOptions,
|
|
32
|
+
type GetTokensOptions,
|
|
33
|
+
type WatchBalancesOptions,
|
|
34
|
+
type ShortcutOptions,
|
|
35
|
+
} from './shortcuts.js'
|
|
36
|
+
export {
|
|
37
|
+
DEFAULT_TOKEN_FIELDS,
|
|
38
|
+
type Commitment,
|
|
39
|
+
type DefaultTokenField,
|
|
40
|
+
type FailureReason,
|
|
41
|
+
type NftDetails,
|
|
42
|
+
type NftOwner,
|
|
43
|
+
type OwnedToken,
|
|
44
|
+
type SolBalance,
|
|
45
|
+
type TokenBalance,
|
|
46
|
+
type TokenDetails,
|
|
47
|
+
type TokenField,
|
|
48
|
+
type TokenProgramName,
|
|
49
|
+
type TokenProgramOption,
|
|
50
|
+
type TokenStandard,
|
|
51
|
+
type TransferFee,
|
|
52
|
+
type TransferFeeConfig,
|
|
53
|
+
} from './types.js'
|
|
54
|
+
export {
|
|
55
|
+
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
56
|
+
DEFAULT_RPC_URLS,
|
|
57
|
+
GENESIS_HASHES,
|
|
58
|
+
METADATA_PROGRAM_ID,
|
|
59
|
+
NATIVE_MINT,
|
|
60
|
+
SOL_DECIMALS,
|
|
61
|
+
SYSTEM_PROGRAM_ID,
|
|
62
|
+
TOKEN_2022_PROGRAM_ID,
|
|
63
|
+
TOKEN_PROGRAM_ID,
|
|
64
|
+
clusterOfGenesis,
|
|
65
|
+
type Cluster,
|
|
66
|
+
} from './constants.js'
|
|
67
|
+
export { decodeAddress, encodeAddress, findProgramAddress, getAssociatedTokenAddress, getMetadataAddress, isAddress, isOnCurve } from './address.js'
|
|
68
|
+
export {
|
|
69
|
+
formatAmount,
|
|
70
|
+
formatUnits,
|
|
71
|
+
parseMetaplexMetadata,
|
|
72
|
+
parseMint,
|
|
73
|
+
parseTokenAccount,
|
|
74
|
+
parseTransferFeeConfig,
|
|
75
|
+
type AccountInfo,
|
|
76
|
+
type MetaplexMetadata,
|
|
77
|
+
type MintInfo,
|
|
78
|
+
type TokenAccountInfo,
|
|
79
|
+
type TokenMetadata,
|
|
80
|
+
type TransferFeeConfigInfo,
|
|
81
|
+
type TransferFeeInfo,
|
|
82
|
+
} from './layout.js'
|
|
83
|
+
export {
|
|
84
|
+
AllNodesFailedError,
|
|
85
|
+
FallbackRpc,
|
|
86
|
+
HttpError,
|
|
87
|
+
HttpRpc,
|
|
88
|
+
MIN_CONTEXT_SLOT_NOT_REACHED,
|
|
89
|
+
RpcError,
|
|
90
|
+
isNodeFault,
|
|
91
|
+
nodeLabel,
|
|
92
|
+
onRequest,
|
|
93
|
+
type FallbackOptions,
|
|
94
|
+
type HttpRpcOptions,
|
|
95
|
+
type RequestEvent,
|
|
96
|
+
type RpcTransport,
|
|
97
|
+
} from './rpc.js'
|
|
98
|
+
export { NetworkCheckedRpc, NetworkMismatchError, type RpcSource, type SourceOptions } from './source.js'
|
package/src/layout.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { encodeAddress } from './address.js'
|
|
2
|
+
|
|
3
|
+
/** getMultipleAccounts 解析后的账户 */
|
|
4
|
+
export interface AccountInfo {
|
|
5
|
+
address: string
|
|
6
|
+
lamports: bigint
|
|
7
|
+
/** 所属程序 */
|
|
8
|
+
owner: string
|
|
9
|
+
data: Uint8Array
|
|
10
|
+
executable: boolean
|
|
11
|
+
}
|
|
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
|
+
export interface MintInfo {
|
|
88
|
+
mintAuthority: string | null
|
|
89
|
+
supply: bigint
|
|
90
|
+
decimals: number
|
|
91
|
+
isInitialized: boolean
|
|
92
|
+
freezeAuthority: string | null
|
|
93
|
+
/** Token-2022 TokenMetadata 扩展(如有) */
|
|
94
|
+
metadata: TokenMetadata | null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface TokenMetadata {
|
|
98
|
+
name: string
|
|
99
|
+
symbol: string
|
|
100
|
+
uri: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const MINT_SIZE = 82
|
|
104
|
+
const ACCOUNT_SIZE = 165
|
|
105
|
+
/** Token-2022 带扩展的账户:基础布局补齐到 165(Account 的大小),第 165 字节是账户类型,之后是扩展 */
|
|
106
|
+
const ACCOUNT_TYPE_OFFSET = 165
|
|
107
|
+
const EXTENSIONS_OFFSET = 166
|
|
108
|
+
const ACCOUNT_TYPE_MINT = 1
|
|
109
|
+
const ACCOUNT_TYPE_ACCOUNT = 2
|
|
110
|
+
const EXTENSION_TRANSFER_FEE_CONFIG = 1
|
|
111
|
+
const EXTENSION_TOKEN_METADATA = 19
|
|
112
|
+
|
|
113
|
+
/** 解析 Mint 账户(SPL Token / Token-2022 通用,Token-2022 额外解析 TokenMetadata 扩展) */
|
|
114
|
+
export function parseMint(data: Uint8Array): MintInfo {
|
|
115
|
+
// 只接受真正的 mint:SPL Token 恰好 82 字节;Token-2022 带扩展时账户类型必须是 Mint(避免把代币账户 / 多签账户当成 mint)
|
|
116
|
+
if (data.length !== MINT_SIZE && !(data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_MINT)) {
|
|
117
|
+
throw new RangeError('Not a mint account')
|
|
118
|
+
}
|
|
119
|
+
const r = new Reader(data)
|
|
120
|
+
const hasMintAuthority = r.u32() === 1
|
|
121
|
+
const mintAuthority = r.pubkey()
|
|
122
|
+
const supply = r.u64()
|
|
123
|
+
const decimals = r.u8()
|
|
124
|
+
const isInitialized = r.u8() === 1
|
|
125
|
+
const hasFreezeAuthority = r.u32() === 1
|
|
126
|
+
const freezeAuthority = r.pubkey()
|
|
127
|
+
return {
|
|
128
|
+
mintAuthority: hasMintAuthority ? mintAuthority : null,
|
|
129
|
+
supply,
|
|
130
|
+
decimals,
|
|
131
|
+
isInitialized,
|
|
132
|
+
freezeAuthority: hasFreezeAuthority ? freezeAuthority : null,
|
|
133
|
+
metadata: data.length > EXTENSIONS_OFFSET ? parseTokenMetadataExtension(data) : null,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 在 Token-2022 账户的扩展(TLV:u16 类型 + u16 长度 + 数据)里找指定类型,返回其数据;没有或数据不完整时为 null */
|
|
138
|
+
function findExtension(data: Uint8Array, extensionType: number): Uint8Array | null {
|
|
139
|
+
if (data.length <= EXTENSIONS_OFFSET) {
|
|
140
|
+
return null
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const r = new Reader(data, EXTENSIONS_OFFSET)
|
|
144
|
+
while (r.offset + 4 <= data.length) {
|
|
145
|
+
const type = r.u16()
|
|
146
|
+
const length = r.u16()
|
|
147
|
+
if (type === 0 && length === 0) {
|
|
148
|
+
return null // 未初始化的填充
|
|
149
|
+
}
|
|
150
|
+
if (type === extensionType) {
|
|
151
|
+
if (r.offset + length > data.length) {
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
154
|
+
return data.slice(r.offset, r.offset + length)
|
|
155
|
+
}
|
|
156
|
+
r.skip(length)
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// 扩展数据不完整:当作没有
|
|
160
|
+
}
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function parseTokenMetadataExtension(data: Uint8Array): TokenMetadata | null {
|
|
165
|
+
const ext = findExtension(data, EXTENSION_TOKEN_METADATA)
|
|
166
|
+
if (!ext) {
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const m = new Reader(ext)
|
|
171
|
+
m.pubkey() // updateAuthority
|
|
172
|
+
m.pubkey() // mint
|
|
173
|
+
return { name: m.string(), symbol: m.string(), uri: m.string() }
|
|
174
|
+
} catch {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Token-2022 转账手续费的一套配置:从 epoch 开始生效,费率 basisPoints(万分比),单笔最多 maximumFee(最小单位) */
|
|
180
|
+
export interface TransferFeeInfo {
|
|
181
|
+
epoch: bigint
|
|
182
|
+
maximumFee: bigint
|
|
183
|
+
basisPoints: number
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Token-2022 TransferFeeConfig 扩展 */
|
|
187
|
+
export interface TransferFeeConfigInfo {
|
|
188
|
+
/** 可以修改手续费的地址;null 表示不能再改 */
|
|
189
|
+
transferFeeConfigAuthority: string | null
|
|
190
|
+
withdrawWithheldAuthority: string | null
|
|
191
|
+
/** mint 上暂存的、已收取未提取的手续费 */
|
|
192
|
+
withheldAmount: bigint
|
|
193
|
+
/** 旧配置:当前 epoch < newer.epoch 时生效 */
|
|
194
|
+
older: TransferFeeInfo
|
|
195
|
+
/** 新配置:从 newer.epoch 起生效 */
|
|
196
|
+
newer: TransferFeeInfo
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const ZERO_PUBKEY = '11111111111111111111111111111111'
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 解析 mint 账户里的 Token-2022 TransferFeeConfig 扩展(类型 1,108 字节);
|
|
203
|
+
* SPL Token、没有这个扩展或数据不完整时为 null。
|
|
204
|
+
* 当前生效的是 `epoch >= newer.epoch ? newer : older`
|
|
205
|
+
*/
|
|
206
|
+
export function parseTransferFeeConfig(data: Uint8Array): TransferFeeConfigInfo | null {
|
|
207
|
+
if (!(data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_MINT)) {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
const ext = findExtension(data, EXTENSION_TRANSFER_FEE_CONFIG)
|
|
211
|
+
if (!ext) {
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
const r = new Reader(ext)
|
|
216
|
+
// OptionalNonZeroPubkey:全 0 表示没有
|
|
217
|
+
const configAuthority = r.pubkey()
|
|
218
|
+
const withdrawAuthority = r.pubkey()
|
|
219
|
+
const withheldAmount = r.u64()
|
|
220
|
+
const fee = (): TransferFeeInfo => ({ epoch: r.u64(), maximumFee: r.u64(), basisPoints: r.u16() })
|
|
221
|
+
const older = fee()
|
|
222
|
+
const newer = fee()
|
|
223
|
+
return {
|
|
224
|
+
transferFeeConfigAuthority: configAuthority === ZERO_PUBKEY ? null : configAuthority,
|
|
225
|
+
withdrawWithheldAuthority: withdrawAuthority === ZERO_PUBKEY ? null : withdrawAuthority,
|
|
226
|
+
withheldAmount,
|
|
227
|
+
older,
|
|
228
|
+
newer,
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface TokenAccountInfo {
|
|
236
|
+
mint: string
|
|
237
|
+
owner: string
|
|
238
|
+
amount: bigint
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 解析代币账户(SPL Token / Token-2022 前 72 字节相同) */
|
|
242
|
+
export function parseTokenAccount(data: Uint8Array): TokenAccountInfo {
|
|
243
|
+
if (data.length !== ACCOUNT_SIZE && !(data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_ACCOUNT)) {
|
|
244
|
+
throw new RangeError('Not a token account')
|
|
245
|
+
}
|
|
246
|
+
const r = new Reader(data)
|
|
247
|
+
return { mint: r.pubkey(), owner: r.pubkey(), amount: r.u64() }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export interface MetaplexMetadata extends TokenMetadata {
|
|
251
|
+
updateAuthority: string
|
|
252
|
+
mint: string
|
|
253
|
+
sellerFeeBasisPoints: number
|
|
254
|
+
creators: Array<{ address: string; verified: boolean; share: number }>
|
|
255
|
+
primarySaleHappened: boolean
|
|
256
|
+
isMutable: boolean
|
|
257
|
+
/** 0 NonFungible,1 FungibleAsset,2 Fungible,3 NonFungibleEdition,4 ProgrammableNonFungible,5 ProgrammableNonFungibleEdition */
|
|
258
|
+
tokenStandard: number | null
|
|
259
|
+
collection: { address: string; verified: boolean } | null
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 解析 Metaplex Token Metadata 账户;老版本账户缺少后面的字段时,缺的部分为默认值 */
|
|
263
|
+
export function parseMetaplexMetadata(data: Uint8Array): MetaplexMetadata {
|
|
264
|
+
const r = new Reader(data)
|
|
265
|
+
if (r.u8() !== 4) {
|
|
266
|
+
throw new RangeError('Not a Metaplex metadata account') // Key::MetadataV1 = 4
|
|
267
|
+
}
|
|
268
|
+
const updateAuthority = r.pubkey()
|
|
269
|
+
const mint = r.pubkey()
|
|
270
|
+
const name = r.string()
|
|
271
|
+
const symbol = r.string()
|
|
272
|
+
const uri = r.string()
|
|
273
|
+
const sellerFeeBasisPoints = r.u16()
|
|
274
|
+
const creators =
|
|
275
|
+
r.option(() => {
|
|
276
|
+
const count = r.u32()
|
|
277
|
+
return Array.from({ length: count }, () => ({ address: r.pubkey(), verified: r.u8() === 1, share: r.u8() }))
|
|
278
|
+
}) ?? []
|
|
279
|
+
const result: MetaplexMetadata = {
|
|
280
|
+
updateAuthority,
|
|
281
|
+
mint,
|
|
282
|
+
name,
|
|
283
|
+
symbol,
|
|
284
|
+
uri,
|
|
285
|
+
sellerFeeBasisPoints,
|
|
286
|
+
creators,
|
|
287
|
+
primarySaleHappened: false,
|
|
288
|
+
isMutable: false,
|
|
289
|
+
tokenStandard: null,
|
|
290
|
+
collection: null,
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
result.primarySaleHappened = r.u8() === 1
|
|
294
|
+
result.isMutable = r.u8() === 1
|
|
295
|
+
r.option(() => r.u8()) // editionNonce
|
|
296
|
+
result.tokenStandard = r.option(() => r.u8())
|
|
297
|
+
result.collection = r.option(() => {
|
|
298
|
+
const verified = r.u8() === 1
|
|
299
|
+
return { address: r.pubkey(), verified }
|
|
300
|
+
})
|
|
301
|
+
} catch {
|
|
302
|
+
// 老版本账户没有这些字段
|
|
303
|
+
}
|
|
304
|
+
return result
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** 按精度格式化(与 ethers 的 formatUnits 同名同义):formatUnits(1_500_000_000n, 9) === '1.5';不四舍五入,整数不带小数点 */
|
|
308
|
+
export function formatUnits(value: bigint, decimals: number): string {
|
|
309
|
+
const negative = value < 0n
|
|
310
|
+
const digits = (negative ? -value : value).toString().padStart(decimals + 1, '0')
|
|
311
|
+
const whole = digits.slice(0, digits.length - decimals)
|
|
312
|
+
const fraction = digits.slice(digits.length - decimals).replace(/0+$/, '')
|
|
313
|
+
return `${negative ? '-' : ''}${whole}${fraction ? `.${fraction}` : ''}`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 同 formatUnits。
|
|
318
|
+
* @deprecated 改用 formatUnits:formatAmount 这个名字容易和项目里的数字展示函数撞名
|
|
319
|
+
*/
|
|
320
|
+
export const formatAmount = formatUnits
|