@w3lib/evm-batch-call 0.1.0 → 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 +32 -0
- package/README.md +103 -12
- package/dist/call.d.ts +8 -2
- package/dist/call.d.ts.map +1 -1
- package/dist/call.js +25 -1
- package/dist/call.js.map +1 -1
- package/dist/chains.d.ts +13 -0
- package/dist/chains.d.ts.map +1 -0
- package/dist/chains.js +35 -0
- package/dist/chains.js.map +1 -0
- package/dist/detect.d.ts +23 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +178 -0
- package/dist/detect.js.map +1 -0
- package/dist/erc20.d.ts +83 -7
- package/dist/erc20.d.ts.map +1 -1
- package/dist/erc20.js +36 -5
- package/dist/erc20.js.map +1 -1
- package/dist/fallback.d.ts +2 -0
- package/dist/fallback.d.ts.map +1 -1
- package/dist/fallback.js +6 -18
- package/dist/fallback.js.map +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/nft.d.ts +71 -0
- package/dist/nft.d.ts.map +1 -0
- package/dist/nft.js +194 -0
- package/dist/nft.js.map +1 -0
- package/dist/provider.d.ts +93 -12
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +306 -48
- package/dist/provider.js.map +1 -1
- package/dist/shortcuts.d.ts +95 -0
- package/dist/shortcuts.d.ts.map +1 -0
- package/dist/shortcuts.js +175 -0
- package/dist/shortcuts.js.map +1 -0
- package/dist/source.d.ts +21 -1
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +96 -3
- package/dist/source.js.map +1 -1
- package/dist/tron.d.ts +6 -0
- package/dist/tron.d.ts.map +1 -1
- package/dist/tron.js +16 -1
- package/dist/tron.js.map +1 -1
- package/dist/util.d.ts +6 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +20 -0
- package/dist/util.js.map +1 -0
- package/package.json +3 -2
- package/src/call.ts +32 -3
- package/src/chains.ts +46 -0
- package/src/detect.ts +193 -0
- package/src/erc20.ts +111 -9
- package/src/fallback.ts +7 -20
- package/src/index.ts +55 -4
- package/src/nft.ts +298 -0
- package/src/provider.ts +416 -54
- package/src/shortcuts.ts +256 -0
- package/src/source.ts +102 -3
- package/src/tron.ts +21 -2
- package/src/util.ts +20 -0
package/src/shortcuts.ts
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { CallOverrides } from './aggregate.js'
|
|
2
|
+
import type { BalanceToken, DefaultTokenField, TokenAllowance, TokenBalance, TokenDetails, TokenField } from './erc20.js'
|
|
3
|
+
import type {
|
|
4
|
+
DefaultNftCollectionField,
|
|
5
|
+
Erc1155Balance,
|
|
6
|
+
NftBalance,
|
|
7
|
+
NftCollection,
|
|
8
|
+
NftCollectionField,
|
|
9
|
+
NftCollectionsOptions,
|
|
10
|
+
NftItem,
|
|
11
|
+
NftOwner,
|
|
12
|
+
NftTokenUri,
|
|
13
|
+
} from './nft.js'
|
|
14
|
+
import { Provider, type BalancesOptions, type ProviderConfig, type TokensOptions } from './provider.js'
|
|
15
|
+
import type { ProviderSource } from './source.js'
|
|
16
|
+
|
|
17
|
+
/** getBalances / getTokens 共用的节点参数 */
|
|
18
|
+
export interface ShortcutProviderOptions extends ProviderConfig {
|
|
19
|
+
/** 链 ID。传了 provider 时可省略(从节点识别,结果会缓存);不传 provider 时必填(用于选择内置公共节点) */
|
|
20
|
+
chainId?: number | string
|
|
21
|
+
/**
|
|
22
|
+
* 节点,与 `new Provider(...)` 相同,不传则使用内置公共节点:
|
|
23
|
+
* - RPC URL,或 URL 数组(主节点 + 备用节点)
|
|
24
|
+
* - ethers Provider
|
|
25
|
+
* - 浏览器插件钱包:`window.ethereum`(EIP-1193)、`window.tronWeb`
|
|
26
|
+
* - 混合数组,如 `[window.ethereum, 'https://…']`:钱包出错或不在这条链上时自动用后面的节点
|
|
27
|
+
*/
|
|
28
|
+
provider?: ProviderSource | readonly ProviderSource[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface GetBalancesOptions extends BalancesOptions, ShortcutProviderOptions {}
|
|
32
|
+
|
|
33
|
+
export interface GetTokensOptions<F extends TokenField = DefaultTokenField> extends TokensOptions<F>, ShortcutProviderOptions {}
|
|
34
|
+
|
|
35
|
+
// 复用 Provider:多次调用共享连接、multicall 地址判定和自动合并队列。
|
|
36
|
+
// 按 “chainId + 配置内容” 分组,组内 URL 按字符串、钱包 / Provider 对象按引用(WeakMap,不阻止回收)。
|
|
37
|
+
// 不缓存的情况:配置里有函数等无法比较内容的值;含对象的节点数组;没传 chainId 且节点是对象
|
|
38
|
+
// (钱包可能切链,每次按当前链新建;chainId 识别本身有缓存,切链时自动失效,不会每次都发请求)
|
|
39
|
+
interface ProviderGroup {
|
|
40
|
+
byString: Map<string, Provider>
|
|
41
|
+
byObject: WeakMap<object, Provider>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const providerCache = new Map<string, ProviderGroup>()
|
|
45
|
+
|
|
46
|
+
/** 缓存上限(分组数、每组 URL 数),超出时淘汰最早加入的;对象来源用 WeakMap,随对象回收 */
|
|
47
|
+
const MAX_CACHED = 32
|
|
48
|
+
|
|
49
|
+
function setBounded<K, V>(map: Map<K, V>, key: K, value: V): void {
|
|
50
|
+
map.set(key, value)
|
|
51
|
+
if (map.size > MAX_CACHED) {
|
|
52
|
+
map.delete(map.keys().next().value as K)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function configKey(config: ProviderConfig): string | null {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.stringify(config, (_key, value: unknown) => {
|
|
59
|
+
if (typeof value === 'function') {
|
|
60
|
+
throw new Error('not comparable')
|
|
61
|
+
}
|
|
62
|
+
return typeof value === 'bigint' ? value.toString() : value
|
|
63
|
+
})
|
|
64
|
+
} catch {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sourceKey(chainId: number | undefined, source: ShortcutProviderOptions['provider']): string | object | null {
|
|
70
|
+
if (source === undefined) {
|
|
71
|
+
return 'default'
|
|
72
|
+
}
|
|
73
|
+
if (typeof source === 'string') {
|
|
74
|
+
return `url:${source}`
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(source)) {
|
|
77
|
+
// URL 数组每次调用都是新数组,按内容缓存
|
|
78
|
+
return source.every((item) => typeof item === 'string') ? `urls:${source.join('\n')}` : null
|
|
79
|
+
}
|
|
80
|
+
return chainId === undefined ? null : (source as object)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getProvider(chainId: number | undefined, source: ShortcutProviderOptions['provider'], config: ProviderConfig): Provider {
|
|
84
|
+
if (chainId === undefined && source === undefined) {
|
|
85
|
+
throw new Error('getBalances / getTokens requires chainId or provider')
|
|
86
|
+
}
|
|
87
|
+
const create = () => (chainId === undefined ? new Provider(source as ProviderSource, config) : new Provider(chainId, source, config))
|
|
88
|
+
const cfg = configKey(config)
|
|
89
|
+
const key = sourceKey(chainId, source)
|
|
90
|
+
if (cfg === null || key === null) {
|
|
91
|
+
return create()
|
|
92
|
+
}
|
|
93
|
+
const groupKey = `${chainId ?? 'auto'}|${cfg}`
|
|
94
|
+
let group = providerCache.get(groupKey)
|
|
95
|
+
if (!group) {
|
|
96
|
+
group = { byString: new Map(), byObject: new WeakMap() }
|
|
97
|
+
setBounded(providerCache, groupKey, group)
|
|
98
|
+
}
|
|
99
|
+
let provider = typeof key === 'string' ? group.byString.get(key) : group.byObject.get(key)
|
|
100
|
+
if (!provider) {
|
|
101
|
+
provider = create()
|
|
102
|
+
if (typeof key === 'string') {
|
|
103
|
+
setBounded(group.byString, key, provider)
|
|
104
|
+
} else {
|
|
105
|
+
group.byObject.set(key, provider)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return provider
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---- 各查询函数:节点参数都相同 ----
|
|
112
|
+
|
|
113
|
+
export interface ShortcutOptions extends CallOverrides, ShortcutProviderOptions {}
|
|
114
|
+
|
|
115
|
+
/** 拆出节点参数、调用参数和函数自己的参数(ownKeys),其余作为 Provider 配置;值为 undefined 的参数忽略 */
|
|
116
|
+
function resolve<T extends ShortcutOptions, K extends keyof T & string = never>(
|
|
117
|
+
options: T,
|
|
118
|
+
ownKeys: readonly K[] = [],
|
|
119
|
+
): { provider: Provider; overrides: CallOverrides; own: Pick<T, K> } {
|
|
120
|
+
const { chainId, provider, blockTag, from, ...others } = options
|
|
121
|
+
const own: Record<string, unknown> = {}
|
|
122
|
+
const config: Record<string, unknown> = {}
|
|
123
|
+
for (const [key, value] of Object.entries(others)) {
|
|
124
|
+
if (value !== undefined) {
|
|
125
|
+
;((ownKeys as readonly string[]).includes(key) ? own : config)[key] = value
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
// null / 空字符串(如状态里还没有 chainId)按“未传”处理,走自动识别,而不是变成 chainId 0
|
|
130
|
+
provider: getProvider(chainId === undefined || chainId === null || chainId === '' ? undefined : Number(chainId), provider, config as ProviderConfig),
|
|
131
|
+
overrides: { blockTag, from },
|
|
132
|
+
own: own as Pick<T, K>,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 批量查余额(主币 + 代币,一次请求),返回原始余额、decimals 和换算后的数值。
|
|
138
|
+
*
|
|
139
|
+
* ```ts
|
|
140
|
+
* import { NATIVE_TOKEN, getBalances } from '@w3lib/evm-batch-call'
|
|
141
|
+
*
|
|
142
|
+
* await getBalances(user, [NATIVE_TOKEN, USDT], { chainId: 56 }) // 内置公共节点
|
|
143
|
+
* await getBalances(user, tokens, { provider: window.ethereum }) // 钱包,chainId 自动识别
|
|
144
|
+
* await getBalances(user, tokens, { provider: 'https://bsc-dataseed.bnbchain.org' })
|
|
145
|
+
* await getBalances(user, tokens, { chainId: 56, provider: [window.ethereum, 'https://…'] }) // 钱包优先,失败用公共节点
|
|
146
|
+
* await getBalances(user, tokens, { chainId: 56, symbol: true }) // 同时返回 symbol
|
|
147
|
+
* // [{ token: NATIVE_TOKEN, native: true, balance: '1500000000000000000', decimals: 18, formatted: '1.5', success: true }, ...]
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
export function getBalances(owner: string, tokens: readonly BalanceToken[], options: GetBalancesOptions = {}): Promise<TokenBalance[]> {
|
|
151
|
+
const { provider, overrides, own } = resolve(options, ['symbol'])
|
|
152
|
+
return provider.balances(owner, tokens, { ...overrides, symbol: own.symbol })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 批量查 ERC20 代币详情(一次请求),可以选择返回哪些字段。节点参数与 getBalances 相同。
|
|
157
|
+
*
|
|
158
|
+
* ```ts
|
|
159
|
+
* import { NATIVE_TOKEN, getTokens } from '@w3lib/evm-batch-call'
|
|
160
|
+
*
|
|
161
|
+
* await getTokens([USDT, NATIVE_TOKEN], { chainId: 56 }) // 默认 name / symbol / decimals
|
|
162
|
+
* await getTokens([USDT], { provider: window.ethereum, fields: ['symbol', 'decimals', 'totalSupply'] })
|
|
163
|
+
* // [{ address: USDT, native: false, symbol: 'USDT', decimals: 18, totalSupply: '…', totalSupplyFormatted: '…', success: true }]
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export function getTokens<const F extends TokenField = DefaultTokenField>(
|
|
167
|
+
tokens: readonly string[],
|
|
168
|
+
options: GetTokensOptions<F> = {},
|
|
169
|
+
): Promise<TokenDetails<F>[]> {
|
|
170
|
+
const { provider, overrides, own } = resolve(options, ['fields'])
|
|
171
|
+
return provider.tokens<F>(tokens, { ...overrides, fields: own.fields })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 批量查 ERC20 授权额度(一次请求)。
|
|
176
|
+
*
|
|
177
|
+
* ```ts
|
|
178
|
+
* const [usdt] = await getAllowances(user, router, [USDT], { chainId: 56 })
|
|
179
|
+
* if (!usdt.unlimited && BigInt(usdt.allowance) < amount) { // 需要 approve
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
export function getAllowances(
|
|
183
|
+
owner: string,
|
|
184
|
+
spender: string,
|
|
185
|
+
tokens: readonly BalanceToken[],
|
|
186
|
+
options: ShortcutOptions = {},
|
|
187
|
+
): Promise<TokenAllowance[]> {
|
|
188
|
+
const { provider, overrides } = resolve(options)
|
|
189
|
+
return provider.allowances(owner, spender, tokens, overrides)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface GetNftCollectionsOptions<F extends NftCollectionField = DefaultNftCollectionField>
|
|
193
|
+
extends NftCollectionsOptions<F>,
|
|
194
|
+
ShortcutProviderOptions {}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 批量查 NFT 集合信息,字段可选(默认 standard / name / symbol)。
|
|
198
|
+
*
|
|
199
|
+
* ```ts
|
|
200
|
+
* await getNftCollections([BAYC, MAYC], { chainId: 1, fields: ['standard', 'name', 'totalSupply'] })
|
|
201
|
+
* // [{ address: BAYC, standard: 'ERC721', name: 'BoredApeYachtClub', totalSupply: '10000', success: true }, ...]
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
export function getNftCollections<const F extends NftCollectionField = DefaultNftCollectionField>(
|
|
205
|
+
collections: readonly string[],
|
|
206
|
+
options: GetNftCollectionsOptions<F> = {},
|
|
207
|
+
): Promise<NftCollection<F>[]> {
|
|
208
|
+
const { provider, overrides, own } = resolve(options, ['fields'])
|
|
209
|
+
return provider.nftCollections<F>(collections, { ...overrides, fields: own.fields })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 批量查 ERC721 持有数量:`await getNftBalances(user, [BAYC, MAYC], { chainId: 1 })` */
|
|
213
|
+
export function getNftBalances(owner: string, collections: readonly string[], options: ShortcutOptions = {}): Promise<NftBalance[]> {
|
|
214
|
+
const { provider, overrides } = resolve(options)
|
|
215
|
+
return provider.nftBalances(owner, collections, overrides)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 批量查 ERC721 持有人:`await getNftOwners([{ contract: BAYC, tokenId: 1 }], { chainId: 1 })` */
|
|
219
|
+
export function getNftOwners(items: readonly NftItem[], options: ShortcutOptions = {}): Promise<NftOwner[]> {
|
|
220
|
+
const { provider, overrides } = resolve(options)
|
|
221
|
+
return provider.nftOwners(items, overrides)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface GetNftTokenUrisOptions extends ShortcutOptions {
|
|
225
|
+
/** 把 ipfs://xxx 转成 `${ipfsGateway}xxx`,如 'https://ipfs.io/ipfs/' */
|
|
226
|
+
ipfsGateway?: string
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 批量查 NFT 元数据地址(ERC721 tokenURI / ERC1155 uri 自动兼容)。
|
|
231
|
+
*
|
|
232
|
+
* ```ts
|
|
233
|
+
* await getNftTokenUris([{ contract: BAYC, tokenId: 1 }], { chainId: 1, ipfsGateway: 'https://ipfs.io/ipfs/' })
|
|
234
|
+
* // [{ contract: BAYC, tokenId: '1', uri: 'https://ipfs.io/ipfs/Qm…/1', success: true }]
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export function getNftTokenUris(items: readonly NftItem[], options: GetNftTokenUrisOptions = {}): Promise<NftTokenUri[]> {
|
|
238
|
+
const { provider, overrides, own } = resolve(options, ['ipfsGateway'])
|
|
239
|
+
return provider.nftTokenUris(items, { ...overrides, ipfsGateway: own.ipfsGateway })
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** 批量查 ERC1155 余额:`await getErc1155Balances(user, [{ contract, tokenId: 1 }], { chainId: 137 })` */
|
|
243
|
+
export function getErc1155Balances(owner: string, items: readonly NftItem[], options: ShortcutOptions = {}): Promise<Erc1155Balance[]> {
|
|
244
|
+
const { provider, overrides } = resolve(options)
|
|
245
|
+
return provider.erc1155Balances(owner, items, overrides)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** 测试用:按参数取(或创建)Provider,用于验证缓存复用(不在包的公开导出里) */
|
|
249
|
+
export function resolveProviderForTest(options: ShortcutOptions): Provider {
|
|
250
|
+
return resolve(options).provider
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** 测试用:清空 getBalances / getTokens 的 Provider 缓存 */
|
|
254
|
+
export function resetBalancesProviderCache(): void {
|
|
255
|
+
providerCache.clear()
|
|
256
|
+
}
|
package/src/source.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { BrowserProvider, FetchRequest, JsonRpcProvider, type Eip1193Provider } from 'ethers'
|
|
1
|
+
import { BrowserProvider, FetchRequest, JsonRpcProvider, makeError, type Eip1193Provider, type TransactionRequest } from 'ethers'
|
|
2
2
|
|
|
3
3
|
import type { EthersLikeProvider } from './aggregate.js'
|
|
4
|
+
import { canDetectChainId, detectChainId, detectChainIdCached } from './detect.js'
|
|
4
5
|
import { FallbackRpc, type FallbackOptions } from './fallback.js'
|
|
5
6
|
import { DEFAULT_RPC_URLS, DEFAULT_TRON_HOSTS } from './rpcNodes.js'
|
|
6
7
|
import { TRON_CHAIN_ID, TronProvider, type TronProviderOptions, type TronWebLike } from './tron.js'
|
|
@@ -50,10 +51,102 @@ export function resolveSource(
|
|
|
50
51
|
}
|
|
51
52
|
// 多节点时 Tron 节点不在单节点内重试 429,直接切到下一个节点
|
|
52
53
|
const multiple = list.length > 1
|
|
53
|
-
const
|
|
54
|
+
const timeout = options.fallback?.timeout ?? 10_000
|
|
55
|
+
const nodes = list.map((item) => {
|
|
56
|
+
const node = toEthersLike(chainId, item, options, multiple)
|
|
57
|
+
// 内置公共节点本来就是按 chainId 选出来的,不需要再校验
|
|
58
|
+
return source === undefined ? node : withChainCheck(chainId, item, node, timeout)
|
|
59
|
+
})
|
|
54
60
|
return multiple ? new FallbackRpc(nodes, options.fallback) : (nodes[0] as EthersLikeProvider)
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
/** 识别失败后多久内不再重试识别(期间请求照常发出,不做校验) */
|
|
64
|
+
const CHECK_RETRY_INTERVAL = 60_000
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 校验节点所在的链与 Provider 的 chainId 一致,避免把另一条链的数据当成这条链的返回:
|
|
68
|
+
* 比如 [钱包, 公共节点] 自动识别出钱包的链后,公共节点其实在另一条链上;或者配错了节点 URL。
|
|
69
|
+
*
|
|
70
|
+
* - 不一致时抛 NETWORK_ERROR(多节点时 FallbackRpc 会换下一个节点)
|
|
71
|
+
* - 通过已配置好的节点连接识别(带 Tron apiKey / 限流等),结果按来源缓存,每个节点只多一次请求
|
|
72
|
+
* - 识别与实际请求并行发出,不增加延迟;不一致时丢弃请求结果
|
|
73
|
+
* - 识别本身失败时不拦截(交给实际请求去报错),并在一段时间内不再重试,避免每次请求都多一次识别
|
|
74
|
+
*/
|
|
75
|
+
export class ChainCheckedProvider implements EthersLikeProvider {
|
|
76
|
+
#checkFailedAt = 0
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
readonly inner: EthersLikeProvider,
|
|
80
|
+
readonly chainId: number,
|
|
81
|
+
readonly source: ProviderSource,
|
|
82
|
+
readonly timeout: number,
|
|
83
|
+
) {}
|
|
84
|
+
|
|
85
|
+
async #check(): Promise<void> {
|
|
86
|
+
if (this.#checkFailedAt && Date.now() - this.#checkFailedAt < CHECK_RETRY_INTERVAL) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
let actual: number
|
|
90
|
+
try {
|
|
91
|
+
actual = await detectChainIdCached(this.source, () => detectViaNode(this.inner, this.source), this.timeout)
|
|
92
|
+
} catch {
|
|
93
|
+
this.#checkFailedAt = Date.now()
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
this.#checkFailedAt = 0
|
|
97
|
+
if (actual !== this.chainId) {
|
|
98
|
+
throw makeError(`chainId mismatch: expected ${this.chainId}, but the node is on ${actual}`, 'NETWORK_ERROR', {
|
|
99
|
+
event: 'chainIdMismatch',
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async #run<T>(request: () => Promise<T>): Promise<T> {
|
|
105
|
+
const [checked, result] = await Promise.allSettled([this.#check(), request()])
|
|
106
|
+
if (checked.status === 'rejected') {
|
|
107
|
+
throw checked.reason
|
|
108
|
+
}
|
|
109
|
+
if (result.status === 'rejected') {
|
|
110
|
+
throw result.reason
|
|
111
|
+
}
|
|
112
|
+
return result.value
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
call(tx: TransactionRequest): Promise<string> {
|
|
116
|
+
return this.#run(() => this.inner.call(tx))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
getBalance(...args: Parameters<EthersLikeProvider['getBalance']>): Promise<bigint> {
|
|
120
|
+
return this.#run(() => this.inner.getBalance(...args))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getChainId(): Promise<number> {
|
|
124
|
+
return this.chainId
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 通过已创建的节点连接识别它实际所在的链 */
|
|
129
|
+
function detectViaNode(node: EthersLikeProvider, source: ProviderSource): Promise<number> {
|
|
130
|
+
if (node instanceof TronProvider) {
|
|
131
|
+
return node.getChainId()
|
|
132
|
+
}
|
|
133
|
+
// JsonRpcProvider(staticNetwork)/ BrowserProvider('any' 网络)的 getNetwork 不反映节点实际的链,直接问节点
|
|
134
|
+
if (node instanceof JsonRpcProvider) {
|
|
135
|
+
return node.send('eth_chainId', []).then(Number)
|
|
136
|
+
}
|
|
137
|
+
return detectChainId(source, Number.POSITIVE_INFINITY)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function withChainCheck(chainId: number, source: ProviderSource, node: EthersLikeProvider, timeout: number): EthersLikeProvider {
|
|
141
|
+
if (!Number.isFinite(chainId) || !canDetectChainId(source) || pinnedWallets.has(node)) {
|
|
142
|
+
return node
|
|
143
|
+
}
|
|
144
|
+
return new ChainCheckedProvider(node, chainId, source, timeout)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 由 EIP-1193 钱包创建、已固定在 chainId 上的 BrowserProvider(每次请求 ethers 都会校验链),不需要再包一层 */
|
|
148
|
+
const pinnedWallets = new WeakSet<EthersLikeProvider>()
|
|
149
|
+
|
|
57
150
|
function toEthersLike(chainId: number, source: ProviderSource, options: SourceOptions, multiple: boolean): EthersLikeProvider {
|
|
58
151
|
if (typeof source === 'string') {
|
|
59
152
|
if (isTronChain(chainId)) {
|
|
@@ -78,7 +171,13 @@ function toEthersLike(chainId: number, source: ProviderSource, options: SourceOp
|
|
|
78
171
|
return TronProvider.fromTronWeb(source as TronWebLike, options.tron)
|
|
79
172
|
}
|
|
80
173
|
if (typeof value.request === 'function') {
|
|
81
|
-
|
|
174
|
+
// 传入 chainId:钱包当前所在的链与之不符时请求报 NETWORK_ERROR(network changed),
|
|
175
|
+
// 而不是静默返回另一条链的数据;在多节点列表里会据此自动切到下一个节点
|
|
176
|
+
const wallet = new BrowserProvider(source as Eip1193Provider, Number.isFinite(chainId) ? chainId : undefined)
|
|
177
|
+
if (Number.isFinite(chainId)) {
|
|
178
|
+
pinnedWallets.add(wallet)
|
|
179
|
+
}
|
|
180
|
+
return wallet
|
|
82
181
|
}
|
|
83
182
|
throw new Error('Unsupported provider: expected an RPC URL, an ethers Provider, an EIP-1193 provider or a tronWeb instance')
|
|
84
183
|
}
|
package/src/tron.ts
CHANGED
|
@@ -62,7 +62,8 @@ function toTronHex(address: string): string {
|
|
|
62
62
|
|
|
63
63
|
/** 只用到 tronWeb 的这几个字段,兼容 tronweb v5/v6 及钱包注入的实例 */
|
|
64
64
|
export interface TronWebLike {
|
|
65
|
-
|
|
65
|
+
/** host 是当前连接的全节点地址,TronLink 切换网络时会变,用于区分 chainId 缓存 */
|
|
66
|
+
fullNode: { request: (url: string, payload: Record<string, unknown>, method: string) => Promise<any>; host?: string }
|
|
66
67
|
defaultAddress?: { base58?: string | false }
|
|
67
68
|
}
|
|
68
69
|
|
|
@@ -85,6 +86,8 @@ export interface TronProviderOptions {
|
|
|
85
86
|
minInterval?: number
|
|
86
87
|
/** 遇到 HTTP 429 时的重试次数(指数退避,从 1s 开始)。默认 3 */
|
|
87
88
|
retries?: number
|
|
89
|
+
/** 单次 HTTP 请求超时(毫秒,仅对内置 fetch 生效,自定义 request 自行处理)。默认 10000;<= 0 不限制 */
|
|
90
|
+
timeout?: number
|
|
88
91
|
/** 不传 from 时使用的调用者地址;传函数则每次调用时取值(钱包切换账号后自动跟随) */
|
|
89
92
|
defaultFrom?: string | (() => string | null | undefined)
|
|
90
93
|
}
|
|
@@ -131,8 +134,14 @@ export class TronProvider implements EthersLikeProvider {
|
|
|
131
134
|
if (options.apiKey) {
|
|
132
135
|
headers['TRON-PRO-API-KEY'] = options.apiKey
|
|
133
136
|
}
|
|
137
|
+
const timeout = options.timeout ?? 10_000
|
|
134
138
|
this.#request = async (path, body) => {
|
|
135
|
-
const res = await fetch(`${host}/${path}`, {
|
|
139
|
+
const res = await fetch(`${host}/${path}`, {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
headers,
|
|
142
|
+
body: JSON.stringify(body),
|
|
143
|
+
signal: timeout > 0 && Number.isFinite(timeout) ? AbortSignal.timeout(timeout) : undefined,
|
|
144
|
+
})
|
|
136
145
|
if (!res.ok) {
|
|
137
146
|
throw new HttpError(`Tron request ${path} failed: HTTP ${res.status}`, res.status)
|
|
138
147
|
}
|
|
@@ -202,6 +211,16 @@ export class TronProvider implements EthersLikeProvider {
|
|
|
202
211
|
return output
|
|
203
212
|
}
|
|
204
213
|
|
|
214
|
+
/** chainId = 创世区块哈希的最后 4 字节(与 TronGrid /jsonrpc 的 eth_chainId 一致,主网 728126428) */
|
|
215
|
+
async getChainId(): Promise<number> {
|
|
216
|
+
const res = await this.#limit(() => this.#request('wallet/getblockbynum', { num: 0 }))
|
|
217
|
+
const blockId = String(res?.blockID ?? '')
|
|
218
|
+
if (!/^[0-9a-f]{64}$/i.test(blockId)) {
|
|
219
|
+
throw new Error('Unable to detect Tron chainId: unexpected genesis block response')
|
|
220
|
+
}
|
|
221
|
+
return Number.parseInt(blockId.slice(-8), 16)
|
|
222
|
+
}
|
|
223
|
+
|
|
205
224
|
async getBalance(address: unknown, blockTag?: unknown): Promise<bigint> {
|
|
206
225
|
assertLatest(blockTag)
|
|
207
226
|
const res = await this.#limit(() => this.#request('wallet/getaccount', { address: toTronHex(String(address)) }))
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class TimeoutError extends Error {
|
|
2
|
+
constructor(ms: number) {
|
|
3
|
+
super(`RPC request timed out after ${ms}ms`)
|
|
4
|
+
this.name = 'TimeoutError'
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** ms <= 0 或非有限值表示不限制 */
|
|
9
|
+
export function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
|
10
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
11
|
+
return promise
|
|
12
|
+
}
|
|
13
|
+
let timer: ReturnType<typeof setTimeout>
|
|
14
|
+
return Promise.race([
|
|
15
|
+
promise,
|
|
16
|
+
new Promise<never>((_, reject) => {
|
|
17
|
+
timer = setTimeout(() => reject(new TimeoutError(ms)), ms)
|
|
18
|
+
}),
|
|
19
|
+
]).finally(() => clearTimeout(timer))
|
|
20
|
+
}
|