@w3lib/solana-batch-call 0.2.0 → 0.2.2
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 +23 -0
- package/README.md +3 -2
- package/dist/address.d.ts +1 -1
- package/dist/address.d.ts.map +1 -1
- package/dist/address.js +10 -1
- package/dist/address.js.map +1 -1
- package/dist/client.d.ts +2 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +191 -78
- package/dist/client.js.map +1 -1
- package/dist/rpc.d.ts +8 -1
- package/dist/rpc.d.ts.map +1 -1
- package/dist/rpc.js +205 -30
- package/dist/rpc.js.map +1 -1
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +13 -1
- package/dist/shortcuts.js.map +1 -1
- package/package.json +1 -1
- package/src/address.ts +11 -1
- package/src/client.ts +239 -80
- package/src/rpc.ts +222 -30
- package/src/shortcuts.ts +14 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
- 修复:服务端临时故障(-32000 server busy / overloaded / temporarily unavailable / timeout,或 HTTP 5xx + -32603)不再暂停批量 10 分钟,按节点故障处理、换节点(与限频一样不降级,降级只会加重负载)。批量收到 5xx + -32603 时先重试一次批量(偶发故障这次就成功);仍失败时逐条发送,逐条成功说明节点用 5xx 拒绝批量,才暂停批量。5xx 下的其他错误(方法数量超限、账户数超限、参数错误、minContextSlot 等)照常按类型处理
|
|
6
|
+
- 说明(0.2.1 起的行为):ATA 模式下所属程序未知时,只统计与 mint 所属程序一致的那个 ATA;一个 mint 只属于一个程序,真实链上不会两个 ATA 同时有余额
|
|
7
|
+
|
|
8
|
+
## 0.2.1
|
|
9
|
+
|
|
10
|
+
- 只查主币余额时不再读回账户数据:`getSolBalances` 和单币模式(含 `getMultiBalances`)里的主币用 `getMultipleAccounts` + `dataSlice: { offset: 0, length: 0 }` 只取 lamports。原来钱包是数据很大的程序账户(如 105KB)时整份读回,并发查询会超时
|
|
11
|
+
- 单币模式下钱包账户第一次单独一组读;确认是普通钱包(系统账户或不存在,数据本来就为空)后,之后与 mint / ATA 账户放在同一个请求里,不多发请求、主币和代币来自同一个快照;程序账户等始终单独读
|
|
12
|
+
- 两组读取共用并发上限(同时最多 3 个 `getMultipleAccounts`),一个请求失败后不再发后面的批次
|
|
13
|
+
- 修复:节点限制单个方法在一个批量请求里的数量时(如内置的 publicnode:`getMultipleAccounts` 最多 1 个,原来整批被拒、当成节点故障切换到下一个节点),按报错里的限制重新分批发送,之后的请求直接按限制分批。原来一次读取超过 100 个账户时就会碰到
|
|
14
|
+
- 余额超过 2^53 lamports 时仍是精确值(新增回归测试,包括 HTTP 层对 JSON 大数的解析)
|
|
15
|
+
- 修复:一次偶发的批量请求失败(如 -32603)就永久关闭 JSON-RPC 批量。现在整批被拒(HTTP 400 / 404 / 405 / 415 / 422,或返回整体错误、账户数超限除外)时这一批逐条重发,10 分钟内逐条请求,之后重新尝试批量
|
|
16
|
+
- 修复:扫描模式和 `accounts: 'all'` 丢掉节点解析不了、退回 base64 的代币账户(如带新扩展的 Token-2022),余额被漏掉或算成 0。现在自己解析出 mint 和数量,decimals(以及节点没给的所属程序)读 mint 账户补上;补读不受 `minContextSlot` 约束、不计入 slot,读不到时只跳过这个代币
|
|
17
|
+
- 修复:未初始化的 mint(创建和初始化之间读到)被当作 decimals = 0 并永久缓存,之后余额被放大
|
|
18
|
+
- 修复:读响应体时超时或断连抛原生 `DOMException` / `TypeError`,单节点时 `ownerTokens` 读元数据的容错失效。现在同样包装成 `HttpError`(超时带 `timeout` 标记)
|
|
19
|
+
- 修复:节点持续返回 JSON-RPC 429 时,批量重试之后又逐条重试,请求数放大约 20 倍。整批都是 429 时只在批量层重试;部分 429 时这些调用单独重试一次
|
|
20
|
+
- 快捷函数的客户端缓存:配置的键顺序不同视为同一份;配置里有函数(如自定义 `fetch`)时按函数对象复用客户端(原来每次都新建)
|
|
21
|
+
- 导入缓存(`importTokenMetaCache` / `persistTokenMetaCache`):写入时间只接受有限值,晚于现在的截到现在(原来未来的时间会让 name / symbol 永不过期);导入的所属程序第一次用时从链上确认(记错时 ATA 余额不再恒为 0)
|
|
22
|
+
- 修复:不支持 `JSON.parse` 源文本访问的环境(Node 21 以下、老浏览器)里,大数兜底会改坏字符串里的长数字。现在跳过字符串字面量,只处理数值
|
|
23
|
+
- 节点限制单次 `getMultipleAccounts` 的账户数(“Too many accounts requested”)时,减半后分多次请求并合并结果、记住上限,不再当成节点故障
|
|
24
|
+
- `findProgramAddress` 检查种子:单个超过 32 字节、或超过 15 个(加上 bump 共 16 个)时抛错,与 web3.js 和链上一致
|
|
25
|
+
|
|
3
26
|
## 0.2.0
|
|
4
27
|
|
|
5
28
|
- `getBalances` / `getOwnerTokens` 新增 `tokenPrograms`:按代币类型过滤,`['spl']` 不含 Token-2022(扫描时也不再请求 Token-2022 的代币账户),`['token-2022']` 反之;也可以传程序地址。不认识的值或空数组报错
|
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ pnpm add @w3lib/solana-batch-call
|
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
- **一次请求查一批**:账户读取用 `getMultipleAccounts`(自动去重、按 100 个一组拆分、最多同时 3 个请求);同一 tick 内发起的 RPC 调用合并成一个 JSON-RPC 批量请求
|
|
10
|
-
- **免费节点也能查余额**:指定代币时在本地推导关联代币账户(ATA)地址,和
|
|
10
|
+
- **免费节点也能查余额**:指定代币时在本地推导关联代币账户(ATA)地址,和 mint 账户一起用 `getMultipleAccounts` 读完,不需要 `getTokenAccountsByOwner` 这类索引方法
|
|
11
|
+
- **只查主币不读账户数据**:SOL 余额(`getSolBalances`、单币模式里的主币)用带 `dataSlice` 的 `getMultipleAccounts` 只取 lamports,与代币账户分开两组、同一个批量请求发出——钱包是数据很大的程序账户时也不会拖慢或超时
|
|
11
12
|
- **Token-2022**:余额、decimals、TokenMetadata 扩展里的 name / symbol / uri 都支持
|
|
12
13
|
- **多节点故障切换**:限频、403、需要 API Key、不支持某方法等节点问题自动换下一个节点;节点不支持批量请求或限制批量大小时自动降级
|
|
13
14
|
- **网络校验**:按创世区块哈希确认节点所在网络,不会把 devnet 的数据当成 mainnet 的返回
|
|
@@ -65,7 +66,7 @@ await getOwnerNfts(owner) // 某地址持有的全部 NFT(需要支持索
|
|
|
65
66
|
| `getOwnerTokens(owner, opts)` | `ownerTokens` | 持有人的全部代币(资产列表),带 name / symbol;默认不含 NFT 和余额为 0 的账户;`tokenPrograms` 按代币类型过滤 |
|
|
66
67
|
| `getMultiBalances(queries, opts)` | `multiBalances` | 多个钱包一次查,参数同 `getBalances` |
|
|
67
68
|
| `watchBalances(owner, mints?, opts)` | — | 轮询余额,相同参数的订阅共用一份轮询,余额变化时才通知 `onChange`;返回取消函数 |
|
|
68
|
-
| `getSolBalances(addresses, opts)` | `solBalances` | 多个地址的 SOL
|
|
69
|
+
| `getSolBalances(addresses, opts)` | `solBalances` | 多个地址的 SOL 余额(只读 lamports,不读回账户数据) |
|
|
69
70
|
| `getTokens(mints, opts)` | `tokens` | 代币详情,`fields` 选择字段,结果类型随之收窄;`transferFee` 返回 Token-2022 转账手续费 |
|
|
70
71
|
| `getNfts(mints, opts)` | `nfts` | NFT 元数据(Metaplex;Token-2022 NFT 取扩展) |
|
|
71
72
|
| `getNftOwners(mints, opts)` | `nftOwners` | NFT 持有人 |
|
package/dist/address.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare function encodeAddress(bytes: Uint8Array): string;
|
|
|
10
10
|
export declare function isOnCurve(bytes: Uint8Array): boolean;
|
|
11
11
|
/**
|
|
12
12
|
* 推导 PDA(与 web3.js PublicKey.findProgramAddressSync 相同),返回地址。
|
|
13
|
-
* seeds 每项不超过 32
|
|
13
|
+
* seeds 每项不超过 32 字节、最多 15 个(加上 bump 共 16 个,链上上限),超出时抛错。
|
|
14
14
|
*/
|
|
15
15
|
export declare function findProgramAddress(seeds: Uint8Array[], programId: string): string;
|
|
16
16
|
/** 关联代币账户(ATA)地址 */
|
package/dist/address.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAKA,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CASzD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAMzD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEvD;AA4BD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAoBpD;
|
|
1
|
+
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAKA,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CASzD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAMzD;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAEvD;AA4BD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAoBpD;AAoBD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CA0BjF;AAED,oBAAoB;AACpB,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnG;AAED,mCAAmC;AACnC,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKvD"}
|
package/dist/address.js
CHANGED
|
@@ -83,13 +83,22 @@ function concat(parts) {
|
|
|
83
83
|
}
|
|
84
84
|
return out;
|
|
85
85
|
}
|
|
86
|
+
const MAX_SEED_LENGTH = 32;
|
|
87
|
+
const MAX_SEEDS = 16;
|
|
86
88
|
// PDA 推导是纯计算但要做多次 sha256 + 曲线判断,结果缓存
|
|
87
89
|
const pdaCache = new Map();
|
|
88
90
|
/**
|
|
89
91
|
* 推导 PDA(与 web3.js PublicKey.findProgramAddressSync 相同),返回地址。
|
|
90
|
-
* seeds 每项不超过 32
|
|
92
|
+
* seeds 每项不超过 32 字节、最多 15 个(加上 bump 共 16 个,链上上限),超出时抛错。
|
|
91
93
|
*/
|
|
92
94
|
export function findProgramAddress(seeds, programId) {
|
|
95
|
+
// 与链上和 web3.js 一致:每个种子最多 32 字节;种子加上 bump 最多 16 个
|
|
96
|
+
if (seeds.some((seed) => seed.length > MAX_SEED_LENGTH)) {
|
|
97
|
+
throw new TypeError('Max seed length exceeded');
|
|
98
|
+
}
|
|
99
|
+
if (seeds.length + 1 > MAX_SEEDS) {
|
|
100
|
+
throw new TypeError('Max seeds exceeded');
|
|
101
|
+
}
|
|
93
102
|
const key = `${programId}:${seeds.map((s) => encodeAddress(s)).join(',')}`;
|
|
94
103
|
const cached = pdaCache.get(key);
|
|
95
104
|
if (cached) {
|
package/dist/address.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"address.js","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,OAAO,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAEjF,qCAAqC;AACrC,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QACxE,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,EAAE,CAAA;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACpC,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,2BAA2B,OAAO,EAAE,CAAC,CAAA;IACvD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAiB;IAC7C,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC7B,CAAC;AAED,wCAAwC;AACxC,MAAM,CAAC,GAAG,EAAE,IAAI,IAAI,GAAG,GAAG,CAAA;AAC1B,MAAM,CAAC,GAAG,8EAA8E,CAAA;AAExF,oBAAoB;AACpB,SAAS,KAAK,CAAC,CAAS,EAAE,CAAS;IACjC,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IAC3C,OAAO,CAAC,CAAA;AACV,CAAC;AAED,qFAAqF;AACrF,SAAS,QAAQ,CAAC,CAAS;IACzB,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA,CAAC,YAAY;IAC/C,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;IAClC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IACjC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;IACnC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACtC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACtC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACtC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACvC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACxC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACxC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAC/C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,KAAiB;IACzC,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACxB,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,CAAC,GAAG,EAAE,CAAA;IACV,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAE,KAAK,CAAC,CAAC,CAAY,GAAG,IAAI,CAAC,CAAC,CAAE,KAAK,CAAC,CAAC,CAAY,CAAC,CAAA;IACvF,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACX,OAAO,KAAK,CAAA;IACd,CAAC;IACD,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IACtB,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IAC3B,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA,CAAC,kBAAkB;IAC9C,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;QACb,iBAAiB;QACjB,OAAO,CAAE,KAAK,CAAC,EAAE,CAAY,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;IAC7C,CAAC;IACD,+BAA+B;IAC/B,OAAO,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAA;AACrC,CAAC;AAED,MAAM,UAAU,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAA;AAEpE,SAAS,MAAM,CAAC,KAAmB;IACjC,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;IACnE,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QACrB,MAAM,IAAI,IAAI,CAAC,MAAM,CAAA;IACvB,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,qCAAqC;AACrC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA;AAE1C;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAmB,EAAE,SAAiB;IACvE,MAAM,GAAG,GAAG,GAAG,SAAS,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;IAC1E,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAA;IACf,CAAC;IACD,MAAM,OAAO,GAAG,aAAa,CAAC,SAAS,CAAC,CAAA;IACxC,KAAK,IAAI,IAAI,GAAG,GAAG,EAAE,IAAI,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,EAAE,IAAI,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;QACpF,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YACrB,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;YACnC,IAAI,QAAQ,CAAC,IAAI,GAAG,MAAM,EAAE,CAAC;gBAC3B,QAAQ,CAAC,KAAK,EAAE,CAAA;YAClB,CAAC;YACD,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;YAC1B,OAAO,OAAO,CAAA;QAChB,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAA;AACtE,CAAC;AAED,oBAAoB;AACpB,MAAM,UAAU,yBAAyB,CAAC,KAAa,EAAE,IAAY,EAAE,YAAoB;IACzF,OAAO,kBAAkB,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,2BAA2B,CAAC,CAAA;AAClI,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,kBAAkB,CACvB,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,EAC/F,mBAAmB,CACpB,CAAA;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"address.js","sourceRoot":"","sources":["../src/address.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAA;AAC9C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,OAAO,EAAE,2BAA2B,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAA;AAEjF,qCAAqC;AACrC,MAAM,UAAU,SAAS,CAAC,KAAc;IACtC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;QACxE,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,KAAK,EAAE,CAAA;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAe;IAC3C,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IACpC,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,2BAA2B,OAAO,EAAE,CAAC,CAAA;IACvD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAiB;IAC7C,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC7B,CAAC;AAED,wCAAwC;AACxC,MAAM,CAAC,GAAG,EAAE,IAAI,IAAI,GAAG,GAAG,CAAA;AAC1B,MAAM,CAAC,GAAG,8EAA8E,CAAA;AAExF,oBAAoB;AACpB,SAAS,KAAK,CAAC,CAAS,EAAE,CAAS;IACjC,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IAC3C,OAAO,CAAC,CAAA;AACV,CAAC;AAED,qFAAqF;AACrF,SAAS,QAAQ,CAAC,CAAS;IACzB,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA,CAAC,YAAY;IAC/C,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;IAClC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IACjC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA;IACnC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACtC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACtC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACtC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACvC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACxC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAA;IACxC,OAAO,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;AAC/C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,KAAiB;IACzC,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;QACxB,OAAO,KAAK,CAAA;IACd,CAAC;IACD,IAAI,CAAC,GAAG,EAAE,CAAA;IACV,KAAK,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAE,KAAK,CAAC,CAAC,CAAY,GAAG,IAAI,CAAC,CAAC,CAAE,KAAK,CAAC,CAAC,CAAY,CAAC,CAAA;IACvF,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACX,OAAO,KAAK,CAAA;IACd,CAAC;IACD,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IACtB,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;IAC3B,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAA,CAAC,kBAAkB;IAC9C,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC;QACb,iBAAiB;QACjB,OAAO,CAAE,KAAK,CAAC,EAAE,CAAY,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;IAC7C,CAAC;IACD,+BAA+B;IAC/B,OAAO,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAA;AACrC,CAAC;AAED,MAAM,UAAU,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAA;AAEpE,SAAS,MAAM,CAAC,KAAmB;IACjC,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAA;IACnE,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QACrB,MAAM,IAAI,IAAI,CAAC,MAAM,CAAA;IACvB,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,eAAe,GAAG,EAAE,CAAA;AAC1B,MAAM,SAAS,GAAG,EAAE,CAAA;AAEpB,qCAAqC;AACrC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA;AAE1C;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAmB,EAAE,SAAiB;IACvE,iDAAiD;IACjD,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,eAAe,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,SAAS,CAAC,0BAA0B,CAAC,CAAA;IACjD,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CAAC,oBAAoB,CAAC,CAAA;IAC3C,CAAC;IACD,MAAM,GAAG,GAAG,GAAG,SAAS,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;IAC1E,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAA;IACf,CAAC;IACD,MAAM,OAAO,GAAG,aAAa,CAAC,SAAS,CAAC,CAAA;IACxC,KAAK,IAAI,IAAI,GAAG,GAAG,EAAE,IAAI,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,EAAE,IAAI,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;QACpF,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;YACrB,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,CAAA;YACnC,IAAI,QAAQ,CAAC,IAAI,GAAG,MAAM,EAAE,CAAC;gBAC3B,QAAQ,CAAC,KAAK,EAAE,CAAA;YAClB,CAAC;YACD,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAA;YAC1B,OAAO,OAAO,CAAA;QAChB,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAA;AACtE,CAAC;AAED,oBAAoB;AACpB,MAAM,UAAU,yBAAyB,CAAC,KAAa,EAAE,IAAY,EAAE,YAAoB;IACzF,OAAO,kBAAkB,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,EAAE,2BAA2B,CAAC,CAAA;AAClI,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,OAAO,kBAAkB,CACvB,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,aAAa,CAAC,mBAAmB,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,EAC/F,mBAAmB,CACpB,CAAA;AACH,CAAC"}
|
package/dist/client.d.ts
CHANGED
|
@@ -136,12 +136,12 @@ export declare class SolanaClient {
|
|
|
136
136
|
* 账户不存在或地址非法时为 null。
|
|
137
137
|
*/
|
|
138
138
|
accounts(addresses: readonly string[], options?: AccountsOptions): Promise<(AccountInfo | null)[]>;
|
|
139
|
-
/** 批量查多个地址的 SOL
|
|
139
|
+
/** 批量查多个地址的 SOL 余额(getMultipleAccounts 只读 lamports、不读回账户数据;账户不存在视为 0) */
|
|
140
140
|
solBalances(addresses: readonly string[], options?: SolBalancesOptions): Promise<SolBalance[]>;
|
|
141
141
|
/**
|
|
142
142
|
* 批量查余额(主币 SOL + SPL Token + Token-2022),返回原始余额、decimals 和换算后的数值。
|
|
143
143
|
*
|
|
144
|
-
* - 传 mints:本地推导 ATA
|
|
144
|
+
* - 传 mints:本地推导 ATA,与 mint 账户一起用 getMultipleAccounts 读完,免费节点也能用;主币只读 lamports(dataSlice),另成一组同时发出
|
|
145
145
|
* - 传 mints 且 accounts: 'all':每个代币按 mint 过滤查 getTokenAccountsByOwner,统计全部代币账户(含非 ATA)
|
|
146
146
|
* - 不传 mints(或 scan: true):用 getTokenAccountsByOwner 扫描全部代币账户,返回所有余额大于 0 的代币
|
|
147
147
|
* - 失败项(success: false)带 error:invalid-address(mint 地址非法)/ not-found(mint 不存在)/ not-token(不是代币 mint)
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAOL,KAAK,OAAO,EACb,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAML,KAAK,WAAW,EAIjB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAsD,KAAK,YAAY,EAAE,MAAM,UAAU,CAAA;AAChG,OAAO,EAAuC,KAAK,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AACrG,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,iBAAiB,EAEtB,KAAK,UAAU,EACf,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,kBAAkB,EAIxB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,YAAa,SAAQ,aAAa;IACjD,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,qBAAqB;IACrB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB;;;;OAIG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC;AAED,gBAAgB;AAChB,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;CACrB;AAED,qCAAqC;AACrC,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,eAAgB,SAAQ,cAAc,EAAE,WAAW;IAClE,6CAA6C;IAC7C,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB;;;OAGG;IACH,IAAI,CAAC,EAAE,OAAO,CAAA;IACd;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,KAAK,GAAG,KAAK,CAAA;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAA;CAC9C;AAED,MAAM,WAAW,kBAAmB,SAAQ,cAAc,EAAE,WAAW;IACrE,kFAAkF;IAClF,aAAa,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAA;IAC7C,yCAAyC;IACzC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,wBAAwB;IACxB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,kCAAkC;IAClC,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC,SAAS,UAAU,GAAG,iBAAiB,CAAE,SAAQ,cAAc;IAC7F,+CAA+C;IAC/C,MAAM,CAAC,EAAE,SAAS,CAAC,EAAE,CAAA;CACtB;AAED,qBAAqB;AACrB,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACrD,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,wBAAwB;AACxB,MAAM,WAAW,kBAAmB,SAAQ,cAAc,EAAE,WAAW;CAAG;AAE1E,qCAAqC;AACrC,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CACzB;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -99,11 +99,33 @@ const TOKEN_STANDARDS = [
|
|
|
99
99
|
const NAME_TTL = 60 * 60 * 1000;
|
|
100
100
|
const MAX_CACHED_TOKENS = 50_000;
|
|
101
101
|
const tokenMetaCache = new Map();
|
|
102
|
+
// 已知数据为空的钱包账户(系统账户或不存在):ATA 模式下和代币账户放在同一个请求里读,不必单独带 dataSlice 多发一个请求。
|
|
103
|
+
// 按 scope + 地址记,最多 1 万个,超出淘汰最早的;读到不是系统账户(如程序账户)时移除
|
|
104
|
+
const MAX_SMALL_OWNERS = 10_000;
|
|
105
|
+
const smallOwners = new Set();
|
|
106
|
+
function isSmallOwner(scope, owner) {
|
|
107
|
+
return smallOwners.has(`${scope}:${owner}`);
|
|
108
|
+
}
|
|
109
|
+
/** 根据只读 lamports 的结果判断钱包账户是否“数据为空”(dataSlice 读不到数据长度,按所属程序判断:系统账户的数据为空) */
|
|
110
|
+
function learnOwner(scope, owner, account) {
|
|
111
|
+
const key = `${scope}:${owner}`;
|
|
112
|
+
if (account === null || account.owner === SYSTEM_PROGRAM_ID) {
|
|
113
|
+
smallOwners.delete(key);
|
|
114
|
+
smallOwners.add(key);
|
|
115
|
+
if (smallOwners.size > MAX_SMALL_OWNERS) {
|
|
116
|
+
smallOwners.delete(smallOwners.values().next().value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
smallOwners.delete(key);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
102
123
|
let clientIds = 0;
|
|
103
124
|
/** 缓存写入监听(persistTokenMetaCache 用) */
|
|
104
125
|
const cacheWriteListeners = new Set();
|
|
105
126
|
/** 测试用:清空代币信息缓存 */
|
|
106
127
|
export function resetTokenMetaCache() {
|
|
128
|
+
smallOwners.clear();
|
|
107
129
|
tokenMetaCache.clear();
|
|
108
130
|
}
|
|
109
131
|
const SHARED_SCOPES = new Set(['mainnet', 'devnet', 'testnet']);
|
|
@@ -116,7 +138,8 @@ export function exportTokenMetaCache(maxEntries = Number.POSITIVE_INFINITY) {
|
|
|
116
138
|
const entries = [];
|
|
117
139
|
for (const [key, meta] of tokenMetaCache) {
|
|
118
140
|
if (SHARED_SCOPES.has(key.slice(0, key.indexOf(':')))) {
|
|
119
|
-
|
|
141
|
+
const { programUnverified: _, ...exported } = meta;
|
|
142
|
+
entries.push([key, exported]);
|
|
120
143
|
}
|
|
121
144
|
}
|
|
122
145
|
return { version: 1, entries: entries.slice(Math.max(0, entries.length - maxEntries)) };
|
|
@@ -149,12 +172,18 @@ function validMeta(entry) {
|
|
|
149
172
|
const meta = {};
|
|
150
173
|
if (typeof raw.decimals === 'number' && Number.isInteger(raw.decimals) && raw.decimals >= 0 && raw.decimals <= 255)
|
|
151
174
|
meta.decimals = raw.decimals;
|
|
152
|
-
if (raw.tokenProgram === TOKEN_PROGRAM_ID || raw.tokenProgram === TOKEN_2022_PROGRAM_ID)
|
|
175
|
+
if (raw.tokenProgram === TOKEN_PROGRAM_ID || raw.tokenProgram === TOKEN_2022_PROGRAM_ID) {
|
|
153
176
|
meta.tokenProgram = raw.tokenProgram;
|
|
154
|
-
|
|
177
|
+
meta.programUnverified = true;
|
|
178
|
+
}
|
|
179
|
+
// 写入时间只接受有限值、且不晚于现在(留 1 分钟时钟误差):未来的时间会让 name / symbol 永不过期,截到现在
|
|
180
|
+
if (typeof raw.namesAt === 'number' &&
|
|
181
|
+
Number.isFinite(raw.namesAt) &&
|
|
182
|
+
(typeof raw.name === 'string' || raw.name === null) &&
|
|
183
|
+
(typeof raw.symbol === 'string' || raw.symbol === null)) {
|
|
155
184
|
meta.name = raw.name;
|
|
156
185
|
meta.symbol = raw.symbol;
|
|
157
|
-
meta.namesAt = raw.namesAt;
|
|
186
|
+
meta.namesAt = Math.min(raw.namesAt, Date.now());
|
|
158
187
|
}
|
|
159
188
|
return Object.keys(meta).length ? meta : null;
|
|
160
189
|
}
|
|
@@ -203,6 +232,27 @@ export function persistTokenMetaCache(store, options = {}) {
|
|
|
203
232
|
}
|
|
204
233
|
};
|
|
205
234
|
}
|
|
235
|
+
/** jsonParsed 结果里的一个代币账户:节点解析好的直接取;退回 base64 的自己解析(这时没有 decimals,要从 mint 账户补) */
|
|
236
|
+
function readTokenEntry(entry) {
|
|
237
|
+
const data = entry.account?.data;
|
|
238
|
+
if (data && !Array.isArray(data) && typeof data === 'object') {
|
|
239
|
+
const info = data.parsed?.info;
|
|
240
|
+
if (info?.mint && info.tokenAmount?.decimals !== undefined) {
|
|
241
|
+
return { mint: info.mint, amount: BigInt(info.tokenAmount.amount ?? '0'), decimals: info.tokenAmount.decimals };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const encoded = Array.isArray(data) ? (data[1] === 'base64' ? data[0] : undefined) : typeof data === 'string' ? data : undefined;
|
|
245
|
+
if (encoded === undefined) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const parsed = parseTokenAccount(base64.decode(encoded));
|
|
250
|
+
return { mint: parsed.mint, amount: parsed.amount, decimals: undefined };
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
206
256
|
export class SolanaClient {
|
|
207
257
|
transport;
|
|
208
258
|
#genesis;
|
|
@@ -273,56 +323,79 @@ export class SolanaClient {
|
|
|
273
323
|
return addresses.map((address) => fetched.get(address) ?? null);
|
|
274
324
|
}
|
|
275
325
|
/**
|
|
276
|
-
* getMultipleAccounts
|
|
277
|
-
*
|
|
326
|
+
* 只读 lamports(主币余额):getMultipleAccounts 带 dataSlice { offset: 0, length: 0 },不读回账户数据——
|
|
327
|
+
* 钱包可能是数据很大的程序账户(上百 KB),整份读回会拖慢甚至超时。结果与 addresses 一一对应,账户不存在或地址非法时为 null
|
|
278
328
|
*/
|
|
329
|
+
async #lamports(addresses, ctx) {
|
|
330
|
+
const [fetched] = await this.#fetchGroups([{ addresses, lamportsOnly: true }], false, ctx);
|
|
331
|
+
return addresses.map((address) => fetched?.get(address)?.lamports ?? null);
|
|
332
|
+
}
|
|
333
|
+
/** getMultipleAccounts(读账户数据):见 #fetchGroups */
|
|
279
334
|
async #fetchAccounts(addresses, tolerant, ctx) {
|
|
280
|
-
const
|
|
335
|
+
const [fetched] = await this.#fetchGroups([{ addresses, lamportsOnly: false }], tolerant, ctx);
|
|
336
|
+
return fetched;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* getMultipleAccounts:每组去重、按 100 个一组拆分,所有组的请求一起排队,最多同时 3 个(避免大批量时触发节点限频)。
|
|
340
|
+
* lamportsOnly 的组带 dataSlice(长度 0,对同一请求里的所有账户生效,所以只要 lamports 的账户要单独成组)。
|
|
341
|
+
* tolerant 为 true 时,某个请求因节点问题(网络、限频等)失败不抛错:该请求的账户标记为 FAILED,并且不再发后面的请求(节点已经在限频了);
|
|
342
|
+
* 否则第一个错误抛出,后面的请求也不再发。结果按组返回
|
|
343
|
+
*/
|
|
344
|
+
async #fetchGroups(groups, tolerant, ctx) {
|
|
281
345
|
const chunks = [];
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
346
|
+
groups.forEach(({ addresses, lamportsOnly }, group) => {
|
|
347
|
+
const unique = [...new Set(addresses.filter((a) => isAddress(a)))];
|
|
348
|
+
for (let i = 0; i < unique.length; i += MAX_ACCOUNTS_PER_REQUEST) {
|
|
349
|
+
chunks.push({ group, addresses: unique.slice(i, i + MAX_ACCOUNTS_PER_REQUEST), lamportsOnly });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
const results = groups.map(() => new Map());
|
|
286
353
|
let aborted = false;
|
|
287
354
|
for (let i = 0; i < chunks.length; i += MAX_PARALLEL_ACCOUNT_REQUESTS) {
|
|
288
355
|
const wave = chunks.slice(i, i + MAX_PARALLEL_ACCOUNT_REQUESTS);
|
|
289
|
-
//
|
|
356
|
+
// 已取消的查询不再发后面的请求
|
|
290
357
|
if (ctx.signal?.aborted) {
|
|
291
358
|
throw ctx.signal.reason;
|
|
292
359
|
}
|
|
293
360
|
if (aborted) {
|
|
294
|
-
wave.
|
|
361
|
+
wave.forEach((chunk) => chunk.addresses.forEach((address) => results[chunk.group]?.set(address, FAILED)));
|
|
295
362
|
continue;
|
|
296
363
|
}
|
|
297
|
-
|
|
364
|
+
// 同一批里的请求同时发出(合并成一个 JSON-RPC 批量请求);一个失败时等这一批都结束再抛,后面的批次不再发
|
|
365
|
+
const settled = await Promise.allSettled(wave.map(async (chunk) => {
|
|
366
|
+
const result = results[chunk.group];
|
|
298
367
|
try {
|
|
299
368
|
const res = await this.#call(ctx, 'getMultipleAccounts', [
|
|
300
|
-
chunk,
|
|
301
|
-
{ encoding: 'base64', commitment: this.#commitment },
|
|
369
|
+
chunk.addresses,
|
|
370
|
+
{ encoding: 'base64', commitment: this.#commitment, ...(chunk.lamportsOnly ? { dataSlice: { offset: 0, length: 0 } } : {}) },
|
|
302
371
|
]);
|
|
303
|
-
chunk.forEach((address, j) => result.set(address, toAccountInfo(address, res?.value?.[j] ?? null)));
|
|
372
|
+
chunk.addresses.forEach((address, j) => result.set(address, toAccountInfo(address, res?.value?.[j] ?? null)));
|
|
304
373
|
}
|
|
305
374
|
catch (err) {
|
|
306
375
|
if (!tolerant || !isRecoverable(err)) {
|
|
307
376
|
throw err;
|
|
308
377
|
}
|
|
309
378
|
aborted = true;
|
|
310
|
-
chunk.forEach((address) => result.set(address, FAILED));
|
|
379
|
+
chunk.addresses.forEach((address) => result.set(address, FAILED));
|
|
311
380
|
}
|
|
312
381
|
}));
|
|
382
|
+
const failed = settled.find((r) => r.status === 'rejected');
|
|
383
|
+
if (failed) {
|
|
384
|
+
throw failed.reason;
|
|
385
|
+
}
|
|
313
386
|
}
|
|
314
|
-
return
|
|
387
|
+
return results;
|
|
315
388
|
}
|
|
316
|
-
/** 批量查多个地址的 SOL
|
|
389
|
+
/** 批量查多个地址的 SOL 余额(getMultipleAccounts 只读 lamports、不读回账户数据;账户不存在视为 0) */
|
|
317
390
|
solBalances(addresses, options = {}) {
|
|
318
391
|
return abortable(options.signal, async () => {
|
|
319
392
|
const ctx = this.#context(options);
|
|
320
|
-
const
|
|
393
|
+
const balances = await this.#lamports(addresses, ctx);
|
|
321
394
|
const list = addresses.map((address, i) => {
|
|
322
395
|
if (!isAddress(address)) {
|
|
323
396
|
return { address, balance: '0', formatted: '0', success: false, error: 'invalid-address' };
|
|
324
397
|
}
|
|
325
|
-
const lamports =
|
|
398
|
+
const lamports = balances[i] ?? 0n;
|
|
326
399
|
return { address, balance: lamports.toString(), formatted: formatUnits(lamports, SOL_DECIMALS), success: true };
|
|
327
400
|
});
|
|
328
401
|
return this.#attachSlot(list, ctx, options);
|
|
@@ -334,7 +407,7 @@ export class SolanaClient {
|
|
|
334
407
|
/**
|
|
335
408
|
* 批量查余额(主币 SOL + SPL Token + Token-2022),返回原始余额、decimals 和换算后的数值。
|
|
336
409
|
*
|
|
337
|
-
* - 传 mints:本地推导 ATA
|
|
410
|
+
* - 传 mints:本地推导 ATA,与 mint 账户一起用 getMultipleAccounts 读完,免费节点也能用;主币只读 lamports(dataSlice),另成一组同时发出
|
|
338
411
|
* - 传 mints 且 accounts: 'all':每个代币按 mint 过滤查 getTokenAccountsByOwner,统计全部代币账户(含非 ATA)
|
|
339
412
|
* - 不传 mints(或 scan: true):用 getTokenAccountsByOwner 扫描全部代币账户,返回所有余额大于 0 的代币
|
|
340
413
|
* - 失败项(success: false)带 error:invalid-address(mint 地址非法)/ not-found(mint 不存在)/ not-token(不是代币 mint)
|
|
@@ -389,25 +462,45 @@ export class SolanaClient {
|
|
|
389
462
|
/** ATA 模式:所有钱包的 主币账户 / mint / 元数据 / ATA 一起读,结果按钱包分组 */
|
|
390
463
|
async #ataBalances(ctx, queries, withSymbol, allowed) {
|
|
391
464
|
const scope = this.#scope();
|
|
465
|
+
// 要数据的账户(mint、元数据、ATA)一组;钱包账户只要 lamports:已知是普通钱包(系统账户 / 不存在,数据为空)的
|
|
466
|
+
// 放进同一组(一个请求、同一个快照),其余(可能是数据很大的程序账户)单独一组带 dataSlice——同时发出,合并在同一个 JSON-RPC 批量请求里
|
|
392
467
|
const addresses = [];
|
|
393
|
-
const
|
|
394
|
-
const
|
|
395
|
-
|
|
468
|
+
const owners = [];
|
|
469
|
+
const plans = queries.map(({ owner, mints }) => mints.map((mint) => this.#ataPlan(scope, owner, mint, withSymbol, allowed, addresses, owners)));
|
|
470
|
+
const [dataMap, ownerMap] = await this.#fetchGroups([
|
|
471
|
+
{ addresses, lamportsOnly: false },
|
|
472
|
+
{ addresses: owners, lamportsOnly: true },
|
|
473
|
+
], false, ctx);
|
|
474
|
+
// 记下钱包账户是否数据为空:单独读的按结果学习;已放进代币那组的,读到不再是系统账户(如被分配给程序)时移除,下次单独读
|
|
475
|
+
for (const owner of new Set(queries.map((q) => q.owner))) {
|
|
476
|
+
// null(账户不存在)也是结果,不能用 ?? 落到另一组
|
|
477
|
+
const account = ownerMap?.has(owner) ? ownerMap.get(owner) : dataMap?.get(owner);
|
|
478
|
+
if (account !== undefined && account !== FAILED) {
|
|
479
|
+
learnOwner(scope, owner, account);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const accounts = addresses.map((address) => dataMap?.get(address) ?? null);
|
|
483
|
+
const lamportsOf = (owner) => (ownerMap?.has(owner) ? ownerMap.get(owner) : dataMap?.get(owner))?.lamports ?? 0n;
|
|
484
|
+
return plans.map((plan) => plan.flatMap((item) => this.#ataResult(scope, item, accounts, lamportsOf, withSymbol, allowed)));
|
|
396
485
|
}
|
|
397
|
-
#ataPlan(scope, owner, mint, withSymbol, allowed, addresses) {
|
|
486
|
+
#ataPlan(scope, owner, mint, withSymbol, allowed, addresses, owners) {
|
|
398
487
|
if (this.#nativeMints.has(mint)) {
|
|
399
|
-
|
|
488
|
+
;
|
|
489
|
+
(isSmallOwner(scope, owner) ? addresses : owners).push(owner);
|
|
490
|
+
return { kind: 'native', mint, owner };
|
|
400
491
|
}
|
|
401
492
|
if (!isAddress(mint)) {
|
|
402
493
|
return { kind: 'invalid', mint };
|
|
403
494
|
}
|
|
404
495
|
const meta = getMeta(scope, mint);
|
|
405
|
-
|
|
496
|
+
// 导入的缓存里的所属程序没确认过:按未知处理(记错时不会让余额恒为 0)
|
|
497
|
+
const program = meta.programUnverified ? undefined : meta.tokenProgram;
|
|
498
|
+
if (isExcluded(program, allowed)) {
|
|
406
499
|
return { kind: 'excluded', mint };
|
|
407
500
|
}
|
|
408
|
-
const needMint = meta.decimals === undefined ||
|
|
501
|
+
const needMint = meta.decimals === undefined || program === undefined || (withSymbol && meta.symbol === undefined);
|
|
409
502
|
// 所属程序未知时只推导允许的程序的 ATA,读到 mint 账户后再确认
|
|
410
|
-
const programs =
|
|
503
|
+
const programs = program ? [program] : allowed;
|
|
411
504
|
return {
|
|
412
505
|
kind: 'token',
|
|
413
506
|
mint,
|
|
@@ -416,9 +509,9 @@ export class SolanaClient {
|
|
|
416
509
|
atas: programs.map((program) => ({ program, index: addresses.push(getAssociatedTokenAddress(owner, mint, program)) - 1 })),
|
|
417
510
|
};
|
|
418
511
|
}
|
|
419
|
-
#ataResult(scope, item, accounts, withSymbol, allowed) {
|
|
512
|
+
#ataResult(scope, item, accounts, lamportsOf, withSymbol, allowed) {
|
|
420
513
|
if (item.kind === 'native') {
|
|
421
|
-
const lamports =
|
|
514
|
+
const lamports = lamportsOf(item.owner);
|
|
422
515
|
return [
|
|
423
516
|
withSymbolField({ token: item.mint, native: true, balance: lamports.toString(), decimals: SOL_DECIMALS, formatted: formatUnits(lamports, SOL_DECIMALS), tokenProgram: null, success: true }, withSymbol, 'SOL'),
|
|
424
517
|
];
|
|
@@ -431,22 +524,25 @@ export class SolanaClient {
|
|
|
431
524
|
}
|
|
432
525
|
const mintAccount = item.mintIndex === -1 ? null : (accounts[item.mintIndex] ?? null);
|
|
433
526
|
const meta = this.#learn(scope, item.mint, mintAccount, item.metadataIndex === -1 ? null : (accounts[item.metadataIndex] ?? null), item.metadataIndex !== -1 && item.mintIndex !== -1);
|
|
434
|
-
// 代币账户由哪个程序持有就是哪个;mint 账户的 owner
|
|
527
|
+
// 代币账户由哪个程序持有就是哪个;mint 账户的 owner 也能确定程序。
|
|
528
|
+
// 所属程序未知时推导了两个程序的 ATA,但一个 mint 只属于一个程序,另一个程序下不可能有这个 mint 的代币账户:找到就结束
|
|
435
529
|
let amount = 0n;
|
|
436
530
|
for (const ata of item.atas) {
|
|
437
531
|
const account = accounts[ata.index];
|
|
438
|
-
if (account
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
}
|
|
532
|
+
if (!account || account.owner !== ata.program) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
amount = parseTokenAccount(account.data).amount;
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
continue; // 不是代币账户
|
|
540
|
+
}
|
|
541
|
+
if (meta.tokenProgram === undefined) {
|
|
542
|
+
setMeta(scope, item.mint, { tokenProgram: ata.program });
|
|
543
|
+
meta.tokenProgram = ata.program;
|
|
449
544
|
}
|
|
545
|
+
break;
|
|
450
546
|
}
|
|
451
547
|
// 读到 mint 账户后才知道是不允许的程序
|
|
452
548
|
if (isExcluded(meta.tokenProgram, allowed)) {
|
|
@@ -469,28 +565,47 @@ export class SolanaClient {
|
|
|
469
565
|
];
|
|
470
566
|
}
|
|
471
567
|
/** 扫描持有人的 SOL 余额和全部代币账户(默认 Token + Token-2022,只请求 programs 里的程序),同一代币的多个账户合计 */
|
|
472
|
-
async #scanHoldings(ctx, owner, programs = TOKEN_PROGRAMS) {
|
|
568
|
+
async #scanHoldings(ctx, owner, programs = TOKEN_PROGRAMS, withSymbol = false) {
|
|
473
569
|
const scope = this.#scope();
|
|
474
570
|
const [lamports, ...results] = await Promise.all([
|
|
475
571
|
this.#call(ctx, 'getBalance', [owner, { commitment: this.#commitment }]),
|
|
476
572
|
...programs.map((programId) => this.#call(ctx, 'getTokenAccountsByOwner', [owner, { programId }, { encoding: 'jsonParsed', commitment: this.#commitment }])),
|
|
477
573
|
]);
|
|
574
|
+
const entries = programs.flatMap((program, i) => (results[i]?.value ?? []).map((entry) => ({ program, entry })));
|
|
575
|
+
const held = await this.#collectHoldings(ctx, scope, entries, withSymbol);
|
|
576
|
+
return { sol: BigInt(lamports.value), held };
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* 把 getTokenAccountsByOwner(jsonParsed)的结果按 mint 汇总。节点解析不了、退回 base64 的账户自己解析,
|
|
580
|
+
* decimals 读 mint 账户补上(不丢弃,也不会把余额算成 0);mint 读不到 decimals 时才跳过
|
|
581
|
+
*/
|
|
582
|
+
async #collectHoldings(ctx, scope, entries, withSymbol = false) {
|
|
583
|
+
const read = entries.flatMap(({ program, entry }) => {
|
|
584
|
+
const token = readTokenEntry(entry);
|
|
585
|
+
return token ? [{ ...token, program: entry.account?.owner || program }] : [];
|
|
586
|
+
});
|
|
587
|
+
read.forEach(({ mint, decimals, program }) => decimals !== undefined && program !== undefined && setMeta(scope, mint, { decimals, tokenProgram: program }));
|
|
588
|
+
// 缺 decimals 或所属程序(节点没给账户 owner)的读 mint 账户补上;需要 symbol 时一起读,后面不必再读一遍
|
|
589
|
+
const unknown = [
|
|
590
|
+
...new Set(read.filter((t) => getMeta(scope, t.mint).decimals === undefined || (t.program === undefined && getMeta(scope, t.mint).tokenProgram === undefined)).map((t) => t.mint)),
|
|
591
|
+
];
|
|
592
|
+
if (unknown.length) {
|
|
593
|
+
// 补读 mint 不受 minContextSlot 约束、不计入扫描的 slot(decimals / 所属程序不会因为刚成交而变化);
|
|
594
|
+
// 节点问题时不让整次扫描失败,读不到的代币跳过
|
|
595
|
+
await this.#loadMeta({ signal: ctx.signal }, scope, unknown, withSymbol, true);
|
|
596
|
+
}
|
|
478
597
|
const held = new Map();
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
const prev = held.get(mint);
|
|
488
|
-
const amount = BigInt(info?.tokenAmount?.amount ?? '0');
|
|
489
|
-
held.set(mint, { amount: (prev?.amount ?? 0n) + amount, decimals, program, accounts: (prev?.accounts ?? 0) + 1 });
|
|
490
|
-
setMeta(scope, mint, { decimals, tokenProgram: program });
|
|
598
|
+
for (const { mint, amount, program: entryProgram } of read) {
|
|
599
|
+
const meta = getMeta(scope, mint);
|
|
600
|
+
const decimals = meta.decimals;
|
|
601
|
+
const program = entryProgram ?? (meta.programUnverified ? undefined : meta.tokenProgram);
|
|
602
|
+
if (decimals === undefined || program === undefined) {
|
|
603
|
+
continue;
|
|
491
604
|
}
|
|
492
|
-
|
|
493
|
-
|
|
605
|
+
const prev = held.get(mint);
|
|
606
|
+
held.set(mint, { amount: (prev?.amount ?? 0n) + amount, decimals, program, accounts: (prev?.accounts ?? 0) + 1 });
|
|
607
|
+
}
|
|
608
|
+
return held;
|
|
494
609
|
}
|
|
495
610
|
/**
|
|
496
611
|
* 列出持有人拥有的全部代币(SOL 在第一位),带 name / symbol,适合做资产列表。
|
|
@@ -510,7 +625,7 @@ export class SolanaClient {
|
|
|
510
625
|
const { metadata = true, includeNative = true, includeZero = false, includeNfts = false } = options;
|
|
511
626
|
const scope = this.#scope();
|
|
512
627
|
const ctx = this.#context(options);
|
|
513
|
-
const { sol, held } = await this.#scanHoldings(ctx, owner, resolvePrograms(options.tokenPrograms));
|
|
628
|
+
const { sol, held } = await this.#scanHoldings(ctx, owner, resolvePrograms(options.tokenPrograms), metadata);
|
|
514
629
|
const scanSlot = ctx.slot;
|
|
515
630
|
// Wrapped SOL 是独立的代币账户(需要 unwrap 才是 SOL),资产列表里单独列出(native: false)
|
|
516
631
|
const tokens = [...held].filter(([, h]) => (includeZero || h.amount > 0n) && (includeNfts || !isNftHolding(h)));
|
|
@@ -544,7 +659,7 @@ export class SolanaClient {
|
|
|
544
659
|
async #scanBalances(ctx, owner, mints, withSymbol, allowed) {
|
|
545
660
|
const scope = this.#scope();
|
|
546
661
|
// held 里只有允许的程序的代币
|
|
547
|
-
const { sol, held } = await this.#scanHoldings(ctx, owner, allowed);
|
|
662
|
+
const { sol, held } = await this.#scanHoldings(ctx, owner, allowed, withSymbol);
|
|
548
663
|
// Wrapped SOL(So111…112)默认按原生 SOL 处理:余额列表里只有一项 SOL,不再列出 wSOL 代币账户
|
|
549
664
|
// 第一项是主币:用 nativeMints 里的地址(nativeMints 不含 So111…112 时 wSOL 作为普通代币列出,不能再拿它当 SOL)
|
|
550
665
|
const nativeLead = this.#nativeMints.has(NATIVE_MINT) ? NATIVE_MINT : this.#nativeMints.values().next().value;
|
|
@@ -569,7 +684,13 @@ export class SolanaClient {
|
|
|
569
684
|
*/
|
|
570
685
|
async #mintAccountBalances(ctx, owner, mints, withSymbol, allowed) {
|
|
571
686
|
const scope = this.#scope();
|
|
572
|
-
|
|
687
|
+
// 导入的、未经链上确认的所属程序不用来排除代币(可能记错):照常查询,从链上学到后再按程序过滤
|
|
688
|
+
const queried = [
|
|
689
|
+
...new Set(mints.filter((mint) => {
|
|
690
|
+
const meta = getMeta(scope, mint);
|
|
691
|
+
return !this.#nativeMints.has(mint) && isAddress(mint) && (meta.programUnverified || !isExcluded(meta.tokenProgram, allowed));
|
|
692
|
+
})),
|
|
693
|
+
];
|
|
573
694
|
const [lamports, ...results] = await Promise.all([
|
|
574
695
|
mints.some((mint) => this.#nativeMints.has(mint))
|
|
575
696
|
? this.#call(ctx, 'getBalance', [owner, { commitment: this.#commitment }])
|
|
@@ -581,20 +702,7 @@ export class SolanaClient {
|
|
|
581
702
|
throw err;
|
|
582
703
|
})),
|
|
583
704
|
]);
|
|
584
|
-
const held =
|
|
585
|
-
queried.forEach((mint, i) => {
|
|
586
|
-
for (const entry of results[i]?.value ?? []) {
|
|
587
|
-
const decimals = entry.account?.data?.parsed?.info?.tokenAmount?.decimals;
|
|
588
|
-
const program = entry.account?.owner;
|
|
589
|
-
if (decimals === undefined || !program) {
|
|
590
|
-
continue;
|
|
591
|
-
}
|
|
592
|
-
const prev = held.get(mint);
|
|
593
|
-
const amount = BigInt(entry.account.data.parsed?.info?.tokenAmount?.amount ?? '0');
|
|
594
|
-
held.set(mint, { amount: (prev?.amount ?? 0n) + amount, decimals, program, accounts: (prev?.accounts ?? 0) + 1 });
|
|
595
|
-
setMeta(scope, mint, { decimals, tokenProgram: program });
|
|
596
|
-
}
|
|
597
|
-
});
|
|
705
|
+
const held = await this.#collectHoldings(ctx, scope, queried.flatMap((_, i) => (results[i]?.value ?? []).map((entry) => ({ program: entry.account?.owner, entry }))), withSymbol);
|
|
598
706
|
// 没持有的代币补 decimals(同时确认 mint 是否存在),需要时补 symbol
|
|
599
707
|
const failures = await this.#loadMeta(ctx, scope, queried, withSymbol);
|
|
600
708
|
const sol = BigInt(lamports?.value ?? 0);
|
|
@@ -916,7 +1024,9 @@ function safeParseMint(account) {
|
|
|
916
1024
|
return null;
|
|
917
1025
|
}
|
|
918
1026
|
try {
|
|
919
|
-
|
|
1027
|
+
const mint = parseMint(account.data);
|
|
1028
|
+
// 未初始化的 mint(创建和初始化分在两笔交易里,恰好在中间读到):decimals 还是 0,不能当真、更不能永久缓存
|
|
1029
|
+
return mint.isInitialized ? mint : null;
|
|
920
1030
|
}
|
|
921
1031
|
catch {
|
|
922
1032
|
return null;
|
|
@@ -952,6 +1062,9 @@ function setMeta(scope, mint, meta) {
|
|
|
952
1062
|
}
|
|
953
1063
|
const key = `${scope}:${mint}`;
|
|
954
1064
|
const merged = { ...tokenMetaCache.get(key), ...defined };
|
|
1065
|
+
if ('tokenProgram' in defined && !defined.programUnverified) {
|
|
1066
|
+
delete merged.programUnverified; // 从链上学到了所属程序
|
|
1067
|
+
}
|
|
955
1068
|
tokenMetaCache.delete(key); // 重新插入,保持“最近写入在后”的顺序
|
|
956
1069
|
tokenMetaCache.set(key, merged);
|
|
957
1070
|
if (tokenMetaCache.size > MAX_CACHED_TOKENS) {
|