@w3lib/evm-batch-call 0.5.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 +38 -0
- package/README.md +240 -4
- 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/liquidity/common.d.ts +72 -0
- package/dist/liquidity/common.d.ts.map +1 -0
- package/dist/liquidity/common.js +271 -0
- package/dist/liquidity/common.js.map +1 -0
- package/dist/liquidity/concentrated.d.ts +19 -0
- package/dist/liquidity/concentrated.d.ts.map +1 -0
- package/dist/liquidity/concentrated.js +106 -0
- package/dist/liquidity/concentrated.js.map +1 -0
- package/dist/liquidity/index.d.ts +42 -0
- package/dist/liquidity/index.d.ts.map +1 -0
- package/dist/liquidity/index.js +143 -0
- package/dist/liquidity/index.js.map +1 -0
- package/dist/liquidity/types.d.ts +182 -0
- package/dist/liquidity/types.d.ts.map +1 -0
- package/dist/liquidity/types.js +2 -0
- package/dist/liquidity/types.js.map +1 -0
- package/dist/liquidity/v2.d.ts +5 -0
- package/dist/liquidity/v2.d.ts.map +1 -0
- package/dist/liquidity/v2.js +342 -0
- package/dist/liquidity/v2.js.map +1 -0
- package/dist/liquidity/v3.d.ts +6 -0
- package/dist/liquidity/v3.d.ts.map +1 -0
- package/dist/liquidity/v3.js +310 -0
- package/dist/liquidity/v3.js.map +1 -0
- package/dist/liquidity/v4.d.ts +5 -0
- package/dist/liquidity/v4.d.ts.map +1 -0
- package/dist/liquidity/v4.js +279 -0
- package/dist/liquidity/v4.js.map +1 -0
- package/dist/lp.d.ts +10 -2
- package/dist/lp.d.ts.map +1 -1
- package/dist/lp.js +31 -19
- package/dist/lp.js.map +1 -1
- package/dist/lpMath.d.ts +55 -0
- package/dist/lpMath.d.ts.map +1 -1
- package/dist/lpMath.js +209 -0
- package/dist/lpMath.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/liquidity.d.ts +24 -0
- package/dist/subpaths/liquidity.d.ts.map +1 -0
- package/dist/subpaths/liquidity.js +21 -0
- package/dist/subpaths/liquidity.js.map +1 -0
- package/dist/subpaths/lp.d.ts +1 -1
- package/dist/subpaths/lp.d.ts.map +1 -1
- package/dist/subpaths/lp.js +1 -1
- package/dist/subpaths/lp.js.map +1 -1
- 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 +27 -0
- package/dist/write/tron.d.ts.map +1 -1
- package/dist/write/tron.js +30 -0
- package/dist/write/tron.js.map +1 -1
- package/package.json +32 -1
- 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/liquidity/common.ts +314 -0
- package/src/liquidity/concentrated.ts +121 -0
- package/src/liquidity/index.ts +171 -0
- package/src/liquidity/types.ts +194 -0
- package/src/liquidity/v2.ts +371 -0
- package/src/liquidity/v3.ts +382 -0
- package/src/liquidity/v4.ts +348 -0
- package/src/lp.ts +39 -19
- package/src/lpMath.ts +223 -0
- 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/liquidity.ts +52 -0
- package/src/subpaths/lp.ts +20 -1
- 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 +48 -0
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'
|
package/src/write/tron.ts
CHANGED
|
@@ -186,6 +186,11 @@ function resolveTronSigner(input: SignerInput, cache: Map<unknown, Promise<TronK
|
|
|
186
186
|
return resolved
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/** 签名器的地址(T 地址),不发交易、不弹钱包 */
|
|
190
|
+
export async function tronSignerAddress(input: SignerInput): Promise<string> {
|
|
191
|
+
return (await resolveTronSigner(input, new Map())).address
|
|
192
|
+
}
|
|
193
|
+
|
|
189
194
|
// ---- 交易编码(protobuf) ----
|
|
190
195
|
|
|
191
196
|
function varint(value: bigint | number): number[] {
|
|
@@ -739,6 +744,49 @@ function canonical(address: string): string {
|
|
|
739
744
|
return toTronAddress(toEvmAddress(address))
|
|
740
745
|
}
|
|
741
746
|
|
|
747
|
+
/** 一笔交易的手续费估算(子路径 /fees 的 estimateCost 在 Tron 上用) */
|
|
748
|
+
export interface TronCostEstimate {
|
|
749
|
+
/** 合约调用消耗的能量(模拟得到);转 TRX 为 0 */
|
|
750
|
+
energy: number
|
|
751
|
+
/** 交易的字节数(带宽) */
|
|
752
|
+
bandwidth: number
|
|
753
|
+
/** 账户剩余的能量 / 带宽不够时要烧的 TRX(sun) */
|
|
754
|
+
energyBurn: bigint
|
|
755
|
+
bandwidthBurn: bigint
|
|
756
|
+
/** 转 TRX 给没激活的地址:激活费(含创建账户的带宽),算在 bandwidthBurn 里 */
|
|
757
|
+
activates: boolean
|
|
758
|
+
/** 每单位能量、每字节带宽的价格(sun) */
|
|
759
|
+
energyPrice: bigint
|
|
760
|
+
bandwidthPrice: bigint
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* 估算一笔交易要烧多少 TRX:合约调用模拟出能量,按发送者剩余的质押能量 / 免费和质押带宽抵扣,不够的部分按链参数的价格烧 TRX。
|
|
765
|
+
* 能量按模拟值(不加余量),实际执行可能略有出入
|
|
766
|
+
*/
|
|
767
|
+
export async function tronEstimateCost(provider: Provider, call: { from: string; to: string; data?: string; value?: bigint }): Promise<TronCostEstimate> {
|
|
768
|
+
const rpc = await tronRpc(provider, {})
|
|
769
|
+
const from = canonical(call.from)
|
|
770
|
+
const to = canonical(call.to)
|
|
771
|
+
const value = call.value ?? 0n
|
|
772
|
+
const data = call.data && call.data !== '0x' ? call.data : null
|
|
773
|
+
const [params, resources, isContract] = await Promise.all([rpc.params(), rpc.resources(from), data ? Promise.resolve(true) : rpc.isContract(to)])
|
|
774
|
+
const contract: TronContract = isContract ? { type: 'trigger', owner: from, contract: to, data: data ?? '0x', callValue: value } : { type: 'transfer', owner: from, to, amount: value }
|
|
775
|
+
const energy = contract.type === 'trigger' ? await rpc.simulate(contract) : 0
|
|
776
|
+
const feeLimit = contract.type === 'trigger' ? BigInt(Math.ceil(energy * 1.5)) * params.energyFee || 1_000_000n : 0n
|
|
777
|
+
const bytes = transactionBytes(contract, feeLimit)
|
|
778
|
+
const activates = contract.type === 'transfer' && !(await rpc.accountExists(to))
|
|
779
|
+
return {
|
|
780
|
+
energy,
|
|
781
|
+
bandwidth: bytes,
|
|
782
|
+
energyBurn: BigInt(Math.max(0, energy - resources.energy)) * params.energyFee,
|
|
783
|
+
bandwidthBurn: contract.type === 'transfer' ? transferBurn(resources, bytes, params, activates) : bandwidthBurn(resources, bytes, params.bandwidthFee),
|
|
784
|
+
activates,
|
|
785
|
+
energyPrice: params.energyFee,
|
|
786
|
+
bandwidthPrice: params.bandwidthFee,
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
742
790
|
/** 转 TRX:普通地址用 TransferContract;合约地址用合约调用(空 calldata 带 call_value,主网禁止 TransferContract 转给合约) */
|
|
743
791
|
async function trxContract(rpc: TronRpc, owner: string, to: string, amount: bigint): Promise<TronContract> {
|
|
744
792
|
return (await rpc.isContract(to)) ? { type: 'trigger', owner, contract: to, data: '0x', callValue: amount } : { type: 'transfer', owner, to, amount }
|