@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/dist/source.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { DEFAULT_RPC_URLS, GENESIS_HASHES } from './constants.js';
|
|
2
|
+
import { FallbackRpc, HttpRpc } from './rpc.js';
|
|
3
|
+
/** @param failFast 多个自定义节点时为 true:不在单节点内重试 429,直接切到下一个节点 */
|
|
4
|
+
function toTransport(source, options, failFast) {
|
|
5
|
+
if (typeof source === 'string') {
|
|
6
|
+
return new HttpRpc(source, { ...options, retries: failFast ? 0 : options.retries });
|
|
7
|
+
}
|
|
8
|
+
if (typeof source.request === 'function') {
|
|
9
|
+
return source;
|
|
10
|
+
}
|
|
11
|
+
const endpoint = source.rpcEndpoint;
|
|
12
|
+
if (typeof endpoint === 'string') {
|
|
13
|
+
return new HttpRpc(endpoint, { ...options, retries: failFast ? 0 : options.retries });
|
|
14
|
+
}
|
|
15
|
+
throw new Error('Unsupported RPC source: expected a URL, a web3.js Connection or an object with request(method, params)');
|
|
16
|
+
}
|
|
17
|
+
// 各节点的创世区块哈希缓存:失败(含超时)时删除,下次重试
|
|
18
|
+
const genesisCache = new WeakMap();
|
|
19
|
+
export function genesisOf(transport) {
|
|
20
|
+
let cached = genesisCache.get(transport);
|
|
21
|
+
if (!cached) {
|
|
22
|
+
const promise = transport.request('getGenesisHash');
|
|
23
|
+
genesisCache.set(transport, promise);
|
|
24
|
+
promise.catch(() => {
|
|
25
|
+
if (genesisCache.get(transport) === promise) {
|
|
26
|
+
genesisCache.delete(transport);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
cached = promise;
|
|
30
|
+
}
|
|
31
|
+
return cached;
|
|
32
|
+
}
|
|
33
|
+
/** 识别失败后多久内不再重试(期间请求照常发出,不做校验) */
|
|
34
|
+
const CHECK_RETRY_INTERVAL = 60_000;
|
|
35
|
+
export class NetworkMismatchError extends Error {
|
|
36
|
+
constructor(expected, actual) {
|
|
37
|
+
super(`Network mismatch: expected genesis ${expected}, but the node is on ${actual}`);
|
|
38
|
+
this.name = 'NetworkMismatchError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 校验节点所在的网络(创世区块哈希),避免把 devnet 的数据当成 mainnet 的返回。
|
|
43
|
+
* 校验与请求并行发出,不增加延迟;结果缓存,每个节点只多一次请求;识别失败时不拦截,并在一段时间内不再重试。
|
|
44
|
+
*/
|
|
45
|
+
export class NetworkCheckedRpc {
|
|
46
|
+
inner;
|
|
47
|
+
expected;
|
|
48
|
+
#checkFailedAt = 0;
|
|
49
|
+
constructor(inner, expected) {
|
|
50
|
+
this.inner = inner;
|
|
51
|
+
this.expected = expected;
|
|
52
|
+
}
|
|
53
|
+
async #check() {
|
|
54
|
+
if (this.#checkFailedAt && Date.now() - this.#checkFailedAt < CHECK_RETRY_INTERVAL) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let expected;
|
|
58
|
+
let actual;
|
|
59
|
+
try {
|
|
60
|
+
;
|
|
61
|
+
[expected, actual] = await Promise.all([this.expected(), genesisOf(this.inner)]);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
this.#checkFailedAt = Date.now();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.#checkFailedAt = 0;
|
|
68
|
+
if (expected !== actual) {
|
|
69
|
+
throw new NetworkMismatchError(expected, actual);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async request(method, params) {
|
|
73
|
+
const [checked, result] = await Promise.allSettled([this.#check(), this.inner.request(method, params)]);
|
|
74
|
+
if (checked.status === 'rejected') {
|
|
75
|
+
throw checked.reason;
|
|
76
|
+
}
|
|
77
|
+
if (result.status === 'rejected') {
|
|
78
|
+
throw result.reason;
|
|
79
|
+
}
|
|
80
|
+
return result.value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function resolveSource(source, cluster, options = {}) {
|
|
84
|
+
const isDefault = source === undefined;
|
|
85
|
+
const list = isDefault ? DEFAULT_RPC_URLS[cluster ?? 'mainnet'] : Array.isArray(source) ? source : [source];
|
|
86
|
+
if (!list.length) {
|
|
87
|
+
throw new Error('RPC source list is empty');
|
|
88
|
+
}
|
|
89
|
+
const multiple = list.length > 1;
|
|
90
|
+
// 内置节点保留 429 重试:浏览器里官方节点总是 403,实际只有 publicnode 一个可用节点
|
|
91
|
+
const raw = list.map((item) => toTransport(item, options, multiple && !isDefault));
|
|
92
|
+
let detecting = null;
|
|
93
|
+
const genesis = () => {
|
|
94
|
+
if (cluster) {
|
|
95
|
+
return Promise.resolve(GENESIS_HASHES[cluster]);
|
|
96
|
+
}
|
|
97
|
+
if (!detecting) {
|
|
98
|
+
const promise = (async () => {
|
|
99
|
+
let lastError;
|
|
100
|
+
for (const node of raw) {
|
|
101
|
+
try {
|
|
102
|
+
return await genesisOf(node);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
lastError = err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
throw lastError;
|
|
109
|
+
})();
|
|
110
|
+
detecting = promise;
|
|
111
|
+
promise.catch(() => {
|
|
112
|
+
if (detecting === promise)
|
|
113
|
+
detecting = null;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return detecting;
|
|
117
|
+
};
|
|
118
|
+
// 内置节点本来就是按 cluster 选的,不校验;单个节点且没指定 cluster 时,期望值就是它自己,也不需要校验
|
|
119
|
+
const check = !isDefault && (multiple || cluster !== undefined);
|
|
120
|
+
const nodes = check ? raw.map((node) => new NetworkCheckedRpc(node, genesis)) : raw;
|
|
121
|
+
return {
|
|
122
|
+
transport: multiple ? new FallbackRpc(nodes, options) : nodes[0],
|
|
123
|
+
genesis,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../src/source.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export type Commitment = 'processed' | 'confirmed' | 'finalized';
|
|
2
|
+
/** balances() 的单项结果,字段都是字符串 / 数字 / 布尔 / null,可以直接 JSON.stringify */
|
|
3
|
+
export interface TokenBalance {
|
|
4
|
+
/** 代币的 mint(原样) */
|
|
5
|
+
token: string;
|
|
6
|
+
/** 是否按原生 SOL 查询 */
|
|
7
|
+
native: boolean;
|
|
8
|
+
/** 余额(最小单位的十进制字符串);需要计算时用 BigInt(balance) */
|
|
9
|
+
balance: string;
|
|
10
|
+
decimals: number;
|
|
11
|
+
/** 按 decimals 换算后的十进制字符串,如 "1234.5";整数不带小数点 */
|
|
12
|
+
formatted: string;
|
|
13
|
+
/** 代币所属程序(SPL Token / Token-2022);主币或未知时为 null */
|
|
14
|
+
tokenProgram: string | null;
|
|
15
|
+
/** 仅在 symbol: true 时返回;读不到时为 null */
|
|
16
|
+
symbol?: string | null;
|
|
17
|
+
/** 查询失败(mint 不存在、地址非法等)时为 false,balance / formatted 为 "0" */
|
|
18
|
+
success: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** ownerTokens() / getOwnerTokens() 的单项结果 */
|
|
21
|
+
export interface OwnedToken extends Omit<TokenBalance, 'symbol'> {
|
|
22
|
+
/** 该代币的代币账户数量;大于 1 时 balance 为所有账户的合计 */
|
|
23
|
+
accounts: number;
|
|
24
|
+
/** metadata 为 true(默认)时返回;没有元数据或读取失败时为 null(见 metadataStatus) */
|
|
25
|
+
name?: string | null;
|
|
26
|
+
symbol?: string | null;
|
|
27
|
+
/** metadata 为 true 时返回:ok 读到了;missing 确认没有元数据;failed 节点问题没能读到(可以稍后单独用 getTokens 重查) */
|
|
28
|
+
metadataStatus?: 'ok' | 'missing' | 'failed';
|
|
29
|
+
}
|
|
30
|
+
export interface SolBalance {
|
|
31
|
+
address: string;
|
|
32
|
+
/** lamports(十进制字符串) */
|
|
33
|
+
balance: string;
|
|
34
|
+
/** 按 9 位精度换算后的 SOL */
|
|
35
|
+
formatted: string;
|
|
36
|
+
success: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** tokens() / getTokens() 可选的字段 */
|
|
39
|
+
export type TokenField = 'name' | 'symbol' | 'uri' | 'decimals' | 'supply' | 'tokenProgram' | 'mintAuthority' | 'freezeAuthority';
|
|
40
|
+
export declare const DEFAULT_TOKEN_FIELDS: readonly ["name", "symbol", "decimals"];
|
|
41
|
+
export type DefaultTokenField = (typeof DEFAULT_TOKEN_FIELDS)[number];
|
|
42
|
+
interface TokenFieldTypes {
|
|
43
|
+
/** 优先取 Token-2022 TokenMetadata 扩展,其次 Metaplex 元数据 */
|
|
44
|
+
name: string | null;
|
|
45
|
+
symbol: string | null;
|
|
46
|
+
uri: string | null;
|
|
47
|
+
decimals: number | null;
|
|
48
|
+
/** 最小单位的十进制字符串 */
|
|
49
|
+
supply: string | null;
|
|
50
|
+
tokenProgram: string | null;
|
|
51
|
+
/** 没有 mint 权限(已放弃)时为 null */
|
|
52
|
+
mintAuthority: string | null;
|
|
53
|
+
freezeAuthority: string | null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* tokens() / getTokens() 的单项结果,只包含请求的字段。
|
|
57
|
+
* 请求 supply 时额外带 supplyFormatted(按 decimals 换算)。
|
|
58
|
+
*/
|
|
59
|
+
export type TokenDetails<F extends TokenField = DefaultTokenField> = {
|
|
60
|
+
/** mint 地址 */
|
|
61
|
+
address: string;
|
|
62
|
+
native: boolean;
|
|
63
|
+
/** mint 存在且请求的 name / symbol / uri / decimals / supply 都读到时为 true */
|
|
64
|
+
success: boolean;
|
|
65
|
+
} & {
|
|
66
|
+
[K in F]: TokenFieldTypes[K];
|
|
67
|
+
} & ('supply' extends F ? {
|
|
68
|
+
supplyFormatted: string | null;
|
|
69
|
+
} : unknown);
|
|
70
|
+
export type TokenStandard = 'NonFungible' | 'FungibleAsset' | 'Fungible' | 'NonFungibleEdition' | 'ProgrammableNonFungible' | 'ProgrammableNonFungibleEdition';
|
|
71
|
+
/** nfts() / getNfts() 的单项结果(Metaplex 元数据;Token-2022 NFT 只有 name / symbol / uri) */
|
|
72
|
+
export interface NftDetails {
|
|
73
|
+
mint: string;
|
|
74
|
+
name: string | null;
|
|
75
|
+
symbol: string | null;
|
|
76
|
+
/** 链下元数据 JSON 的地址 */
|
|
77
|
+
uri: string | null;
|
|
78
|
+
/** 所属集合;verified 为 false 表示未经集合方签名确认 */
|
|
79
|
+
collection: {
|
|
80
|
+
address: string;
|
|
81
|
+
verified: boolean;
|
|
82
|
+
} | null;
|
|
83
|
+
creators: Array<{
|
|
84
|
+
address: string;
|
|
85
|
+
verified: boolean;
|
|
86
|
+
share: number;
|
|
87
|
+
}>;
|
|
88
|
+
/** 版税,万分比 */
|
|
89
|
+
sellerFeeBasisPoints: number | null;
|
|
90
|
+
tokenStandard: TokenStandard | null;
|
|
91
|
+
isMutable: boolean | null;
|
|
92
|
+
updateAuthority: string | null;
|
|
93
|
+
success: boolean;
|
|
94
|
+
}
|
|
95
|
+
export interface NftOwner {
|
|
96
|
+
mint: string;
|
|
97
|
+
/** 持有人钱包地址 */
|
|
98
|
+
owner: string | null;
|
|
99
|
+
/** 持有该 NFT 的代币账户 */
|
|
100
|
+
tokenAccount: string | null;
|
|
101
|
+
success: boolean;
|
|
102
|
+
}
|
|
103
|
+
export {};
|
|
104
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA4CA,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,UAAU,CAA0C,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@w3lib/solana-batch-call",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Batched Solana reads: SOL / SPL / Token-2022 balances, token metadata, NFTs. Auto-batching JSON-RPC, getMultipleAccounts chunking, RPC failover. No Solana SDK dependency.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"solana",
|
|
7
|
+
"spl-token",
|
|
8
|
+
"token-2022",
|
|
9
|
+
"metaplex",
|
|
10
|
+
"batch",
|
|
11
|
+
"getMultipleAccounts",
|
|
12
|
+
"json-rpc"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "L",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/web3-lib/libs.git",
|
|
39
|
+
"directory": "packages/solana-batch-call"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/web3-lib/libs/tree/main/packages/solana-batch-call#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/web3-lib/libs/issues"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@noble/curves": "^2.4.0",
|
|
50
|
+
"@noble/hashes": "^2.4.0",
|
|
51
|
+
"@scure/base": "^2.4.0"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
55
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
56
|
+
"test": "vitest run",
|
|
57
|
+
"test:live": "LIVE=1 vitest run test/live.test.ts"
|
|
58
|
+
}
|
|
6
59
|
}
|
package/src/address.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ed25519 } from '@noble/curves/ed25519.js'
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha2.js'
|
|
3
|
+
import { base58 } from '@scure/base'
|
|
4
|
+
|
|
5
|
+
import { ASSOCIATED_TOKEN_PROGRAM_ID, METADATA_PROGRAM_ID } from './constants.js'
|
|
6
|
+
|
|
7
|
+
/** 是否是合法的 Solana 地址(base58,32 字节) */
|
|
8
|
+
export function isAddress(value: unknown): value is string {
|
|
9
|
+
if (typeof value !== 'string' || value.length < 32 || value.length > 44) {
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return base58.decode(value).length === 32
|
|
14
|
+
} catch {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function decodeAddress(address: string): Uint8Array {
|
|
20
|
+
const bytes = base58.decode(address)
|
|
21
|
+
if (bytes.length !== 32) {
|
|
22
|
+
throw new Error(`Invalid Solana address: ${address}`)
|
|
23
|
+
}
|
|
24
|
+
return bytes
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function encodeAddress(bytes: Uint8Array): string {
|
|
28
|
+
return base58.encode(bytes)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 32 字节是否在 ed25519 曲线上(PDA 必须不在曲线上) */
|
|
32
|
+
function isOnCurve(bytes: Uint8Array): boolean {
|
|
33
|
+
try {
|
|
34
|
+
ed25519.Point.fromBytes(bytes)
|
|
35
|
+
return true
|
|
36
|
+
} catch {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PDA_MARKER = new TextEncoder().encode('ProgramDerivedAddress')
|
|
42
|
+
|
|
43
|
+
function concat(parts: Uint8Array[]): Uint8Array {
|
|
44
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0))
|
|
45
|
+
let offset = 0
|
|
46
|
+
for (const part of parts) {
|
|
47
|
+
out.set(part, offset)
|
|
48
|
+
offset += part.length
|
|
49
|
+
}
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// PDA 推导是纯计算但要做多次 sha256 + 曲线判断,结果缓存
|
|
54
|
+
const pdaCache = new Map<string, string>()
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 推导 PDA(与 web3.js PublicKey.findProgramAddressSync 相同),返回地址。
|
|
58
|
+
* seeds 每项不超过 32 字节。
|
|
59
|
+
*/
|
|
60
|
+
export function findProgramAddress(seeds: Uint8Array[], programId: string): string {
|
|
61
|
+
const key = `${programId}:${seeds.map((s) => encodeAddress(s)).join(',')}`
|
|
62
|
+
const cached = pdaCache.get(key)
|
|
63
|
+
if (cached) {
|
|
64
|
+
return cached
|
|
65
|
+
}
|
|
66
|
+
const program = decodeAddress(programId)
|
|
67
|
+
for (let bump = 255; bump >= 0; bump--) {
|
|
68
|
+
const hash = sha256(concat([...seeds, new Uint8Array([bump]), program, PDA_MARKER]))
|
|
69
|
+
if (!isOnCurve(hash)) {
|
|
70
|
+
const address = encodeAddress(hash)
|
|
71
|
+
if (pdaCache.size > 10_000) {
|
|
72
|
+
pdaCache.clear()
|
|
73
|
+
}
|
|
74
|
+
pdaCache.set(key, address)
|
|
75
|
+
return address
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new Error('Unable to find a viable program address bump seed')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 关联代币账户(ATA)地址 */
|
|
82
|
+
export function getAssociatedTokenAddress(owner: string, mint: string, tokenProgram: string): string {
|
|
83
|
+
return findProgramAddress([decodeAddress(owner), decodeAddress(tokenProgram), decodeAddress(mint)], ASSOCIATED_TOKEN_PROGRAM_ID)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Metaplex Token Metadata 账户地址 */
|
|
87
|
+
export function getMetadataAddress(mint: string): string {
|
|
88
|
+
return findProgramAddress(
|
|
89
|
+
[new TextEncoder().encode('metadata'), decodeAddress(METADATA_PROGRAM_ID), decodeAddress(mint)],
|
|
90
|
+
METADATA_PROGRAM_ID,
|
|
91
|
+
)
|
|
92
|
+
}
|