@w3lib/evm-batch-call 0.6.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 (248) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +367 -3
  3. package/contracts/TokenProbe.sol +103 -0
  4. package/dist/aggregate.d.ts.map +1 -1
  5. package/dist/aggregate.js +114 -3
  6. package/dist/aggregate.js.map +1 -1
  7. package/dist/approvals.d.ts +169 -0
  8. package/dist/approvals.d.ts.map +1 -0
  9. package/dist/approvals.js +310 -0
  10. package/dist/approvals.js.map +1 -0
  11. package/dist/chains.d.ts.map +1 -1
  12. package/dist/chains.js +4 -0
  13. package/dist/chains.js.map +1 -1
  14. package/dist/dividends.d.ts +135 -0
  15. package/dist/dividends.d.ts.map +1 -0
  16. package/dist/dividends.js +253 -0
  17. package/dist/dividends.js.map +1 -0
  18. package/dist/fees.d.ts +97 -0
  19. package/dist/fees.d.ts.map +1 -0
  20. package/dist/fees.js +115 -0
  21. package/dist/fees.js.map +1 -0
  22. package/dist/history.d.ts +154 -0
  23. package/dist/history.d.ts.map +1 -0
  24. package/dist/history.js +247 -0
  25. package/dist/history.js.map +1 -0
  26. package/dist/historySources.d.ts +32 -0
  27. package/dist/historySources.d.ts.map +1 -0
  28. package/dist/historySources.js +319 -0
  29. package/dist/historySources.js.map +1 -0
  30. package/dist/http.d.ts +10 -0
  31. package/dist/http.d.ts.map +1 -0
  32. package/dist/http.js +30 -0
  33. package/dist/http.js.map +1 -0
  34. package/dist/liquidity/v3.d.ts +2 -0
  35. package/dist/liquidity/v3.d.ts.map +1 -1
  36. package/dist/liquidity/v3.js +1 -1
  37. package/dist/liquidity/v3.js.map +1 -1
  38. package/dist/liquidity/v4.d.ts +21 -0
  39. package/dist/liquidity/v4.d.ts.map +1 -1
  40. package/dist/liquidity/v4.js +4 -3
  41. package/dist/liquidity/v4.js.map +1 -1
  42. package/dist/lp.d.ts +10 -1
  43. package/dist/lp.d.ts.map +1 -1
  44. package/dist/lp.js +19 -2
  45. package/dist/lp.js.map +1 -1
  46. package/dist/lpV4.d.ts +4 -0
  47. package/dist/lpV4.d.ts.map +1 -1
  48. package/dist/lpV4.js +27 -3
  49. package/dist/lpV4.js.map +1 -1
  50. package/dist/multicall.d.ts.map +1 -1
  51. package/dist/multicall.js +3 -0
  52. package/dist/multicall.js.map +1 -1
  53. package/dist/names.d.ts +55 -0
  54. package/dist/names.d.ts.map +1 -0
  55. package/dist/names.js +93 -0
  56. package/dist/names.js.map +1 -0
  57. package/dist/owner.d.ts +4 -26
  58. package/dist/owner.d.ts.map +1 -1
  59. package/dist/owner.js +7 -112
  60. package/dist/owner.js.map +1 -1
  61. package/dist/permit.d.ts +137 -0
  62. package/dist/permit.d.ts.map +1 -0
  63. package/dist/permit.js +355 -0
  64. package/dist/permit.js.map +1 -0
  65. package/dist/portfolio.d.ts +56 -0
  66. package/dist/portfolio.d.ts.map +1 -0
  67. package/dist/portfolio.js +54 -0
  68. package/dist/portfolio.js.map +1 -0
  69. package/dist/price.d.ts +95 -0
  70. package/dist/price.d.ts.map +1 -0
  71. package/dist/price.js +329 -0
  72. package/dist/price.js.map +1 -0
  73. package/dist/rewards/farms.d.ts +26 -0
  74. package/dist/rewards/farms.d.ts.map +1 -0
  75. package/dist/rewards/farms.js +210 -0
  76. package/dist/rewards/farms.js.map +1 -0
  77. package/dist/rewards/index.d.ts +42 -0
  78. package/dist/rewards/index.d.ts.map +1 -0
  79. package/dist/rewards/index.js +382 -0
  80. package/dist/rewards/index.js.map +1 -0
  81. package/dist/rewards/types.d.ts +170 -0
  82. package/dist/rewards/types.d.ts.map +1 -0
  83. package/dist/rewards/types.js +2 -0
  84. package/dist/rewards/types.js.map +1 -0
  85. package/dist/risk.d.ts +42 -0
  86. package/dist/risk.d.ts.map +1 -0
  87. package/dist/risk.js +134 -0
  88. package/dist/risk.js.map +1 -0
  89. package/dist/rpcNodes.d.ts.map +1 -1
  90. package/dist/rpcNodes.js +18 -0
  91. package/dist/rpcNodes.js.map +1 -1
  92. package/dist/simulate.d.ts +186 -0
  93. package/dist/simulate.d.ts.map +1 -0
  94. package/dist/simulate.js +559 -0
  95. package/dist/simulate.js.map +1 -0
  96. package/dist/simulateDecode.d.ts +33 -0
  97. package/dist/simulateDecode.d.ts.map +1 -0
  98. package/dist/simulateDecode.js +321 -0
  99. package/dist/simulateDecode.js.map +1 -0
  100. package/dist/smartAccount/account.d.ts +31 -0
  101. package/dist/smartAccount/account.d.ts.map +1 -0
  102. package/dist/smartAccount/account.js +33 -0
  103. package/dist/smartAccount/account.js.map +1 -0
  104. package/dist/smartAccount/clients.d.ts +77 -0
  105. package/dist/smartAccount/clients.d.ts.map +1 -0
  106. package/dist/smartAccount/clients.js +122 -0
  107. package/dist/smartAccount/clients.js.map +1 -0
  108. package/dist/smartAccount/eip5792.d.ts +77 -0
  109. package/dist/smartAccount/eip5792.d.ts.map +1 -0
  110. package/dist/smartAccount/eip5792.js +152 -0
  111. package/dist/smartAccount/eip5792.js.map +1 -0
  112. package/dist/smartAccount/send.d.ts +40 -0
  113. package/dist/smartAccount/send.d.ts.map +1 -0
  114. package/dist/smartAccount/send.js +271 -0
  115. package/dist/smartAccount/send.js.map +1 -0
  116. package/dist/smartAccount/userOp.d.ts +91 -0
  117. package/dist/smartAccount/userOp.d.ts.map +1 -0
  118. package/dist/smartAccount/userOp.js +124 -0
  119. package/dist/smartAccount/userOp.js.map +1 -0
  120. package/dist/subpaths/approvals.d.ts +26 -0
  121. package/dist/subpaths/approvals.d.ts.map +1 -0
  122. package/dist/subpaths/approvals.js +27 -0
  123. package/dist/subpaths/approvals.js.map +1 -0
  124. package/dist/subpaths/dividends.d.ts +28 -0
  125. package/dist/subpaths/dividends.d.ts.map +1 -0
  126. package/dist/subpaths/dividends.js +29 -0
  127. package/dist/subpaths/dividends.js.map +1 -0
  128. package/dist/subpaths/fees.d.ts +23 -0
  129. package/dist/subpaths/fees.d.ts.map +1 -0
  130. package/dist/subpaths/fees.js +25 -0
  131. package/dist/subpaths/fees.js.map +1 -0
  132. package/dist/subpaths/history.d.ts +22 -0
  133. package/dist/subpaths/history.d.ts.map +1 -0
  134. package/dist/subpaths/history.js +23 -0
  135. package/dist/subpaths/history.js.map +1 -0
  136. package/dist/subpaths/names.d.ts +35 -0
  137. package/dist/subpaths/names.d.ts.map +1 -0
  138. package/dist/subpaths/names.js +39 -0
  139. package/dist/subpaths/names.js.map +1 -0
  140. package/dist/subpaths/permit.d.ts +14 -0
  141. package/dist/subpaths/permit.d.ts.map +1 -0
  142. package/dist/subpaths/permit.js +14 -0
  143. package/dist/subpaths/permit.js.map +1 -0
  144. package/dist/subpaths/price.d.ts +38 -0
  145. package/dist/subpaths/price.d.ts.map +1 -0
  146. package/dist/subpaths/price.js +40 -0
  147. package/dist/subpaths/price.js.map +1 -0
  148. package/dist/subpaths/rewards.d.ts +27 -0
  149. package/dist/subpaths/rewards.d.ts.map +1 -0
  150. package/dist/subpaths/rewards.js +25 -0
  151. package/dist/subpaths/rewards.js.map +1 -0
  152. package/dist/subpaths/risk.d.ts +19 -0
  153. package/dist/subpaths/risk.d.ts.map +1 -0
  154. package/dist/subpaths/risk.js +20 -0
  155. package/dist/subpaths/risk.js.map +1 -0
  156. package/dist/subpaths/simulate.d.ts +25 -0
  157. package/dist/subpaths/simulate.d.ts.map +1 -0
  158. package/dist/subpaths/simulate.js +28 -0
  159. package/dist/subpaths/simulate.js.map +1 -0
  160. package/dist/subpaths/smart-account.d.ts +24 -0
  161. package/dist/subpaths/smart-account.d.ts.map +1 -0
  162. package/dist/subpaths/smart-account.js +24 -0
  163. package/dist/subpaths/smart-account.js.map +1 -0
  164. package/dist/subpaths/swap.d.ts +19 -0
  165. package/dist/subpaths/swap.d.ts.map +1 -0
  166. package/dist/subpaths/swap.js +20 -0
  167. package/dist/subpaths/swap.js.map +1 -0
  168. package/dist/swap.d.ts +168 -0
  169. package/dist/swap.d.ts.map +1 -0
  170. package/dist/swap.js +387 -0
  171. package/dist/swap.js.map +1 -0
  172. package/dist/tokenProbe.d.ts +3 -0
  173. package/dist/tokenProbe.d.ts.map +1 -0
  174. package/dist/tokenProbe.js +4 -0
  175. package/dist/tokenProbe.js.map +1 -0
  176. package/dist/write/batch.d.ts +34 -2
  177. package/dist/write/batch.d.ts.map +1 -1
  178. package/dist/write/batch.js +22 -3
  179. package/dist/write/batch.js.map +1 -1
  180. package/dist/write/collect.d.ts +9 -0
  181. package/dist/write/collect.d.ts.map +1 -1
  182. package/dist/write/collect.js +41 -1
  183. package/dist/write/collect.js.map +1 -1
  184. package/dist/write/send.d.ts +4 -0
  185. package/dist/write/send.d.ts.map +1 -1
  186. package/dist/write/send.js.map +1 -1
  187. package/dist/write/signer.d.ts +5 -1
  188. package/dist/write/signer.d.ts.map +1 -1
  189. package/dist/write/signer.js.map +1 -1
  190. package/dist/write/transfer.d.ts +5 -3
  191. package/dist/write/transfer.d.ts.map +1 -1
  192. package/dist/write/transfer.js +12 -6
  193. package/dist/write/transfer.js.map +1 -1
  194. package/dist/write/tron.d.ts +25 -0
  195. package/dist/write/tron.d.ts.map +1 -1
  196. package/dist/write/tron.js +26 -0
  197. package/dist/write/tron.js.map +1 -1
  198. package/package.json +73 -16
  199. package/src/aggregate.ts +110 -3
  200. package/src/approvals.ts +462 -0
  201. package/src/chains.ts +4 -0
  202. package/src/dividends.ts +360 -0
  203. package/src/fees.ts +201 -0
  204. package/src/history.ts +367 -0
  205. package/src/historySources.ts +360 -0
  206. package/src/http.ts +34 -0
  207. package/src/liquidity/v3.ts +1 -1
  208. package/src/liquidity/v4.ts +4 -3
  209. package/src/lp.ts +21 -2
  210. package/src/lpV4.ts +32 -2
  211. package/src/multicall.ts +3 -0
  212. package/src/names.ts +141 -0
  213. package/src/owner.ts +7 -138
  214. package/src/permit.ts +457 -0
  215. package/src/portfolio.ts +98 -0
  216. package/src/price.ts +415 -0
  217. package/src/rewards/farms.ts +229 -0
  218. package/src/rewards/index.ts +431 -0
  219. package/src/rewards/types.ts +179 -0
  220. package/src/risk.ts +166 -0
  221. package/src/rpcNodes.ts +18 -0
  222. package/src/simulate.ts +727 -0
  223. package/src/simulateDecode.ts +352 -0
  224. package/src/smartAccount/account.ts +63 -0
  225. package/src/smartAccount/clients.ts +180 -0
  226. package/src/smartAccount/eip5792.ts +187 -0
  227. package/src/smartAccount/send.ts +341 -0
  228. package/src/smartAccount/userOp.ts +180 -0
  229. package/src/subpaths/approvals.ts +48 -0
  230. package/src/subpaths/dividends.ts +47 -0
  231. package/src/subpaths/fees.ts +43 -0
  232. package/src/subpaths/history.ts +41 -0
  233. package/src/subpaths/names.ts +43 -0
  234. package/src/subpaths/permit.ts +39 -0
  235. package/src/subpaths/price.ts +60 -0
  236. package/src/subpaths/rewards.ts +46 -0
  237. package/src/subpaths/risk.ts +23 -0
  238. package/src/subpaths/simulate.ts +51 -0
  239. package/src/subpaths/smart-account.ts +40 -0
  240. package/src/subpaths/swap.ts +36 -0
  241. package/src/swap.ts +534 -0
  242. package/src/tokenProbe.ts +5 -0
  243. package/src/write/batch.ts +44 -5
  244. package/src/write/collect.ts +60 -1
  245. package/src/write/send.ts +4 -0
  246. package/src/write/signer.ts +5 -1
  247. package/src/write/transfer.ts +17 -10
  248. package/src/write/tron.ts +43 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 智能账户批量执行(子路径 `@w3lib/evm-batch-call/smart-account`):ERC-4337(EntryPoint v0.7 / v0.8)UserOperation 的构建 / 哈希 / 签名、
