@w3lib/evm-batch-call 0.1.0 → 0.3.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +194 -12
  3. package/dist/aggregate.d.ts +5 -2
  4. package/dist/aggregate.d.ts.map +1 -1
  5. package/dist/aggregate.js.map +1 -1
  6. package/dist/call.d.ts +8 -2
  7. package/dist/call.d.ts.map +1 -1
  8. package/dist/call.js +25 -1
  9. package/dist/call.js.map +1 -1
  10. package/dist/chains.d.ts +13 -0
  11. package/dist/chains.d.ts.map +1 -0
  12. package/dist/chains.js +35 -0
  13. package/dist/chains.js.map +1 -0
  14. package/dist/detect.d.ts +23 -0
  15. package/dist/detect.d.ts.map +1 -0
  16. package/dist/detect.js +178 -0
  17. package/dist/detect.js.map +1 -0
  18. package/dist/erc20.d.ts +83 -7
  19. package/dist/erc20.d.ts.map +1 -1
  20. package/dist/erc20.js +36 -5
  21. package/dist/erc20.js.map +1 -1
  22. package/dist/fallback.d.ts +25 -0
  23. package/dist/fallback.d.ts.map +1 -1
  24. package/dist/fallback.js +123 -21
  25. package/dist/fallback.js.map +1 -1
  26. package/dist/index.d.ts +10 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +9 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/nft.d.ts +71 -0
  31. package/dist/nft.d.ts.map +1 -0
  32. package/dist/nft.js +194 -0
  33. package/dist/nft.js.map +1 -0
  34. package/dist/owner.d.ts +136 -0
  35. package/dist/owner.d.ts.map +1 -0
  36. package/dist/owner.js +501 -0
  37. package/dist/owner.js.map +1 -0
  38. package/dist/provider.d.ts +112 -12
  39. package/dist/provider.d.ts.map +1 -1
  40. package/dist/provider.js +343 -48
  41. package/dist/provider.js.map +1 -1
  42. package/dist/scan.d.ts +59 -0
  43. package/dist/scan.d.ts.map +1 -0
  44. package/dist/scan.js +245 -0
  45. package/dist/scan.js.map +1 -0
  46. package/dist/shortcuts.d.ts +113 -0
  47. package/dist/shortcuts.d.ts.map +1 -0
  48. package/dist/shortcuts.js +193 -0
  49. package/dist/shortcuts.js.map +1 -0
  50. package/dist/source.d.ts +23 -1
  51. package/dist/source.d.ts.map +1 -1
  52. package/dist/source.js +110 -3
  53. package/dist/source.js.map +1 -1
  54. package/dist/tron.d.ts +6 -0
  55. package/dist/tron.d.ts.map +1 -1
  56. package/dist/tron.js +16 -1
  57. package/dist/tron.js.map +1 -1
  58. package/dist/util.d.ts +6 -0
  59. package/dist/util.d.ts.map +1 -0
  60. package/dist/util.js +20 -0
  61. package/dist/util.js.map +1 -0
  62. package/package.json +3 -2
  63. package/src/aggregate.ts +5 -2
  64. package/src/call.ts +32 -3
  65. package/src/chains.ts +46 -0
  66. package/src/detect.ts +193 -0
  67. package/src/erc20.ts +111 -9
  68. package/src/fallback.ts +127 -23
  69. package/src/index.ts +90 -4
  70. package/src/nft.ts +298 -0
  71. package/src/owner.ts +651 -0
  72. package/src/provider.ts +458 -54
  73. package/src/scan.ts +293 -0
  74. package/src/shortcuts.ts +278 -0
  75. package/src/source.ts +118 -3
  76. package/src/tron.ts +21 -2
  77. package/src/util.ts +20 -0
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,118 @@ export function resolveSource(
50
51
  }
51
52
  // 多节点时 Tron 节点不在单节点内重试 429,直接切到下一个节点
