@w3lib/evm-batch-call 0.2.0 → 0.4.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 +56 -0
- package/README.md +184 -30
- package/contracts/DeploylessMulticall3.sol +5 -0
- package/dist/aggregate.d.ts +25 -3
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js +143 -5
- package/dist/aggregate.js.map +1 -1
- package/dist/batcher.d.ts +6 -1
- package/dist/batcher.d.ts.map +1 -1
- package/dist/batcher.js +59 -9
- package/dist/batcher.js.map +1 -1
- package/dist/call.d.ts +27 -2
- package/dist/call.d.ts.map +1 -1
- package/dist/call.js +22 -0
- package/dist/call.js.map +1 -1
- package/dist/chains.d.ts +7 -0
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +11 -0
- package/dist/chains.js.map +1 -1
- package/dist/deployless.d.ts +1 -1
- package/dist/deployless.d.ts.map +1 -1
- package/dist/deployless.js +1 -1
- package/dist/deployless.js.map +1 -1
- package/dist/erc20.d.ts +70 -4
- package/dist/erc20.d.ts.map +1 -1
- package/dist/erc20.js +121 -5
- package/dist/erc20.js.map +1 -1
- package/dist/events.d.ts +25 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +25 -0
- package/dist/events.js.map +1 -0
- package/dist/fallback.d.ts +54 -3
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +273 -21
- package/dist/fallback.js.map +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/multicall.d.ts.map +1 -1
- package/dist/multicall.js +33 -2
- package/dist/multicall.js.map +1 -1
- package/dist/nft.d.ts +5 -0
- package/dist/nft.d.ts.map +1 -1
- package/dist/nft.js +5 -0
- package/dist/nft.js.map +1 -1
- package/dist/owner.d.ts +145 -0
- package/dist/owner.d.ts.map +1 -0
- package/dist/owner.js +510 -0
- package/dist/owner.js.map +1 -0
- package/dist/provider.d.ts +63 -18
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +166 -100
- package/dist/provider.js.map +1 -1
- package/dist/scan.d.ts +59 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +245 -0
- package/dist/scan.js.map +1 -0
- package/dist/shortcuts.d.ts +34 -35
- package/dist/shortcuts.d.ts.map +1 -1
- package/dist/shortcuts.js +41 -73
- package/dist/shortcuts.js.map +1 -1
- package/dist/source.d.ts +6 -2
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +49 -6
- package/dist/source.js.map +1 -1
- package/dist/subpaths/nft.d.ts +46 -0
- package/dist/subpaths/nft.d.ts.map +1 -0
- package/dist/subpaths/nft.js +55 -0
- package/dist/subpaths/nft.js.map +1 -0
- package/dist/subpaths/owner.d.ts +33 -0
- package/dist/subpaths/owner.d.ts.map +1 -0
- package/dist/subpaths/owner.js +34 -0
- package/dist/subpaths/owner.js.map +1 -0
- package/dist/tron.d.ts +2 -0
- package/dist/tron.d.ts.map +1 -1
- package/dist/tron.js +5 -0
- package/dist/tron.js.map +1 -1
- package/dist/util.d.ts +5 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +23 -0
- package/dist/util.js.map +1 -1
- package/dist/watch.d.ts +25 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +105 -0
- package/dist/watch.js.map +1 -0
- package/package.json +19 -10
- package/src/aggregate.ts +177 -9
- package/src/batcher.ts +69 -9
- package/src/call.ts +42 -2
- package/src/chains.ts +15 -0
- package/src/deployless.ts +1 -1
- package/src/erc20.ts +171 -8
- package/src/events.ts +42 -0
- package/src/fallback.ts +306 -22
- package/src/index.ts +20 -27
- package/src/multicall.ts +34 -2
- package/src/nft.ts +5 -0
- package/src/owner.ts +660 -0
- package/src/provider.ts +255 -112
- package/src/scan.ts +293 -0
- package/src/shortcuts.ts +76 -88
- package/src/source.ts +51 -8
- package/src/subpaths/nft.ts +107 -0
- package/src/subpaths/owner.ts +69 -0
- package/src/tron.ts +7 -0
- package/src/util.ts +27 -0
- package/src/watch.ts +149 -0
package/src/call.ts
CHANGED
|
@@ -32,9 +32,10 @@ export interface Call<T = any> {
|
|
|
32
32
|
/**
|
|
33
33
|
* 内部标记:
|
|
34
34
|
* - ethBalance:原生币余额查询(见 Provider.getEthBalance)
|
|
35
|
+
* - blockNumber:当前区块号(Multicall3.getBlockNumber;deployless 合约里直接返回 block.number)
|
|
35
36
|
* - stringOrBytes32:string 返回值,解码失败时按 bytes32 解析(MKR 等老代币的 symbol / name)
|
|
36
37
|
*/
|
|
37
|
-
kind?: 'ethBalance' | 'stringOrBytes32'
|
|
38
|
+
kind?: 'ethBalance' | 'blockNumber' | 'stringOrBytes32'
|
|
38
39
|
/** 仅用于类型推断 */
|
|
39
40
|
readonly __result?: T
|
|
40
41
|
}
|
|
@@ -52,13 +53,51 @@ export interface RawResult {
|
|
|
52
53
|
returnData: string
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 单条调用的失败原因:
|
|
58
|
+
* - invalid-address:合约地址或参数里的地址非法
|
|
59
|
+
* - invalid-argument:其他参数编码失败(个数不对、数值超出范围等)
|
|
60
|
+
* - no-contract:调用成功但返回空数据,多半是目标地址上没有合约
|
|
61
|
+
* - reverted:执行 revert
|
|
62
|
+
* - decode-failed:有返回数据但无法按 ABI 解码(如合约不是预期的标准)
|
|
63
|
+
* - not-configured:没有发请求的信息缺失,如主币不在内置链信息表里、也没有配置 nativeName / nativeSymbol
|
|
64
|
+
*/
|
|
65
|
+
export type FailureReason = 'invalid-address' | 'invalid-argument' | 'no-contract' | 'reverted' | 'decode-failed' | 'not-configured'
|
|
66
|
+
|
|
67
|
+
/** 单条调用的结果:成功的值,或失败原因 */
|
|
68
|
+
export type Settled<T = any> = { ok: true; value: T } | { ok: false; reason: FailureReason }
|
|
69
|
+
|
|
70
|
+
/** 按 multicall 的原始结果判断成功 / 失败原因 */
|
|
71
|
+
export function settleResult<T>(call: Call, result: RawResult): Settled<T> {
|
|
72
|
+
if (!result.success) {
|
|
73
|
+
return { ok: false, reason: 'reverted' }
|
|
74
|
+
}
|
|
75
|
+
// 没有返回值的函数成功时本来就返回空数据,照常解码;有返回值却拿到空数据,多半是地址上没有合约
|
|
76
|
+
if ((!result.returnData || result.returnData === '0x') && call.outputs.length > 0) {
|
|
77
|
+
return { ok: false, reason: 'no-contract' }
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return { ok: true, value: decodeCall<T>(call, result.returnData) }
|
|
81
|
+
} catch {
|
|
82
|
+
return { ok: false, reason: 'decode-failed' }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 参数编码失败的原因:地址非法,还是其他参数问题 */
|
|
87
|
+
export function encodeFailureReason(err: unknown): FailureReason {
|
|
88
|
+
const e = err as { code?: unknown; argument?: unknown; message?: unknown } | null
|
|
89
|
+
return /address/i.test(String(e?.argument ?? '')) || /address/i.test(String(e?.message ?? '')) ? 'invalid-address' : 'invalid-argument'
|
|
90
|
+
}
|
|
91
|
+
|
|
55
92
|
/** 发给 aggregate3 前的单条请求 */
|
|
56
93
|
export interface CallRequest {
|
|
57
94
|
target: string
|
|
58
95
|
allowFailure: boolean
|
|
59
96
|
callData: string
|
|
60
|
-
/**
|
|
97
|
+
/** 原生币余额查询:默认有 multicall 合约时走 getEthBalance、deployless 时用 BALANCE;其他读取方式见 NativeBalanceMode */
|
|
61
98
|
ethBalanceOf?: string
|
|
99
|
+
/** 区块号查询:目标换成实际的 multicall 地址(deployless 时由合约内部处理) */
|
|
100
|
+
blockNumber?: boolean
|
|
62
101
|
}
|
|
63
102
|
|
|
64
103
|
// inputs → name → outputs → Interface。同一份 ABI 生成的 Call 共享 inputs/outputs 数组(见 contract.ts 的 ABI 缓存),
|
|
@@ -100,6 +139,7 @@ export function encodeCall(call: Call, allowFailure: boolean): CallRequest {
|
|
|
100
139
|
allowFailure,
|
|
101
140
|
callData: iface.encodeFunctionData(fragment, params),
|
|
102
141
|
ethBalanceOf: call.kind === 'ethBalance' ? toEvmAddress(params[0] as string) : undefined,
|
|
142
|
+
blockNumber: call.kind === 'blockNumber' ? true : undefined,
|
|
103
143
|
}
|
|
104
144
|
}
|
|
105
145
|
|
package/src/chains.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { NativeBalanceMode } from './aggregate.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* 内置链的主币信息(balances() / tokens() 里主币的 name / symbol / 精度)。
|
|
3
5
|
* 来源:https://chainid.network/chains.json 的 nativeCurrency;999 在该表里与 Wanchain 测试网撞号,
|
|
@@ -44,3 +46,16 @@ export const NATIVE_CURRENCIES: Readonly<Record<number, NativeCurrency>> = {
|
|
|
44
46
|
export function getNativeCurrency(chainId: number): NativeCurrency | undefined {
|
|
45
47
|
return NATIVE_CURRENCIES[chainId]
|
|
46
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 合约里读主币余额不可靠的链,以及应改用的读取方式(ProviderConfig.nativeBalance 的默认值)。
|
|
52
|
+
* 新链可用 `pnpm check:rpc` 比对“合约里读到的主币余额”和 eth_getBalance 来发现。
|
|
53
|
+
*/
|
|
54
|
+
export const NATIVE_BALANCE_MODES: Readonly<Record<number, NativeBalanceMode>> = {
|
|
55
|
+
// Anubis:Multicall3.getEthBalance 和 deployless 里的 BALANCE 对任何地址都返回 0,eth_getBalance 正常
|
|
56
|
+
6714: 'rpc',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getNativeBalanceMode(chainId: number): NativeBalanceMode {
|
|
60
|
+
return NATIVE_BALANCE_MODES[chainId] ?? 'contract'
|
|
61
|
+
}
|
package/src/deployless.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
/** constructor((address target, bool allowFailure, bytes callData)[] calls) — 以 aggregate3 的返回格式带回结果 */
|
|
4
4
|
export const DEPLOYLESS_MULTICALL3_BYTECODE =
|
|
5
|
-
'
|
|
5
|
+
'0x60806040526040516106c13803806106c18339810160408190526100229161041e565b80516000816001600160401b0381111561003e5761003e61038c565b60405190808252806020026020018201604052801561008457816020015b60408051808201909152600081526060602082015281526020019060019003908161005c5790505b50905060005b828110156103395760008482815181106100a6576100a66105b9565b602090810291909101015160408101518151919250906001600160a01b031673ca11bde05977b3631167028862be2a173976ca111480156100e8575080516024145b801561010c5750631348c07360e21b610100826105cf565b6001600160e01b031916145b1561017e5760248101516040805180820182526001815290516001600160a01b039290921691602080830191610149918531910190815260200190565b60405160208183030381529060405281525085858151811061016d5761016d6105b9565b60200260200101819052505061032f565b81516001600160a01b031673ca11bde05977b3631167028862be2a173976ca111480156101ac575080516004145b80156101d057506310b2ec5760e21b6101c4826105cf565b6001600160e01b031916145b15610230576040518060400160405280600115158152602001436040516020016101fc91815260200190565b604051602081830303815290604052815250848481518110610220576102206105b9565b602002602001018190525061032f565b60008083600001516001600160a01b03168360405161024f919061060d565b6000604051808303816000865af19150503d806000811461028c576040519150601f19603f3d011682016040523d82523d6000602084013e610291565b606091505b5091509150811580156102a657508360200151155b156102f85760405162461bcd60e51b815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064015b60405180910390fd5b6040518060400160405280831515815260200182815250868681518110610321576103216105b9565b602002602001018190525050505b505060010161008a565b5060008160405160200161034d9190610629565b6040516020818303038152906040529050616000815111156103845781604051633649f05160e01b81526004016102ef9190610629565b805160208201f35b634e487b7160e01b600052604160045260246000fd5b604051606081016001600160401b03811182821017156103c4576103c461038c565b60405290565b604051601f8201601f191681016001600160401b03811182821017156103f2576103f261038c565b604052919050565b60005b838110156104155781810151838201526020016103fd565b50506000910152565b60006020828403121561043057600080fd5b81516001600160401b0381111561044657600080fd5b8201601f8101841361045757600080fd5b80516001600160401b038111156104705761047061038c565b8060051b610480602082016103ca565b9182526020818401810192908101908784111561049c57600080fd5b6020850192505b838310156105ae5782516001600160401b038111156104c157600080fd5b85016060818a03601f190112156104d757600080fd5b6104df6103a2565b60208201516001600160a01b03811681146104f957600080fd5b81526040820151801515811461050e57600080fd5b602082015260608201516001600160401b0381111561052c57600080fd5b60208184010192505089601f83011261054457600080fd5b81516001600160401b0381111561055d5761055d61038c565b610570601f8201601f19166020016103ca565b8181528b602083860101111561058557600080fd5b6105968260208301602087016103fa565b604083015250835250602092830192909101906104a3565b979650505050505050565b634e487b7160e01b600052603260045260246000fd5b805160208201516001600160e01b0319811691906004821015610606576001600160e01b0319600483900360031b81901b82161692505b5050919050565b6000825161061f8184602087016103fa565b9190910192915050565b6000602082016020835280845180835260408501915060408160051b86010192506020860160005b828110156106b457603f198786030184528151805115158652602081015190506040602087015280518060408801526106918160608901602085016103fa565b601f01601f19169590950160600194506020938401939190910190600101610651565b5092969550505050505056fe'
|
package/src/erc20.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BoundCall } from './call.js'
|
|
1
|
+
import type { BoundCall, FailureReason } from './call.js'
|
|
2
2
|
import type { BoundMethod, Contract } from './contract.js'
|
|
3
3
|
|
|
4
4
|
export const ERC20_ABI = [
|
|
@@ -41,9 +41,18 @@ export interface TokenAllowance {
|
|
|
41
41
|
formatted: string
|
|
42
42
|
/** 额度 ≥ uint96 最大值时为 true(覆盖 MaxUint256,以及 UNI / COMP 等把额度截断为 uint96 的代币) */
|
|
43
43
|
unlimited: boolean
|
|
44
|
+
/** 仅在 withBlock / minBlock 时返回:读取时的区块号(读取失败为 null) */
|
|
45
|
+
blockNumber?: number | null
|
|
44
46
|
success: boolean
|
|
47
|
+
/** 仅在 success 为 false 时返回:失败原因 */
|
|
48
|
+
error?: FailureReason
|
|
49
|
+
/** 仅在 success 为 false 时返回:哪一项没读到 */
|
|
50
|
+
errorField?: 'allowance' | 'decimals'
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/** allowances(..., { decimals: false }) 的单项结果:不查 decimals,不带 decimals / formatted */
|
|
54
|
+
export type RawTokenAllowance = Omit<TokenAllowance, 'decimals' | 'formatted'>
|
|
55
|
+
|
|
47
56
|
/** unlimited 的判定阈值:uint96 最大值 */
|
|
48
57
|
export const UNLIMITED_ALLOWANCE_THRESHOLD = 2n ** 96n - 1n
|
|
49
58
|
|
|
@@ -75,8 +84,14 @@ export type TokenDetails<F extends TokenField = DefaultTokenField> = {
|
|
|
75
84
|
address: string
|
|
76
85
|
/** 是否是主币占位地址(信息来自内置链信息表 / nativeSymbol 等配置) */
|
|
77
86
|
native: boolean
|
|
87
|
+
/** 仅在 withBlock / minBlock 时返回:读取时的区块号(读取失败为 null) */
|
|
88
|
+
blockNumber?: number | null
|
|
78
89
|
/** 请求的字段都读到时为 true(主币没有 totalSupply,不计入) */
|
|
79
90
|
success: boolean
|
|
91
|
+
/** 仅在 success 为 false 时返回:第一个没读到的字段的失败原因 */
|
|
92
|
+
error?: FailureReason
|
|
93
|
+
/** 仅在 success 为 false 时返回:第一个没读到的字段 */
|
|
94
|
+
errorField?: TokenField
|
|
80
95
|
} & { [K in F]: TokenFieldTypes[K] } & ('totalSupply' extends F ? { totalSupplyFormatted: string | null } : unknown)
|
|
81
96
|
|
|
82
97
|
/** 主币占位地址(多数聚合器、钱包的约定),传给 balances() 时按主币处理 */
|
|
@@ -100,15 +115,27 @@ export interface TokenBalance {
|
|
|
100
115
|
formatted: string
|
|
101
116
|
/** 仅在 symbol: true 时返回;读取失败或主币 symbol 未知时为 null */
|
|
102
117
|
symbol?: string | null
|
|
118
|
+
/** 仅在 withBlock / minBlock 时返回:读取时的区块号(与余额在同一次 eth_call 里读出;读取失败为 null) */
|
|
119
|
+
blockNumber?: number | null
|
|
103
120
|
/** 余额和 decimals 都查到时为 true;失败时 balance、formatted 都为 "0" */
|
|
104
121
|
success: boolean
|
|
122
|
+
/**
|
|
123
|
+
* 仅在 success 为 false 时返回:失败原因(invalid-address 地址非法、no-contract 地址上没有合约、
|
|
124
|
+
* reverted 执行 revert、decode-failed 返回值无法解码)。symbol 读不到不算失败(symbol 为 null)
|
|
125
|
+
*/
|
|
126
|
+
error?: FailureReason
|
|
127
|
+
/** 仅在 success 为 false 时返回:哪一项没读到(两项都失败时为 balance) */
|
|
128
|
+
errorField?: 'balance' | 'decimals'
|
|
105
129
|
}
|
|
106
130
|
|
|
131
|
+
/** balances(..., { decimals: false }) 的单项结果:不查 decimals,不带 decimals / formatted */
|
|
132
|
+
export type RawTokenBalance = Omit<TokenBalance, 'decimals' | 'formatted' | 'errorField'> & { errorField?: 'balance' }
|
|
133
|
+
|
|
107
134
|
/**
|
|
108
|
-
*
|
|
109
|
-
*
|
|
135
|
+
* 按精度格式化(与 ethers 的 formatUnits 同名同义):formatUnits(1234500000n, 6) === '1234.5',formatUnits(10n ** 18n, 18) === '1'。
|
|
136
|
+
* 不做四舍五入,保留全部有效小数位,整数不带小数点(与 ethers 不同:ethers 会返回 '1.0')。
|
|
110
137
|
*/
|
|
111
|
-
export function
|
|
138
|
+
export function formatUnits(value: bigint, decimals: number): string {
|
|
112
139
|
const negative = value < 0n
|
|
113
140
|
const digits = (negative ? -value : value).toString().padStart(decimals + 1, '0')
|
|
114
141
|
const whole = digits.slice(0, digits.length - decimals)
|
|
@@ -116,7 +143,11 @@ export function formatAmount(value: bigint, decimals: number): string {
|
|
|
116
143
|
return `${negative ? '-' : ''}${whole}${fraction ? `.${fraction}` : ''}`
|
|
117
144
|
}
|
|
118
145
|
|
|
119
|
-
|
|
146
|
+
/** @deprecated 改用 formatUnits(同一个函数;formatAmount 容易与项目里的数字展示函数撞名) */
|
|
147
|
+
export const formatAmount = formatUnits
|
|
148
|
+
|
|
149
|
+
// 代币信息缓存(按 chainId + 地址):decimals / NFT 标准不会变,永久缓存;name / symbol 在可升级合约上可能改,缓存 1 小时。
|
|
150
|
+
// 条目数有上限,超出时淘汰最早写入的(与 solana-batch-call 一致)。轮询余额时只需要查 balanceOf
|
|
120
151
|
interface TokenMeta {
|
|
121
152
|
decimals?: number
|
|
122
153
|
symbol?: string
|
|
@@ -125,21 +156,153 @@ interface TokenMeta {
|
|
|
125
156
|
standard?: 'ERC721' | 'ERC1155' | null
|
|
126
157
|
}
|
|
127
158
|
|
|
128
|
-
|
|
159
|
+
interface CachedMeta extends TokenMeta {
|
|
160
|
+
/** name / symbol 各自的写入时间(分开计:只刷新其中一个时,另一个照常过期) */
|
|
161
|
+
nameAt?: number
|
|
162
|
+
symbolAt?: number
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const NAME_TTL = 60 * 60 * 1000
|
|
166
|
+
const MAX_CACHED_TOKENS = 50_000
|
|
167
|
+
const tokenMetaCache = new Map<string, CachedMeta>()
|
|
168
|
+
const cacheListeners = new Set<() => void>()
|
|
129
169
|
|
|
130
170
|
export function getCachedTokenMeta(chainId: number, token: string): TokenMeta {
|
|
131
|
-
|
|
171
|
+
const cached = tokenMetaCache.get(`${chainId}:${token.toLowerCase()}`)
|
|
172
|
+
if (!cached) {
|
|
173
|
+
return {}
|
|
174
|
+
}
|
|
175
|
+
const { nameAt, symbolAt, ...meta } = cached
|
|
176
|
+
const now = Date.now()
|
|
177
|
+
if (nameAt === undefined || now - nameAt > NAME_TTL) {
|
|
178
|
+
delete meta.name
|
|
179
|
+
}
|
|
180
|
+
if (symbolAt === undefined || now - symbolAt > NAME_TTL) {
|
|
181
|
+
delete meta.symbol
|
|
182
|
+
}
|
|
183
|
+
return meta
|
|
132
184
|
}
|
|
133
185
|
|
|
134
186
|
export function setCachedTokenMeta(chainId: number, token: string, meta: TokenMeta): void {
|
|
135
187
|
if (Object.keys(meta).length === 0) {
|
|
136
188
|
return // 没有新数据(如查询失败)时不写入空条目
|
|
137
189
|
}
|
|
190
|
+
const defined: CachedMeta = { ...meta }
|
|
191
|
+
if ('name' in meta) {
|
|
192
|
+
defined.nameAt = Date.now()
|
|
193
|
+
}
|
|
194
|
+
if ('symbol' in meta) {
|
|
195
|
+
defined.symbolAt = Date.now()
|
|
196
|
+
}
|
|
138
197
|
const key = `${chainId}:${token.toLowerCase()}`
|
|
139
|
-
|
|
198
|
+
const merged = { ...tokenMetaCache.get(key), ...defined }
|
|
199
|
+
tokenMetaCache.delete(key) // 重新插入,保持“最近写入在后”的顺序
|
|
200
|
+
tokenMetaCache.set(key, merged)
|
|
201
|
+
if (tokenMetaCache.size > MAX_CACHED_TOKENS) {
|
|
202
|
+
tokenMetaCache.delete(tokenMetaCache.keys().next().value as string)
|
|
203
|
+
}
|
|
204
|
+
cacheListeners.forEach((listener) => listener())
|
|
140
205
|
}
|
|
141
206
|
|
|
142
207
|
/** 测试用:清空 decimals / symbol 缓存 */
|
|
143
208
|
export function resetDecimalsCache(): void {
|
|
144
209
|
tokenMetaCache.clear()
|
|
145
210
|
}
|
|
211
|
+
|
|
212
|
+
/** exportTokenMetaCache() 的结果,可以 JSON.stringify */
|
|
213
|
+
export interface TokenMetaSnapshot {
|
|
214
|
+
version: 1
|
|
215
|
+
/** [chainId:小写地址, 信息],按写入时间先后 */
|
|
216
|
+
entries: Array<[string, CachedMeta]>
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 导出代币信息缓存(decimals / symbol / name / NFT 标准),可存到任意地方,下次用 importTokenMetaCache 导入,
|
|
221
|
+
* 页面刷新后不必重查精度。name / symbol 带写入时间,导入后仍按 1 小时过期。
|
|
222
|
+
*
|
|
223
|
+
* @param maxEntries 最多导出多少条(最近写入的优先),默认全部
|
|
224
|
+
*/
|
|
225
|
+
export function exportTokenMetaCache(maxEntries = Number.POSITIVE_INFINITY): TokenMetaSnapshot {
|
|
226
|
+
const entries = [...tokenMetaCache]
|
|
227
|
+
return { version: 1, entries: entries.slice(Math.max(0, entries.length - maxEntries)) }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 导入 exportTokenMetaCache 的结果;格式不对的数据忽略。已有的条目以缓存里的为准(更新) */
|
|
231
|
+
export function importTokenMetaCache(snapshot: unknown): void {
|
|
232
|
+
const entries = (snapshot as Partial<TokenMetaSnapshot> | null)?.entries
|
|
233
|
+
if ((snapshot as Partial<TokenMetaSnapshot> | null)?.version !== 1 || !Array.isArray(entries)) {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
for (const entry of entries) {
|
|
237
|
+
if (!Array.isArray(entry) || typeof entry[0] !== 'string' || !/^\d+:\S+$/.test(entry[0]) || !entry[1] || typeof entry[1] !== 'object') {
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
const [rawKey, value] = entry as [string, CachedMeta]
|
|
241
|
+
// 与 getCachedTokenMeta 的 key 一致:地址部分小写
|
|
242
|
+
const separator = rawKey.indexOf(':')
|
|
243
|
+
const key = `${rawKey.slice(0, separator)}:${rawKey.slice(separator + 1).toLowerCase()}`
|
|
244
|
+
const meta: CachedMeta = {}
|
|
245
|
+
if (Number.isInteger(value.decimals) && (value.decimals as number) >= 0 && (value.decimals as number) <= 255) meta.decimals = value.decimals
|
|
246
|
+
if (typeof value.name === 'string' && typeof value.nameAt === 'number') {
|
|
247
|
+
meta.name = value.name
|
|
248
|
+
meta.nameAt = value.nameAt
|
|
249
|
+
}
|
|
250
|
+
if (typeof value.symbol === 'string' && typeof value.symbolAt === 'number') {
|
|
251
|
+
meta.symbol = value.symbol
|
|
252
|
+
meta.symbolAt = value.symbolAt
|
|
253
|
+
}
|
|
254
|
+
if (value.standard === 'ERC721' || value.standard === 'ERC1155' || value.standard === null) meta.standard = value.standard
|
|
255
|
+
if (Object.keys(meta).length && !tokenMetaCache.has(key)) {
|
|
256
|
+
tokenMetaCache.set(key, meta)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
while (tokenMetaCache.size > MAX_CACHED_TOKENS) {
|
|
260
|
+
tokenMetaCache.delete(tokenMetaCache.keys().next().value as string)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** persistTokenMetaCache 的存储:localStorage / sessionStorage 可以直接传,也可以自己实现 */
|
|
265
|
+
export interface TokenMetaStore {
|
|
266
|
+
getItem(key: string): string | null
|
|
267
|
+
setItem(key: string, value: string): void
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 把代币信息缓存持久化到同步的键值存储(如 localStorage):调用时先读入,之后缓存有更新就防抖(1 秒)整体写回。
|
|
272
|
+
* 返回停止持久化的函数。存储读写出错(如超出配额)时忽略。
|
|
273
|
+
*
|
|
274
|
+
* ```ts
|
|
275
|
+
* persistTokenMetaCache(localStorage) // 刷新页面后不必重查 decimals
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
export function persistTokenMetaCache(store: TokenMetaStore, options: { key?: string; maxEntries?: number } = {}): () => void {
|
|
279
|
+
const { key = 'w3lib:evm-token-meta', maxEntries = 5000 } = options
|
|
280
|
+
try {
|
|
281
|
+
const raw = store.getItem(key)
|
|
282
|
+
if (raw) {
|
|
283
|
+
importTokenMetaCache(JSON.parse(raw))
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
// 数据损坏:忽略,之后会被覆盖
|
|
287
|
+
}
|
|
288
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
289
|
+
const save = () => {
|
|
290
|
+
timer = undefined
|
|
291
|
+
try {
|
|
292
|
+
store.setItem(key, JSON.stringify(exportTokenMetaCache(maxEntries)))
|
|
293
|
+
} catch {
|
|
294
|
+
// 超出配额等:忽略
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const listener = () => {
|
|
298
|
+
timer ??= setTimeout(save, 1000)
|
|
299
|
+
}
|
|
300
|
+
cacheListeners.add(listener)
|
|
301
|
+
return () => {
|
|
302
|
+
cacheListeners.delete(listener)
|
|
303
|
+
if (timer !== undefined) {
|
|
304
|
+
clearTimeout(timer)
|
|
305
|
+
save()
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** 一次对某个节点的请求(onRequest 的事件) */
|
|
2
|
+
export interface RequestEvent {
|
|
3
|
+
/** 节点标识:URL 节点只取 host(不含 path / query,避免泄露 API Key);钱包为 'wallet',tronWeb 为 'tronWeb',其他对象为 'provider' */
|
|
4
|
+
node: string
|
|
5
|
+
/** 'call' | 'getBalance' | 'getChainId' | 'getBlockNumber' | 'getLogs' */
|
|
6
|
+
method: string
|
|
7
|
+
/** 耗时(毫秒) */
|
|
8
|
+
ms: number
|
|
9
|
+
ok: boolean
|
|
10
|
+
/** 失败时的错误 */
|
|
11
|
+
error?: unknown
|
|
12
|
+
/** 本次请求按故障切换顺序的第几个节点(0 起);大于 0 表示换过节点 */
|
|
13
|
+
attempt: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type RequestListener = (event: RequestEvent) => void
|
|
17
|
+
|
|
18
|
+
const listeners = new Set<RequestListener>()
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 订阅所有节点请求(全局,对所有 Provider 生效),用于观察哪个节点返回、换过几次节点、每次耗时。返回取消订阅的函数。
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* const off = onRequest((e) => console.log(e.node, e.method, e.ms, e.ok, e.attempt))
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function onRequest(listener: RequestListener): () => void {
|
|
28
|
+
listeners.add(listener)
|
|
29
|
+
return () => {
|
|
30
|
+
listeners.delete(listener)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function emitRequest(event: RequestEvent): void {
|
|
35
|
+
for (const listener of listeners) {
|
|
36
|
+
try {
|
|
37
|
+
listener(event)
|
|
38
|
+
} catch {
|
|
39
|
+
// 监听函数出错不影响请求
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|