@w3lib/evm-batch-call 0.4.2 → 0.5.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 (88) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +165 -2
  3. package/dist/fallback.d.ts +5 -0
  4. package/dist/fallback.d.ts.map +1 -1
  5. package/dist/fallback.js +7 -0
  6. package/dist/fallback.js.map +1 -1
  7. package/dist/lp.d.ts +149 -0
  8. package/dist/lp.d.ts.map +1 -0
  9. package/dist/lp.js +499 -0
  10. package/dist/lp.js.map +1 -0
  11. package/dist/lpMath.d.ts +25 -0
  12. package/dist/lpMath.d.ts.map +1 -0
  13. package/dist/lpMath.js +103 -0
  14. package/dist/lpMath.js.map +1 -0
  15. package/dist/lpV4.d.ts +108 -0
  16. package/dist/lpV4.d.ts.map +1 -0
  17. package/dist/lpV4.js +283 -0
  18. package/dist/lpV4.js.map +1 -0
  19. package/dist/owner.d.ts +1 -1
  20. package/dist/owner.d.ts.map +1 -1
  21. package/dist/owner.js +18 -5
  22. package/dist/owner.js.map +1 -1
  23. package/dist/scan.d.ts +54 -0
  24. package/dist/scan.d.ts.map +1 -1
  25. package/dist/scan.js +124 -92
  26. package/dist/scan.js.map +1 -1
  27. package/dist/source.d.ts +2 -0
  28. package/dist/source.d.ts.map +1 -1
  29. package/dist/source.js +4 -0
  30. package/dist/source.js.map +1 -1
  31. package/dist/subpaths/lp.d.ts +33 -0
  32. package/dist/subpaths/lp.d.ts.map +1 -0
  33. package/dist/subpaths/lp.js +47 -0
  34. package/dist/subpaths/lp.js.map +1 -0
  35. package/dist/subpaths/write.d.ts +20 -0
  36. package/dist/subpaths/write.d.ts.map +1 -0
  37. package/dist/subpaths/write.js +18 -0
  38. package/dist/subpaths/write.js.map +1 -0
  39. package/dist/tron.d.ts +2 -0
  40. package/dist/tron.d.ts.map +1 -1
  41. package/dist/tron.js +4 -0
  42. package/dist/tron.js.map +1 -1
  43. package/dist/write/batch.d.ts +88 -0
  44. package/dist/write/batch.d.ts.map +1 -0
  45. package/dist/write/batch.js +231 -0
  46. package/dist/write/batch.js.map +1 -0
  47. package/dist/write/collect.d.ts +53 -0
  48. package/dist/write/collect.d.ts.map +1 -0
  49. package/dist/write/collect.js +255 -0
  50. package/dist/write/collect.js.map +1 -0
  51. package/dist/write/rpc.d.ts +68 -0
  52. package/dist/write/rpc.d.ts.map +1 -0
  53. package/dist/write/rpc.js +236 -0
  54. package/dist/write/rpc.js.map +1 -0
  55. package/dist/write/send.d.ts +124 -0
  56. package/dist/write/send.d.ts.map +1 -0
  57. package/dist/write/send.js +373 -0
  58. package/dist/write/send.js.map +1 -0
  59. package/dist/write/signer.d.ts +58 -0
  60. package/dist/write/signer.d.ts.map +1 -0
  61. package/dist/write/signer.js +41 -0
  62. package/dist/write/signer.js.map +1 -0
  63. package/dist/write/transfer.d.ts +83 -0
  64. package/dist/write/transfer.d.ts.map +1 -0
  65. package/dist/write/transfer.js +259 -0
  66. package/dist/write/transfer.js.map +1 -0
  67. package/dist/write/tron.d.ts +58 -0
  68. package/dist/write/tron.d.ts.map +1 -0
  69. package/dist/write/tron.js +956 -0
  70. package/dist/write/tron.js.map +1 -0
  71. package/package.json +27 -14
  72. package/src/fallback.ts +8 -0
  73. package/src/lp.ts +721 -0
  74. package/src/lpMath.ts +113 -0
  75. package/src/lpV4.ts +406 -0
  76. package/src/owner.ts +19 -6
  77. package/src/scan.ts +155 -86
  78. package/src/source.ts +5 -0
  79. package/src/subpaths/lp.ts +71 -0
  80. package/src/subpaths/write.ts +26 -0
  81. package/src/tron.ts +5 -0
  82. package/src/write/batch.ts +302 -0
  83. package/src/write/collect.ts +312 -0
  84. package/src/write/rpc.ts +277 -0
  85. package/src/write/send.ts +490 -0
  86. package/src/write/signer.ts +90 -0
  87. package/src/write/transfer.ts +344 -0
  88. package/src/write/tron.ts +1100 -0