52
53
  const multiple = list.length > 1
53
- const nodes = list.map((item) => toEthersLike(chainId, item, options, multiple))
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
+ getBlockNumber(): Promise<number> {
124
+ const inner = this.inner
125
+ if (!inner.getBlockNumber) {
126
+ return Promise.reject(new Error('getBlockNumber is not supported by this node'))
127
+ }
128
+ return this.#run(() => (inner.getBlockNumber as () => Promise<number>)())
129
+ }
130
+
131
+ getLogs(filter: Parameters<NonNullable<EthersLikeProvider['getLogs']>>[0]): Promise<Awaited<ReturnType<NonNullable<EthersLikeProvider['getLogs']>>>> {
132
+ const inner = this.inner
133
+ if (!inner.getLogs) {
134
+ return Promise.reject(new Error('getLogs is not supported by this node'))
135
+ }
136
+ return this.#run(() => (inner.getLogs as NonNullable<EthersLikeProvider['getLogs']>)(filter))
137
+ }
138
+
139
+ async getChainId(): Promise<number> {
140
+ return this.chainId
141
+ }
142
+ }
143
+
144
+ /** 通过已创建的节点连接识别它实际所在的链 */
145
+ function detectViaNode(node: EthersLikeProvider, source: ProviderSource): Promise<number> {
146
+ if (node instanceof TronProvider) {
147
+ return node.getChainId()
148
+ }
149
+ // JsonRpcProvider(staticNetwork)/ BrowserProvider('any' 网络)的 getNetwork 不反映节点实际的链,直接问节点
150
+ if (node instanceof JsonRpcProvider) {
151
+ return node.send('eth_chainId', []).then(Number)
152
+ }
153
+ return detectChainId(source, Number.POSITIVE_INFINITY)
154
+ }
155
+
156
+ function withChainCheck(chainId: number, source: ProviderSource, node: EthersLikeProvider, timeout: number): EthersLikeProvider {
157
+ if (!Number.isFinite(chainId) || !canDetectChainId(source) || pinnedWallets.has(node)) {
158
+ return node
159
+ }
160
+ return new ChainCheckedProvider(node, chainId, source, timeout)
161
+ }
162
+
163
+ /** 由 EIP-1193 钱包创建、已固定在 chainId 上的 BrowserProvider(每次请求 ethers 都会校验链),不需要再包一层 */
164
+ const pinnedWallets = new WeakSet<EthersLikeProvider>()
165
+
57
166
  function toEthersLike(chainId: number, source: ProviderSource, options: SourceOptions, multiple: boolean): EthersLikeProvider {
58
167
  if (typeof source === 'string') {
59
168
  if (isTronChain(chainId)) {
@@ -78,7 +187,13 @@ function toEthersLike(chainId: number, source: ProviderSource, options: SourceOp
78
187
  return TronProvider.fromTronWeb(source as TronWebLike, options.tron)
79
188
  }
80
189
  if (typeof value.request === 'function') {
81
- return new BrowserProvider(source as Eip1193Provider)
190
+ // 传入 chainId:钱包当前所在的链与之不符时请求报 NETWORK_ERROR(network changed),
191
+ // 而不是静默返回另一条链的数据;在多节点列表里会据此自动切到下一个节点
192
+ const wallet = new BrowserProvider(source as Eip1193Provider, Number.isFinite(chainId) ? chainId : undefined)
193
+ if (Number.isFinite(chainId)) {
194
+ pinnedWallets.add(wallet)
195
+ }
196
+ return wallet
82
197
  }
83
198
  throw new Error('Unsupported provider: expected an RPC URL, an ethers Provider, an EIP-1193 provider or a tronWeb instance')
84
199
  }
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
- fullNode: { request: (url: string, payload: Record<string, unknown>, method: string) => Promise<any> }
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}`, { method: 'POST', headers, body: JSON.stringify(body) })
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
+ }