@w3lib/evm-batch-call 0.7.0 → 0.8.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 (159) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +206 -1
  3. package/dist/dividends.d.ts +135 -0
  4. package/dist/dividends.d.ts.map +1 -0
  5. package/dist/dividends.js +253 -0
  6. package/dist/dividends.js.map +1 -0
  7. package/dist/history.d.ts +154 -0
  8. package/dist/history.d.ts.map +1 -0
  9. package/dist/history.js +247 -0
  10. package/dist/history.js.map +1 -0
  11. package/dist/historySources.d.ts +32 -0
  12. package/dist/historySources.d.ts.map +1 -0
  13. package/dist/historySources.js +319 -0
  14. package/dist/historySources.js.map +1 -0
  15. package/dist/liquidity/v3.d.ts +2 -0
  16. package/dist/liquidity/v3.d.ts.map +1 -1
  17. package/dist/liquidity/v3.js +1 -1
  18. package/dist/liquidity/v3.js.map +1 -1
  19. package/dist/liquidity/v4.d.ts +21 -0
  20. package/dist/liquidity/v4.d.ts.map +1 -1
  21. package/dist/liquidity/v4.js +4 -3
  22. package/dist/liquidity/v4.js.map +1 -1
  23. package/dist/lp.d.ts +8 -0
  24. package/dist/lp.d.ts.map +1 -1
  25. package/dist/lp.js +17 -1
  26. package/dist/lp.js.map +1 -1
  27. package/dist/lpV4.d.ts +4 -0
  28. package/dist/lpV4.d.ts.map +1 -1
  29. package/dist/lpV4.js +27 -3
  30. package/dist/lpV4.js.map +1 -1
  31. package/dist/permit.d.ts +137 -0
  32. package/dist/permit.d.ts.map +1 -0
  33. package/dist/permit.js +355 -0
  34. package/dist/permit.js.map +1 -0
  35. package/dist/portfolio.d.ts +2 -0
  36. package/dist/portfolio.d.ts.map +1 -1
  37. package/dist/portfolio.js +9 -4
  38. package/dist/portfolio.js.map +1 -1
  39. package/dist/rewards/farms.d.ts +26 -0
  40. package/dist/rewards/farms.d.ts.map +1 -0
  41. package/dist/rewards/farms.js +210 -0
  42. package/dist/rewards/farms.js.map +1 -0
  43. package/dist/rewards/index.d.ts +42 -0
  44. package/dist/rewards/index.d.ts.map +1 -0
  45. package/dist/rewards/index.js +382 -0
  46. package/dist/rewards/index.js.map +1 -0
  47. package/dist/rewards/types.d.ts +170 -0
  48. package/dist/rewards/types.d.ts.map +1 -0
  49. package/dist/rewards/types.js +2 -0
  50. package/dist/rewards/types.js.map +1 -0
  51. package/dist/simulate.d.ts +186 -0
  52. package/dist/simulate.d.ts.map +1 -0
  53. package/dist/simulate.js +559 -0
  54. package/dist/simulate.js.map +1 -0
  55. package/dist/simulateDecode.d.ts +33 -0
  56. package/dist/simulateDecode.d.ts.map +1 -0
  57. package/dist/simulateDecode.js +321 -0
  58. package/dist/simulateDecode.js.map +1 -0
  59. package/dist/smartAccount/account.d.ts +31 -0
  60. package/dist/smartAccount/account.d.ts.map +1 -0
  61. package/dist/smartAccount/account.js +33 -0
  62. package/dist/smartAccount/account.js.map +1 -0
  63. package/dist/smartAccount/clients.d.ts +77 -0
  64. package/dist/smartAccount/clients.d.ts.map +1 -0
  65. package/dist/smartAccount/clients.js +122 -0
  66. package/dist/smartAccount/clients.js.map +1 -0
  67. package/dist/smartAccount/eip5792.d.ts +77 -0
  68. package/dist/smartAccount/eip5792.d.ts.map +1 -0
  69. package/dist/smartAccount/eip5792.js +152 -0
  70. package/dist/smartAccount/eip5792.js.map +1 -0
  71. package/dist/smartAccount/send.d.ts +40 -0
  72. package/dist/smartAccount/send.d.ts.map +1 -0
  73. package/dist/smartAccount/send.js +271 -0
  74. package/dist/smartAccount/send.js.map +1 -0
  75. package/dist/smartAccount/userOp.d.ts +91 -0
  76. package/dist/smartAccount/userOp.d.ts.map +1 -0
  77. package/dist/smartAccount/userOp.js +124 -0
  78. package/dist/smartAccount/userOp.js.map +1 -0
  79. package/dist/subpaths/dividends.d.ts +28 -0
  80. package/dist/subpaths/dividends.d.ts.map +1 -0
  81. package/dist/subpaths/dividends.js +29 -0
  82. package/dist/subpaths/dividends.js.map +1 -0
  83. package/dist/subpaths/history.d.ts +22 -0
  84. package/dist/subpaths/history.d.ts.map +1 -0
  85. package/dist/subpaths/history.js +23 -0
  86. package/dist/subpaths/history.js.map +1 -0
  87. package/dist/subpaths/permit.d.ts +14 -0
  88. package/dist/subpaths/permit.d.ts.map +1 -0
  89. package/dist/subpaths/permit.js +14 -0
  90. package/dist/subpaths/permit.js.map +1 -0
  91. package/dist/subpaths/price.js +2 -2
  92. package/dist/subpaths/price.js.map +1 -1
  93. package/dist/subpaths/rewards.d.ts +27 -0
  94. package/dist/subpaths/rewards.d.ts.map +1 -0
  95. package/dist/subpaths/rewards.js +25 -0
  96. package/dist/subpaths/rewards.js.map +1 -0
  97. package/dist/subpaths/simulate.d.ts +25 -0
  98. package/dist/subpaths/simulate.d.ts.map +1 -0
  99. package/dist/subpaths/simulate.js +28 -0
  100. package/dist/subpaths/simulate.js.map +1 -0
  101. package/dist/subpaths/smart-account.d.ts +24 -0
  102. package/dist/subpaths/smart-account.d.ts.map +1 -0
  103. package/dist/subpaths/smart-account.js +24 -0
  104. package/dist/subpaths/smart-account.js.map +1 -0
  105. package/dist/swap.d.ts +45 -5
  106. package/dist/swap.d.ts.map +1 -1
  107. package/dist/swap.js +108 -12
  108. package/dist/swap.js.map +1 -1
  109. package/dist/write/batch.d.ts +34 -2
  110. package/dist/write/batch.d.ts.map +1 -1
  111. package/dist/write/batch.js +22 -3
  112. package/dist/write/batch.js.map +1 -1
  113. package/dist/write/collect.d.ts +9 -0
  114. package/dist/write/collect.d.ts.map +1 -1
  115. package/dist/write/collect.js +41 -1
  116. package/dist/write/collect.js.map +1 -1
  117. package/dist/write/send.d.ts +4 -0
  118. package/dist/write/send.d.ts.map +1 -1
  119. package/dist/write/send.js.map +1 -1
  120. package/dist/write/signer.d.ts +5 -1
  121. package/dist/write/signer.d.ts.map +1 -1
  122. package/dist/write/signer.js.map +1 -1
  123. package/dist/write/transfer.d.ts +5 -3
  124. package/dist/write/transfer.d.ts.map +1 -1
  125. package/dist/write/transfer.js +12 -6
  126. package/dist/write/transfer.js.map +1 -1
  127. package/package.json +35 -1
  128. package/src/dividends.ts +360 -0
  129. package/src/history.ts +367 -0
  130. package/src/historySources.ts +360 -0
  131. package/src/liquidity/v3.ts +1 -1
  132. package/src/liquidity/v4.ts +4 -3
  133. package/src/lp.ts +18 -1
  134. package/src/lpV4.ts +32 -2
  135. package/src/permit.ts +457 -0
  136. package/src/portfolio.ts +11 -4
  137. package/src/rewards/farms.ts +229 -0
  138. package/src/rewards/index.ts +431 -0
  139. package/src/rewards/types.ts +179 -0
  140. package/src/simulate.ts +727 -0
  141. package/src/simulateDecode.ts +352 -0
  142. package/src/smartAccount/account.ts +63 -0
  143. package/src/smartAccount/clients.ts +180 -0
  144. package/src/smartAccount/eip5792.ts +187 -0
  145. package/src/smartAccount/send.ts +341 -0
  146. package/src/smartAccount/userOp.ts +180 -0
  147. package/src/subpaths/dividends.ts +47 -0
  148. package/src/subpaths/history.ts +41 -0
  149. package/src/subpaths/permit.ts +39 -0
  150. package/src/subpaths/price.ts +2 -2
  151. package/src/subpaths/rewards.ts +46 -0
  152. package/src/subpaths/simulate.ts +51 -0
  153. package/src/subpaths/smart-account.ts +40 -0
  154. package/src/swap.ts +139 -18
  155. package/src/write/batch.ts +44 -5
  156. package/src/write/collect.ts +60 -1
  157. package/src/write/send.ts +4 -0
  158. package/src/write/signer.ts +5 -1
  159. package/src/write/transfer.ts +17 -10