package/src/scan.ts CHANGED
@@ -165,6 +165,139 @@ async function loadState(storage: TransferScanStorage, key: string): Promise<Tra
165
165
  }
166
166
  }
167
167
 
168
+ /** 通用的增量扫描进度(transferScan 存储时把 items 存成 tokens) */
169
+ export interface LogScanProgress {
170
+ startBlock: number
171
+ cursor: number
172
+ /** 扫描中收集到的条目(去重) */
173
+ items: string[]
174
+ blockRange: number
175
+ rangeLimit?: number
176
+ rangeLimitAt?: number
177
+ pausedUntil?: number
178
+ }
179
+
180
+ /** 能查日志的节点(Provider 满足) */
181
+ export interface LogScanProvider {
182
+ getBlockNumber(): Promise<number>
183
+ getLogs(filter: { address?: string; fromBlock: number; toBlock: number; topics: Array<string | null> }): Promise<Array<{ address: string; topics: readonly string[] }>>
184
+ }
185
+
186
+ export interface LogScanRun {
187
+ provider: LogScanProvider
188
+ /** 存储里读出的进度(没有时从头开始) */
189
+ saved: LogScanProgress | undefined
190
+ /** 第一次扫描时的起点:返回 cursor(已扫到的区块,下一次从 cursor + 1 开始),参数是本次的目标区块 */
191
+ initialCursor(target: number): number
192
+ filter: { address?: string; topics: Array<string | null> }
193
+ /** 从一条日志里取出要收集的条目,不需要的返回 null */
194
+ collect(log: { address: string; topics: readonly string[] }): string | null
195
+ maxRange: number
196
+ maxRequests: number
197
+ timeBudget: number
198
+ confirmations: number
199
+ /** 已取消时不再发新的 getLogs(已扫到的进度照常返回) */
200
+ signal?: AbortSignal
201
+ /** 本次扫描结束时调用:cursor 是已扫到的区块,target 是本次的目标区块(cursor ≥ target 表示已扫到最新) */
202
+ onDone?(state: LogScanProgress, target: number): void
203
+ }
204
+
205
+ /**
206
+ * 增量扫描日志(transferScan、LP 仓位的 V4 扫描共用):从上次的位置往后扫到最新区块前 confirmations 个区块,
207
+ * 区块范围按节点的限制自动调整,限频 / 超时退避重试,节点不开放 getLogs 时暂停一段时间。
208
+ * 返回新的进度;拿不到最新区块且没有旧进度时返回 null
209
+ */
210
+ export async function runLogScan(run: LogScanRun): Promise<LogScanProgress | null> {
211
+ const { provider, saved, filter, collect, maxRange, maxRequests, timeBudget, confirmations } = run
212
+ if (saved?.pausedUntil && Date.now() < saved.pausedUntil) {
213
+ return saved
214
+ }
215
+ const startedAt = Date.now()
216
+ let target: number
217
+ try {
218
+ target = (await provider.getBlockNumber()) - confirmations
219
+ } catch {
220
+ return saved ?? null // 节点不支持 / 暂时不可用:返回已有的进度
221
+ }
222
+ const now = Date.now()
223
+ let state: LogScanProgress
224
+ if (saved) {
225
+ state = { ...saved, items: [...saved.items] }
226
+ delete state.pausedUntil
227
+ if (state.rangeLimitAt !== undefined && now - state.rangeLimitAt > RANGE_LIMIT_TTL) {
228
+ delete state.rangeLimit
229
+ delete state.rangeLimitAt
230
+ }
231
+ } else {
232
+ const cursor = Math.max(-1, Math.min(target, run.initialCursor(target)))
233
+ state = { startBlock: cursor + 1, cursor, items: [], blockRange: maxRange }
234
+ }
235
+ const items = new Set(state.items)
236
+ const cap = () => Math.min(maxRange, state.rangeLimit ?? maxRange)
237
+ const setLimit = (limit: number) => {
238
+ state.rangeLimit = limit
239
+ state.rangeLimitAt = Date.now()
240
+ }
241
+ let range = Math.min(state.blockRange, cap())
242
+ let successes = 0
243
+ let transient = 0
244
+
245
+ for (let requests = 0; requests < maxRequests && state.cursor < target; requests++) {
246
+ if ((timeBudget > 0 && Date.now() - startedAt >= timeBudget) || run.signal?.aborted) {
247
+ break // 超出时间预算 / 已取消,留到下次调用
248
+ }
249
+ const fromBlock = state.cursor + 1
250
+ const toBlock = Math.min(target, fromBlock + range - 1)
251
+ try {
252
+ const logs = await provider.getLogs({ ...(filter.address ? { address: filter.address } : {}), fromBlock, toBlock, topics: filter.topics })
253
+ for (const log of logs) {
254
+ const item = collect(log)
255
+ if (item !== null) {
256
+ items.add(item)
257
+ }
258
+ }
259
+ state.cursor = toBlock
260
+ transient = 0
261
+ if (++successes >= GROW_AFTER && range < cap()) {
262
+ range = Math.min(cap(), range * 2) // 之前缩小过(偶发错误 / 换了节点):逐步放大回去
263
+ successes = 0
264
+ }
265
+ } catch (err) {
266
+ successes = 0
267
+ const texts = errorTexts(err)
268
+ const kinds = texts.map(kindOf)
269
+ // 上次成功的节点这次的错误最能说明问题(其他节点可能一直就不能用,它们的报错不代表当前范围有问题)
270
+ const preferred = err instanceof GetLogsError && err.preferred !== undefined ? kindOf(errorText(err.preferred)) : undefined
271
+ const limit = parseRangeLimit(texts)
272
+ if (limit !== null && limit < range) {
273
+ range = limit // 节点明确给了范围上限
274
+ setLimit(limit)
275
+ } else if (kinds.every((k) => k === 'unsupported')) {
276
+ // 所有节点都不开放 getLogs:暂停一段时间,避免每次调用都白发请求
277
+ state.pausedUntil = Date.now() + PAUSE_ON_UNSUPPORTED
278
+ break
279
+ } else if (preferred === 'transient' || (preferred === undefined && kinds.every((k) => k === 'transient' || k === 'unsupported'))) {
280
+ // 在用的节点只是限频 / 超时:不缩小范围,退避后重试;连续 3 次就留到下次调用
281
+ const remaining = timeBudget > 0 ? timeBudget - (Date.now() - startedAt) : Infinity
282
+ if (++transient >= 3 || remaining <= 0) {
283
+ break
284
+ }
285
+ await sleep(Math.min(500 * transient, remaining))
286
+ } else if (range > 1) {
287
+ range = Math.max(1, Math.floor(range / 2)) // 范围超限但没说上限:减半再试
288
+ setLimit(range)
289
+ } else {
290
+ state.pausedUntil = Date.now() + PAUSE_ON_UNSUPPORTED // 连 1 个区块都查不了
291
+ break
292
+ }
293
+ }
294
+ }
295
+ state.blockRange = range
296
+ state.items = [...items]
297
+ run.onDone?.(state, target)
298
+ return state
299
+ }
300
+
168
301
  /**
169
302
  * 增量扫描地址收到的 ERC20 代币(Transfer 事件,to = 该地址):第一次调用记下当前区块,之后每次从上次的位置往后扫。
170
303
  * 用来补充代币列表发现不了的新代币。作为代币来源使用:`combine(defaultTokenSource(), transferScan())`,
@@ -188,100 +321,36 @@ export function transferScan(options: TransferScanOptions = {}): TokenSource {
188
321
  }
189
322
  const key = `${chainId}:${owner.toLowerCase()}`
190
323
  const saved = await loadState(storage, key)
191
- const toResult = (state: TransferScanState | undefined): DiscoveredToken[] => (state?.tokens ?? []).map((address) => ({ address }))
192
- if (saved?.pausedUntil && Date.now() < saved.pausedUntil) {
193
- return toResult(saved)
194
- }
195
-
196
- const startedAt = Date.now()
197
- let target: number
198
- try {
199
- target = (await provider.getBlockNumber()) - confirmations
200
- } catch {
201
- return saved ? toResult(saved) : null // 节点不支持 / 暂时不可用:返回已发现的代币
202
- }
203
- const now = Date.now()
204
- let state: TransferScanState
205
- if (saved) {
206
- state = { ...saved, tokens: [...saved.tokens] }
207
- delete state.pausedUntil
208
- if (state.rangeLimitAt !== undefined && now - state.rangeLimitAt > RANGE_LIMIT_TTL) {
209
- delete state.rangeLimit
210
- delete state.rangeLimitAt
211
- }
212
- } else {
213
- const cursor = Math.max(-1, target - lookback)
214
- state = { startBlock: cursor + 1, cursor, tokens: [], blockRange: maxRange }
215
- }
216
- const tokens = new Set(state.tokens)
324
+ const toResult = (tokens: readonly string[]): DiscoveredToken[] => tokens.map((address) => ({ address }))
217
325
  const ownerTopic = `0x${owner.slice(2).toLowerCase().padStart(64, '0')}`
218
- const cap = () => Math.min(maxRange, state.rangeLimit ?? maxRange)
219
- const setLimit = (limit: number) => {
220
- state.rangeLimit = limit
221
- state.rangeLimitAt = Date.now()
326
+ const savedProgress = saved && { ...saved, items: saved.tokens }
327
+ const progress = await runLogScan({
328
+ provider,
329
+ saved: savedProgress,
330
+ initialCursor: (target) => target - lookback,
331
+ filter: { topics: [TRANSFER_TOPIC, null, ownerTopic] },
332
+ // ERC20 的 Transfer 有 3 个 topic;ERC721 的 tokenId 也是 indexed,有 4 个,排除
333
+ collect: (log) => (log.topics.length === 3 ? log.address.toLowerCase() : null),
334
+ maxRange,
335
+ maxRequests,
336
+ timeBudget,
337
+ confirmations,
338
+ })
339
+ if (!progress) {
340
+ return null
222
341
  }
223
- let range = Math.min(state.blockRange, cap())
224
- let successes = 0
225
- let transient = 0
226
-
227
- for (let requests = 0; requests < maxRequests && state.cursor < target; requests++) {
228
- if (timeBudget > 0 && Date.now() - startedAt >= timeBudget) {
229
- break // 超出时间预算,留到下次调用
230
- }
231
- const fromBlock = state.cursor + 1
232
- const toBlock = Math.min(target, fromBlock + range - 1)
233
- try {
234
- const logs = await provider.getLogs({ fromBlock, toBlock, topics: [TRANSFER_TOPIC, null, ownerTopic] })
235
- for (const log of logs) {
236
- // ERC20 的 Transfer 有 3 个 topic;ERC721 的 tokenId 也是 indexed,有 4 个,排除
237
- if (log.topics.length === 3) {
238
- tokens.add(log.address.toLowerCase())
239
- }
240
- }
241
- state.cursor = toBlock
242
- transient = 0
243
- if (++successes >= GROW_AFTER && range < cap()) {
244
- range = Math.min(cap(), range * 2) // 之前缩小过(偶发错误 / 换了节点):逐步放大回去
245
- successes = 0
246
- }
247
- } catch (err) {
248
- successes = 0
249
- const texts = errorTexts(err)
250
- const kinds = texts.map(kindOf)
251
- // 上次成功的节点这次的错误最能说明问题(其他节点可能一直就不能用,它们的报错不代表当前范围有问题)
252
- const preferred = err instanceof GetLogsError && err.preferred !== undefined ? kindOf(errorText(err.preferred)) : undefined
253
- const limit = parseRangeLimit(texts)
254
- if (limit !== null && limit < range) {
255
- range = limit // 节点明确给了范围上限
256
- setLimit(limit)
257
- } else if (kinds.every((k) => k === 'unsupported')) {
258
- // 所有节点都不开放 getLogs:暂停一段时间,避免每次调用都白发请求
259
- state.pausedUntil = Date.now() + PAUSE_ON_UNSUPPORTED
260
- break
261
- } else if (preferred === 'transient' || (preferred === undefined && kinds.every((k) => k === 'transient' || k === 'unsupported'))) {
262
- // 在用的节点只是限频 / 超时:不缩小范围,退避后重试;连续 3 次就留到下次调用
263
- const remaining = timeBudget > 0 ? timeBudget - (Date.now() - startedAt) : Infinity
264
- if (++transient >= 3 || remaining <= 0) {
265
- break
266
- }
267
- await sleep(Math.min(500 * transient, remaining))
268
- } else if (range > 1) {
269
- range = Math.max(1, Math.floor(range / 2)) // 范围超限但没说上限:减半再试
270
- setLimit(range)
271
- } else {
272
- state.pausedUntil = Date.now() + PAUSE_ON_UNSUPPORTED // 连 1 个区块都查不了
273
- break
274
- }
275
- }
342
+ // 暂停中 / 拿不到最新区块:进度没变,不用写回
343
+ if (progress === savedProgress) {
344
+ return toResult(progress.items)
276
345
  }
277
- state.blockRange = range
278
- state.tokens = [...tokens]
346
+ const { items, ...rest } = progress
347
+ const state: TransferScanState = { ...rest, tokens: items }
279
348
  try {
280
349
  await storage.set(key, state)
281
350
  } catch {
282
351
  // 存储不可用:只影响下次能否接着扫
283
352
  }
284
- return toResult(state)
353
+ return toResult(state.tokens)
285
354
  },
286
355
  }
287
356
  }
package/src/source.ts CHANGED
@@ -143,6 +143,11 @@ export class ChainCheckedProvider implements EthersLikeProvider {
143
143
  return this.#run(() => this.inner.call(tx))
144
144
  }
145
145
 
146
+ /** 对内层节点做任意请求,同样先校验链(写操作模块用) */
147
+ request<T>(fn: (inner: EthersLikeProvider) => Promise<T>): Promise<T> {
148
+ return this.#run(() => fn(this.inner))
149
+ }
150
+
146
151
  getBalance(...args: Parameters<EthersLikeProvider['getBalance']>): Promise<bigint> {
147
152
  return this.#run(() => this.inner.getBalance(...args))
148
153
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * LP 仓位(子路径 `@w3lib/evm-batch-call/lp`):查一个地址在各 DEX 的流动性仓位——V2 的 LP 代币、V3 / V4 的仓位 NFT,
3
+ * 代币数量、未领取的手续费、可选的美元价值。不放在主入口,不用时打包不会带上这部分代码。
4
+ *
5
+ * ```ts
6
+ * import { getLpPositions } from '@w3lib/evm-batch-call/lp'
7
+ *
8
+ * await getLpPositions(user, { chainId: 56 }) // 节点参数与 getBalances 相同
9
+ * await getLpPositions(user, { chainId: 56, prices: true }) // 带美元价值(DefiLlama)
10
+ * ```
11
+ */
12
+ import { lpPositions, type LpPosition, type LpPositionsOptions } from '../lp.js'
13
+ import { resolve, type ShortcutProviderOptions } from '../shortcuts.js'
14
+
15
+ export {
16
+ LP_PROTOCOLS,
17
+ LP_QUOTE_TOKENS,
18
+ lpPositions,
19
+ resolveLpProtocols,
20
+ type LpConcentratedPosition,
21
+ type LpPosition,
22
+ type LpPositionsOptions,
23
+ type LpProtocol,
24
+ type LpToken,
25
+ type LpV2Position,
26
+ type LpVersion,
27
+ } from '../lp.js'
28
+ export {
29
+ decodePositionInfo,
30
+ keyValueLpScanStorage,
31
+ memoryLpScanStorage,
32
+ poolIdOf,
33
+ type LpScanStorage,
34
+ type LpV4Options,
35
+ type V4ScanOptions,
36
+ type V4ScanProgress,
37
+ } from '../lpV4.js'
38
+ export { MAX_TICK, MIN_TICK, getAmountsForLiquidity, getFeeGrowthInside, getFeesEarned, getSqrtRatioAtTick, sqrtPriceToPrice, tickToPrice } from '../lpMath.js'
39
+ export { defillamaPrices, type PriceSource } from '../owner.js'
40
+
41
+ export interface GetLpPositionsOptions extends LpPositionsOptions, ShortcutProviderOptions {}
42
+
43
+ /**
44
+ * 查一个地址的 LP 仓位(V2 LP 代币、V3 仓位 NFT;V4 仓位 NFT 需要传 v4.tokenIds 或开启 v4.scan),所有合约读取用 multicall 批量完成。
45
+ *
46
+ * ```ts
47
+ * await getLpPositions(user, { chainId: 56 })
48
+ * // [{ version: 'v3', protocol: 'pancakeswap-v3', pool, tokenId: '123', token0: { symbol: 'WBNB', … }, token1: { symbol: 'USDT', … },
49
+ * // amount0Formatted: '1.2', amount1Formatted: '700', fees0Formatted: '0.003', fees1Formatted: '1.8', inRange: true, … },
50
+ * // { version: 'v2', protocol: 'pancakeswap-v2', pool, balance, share: 0.0001, amount0Formatted, amount1Formatted, … }]
51
+ * await getLpPositions(user, { chainId: 56, tokens: heldTokens }) // V2:用持有的代币 × 计价币找池子
52
+ * await getLpPositions(user, { chainId: 56, prices: true }) // 带美元价值,按价值排序
53
+ * ```
54
+ */
55
+ export function getLpPositions(owner: string, options: GetLpPositionsOptions = {}): Promise<LpPosition[]> {
56
+ const { provider, overrides, own } = resolve(options, [
57
+ 'protocols',
58
+ 'versions',
59
+ 'pools',
60
+ 'tokens',
61
+ 'quoteTokens',
62
+ 'source',
63
+ 'includeClosed',
64
+ 'maxPositions',
65
+ 'v4',
66
+ 'prices',
67
+ 'onPriceError',
68
+ 'fetch',
69
+ ])
70
+ return lpPositions(provider, owner, { ...overrides, ...own })
71
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 批量写操作(子路径 `@w3lib/evm-batch-call/write`):批量调用合约、一对多转账、多钱包归集。
3
+ * 不放在主入口,只做查询时打包不会带上这部分代码。
4
+ *
5
+ * ```ts
6
+ * import { Provider, NATIVE_TOKEN } from '@w3lib/evm-batch-call'
7
+ * import { batchCall, batchCollect, batchTransfer } from '@w3lib/evm-batch-call/write'
8
+ *
9
+ * const multi = new Provider(56, rpcUrl)
10
+ * await batchTransfer(multi, privateKey, [{ to: a, formatted: '1.5' }, { to: b, formatted: '2' }], { token: USDT })
11
+ * await batchCollect(multi, [key1, key2], hotWallet, { tokens: [USDT, NATIVE_TOKEN], gasWallet: gasKey })
12
+ * await batchCall(multi, [{ from: privateKey, call: usdt.approve(router, amount) }, { from: privateKey, to: router, data }])
13
+ * ```
14
+ */
15
+ export {
16
+ SIMPLE_7702_ACCOUNT,
17
+ batchCall,
18
+ revokeDelegation,
19
+ type BatchCallMode,
20
+ type BatchCallOptions,
21
+ type WriteCall,
22
+ } from '../write/batch.js'
23
+ export { batchCollect, type BatchCollectOptions, type CollectItemResult, type CollectResult } from '../write/collect.js'
24
+ export { DISPERSE_ADDRESS, batchTransfer, type ApproveResult, type BatchTransferOptions, type TransferItem, type TransferMode, type TransferResult } from '../write/transfer.js'
25
+ export type { SendOptions, TronSendOptions, WriteResult, WriteStatus } from '../write/send.js'
26
+ export type { SignerInput, TronSigner, TronWalletLike, WriteSigner } from '../write/signer.js'
package/src/tron.ts CHANGED
@@ -169,6 +169,11 @@ export class TronProvider implements EthersLikeProvider {
169
169
  })
170
170
  }
171
171
 
172
+ /** 直接请求 HTTP API(如 'wallet/getnowblock'),同样经过限流和 429 退避(写操作模块用) */
173
+ request(path: string, body: Record<string, unknown> = {}): Promise<any> {
174
+ return this.#limit(() => this.#request(path, body))
175
+ }
176
+
172
177
  async call(tx: TransactionRequest): Promise<string> {
173
178
  assertLatest(tx.blockTag)
174
179
  const body: Record<string, unknown> = {