@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.
- package/CHANGELOG.md +30 -0
- package/README.md +206 -1
- package/dist/dividends.d.ts +135 -0
- package/dist/dividends.d.ts.map +1 -0
- package/dist/dividends.js +253 -0
- package/dist/dividends.js.map +1 -0
- package/dist/history.d.ts +154 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +247 -0
- package/dist/history.js.map +1 -0
- package/dist/historySources.d.ts +32 -0
- package/dist/historySources.d.ts.map +1 -0
- package/dist/historySources.js +319 -0
- package/dist/historySources.js.map +1 -0
- package/dist/liquidity/v3.d.ts +2 -0
- package/dist/liquidity/v3.d.ts.map +1 -1
- package/dist/liquidity/v3.js +1 -1
- package/dist/liquidity/v3.js.map +1 -1
- package/dist/liquidity/v4.d.ts +21 -0
- package/dist/liquidity/v4.d.ts.map +1 -1
- package/dist/liquidity/v4.js +4 -3
- package/dist/liquidity/v4.js.map +1 -1
- package/dist/lp.d.ts +8 -0
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +17 -1
- package/dist/lp.js.map +1 -1
- package/dist/lpV4.d.ts +4 -0
- package/dist/lpV4.d.ts.map +1 -1
- package/dist/lpV4.js +27 -3
- package/dist/lpV4.js.map +1 -1
- package/dist/permit.d.ts +137 -0
- package/dist/permit.d.ts.map +1 -0
- package/dist/permit.js +355 -0
- package/dist/permit.js.map +1 -0
- package/dist/portfolio.d.ts +2 -0
- package/dist/portfolio.d.ts.map +1 -1
- package/dist/portfolio.js +9 -4
- package/dist/portfolio.js.map +1 -1
- package/dist/rewards/farms.d.ts +26 -0
- package/dist/rewards/farms.d.ts.map +1 -0
- package/dist/rewards/farms.js +210 -0
- package/dist/rewards/farms.js.map +1 -0
- package/dist/rewards/index.d.ts +42 -0
- package/dist/rewards/index.d.ts.map +1 -0
- package/dist/rewards/index.js +382 -0
- package/dist/rewards/index.js.map +1 -0
- package/dist/rewards/types.d.ts +170 -0
- package/dist/rewards/types.d.ts.map +1 -0
- package/dist/rewards/types.js +2 -0
- package/dist/rewards/types.js.map +1 -0
- package/dist/simulate.d.ts +186 -0
- package/dist/simulate.d.ts.map +1 -0
- package/dist/simulate.js +559 -0
- package/dist/simulate.js.map +1 -0
- package/dist/simulateDecode.d.ts +33 -0
- package/dist/simulateDecode.d.ts.map +1 -0
- package/dist/simulateDecode.js +321 -0
- package/dist/simulateDecode.js.map +1 -0
- package/dist/smartAccount/account.d.ts +31 -0
- package/dist/smartAccount/account.d.ts.map +1 -0
- package/dist/smartAccount/account.js +33 -0
- package/dist/smartAccount/account.js.map +1 -0
- package/dist/smartAccount/clients.d.ts +77 -0
- package/dist/smartAccount/clients.d.ts.map +1 -0
- package/dist/smartAccount/clients.js +122 -0
- package/dist/smartAccount/clients.js.map +1 -0
- package/dist/smartAccount/eip5792.d.ts +77 -0
- package/dist/smartAccount/eip5792.d.ts.map +1 -0
- package/dist/smartAccount/eip5792.js +152 -0
- package/dist/smartAccount/eip5792.js.map +1 -0
- package/dist/smartAccount/send.d.ts +40 -0
- package/dist/smartAccount/send.d.ts.map +1 -0
- package/dist/smartAccount/send.js +271 -0
- package/dist/smartAccount/send.js.map +1 -0
- package/dist/smartAccount/userOp.d.ts +91 -0
- package/dist/smartAccount/userOp.d.ts.map +1 -0
- package/dist/smartAccount/userOp.js +124 -0
- package/dist/smartAccount/userOp.js.map +1 -0
- package/dist/subpaths/dividends.d.ts +28 -0
- package/dist/subpaths/dividends.d.ts.map +1 -0
- package/dist/subpaths/dividends.js +29 -0
- package/dist/subpaths/dividends.js.map +1 -0
- package/dist/subpaths/history.d.ts +22 -0
- package/dist/subpaths/history.d.ts.map +1 -0
- package/dist/subpaths/history.js +23 -0
- package/dist/subpaths/history.js.map +1 -0
- package/dist/subpaths/permit.d.ts +14 -0
- package/dist/subpaths/permit.d.ts.map +1 -0
- package/dist/subpaths/permit.js +14 -0
- package/dist/subpaths/permit.js.map +1 -0
- package/dist/subpaths/price.js +2 -2
- package/dist/subpaths/price.js.map +1 -1
- package/dist/subpaths/rewards.d.ts +27 -0
- package/dist/subpaths/rewards.d.ts.map +1 -0
- package/dist/subpaths/rewards.js +25 -0
- package/dist/subpaths/rewards.js.map +1 -0
- package/dist/subpaths/simulate.d.ts +25 -0
- package/dist/subpaths/simulate.d.ts.map +1 -0
- package/dist/subpaths/simulate.js +28 -0
- package/dist/subpaths/simulate.js.map +1 -0
- package/dist/subpaths/smart-account.d.ts +24 -0
- package/dist/subpaths/smart-account.d.ts.map +1 -0
- package/dist/subpaths/smart-account.js +24 -0
- package/dist/subpaths/smart-account.js.map +1 -0
- package/dist/swap.d.ts +45 -5
- package/dist/swap.d.ts.map +1 -1
- package/dist/swap.js +108 -12
- package/dist/swap.js.map +1 -1
- package/dist/write/batch.d.ts +34 -2
- package/dist/write/batch.d.ts.map +1 -1
- package/dist/write/batch.js +22 -3
- package/dist/write/batch.js.map +1 -1
- package/dist/write/collect.d.ts +9 -0
- package/dist/write/collect.d.ts.map +1 -1
- package/dist/write/collect.js +41 -1
- package/dist/write/collect.js.map +1 -1
- package/dist/write/send.d.ts +4 -0
- package/dist/write/send.d.ts.map +1 -1
- package/dist/write/send.js.map +1 -1
- package/dist/write/signer.d.ts +5 -1
- package/dist/write/signer.d.ts.map +1 -1
- package/dist/write/signer.js.map +1 -1
- package/dist/write/transfer.d.ts +5 -3
- package/dist/write/transfer.d.ts.map +1 -1
- package/dist/write/transfer.js +12 -6
- package/dist/write/transfer.js.map +1 -1
- package/package.json +35 -1
- package/src/dividends.ts +360 -0
- package/src/history.ts +367 -0
- package/src/historySources.ts +360 -0
- package/src/liquidity/v3.ts +1 -1
- package/src/liquidity/v4.ts +4 -3
- package/src/lp.ts +18 -1
- package/src/lpV4.ts +32 -2
- package/src/permit.ts +457 -0
- package/src/portfolio.ts +11 -4
- package/src/rewards/farms.ts +229 -0
- package/src/rewards/index.ts +431 -0
- package/src/rewards/types.ts +179 -0
- package/src/simulate.ts +727 -0
- package/src/simulateDecode.ts +352 -0
- package/src/smartAccount/account.ts +63 -0
- package/src/smartAccount/clients.ts +180 -0
- package/src/smartAccount/eip5792.ts +187 -0
- package/src/smartAccount/send.ts +341 -0
- package/src/smartAccount/userOp.ts +180 -0
- package/src/subpaths/dividends.ts +47 -0
- package/src/subpaths/history.ts +41 -0
- package/src/subpaths/permit.ts +39 -0
- package/src/subpaths/price.ts +2 -2
- package/src/subpaths/rewards.ts +46 -0
- package/src/subpaths/simulate.ts +51 -0
- package/src/subpaths/smart-account.ts +40 -0
- package/src/swap.ts +139 -18
- package/src/write/batch.ts +44 -5
- package/src/write/collect.ts +60 -1
- package/src/write/send.ts +4 -0
- package/src/write/signer.ts +5 -1
- 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
|
-
/**
|
|
79
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
379
|
-
|
|
380
|
-
const
|
|
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 (
|
|
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,
|
|
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
|
-
|
package/src/write/batch.ts
CHANGED
|
@@ -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 (
|
|
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' }
|
package/src/write/collect.ts
CHANGED
|
@@ -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
|
/** 交易层面的结果(一笔交易可能对应多个调用的结果) */
|
package/src/write/signer.ts
CHANGED
|
@@ -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
|
/** 归一化后的钱包 */
|
package/src/write/transfer.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
328
|
+
await sendByMode(rpc, calls, {
|
|
322
329
|
...options,
|
|
323
330
|
mode: 'tx',
|
|
324
331
|
continueOnError: true,
|