package/src/swap.ts CHANGED
@@ -3,13 +3,14 @@
3
3
  * 批量报价(V2 用 Router 的 getAmountsOut,V3 用 QuoterV2,按 gas 分成几次并发的 eth_call),选输出最多的;按滑点算最少收到的数量,
4
4
  * 构建授权 + 兑换的交易(V2 走 Router 的 swapExact…SupportingFeeOnTransferTokens,V3 走 SwapRouter02 / PancakeSwap SmartRouter 的 multicall)
5
5
  */
6
- import { Interface, type BigNumberish } from 'ethers'
6
+ import { Interface, getAddress, type BigNumberish } from 'ethers'
7
7
 
8
8
  import { approvals, createCtx, evm, fmt, isNative, minus, out, parseAmount, same, tokenInfo, type Ctx } from './liquidity/common.js'
9
9
  import type { LiquidityTxResult } from './liquidity/types.js'
10
10
  import { LP_PROTOCOLS, LP_QUOTE_TOKENS, type LpProtocol, type LpToken } from './lp.js'
11
11
  import type { BlockTag } from './aggregate.js'
12
12
  import type { Provider } from './provider.js'
13
+ import type { SignedPermit } from './permit.js'
13
14
  import type { BatchCallOptions } from './write/batch.js'
14
15
  import type { SignerInput } from './write/signer.js'
15
16
 
@@ -42,6 +43,7 @@ const v2RouterInterface = new Interface([
42
43
  'function swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable',
43
44
  'function swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
44
45
  ])
46
+ const permitInterface = new Interface(['function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)'])
45
47
  const v3FactoryInterface = new Interface(['function getPool(address tokenA, address tokenB, uint24 fee) view returns (address)'])
46
48
  const v3PoolInterface = new Interface(['function liquidity() view returns (uint128)'])
47
49
  /** QuoterV2 很费 gas(池子很薄时一次可达上千万):每次 eth_call 只放几个报价,多个 eth_call 并发,避免超过节点的 gas 上限后被悄悄截掉 */
@@ -56,6 +58,7 @@ const swapRouterInterface = new Interface([
56
58
  'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)',
57
59
  'function exactInput((bytes path, address recipient, uint256 amountIn, uint256 amountOutMinimum) params) payable returns (uint256 amountOut)',
58
60
  'function unwrapWETH9(uint256 amountMinimum, address recipient) payable',
61
+ 'function selfPermitIfNecessary(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) payable',
59
62
  'function multicall(uint256 deadline, bytes[] data) payable returns (bytes[] results)',
60
63
  ])
61
64
 