3
+ * bundler 与 ERC-7677 paymaster 客户端、自己打包(handleOps)、EIP-5792(wallet_sendCalls)。
4
+ * 通常通过 `/write` 的 `mode: '4337' | '5792'`、`mode: '7702' + sponsor` 使用,这里是底层积木。
5
+ *
6
+ * ```ts
7
+ * import { BundlerClient } from '@w3lib/evm-batch-call/smart-account'
8
+ * const bundler = new BundlerClient('https://public.pimlico.io/v2/8453/rpc')
9
+ * await bundler.supportedEntryPoints()
10
+ * ```
11
+ */
12
+ export { BundlerClient, PaymasterClient, type GasEstimate, type PaymasterContext, type PaymasterFields, type PaymasterHook, type RpcClientOptions, type UserOperationReceipt } from '../smartAccount/clients.js'
13
+ export {
14
+ DUMMY_SIGNATURE,
15
+ EIP7702_MARKER,
16
+ ENTRY_POINT_V07,
17
+ ENTRY_POINT_V08,
18
+ entryPointAddress,
19
+ getUserOpHash,
20
+ packUserOp,
21
+ userOpToRpc,
22
+ userOpTypedData,
23
+ type EntryPointVersion,
24
+ type PackedUserOperation,
25
+ type UserOperation,
26
+ } from '../smartAccount/userOp.js'
27
+ import type { Provider } from '../provider.js'
28
+ import { WriteRpc } from '../write/rpc.js'
29
+ import { resolveSimpleAccount } from '../smartAccount/account.js'
30
+ import type { SmartAccountOptions } from '../smartAccount/send.js'
31
+ export { SIMPLE_ACCOUNT_FACTORY_V07, SIMPLE_ACCOUNT_FACTORY_V08, encodeExecuteBatch, type SmartAccountKind } from '../smartAccount/account.js'
32
+ export { getCallsStatus, getWalletCapabilities, sendCalls, waitForCalls, type CallsStatus, type Eip1193Like, type SendCallsRequest, type WalletCapabilities } from '../smartAccount/eip5792.js'
33
+ export type { SmartAccountOptions }
34
+
35
+ /** owner 对应的 SimpleAccount 地址(部署前就能算出,向它转入资产后再用 `mode: '4337'` 发送) */
36
+ export async function getSimpleAccount(provider: Provider, owner: string, options: Pick<SmartAccountOptions, 'entryPoint' | 'salt' | 'factory'> = {}): Promise<{ address: string; deployed: boolean }> {
37
+ const rpc = await WriteRpc.from(provider)
38
+ const { address, deployed } = await resolveSimpleAccount(rpc, owner, options.entryPoint ?? '0.7', options.salt, options.factory)
39
+ return { address, deployed }
40
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 兑换(子路径 `@w3lib/evm-batch-call/swap`):在内置协议的 V2 / V3 池子里找最好的路线(直接或两跳),报价并构建 / 发送兑换交易。
3
+ * 不放在主入口,不用时打包不会带上这部分代码;发送的代码按需加载。
4
+ *
5
+ * ```ts
6
+ * import { getSwapQuote, swap } from '@w3lib/evm-batch-call/swap'
7
+ *
8
+ * const plan = await getSwapQuote(user, { tokenIn: NATIVE_TOKEN, tokenOut: USDT, amountInFormatted: '0.1' }, { chainId: 56 })
9
+ * await swap(provider, key, { tokenIn: NATIVE_TOKEN, tokenOut: USDT, amountInFormatted: '0.1' })
10
+ * ```
11
+ */
12
+ import { buildSwap, type SwapOptions, type SwapParams, type SwapPlan } from '../swap.js'
13
+ import { resolve, type ShortcutProviderOptions } from '../shortcuts.js'
14
+
15
+ export {
16
+ V3_SWAP_CONTRACTS,
17
+ buildSwap,
18
+ swap,
19
+ swapProtocols,
20
+ type SwapOptions,
21
+ type SwapParams,
22
+ type SwapPlan,
23
+ type SwapResult,
24
+ type SwapRoute,
25
+ type SwapSendOptions,
26
+ type SwapTx,
27
+ type SwapTxResult,
28
+ } from '../swap.js'
29
+
30
+ export interface GetSwapQuoteOptions extends SwapOptions, ShortcutProviderOptions {}
31
+
32
+ /** 报价并构建兑换的交易(同 buildSwap),节点参数与 getBalances 相同 */
33
+ export function getSwapQuote(owner: string, params: SwapParams, options: GetSwapQuoteOptions = {}): Promise<SwapPlan> {
34
+ const { provider, overrides, own } = resolve(options, ['slippage', 'deadline', 'recipient', 'approve', 'protocols', 'intermediates', 'route'])
35
+ return buildSwap(provider, owner, params, { ...own, ...(overrides.blockTag === undefined ? {} : { blockTag: overrides.blockTag }) })
36
+ }
package/src/swap.ts ADDED
@@ -0,0 +1,534 @@
1
+ /**
2
+ * 兑换(子路径 `@w3lib/evm-batch-call/swap`):在内置协议表的 V2 / V3 池子里找路线(直接兑换,或经过包装主币 / 稳定币的两跳),
3
+ * 批量报价(V2 用 Router 的 getAmountsOut,V3 用 QuoterV2,按 gas 分成几次并发的 eth_call),选输出最多的;按滑点算最少收到的数量,
4
+ * 构建授权 + 兑换的交易(V2 走 Router 的 swapExact…SupportingFeeOnTransferTokens,V3 走 SwapRouter02 / PancakeSwap SmartRouter 的 multicall)
5
+ */
6
+ import { Interface, getAddress, type BigNumberish } from 'ethers'
7
+
8
+ import { approvals, createCtx, evm, fmt, isNative, minus, out, parseAmount, same, tokenInfo, type Ctx } from './liquidity/common.js'
9
+ import type { LiquidityTxResult } from './liquidity/types.js'
10
+ import { LP_PROTOCOLS, LP_QUOTE_TOKENS, type LpProtocol, type LpToken } from './lp.js'
11
+ import type { BlockTag } from './aggregate.js'
12
+ import type { Provider } from './provider.js'
13
+ import type { SignedPermit } from './permit.js'
14
+ import type { BatchCallOptions } from './write/batch.js'
15
+ import type { SignerInput } from './write/signer.js'
16
+
17
+ /** V3 协议的报价合约(QuoterV2)和兑换合约(SwapRouter02 接口),按 协议 id → chainId。2026-10 在链上核对过(Router 的 WETH9 / deployer,并用 eth_call 实际执行过兑换) */
18
+ export const V3_SWAP_CONTRACTS: Readonly<Record<string, Readonly<Record<number, { quoter: string; router: string }>>>> = {
19
+ 'uniswap-v3': {
20
+ 1: { quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
21
+ 10: { quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
22
+ 56: { quoter: '0x78D78E420Da98ad378D7799bE8f4AF69033EB077', router: '0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2' },
23
+ 137: { quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
24
+ 8453: { quoter: '0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a', router: '0x2626664c2603336E57B271c5C0b26F421741e481' },
25
+ 42161: { quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
26
+ 43114: { quoter: '0xbe0F5544EC67e9B3b2D979aaA43f18Fd87E6257F', router: '0xbb00FF08d01D300023C629E8fFfFcb65A5a578cE' },
27
+ },
28
+ 'pancakeswap-v3': {
29
+ 1: { quoter: '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997', router: '0x13f4EA83D0bd40E75C8222255bc855a974568Dd4' },
30
+ 56: { quoter: '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997', router: '0x13f4EA83D0bd40E75C8222255bc855a974568Dd4' },
31
+ // Base 上的 SmartRouter 地址与 Ethereum / BSC 不同
32
+ 8453: { quoter: '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997', router: '0x678Aa4bF4E210cf2166753e054d5b7c31cc7fa86' },
33
+ },
34
+ }
35
+
36
+ const V3_FEES = [100, 500, 2500, 3000, 10000] as const
37
+ /** SwapRouter02 的特殊收款地址:留在 Router 里(之后 unwrapWETH9 换成主币) */
38
+ const ADDRESS_THIS = '0x0000000000000000000000000000000000000002'
39
+
40
+ const v2RouterInterface = new Interface([
41
+ 'function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[] amounts)',
42
+ 'function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
43
+ 'function swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable',
44
+ 'function swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
45
+ ])
46
+ const permitInterface = new Interface(['function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s)'])
47
+ const v3FactoryInterface = new Interface(['function getPool(address tokenA, address tokenB, uint24 fee) view returns (address)'])
48
+ const v3PoolInterface = new Interface(['function liquidity() view returns (uint128)'])
49
+ /** QuoterV2 很费 gas(池子很薄时一次可达上千万):每次 eth_call 只放几个报价,多个 eth_call 并发,避免超过节点的 gas 上限后被悄悄截掉 */
50
+ const QUOTES_PER_CALL = 3
51
+ /** 两跳时每一跳最多用几个手续费档位(按当前流动性取最大的) */
52
+ const TIERS_PER_LEG = 2
53
+ const quoterInterface = new Interface([
54
+ 'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96) params) returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',
55
+ 'function quoteExactInput(bytes path, uint256 amountIn) returns (uint256 amountOut, uint160[] sqrtPriceX96AfterList, uint32[] initializedTicksCrossedList, uint256 gasEstimate)',
56
+ ])
57
+ const swapRouterInterface = new Interface([
58
+ 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)',
59
+ 'function exactInput((bytes path, address recipient, uint256 amountIn, uint256 amountOutMinimum) params) payable returns (uint256 amountOut)',
60
+ 'function unwrapWETH9(uint256 amountMinimum, address recipient) payable',
61
+ 'function selfPermitIfNecessary(address token, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) payable',
62
+ 'function multicall(uint256 deadline, bytes[] data) payable returns (bytes[] results)',
63
+ ])
64
+
65
+ export interface SwapParams {
66
+ /** 卖出 / 买入的代币;主币用 NATIVE_TOKEN 或零地址(池子里按包装主币,交易里自动包装 / 解包) */
67
+ tokenIn: string
68
+ tokenOut: string
69
+ /** 卖出的数量:amountIn(最小单位)和 amountInFormatted(按精度换算,如 '1.5')二选一 */
70
+ amountIn?: BigNumberish
71
+ amountInFormatted?: string
72
+ }
73
+
74
+ export interface SwapOptions {
75
+ /** 价格滑点(比例,0.005 = 0.5%),最少收到 = 报价 × (1 − slippage),默认 0.005 */
76
+ slippage?: number
77
+ /** 交易的有效期(秒),默认 1200 */
78
+ deadline?: number
79
+ /** 收到代币的地址,默认 owner */
80
+ recipient?: string
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
95
+ /**
96
+ * 代币的转账税(比例,0.05 = 5%),默认 0。V2 报价(getAmountsOut)不含转账税,带税的代币
97
+ * 实际收到的比报价少,最少收到按 (1 − slippage) × (1 − tax) 算,否则会因 INSUFFICIENT_OUTPUT_AMOUNT 回滚。
98
+ * 税率可用 /risk 的 getTokenRisks 查;只对 V2 路线生效(V3 不支持带税的代币)。plan.amountOut 仍是不含税的报价
99
+ */
100
+ tax?: number
101
+ /** 只用这些协议(id,如 ['pancakeswap-v3']),默认这条链内置表里能兑换的 V2 / V3 协议 */
102
+ protocols?: readonly string[]
103
+ /** 两跳的中间代币,默认 LP_QUOTE_TOKENS 里这条链的(包装主币和稳定币);传 [] 只找直接兑换 */
104
+ intermediates?: readonly string[]
105
+ /** 按这个区块的状态报价(默认最新):同一个区块里报价可以复现 */
106
+ blockTag?: BlockTag
107
+ /** 指定路线(如之前报价结果 routes 里的某一条):只用这条路线报价和兑换,不选最好的 */
108
+ route?: Pick<SwapRoute, 'protocol' | 'path' | 'fees'>
109
+ }
110
+
111
+ export interface SwapRoute {
112
+ protocol: string
113
+ protocolName: string
114
+ version: 'v2' | 'v3'
115
+ /** 经过的代币(池子里的地址,主币是包装主币) */
116
+ path: string[]
117
+ /** V3:每一跳池子的手续费档位;V2 为空 */
118
+ fees: number[]
119
+ /** 这条路线的报价(最小单位) */
120
+ amountOut: string
121
+ }
122
+
123
+ export interface SwapTx {
124
+ /** approve:卖出的代币授权给 Router;permit:提交 EIP-2612 签名(V2 路线批量发送时);swap:兑换 */
125
+ kind: 'approve' | 'permit' | 'swap'
126
+ to: string
127
+ data: string
128
+ /** 附带的主币(最小单位):卖出主币时为卖出的数量 */
129
+ value: string
130
+ token?: string
131
+ spender?: string
132
+ amount?: string
133
+ }
134
+
135
+ export interface SwapPlan {
136
+ tokenIn: LpToken
137
+ tokenOut: LpToken
138
+ amountIn: string
139
+ amountInFormatted: string | null
140
+ /** 报价(最小单位)和最少收到的数量(按滑点) */
141
+ amountOut: string
142
+ amountOutFormatted: string | null
143
+ amountOutMin: string
144
+ amountOutMinFormatted: string | null
145
+ /** 1 个 tokenIn 换多少 tokenOut(按这笔的数量) */
146
+ price: number | null
147
+ /** 价格影响(0~1):这笔的价格比极小数量的价格差多少;算不出时为 null */
148
+ priceImpact: number | null
149
+ route: SwapRoute
150
+ /** 其他候选路线的报价,从高到低(含选中的) */
151
+ routes: SwapRoute[]
152
+ /** 按顺序发送的交易(授权在前) */
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 }
167
+ }
168
+
169
+ interface Candidate {
170
+ protocol: LpProtocol
171
+ version: 'v2' | 'v3'
172
+ path: string[]
173
+ fees: number[]
174
+ /** V2:Router;V3:报价 / 兑换合约 */
175
+ router: string
176
+ quoter?: string
177
+ }
178
+
179
+ /** 这条链上能兑换的协议:V2 要有 Router,V3 要有报价和兑换合约(V3_SWAP_CONTRACTS) */
180
+ export function swapProtocols(chainId: number): LpProtocol[] {
181
+ return LP_PROTOCOLS.filter((p) => p.chainId === chainId && ((p.version === 'v2' && !!p.router) || (p.version === 'v3' && !!V3_SWAP_CONTRACTS[p.id]?.[chainId])))
182
+ }
183
+
184
+ /** V3 的路径编码:token ‖ fee(uint24) ‖ token ‖ … */
185
+ function encodePath(path: readonly string[], fees: readonly number[]): string {
186
+ let hex = path[0]?.slice(2).toLowerCase() ?? ''
187
+ fees.forEach((fee, i) => {
188
+ hex += fee.toString(16).padStart(6, '0') + (path[i + 1] as string).slice(2).toLowerCase()
189
+ })
190
+ return `0x${hex}`
191
+ }
192
+
193
+ /** 列出候选路线(V3 先查哪些池子存在)并一次 multicall 报价,按输出从高到低 */
194
+ async function quoteRoutes(ctx: Ctx, tokenIn: string, tokenOut: string, amountIn: bigint, options: SwapOptions): Promise<Array<Candidate & { amountOut: bigint }>> {
195
+ const all = swapProtocols(ctx.chainId)
196
+ const protocols = options.protocols ? all.filter((p) => options.protocols?.includes(p.id)) : all
197
+ if (!protocols.length) throw new Error(`No swap protocol on chain ${ctx.chainId}${options.protocols ? ` among ${options.protocols.join(', ')}` : ''}`)
198
+ const mids = (options.intermediates ?? LP_QUOTE_TOKENS[ctx.chainId] ?? []).map(evm).filter((m) => !same(m, tokenIn) && !same(m, tokenOut))
199
+
200
+ // V3:先查池子是否存在(每一跳的每个手续费档位)
201
+ const v3 = protocols.filter((p) => p.version === 'v3')
202
+ const legs: Array<{ p: LpProtocol; a: string; b: string; fee: number }> = []
203
+ for (const p of v3) {
204
+ for (const fee of V3_FEES) {
205
+ legs.push({ p, a: tokenIn, b: tokenOut, fee })
206
+ for (const m of mids) legs.push({ p, a: tokenIn, b: m, fee }, { p, a: m, b: tokenOut, fee })
207
+ }
208
+ }
209
+ const overrides = options.blockTag === undefined ? undefined : { blockTag: options.blockTag }
210
+ const pools = legs.length ? await ctx.provider.tryAll<string>(legs.map((l) => ctx.provider.contract(l.p.factory as string, v3FactoryInterface).getPool(l.a, l.b, l.fee)), overrides) : []
211
+ // 再读存在的池子的当前流动性:没有流动性的不报价;两跳的每一跳只留流动性最大的几个档位(同一对代币的流动性可以直接比)
212
+ const found = legs.flatMap((l, i) => (pools[i] && BigInt(pools[i] as string) !== 0n ? [{ ...l, pool: pools[i] as string }] : []))
213
+ const liquidity = found.length ? await ctx.provider.tryAll<bigint>(found.map((l) => ctx.provider.contract(l.pool, v3PoolInterface).liquidity()), overrides) : []
214
+ const live = found.map((l, i) => ({ ...l, liquidity: liquidity[i] ?? 0n })).filter((l) => l.liquidity > 0n)
215
+ const legKey = (l: { p: LpProtocol; a: string; b: string }) => `${l.p.id}:${l.a}:${l.b}`
216
+ const byLeg = new Map<string, typeof live>()
217
+ for (const l of live) byLeg.set(legKey(l), [...(byLeg.get(legKey(l)) ?? []), l])
218
+ const exists = new Set<string>()
219
+ for (const [key, list] of byLeg) {
220
+ const direct = key.endsWith(`:${tokenIn}:${tokenOut}`)
221
+ const kept = direct ? list : [...list].sort((x, y) => (x.liquidity < y.liquidity ? 1 : x.liquidity > y.liquidity ? -1 : 0)).slice(0, TIERS_PER_LEG)
222
+ for (const l of kept) exists.add(`${l.p.id}:${l.a}:${l.b}:${l.fee}`)
223
+ }
224
+ const has = (p: LpProtocol, a: string, b: string, fee: number) => exists.has(`${p.id}:${a}:${b}:${fee}`)
225
+
226
+ const candidates: Candidate[] = []
227
+ for (const p of protocols) {
228
+ if (p.version === 'v2') {
229
+ const router = p.router as string
230
+ candidates.push({ protocol: p, version: 'v2', path: [tokenIn, tokenOut], fees: [], router })
231
+ for (const m of mids) candidates.push({ protocol: p, version: 'v2', path: [tokenIn, m, tokenOut], fees: [], router })
232
+ continue
233
+ }
234
+ const c = V3_SWAP_CONTRACTS[p.id]?.[ctx.chainId] as { quoter: string; router: string }
235
+ for (const fee of V3_FEES) {
236
+ if (has(p, tokenIn, tokenOut, fee)) candidates.push({ protocol: p, version: 'v3', path: [tokenIn, tokenOut], fees: [fee], router: c.router, quoter: c.quoter })
237
+ }
238
+ for (const m of mids) {
239
+ for (const f1 of V3_FEES) {
240
+ if (!has(p, tokenIn, m, f1)) continue
241
+ for (const f2 of V3_FEES) {
242
+ if (has(p, m, tokenOut, f2)) candidates.push({ protocol: p, version: 'v3', path: [tokenIn, m, tokenOut], fees: [f1, f2], router: c.router, quoter: c.quoter })
243
+ }
244
+ }
245
+ }
246
+ }
247
+ // V2 的 getAmountsOut 很便宜,一次报完;V3 每 QUOTES_PER_CALL 个一组并发
248
+ const v2Index = candidates.flatMap((c, i) => (c.version === 'v2' ? [i] : []))
249
+ const v3Index = candidates.flatMap((c, i) => (c.version === 'v3' ? [i] : []))
250
+ const groups = [v2Index, ...Array.from({ length: Math.ceil(v3Index.length / QUOTES_PER_CALL) }, (_, k) => v3Index.slice(k * QUOTES_PER_CALL, (k + 1) * QUOTES_PER_CALL))].filter((g) => g.length)
251
+ const quotes: unknown[] = new Array(candidates.length).fill(null)
252
+ await Promise.all(
253
+ groups.map(async (group) => {
254
+ const results = await ctx.provider.tryAll(
255
+ group.map((i) => quoteCall(ctx, candidates[i] as Candidate, amountIn)),
256
+ overrides,
257
+ )
258
+ group.forEach((i, k) => (quotes[i] = results[k]))
259
+ }),
260
+ )
261
+ return candidates
262
+ .map((c, i) => ({ ...c, amountOut: amountOf(c, quotes[i]) }))
263
+ .filter((c) => c.amountOut > 0n)
264
+ .sort((a, b) => (a.amountOut < b.amountOut ? 1 : a.amountOut > b.amountOut ? -1 : 0))
265
+ }
266
+
267
+ function quoteCall(ctx: Ctx, c: Candidate, amountIn: bigint) {
268
+ if (c.version === 'v2') return ctx.provider.contract(c.router, v2RouterInterface).getAmountsOut(amountIn, c.path)
269
+ const quoter = ctx.provider.contract(c.quoter as string, quoterInterface)
270
+ return c.path.length === 2
271
+ ? quoter.quoteExactInputSingle({ tokenIn: c.path[0], tokenOut: c.path[1], amountIn, fee: c.fees[0], sqrtPriceLimitX96: 0n })
272
+ : quoter.quoteExactInput(encodePath(c.path, c.fees), amountIn)
273
+ }
274
+
275
+ function amountOf(c: Candidate, result: unknown): bigint {
276
+ if (result === null || result === undefined) return 0n
277
+ if (c.version === 'v2') {
278
+ const amounts = result as readonly bigint[]
279
+ return amounts.length === c.path.length ? (amounts[amounts.length - 1] as bigint) : 0n
280
+ }
281
+ return (result as { 0: bigint })[0] ?? 0n
282
+ }
283
+
284
+ /**
285
+ * 报价并构建兑换的交易(不签名):选输出最多的路线,最少收到 = 报价 × (1 − slippage);卖出代币时额度不够先授权给 Router。
286
+ *
287
+ * ```ts
288
+ * const plan = await buildSwap(provider, user, { tokenIn: NATIVE_TOKEN, tokenOut: USDT, amountInFormatted: '0.1' })
289
+ * plan.amountOutFormatted // '61.2'
290
+ * plan.route // { protocol: 'pancakeswap-v3', path: [WBNB, USDT], fees: [100], … }
291
+ * ```
292
+ */
293
+ export async function buildSwap(provider: Provider, owner: string, params: SwapParams, options: SwapOptions = {}): Promise<SwapPlan> {
294
+ const { slippage, deadline, recipient, approve } = options
295
+ const ctx = await createCtx(provider, owner, { slippage, deadline, recipient, approve: approve === 'max' ? 'max' : 'exact' })
296
+ const wrapped = LP_QUOTE_TOKENS[ctx.chainId]?.[0]
297
+ const nativeIn = isNative(ctx, params.tokenIn)
298
+ const nativeOut = isNative(ctx, params.tokenOut)
299
+ if ((nativeIn || nativeOut) && !wrapped) throw new Error(`No wrapped native token known on chain ${ctx.chainId}`)
300
+ const tokenIn = nativeIn ? evm(wrapped as string) : evm(params.tokenIn)
301
+ const tokenOut = nativeOut ? evm(wrapped as string) : evm(params.tokenOut)
302
+ if (same(tokenIn, tokenOut)) throw new Error(nativeIn || nativeOut ? 'Wrapping / unwrapping the native token is not a swap; use the wrapped token contract' : 'tokenIn and tokenOut are the same')
303
+ const [infoIn, infoOut] = await tokenInfo(ctx, [nativeIn ? params.tokenIn : tokenIn, nativeOut ? params.tokenOut : tokenOut])
304
+ const tIn = infoIn as LpToken
305
+ const tOut = infoOut as LpToken
306
+ const amountIn = parseAmount(params.amountIn, params.amountInFormatted, tIn, 'amountIn')
307
+ if (amountIn === undefined || amountIn <= 0n) throw new Error('amountIn must be greater than 0')
308
+
309
+ const all = await quoteRoutes(ctx, tokenIn, tokenOut, amountIn, options)
310
+ const wanted = options.route
311
+ const routes = wanted
312
+ ? all.filter((c) => c.protocol.id === wanted.protocol && c.path.length === wanted.path.length && c.path.every((a, i) => same(a, wanted.path[i] as string)) && c.fees.join() === wanted.fees.join())
313
+ : all
314
+ const best = routes[0]
315
+ if (!best) throw new Error(wanted ? 'The given route has no quote (pool missing or no liquidity)' : 'No route found: no pool for this pair (directly or through the intermediate tokens) in the built-in protocols')
316
+ // 价格影响:同一路线卖出极小的数量(1 / 10000)的价格
317
+ const small = amountIn / 10_000n
318
+ const smallOut = small > 0n ? amountOf(best, await provider.tryAll([quoteCall(ctx, best, small)], options.blockTag === undefined ? undefined : { blockTag: options.blockTag }).then((r) => r[0])) : 0n
319
+ // 参考交易的输出太小时取整误差太大(几个最小单位就差好几个百分点),算不准,返回 null
320
+ const priceImpact = smallOut >= 10_000n ? Math.max(0, 1 - Number(best.amountOut) / Number(amountIn) / (Number(smallOut) / Number(small))) : null
321
+ const tax = options.tax ?? 0
322
+ if (!(tax >= 0 && tax < 1)) throw new RangeError(`tax must be in [0, 1): ${tax}`)
323
+ const keep = best.version === 'v2' ? 1 - tax : 1
324
+ const amountOutMin = minus((best.amountOut * BigInt(Math.round(keep * 1_000_000))) / 1_000_000n, ctx.slippage)
325
+
326
+ // approvals 只会返回 kind 为 'approve' 的交易
327
+ // Router 在 Tron 上是 T 地址:授权的 ABI 参数要用 0x 格式
328
+ const transactions = (nativeIn ? [] : await approvals(ctx, [{ token: tokenIn, spender: evm(best.router), amount: amountIn }])) as SwapTx[]
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))
339
+
340
+ const route = (c: Candidate & { amountOut: bigint }): SwapRoute => ({
341
+ protocol: c.protocol.id,
342
+ protocolName: c.protocol.name,
343
+ version: c.version,
344
+ path: c.path.map((a) => out(ctx, a)),
345
+ fees: c.fees,
346
+ amountOut: c.amountOut.toString(),
347
+ })
348
+ const decimals = (t: LpToken) => t.decimals ?? 0
349
+ return {
350
+ tokenIn: tIn,
351
+ tokenOut: tOut,
352
+ amountIn: amountIn.toString(),
353
+ amountInFormatted: fmt(amountIn, tIn),
354
+ amountOut: best.amountOut.toString(),
355
+ amountOutFormatted: fmt(best.amountOut, tOut),
356
+ amountOutMin: amountOutMin.toString(),
357
+ amountOutMinFormatted: fmt(amountOutMin, tOut),
358
+ price: tIn.decimals === null || tOut.decimals === null ? null : Number(best.amountOut) / 10 ** decimals(tOut) / (Number(amountIn) / 10 ** decimals(tIn)),
359
+ priceImpact,
360
+ route: route(best),
361
+ routes: routes.map(route),
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])
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
403
+ }
404
+
405
+ function swapTx(ctx: Ctx, c: Candidate, amountIn: bigint, amountOutMin: bigint, nativeIn: boolean, nativeOut: boolean, selfPermit?: string): SwapTx {
406
+ const value = nativeIn ? amountIn.toString() : '0'
407
+ if (c.version === 'v2') {
408
+ const data = nativeIn
409
+ ? v2RouterInterface.encodeFunctionData('swapExactETHForTokensSupportingFeeOnTransferTokens', [amountOutMin, c.path, ctx.recipient, ctx.deadline])
410
+ : nativeOut
411
+ ? v2RouterInterface.encodeFunctionData('swapExactTokensForETHSupportingFeeOnTransferTokens', [amountIn, amountOutMin, c.path, ctx.recipient, ctx.deadline])
412
+ : v2RouterInterface.encodeFunctionData('swapExactTokensForTokensSupportingFeeOnTransferTokens', [amountIn, amountOutMin, c.path, ctx.recipient, ctx.deadline])
413
+ return { kind: 'swap', to: out(ctx, c.router), data, value }
414
+ }
415
+ // V3:卖出主币时附带 value(Router 自动包装);买入主币时先收到 Router,再 unwrapWETH9 给收款地址
416
+ const recipient = nativeOut ? ADDRESS_THIS : ctx.recipient
417
+ const swap =
418
+ c.path.length === 2
419
+ ? swapRouterInterface.encodeFunctionData('exactInputSingle', [
420
+ { tokenIn: c.path[0], tokenOut: c.path[1], fee: c.fees[0], recipient, amountIn, amountOutMinimum: amountOutMin, sqrtPriceLimitX96: 0n },
421
+ ])
422
+ : swapRouterInterface.encodeFunctionData('exactInput', [{ path: encodePath(c.path, c.fees), recipient, amountIn, amountOutMinimum: amountOutMin }])
423
+ const calls = [...(selfPermit ? [selfPermit] : []), swap, ...(nativeOut ? [swapRouterInterface.encodeFunctionData('unwrapWETH9', [amountOutMin, ctx.recipient])] : [])]
424
+ return { kind: 'swap', to: out(ctx, c.router), data: swapRouterInterface.encodeFunctionData('multicall', [ctx.deadline, calls]), value }
425
+ }
426
+
427
+ export type SwapTxResult = Omit<LiquidityTxResult, 'kind'> & { kind: SwapTx['kind'] }
428
+
429
+ export interface SwapResult {
430
+ plan: SwapPlan
431
+ /** 每笔交易的结果(授权 / permit、兑换) */
432
+ results: SwapTxResult[]
433
+ /** 兑换那一笔的状态和哈希 */
434
+ status: LiquidityTxResult['status']
435
+ hash: string | null
436
+ /** 请求 permit / permit2 / auto 时:实际采用的授权方式(同 plan.approval,退回普通授权时有原因) */
437
+ approval?: SwapApproval
438
+ }
439
+
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'
446
+ onUpdate?: (result: SwapTxResult) => void
447
+ }
448
+
449
+ /**
450
+ * 兑换:构建(同 buildSwap)后用 from 签名发送(钱包同 /write:私钥、ethers Signer、浏览器钱包、Tron 钱包)。
451
+ * 授权失败时不发兑换的交易
452
+ *
453
+ * ```ts
454
+ * const r = await swap(provider, key, { tokenIn: USDT, tokenOut: NATIVE_TOKEN, amountInFormatted: '100' }, { slippage: 0.01 })
455
+ * r.status // 'confirmed'
456
+ *
457
+ * // 用 EIP-2612 签名代替授权交易(V3 路线一笔交易完成;不支持时退回普通授权,r.approval.reason 说明原因)
458
+ * await swap(provider, signer, params, { approve: 'auto' })
459
+ * ```
460
+ */
461
+ export async function swap(provider: Provider, from: SignerInput, params: SwapParams, options: SwapSendOptions = {}): Promise<SwapResult> {
462
+ const [{ batchCall, tronModule }, { resolveSigner }, { emptyOutcome, toWriteResult }] = await Promise.all([import('./write/batch.js'), import('./write/signer.js'), import('./write/send.js')])
463
+ const tron = await tronModule(provider)
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[]> =>
498
+ (
499
+ await batchCall(
500
+ provider,
501
+ txs.map((tx) => ({ from, to: tx.to, data: tx.data, value: BigInt(tx.value) })),
502
+ {
503
+ ...options,
504
+ mode: asMode === 'atomic' ? mode : 'tx',
505
+ continueOnError: false,
506
+ onUpdate: options.onUpdate ? (r) => options.onUpdate?.({ ...r, kind: (txs[r.index] as SwapTx).kind }) : undefined,
507
+ },
508
+ )
509
+ ).map((r, i) => ({ ...r, kind: (txs[i] as SwapTx).kind }))
510
+ const txs = plan.transactions
511
+ let results: SwapTxResult[]
512
+ if (atomic) {
513
+ // 授权 / permit 和兑换在同一笔原子批量里:要么都成功,要么都不生效
514
+ results = await send(txs, 'atomic')
515
+ } else if (txs.length === 1) {
516
+ results = await send(txs)
517
+ } else {
518
+ // 有授权时先发授权:确认后重新报价再发兑换,deadline 和最少收到从发兑换那一刻算,
519
+ // 不会因为授权等得久而过期或按旧价成交
520
+ const approved = await send(txs.slice(0, -1))
521
+ const last = txs[txs.length - 1] as SwapTx
522
+ const approval = plan.approval
523
+ if (approved.every((r) => r.status === 'confirmed')) {
524
+ plan = await buildSwap(provider, holder, params, { ...options, approve: options.approve === 'max' ? 'max' : 'exact' })
525
+ if (approval) plan.approval = approval // 重新报价时额度已够:保留最初的选择和原因
526
+ const fresh = plan.transactions[plan.transactions.length - 1] as SwapTx
527
+ results = [...approved, ...(await send([fresh])).map((r) => ({ ...r, index: txs.length - 1 }))]
528
+ } else {
529
+ results = [...approved, { ...toWriteResult(txs.length - 1, owner, last.to, emptyOutcome('skipped', 'skipped: the approval did not succeed')), kind: last.kind }]
530
+ }
531
+ }
532
+ const main = results[results.length - 1] as SwapTxResult
533
+ return { plan, results, status: main.status, hash: main.hash, ...(plan.approval ? { approval: plan.approval } : {}) }
534
+ }
@@ -0,0 +1,5 @@
1
+ // 由 scripts/build-contracts.mjs 生成,不要手改。源码:contracts/TokenProbe.sol(solc 0.8.30+commit.73712a01.Emscripten.clang)
2
+
3
+ /** constructor(address router, address token, address pair) payable — 返回 (stage, buyQuote, bought, transferSent, transferReceived, sellAmount, sellQuote, sold, buyGas, sellGas, pairReceived) */
4
+ export const TOKEN_PROBE_BYTECODE =
5
+ '0x6080604052604051610ba0380380610ba0833981016040819052610022916108be565b61007f60405180610160016040528060008152602001600081526020016000815260200160008152602001600081526020016000815260200160008152602001600081526020016000815260200160008152602001600081525090565b6000846001600160a01b031663ad5c46486040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100bf573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e39190610901565b6040805160028082526060820183529293506000929091602083019080368337019050509050818160008151811061011d5761011d610939565b60200260200101906001600160a01b031690816001600160a01b031681525050848160018151811061015157610151610939565b6001600160a01b03928316602091820292909201015260405163d06ca61f60e01b81529087169063d06ca61f9061018e9034908590600401610994565b600060405180830381865afa9250505080156101cc57506040513d6000823e601f3d908101601f191682016040526101c991908101906109b5565b60015b156101f757806001815181106101e4576101e4610939565b6020026020010151846020018181525050505b60005a9050866001600160a01b031663b6f9de953460008530426040518663ffffffff1660e01b81526004016102309493929190610a81565b6000604051808303818588803b15801561024957600080fd5b505af19350505050801561025b575060015b61026857600184526102e7565b5a6102739082610ab6565b6101008501526040516370a0823160e01b81523060048201526001600160a01b038716906370a0823190602401602060405180830381865afa1580156102bd573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906102e19190610ad7565b60408501525b83511580156102fb5750600a846040015110155b1561042d5760408051306020808301919091526001600160a01b038916828401528251808303840181526060909201835281519101209085015161034190600a90610af0565b606086018190526040516001600160a01b038316602482015260448101919091526103ad90889063a9059cbb60e01b906064015b60408051808303601f190181529190526020810180516001600160e01b0319939093166001600160e01b039384161790529061080116565b15610426576040516370a0823160e01b81526001600160a01b0382811660048301528816906370a0823190602401602060405180830381865afa1580156103f8573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061041c9190610ad7565b608086015261042b565b600285525b505b83516000036104dd576040516370a0823160e01b81523060048201526001600160a01b038716906370a0823190602401602060405180830381865afa15801561047a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061049e9190610ad7565b60a08501526040516001600160a01b038816602482015260001960448201526104d490879063095ea7b360e01b90606401610375565b6104dd57600384525b835160000361075d5785826000815181106104fa576104fa610939565b60200260200101906001600160a01b031690816001600160a01b031681525050828260018151811061052e5761052e610939565b6001600160a01b03928316602091820292909201015260a085015160405163d06ca61f60e01b81529189169163d06ca61f9161056e918690600401610994565b600060405180830381865afa9250505080156105ac57506040513d6000823e601f3d908101601f191682016040526105a991908101906109b5565b60015b156105d757806001815181106105c4576105c4610939565b60200260200101518560c0018181525050505b6040516370a0823160e01b81526001600160a01b03868116600483015247916000918916906370a0823190602401602060405180830381865afa158015610622573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906106469190610ad7565b90505a60a087015160405163791ac94760e01b81529194506001600160a01b038b169163791ac9479161068491600090899030904290600401610b12565b600060405180830381600087803b15801561069e57600080fd5b505af19250505080156106af575060015b6106bc576004865261075a565b5a6106c79084610ab6565b6101208701526106d78247610ab6565b60e08701526040516370a0823160e01b81526001600160a01b0388811660048301528291908a16906370a0823190602401602060405180830381865afa158015610725573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906107499190610ad7565b6107539190610ab6565b6101408701525b50505b6000846040516020016107e89190600061016082019050825182526020830151602083015260408301516040830152606083015160608301526080830151608083015260a083015160a083015260c083015160c083015260e083015160e083015261010083015161010083015261012083015161012083015261014083015161014083015292915050565b6040516020818303038152906040529050805160208201f35b6000806000846001600160a01b03168460405161081e9190610b4e565b6000604051808303816000865af19150503d806000811461085b576040519150601f19603f3d011682016040523d82523d6000602084013e610860565b606091505b5091509150818015610897575080511580610897575060208151101580156108975750808060200190518101906108979190610b7d565b925050505b92915050565b80516001600160a01b03811681146108b957600080fd5b919050565b6000806000606084860312156108d357600080fd5b6108dc846108a2565b92506108ea602085016108a2565b91506108f8604085016108a2565b90509250925092565b60006020828403121561091357600080fd5b61091c826108a2565b9392505050565b634e487b7160e01b600052604160045260246000fd5b634e487b7160e01b600052603260045260246000fd5b600081518084526020840193506020830160005b8281101561098a5781516001600160a01b0316865260209586019590910190600101610963565b5093949350505050565b8281526040602082015260006109ad604083018461094f565b949350505050565b6000602082840312156109c757600080fd5b81516001600160401b038111156109dd57600080fd5b8201601f810184136109ee57600080fd5b80516001600160401b03811115610a0757610a07610923565b604051600582901b90603f8201601f191681016001600160401b0381118282101715610a3557610a35610923565b604052918252602081840181019290810187841115610a5357600080fd5b6020850194505b83851015610a7657845180825260209586019590935001610a5a565b509695505050505050565b848152608060208201526000610a9a608083018661094f565b6001600160a01b03949094166040830152506060015292915050565b8181038181111561089c57634e487b7160e01b600052601160045260246000fd5b600060208284031215610ae957600080fd5b5051919050565b600082610b0d57634e487b7160e01b600052601260045260246000fd5b500490565b85815284602082015260a060408201526000610b3160a083018661094f565b6001600160a01b0394909416606083015250608001529392505050565b6000825160005b81811015610b6f5760208186018101518583015201610b55565b506000920191825250919050565b600060208284031215610b8f57600080fd5b8151801515811461091c57600080fdfe'