@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/fees.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 手续费估算(子路径 `@w3lib/evm-batch-call/fees`):EIP-1559 的 baseFee 和三档优先费(按最近区块的 eth_feeHistory),
|
|
3
|
+
* 一笔交易的手续费(含 OP Stack / Scroll 的 L1 数据费);Tron 按能量 / 带宽和账户剩余的资源算要烧的 TRX
|
|
4
|
+
*/
|
|
5
|
+
import type { BigNumberish } from 'ethers'
|
|
6
|
+
|
|
7
|
+
import { getNativeCurrency } from './chains.js'
|
|
8
|
+
import { formatUnits } from './erc20.js'
|
|
9
|
+
import type { Provider } from './provider.js'
|
|
10
|
+
import { isTronChain } from './tron.js'
|
|
11
|
+
|
|
12
|
+
export type FeeLevel = 'slow' | 'normal' | 'fast'
|
|
13
|
+
|
|
14
|
+
/** 一档费用(wei 的十进制字符串):EIP-1559 链是 maxFeePerGas / maxPriorityFeePerGas,legacy 链是 gasPrice */
|
|
15
|
+
export type FeeLevelEstimate = { maxFeePerGas: string; maxPriorityFeePerGas: string } | { gasPrice: string }
|
|
16
|
+
|
|
17
|
+
export interface FeeEstimate {
|
|
18
|
+
type: 'eip1559' | 'legacy'
|
|
19
|
+
/** 下一个区块的 baseFee(wei);legacy 链为 null */
|
|
20
|
+
baseFee: string | null
|
|
21
|
+
slow: FeeLevelEstimate
|
|
22
|
+
normal: FeeLevelEstimate
|
|
23
|
+
fast: FeeLevelEstimate
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface EstimateFeesOptions {
|
|
27
|
+
/** 看最近多少个区块,默认 20 */
|
|
28
|
+
blocks?: number
|
|
29
|
+
/** 三档优先费取各区块交易优先费的百分位,默认 [10, 50, 90] */
|
|
30
|
+
percentiles?: readonly [number, number, number]
|
|
31
|
+
/** maxFeePerGas = baseFee × baseFeeMultiplier + 优先费,默认 2(baseFee 连涨几个区块也够) */
|
|
32
|
+
baseFeeMultiplier?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface FeeHistory {
|
|
36
|
+
baseFeePerGas?: string[]
|
|
37
|
+
reward?: string[][]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 节点的 JSON-RPC send(ethers 的 JsonRpcProvider / BrowserProvider) */
|
|
41
|
+
type Sender = { send(method: string, params: unknown[]): Promise<unknown> }
|
|
42
|
+
|
|
43
|
+
async function send<T>(provider: Provider, method: string, params: unknown[]): Promise<T> {
|
|
44
|
+
const { runOnNodes } = await import('./write/rpc.js')
|
|
45
|
+
return runOnNodes(provider.rpc, method, async (node) => {
|
|
46
|
+
if (typeof (node as Partial<Sender>).send !== 'function') throw new Error(`${method} is not supported by this node; pass an RPC URL or an ethers Provider`)
|
|
47
|
+
return (await (node as unknown as Sender).send(method, params)) as T
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const median = (values: bigint[]): bigint => {
|
|
52
|
+
if (!values.length) return 0n
|
|
53
|
+
const sorted = [...values].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
54
|
+
return sorted[Math.floor(sorted.length / 2)] as bigint
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 当前的手续费建议(EVM 链):baseFee 取下一个区块的,三档优先费是最近区块里交易优先费的百分位的中位数;
|
|
59
|
+
* 节点不支持 eth_feeHistory 或链上没有 baseFee 时按 eth_gasPrice(三档为 1、1、1.2 倍)。Tron 用 estimateCost
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const fees = await estimateFees(provider)
|
|
63
|
+
* // { type: 'eip1559', baseFee: '1200000000', slow: { maxFeePerGas, maxPriorityFeePerGas }, normal: {…}, fast: {…} }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export async function estimateFees(provider: Provider, options: EstimateFeesOptions = {}): Promise<FeeEstimate> {
|
|
67
|
+
if (isTronChain(await provider.getChainId())) throw new Error('estimateFees is for EVM chains; use estimateCost on Tron')
|
|
68
|
+
const blocks = Math.max(1, Math.min(1024, Math.floor(options.blocks ?? 20)))
|
|
69
|
+
const percentiles = options.percentiles ?? [10, 50, 90]
|
|
70
|
+
if (!percentiles.every((p, i) => p >= 0 && p <= 100 && (i === 0 || p >= (percentiles[i - 1] as number)))) throw new RangeError('percentiles must be ascending numbers in [0, 100]')
|
|
71
|
+
const multiplier = options.baseFeeMultiplier ?? 2
|
|
72
|
+
let history: FeeHistory | null = null
|
|
73
|
+
try {
|
|
74
|
+
history = await send<FeeHistory>(provider, 'eth_feeHistory', [`0x${blocks.toString(16)}`, 'latest', [...percentiles]])
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// 只有节点 / 链不支持 feeHistory 时才按 gasPrice;网络等其他错误照常抛出(不要在 EIP-1559 链上悄悄退回 legacy)
|
|
77
|
+
if (!/not (supported|available|found)|unsupported|does not exist|unknown method|-32601|method.*not/i.test(err instanceof Error ? err.message : String(err))) throw err
|
|
78
|
+
history = null
|
|
79
|
+
}
|
|
80
|
+
const baseFees = history?.baseFeePerGas ?? []
|
|
81
|
+
const next = baseFees.length ? BigInt(baseFees[baseFees.length - 1] as string) : 0n
|
|
82
|
+
if (!history || !baseFees.length || next === 0n) {
|
|
83
|
+
const gasPrice = BigInt(await send<string>(provider, 'eth_gasPrice', []))
|
|
84
|
+
const level = (n: bigint, d: bigint) => ({ gasPrice: ((gasPrice * n) / d).toString() })
|
|
85
|
+
return { type: 'legacy', baseFee: null, slow: level(1n, 1n), normal: level(1n, 1n), fast: level(6n, 5n) }
|
|
86
|
+
}
|
|
87
|
+
const rewards = history.reward ?? []
|
|
88
|
+
// 每档:各区块这个百分位的优先费取中位数(空区块的 0 不算)
|
|
89
|
+
const tip = (k: number) => median(rewards.map((r) => BigInt(r[k] ?? '0')).filter((v) => v > 0n))
|
|
90
|
+
const base = (next * BigInt(Math.round(multiplier * 1000))) / 1000n
|
|
91
|
+
const level = (priority: bigint) => ({ maxFeePerGas: (base + priority).toString(), maxPriorityFeePerGas: priority.toString() })
|
|
92
|
+
const [slow, normal, fast] = [tip(0), tip(1), tip(2)]
|
|
93
|
+
// 优先费不递减(区块很空时三档可能相同)
|
|
94
|
+
return { type: 'eip1559', baseFee: next.toString(), slow: level(slow), normal: level(normal > slow ? normal : slow), fast: level(fast > normal ? fast : normal > slow ? normal : slow) }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface CostCall {
|
|
98
|
+
/** 发送者(预估 gas、Tron 上按它剩余的资源算) */
|
|
99
|
+
from: string
|
|
100
|
+
to: string
|
|
101
|
+
data?: string
|
|
102
|
+
value?: BigNumberish
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface EvmCostEstimate {
|
|
106
|
+
type: 'evm'
|
|
107
|
+
gasLimit: string
|
|
108
|
+
/** 这一档的每 gas 价格:EIP-1559 是 baseFee + 优先费(预计实际付的),legacy 是 gasPrice */
|
|
109
|
+
gasPrice: string
|
|
110
|
+
/** gasLimit × gasPrice(wei) */
|
|
111
|
+
fee: string
|
|
112
|
+
/** 最多付多少:gasLimit × maxFeePerGas(legacy 同 fee) */
|
|
113
|
+
maxFee: string
|
|
114
|
+
/** OP Stack / Scroll 的 L1 数据费(wei,含 25% 余量),其他链为 0 */
|
|
115
|
+
l1Fee: string
|
|
116
|
+
/** fee + l1Fee(wei)和按主币精度换算的值 */
|
|
117
|
+
total: string
|
|
118
|
+
totalFormatted: string
|
|
119
|
+
symbol: string | null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface TronCost {
|
|
123
|
+
type: 'tron'
|
|
124
|
+
energy: number
|
|
125
|
+
bandwidth: number
|
|
126
|
+
/** 剩余能量 / 带宽不够时要烧的 TRX(sun) */
|
|
127
|
+
energyBurn: string
|
|
128
|
+
bandwidthBurn: string
|
|
129
|
+
/** 转 TRX 给没激活的地址(激活费算在 bandwidthBurn 里) */
|
|
130
|
+
activates: boolean
|
|
131
|
+
energyPrice: string
|
|
132
|
+
bandwidthPrice: string
|
|
133
|
+
/** energyBurn + bandwidthBurn(sun)和按 TRX 换算的值 */
|
|
134
|
+
total: string
|
|
135
|
+
totalFormatted: string
|
|
136
|
+
symbol: 'TRX'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type CostEstimate = EvmCostEstimate | TronCost
|
|
140
|
+
|
|
141
|
+
export interface EstimateCostOptions extends EstimateFeesOptions {
|
|
142
|
+
/** 用哪一档,默认 'normal' */
|
|
143
|
+
level?: FeeLevel
|
|
144
|
+
/** 已经查好的费用建议(多笔交易共用,不再查一次) */
|
|
145
|
+
fees?: FeeEstimate
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 估算一笔交易的手续费:EVM 预估 gas × 这一档的价格(+ L1 数据费);Tron 模拟能量,按发送者剩余的资源算要烧的 TRX。
|
|
150
|
+
* 预估 gas 会执行一遍交易,revert 时抛出
|
|
151
|
+
*
|
|
152
|
+
* ```ts
|
|
153
|
+
* await estimateCost(provider, { from: user, to: USDT, data }) // { type: 'evm', gasLimit, fee, total, totalFormatted: '0.00042', symbol: 'BNB', … }
|
|
154
|
+
* await estimateCost(tronProvider, { from, to: USDT, data }) // { type: 'tron', energy: 64285, energyBurn: '6750000', totalFormatted: '7.0', … }
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export async function estimateCost(provider: Provider, call: CostCall, options: EstimateCostOptions = {}): Promise<CostEstimate> {
|
|
158
|
+
const chainId = await provider.getChainId()
|
|
159
|
+
const value = BigInt(call.value ?? 0)
|
|
160
|
+
if (isTronChain(chainId)) {
|
|
161
|
+
const { tronEstimateCost } = await import('./write/tron.js')
|
|
162
|
+
const c = await tronEstimateCost(provider, { from: call.from, to: call.to, ...(call.data ? { data: call.data } : {}), value })
|
|
163
|
+
const total = c.energyBurn + c.bandwidthBurn
|
|
164
|
+
return {
|
|
165
|
+
type: 'tron',
|
|
166
|
+
energy: c.energy,
|
|
167
|
+
bandwidth: c.bandwidth,
|
|
168
|
+
energyBurn: c.energyBurn.toString(),
|
|
169
|
+
bandwidthBurn: c.bandwidthBurn.toString(),
|
|
170
|
+
activates: c.activates,
|
|
171
|
+
energyPrice: c.energyPrice.toString(),
|
|
172
|
+
bandwidthPrice: c.bandwidthPrice.toString(),
|
|
173
|
+
total: total.toString(),
|
|
174
|
+
totalFormatted: formatUnits(total, 6),
|
|
175
|
+
symbol: 'TRX',
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const { WriteRpc, l1Fee } = await import('./write/rpc.js')
|
|
179
|
+
const rpc = await WriteRpc.from(provider)
|
|
180
|
+
const tx = { from: call.from, to: call.to, data: call.data ?? '0x', value }
|
|
181
|
+
const [gasLimit, fees] = await Promise.all([rpc.estimateGas(tx), options.fees ? Promise.resolve(options.fees) : estimateFees(provider, options)])
|
|
182
|
+
const level = fees[options.level ?? 'normal']
|
|
183
|
+
const perGas = 'gasPrice' in level ? BigInt(level.gasPrice) : BigInt(fees.baseFee ?? '0') + BigInt(level.maxPriorityFeePerGas)
|
|
184
|
+
const maxPerGas = 'gasPrice' in level ? perGas : BigInt(level.maxFeePerGas)
|
|
185
|
+
const feeFields = 'gasPrice' in level ? { type: 0, gasPrice: perGas } : { type: 2, maxFeePerGas: maxPerGas, maxPriorityFeePerGas: BigInt(level.maxPriorityFeePerGas) }
|
|
186
|
+
const l1 = await l1Fee(rpc, { ...tx, from: undefined, gasLimit, ...feeFields })
|
|
187
|
+
const fee = gasLimit * perGas
|
|
188
|
+
const total = fee + l1
|
|
189
|
+
const native = getNativeCurrency(chainId)
|
|
190
|
+
return {
|
|
191
|
+
type: 'evm',
|
|
192
|
+
gasLimit: gasLimit.toString(),
|
|
193
|
+
gasPrice: perGas.toString(),
|
|
194
|
+
fee: fee.toString(),
|
|
195
|
+
maxFee: (gasLimit * maxPerGas).toString(),
|
|
196
|
+
l1Fee: l1.toString(),
|
|
197
|
+
total: total.toString(),
|
|
198
|
+
totalFormatted: formatUnits(total, native?.decimals ?? 18),
|
|
199
|
+
symbol: native?.symbol ?? null,
|
|
200
|
+
}
|
|
201
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** HTTP 请求(代币列表、价格服务):超时、状态码检查 */
|
|
2
|
+
|
|
3
|
+
const REQUEST_TIMEOUT = 20_000
|
|
4
|
+
|
|
5
|
+
export class HttpStatusError extends Error {
|
|
6
|
+
constructor(
|
|
7
|
+
readonly status: number,
|
|
8
|
+
target: string,
|
|
9
|
+
) {
|
|
10
|
+
super(`HTTP ${status} from ${target}`)
|
|
11
|
+
this.name = 'HttpStatusError'
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param secret 请求里带的 Key:出错时错误信息只写域名,不写完整地址,避免 Key 出现在日志里
|
|
17
|
+
*/
|
|
18
|
+
export function fetchJson<T>(fetchFn: typeof fetch, url: string, init?: RequestInit, secret?: string): Promise<T> {
|
|
19
|
+
return fetchFn(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT) }).then(async (res) => {
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
throw new HttpStatusError(res.status, secret ? safeHost(url) : url)
|
|
22
|
+
}
|
|
23
|
+
return (await res.json()) as T
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeHost(url: string): string {
|
|
28
|
+
try {
|
|
29
|
+
return new URL(url).host
|
|
30
|
+
} catch {
|
|
31
|
+
return 'request'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { Interface, MaxUint256, ZeroAddress, getAddress, getBigInt, parseUnits, type BigNumberish } from 'ethers'
|
|
2
|
+
|
|
3
|
+
import { formatUnits } from '../erc20.js'
|
|
4
|
+
import { LP_PROTOCOLS, type LpPosition, type LpProtocol, type LpToken, type LpVersion } from '../lp.js'
|
|
5
|
+
import { sqrtPriceToPrice, tickToPrice } from '../lpMath.js'
|
|
6
|
+
import type { Provider } from '../provider.js'
|
|
7
|
+
import { isTronAddress, isTronChain, toEvmAddress, toTronAddress } from '../tron.js'
|
|
8
|
+
import type { LiquidityOptions, LiquidityPlan, LiquidityTx, PositionRef } from './types.js'
|
|
9
|
+
|
|
10
|
+
export const ZERO_ADDRESS = ZeroAddress
|
|
11
|
+
export { MAX_UINT128 } from '../lpMath.js'
|
|
12
|
+
const MAX_UINT160 = 2n ** 160n - 1n
|
|
13
|
+
/** Uniswap 的 Permit2(各 EVM 链上地址相同),V4 的仓位合约通过它收取代币 */
|
|
14
|
+
export const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3'
|
|
15
|
+
/** Permit2 授权的有效期:30 天(与 Uniswap 网页一致) */
|
|
16
|
+
const PERMIT2_EXPIRATION = 30 * 24 * 3600
|
|
17
|
+
|
|
18
|
+
export const erc20Interface = new Interface([
|
|
19
|
+
'function allowance(address owner, address spender) view returns (uint256)',
|
|
20
|
+
'function approve(address spender, uint256 amount) returns (bool)',
|
|
21
|
+
])
|
|
22
|
+
const permit2Interface = new Interface([
|
|
23
|
+
'function allowance(address owner, address token, address spender) view returns (uint160 amount, uint48 expiration, uint48 nonce)',
|
|
24
|
+
'function approve(address token, address spender, uint160 amount, uint48 expiration)',
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
/** 一次构建用到的公共信息 */
|
|
28
|
+
export interface Ctx {
|
|
29
|
+
provider: Provider
|
|
30
|
+
chainId: number
|
|
31
|
+
tron: boolean
|
|
32
|
+
/** 0x 格式(ABI 编码用) */
|
|
33
|
+
owner: string
|
|
34
|
+
recipient: string
|
|
35
|
+
slippage: number
|
|
36
|
+
/** 截止时间(秒级时间戳) */
|
|
37
|
+
deadline: number
|
|
38
|
+
options: LiquidityOptions
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 地址统一成 0x 的校验和格式(Tron 的 T 地址先转换) */
|
|
42
|
+
export function evm(address: string): string {
|
|
43
|
+
return getAddress(isTronAddress(address) ? toEvmAddress(address) : address)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const same = (a: string, b: string) => evm(a) === evm(b)
|
|
47
|
+
|
|
48
|
+
/** 输出的地址:Tron 上转成 T 地址 */
|
|
49
|
+
export function out(ctx: Pick<Ctx, 'tron'>, address: string): string {
|
|
50
|
+
return ctx.tron ? toTronAddress(address) : address
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function createCtx(provider: Provider, owner: string, options: LiquidityOptions): Promise<Ctx> {
|
|
54
|
+
const chainId = await provider.getChainId()
|
|
55
|
+
const slippage = options.slippage ?? 0.005
|
|
56
|
+
if (!(slippage >= 0 && slippage < 1)) {
|
|
57
|
+
throw new RangeError(`slippage must be in [0, 1): ${slippage}`)
|
|
58
|
+
}
|
|
59
|
+
const seconds = options.deadline ?? 1200
|
|
60
|
+
if (!(seconds > 0)) {
|
|
61
|
+
throw new RangeError(`deadline must be greater than 0: ${seconds}`)
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
provider,
|
|
65
|
+
chainId,
|
|
66
|
+
tron: isTronChain(chainId),
|
|
67
|
+
owner: evm(owner),
|
|
68
|
+
recipient: evm(options.recipient ?? owner),
|
|
69
|
+
slippage,
|
|
70
|
+
deadline: Math.floor(Date.now() / 1000) + Math.floor(seconds),
|
|
71
|
+
options,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 是否是主币地址(NATIVE_TOKEN / 零地址,或 Provider 配置的主币地址) */
|
|
76
|
+
export function isNative(ctx: Ctx, address: string): boolean {
|
|
77
|
+
return evm(address) === ZERO_ADDRESS || ctx.provider.isNativeToken(address) || ctx.provider.isNativeToken(evm(address))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 代币信息(symbol / 精度);零地址是 V4 的主币 */
|
|
81
|
+
export async function tokenInfo(ctx: Ctx, addresses: readonly string[]): Promise<LpToken[]> {
|
|
82
|
+
const nativeAlias = ctx.provider.isNativeToken(ZERO_ADDRESS) ? undefined : ctx.provider.nativeTokens[0]
|
|
83
|
+
const query = addresses.map((a) => (a === ZERO_ADDRESS && nativeAlias !== undefined ? nativeAlias : a))
|
|
84
|
+
const details = await ctx.provider.tokens(query, { fields: ['symbol', 'decimals'] })
|
|
85
|
+
return addresses.map((address, i) => {
|
|
86
|
+
const d = details[i]
|
|
87
|
+
return { address: out(ctx, address), native: d?.native ?? false, symbol: d?.symbol ?? null, decimals: d?.decimals ?? null }
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function fmt(value: bigint, token: LpToken): string | null {
|
|
92
|
+
return token.decimals === null ? null : formatUnits(value, token.decimals)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 解析输入的数量:amount(最小单位)和 formatted 二选一,都没给时返回 undefined */
|
|
96
|
+
export function parseAmount(raw: BigNumberish | undefined, formatted: string | undefined, token: LpToken, name: string): bigint | undefined {
|
|
97
|
+
if (raw !== undefined && formatted !== undefined) {
|
|
98
|
+
throw new Error(`Pass either ${name} or ${name}Formatted, not both`)
|
|
99
|
+
}
|
|
100
|
+
let value: bigint
|
|
101
|
+
if (formatted !== undefined) {
|
|
102
|
+
if (token.decimals === null) {
|
|
103
|
+
throw new Error(`Cannot read the decimals of ${token.address}; pass ${name} in raw units instead of ${name}Formatted`)
|
|
104
|
+
}
|
|
105
|
+
value = parseUnits(formatted, token.decimals)
|
|
106
|
+
} else if (raw !== undefined) {
|
|
107
|
+
value = getBigInt(raw)
|
|
108
|
+
} else {
|
|
109
|
+
return undefined
|
|
110
|
+
}
|
|
111
|
+
if (value < 0n) {
|
|
112
|
+
throw new RangeError(`${name} must not be negative`)
|
|
113
|
+
}
|
|
114
|
+
return value
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 按滑点向下取:value × (1 − slippage) */
|
|
118
|
+
export function minus(value: bigint, slippage: number): bigint {
|
|
119
|
+
return (value * (1_000_000n - BigInt(Math.round(slippage * 1e6)))) / 1_000_000n
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 移除的数量:percent(百分数)或 liquidity,二选一,默认全部 */
|
|
123
|
+
export function removeAmount(total: bigint, percent: number | undefined, liquidity: BigNumberish | undefined): bigint {
|
|
124
|
+
if (percent !== undefined && liquidity !== undefined) {
|
|
125
|
+
throw new Error('Pass either percent or liquidity, not both')
|
|
126
|
+
}
|
|
127
|
+
if (liquidity !== undefined) {
|
|
128
|
+
const value = getBigInt(liquidity)
|
|
129
|
+
if (value <= 0n || value > total) {
|
|
130
|
+
throw new RangeError(`liquidity must be in (0, ${total}]: ${value}`)
|
|
131
|
+
}
|
|
132
|
+
return value
|
|
133
|
+
}
|
|
134
|
+
const p = percent ?? 100
|
|
135
|
+
if (!(p > 0 && p <= 100)) {
|
|
136
|
+
throw new RangeError(`percent must be in (0, 100]: ${p}`)
|
|
137
|
+
}
|
|
138
|
+
const amount = p === 100 ? total : (total * BigInt(Math.round(p * 10_000))) / 1_000_000n
|
|
139
|
+
if (amount === 0n) {
|
|
140
|
+
throw new RangeError(`${p}% of ${total} rounds to 0`)
|
|
141
|
+
}
|
|
142
|
+
return amount
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 选出协议:自定义的优先,再找内置表 */
|
|
146
|
+
function candidates(ctx: Ctx): LpProtocol[] {
|
|
147
|
+
return [...(ctx.options.protocols ?? []).filter((p) => p.chainId === ctx.chainId), ...LP_PROTOCOLS.filter((p) => p.chainId === ctx.chainId)]
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function protocolById(ctx: Ctx, protocol: string | LpProtocol): LpProtocol {
|
|
151
|
+
if (typeof protocol !== 'string') {
|
|
152
|
+
if (protocol.chainId !== ctx.chainId) {
|
|
153
|
+
throw new Error(`Protocol ${protocol.id} is on chain ${protocol.chainId}, not ${ctx.chainId}`)
|
|
154
|
+
}
|
|
155
|
+
return protocol
|
|
156
|
+
}
|
|
157
|
+
const found = candidates(ctx).find((p) => p.id === protocol)
|
|
158
|
+
if (!found) {
|
|
159
|
+
throw new Error(`Unknown LP protocol ${protocol} on chain ${ctx.chainId}; pass an LpProtocol object (or options.protocols) for protocols that are not built in`)
|
|
160
|
+
}
|
|
161
|
+
return found
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** V3 / V4 仓位所属的协议:按 positionManager 找,没有时按协议 id */
|
|
165
|
+
export function concentratedProtocol(ctx: Ctx, ref: PositionRef | LpPosition): LpProtocol {
|
|
166
|
+
const pm = 'positionManager' in ref && ref.positionManager ? evm(ref.positionManager) : null
|
|
167
|
+
if (pm) {
|
|
168
|
+
const fromRef = ref.protocol && typeof ref.protocol !== 'string' ? [ref.protocol] : []
|
|
169
|
+
const found = [...fromRef, ...candidates(ctx)].find((p) => (p.version === 'v3' || p.version === 'v4') && p.positionManager && same(p.positionManager, pm))
|
|
170
|
+
if (!found) {
|
|
171
|
+
throw new Error(`Unknown position manager ${pm} on chain ${ctx.chainId}; pass its LpProtocol in options.protocols`)
|
|
172
|
+
}
|
|
173
|
+
return found
|
|
174
|
+
}
|
|
175
|
+
if (!ref.protocol) {
|
|
176
|
+
throw new Error('position needs positionManager or protocol')
|
|
177
|
+
}
|
|
178
|
+
const found = protocolById(ctx, ref.protocol)
|
|
179
|
+
if (found.version === 'v2') {
|
|
180
|
+
throw new Error(`${found.id} is a V2 protocol; V2 positions are identified by pool`)
|
|
181
|
+
}
|
|
182
|
+
return found
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 仓位是哪个版本:LpPosition 自带;PositionRef 有 tokenId 的看协议,否则是 V2 */
|
|
186
|
+
export function positionVersion(ctx: Ctx, ref: PositionRef | LpPosition): LpVersion {
|
|
187
|
+
if ('version' in ref && (ref.version === 'v2' || ref.version === 'v3' || ref.version === 'v4')) {
|
|
188
|
+
return ref.version
|
|
189
|
+
}
|
|
190
|
+
if (!('tokenId' in ref) || ref.tokenId === undefined) {
|
|
191
|
+
if (!ref.pool) {
|
|
192
|
+
throw new Error('position needs pool (V2) or tokenId (V3 / V4)')
|
|
193
|
+
}
|
|
194
|
+
// 传了 V3 / V4 的协议却没有 tokenId:说清楚,不要当成 V2 的池子去读
|
|
195
|
+
if (ref.protocol && (typeof ref.protocol === 'string' ? protocolById(ctx, ref.protocol) : ref.protocol).version !== 'v2') {
|
|
196
|
+
throw new Error('position.tokenId is required for V3 / V4 positions')
|
|
197
|
+
}
|
|
198
|
+
return 'v2'
|
|
199
|
+
}
|
|
200
|
+
return concentratedProtocol(ctx, ref).version
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function tokenIdOf(ref: PositionRef | LpPosition): bigint {
|
|
204
|
+
if (!('tokenId' in ref) || ref.tokenId === undefined) {
|
|
205
|
+
throw new Error('position.tokenId is required')
|
|
206
|
+
}
|
|
207
|
+
if (typeof ref.tokenId === 'number' && !Number.isSafeInteger(ref.tokenId)) {
|
|
208
|
+
throw new RangeError(`tokenId ${ref.tokenId} is not a safe integer; pass it as a string or bigint`)
|
|
209
|
+
}
|
|
210
|
+
return getBigInt(ref.tokenId)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 要求先把额度归零才能改成另一个非零值的代币(chainId:小写地址)。只对它们先 approve 0:
|
|
215
|
+
* 有的代币(如 Ethereum 上的 BNB)反过来不允许 approve 0
|
|
216
|
+
*/
|
|
217
|
+
const RESET_FIRST = new Set([
|
|
218
|
+
'1:0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
|
|
219
|
+
'1:0xdd974d5c2e2928dea5f71b9825b8b646686bd200', // KNC(旧版)
|
|
220
|
+
])
|
|
221
|
+
|
|
222
|
+
/** ERC20 授权:额度不够时 approve(USDT 等已有非零额度时先 approve 0)。amount 为 0 的不需要 */
|
|
223
|
+
export async function approvals(ctx: Ctx, items: ReadonlyArray<{ token: string; spender: string; amount: bigint }>): Promise<LiquidityTx[]> {
|
|
224
|
+
const needed = items.filter((i) => i.amount > 0n)
|
|
225
|
+
if (!needed.length) {
|
|
226
|
+
return []
|
|
227
|
+
}
|
|
228
|
+
const current = await ctx.provider.all(needed.map((i) => ctx.provider.contract(i.token, erc20Interface).allowance(ctx.owner, i.spender)))
|
|
229
|
+
return needed.flatMap((item, k): LiquidityTx[] => {
|
|
230
|
+
const allowance = (current as bigint[])[k] as bigint
|
|
231
|
+
if (allowance >= item.amount) {
|
|
232
|
+
return []
|
|
233
|
+
}
|
|
234
|
+
const amount = ctx.options.approve === 'max' ? MaxUint256 : item.amount
|
|
235
|
+
const tx = (value: bigint): LiquidityTx => ({
|
|
236
|
+
kind: 'approve',
|
|
237
|
+
to: out(ctx, item.token),
|
|
238
|
+
data: erc20Interface.encodeFunctionData('approve', [item.spender, value]),
|
|
239
|
+
value: '0',
|
|
240
|
+
token: out(ctx, item.token),
|
|
241
|
+
spender: out(ctx, item.spender),
|
|
242
|
+
amount: value.toString(),
|
|
243
|
+
})
|
|
244
|
+
return allowance > 0n && RESET_FIRST.has(`${ctx.chainId}:${item.token.toLowerCase()}`) ? [tx(0n), tx(amount)] : [tx(amount)]
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** V4:代币先授权给 Permit2,再在 Permit2 里授权给仓位合约(额度不够或已过期时) */
|
|
249
|
+
export async function permit2Approvals(ctx: Ctx, spender: string, items: ReadonlyArray<{ token: string; amount: bigint }>): Promise<LiquidityTx[]> {
|
|
250
|
+
const needed = items.filter((i) => i.amount > 0n)
|
|
251
|
+
if (!needed.length) {
|
|
252
|
+
return []
|
|
253
|
+
}
|
|
254
|
+
const erc20 = await approvals(
|
|
255
|
+
ctx,
|
|
256
|
+
needed.map((i) => ({ token: i.token, spender: PERMIT2_ADDRESS, amount: i.amount })),
|
|
257
|
+
)
|
|
258
|
+
const permits = (await ctx.provider.all(needed.map((i) => ctx.provider.contract(PERMIT2_ADDRESS, permit2Interface).allowance(ctx.owner, i.token, spender)))) as Array<{ 0: bigint; 1: bigint }>
|
|
259
|
+
const now = Math.floor(Date.now() / 1000)
|
|
260
|
+
const permit2 = needed.flatMap((item, k): LiquidityTx[] => {
|
|
261
|
+
const [amount, expiration] = [permits[k]?.[0] ?? 0n, Number(permits[k]?.[1] ?? 0n)]
|
|
262
|
+
// 额度够、并且在交易截止时间之后才过期:不用再授权
|
|
263
|
+
if (amount >= item.amount && expiration >= ctx.deadline) {
|
|
264
|
+
return []
|
|
265
|
+
}
|
|
266
|
+
// 过期重新授权时保留原来更大的额度(如之前授权过无限额度)
|
|
267
|
+
const value = ctx.options.approve === 'max' ? MAX_UINT160 : amount > item.amount ? amount : item.amount
|
|
268
|
+
return [
|
|
269
|
+
{
|
|
270
|
+
kind: 'permit2',
|
|
271
|
+
to: PERMIT2_ADDRESS,
|
|
272
|
+
data: permit2Interface.encodeFunctionData('approve', [item.token, spender, value, Math.max(now, ctx.deadline) + PERMIT2_EXPIRATION]),
|
|
273
|
+
value: '0',
|
|
274
|
+
token: item.token,
|
|
275
|
+
spender,
|
|
276
|
+
amount: value.toString(),
|
|
277
|
+
},
|
|
278
|
+
]
|
|
279
|
+
})
|
|
280
|
+
return [...erc20, ...permit2]
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 计划里的价格字段(token1 / token0,按精度换算) */
|
|
284
|
+
export function priceFields(token0: LpToken, token1: LpToken, sqrtPriceX96: bigint | null, tickLower: number, tickUpper: number) {
|
|
285
|
+
if (token0.decimals === null || token1.decimals === null) {
|
|
286
|
+
return { price: null, priceLower: null, priceUpper: null }
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
price: sqrtPriceX96 ? sqrtPriceToPrice(sqrtPriceX96, token0.decimals, token1.decimals) : null,
|
|
290
|
+
priceLower: tickToPrice(tickLower, token0.decimals, token1.decimals),
|
|
291
|
+
priceUpper: tickToPrice(tickUpper, token0.decimals, token1.decimals),
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
type PlanInput = Omit<LiquidityPlan, 'chainId' | 'owner' | 'recipient' | 'deadline' | 'amount0Formatted' | 'amount1Formatted' | 'fees0Formatted' | 'fees1Formatted' | 'pool'> & {
|
|
296
|
+
pool: string | null
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** 补上公共字段、格式化数量 */
|
|
300
|
+
export function makePlan(ctx: Ctx, input: PlanInput): LiquidityPlan {
|
|
301
|
+
const { token0, token1 } = input
|
|
302
|
+
return {
|
|
303
|
+
...input,
|
|
304
|
+
chainId: ctx.chainId,
|
|
305
|
+
pool: input.pool === null || input.version === 'v4' ? input.pool : out(ctx, input.pool),
|
|
306
|
+
owner: out(ctx, ctx.owner),
|
|
307
|
+
recipient: out(ctx, ctx.recipient),
|
|
308
|
+
amount0Formatted: fmt(BigInt(input.amount0), token0),
|
|
309
|
+
amount1Formatted: fmt(BigInt(input.amount1), token1),
|
|
310
|
+
fees0Formatted: fmt(BigInt(input.fees0), token0),
|
|
311
|
+
fees1Formatted: fmt(BigInt(input.fees1), token1),
|
|
312
|
+
deadline: ctx.deadline,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { LpToken } from '../lp.js'
|
|
2
|
+
import {
|
|
3
|
+
MAX_TICK,
|
|
4
|
+
MIN_TICK,
|
|
5
|
+
clampedTickAtSqrtRatio,
|
|
6
|
+
getLiquidityForAmount0,
|
|
7
|
+
getLiquidityForAmount1,
|
|
8
|
+
getLiquidityForAmounts,
|
|
9
|
+
getMintAmounts,
|
|
10
|
+
getSqrtRatioAtTick,
|
|
11
|
+
nearestUsableTick,
|
|
12
|
+
parseDecimal,
|
|
13
|
+
ratioToSqrtPriceX96,
|
|
14
|
+
sqrtPriceBounds,
|
|
15
|
+
toUint128,
|
|
16
|
+
} from '../lpMath.js'
|
|
17
|
+
import type { NewPositionParams } from './types.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 价格(tokenB / tokenA,按精度换算)→ 池子的 sqrtPriceX96(token1 / token0)。aIs0:tokenA 是不是 token0
|
|
21
|
+
*/
|
|
22
|
+
export function sqrtPriceOf(price: string | number, aIs0: boolean, token0: LpToken, token1: LpToken, name: string): bigint {
|
|
23
|
+
if (token0.decimals === null || token1.decimals === null) {
|
|
24
|
+
throw new Error(`Cannot read token decimals to convert ${name}; pass ticks instead`)
|
|
25
|
+
}
|
|
26
|
+
const { num, den } = parseDecimal(price)
|
|
27
|
+
if (num === 0n) {
|
|
28
|
+
throw new RangeError(`${name} must be greater than 0`)
|
|
29
|
+
}
|
|
30
|
+
// tokenA 是 token1 时取倒数
|
|
31
|
+
return aIs0 ? ratioToSqrtPriceX96(num, den, token0.decimals, token1.decimals) : ratioToSqrtPriceX96(den, num, token0.decimals, token1.decimals)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 新建仓位的 tick 区间:fullRange / tickLower + tickUpper / priceLower + priceUpper 三选一 */
|
|
35
|
+
export function resolveRange(params: NewPositionParams, aIs0: boolean, token0: LpToken, token1: LpToken, tickSpacing: number): [number, number] {
|
|
36
|
+
const byTicks = params.tickLower !== undefined || params.tickUpper !== undefined
|
|
37
|
+
const byPrices = params.priceLower !== undefined || params.priceUpper !== undefined
|
|
38
|
+
if (Number(!!params.fullRange) + Number(byTicks) + Number(byPrices) !== 1) {
|
|
39
|
+
throw new Error('Pass exactly one of fullRange, tickLower + tickUpper, or priceLower + priceUpper')
|
|
40
|
+
}
|
|
41
|
+
let lower: number
|
|
42
|
+
let upper: number
|
|
43
|
+
if (params.fullRange) {
|
|
44
|
+
lower = Math.ceil(MIN_TICK / tickSpacing) * tickSpacing
|
|
45
|
+
upper = Math.floor(MAX_TICK / tickSpacing) * tickSpacing
|
|
46
|
+
} else if (byTicks) {
|
|
47
|
+
if (params.tickLower === undefined || params.tickUpper === undefined) {
|
|
48
|
+
throw new Error('Pass both tickLower and tickUpper')
|
|
49
|
+
}
|
|
50
|
+
lower = params.tickLower
|
|
51
|
+
upper = params.tickUpper
|
|
52
|
+
for (const tick of [lower, upper]) {
|
|
53
|
+
if (!Number.isInteger(tick) || tick < MIN_TICK || tick > MAX_TICK || tick % tickSpacing !== 0) {
|
|
54
|
+
throw new RangeError(`tick ${tick} must be an integer multiple of tickSpacing ${tickSpacing} within [${MIN_TICK}, ${MAX_TICK}]`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
if (params.priceLower === undefined || params.priceUpper === undefined) {
|
|
59
|
+
throw new Error('Pass both priceLower and priceUpper')
|
|
60
|
+
}
|
|
61
|
+
// tokenA 是 token1 时价格取倒数,区间两端也随之对调;下面按大小排序
|
|
62
|
+
const ticks = [params.priceLower, params.priceUpper].map((price, i) => {
|
|
63
|
+
return nearestUsableTick(clampedTickAtSqrtRatio(sqrtPriceOf(price, aIs0, token0, token1, i ? 'priceUpper' : 'priceLower')), tickSpacing)
|
|
64
|
+
}) as [number, number]
|
|
65
|
+
;[lower, upper] = ticks[0] <= ticks[1] ? ticks : [ticks[1], ticks[0]]
|
|
66
|
+
}
|
|
67
|
+
if (lower >= upper) {
|
|
68
|
+
throw new RangeError(`The price range is too narrow: tickLower ${lower} must be below tickUpper ${upper} (tickSpacing ${tickSpacing})`)
|
|
69
|
+
}
|
|
70
|
+
return [lower, upper]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 按输入的数量算添加的流动性:两边都给时取较少的一边;只给一边时另一边按当前价格算。
|
|
75
|
+
* 价格在区间外时只需要一种代币,给错了另一种会报错
|
|
76
|
+
*/
|
|
77
|
+
export function liquidityFor(sqrtPrice: bigint, tickLower: number, tickUpper: number, amount0: bigint | undefined, amount1: bigint | undefined, token0: LpToken, token1: LpToken): bigint {
|
|
78
|
+
const sqrtA = getSqrtRatioAtTick(tickLower)
|
|
79
|
+
const sqrtB = getSqrtRatioAtTick(tickUpper)
|
|
80
|
+
const name = (t: LpToken) => t.symbol ?? t.address
|
|
81
|
+
if (amount0 === undefined && amount1 === undefined) {
|
|
82
|
+
throw new Error('Pass the amount of at least one token')
|
|
83
|
+
}
|
|
84
|
+
// 价格在区间外时只需要一种代币:给的是另一种(或需要的那种是 0)时说明要给哪种
|
|
85
|
+
if (sqrtPrice >= sqrtB && !amount1) {
|
|
86
|
+
throw new Error(`The current price is above the range: the position takes only ${name(token1)} (token1); pass its amount instead`)
|
|
87
|
+
}
|
|
88
|
+
if (sqrtPrice <= sqrtA && !amount0) {
|
|
89
|
+
throw new Error(`The current price is below the range: the position takes only ${name(token0)} (token0); pass its amount instead`)
|
|
90
|
+
}
|
|
91
|
+
let liquidity: bigint
|
|
92
|
+
if (amount0 !== undefined && amount1 !== undefined) {
|
|
93
|
+
liquidity = getLiquidityForAmounts(sqrtPrice, sqrtA, sqrtB, amount0, amount1)
|
|
94
|
+
} else if (amount0 !== undefined) {
|
|
95
|
+
liquidity = getLiquidityForAmount0(sqrtPrice > sqrtA ? sqrtPrice : sqrtA, sqrtB, amount0)
|
|
96
|
+
} else {
|
|
97
|
+
liquidity = getLiquidityForAmount1(sqrtA, sqrtPrice < sqrtB ? sqrtPrice : sqrtB, amount1 as bigint)
|
|
98
|
+
}
|
|
99
|
+
if (liquidity === 0n) {
|
|
100
|
+
throw new Error('Amount too small: the liquidity rounds to 0')
|
|
101
|
+
}
|
|
102
|
+
return toUint128(liquidity)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* V3:按传给仓位合约的数量(desired),价格在滑点范围内变动时至少会用掉多少(amountMin)。
|
|
107
|
+
* 合约按当时的价格重新算流动性:token0 在价格最高时用得最少,token1 在最低时;取三个价格下的最小值
|
|
108
|
+
*/
|
|
109
|
+
export function v3MinAmounts(sqrtPrice: bigint, tickLower: number, tickUpper: number, desired0: bigint, desired1: bigint, slippage: number): [bigint, bigint] {
|
|
110
|
+
const sqrtA = getSqrtRatioAtTick(tickLower)
|
|
111
|
+
const sqrtB = getSqrtRatioAtTick(tickUpper)
|
|
112
|
+
const [lower, upper] = sqrtPriceBounds(sqrtPrice, slippage)
|
|
113
|
+
let min0: bigint | null = null
|
|
114
|
+
let min1: bigint | null = null
|
|
115
|
+
for (const s of [lower, sqrtPrice, upper]) {
|
|
116
|
+
const [a0, a1] = getMintAmounts(s, sqrtA, sqrtB, getLiquidityForAmounts(s, sqrtA, sqrtB, desired0, desired1))
|
|
117
|
+
min0 = min0 === null || a0 < min0 ? a0 : min0
|
|
118
|
+
min1 = min1 === null || a1 < min1 ? a1 : min1
|
|
119
|
+
}
|
|
120
|
+
return [min0 as bigint, min1 as bigint]
|
|
121
|
+
}
|