@@ -75,8 +78,20 @@ export interface SwapOptions {
75
78
  deadline?: number
76
79
  /** 收到代币的地址,默认 owner */
77
80
  recipient?: string
78
- /** 授权额度:'exact'(默认,本次卖出的数量)或 'max' */
79
- approve?: 'exact' | 'max'
81
+ /**
82
+ * 授权方式:
83
+ * - 'exact'(默认):授权本次卖出的数量;'max':无限额度
84
+ * - 'permit':用 EIP-2612 签名代替授权交易(要代币支持,见 /permit 的 detectPermit)。V3 路线(SwapRouter02 / SmartRouter)把 selfPermit 放进同一笔 multicall,
85
+ * 一次签名、一笔交易;V2 路线的 Router 没有 selfPermit,只有 `atomic` 批量(7702 / 4337 / 5792)发送时才用 permit,否则仍然是普通授权
86
+ * - 'permit2':内置的 V2 / V3 路由都不使用 Permit2(它们直接 transferFrom),所以会退回普通的 'exact' 授权;结果的 plan.approval 里有说明
87
+ * - 'auto':能用 permit 就用,否则普通授权
88
+ * 退回普通授权时 plan.approval.method 为 'approve',reason 写明原因
89
+ */
90
+ approve?: 'exact' | 'max' | 'permit' | 'permit2' | 'auto'
91
+ /** 交易会在一笔原子批量里发送(`swap` 的 mode 为 7702 / 4337 / 5792 时自动为 true):V2 路线才能把 permit 和兑换放进同一笔 */
92
+ atomic?: boolean
93
+ /** 已签名的 permit(`swap` 内部使用):V3 路线嵌入 multicall 的 selfPermit,V2 路线放在兑换前;不再生成授权交易 */
94
+ signedPermit?: SignedPermit
80
95
  /**
81
96
  * 代币的转账税(比例,0.05 = 5%),默认 0。V2 报价(getAmountsOut)不含转账税,带税的代币
82
97
  * 实际收到的比报价少,最少收到按 (1 − slippage) × (1 − tax) 算,否则会因 INSUFFICIENT_OUTPUT_AMOUNT 回滚。
@@ -106,8 +121,8 @@ export interface SwapRoute {
106
121
  }
107
122
 
108
123
  export interface SwapTx {
109
- /** approve:卖出的代币授权给 Router;swap:兑换 */
110
- kind: 'approve' | 'swap'
124
+ /** approve:卖出的代币授权给 Router;permit:提交 EIP-2612 签名(V2 路线批量发送时);swap:兑换 */
125
+ kind: 'approve' | 'permit' | 'swap'
111
126
  to: string
112
127
  data: string
113
128
  /** 附带的主币(最小单位):卖出主币时为卖出的数量 */
@@ -136,6 +151,19 @@ export interface SwapPlan {
136
151
  routes: SwapRoute[]
137
152
  /** 按顺序发送的交易(授权在前) */
138
153
  transactions: SwapTx[]
154
+ /** 请求了 permit / permit2 / auto 时:实际采用的授权方式和原因 */
155
+ approval?: SwapApproval
156
+ }
157
+
158
+ export interface SwapApproval {
159
+ requested: 'permit' | 'permit2' | 'auto'
160
+ /** 'permit':用签名;'approve':退回普通授权交易(reason 说明);'none':不需要授权(卖出主币 / 额度已够) */
161
+ method: 'permit' | 'approve' | 'none'
162
+ /** permit 的提交方式:selfPermit(V3 的 multicall 内)/ batch(和兑换放在同一笔原子批量) */
163
+ via?: 'selfPermit' | 'batch'
164
+ reason?: string
165
+ /** 要签的 permit(owner 授权 spender 精确的卖出数量,签名有效期与兑换的 deadline 相同) */
166
+ permit?: { token: string; spender: string; value: string; deadline: number }
139
167
  }
140
168
 
141
169
  interface Candidate {
@@ -264,7 +292,7 @@ function amountOf(c: Candidate, result: unknown): bigint {
264
292
  */
265
293
  export async function buildSwap(provider: Provider, owner: string, params: SwapParams, options: SwapOptions = {}): Promise<SwapPlan> {
266
294
  const { slippage, deadline, recipient, approve } = options
267
- const ctx = await createCtx(provider, owner, { slippage, deadline, recipient, approve })
295
+ const ctx = await createCtx(provider, owner, { slippage, deadline, recipient, approve: approve === 'max' ? 'max' : 'exact' })
268
296
  const wrapped = LP_QUOTE_TOKENS[ctx.chainId]?.[0]
269
297
  const nativeIn = isNative(ctx, params.tokenIn)
270
298
  const nativeOut = isNative(ctx, params.tokenOut)
@@ -298,7 +326,16 @@ export async function buildSwap(provider: Provider, owner: string, params: SwapP
298
326
  // approvals 只会返回 kind 为 'approve' 的交易
299
327
  // Router 在 Tron 上是 T 地址:授权的 ABI 参数要用 0x 格式
300
328
  const transactions = (nativeIn ? [] : await approvals(ctx, [{ token: tokenIn, spender: evm(best.router), amount: amountIn }])) as SwapTx[]
301
- transactions.push(swapTx(ctx, best, amountIn, amountOutMin, nativeIn, nativeOut))
329
+ let approval: SwapApproval | undefined
330
+ const requested = options.approve
331
+ if (requested === 'permit' || requested === 'permit2' || requested === 'auto') {
332
+ approval = await decideApproval(ctx, requested, best, tokenIn, amountIn, nativeIn, transactions.some((t) => t.kind === 'approve'), options)
333
+ }
334
+ let permitCall: string | undefined
335
+ if (options.signedPermit && approval?.method === 'permit') {
336
+ permitCall = applySignedPermit(options.signedPermit, approval, ctx, transactions)
337
+ }
338
+ transactions.push(swapTx(ctx, best, amountIn, amountOutMin, nativeIn, nativeOut, permitCall))
302
339
 
303
340
  const route = (c: Candidate & { amountOut: bigint }): SwapRoute => ({
304
341
  protocol: c.protocol.id,
@@ -323,10 +360,49 @@ export async function buildSwap(provider: Provider, owner: string, params: SwapP
323
360
  route: route(best),
324
361
  routes: routes.map(route),
325
362
  transactions,
363
+ ...(approval ? { approval } : {}),
364
+ }
365
+ }
366
+
367
+ /** permit / permit2 / auto:判断这条路线和这个代币能不能用签名代替授权交易,不能的退回普通授权并写明原因 */
368
+ async function decideApproval(ctx: Ctx, requested: SwapApproval['requested'], best: Candidate, tokenIn: string, amountIn: bigint, nativeIn: boolean, needsApprove: boolean, options: SwapOptions): Promise<SwapApproval> {
369
+ if (nativeIn || !needsApprove) return { requested, method: 'none' }
370
+ const fallback = (reason: string): SwapApproval => ({ requested, method: 'approve', reason })
371
+ if (requested === 'permit2') {
372
+ return fallback('The built-in V2 / V3 routers pull tokens with transferFrom and do not consume Permit2 signatures; used a normal exact approve')
373
+ }
374
+ if (ctx.tron) return fallback('permit is not available on Tron; used a normal approve')
375
+ const { detectPermit } = await import('./permit.js')
376
+ const support = await detectPermit(ctx.provider, tokenIn)
377
+ if (!support.kind) return fallback(`${support.reason}; used a normal approve`)
378
+ if (support.kind === 'dai') return fallback('A DAI-style permit can only grant an unlimited allowance; used a normal exact approve')
379
+ const via = best.version === 'v3' ? 'selfPermit' : 'batch'
380
+ if (via === 'batch' && !options.atomic) {
381
+ return fallback("V2 routers have no selfPermit, so a separate permit() transaction saves nothing; use mode '7702' / '4337' / '5792' to send the permit and the swap atomically. Used a normal approve")
382
+ }
383
+ return { requested, method: 'permit', via, permit: { token: tokenIn, spender: evm(best.router), value: amountIn.toString(), deadline: ctx.deadline } }
384
+ }
385
+
386
+ /** 已签名的 permit 放进交易:V3 返回 selfPermit 的 calldata(嵌进 multicall),V2 往 transactions 里加一笔 permit 交易 */
387
+ function applySignedPermit(signed: SignedPermit, approval: SwapApproval, ctx: Ctx, transactions: SwapTx[]): string | undefined {
388
+ const want = approval.permit as NonNullable<SwapApproval['permit']>
389
+ const p = signed.permit
390
+ if (p.kind !== 'eip2612' || getAddress(p.tokens[0] as string) !== getAddress(want.token) || getAddress(p.spender) !== getAddress(want.spender) || p.owner !== ctx.owner || BigInt(p.amounts[0] as string) !== BigInt(want.value)) {
391
+ throw new Error('The signed permit does not match the swap (token / spender / owner / amount)')
392
+ }
393
+ // 授权交易换成 permit
394
+ const at = transactions.findIndex((t) => t.kind === 'approve')
395
+ transactions.splice(at, transactions.length)
396
+ if (approval.via === 'selfPermit') {
397
+ return swapRouterInterface.encodeFunctionData('selfPermitIfNecessary', [want.token, want.value, p.deadline, signed.v, signed.r, signed.s])
326
398
  }
399
+ const m = p.typedData.message
400
+ const data = permitInterface.encodeFunctionData('permit', [m.owner, m.spender, m.value, m.deadline, signed.v, signed.r, signed.s])
401
+ transactions.push({ kind: 'permit', to: out(ctx, want.token), data, value: '0', token: out(ctx, want.token), spender: out(ctx, want.spender), amount: want.value })
402
+ return undefined
327
403
  }
328
404
 
329
- function swapTx(ctx: Ctx, c: Candidate, amountIn: bigint, amountOutMin: bigint, nativeIn: boolean, nativeOut: boolean): SwapTx {
405
+ function swapTx(ctx: Ctx, c: Candidate, amountIn: bigint, amountOutMin: bigint, nativeIn: boolean, nativeOut: boolean, selfPermit?: string): SwapTx {
330
406
  const value = nativeIn ? amountIn.toString() : '0'
331
407
  if (c.version === 'v2') {
332
408
  const data = nativeIn
@@ -344,7 +420,7 @@ function swapTx(ctx: Ctx, c: Candidate, amountIn: bigint, amountOutMin: bigint,
344
420
  { tokenIn: c.path[0], tokenOut: c.path[1], fee: c.fees[0], recipient, amountIn, amountOutMinimum: amountOutMin, sqrtPriceLimitX96: 0n },
345
421
  ])
346
422
  : swapRouterInterface.encodeFunctionData('exactInput', [{ path: encodePath(c.path, c.fees), recipient, amountIn, amountOutMinimum: amountOutMin }])
347
- const calls = nativeOut ? [swap, swapRouterInterface.encodeFunctionData('unwrapWETH9', [amountOutMin, ctx.recipient])] : [swap]
423
+ const calls = [...(selfPermit ? [selfPermit] : []), swap, ...(nativeOut ? [swapRouterInterface.encodeFunctionData('unwrapWETH9', [amountOutMin, ctx.recipient])] : [])]
348
424
  return { kind: 'swap', to: out(ctx, c.router), data: swapRouterInterface.encodeFunctionData('multicall', [ctx.deadline, calls]), value }
349
425
  }
350
426
 
@@ -352,14 +428,21 @@ export type SwapTxResult = Omit<LiquidityTxResult, 'kind'> & { kind: SwapTx['kin
352
428
 
353
429
  export interface SwapResult {
354
430
  plan: SwapPlan
355
- /** 每笔交易的结果(授权、兑换) */
431
+ /** 每笔交易的结果(授权 / permit、兑换) */
356
432
  results: SwapTxResult[]
357
433
  /** 兑换那一笔的状态和哈希 */
358
434
  status: LiquidityTxResult['status']
359
435
  hash: string | null
436
+ /** 请求 permit / permit2 / auto 时:实际采用的授权方式(同 plan.approval,退回普通授权时有原因) */
437
+ approval?: SwapApproval
360
438
  }
361
439
 
362
440
  export interface SwapSendOptions extends SwapOptions, Omit<BatchCallOptions, 'mode' | 'continueOnError' | 'onUpdate'> {
441
+ /**
442
+ * 发送方式:'tx'(默认,授权和兑换分别发,授权确认后重新报价);
443
+ * '7702' / '4337' / '5792':授权(或 permit)和兑换放进同一笔原子批量,见 BatchCallOptions.mode
444
+ */
445
+ mode?: 'tx' | '7702' | '4337' | '5792'
363
446
  onUpdate?: (result: SwapTxResult) => void
364
447
  }
365
448
 
@@ -370,21 +453,55 @@ export interface SwapSendOptions extends SwapOptions, Omit<BatchCallOptions, 'mo
370
453
  * ```ts
371
454
  * const r = await swap(provider, key, { tokenIn: USDT, tokenOut: NATIVE_TOKEN, amountInFormatted: '100' }, { slippage: 0.01 })
372
455
  * r.status // 'confirmed'
456
+ *
457
+ * // 用 EIP-2612 签名代替授权交易(V3 路线一笔交易完成;不支持时退回普通授权,r.approval.reason 说明原因)
458
+ * await swap(provider, signer, params, { approve: 'auto' })
373
459
  * ```
374
460
  */
375
461
  export async function swap(provider: Provider, from: SignerInput, params: SwapParams, options: SwapSendOptions = {}): Promise<SwapResult> {
376
462
  const [{ batchCall, tronModule }, { resolveSigner }, { emptyOutcome, toWriteResult }] = await Promise.all([import('./write/batch.js'), import('./write/signer.js'), import('./write/send.js')])
377
463
  const tron = await tronModule(provider)
378
- const owner = tron ? await tron.tronSignerAddress(from) : (await resolveSigner(from)).address
379
- let plan = await buildSwap(provider, owner, params, options)
380
- const send = async (txs: SwapTx[]): Promise<SwapTxResult[]> =>
464
+ const resolved = tron ? null : await resolveSigner(from)
465
+ const owner = tron ? await tron.tronSignerAddress(from) : (resolved as NonNullable<typeof resolved>).address
466
+ const mode = options.mode ?? 'tx'
467
+ const atomic = mode !== 'tx'
468
+ // 4337 的 SimpleAccount:资产在账户地址里,兑换的 owner 是账户(合约账户不能签 EIP-2612,不用 permit)
469
+ let holder = owner
470
+ let smartHolder = false
471
+ if (mode === '4337' && (options.smartAccount?.account ?? 'simple') === 'simple') {
472
+ const [{ resolveSimpleAccount }, { WriteRpc }] = await Promise.all([import('./smartAccount/account.js'), import('./write/rpc.js')])
473
+ const sa = options.smartAccount
474
+ holder = (await resolveSimpleAccount(await WriteRpc.from(provider), owner, sa?.entryPoint ?? '0.7', sa?.salt, sa?.factory)).address
475
+ smartHolder = true
476
+ }
477
+ const wantsPermit = options.approve === 'permit' || options.approve === 'permit2' || options.approve === 'auto'
478
+ let plan = await buildSwap(provider, holder, params, { ...options, atomic, ...(smartHolder && wantsPermit ? { approve: 'exact' as const } : {}) })
479
+ if (smartHolder && wantsPermit && plan.transactions.length > 1) {
480
+ plan.approval = { requested: options.approve as 'permit', method: 'approve', reason: 'The 4337 smart account is a contract and cannot sign EIP-2612 permits; used a normal approve' }
481
+ }
482
+ if (plan.approval?.method === 'permit' && resolved) {
483
+ // 签 permit:精确的卖出数量、有效期与兑换的 deadline 相同;用户拒绝签名就整体中止(不悄悄改成发授权交易)
484
+ const { buildPermit, signPermit } = await import('./permit.js')
485
+ const want = plan.approval.permit as NonNullable<SwapApproval['permit']>
486
+ const permit = await buildPermit(provider, { token: want.token, owner: holder, spender: want.spender, value: want.value }, { deadlineAt: want.deadline })
487
+ const signed = await signPermit(resolved.signer as Parameters<typeof signPermit>[0], permit, { chainId: await provider.getChainId() })
488
+ try {
489
+ plan = await buildSwap(provider, holder, params, { ...options, atomic, signedPermit: signed })
490
+ } catch (err) {
491
+ // 签名后重新报价,最优路线变了(路由 / 版本 / 数量不同),签名对不上:改用普通授权,不让已经签过的请求白白中止
492
+ if (!/signed permit does not match/.test(err instanceof Error ? err.message : String(err))) throw err
493
+ plan = await buildSwap(provider, holder, params, { ...options, atomic, approve: 'exact' })
494
+ if (plan.approval) plan.approval = { ...plan.approval, requested: options.approve as 'permit', method: 'approve', reason: 'The best route changed after the permit was signed; used a normal approve' }
495
+ }
496
+ }
497
+ const send = async (txs: SwapTx[], asMode: 'tx' | 'atomic' = 'tx'): Promise<SwapTxResult[]> =>
381
498
  (
382
499
  await batchCall(
383
500
  provider,
384
501
  txs.map((tx) => ({ from, to: tx.to, data: tx.data, value: BigInt(tx.value) })),
385
502
  {
386
503
  ...options,
387
- mode: 'tx',
504
+ mode: asMode === 'atomic' ? mode : 'tx',
388
505
  continueOnError: false,
389
506
  onUpdate: options.onUpdate ? (r) => options.onUpdate?.({ ...r, kind: (txs[r.index] as SwapTx).kind }) : undefined,
390
507
  },
@@ -392,15 +509,20 @@ export async function swap(provider: Provider, from: SignerInput, params: SwapPa
392
509
  ).map((r, i) => ({ ...r, kind: (txs[i] as SwapTx).kind }))
393
510
  const txs = plan.transactions
394
511
  let results: SwapTxResult[]
395
- if (txs.length === 1) {
512
+ if (atomic) {
513
+ // 授权 / permit 和兑换在同一笔原子批量里:要么都成功,要么都不生效
514
+ results = await send(txs, 'atomic')
515
+ } else if (txs.length === 1) {
396
516
  results = await send(txs)
397
517
  } else {
398
518
  // 有授权时先发授权:确认后重新报价再发兑换,deadline 和最少收到从发兑换那一刻算,
399
519
  // 不会因为授权等得久而过期或按旧价成交
400
520
  const approved = await send(txs.slice(0, -1))
401
521
  const last = txs[txs.length - 1] as SwapTx
522
+ const approval = plan.approval
402
523
  if (approved.every((r) => r.status === 'confirmed')) {
403
- plan = await buildSwap(provider, owner, params, options)
524
+ plan = await buildSwap(provider, holder, params, { ...options, approve: options.approve === 'max' ? 'max' : 'exact' })
525
+ if (approval) plan.approval = approval // 重新报价时额度已够:保留最初的选择和原因
404
526
  const fresh = plan.transactions[plan.transactions.length - 1] as SwapTx
405
527
  results = [...approved, ...(await send([fresh])).map((r) => ({ ...r, index: txs.length - 1 }))]
406
528
  } else {
@@ -408,6 +530,5 @@ export async function swap(provider: Provider, from: SignerInput, params: SwapPa
408
530
  }
409
531
  }
410
532
  const main = results[results.length - 1] as SwapTxResult
411
- return { plan, results, status: main.status, hash: main.hash }
533
+ return { plan, results, status: main.status, hash: main.hash, ...(plan.approval ? { approval: plan.approval } : {}) }
412
534
  }
413
-
@@ -6,6 +6,8 @@ import type { Provider } from '../provider.js'
6
6
  import { isTronChain } from '../tron.js'
7
7
  import { WriteRpc } from './rpc.js'
8
8
  import { emptyOutcome, sendJobs, toWriteResult, type SendOptions, type TronSendOptions, type TxJob, type TxOutcome, type WriteResult } from './send.js'
9
+ import type { Eip1193Like } from '../smartAccount/eip5792.js'
10
+ import type { SmartAccountOptions } from '../smartAccount/send.js'
9
11
  import { resolveSigner, type ResolvedSigner, type SignerInput } from './signer.js'
10
12
 
11
13
  /**
@@ -38,13 +40,17 @@ export interface WriteCall {
38
40
  gasLimit?: bigint
39
41
  }
40
42
 
41
- export type BatchCallMode = 'tx' | '7702'
43
+ export type BatchCallMode = 'tx' | '7702' | '4337' | '5792'
42
44
 
43
45
  export interface BatchCallOptions extends SendOptions, TronSendOptions {
44
46
  /**
45
47
  * - 'tx'(默认):每个调用一笔交易,所有链都支持
46
48
  * - '7702':钱包通过 EIP-7702 委托给批量执行合约,同一钱包的调用合成一笔交易(按 batchSize 拆分),一笔里的调用要么全部成功要么全部失败。
47
- * 需要链支持 EIP-7702,钱包需要能本地签名(私钥 / ethers Wallet)。委托会一直保留,可用 revokeDelegation 撤销
49
+ * 需要链支持 EIP-7702,钱包需要能本地签名(私钥 / ethers Wallet)。委托会一直保留,可用 revokeDelegation 撤销。
50
+ * 传 sponsor 时由 sponsor 钱包发交易并付 gas(用户只签授权和 UserOperation,不需要有主币),见 sponsor
51
+ * - '4337':ERC-4337 智能账户(smartAccount 选项:bundler / paymaster / selfBundle),一个 UserOperation 原子执行同一钱包的调用,可由 paymaster 代付 gas。
52
+ * 默认账户是 owner 对应的 SimpleAccount(首次使用时部署,资产要放在账户地址里)。子路径 /smart-account
53
+ * - '5792':浏览器钱包的 EIP-5792(wallet_sendCalls):钱包支持原子批量时一次确认原子执行,不支持时退回逐笔发送
48
54
  */
49
55
  mode?: BatchCallMode
50
56
  /**
@@ -58,6 +64,20 @@ export interface BatchCallOptions extends SendOptions, TronSendOptions {
58
64
  batchSize?: number
59
65
  /** 7702:钱包已委托给其他合约时改为委托给 delegate。默认 false(报错,不动已有的委托) */
60
66
  redelegate?: boolean
67
+ /**
68
+ * '7702' 代付 gas:用这个钱包(私钥 / Signer)发 EntryPoint.handleOps 交易,付 gas 的是它,用户钱包不需要主币。
69
+ * 用户签 EIP-7702 授权(未委托时)+ UserOperation;UserOperation 的 gas 价格为 0,sponsor 拿不到补偿,也不能改调用内容。
70
+ * 用 EntryPoint v0.8 + Simple7702Account
71
+ */
72
+ sponsor?: SignerInput
73
+ /** '4337' 的智能账户选项 */
74
+ smartAccount?: SmartAccountOptions
75
+ /** '5792':钱包的 provider(EIP-1193 / BrowserProvider)。默认用 from 的 JsonRpcSigner 背后的 provider */
76
+ eip1193?: Eip1193Like
77
+ /** '5792':要求原子执行,钱包不能原子批量时不退回逐笔发送,直接失败。默认 false */
78
+ atomicRequired?: boolean
79
+ /** '5792':传给 wallet_sendCalls 的 capabilities(如 `{ paymasterService: { url } }`) */
80
+ capabilities?: Record<string, unknown>
61
81
  }
62
82
 
63
83
  /** 归一化后的调用 */
@@ -111,7 +131,26 @@ export async function batchCall(provider: Provider, calls: readonly WriteCall[],
111
131
  }
112
132
  const rpc = await WriteRpc.from(provider)
113
133
  const normalized = await normalizeCalls(calls)
114
- return (options.mode ?? 'tx') === '7702' ? send7702(rpc, normalized, options) : sendEach(rpc, normalized, options)
134
+ return sendByMode(rpc, normalized, options)
135
+ }
136
+
137
+ /** 按 mode 发送;智能账户 / 5792 的代码按需加载,不用时 /write 的体积不变 */
138
+ export async function sendByMode(rpc: WriteRpc, calls: readonly NormalizedCall[], options: BatchCallOptions): Promise<WriteResult[]> {
139
+ const mode = options.mode ?? 'tx'
140
+ if (mode === '7702' && options.sponsor) {
141
+ const { sendSmartBatch } = await import('../smartAccount/send.js')
142
+ return sendSmartBatch(rpc, calls, { ...options, smartAccount: { ...options.smartAccount, account: 'simple7702', entryPoint: '0.8', selfBundle: options.sponsor, bundler: undefined } })
143
+ }
144
+ if (mode === '7702') return send7702(rpc, calls, options)
145
+ if (mode === '4337') {
146
+ const { sendSmartBatch } = await import('../smartAccount/send.js')
147
+ return sendSmartBatch(rpc, calls, options)
148
+ }
149
+ if (mode === '5792') {
150
+ const { send5792 } = await import('../smartAccount/eip5792.js')
151
+ return send5792(rpc, calls, options)
152
+ }
153
+ return sendEach(rpc, calls, options)
115
154
  }
116
155
 
117
156
  /** Tron 链上用 Tron 的实现(按需加载:只用 EVM 时打包不会带上) */
@@ -137,7 +176,7 @@ export async function sendEach(rpc: WriteRpc, calls: readonly NormalizedCall[],
137
176
  }
138
177
 
139
178
  /** 钱包当前的委托状态 */
140
- async function delegationOf(rpc: WriteRpc, address: string): Promise<{ delegated: boolean; to: string | null }> {
179
+ export async function delegationOf(rpc: WriteRpc, address: string): Promise<{ delegated: boolean; to: string | null }> {
141
180
  const code = (await rpc.code(address)).toLowerCase()
142
181
  if (code === '0x' || code === '') {
143
182
  return { delegated: false, to: null }
@@ -251,7 +290,7 @@ export async function send7702(rpc: WriteRpc, calls: readonly NormalizedCall[],
251
290
  }
252
291
 
253
292
  /** 解析 executeBatch 的 ExecuteError(index, error);只有一个调用时合约直接透传 revert 数据 */
254
- function failedIndex(data: string, size: number): { index: number; reason: string } | null {
293
+ export function failedIndex(data: string, size: number): { index: number; reason: string } | null {
255
294
  if (size === 1) {
256
295
  const reason = decodeRevertReason(data)
257
296
  return { index: 0, reason: reason ? `execution reverted: ${reason}` : 'execution reverted' }
@@ -6,7 +6,7 @@ import { isExecutionError } from '../errors.js'
6
6
  import { WriteRpc, failureText, l1Fee } from './rpc.js'
7
7
  import { FeeSource, applyGasMultiplier, emptyOutcome, mapPool, sendJobs, toWriteResult, type SendOptions, type TronSendOptions, type TxFees, type TxJob, type TxOutcome, type WriteResult } from './send.js'
8
8
  import { resolveSigner, type ResolvedSigner, type SignerInput } from './signer.js'
9
- import { tronModule } from './batch.js'
9
+ import { sendByMode, tronModule, type NormalizedCall } from './batch.js'
10
10
  import { batchTransfer, type TransferResult } from './transfer.js'
11
11
 
12
12
  const erc20Interface = new Interface(ERC20_ABI as unknown as string[])
@@ -31,6 +31,15 @@ export interface BatchCollectOptions extends SendOptions, TronSendOptions {
31
31
  gasBuffer?: number
32
32
  /** 各代币的最小归集数量(最小单位),余额低于它的不归集:`{ [USDT]: 10n ** 6n }` */
33
33
  minAmount?: Readonly<Record<string, BigNumberish>>
34
+ /**
35
+ * 代付 gas 归集:用这个钱包发交易(EIP-7702 + EntryPoint v0.8,见 BatchCallOptions.sponsor),子钱包不需要主币、不用补 gas,
36
+ * 每个子钱包的所有代币一个 UserOperation 原子归集;包含主币时全部主币一起转走(没有手续费要留)。
37
+ * 子钱包会被委托给 delegate(默认 Simple7702Account,委托保留,可用 revokeDelegation 撤销)
38
+ */
39
+ sponsor?: SignerInput
40
+ /** sponsor 模式:委托的合约、是否替换已有委托,见 BatchCallOptions */
41
+ delegate?: string
42
+ redelegate?: boolean
34
43
  }
35
44
 
36
45
  /** 单个钱包单个代币的归集结果;from 是子钱包,to 是归集地址 */
@@ -136,6 +145,11 @@ export async function batchCollect(provider: Provider, wallets: readonly SignerI
136
145
  }
137
146
  const active = (item: Item) => item.result.error === null
138
147
 
148
+ if (options.sponsor) {
149
+ await collectSponsored(rpc, items, target, options, balances, nativeAt, results)
150
+ return { results, topUps: [] }
151
+ }
152
+
139
153
  // 预估每个钱包的代币归集 gas,算出需要补多少主币。
140
154
  // 手续费价格只取这一次,代币归集也按它发:补 gas 等确认期间节点报价上涨,也不会超出补的数量、因余额不足被拒
141
155
  const fee = await new FeeSource(rpc, options).get()
@@ -291,6 +305,51 @@ export async function batchCollect(provider: Provider, wallets: readonly SignerI
291
305
  return { results, topUps }
292
306
  }
293
307
 
308
+ /** 代付 gas 归集:每个子钱包一批(所有代币 + 主币全额)原子转到归集地址 */
309
+ async function collectSponsored(
310
+ rpc: WriteRpc,
311
+ items: readonly Item[],
312
+ target: string,
313
+ options: BatchCollectOptions,
314
+ balances: TokenBalance[][],
315
+ nativeAt: number,
316
+ results: CollectItemResult[],
317
+ ): Promise<void> {
318
+ const list = items.filter((i) => i.result.error === null)
319
+ for (const item of list) {
320
+ if (item.native) {
321
+ // 没有手续费要留:全部主币
322
+ const balance = balances[Math.floor(item.index / options.tokens.length)]?.[nativeAt] as TokenBalance
323
+ item.amount = balance.success ? BigInt(balance.balance) : 0n
324
+ }
325
+ }
326
+ const sendable = list.filter((i) => i.amount > 0n)
327
+ list.filter((i) => i.amount === 0n).forEach((i) => (results[i.index] = toCollectResult(i, target, emptyOutcome('skipped', 'skipped: zero balance'))))
328
+ const calls: NormalizedCall[] = sendable.map((i) => ({
329
+ signer: i.signer,
330
+ to: i.native ? target : i.token,
331
+ data: i.native ? '0x' : erc20Interface.encodeFunctionData('transfer', [target, i.amount]),
332
+ value: i.native ? i.amount : 0n,
333
+ }))
334
+ const written = await sendByMode(rpc, calls, {
335
+ ...sendOptions(options),
336
+ mode: '7702',
337
+ sponsor: options.sponsor,
338
+ delegate: options.delegate,
339
+ redelegate: options.redelegate,
340
+ continueOnError: true,
341
+ onUpdate: (r) => {
342
+ const item = sendable[r.index] as Item
343
+ results[item.index] = { ...toCollectResult(item, target, emptyOutcome('skipped')), ...r, index: item.index, to: target }
344
+ options.onUpdate?.(results[item.index] as CollectItemResult)
345
+ },
346
+ })
347
+ written.forEach((r, k) => {
348
+ const item = sendable[k] as Item
349
+ results[item.index] = { ...toCollectResult(item, target, emptyOutcome('skipped')), ...r, index: item.index, to: target }
350
+ })
351
+ }
352
+
294
353
  /** 只保留发送参数(补 gas 时不带 tokens / onUpdate 等归集参数) */
295
354
  function sendOptions(options: BatchCollectOptions): SendOptions {
296
355
  const { tokens: _tokens, gasWallet: _gasWallet, gasMode: _gasMode, gasBuffer: _gasBuffer, minAmount: _minAmount, onUpdate: _onUpdate, ...rest } = options
package/src/write/send.ts CHANGED
@@ -69,6 +69,10 @@ export interface WriteResult {
69
69
  fee: string | null
70
70
  blockNumber: number | null
71
71
  error: string | null
72
+ /** 智能账户模式('4337' / sponsored '7702'):UserOperation 哈希;此时 from 是账户地址,gasUsed / fee 是 UserOperation 的 */
73
+ userOpHash?: string | null
74
+ /** 智能账户 / EIP-5792 模式:同一批的调用是否在一笔里原子执行(5792 钱包不支持而退回逐笔发送时为 false) */
75
+ atomic?: boolean
72
76
  }
73
77
 
74
78
  /** 交易层面的结果(一笔交易可能对应多个调用的结果) */
@@ -1,4 +1,4 @@
1
- import { BaseWallet, JsonRpcSigner, SigningKey, getAddress, type Authorization, type AuthorizationRequest, type TransactionRequest } from 'ethers'
1
+ import { BaseWallet, JsonRpcSigner, SigningKey, getAddress, type Authorization, type AuthorizationRequest, type TransactionRequest, type TypedDataDomain, type TypedDataField } from 'ethers'
2
2
 
3
3
  /**
4
4
  * 发交易的钱包,两种都可以:
@@ -35,6 +35,10 @@ export interface WriteSigner {
35
35
  sendTransaction?(tx: TransactionRequest): Promise<{ hash: string }>
36
36
  /** EIP-7702 授权签名(ethers Wallet 支持;'7702' 模式需要) */
37
37
  authorize?(auth: AuthorizationRequest): Promise<Authorization>
38
+ /** EIP-712 签名(Wallet / 浏览器钱包都支持):4337 v0.8 的 UserOperation、permit */
39
+ signTypedData?(domain: TypedDataDomain, types: Record<string, TypedDataField[]>, value: Record<string, unknown>): Promise<string>
40
+ /** EIP-191 个人签名:4337 v0.7 的 UserOperation */
41
+ signMessage?(message: string | Uint8Array): Promise<string>
38
42
  }
39
43
 
40
44
  /** 归一化后的钱包 */
@@ -2,7 +2,7 @@ import { Interface, getAddress, getBigInt, parseUnits, type BigNumberish } from
2
2
 
3
3
  import { ERC20_ABI, NATIVE_TOKEN, formatUnits } from '../erc20.js'
4
4
  import type { Provider } from '../provider.js'
5
- import { send7702, sendEach, tronModule, type NormalizedCall } from './batch.js'
5
+ import { sendByMode, tronModule, type BatchCallOptions, type NormalizedCall } from './batch.js'
6
6
  import { WriteRpc } from './rpc.js'
7
7
  import { emptyOutcome, sendJobs, toWriteResult, type SendOptions, type TronSendOptions, type TxJob, type TxOutcome, type WriteResult } from './send.js'
8
8
  import { resolveSigner, type ResolvedSigner, type SignerInput } from './signer.js'
@@ -34,11 +34,12 @@ export interface TransferItem {
34
34
  * - 'disperse'(默认):通过 Disperse 合约,每种代币每 batchSize 个收款人一笔交易。以下情况自动改为逐笔发送:
35
35
  * 链上没有 Disperse 合约;某种代币只有一个收款人;某一批预估 revert(收款地址拒收主币、转账收税的代币等,逐笔发送能把出问题的那一个单独标成失败)
36
36
  * - 'tx':每个收款人一笔交易
37
- * - '7702':EIP-7702 批量执行,每 batchSize 个收款人一笔(见 BatchCallOptions.mode)
37
+ * - '7702':EIP-7702 批量执行,每 batchSize 个收款人一笔(见 BatchCallOptions.mode);传 sponsor 时由 sponsor 代付 gas
38
+ * - '4337' / '5792':智能账户 UserOperation / 浏览器钱包 wallet_sendCalls,每 batchSize 个收款人一笔、原子执行(见 BatchCallOptions.mode)
38
39
  */
39
- export type TransferMode = 'disperse' | 'tx' | '7702'
40
+ export type TransferMode = 'disperse' | 'tx' | '7702' | '4337' | '5792'
40
41
 
41
- export interface BatchTransferOptions extends SendOptions, TronSendOptions {
42
+ export interface BatchTransferOptions extends SendOptions, TronSendOptions, Pick<BatchCallOptions, 'sponsor' | 'smartAccount' | 'eip1193' | 'atomicRequired' | 'capabilities'> {
42
43
  /** 默认转的代币,默认主币(NATIVE_TOKEN);每项也可以单独指定 token */
43
44
  token?: string
44
45
  mode?: TransferMode
@@ -130,9 +131,15 @@ export async function batchTransfer(provider: Provider, from: SignerInput, recip
130
131
  }
131
132
  const rpc = await WriteRpc.from(provider)
132
133
  const signer = await resolveSigner(from)
133
- const planned = await plan(provider, signer.address, recipients, options)
134
134
  const mode = options.mode ?? 'disperse'
135
- if (mode === 'tx' || mode === '7702') {
135
+ // 4337 的 SimpleAccount:资产在账户地址里,不是 owner 的地址
136
+ let holder = signer.address
137
+ if (mode === '4337' && (options.smartAccount?.account ?? 'simple') === 'simple') {
138
+ const { getSimpleAccountAddress } = await import('../smartAccount/account.js')
139
+ holder = await getSimpleAccountAddress(rpc, signer.address, options.smartAccount?.entryPoint ?? '0.7', options.smartAccount?.salt, options.smartAccount?.factory)
140
+ }
141
+ const planned = await plan(provider, holder, recipients, options)
142
+ if (mode !== 'disperse') {
136
143
  return sendDirect(rpc, signer, planned, mode, options)
137
144
  }
138
145
  return sendDisperse(rpc, provider, signer, planned, options)
@@ -203,8 +210,8 @@ function toTransferResult(p: Planned, result: WriteResult, via: TransferMode): T
203
210
  }
204
211
  }
205
212
 
206
- /** 'tx' / '7702':直接转账 */
207
- async function sendDirect(rpc: WriteRpc, signer: ResolvedSigner, planned: readonly Planned[], mode: 'tx' | '7702', options: BatchTransferOptions): Promise<TransferResult[]> {
213
+ /** 'tx' / '7702' / '4337' / '5792':直接转账 */
214
+ async function sendDirect(rpc: WriteRpc, signer: ResolvedSigner, planned: readonly Planned[], mode: Exclude<TransferMode, 'disperse'>, options: BatchTransferOptions): Promise<TransferResult[]> {
208
215
  const calls = planned.map((p) => transferCall(signer, p))
209
216
  const callOptions = {
210
217
  ...options,
@@ -212,7 +219,7 @@ async function sendDirect(rpc: WriteRpc, signer: ResolvedSigner, planned: readon
212
219
  continueOnError: true,
213
220
  onUpdate: options.onUpdate && ((r: WriteResult) => options.onUpdate?.(toTransferResult(planned[r.index] as Planned, r, mode))),
214
221
  }
215
- const results = mode === '7702' ? await send7702(rpc, calls, callOptions) : await sendEach(rpc, calls, callOptions)
222
+ const results = await sendByMode(rpc, calls, callOptions)
216
223
  return results.map((r, i) => toTransferResult(planned[i] as Planned, r, mode))
217
224
  }
218
225
 
@@ -318,7 +325,7 @@ async function sendDisperse(rpc: WriteRpc, provider: Provider, signer: ResolvedS
318
325
  if (direct.length && !options.signal?.aborted) {
319
326
  direct.sort((a, b) => a.index - b.index)
320
327
  const calls = direct.map((p) => transferCall(signer, p))
321
- await sendEach(rpc, calls, {
328
+ await sendByMode(rpc, calls, {
322
329
  ...options,
323
330
  mode: 'tx',
324
331
  continueOnError: true,