@w3lib/evm-batch-call 0.6.0 → 0.7.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 +24 -0
- package/README.md +161 -2
- package/contracts/TokenProbe.sol +103 -0
- package/dist/aggregate.d.ts.map +1 -1
- package/dist/aggregate.js +114 -3
- package/dist/aggregate.js.map +1 -1
- package/dist/approvals.d.ts +169 -0
- package/dist/approvals.d.ts.map +1 -0
- package/dist/approvals.js +310 -0
- package/dist/approvals.js.map +1 -0
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +4 -0
- package/dist/chains.js.map +1 -1
- package/dist/fees.d.ts +97 -0
- package/dist/fees.d.ts.map +1 -0
- package/dist/fees.js +115 -0
- package/dist/fees.js.map +1 -0
- package/dist/http.d.ts +10 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +30 -0
- package/dist/http.js.map +1 -0
- package/dist/lp.d.ts +2 -1
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +2 -1
- package/dist/lp.js.map +1 -1
- package/dist/multicall.d.ts.map +1 -1
- package/dist/multicall.js +3 -0
- package/dist/multicall.js.map +1 -1
- package/dist/names.d.ts +55 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +93 -0
- package/dist/names.js.map +1 -0
- package/dist/owner.d.ts +4 -26
- package/dist/owner.d.ts.map +1 -1
- package/dist/owner.js +7 -112
- package/dist/owner.js.map +1 -1
- package/dist/portfolio.d.ts +54 -0
- package/dist/portfolio.d.ts.map +1 -0
- package/dist/portfolio.js +49 -0
- package/dist/portfolio.js.map +1 -0
- package/dist/price.d.ts +95 -0
- package/dist/price.d.ts.map +1 -0
- package/dist/price.js +329 -0
- package/dist/price.js.map +1 -0
- package/dist/risk.d.ts +42 -0
- package/dist/risk.d.ts.map +1 -0
- package/dist/risk.js +134 -0
- package/dist/risk.js.map +1 -0
- package/dist/rpcNodes.d.ts.map +1 -1
- package/dist/rpcNodes.js +18 -0
- package/dist/rpcNodes.js.map +1 -1
- package/dist/subpaths/approvals.d.ts +26 -0
- package/dist/subpaths/approvals.d.ts.map +1 -0
- package/dist/subpaths/approvals.js +27 -0
- package/dist/subpaths/approvals.js.map +1 -0
- package/dist/subpaths/fees.d.ts +23 -0
- package/dist/subpaths/fees.d.ts.map +1 -0
- package/dist/subpaths/fees.js +25 -0
- package/dist/subpaths/fees.js.map +1 -0
- package/dist/subpaths/names.d.ts +35 -0
- package/dist/subpaths/names.d.ts.map +1 -0
- package/dist/subpaths/names.js +39 -0
- package/dist/subpaths/names.js.map +1 -0
- package/dist/subpaths/price.d.ts +38 -0
- package/dist/subpaths/price.d.ts.map +1 -0
- package/dist/subpaths/price.js +40 -0
- package/dist/subpaths/price.js.map +1 -0
- package/dist/subpaths/risk.d.ts +19 -0
- package/dist/subpaths/risk.d.ts.map +1 -0
- package/dist/subpaths/risk.js +20 -0
- package/dist/subpaths/risk.js.map +1 -0
- package/dist/subpaths/swap.d.ts +19 -0
- package/dist/subpaths/swap.d.ts.map +1 -0
- package/dist/subpaths/swap.js +20 -0
- package/dist/subpaths/swap.js.map +1 -0
- package/dist/swap.d.ts +128 -0
- package/dist/swap.d.ts.map +1 -0
- package/dist/swap.js +291 -0
- package/dist/swap.js.map +1 -0
- package/dist/tokenProbe.d.ts +3 -0
- package/dist/tokenProbe.d.ts.map +1 -0
- package/dist/tokenProbe.js +4 -0
- package/dist/tokenProbe.js.map +1 -0
- package/dist/write/tron.d.ts +25 -0
- package/dist/write/tron.d.ts.map +1 -1
- package/dist/write/tron.js +26 -0
- package/dist/write/tron.js.map +1 -1
- package/package.json +39 -16
- package/src/aggregate.ts +110 -3
- package/src/approvals.ts +462 -0
- package/src/chains.ts +4 -0
- package/src/fees.ts +201 -0
- package/src/http.ts +34 -0
- package/src/lp.ts +3 -1
- package/src/multicall.ts +3 -0
- package/src/names.ts +141 -0
- package/src/owner.ts +7 -138
- package/src/portfolio.ts +91 -0
- package/src/price.ts +415 -0
- package/src/risk.ts +166 -0
- package/src/rpcNodes.ts +18 -0
- package/src/subpaths/approvals.ts +48 -0
- package/src/subpaths/fees.ts +43 -0
- package/src/subpaths/names.ts +43 -0
- package/src/subpaths/price.ts +60 -0
- package/src/subpaths/risk.ts +23 -0
- package/src/subpaths/swap.ts +36 -0
- package/src/swap.ts +413 -0
- package/src/tokenProbe.ts +5 -0
- package/src/write/tron.ts +43 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 手续费估算(子路径 `@w3lib/evm-batch-call/fees`):EIP-1559 的三档费用建议、一笔交易的手续费(含 L1 数据费),
|
|
3
|
+
* Tron 按能量 / 带宽和账户剩余的资源算要烧的 TRX。不放在主入口,不用时打包不会带上这部分代码。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { getCostEstimate, getFeeEstimate } from '@w3lib/evm-batch-call/fees'
|
|
7
|
+
*
|
|
8
|
+
* await getFeeEstimate({ chainId: 1 })
|
|
9
|
+
* await getCostEstimate({ from: user, to: USDT, data }, { chainId: 728126428 })
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { estimateCost, estimateFees, type CostCall, type CostEstimate, type EstimateCostOptions, type EstimateFeesOptions, type FeeEstimate } from '../fees.js'
|
|
13
|
+
import { resolve, type ShortcutProviderOptions } from '../shortcuts.js'
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
estimateCost,
|
|
17
|
+
estimateFees,
|
|
18
|
+
type CostCall,
|
|
19
|
+
type CostEstimate,
|
|
20
|
+
type EstimateCostOptions,
|
|
21
|
+
type EstimateFeesOptions,
|
|
22
|
+
type EvmCostEstimate,
|
|
23
|
+
type FeeEstimate,
|
|
24
|
+
type FeeLevel,
|
|
25
|
+
type FeeLevelEstimate,
|
|
26
|
+
type TronCost,
|
|
27
|
+
} from '../fees.js'
|
|
28
|
+
|
|
29
|
+
export interface GetFeeEstimateOptions extends EstimateFeesOptions, ShortcutProviderOptions {}
|
|
30
|
+
|
|
31
|
+
/** 当前的三档手续费建议(EVM 链),节点参数与 getBalances 相同 */
|
|
32
|
+
export function getFeeEstimate(options: GetFeeEstimateOptions = {}): Promise<FeeEstimate> {
|
|
33
|
+
const { provider, own } = resolve(options, ['blocks', 'percentiles', 'baseFeeMultiplier'])
|
|
34
|
+
return estimateFees(provider, own)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GetCostEstimateOptions extends EstimateCostOptions, ShortcutProviderOptions {}
|
|
38
|
+
|
|
39
|
+
/** 一笔交易的手续费(EVM:gas × 价格 + L1 数据费;Tron:要烧的 TRX) */
|
|
40
|
+
export function getCostEstimate(call: CostCall, options: GetCostEstimateOptions = {}): Promise<CostEstimate> {
|
|
41
|
+
const { provider, own } = resolve(options, ['blocks', 'percentiles', 'baseFeeMultiplier', 'level', 'fees'])
|
|
42
|
+
return estimateCost(provider, call, own)
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 域名解析(子路径 `@w3lib/evm-batch-call/names`):ENS、SPACE ID(.bnb)、Basenames(.base.eth),名字 → 地址、地址 → 名字。
|
|
3
|
+
* 不放在主入口,不用时打包不会带上这部分代码。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { getAddressNames, getNameAddresses } from '@w3lib/evm-batch-call/names'
|
|
7
|
+
*
|
|
8
|
+
* await getNameAddresses(['vitalik.eth'], { chainId: 1 })
|
|
9
|
+
* await getAddressNames([user], { chainId: 56 })
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { lookupAddresses, resolveNames, type AddressName, type NamesOptions, type ResolvedName } from '../names.js'
|
|
13
|
+
import { resolve, type ShortcutProviderOptions } from '../shortcuts.js'
|
|
14
|
+
|
|
15
|
+
export { NAME_SERVICES, lookupAddresses, resolveNames, type AddressName, type NameService, type NamesOptions, type ResolvedName } from '../names.js'
|
|
16
|
+
|
|
17
|
+
export interface GetNamesOptions extends NamesOptions, ShortcutProviderOptions {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 名字 → 地址,节点参数与 getBalances 相同(用这条链的域名服务:Ethereum 是 ENS,BSC 是 SPACE ID,Base 是 Basenames)。
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* await getNameAddresses(['vitalik.eth', 'nick.eth'], { chainId: 1 })
|
|
24
|
+
* // [{ name: 'vitalik.eth', normalized: 'vitalik.eth', address: '0xd8dA…6045', service: 'ens' }, …]
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function getNameAddresses(names: readonly string[], options: GetNamesOptions = {}): Promise<ResolvedName[]> {
|
|
28
|
+
const { provider, own } = resolve(options, ['services'])
|
|
29
|
+
return resolveNames(provider, names, own)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 地址 → 名字(反查的主名字,正查核对过)。
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* await getAddressNames(['0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'], { chainId: 1 })
|
|
37
|
+
* // [{ address: '0xd8dA…', name: 'vitalik.eth', service: 'ens' }]
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function getAddressNames(addresses: readonly string[], options: GetNamesOptions = {}): Promise<AddressName[]> {
|
|
41
|
+
const { provider, own } = resolve(options, ['services'])
|
|
42
|
+
return lookupAddresses(provider, addresses, own)
|
|
43
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 价格(子路径 `@w3lib/evm-batch-call/price`):代币的美元价格(DefiLlama 当前 / 历史价格,没有收录的从链上 V2 / V3 池子算)、
|
|
3
|
+
* 组合价格源、地址的资产总值。不放在主入口,不用时打包不会带上这部分代码。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { getTokenPrices, getPortfolio } from '@w3lib/evm-batch-call/price'
|
|
7
|
+
*
|
|
8
|
+
* await getTokenPrices([NATIVE_TOKEN, USDT, newToken], { chainId: 56 })
|
|
9
|
+
* await getPortfolio(user, { chainId: 56, tokens: [NATIVE_TOKEN, USDT], lp: true })
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { portfolio, type Portfolio, type PortfolioOptions } from '../portfolio.js'
|
|
13
|
+
import { tokenPrices, type TokenPrice, type TokenPricesOptions } from '../price.js'
|
|
14
|
+
import { resolve, type ShortcutProviderOptions } from '../shortcuts.js'
|
|
15
|
+
|
|
16
|
+
export { portfolio, type Portfolio, type PortfolioOptions, type PortfolioToken } from '../portfolio.js'
|
|
17
|
+
export {
|
|
18
|
+
DEFILLAMA_CHAINS,
|
|
19
|
+
STABLECOINS,
|
|
20
|
+
combinePrices,
|
|
21
|
+
defillamaPrices,
|
|
22
|
+
onchainPrices,
|
|
23
|
+
priceKeyOf,
|
|
24
|
+
tokenPrices,
|
|
25
|
+
type DefillamaPriceOptions,
|
|
26
|
+
type OnchainPriceOptions,
|
|
27
|
+
type PriceSource,
|
|
28
|
+
type PriceSourceContext,
|
|
29
|
+
type TokenPrice,
|
|
30
|
+
type TokenPricesOptions,
|
|
31
|
+
} from '../price.js'
|
|
32
|
+
|
|
33
|
+
export interface GetTokenPricesOptions extends TokenPricesOptions, ShortcutProviderOptions {}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 查代币的美元价格(主币用 NATIVE_TOKEN),节点参数与 getBalances 相同。
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* await getTokenPrices([NATIVE_TOKEN, USDT], { chainId: 56 })
|
|
40
|
+
* // [{ token: '0xEeee…', native: true, price: 612.3, source: 'defillama' }, { token: USDT, …, price: 1, source: 'defillama' }]
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function getTokenPrices(tokens: readonly string[], options: GetTokenPricesOptions = {}): Promise<TokenPrice[]> {
|
|
44
|
+
const { provider, own } = resolve(options, ['sources', 'onError', 'fetch'])
|
|
45
|
+
return tokenPrices(provider, tokens, own)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface GetPortfolioOptions extends PortfolioOptions, ShortcutProviderOptions {}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 地址的资产总值:传入代币的余额 × 价格,可选加上 LP 仓位(lp: true)。
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* const p = await getPortfolio(user, { chainId: 56, tokens: [NATIVE_TOKEN, USDT, CAKE], lp: true })
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function getPortfolio(owner: string, options: GetPortfolioOptions): Promise<Portfolio> {
|
|
58
|
+
const { provider, own } = resolve(options, ['tokens', 'sources', 'lp', 'minUsd', 'onPriceError', 'fetch'])
|
|
59
|
+
return portfolio(provider, owner, own as PortfolioOptions)
|
|
60
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 代币风险检查(子路径 `@w3lib/evm-batch-call/risk`):模拟买入 / 转账 / 卖出,算出买卖税、判断貔貅盘,看是不是可升级代理、owner 有没有放弃。
|
|
3
|
+
* 不放在主入口,不用时打包不会带上这部分代码(含约 2.7KB 的探测合约字节码)。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { getTokenRisks } from '@w3lib/evm-batch-call/risk'
|
|
7
|
+
*
|
|
8
|
+
* const [risk] = await getTokenRisks([token], { chainId: 56 })
|
|
9
|
+
* if (risk.honeypot) alert('卖不出')
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { tokenRisks, type TokenRisk, type TokenRiskOptions } from '../risk.js'
|
|
13
|
+
import { resolve, type ShortcutProviderOptions } from '../shortcuts.js'
|
|
14
|
+
|
|
15
|
+
export { tokenRisks, type ProbeStage, type TokenRisk, type TokenRiskOptions } from '../risk.js'
|
|
16
|
+
|
|
17
|
+
export interface GetTokenRisksOptions extends TokenRiskOptions, ShortcutProviderOptions {}
|
|
18
|
+
|
|
19
|
+
/** 检查代币的风险,节点参数与 getBalances 相同 */
|
|
20
|
+
export function getTokenRisks(tokens: readonly string[], options: GetTokenRisksOptions = {}): Promise<TokenRisk[]> {
|
|
21
|
+
const { provider, own } = resolve(options, ['amount', 'protocols'])
|
|
22
|
+
return tokenRisks(provider, tokens, own)
|
|
23
|
+
}
|
|
@@ -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,413 @@
|
|
|
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, 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 { BatchCallOptions } from './write/batch.js'
|
|
14
|
+
import type { SignerInput } from './write/signer.js'
|
|
15
|
+
|
|
16
|
+
/** V3 协议的报价合约(QuoterV2)和兑换合约(SwapRouter02 接口),按 协议 id → chainId。2026-10 在链上核对过(Router 的 WETH9 / deployer,并用 eth_call 实际执行过兑换) */
|
|
17
|
+
export const V3_SWAP_CONTRACTS: Readonly<Record<string, Readonly<Record<number, { quoter: string; router: string }>>>> = {
|
|
18
|
+
'uniswap-v3': {
|
|
19
|
+
1: { quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
|
|
20
|
+
10: { quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
|
|
21
|
+
56: { quoter: '0x78D78E420Da98ad378D7799bE8f4AF69033EB077', router: '0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2' },
|
|
22
|
+
137: { quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
|
|
23
|
+
8453: { quoter: '0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a', router: '0x2626664c2603336E57B271c5C0b26F421741e481' },
|
|
24
|
+
42161: { quoter: '0x61fFE014bA17989E743c5F6cB21bF9697530B21e', router: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
|
|
25
|
+
43114: { quoter: '0xbe0F5544EC67e9B3b2D979aaA43f18Fd87E6257F', router: '0xbb00FF08d01D300023C629E8fFfFcb65A5a578cE' },
|
|
26
|
+
},
|
|
27
|
+
'pancakeswap-v3': {
|
|
28
|
+
1: { quoter: '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997', router: '0x13f4EA83D0bd40E75C8222255bc855a974568Dd4' },
|
|
29
|
+
56: { quoter: '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997', router: '0x13f4EA83D0bd40E75C8222255bc855a974568Dd4' },
|
|
30
|
+
// Base 上的 SmartRouter 地址与 Ethereum / BSC 不同
|
|
31
|
+
8453: { quoter: '0xB048Bbc1Ee6b733FFfCFb9e9CeF7375518e25997', router: '0x678Aa4bF4E210cf2166753e054d5b7c31cc7fa86' },
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const V3_FEES = [100, 500, 2500, 3000, 10000] as const
|
|
36
|
+
/** SwapRouter02 的特殊收款地址:留在 Router 里(之后 unwrapWETH9 换成主币) */
|
|
37
|
+
const ADDRESS_THIS = '0x0000000000000000000000000000000000000002'
|
|
38
|
+
|
|
39
|
+
const v2RouterInterface = new Interface([
|
|
40
|
+
'function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[] amounts)',
|
|
41
|
+
'function swapExactTokensForTokensSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
|
|
42
|
+
'function swapExactETHForTokensSupportingFeeOnTransferTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable',
|
|
43
|
+
'function swapExactTokensForETHSupportingFeeOnTransferTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline)',
|
|
44
|
+
])
|
|
45
|
+
const v3FactoryInterface = new Interface(['function getPool(address tokenA, address tokenB, uint24 fee) view returns (address)'])
|
|
46
|
+
const v3PoolInterface = new Interface(['function liquidity() view returns (uint128)'])
|
|
47
|
+
/** QuoterV2 很费 gas(池子很薄时一次可达上千万):每次 eth_call 只放几个报价,多个 eth_call 并发,避免超过节点的 gas 上限后被悄悄截掉 */
|
|
48
|
+
const QUOTES_PER_CALL = 3
|
|
49
|
+
/** 两跳时每一跳最多用几个手续费档位(按当前流动性取最大的) */
|
|
50
|
+
const TIERS_PER_LEG = 2
|
|
51
|
+
const quoterInterface = new Interface([
|
|
52
|
+
'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96) params) returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',
|
|
53
|
+
'function quoteExactInput(bytes path, uint256 amountIn) returns (uint256 amountOut, uint160[] sqrtPriceX96AfterList, uint32[] initializedTicksCrossedList, uint256 gasEstimate)',
|
|
54
|
+
])
|
|
55
|
+
const swapRouterInterface = new Interface([
|
|
56
|
+
'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)',
|
|
57
|
+
'function exactInput((bytes path, address recipient, uint256 amountIn, uint256 amountOutMinimum) params) payable returns (uint256 amountOut)',
|
|
58
|
+
'function unwrapWETH9(uint256 amountMinimum, address recipient) payable',
|
|
59
|
+
'function multicall(uint256 deadline, bytes[] data) payable returns (bytes[] results)',
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
export interface SwapParams {
|
|
63
|
+
/** 卖出 / 买入的代币;主币用 NATIVE_TOKEN 或零地址(池子里按包装主币,交易里自动包装 / 解包) */
|
|
64
|
+
tokenIn: string
|
|
65
|
+
tokenOut: string
|
|
66
|
+
/** 卖出的数量:amountIn(最小单位)和 amountInFormatted(按精度换算,如 '1.5')二选一 */
|
|
67
|
+
amountIn?: BigNumberish
|
|
68
|
+
amountInFormatted?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SwapOptions {
|
|
72
|
+
/** 价格滑点(比例,0.005 = 0.5%),最少收到 = 报价 × (1 − slippage),默认 0.005 */
|
|
73
|
+
slippage?: number
|
|
74
|
+
/** 交易的有效期(秒),默认 1200 */
|
|
75
|
+
deadline?: number
|
|
76
|
+
/** 收到代币的地址,默认 owner */
|
|
77
|
+
recipient?: string
|
|
78
|
+
/** 授权额度:'exact'(默认,本次卖出的数量)或 'max' */
|
|
79
|
+
approve?: 'exact' | 'max'
|
|
80
|
+
/**
|
|
81
|
+
* 代币的转账税(比例,0.05 = 5%),默认 0。V2 报价(getAmountsOut)不含转账税,带税的代币
|
|
82
|
+
* 实际收到的比报价少,最少收到按 (1 − slippage) × (1 − tax) 算,否则会因 INSUFFICIENT_OUTPUT_AMOUNT 回滚。
|
|
83
|
+
* 税率可用 /risk 的 getTokenRisks 查;只对 V2 路线生效(V3 不支持带税的代币)。plan.amountOut 仍是不含税的报价
|
|
84
|
+
*/
|
|
85
|
+
tax?: number
|
|
86
|
+
/** 只用这些协议(id,如 ['pancakeswap-v3']),默认这条链内置表里能兑换的 V2 / V3 协议 */
|
|
87
|
+
protocols?: readonly string[]
|
|
88
|
+
/** 两跳的中间代币,默认 LP_QUOTE_TOKENS 里这条链的(包装主币和稳定币);传 [] 只找直接兑换 */
|
|
89
|
+
intermediates?: readonly string[]
|
|
90
|
+
/** 按这个区块的状态报价(默认最新):同一个区块里报价可以复现 */
|
|
91
|
+
blockTag?: BlockTag
|
|
92
|
+
/** 指定路线(如之前报价结果 routes 里的某一条):只用这条路线报价和兑换,不选最好的 */
|
|
93
|
+
route?: Pick<SwapRoute, 'protocol' | 'path' | 'fees'>
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SwapRoute {
|
|
97
|
+
protocol: string
|
|
98
|
+
protocolName: string
|
|
99
|
+
version: 'v2' | 'v3'
|
|
100
|
+
/** 经过的代币(池子里的地址,主币是包装主币) */
|
|
101
|
+
path: string[]
|
|
102
|
+
/** V3:每一跳池子的手续费档位;V2 为空 */
|
|
103
|
+
fees: number[]
|
|
104
|
+
/** 这条路线的报价(最小单位) */
|
|
105
|
+
amountOut: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SwapTx {
|
|
109
|
+
/** approve:卖出的代币授权给 Router;swap:兑换 */
|
|
110
|
+
kind: 'approve' | 'swap'
|
|
111
|
+
to: string
|
|
112
|
+
data: string
|
|
113
|
+
/** 附带的主币(最小单位):卖出主币时为卖出的数量 */
|
|
114
|
+
value: string
|
|
115
|
+
token?: string
|
|
116
|
+
spender?: string
|
|
117
|
+
amount?: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface SwapPlan {
|
|
121
|
+
tokenIn: LpToken
|
|
122
|
+
tokenOut: LpToken
|
|
123
|
+
amountIn: string
|
|
124
|
+
amountInFormatted: string | null
|
|
125
|
+
/** 报价(最小单位)和最少收到的数量(按滑点) */
|
|
126
|
+
amountOut: string
|
|
127
|
+
amountOutFormatted: string | null
|
|
128
|
+
amountOutMin: string
|
|
129
|
+
amountOutMinFormatted: string | null
|
|
130
|
+
/** 1 个 tokenIn 换多少 tokenOut(按这笔的数量) */
|
|
131
|
+
price: number | null
|
|
132
|
+
/** 价格影响(0~1):这笔的价格比极小数量的价格差多少;算不出时为 null */
|
|
133
|
+
priceImpact: number | null
|
|
134
|
+
route: SwapRoute
|
|
135
|
+
/** 其他候选路线的报价,从高到低(含选中的) */
|
|
136
|
+
routes: SwapRoute[]
|
|
137
|
+
/** 按顺序发送的交易(授权在前) */
|
|
138
|
+
transactions: SwapTx[]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface Candidate {
|
|
142
|
+
protocol: LpProtocol
|
|
143
|
+
version: 'v2' | 'v3'
|
|
144
|
+
path: string[]
|
|
145
|
+
fees: number[]
|
|
146
|
+
/** V2:Router;V3:报价 / 兑换合约 */
|
|
147
|
+
router: string
|
|
148
|
+
quoter?: string
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 这条链上能兑换的协议:V2 要有 Router,V3 要有报价和兑换合约(V3_SWAP_CONTRACTS) */
|
|
152
|
+
export function swapProtocols(chainId: number): LpProtocol[] {
|
|
153
|
+
return LP_PROTOCOLS.filter((p) => p.chainId === chainId && ((p.version === 'v2' && !!p.router) || (p.version === 'v3' && !!V3_SWAP_CONTRACTS[p.id]?.[chainId])))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** V3 的路径编码:token ‖ fee(uint24) ‖ token ‖ … */
|
|
157
|
+
function encodePath(path: readonly string[], fees: readonly number[]): string {
|
|
158
|
+
let hex = path[0]?.slice(2).toLowerCase() ?? ''
|
|
159
|
+
fees.forEach((fee, i) => {
|
|
160
|
+
hex += fee.toString(16).padStart(6, '0') + (path[i + 1] as string).slice(2).toLowerCase()
|
|
161
|
+
})
|
|
162
|
+
return `0x${hex}`
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 列出候选路线(V3 先查哪些池子存在)并一次 multicall 报价,按输出从高到低 */
|
|
166
|
+
async function quoteRoutes(ctx: Ctx, tokenIn: string, tokenOut: string, amountIn: bigint, options: SwapOptions): Promise<Array<Candidate & { amountOut: bigint }>> {
|
|
167
|
+
const all = swapProtocols(ctx.chainId)
|
|
168
|
+
const protocols = options.protocols ? all.filter((p) => options.protocols?.includes(p.id)) : all
|
|
169
|
+
if (!protocols.length) throw new Error(`No swap protocol on chain ${ctx.chainId}${options.protocols ? ` among ${options.protocols.join(', ')}` : ''}`)
|
|
170
|
+
const mids = (options.intermediates ?? LP_QUOTE_TOKENS[ctx.chainId] ?? []).map(evm).filter((m) => !same(m, tokenIn) && !same(m, tokenOut))
|
|
171
|
+
|
|
172
|
+
// V3:先查池子是否存在(每一跳的每个手续费档位)
|
|
173
|
+
const v3 = protocols.filter((p) => p.version === 'v3')
|
|
174
|
+
const legs: Array<{ p: LpProtocol; a: string; b: string; fee: number }> = []
|
|
175
|
+
for (const p of v3) {
|
|
176
|
+
for (const fee of V3_FEES) {
|
|
177
|
+
legs.push({ p, a: tokenIn, b: tokenOut, fee })
|
|
178
|
+
for (const m of mids) legs.push({ p, a: tokenIn, b: m, fee }, { p, a: m, b: tokenOut, fee })
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const overrides = options.blockTag === undefined ? undefined : { blockTag: options.blockTag }
|
|
182
|
+
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) : []
|
|
183
|
+
// 再读存在的池子的当前流动性:没有流动性的不报价;两跳的每一跳只留流动性最大的几个档位(同一对代币的流动性可以直接比)
|
|
184
|
+
const found = legs.flatMap((l, i) => (pools[i] && BigInt(pools[i] as string) !== 0n ? [{ ...l, pool: pools[i] as string }] : []))
|
|
185
|
+
const liquidity = found.length ? await ctx.provider.tryAll<bigint>(found.map((l) => ctx.provider.contract(l.pool, v3PoolInterface).liquidity()), overrides) : []
|
|
186
|
+
const live = found.map((l, i) => ({ ...l, liquidity: liquidity[i] ?? 0n })).filter((l) => l.liquidity > 0n)
|
|
187
|
+
const legKey = (l: { p: LpProtocol; a: string; b: string }) => `${l.p.id}:${l.a}:${l.b}`
|
|
188
|
+
const byLeg = new Map<string, typeof live>()
|
|
189
|
+
for (const l of live) byLeg.set(legKey(l), [...(byLeg.get(legKey(l)) ?? []), l])
|
|
190
|
+
const exists = new Set<string>()
|
|
191
|
+
for (const [key, list] of byLeg) {
|
|
192
|
+
const direct = key.endsWith(`:${tokenIn}:${tokenOut}`)
|
|
193
|
+
const kept = direct ? list : [...list].sort((x, y) => (x.liquidity < y.liquidity ? 1 : x.liquidity > y.liquidity ? -1 : 0)).slice(0, TIERS_PER_LEG)
|
|
194
|
+
for (const l of kept) exists.add(`${l.p.id}:${l.a}:${l.b}:${l.fee}`)
|
|
195
|
+
}
|
|
196
|
+
const has = (p: LpProtocol, a: string, b: string, fee: number) => exists.has(`${p.id}:${a}:${b}:${fee}`)
|
|
197
|
+
|
|
198
|
+
const candidates: Candidate[] = []
|
|
199
|
+
for (const p of protocols) {
|
|
200
|
+
if (p.version === 'v2') {
|
|
201
|
+
const router = p.router as string
|
|
202
|
+
candidates.push({ protocol: p, version: 'v2', path: [tokenIn, tokenOut], fees: [], router })
|
|
203
|
+
for (const m of mids) candidates.push({ protocol: p, version: 'v2', path: [tokenIn, m, tokenOut], fees: [], router })
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
const c = V3_SWAP_CONTRACTS[p.id]?.[ctx.chainId] as { quoter: string; router: string }
|
|
207
|
+
for (const fee of V3_FEES) {
|
|
208
|
+
if (has(p, tokenIn, tokenOut, fee)) candidates.push({ protocol: p, version: 'v3', path: [tokenIn, tokenOut], fees: [fee], router: c.router, quoter: c.quoter })
|
|
209
|
+
}
|
|
210
|
+
for (const m of mids) {
|
|
211
|
+
for (const f1 of V3_FEES) {
|
|
212
|
+
if (!has(p, tokenIn, m, f1)) continue
|
|
213
|
+
for (const f2 of V3_FEES) {
|
|
214
|
+
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 })
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// V2 的 getAmountsOut 很便宜,一次报完;V3 每 QUOTES_PER_CALL 个一组并发
|
|
220
|
+
const v2Index = candidates.flatMap((c, i) => (c.version === 'v2' ? [i] : []))
|
|
221
|
+
const v3Index = candidates.flatMap((c, i) => (c.version === 'v3' ? [i] : []))
|
|
222
|
+
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)
|
|
223
|
+
const quotes: unknown[] = new Array(candidates.length).fill(null)
|
|
224
|
+
await Promise.all(
|
|
225
|
+
groups.map(async (group) => {
|
|
226
|
+
const results = await ctx.provider.tryAll(
|
|
227
|
+
group.map((i) => quoteCall(ctx, candidates[i] as Candidate, amountIn)),
|
|
228
|
+
overrides,
|
|
229
|
+
)
|
|
230
|
+
group.forEach((i, k) => (quotes[i] = results[k]))
|
|
231
|
+
}),
|
|
232
|
+
)
|
|
233
|
+
return candidates
|
|
234
|
+
.map((c, i) => ({ ...c, amountOut: amountOf(c, quotes[i]) }))
|
|
235
|
+
.filter((c) => c.amountOut > 0n)
|
|
236
|
+
.sort((a, b) => (a.amountOut < b.amountOut ? 1 : a.amountOut > b.amountOut ? -1 : 0))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function quoteCall(ctx: Ctx, c: Candidate, amountIn: bigint) {
|
|
240
|
+
if (c.version === 'v2') return ctx.provider.contract(c.router, v2RouterInterface).getAmountsOut(amountIn, c.path)
|
|
241
|
+
const quoter = ctx.provider.contract(c.quoter as string, quoterInterface)
|
|
242
|
+
return c.path.length === 2
|
|
243
|
+
? quoter.quoteExactInputSingle({ tokenIn: c.path[0], tokenOut: c.path[1], amountIn, fee: c.fees[0], sqrtPriceLimitX96: 0n })
|
|
244
|
+
: quoter.quoteExactInput(encodePath(c.path, c.fees), amountIn)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function amountOf(c: Candidate, result: unknown): bigint {
|
|
248
|
+
if (result === null || result === undefined) return 0n
|
|
249
|
+
if (c.version === 'v2') {
|
|
250
|
+
const amounts = result as readonly bigint[]
|
|
251
|
+
return amounts.length === c.path.length ? (amounts[amounts.length - 1] as bigint) : 0n
|
|
252
|
+
}
|
|
253
|
+
return (result as { 0: bigint })[0] ?? 0n
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 报价并构建兑换的交易(不签名):选输出最多的路线,最少收到 = 报价 × (1 − slippage);卖出代币时额度不够先授权给 Router。
|
|
258
|
+
*
|
|
259
|
+
* ```ts
|
|
260
|
+
* const plan = await buildSwap(provider, user, { tokenIn: NATIVE_TOKEN, tokenOut: USDT, amountInFormatted: '0.1' })
|
|
261
|
+
* plan.amountOutFormatted // '61.2'
|
|
262
|
+
* plan.route // { protocol: 'pancakeswap-v3', path: [WBNB, USDT], fees: [100], … }
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
export async function buildSwap(provider: Provider, owner: string, params: SwapParams, options: SwapOptions = {}): Promise<SwapPlan> {
|
|
266
|
+
const { slippage, deadline, recipient, approve } = options
|
|
267
|
+
const ctx = await createCtx(provider, owner, { slippage, deadline, recipient, approve })
|
|
268
|
+
const wrapped = LP_QUOTE_TOKENS[ctx.chainId]?.[0]
|
|
269
|
+
const nativeIn = isNative(ctx, params.tokenIn)
|
|
270
|
+
const nativeOut = isNative(ctx, params.tokenOut)
|
|
271
|
+
if ((nativeIn || nativeOut) && !wrapped) throw new Error(`No wrapped native token known on chain ${ctx.chainId}`)
|
|
272
|
+
const tokenIn = nativeIn ? evm(wrapped as string) : evm(params.tokenIn)
|
|
273
|
+
const tokenOut = nativeOut ? evm(wrapped as string) : evm(params.tokenOut)
|
|
274
|
+
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')
|
|
275
|
+
const [infoIn, infoOut] = await tokenInfo(ctx, [nativeIn ? params.tokenIn : tokenIn, nativeOut ? params.tokenOut : tokenOut])
|
|
276
|
+
const tIn = infoIn as LpToken
|
|
277
|
+
const tOut = infoOut as LpToken
|
|
278
|
+
const amountIn = parseAmount(params.amountIn, params.amountInFormatted, tIn, 'amountIn')
|
|
279
|
+
if (amountIn === undefined || amountIn <= 0n) throw new Error('amountIn must be greater than 0')
|
|
280
|
+
|
|
281
|
+
const all = await quoteRoutes(ctx, tokenIn, tokenOut, amountIn, options)
|
|
282
|
+
const wanted = options.route
|
|
283
|
+
const routes = wanted
|
|
284
|
+
? 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())
|
|
285
|
+
: all
|
|
286
|
+
const best = routes[0]
|
|
287
|
+
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')
|
|
288
|
+
// 价格影响:同一路线卖出极小的数量(1 / 10000)的价格
|
|
289
|
+
const small = amountIn / 10_000n
|
|
290
|
+
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
|
|
291
|
+
// 参考交易的输出太小时取整误差太大(几个最小单位就差好几个百分点),算不准,返回 null
|
|
292
|
+
const priceImpact = smallOut >= 10_000n ? Math.max(0, 1 - Number(best.amountOut) / Number(amountIn) / (Number(smallOut) / Number(small))) : null
|
|
293
|
+
const tax = options.tax ?? 0
|
|
294
|
+
if (!(tax >= 0 && tax < 1)) throw new RangeError(`tax must be in [0, 1): ${tax}`)
|
|
295
|
+
const keep = best.version === 'v2' ? 1 - tax : 1
|
|
296
|
+
const amountOutMin = minus((best.amountOut * BigInt(Math.round(keep * 1_000_000))) / 1_000_000n, ctx.slippage)
|
|
297
|
+
|
|
298
|
+
// approvals 只会返回 kind 为 'approve' 的交易
|
|
299
|
+
// Router 在 Tron 上是 T 地址:授权的 ABI 参数要用 0x 格式
|
|
300
|
+
const transactions = (nativeIn ? [] : await approvals(ctx, [{ token: tokenIn, spender: evm(best.router), amount: amountIn }])) as SwapTx[]
|
|
301
|
+
transactions.push(swapTx(ctx, best, amountIn, amountOutMin, nativeIn, nativeOut))
|
|
302
|
+
|
|
303
|
+
const route = (c: Candidate & { amountOut: bigint }): SwapRoute => ({
|
|
304
|
+
protocol: c.protocol.id,
|
|
305
|
+
protocolName: c.protocol.name,
|
|
306
|
+
version: c.version,
|
|
307
|
+
path: c.path.map((a) => out(ctx, a)),
|
|
308
|
+
fees: c.fees,
|
|
309
|
+
amountOut: c.amountOut.toString(),
|
|
310
|
+
})
|
|
311
|
+
const decimals = (t: LpToken) => t.decimals ?? 0
|
|
312
|
+
return {
|
|
313
|
+
tokenIn: tIn,
|
|
314
|
+
tokenOut: tOut,
|
|
315
|
+
amountIn: amountIn.toString(),
|
|
316
|
+
amountInFormatted: fmt(amountIn, tIn),
|
|
317
|
+
amountOut: best.amountOut.toString(),
|
|
318
|
+
amountOutFormatted: fmt(best.amountOut, tOut),
|
|
319
|
+
amountOutMin: amountOutMin.toString(),
|
|
320
|
+
amountOutMinFormatted: fmt(amountOutMin, tOut),
|
|
321
|
+
price: tIn.decimals === null || tOut.decimals === null ? null : Number(best.amountOut) / 10 ** decimals(tOut) / (Number(amountIn) / 10 ** decimals(tIn)),
|
|
322
|
+
priceImpact,
|
|
323
|
+
route: route(best),
|
|
324
|
+
routes: routes.map(route),
|
|
325
|
+
transactions,
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function swapTx(ctx: Ctx, c: Candidate, amountIn: bigint, amountOutMin: bigint, nativeIn: boolean, nativeOut: boolean): SwapTx {
|
|
330
|
+
const value = nativeIn ? amountIn.toString() : '0'
|
|
331
|
+
if (c.version === 'v2') {
|
|
332
|
+
const data = nativeIn
|
|
333
|
+
? v2RouterInterface.encodeFunctionData('swapExactETHForTokensSupportingFeeOnTransferTokens', [amountOutMin, c.path, ctx.recipient, ctx.deadline])
|
|
334
|
+
: nativeOut
|
|
335
|
+
? v2RouterInterface.encodeFunctionData('swapExactTokensForETHSupportingFeeOnTransferTokens', [amountIn, amountOutMin, c.path, ctx.recipient, ctx.deadline])
|
|
336
|
+
: v2RouterInterface.encodeFunctionData('swapExactTokensForTokensSupportingFeeOnTransferTokens', [amountIn, amountOutMin, c.path, ctx.recipient, ctx.deadline])
|
|
337
|
+
return { kind: 'swap', to: out(ctx, c.router), data, value }
|
|
338
|
+
}
|
|
339
|
+
// V3:卖出主币时附带 value(Router 自动包装);买入主币时先收到 Router,再 unwrapWETH9 给收款地址
|
|
340
|
+
const recipient = nativeOut ? ADDRESS_THIS : ctx.recipient
|
|
341
|
+
const swap =
|
|
342
|
+
c.path.length === 2
|
|
343
|
+
? swapRouterInterface.encodeFunctionData('exactInputSingle', [
|
|
344
|
+
{ tokenIn: c.path[0], tokenOut: c.path[1], fee: c.fees[0], recipient, amountIn, amountOutMinimum: amountOutMin, sqrtPriceLimitX96: 0n },
|
|
345
|
+
])
|
|
346
|
+
: swapRouterInterface.encodeFunctionData('exactInput', [{ path: encodePath(c.path, c.fees), recipient, amountIn, amountOutMinimum: amountOutMin }])
|
|
347
|
+
const calls = nativeOut ? [swap, swapRouterInterface.encodeFunctionData('unwrapWETH9', [amountOutMin, ctx.recipient])] : [swap]
|
|
348
|
+
return { kind: 'swap', to: out(ctx, c.router), data: swapRouterInterface.encodeFunctionData('multicall', [ctx.deadline, calls]), value }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export type SwapTxResult = Omit<LiquidityTxResult, 'kind'> & { kind: SwapTx['kind'] }
|
|
352
|
+
|
|
353
|
+
export interface SwapResult {
|
|
354
|
+
plan: SwapPlan
|
|
355
|
+
/** 每笔交易的结果(授权、兑换) */
|
|
356
|
+
results: SwapTxResult[]
|
|
357
|
+
/** 兑换那一笔的状态和哈希 */
|
|
358
|
+
status: LiquidityTxResult['status']
|
|
359
|
+
hash: string | null
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface SwapSendOptions extends SwapOptions, Omit<BatchCallOptions, 'mode' | 'continueOnError' | 'onUpdate'> {
|
|
363
|
+
onUpdate?: (result: SwapTxResult) => void
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 兑换:构建(同 buildSwap)后用 from 签名发送(钱包同 /write:私钥、ethers Signer、浏览器钱包、Tron 钱包)。
|
|
368
|
+
* 授权失败时不发兑换的交易
|
|
369
|
+
*
|
|
370
|
+
* ```ts
|
|
371
|
+
* const r = await swap(provider, key, { tokenIn: USDT, tokenOut: NATIVE_TOKEN, amountInFormatted: '100' }, { slippage: 0.01 })
|
|
372
|
+
* r.status // 'confirmed'
|
|
373
|
+
* ```
|
|
374
|
+
*/
|
|
375
|
+
export async function swap(provider: Provider, from: SignerInput, params: SwapParams, options: SwapSendOptions = {}): Promise<SwapResult> {
|
|
376
|
+
const [{ batchCall, tronModule }, { resolveSigner }, { emptyOutcome, toWriteResult }] = await Promise.all([import('./write/batch.js'), import('./write/signer.js'), import('./write/send.js')])
|
|
377
|
+
const tron = await tronModule(provider)
|
|
378
|
+
const owner = tron ? await tron.tronSignerAddress(from) : (await resolveSigner(from)).address
|
|
379
|
+
let plan = await buildSwap(provider, owner, params, options)
|
|
380
|
+
const send = async (txs: SwapTx[]): Promise<SwapTxResult[]> =>
|
|
381
|
+
(
|
|
382
|
+
await batchCall(
|
|
383
|
+
provider,
|
|
384
|
+
txs.map((tx) => ({ from, to: tx.to, data: tx.data, value: BigInt(tx.value) })),
|
|
385
|
+
{
|
|
386
|
+
...options,
|
|
387
|
+
mode: 'tx',
|
|
388
|
+
continueOnError: false,
|
|
389
|
+
onUpdate: options.onUpdate ? (r) => options.onUpdate?.({ ...r, kind: (txs[r.index] as SwapTx).kind }) : undefined,
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
).map((r, i) => ({ ...r, kind: (txs[i] as SwapTx).kind }))
|
|
393
|
+
const txs = plan.transactions
|
|
394
|
+
let results: SwapTxResult[]
|
|
395
|
+
if (txs.length === 1) {
|
|
396
|
+
results = await send(txs)
|
|
397
|
+
} else {
|
|
398
|
+
// 有授权时先发授权:确认后重新报价再发兑换,deadline 和最少收到从发兑换那一刻算,
|
|
399
|
+
// 不会因为授权等得久而过期或按旧价成交
|
|
400
|
+
const approved = await send(txs.slice(0, -1))
|
|
401
|
+
const last = txs[txs.length - 1] as SwapTx
|
|
402
|
+
if (approved.every((r) => r.status === 'confirmed')) {
|
|
403
|
+
plan = await buildSwap(provider, owner, params, options)
|
|
404
|
+
const fresh = plan.transactions[plan.transactions.length - 1] as SwapTx
|
|
405
|
+
results = [...approved, ...(await send([fresh])).map((r) => ({ ...r, index: txs.length - 1 }))]
|
|
406
|
+
} else {
|
|
407
|
+
results = [...approved, { ...toWriteResult(txs.length - 1, owner, last.to, emptyOutcome('skipped', 'skipped: the approval did not succeed')), kind: last.kind }]
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const main = results[results.length - 1] as SwapTxResult
|
|
411
|
+
return { plan, results, status: main.status, hash: main.hash }
|
|
412
|
+
}
|
|
413
|
+
|
|
@@ -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'
|