@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/price.ts
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 价格(子路径 `@w3lib/evm-batch-call/price`):DefiLlama(当前 / 历史)、链上池子价格(V2 / V3 池子,按流动性选)、
|
|
3
|
+
* 组合多个价格源、按地址算资产总值。价格只用于展示和估值,不要用来计算交易的最小数量
|
|
4
|
+
*/
|
|
5
|
+
import type { CallOverrides } from './aggregate.js'
|
|
6
|
+
import type { Call } from './call.js'
|
|
7
|
+
import { formatUnits } from './erc20.js'
|
|
8
|
+
import { fetchJson } from './http.js'
|
|
9
|
+
import { LP_PROTOCOLS, LP_QUOTE_TOKENS, type LpProtocol } from './lp.js'
|
|
10
|
+
import { sqrtPriceToPrice } from './lpMath.js'
|
|
11
|
+
import { getNativeCurrency } from './chains.js'
|
|
12
|
+
import type { Provider } from './provider.js'
|
|
13
|
+
import { TRON_CHAIN_ID, isTronChain, toTronAddress } from './tron.js'
|
|
14
|
+
|
|
15
|
+
export interface PriceSourceContext {
|
|
16
|
+
chainId: number
|
|
17
|
+
/** 代币地址(不含主币) */
|
|
18
|
+
tokens: readonly string[]
|
|
19
|
+
/** 主币 symbol(需要主币价格时);不需要时为 null */
|
|
20
|
+
nativeSymbol: string | null
|
|
21
|
+
fetch: typeof fetch
|
|
22
|
+
/** 查询用的 Provider(链上价格源 onchainPrices 用它读池子) */
|
|
23
|
+
provider?: Provider
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PriceSource {
|
|
27
|
+
readonly name: string
|
|
28
|
+
/** 返回 去重键 → 美元价格:EVM 是小写 0x 地址;Tron 是 T 开头的 base58 地址(区分大小写);主币用键 'native'。没有价格的不返回 */
|
|
29
|
+
prices(ctx: PriceSourceContext): Promise<Map<string, number>>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** DefiLlama 的链标识:chainId → slug(2026-10 实测可用) */
|
|
33
|
+
export const DEFILLAMA_CHAINS: Readonly<Record<number, string>> = {
|
|
34
|
+
1: 'ethereum',
|
|
35
|
+
10: 'optimism',
|
|
36
|
+
56: 'bsc',
|
|
37
|
+
130: 'unichain',
|
|
38
|
+
137: 'polygon',
|
|
39
|
+
143: 'monad',
|
|
40
|
+
196: 'xlayer',
|
|
41
|
+
204: 'op_bnb',
|
|
42
|
+
324: 'era',
|
|
43
|
+
999: 'hyperliquid',
|
|
44
|
+
1116: 'core',
|
|
45
|
+
2818: 'morph',
|
|
46
|
+
5000: 'mantle',
|
|
47
|
+
8453: 'base',
|
|
48
|
+
9745: 'plasma',
|
|
49
|
+
34443: 'mode',
|
|
50
|
+
42161: 'arbitrum',
|
|
51
|
+
43114: 'avax',
|
|
52
|
+
57073: 'ink',
|
|
53
|
+
59144: 'linea',
|
|
54
|
+
80094: 'berachain',
|
|
55
|
+
81457: 'blast',
|
|
56
|
+
534352: 'scroll',
|
|
57
|
+
[TRON_CHAIN_ID.mainnet]: 'tron',
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 主币价格:按主币 symbol 对应 CoinGecko 的币种 id(DefiLlama 用 coingecko:<id> 查) */
|
|
61
|
+
const NATIVE_PRICE_IDS: Readonly<Record<string, string>> = {
|
|
62
|
+
ETH: 'ethereum',
|
|
63
|
+
BNB: 'binancecoin',
|
|
64
|
+
POL: 'polygon-ecosystem-token',
|
|
65
|
+
AVAX: 'avalanche-2',
|
|
66
|
+
MNT: 'mantle',
|
|
67
|
+
OKB: 'okb',
|
|
68
|
+
HYPE: 'hyperliquid',
|
|
69
|
+
BERA: 'berachain-bera',
|
|
70
|
+
CORE: 'coredaoorg',
|
|
71
|
+
MON: 'monad',
|
|
72
|
+
XPL: 'plasma',
|
|
73
|
+
TRX: 'tron',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface DefillamaPriceOptions {
|
|
77
|
+
/** 置信度下限(0~1),DefiLlama 给出的置信度低于它视为没有价格,默认 0.9 */
|
|
78
|
+
minConfidence?: number
|
|
79
|
+
/** 历史价格:这个时间点(unix 秒或 Date)附近的价格;不传为当前价格 */
|
|
80
|
+
at?: number | Date
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* DefiLlama 价格(免费、不需要 Key)。
|
|
85
|
+
* 注意:流动性很差的代币价格可能虚高,展示总资产时建议配合 minUsd 或自己的价格源。
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* defillamaPrices() // 当前价格
|
|
89
|
+
* defillamaPrices({ at: new Date('2026-01-01') }) // 历史价格
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function defillamaPrices(options: DefillamaPriceOptions = {}): PriceSource {
|
|
93
|
+
const minConfidence = options.minConfidence ?? 0.9
|
|
94
|
+
const at = options.at instanceof Date ? Math.floor(options.at.getTime() / 1000) : options.at
|
|
95
|
+
const endpoint = at === undefined ? 'https://coins.llama.fi/prices/current' : `https://coins.llama.fi/prices/historical/${Math.floor(at)}`
|
|
96
|
+
return {
|
|
97
|
+
name: at === undefined ? 'defillama' : `defillama@${Math.floor(at)}`,
|
|
98
|
+
async prices({ chainId, tokens, nativeSymbol, fetch: fetchFn }) {
|
|
99
|
+
const result = new Map<string, number>()
|
|
100
|
+
const slug = DEFILLAMA_CHAINS[chainId]
|
|
101
|
+
const nativeId = nativeSymbol ? NATIVE_PRICE_IDS[nativeSymbol.toUpperCase()] : undefined
|
|
102
|
+
// 查询键 → 结果里用的去重键:EVM 地址小写;Tron 地址区分大小写,DefiLlama 要用原样的 base58(T…)地址
|
|
103
|
+
const resultKey = new Map<string, string>()
|
|
104
|
+
const tokenKeys = slug
|
|
105
|
+
? tokens.map((t) => {
|
|
106
|
+
const tron = slug === 'tron'
|
|
107
|
+
const query = `${slug}:${tron ? toTronAddress(t) : t.toLowerCase()}`
|
|
108
|
+
resultKey.set(query.toLowerCase(), tron ? toTronAddress(t) : t.toLowerCase())
|
|
109
|
+
return query
|
|
110
|
+
})
|
|
111
|
+
: []
|
|
112
|
+
const keys = [...(nativeId ? [`coingecko:${nativeId}`] : []), ...tokenKeys]
|
|
113
|
+
const batches: string[][] = []
|
|
114
|
+
for (let i = 0; i < keys.length; i += 100) {
|
|
115
|
+
batches.push(keys.slice(i, i + 100))
|
|
116
|
+
}
|
|
117
|
+
const responses = await Promise.all(
|
|
118
|
+
batches.map((batch) => fetchJson<{ coins?: Record<string, { price?: number; confidence?: number }> }>(fetchFn, `${endpoint}/${batch.join(',')}`)),
|
|
119
|
+
)
|
|
120
|
+
for (const res of responses) {
|
|
121
|
+
for (const [key, coin] of Object.entries(res.coins ?? {})) {
|
|
122
|
+
if (typeof coin.price !== 'number' || !Number.isFinite(coin.price) || (coin.confidence !== undefined && coin.confidence < minConfidence)) {
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
result.set(key.startsWith('coingecko:') ? 'native' : (resultKey.get(key.toLowerCase()) ?? key.slice(key.indexOf(':') + 1).toLowerCase()), coin.price)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// 组合
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 依次使用多个价格源:前面的源没给出价格的代币(和主币)交给下一个源。某个源出错时跳过它(错误交给 onError)。
|
|
140
|
+
*
|
|
141
|
+
* ```ts
|
|
142
|
+
* combinePrices([defillamaPrices(), onchainPrices()])
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export function combinePrices(sources: readonly PriceSource[], options: { onError?: (source: PriceSource, err: unknown) => void } = {}): PriceSource {
|
|
146
|
+
return {
|
|
147
|
+
name: sources.map((s) => s.name).join('+'),
|
|
148
|
+
async prices(ctx) {
|
|
149
|
+
const found = await runSources(sources, ctx, options.onError)
|
|
150
|
+
return new Map([...found].map(([k, v]) => [k, v.price]))
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 依次查询,记下每个价格来自哪个源 */
|
|
156
|
+
async function runSources(sources: readonly PriceSource[], ctx: PriceSourceContext, onError?: (source: PriceSource, err: unknown) => void): Promise<Map<string, { price: number; source: string }>> {
|
|
157
|
+
const result = new Map<string, { price: number; source: string }>()
|
|
158
|
+
let tokens = [...ctx.tokens]
|
|
159
|
+
let nativeSymbol = ctx.nativeSymbol
|
|
160
|
+
for (const source of sources) {
|
|
161
|
+
if (!tokens.length && !nativeSymbol) break
|
|
162
|
+
let found: Map<string, number>
|
|
163
|
+
try {
|
|
164
|
+
found = await source.prices({ ...ctx, tokens, nativeSymbol })
|
|
165
|
+
} catch (err) {
|
|
166
|
+
onError?.(source, err)
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
for (const [key, price] of found) if (!result.has(key) && Number.isFinite(price) && price > 0) result.set(key, { price, source: source.name })
|
|
170
|
+
tokens = tokens.filter((t) => !result.has(priceKeyOf(ctx.chainId, t)))
|
|
171
|
+
if (result.has('native')) nativeSymbol = null
|
|
172
|
+
}
|
|
173
|
+
return result
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 价格结果里的键:EVM 小写 0x 地址;Tron 是 T 开头的 base58 地址;主币为 'native' */
|
|
177
|
+
export function priceKeyOf(chainId: number, token: string): string {
|
|
178
|
+
return isTronChain(chainId) ? toTronAddress(token) : token.toLowerCase()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// 链上价格
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/** 按 1 美元计的稳定币(链上价格的计价锚点之一;另一个是包装主币) */
|
|
186
|
+
export const STABLECOINS: Readonly<Record<number, readonly string[]>> = {
|
|
187
|
+
1: ['0xdAC17F958D2ee523a2206206994597C13D831ec7', '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', '0x6B175474E89094C44Da98b954EedeAC495271d0F'],
|
|
188
|
+
56: ['0x55d398326f99059fF775485246999027B3197955', '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d'],
|
|
189
|
+
8453: ['0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'],
|
|
190
|
+
42161: ['0xaf88d065e77c8cC2239327C5EDb3A432268e5831', '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9'],
|
|
191
|
+
10: ['0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', '0x94b008aA00579c1307B0EF2c499aD98a8ce58e58'],
|
|
192
|
+
137: ['0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', '0xc2132D05D31c914a87C6611C10748AEb04B58e8F'],
|
|
193
|
+
43114: ['0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E'],
|
|
194
|
+
[TRON_CHAIN_ID.mainnet]: ['TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'],
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface OnchainPriceOptions {
|
|
198
|
+
/** 池子里计价币一侧的价值(美元)下限,低于它的池子不用(防止小池子的价格被操纵),默认 10000 */
|
|
199
|
+
minLiquidityUsd?: number
|
|
200
|
+
/** 包装主币(计价锚点,主币的价格也按它),默认 LP_QUOTE_TOKENS 里这条链的第一个 */
|
|
201
|
+
wrappedNative?: string
|
|
202
|
+
/** 按 1 美元计的稳定币,默认 STABLECOINS 里这条链的 */
|
|
203
|
+
stablecoins?: readonly string[]
|
|
204
|
+
/** 用哪些协议的池子,默认 LP_PROTOCOLS 里这条链的 V2 和 V3 */
|
|
205
|
+
protocols?: readonly LpProtocol[]
|
|
206
|
+
/** V3 池子的手续费档位,默认 [100, 500, 2500, 3000, 10000] */
|
|
207
|
+
fees?: readonly number[]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const ERC20_ABI = ['function decimals() view returns (uint8)', 'function balanceOf(address owner) view returns (uint256)']
|
|
211
|
+
const V2_FACTORY_ABI = ['function getPair(address tokenA, address tokenB) view returns (address)']
|
|
212
|
+
const V2_PAIR_ABI = ['function token0() view returns (address)', 'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)']
|
|
213
|
+
const V3_FACTORY_ABI = ['function getPool(address tokenA, address tokenB, uint24 fee) view returns (address)']
|
|
214
|
+
const V3_POOL_ABI = ['function token0() view returns (address)', 'function slot0() view returns (uint160 sqrtPriceX96, int24 tick)', 'function liquidity() view returns (uint128)']
|
|
215
|
+
const ZERO = '0x0000000000000000000000000000000000000000'
|
|
216
|
+
|
|
217
|
+
interface PoolQuote {
|
|
218
|
+
/** 1 个代币值多少个计价币 */
|
|
219
|
+
price: number
|
|
220
|
+
/** 池子里计价币的数量(按精度换算) */
|
|
221
|
+
depth: number
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 链上价格:在 V2 / V3 池子里找代币对包装主币、稳定币的池子,用计价币一侧最深的池子的价格。
|
|
226
|
+
* 稳定币按 1 美元;包装主币(和主币)按它对稳定币最深的池子。读取全部用 multicall 合并。
|
|
227
|
+
* 适合 DefiLlama 没有收录的新币;价格是池子当前的价格,可能被单个区块内的交易影响,只用于展示
|
|
228
|
+
*/
|
|
229
|
+
export function onchainPrices(options: OnchainPriceOptions = {}): PriceSource {
|
|
230
|
+
return {
|
|
231
|
+
name: 'onchain',
|
|
232
|
+
async prices({ chainId, tokens, nativeSymbol, provider }) {
|
|
233
|
+
if (!provider) throw new Error('onchainPrices needs a Provider (ctx.provider)')
|
|
234
|
+
const key = (t: string) => priceKeyOf(chainId, t)
|
|
235
|
+
const wrapped = options.wrappedNative ?? LP_QUOTE_TOKENS[chainId]?.[0]
|
|
236
|
+
const stables = options.stablecoins ?? STABLECOINS[chainId] ?? []
|
|
237
|
+
const protocols = (options.protocols ?? LP_PROTOCOLS.filter((p) => p.chainId === chainId)).filter((p) => (p.version === 'v2' || p.version === 'v3') && p.factory)
|
|
238
|
+
const minLiquidity = options.minLiquidityUsd ?? 10_000
|
|
239
|
+
const result = new Map<string, number>()
|
|
240
|
+
const stableKeys = new Set(stables.map(key))
|
|
241
|
+
for (const t of tokens) if (stableKeys.has(key(t))) result.set(key(t), 1)
|
|
242
|
+
if (!stables.length || !protocols.length) return result
|
|
243
|
+
|
|
244
|
+
// 包装主币先对稳定币定价,其他代币再对包装主币和稳定币定价(两轮都用一次 multicall 找池子 + 一次读池子)
|
|
245
|
+
const anchorsUsd = new Map(stables.map((s) => [key(s), 1]))
|
|
246
|
+
const needWrapped = wrapped && (nativeSymbol || tokens.some((t) => key(t) === key(wrapped)) || tokens.some((t) => !stableKeys.has(key(t))))
|
|
247
|
+
if (wrapped && needWrapped) {
|
|
248
|
+
const [quote] = await bestQuotes(provider, [wrapped], stables, anchorsUsd, protocols, options.fees, minLiquidity)
|
|
249
|
+
if (quote) {
|
|
250
|
+
anchorsUsd.set(key(wrapped), quote)
|
|
251
|
+
if (tokens.some((t) => key(t) === key(wrapped))) result.set(key(wrapped), quote)
|
|
252
|
+
if (nativeSymbol) result.set('native', quote)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const rest = [...new Set(tokens.map(key))].filter((k) => !result.has(k)).map((k) => tokens.find((t) => key(t) === k) as string)
|
|
256
|
+
if (!rest.length) return result
|
|
257
|
+
const anchors = [...(wrapped && anchorsUsd.has(key(wrapped)) ? [wrapped] : []), ...stables]
|
|
258
|
+
const quotes = await bestQuotes(provider, rest, anchors, anchorsUsd, protocols, options.fees, minLiquidity)
|
|
259
|
+
rest.forEach((t, i) => {
|
|
260
|
+
const q = quotes[i]
|
|
261
|
+
if (q !== null && q !== undefined) result.set(key(t), q)
|
|
262
|
+
})
|
|
263
|
+
return result
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** 每个代币在 anchors 上最深的池子给出的美元价格(池子深度不够时为 null) */
|
|
269
|
+
async function bestQuotes(
|
|
270
|
+
provider: Provider,
|
|
271
|
+
tokens: readonly string[],
|
|
272
|
+
anchors: readonly string[],
|
|
273
|
+
anchorsUsd: ReadonlyMap<string, number>,
|
|
274
|
+
protocols: readonly LpProtocol[],
|
|
275
|
+
fees: readonly number[] = [100, 500, 2500, 3000, 10000],
|
|
276
|
+
minLiquidityUsd: number,
|
|
277
|
+
overrides?: CallOverrides,
|
|
278
|
+
): Promise<Array<number | null>> {
|
|
279
|
+
const chainId = await provider.getChainId()
|
|
280
|
+
const key = (t: string) => priceKeyOf(chainId, t)
|
|
281
|
+
// 1. 找池子:代币 × 计价币 × (V3 的每个手续费档位 + V2)
|
|
282
|
+
const lookups: Array<{ token: number; anchor: string; v3: boolean }> = []
|
|
283
|
+
const calls: Call[] = []
|
|
284
|
+
tokens.forEach((token, i) => {
|
|
285
|
+
for (const anchor of anchors) {
|
|
286
|
+
if (key(anchor) === key(token)) continue
|
|
287
|
+
for (const p of protocols) {
|
|
288
|
+
const factory = p.factory as string
|
|
289
|
+
if (p.version === 'v2') {
|
|
290
|
+
lookups.push({ token: i, anchor, v3: false })
|
|
291
|
+
calls.push(provider.contract(factory, V2_FACTORY_ABI).getPair(token, anchor))
|
|
292
|
+
} else {
|
|
293
|
+
for (const fee of fees) {
|
|
294
|
+
lookups.push({ token: i, anchor, v3: true })
|
|
295
|
+
calls.push(provider.contract(factory, V3_FACTORY_ABI).getPool(token, anchor, fee))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
const addresses = await provider.tryAll<string>(calls, overrides)
|
|
302
|
+
const pools = lookups.flatMap((l, i) => {
|
|
303
|
+
const address = addresses[i]
|
|
304
|
+
return address && address.toLowerCase() !== ZERO ? [{ ...l, address }] : []
|
|
305
|
+
})
|
|
306
|
+
if (!pools.length) return tokens.map(() => null)
|
|
307
|
+
|
|
308
|
+
// 2. 读池子(token0 和价格 / 储备、计价币在池子里的余额)和精度
|
|
309
|
+
const decimalsOf = [...new Set([...tokens, ...anchors].map(key))]
|
|
310
|
+
const original = new Map([...tokens, ...anchors].map((t) => [key(t), t]))
|
|
311
|
+
const state = await provider.tryAll(
|
|
312
|
+
[
|
|
313
|
+
...decimalsOf.map((k) => provider.contract(original.get(k) as string, ERC20_ABI).decimals()),
|
|
314
|
+
...pools.flatMap((p) =>
|
|
315
|
+
p.v3
|
|
316
|
+
? [
|
|
317
|
+
provider.contract(p.address, V3_POOL_ABI).token0(),
|
|
318
|
+
provider.contract(p.address, V3_POOL_ABI).slot0(),
|
|
319
|
+
provider.contract(p.anchor, ERC20_ABI).balanceOf(p.address),
|
|
320
|
+
provider.contract(p.address, V3_POOL_ABI).liquidity(),
|
|
321
|
+
]
|
|
322
|
+
: [provider.contract(p.address, V2_PAIR_ABI).token0(), provider.contract(p.address, V2_PAIR_ABI).getReserves()],
|
|
323
|
+
),
|
|
324
|
+
],
|
|
325
|
+
overrides,
|
|
326
|
+
)
|
|
327
|
+
const decimals = new Map(decimalsOf.map((k, i) => [k, state[i] === null || state[i] === undefined ? null : Number(state[i])]))
|
|
328
|
+
const best: Array<{ usd: number; depthUsd: number } | null> = tokens.map(() => null)
|
|
329
|
+
let offset = decimalsOf.length
|
|
330
|
+
for (const p of pools) {
|
|
331
|
+
const size = p.v3 ? 4 : 2
|
|
332
|
+
const [token0, second, third, liquidity] = state.slice(offset, offset + size) as [string | null, { 0: bigint; 1: bigint } | null, bigint | null, bigint | null]
|
|
333
|
+
offset += size
|
|
334
|
+
const token = tokens[p.token] as string
|
|
335
|
+
const dt = decimals.get(key(token))
|
|
336
|
+
const da = decimals.get(key(p.anchor))
|
|
337
|
+
const anchorUsd = anchorsUsd.get(key(p.anchor))
|
|
338
|
+
if (!token0 || !second || dt === null || dt === undefined || da === null || da === undefined || anchorUsd === undefined) continue
|
|
339
|
+
const tokenIs0 = key(token0) === key(token)
|
|
340
|
+
const q = p.v3 ? v3Quote(second[0], third, liquidity ?? null, tokenIs0, dt, da) : v2Quote(second[0], second[1], tokenIs0, dt, da)
|
|
341
|
+
if (!q || !Number.isFinite(q.price) || q.price <= 0) continue
|
|
342
|
+
const depthUsd = q.depth * anchorUsd
|
|
343
|
+
const current = best[p.token]
|
|
344
|
+
if (depthUsd >= minLiquidityUsd && (!current || depthUsd > current.depthUsd)) best[p.token] = { usd: q.price * anchorUsd, depthUsd }
|
|
345
|
+
}
|
|
346
|
+
return best.map((b) => (b ? b.usd : null))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* V3 的深度:池子里计价币的余额和当前价格附近的流动性折算的计价币数量(虚拟储备:计价币是 token1 时 L·√P,是 token0 时 L / √P)取较小的。
|
|
351
|
+
* 只看余额的话,在远离当前价格的区间放一笔计价币、再把价格推过没有流动性的区间,就能用很少的成本让池子“看起来很深”
|
|
352
|
+
*/
|
|
353
|
+
function v3Quote(sqrtPriceX96: bigint, anchorBalance: bigint | null, liquidity: bigint | null, tokenIs0: boolean, dt: number, da: number): PoolQuote | null {
|
|
354
|
+
if (sqrtPriceX96 === 0n || anchorBalance === null || liquidity === null) return null
|
|
355
|
+
// price = token1 / token0(按精度换算)
|
|
356
|
+
const price1Per0 = tokenIs0 ? sqrtPriceToPrice(sqrtPriceX96, dt, da) : sqrtPriceToPrice(sqrtPriceX96, da, dt)
|
|
357
|
+
const sqrtP = Number(sqrtPriceX96) / 2 ** 96
|
|
358
|
+
const virtual = (tokenIs0 ? Number(liquidity) * sqrtP : Number(liquidity) / sqrtP) / 10 ** da
|
|
359
|
+
return { price: tokenIs0 ? price1Per0 : 1 / price1Per0, depth: Math.min(Number(formatUnits(anchorBalance, da)), virtual) }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function v2Quote(reserve0: bigint, reserve1: bigint, tokenIs0: boolean, dt: number, da: number): PoolQuote | null {
|
|
363
|
+
const [rt, ra] = tokenIs0 ? [reserve0, reserve1] : [reserve1, reserve0]
|
|
364
|
+
if (rt === 0n || ra === 0n) return null
|
|
365
|
+
const anchor = Number(formatUnits(ra, da))
|
|
366
|
+
return { price: anchor / Number(formatUnits(rt, dt)), depth: anchor }
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// tokenPrices
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
|
|
373
|
+
export interface TokenPricesOptions {
|
|
374
|
+
/** 价格源,依次使用(前面没有价格的交给后面的),默认 [defillamaPrices(), onchainPrices()] */
|
|
375
|
+
sources?: readonly PriceSource[]
|
|
376
|
+
/** 某个价格源出错时调用(继续用下一个源) */
|
|
377
|
+
onError?: (source: PriceSource, err: unknown) => void
|
|
378
|
+
/** 自定义 fetch(代理等场景);默认全局 fetch */
|
|
379
|
+
fetch?: typeof fetch
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export interface TokenPrice {
|
|
383
|
+
/** 传入的代币地址(原样) */
|
|
384
|
+
token: string
|
|
385
|
+
native: boolean
|
|
386
|
+
/** 美元价格;所有价格源都没有时为 null */
|
|
387
|
+
price: number | null
|
|
388
|
+
/** 给出价格的价格源(如 'defillama'、'onchain') */
|
|
389
|
+
source: string | null
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* 查代币的美元价格(主币用 NATIVE_TOKEN 或零地址)。默认先查 DefiLlama,没有收录的再从链上池子算。
|
|
394
|
+
*
|
|
395
|
+
* ```ts
|
|
396
|
+
* await tokenPrices(provider, [NATIVE_TOKEN, USDT, newToken])
|
|
397
|
+
* // [{ token: '0xEeee…', native: true, price: 612.3, source: 'defillama' }, …, { token: newToken, price: 0.0012, source: 'onchain' }]
|
|
398
|
+
* await tokenPrices(provider, tokens, { sources: [defillamaPrices({ at: new Date('2026-01-01') })] }) // 历史价格
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
export async function tokenPrices(provider: Provider, tokens: readonly string[], options: TokenPricesOptions = {}): Promise<TokenPrice[]> {
|
|
402
|
+
const { sources = [defillamaPrices(), onchainPrices()], onError, fetch: fetchOption } = options
|
|
403
|
+
const chainId = await provider.getChainId()
|
|
404
|
+
const fetchFn = fetchOption ?? ((...args: Parameters<typeof fetch>) => fetch(...args))
|
|
405
|
+
const native = tokens.map((t) => provider.isNativeToken(t))
|
|
406
|
+
const erc20 = [...new Set(tokens.filter((_, i) => !native[i]).map((t) => (isTronChain(chainId) ? toTronAddress(t) : t)))]
|
|
407
|
+
// 主币 symbol:内置链信息表,或 Provider 的 nativeSymbol 配置(不在表里的链)
|
|
408
|
+
const nativeIndex = native.indexOf(true)
|
|
409
|
+
const nativeSymbol = nativeIndex < 0 ? null : (getNativeCurrency(chainId)?.symbol ?? (await provider.tokens([tokens[nativeIndex] as string], { fields: ['symbol'] }))[0]?.symbol ?? null)
|
|
410
|
+
const found = await runSources(sources, { chainId, tokens: erc20, nativeSymbol, fetch: fetchFn, provider }, onError)
|
|
411
|
+
return tokens.map((token, i) => {
|
|
412
|
+
const hit = found.get(native[i] ? 'native' : priceKeyOf(chainId, token))
|
|
413
|
+
return { token, native: native[i] as boolean, price: hit?.price ?? null, source: hit?.source ?? null }
|
|
414
|
+
})
|
|
415
|
+
}
|
package/src/risk.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 代币风险检查(子路径 `@w3lib/evm-batch-call/risk`):在 eth_call 里模拟“买入 → 转账 → 卖出”(deployless 的探测合约,不部署、不发交易),
|
|
3
|
+
* 算出买入税、转账税、卖出税,判断能不能卖(貔貅盘);另外看合约是不是可升级的代理、owner 有没有放弃。
|
|
4
|
+
* 只能检查在内置 V2 协议里有 代币 / 包装主币 池子的代币;Tron 不支持
|
|
5
|
+
*/
|
|
6
|
+
import { AbiCoder, Interface, concat } from 'ethers'
|
|
7
|
+
|
|
8
|
+
import { LP_PROTOCOLS, LP_QUOTE_TOKENS, type LpProtocol } from './lp.js'
|
|
9
|
+
import type { Provider } from './provider.js'
|
|
10
|
+
import { TOKEN_PROBE_BYTECODE } from './tokenProbe.js'
|
|
11
|
+
import { isExecutionError } from './errors.js'
|
|
12
|
+
import { isTronChain } from './tron.js'
|
|
13
|
+
|
|
14
|
+
export type ProbeStage = 'ok' | 'buy-failed' | 'transfer-failed' | 'approve-failed' | 'sell-failed'
|
|
15
|
+
|
|
16
|
+
export interface TokenRisk {
|
|
17
|
+
token: string
|
|
18
|
+
/** 模拟用的 V2 协议和池子(代币 / 包装主币里包装主币一侧最深的);没有池子时为 null */
|
|
19
|
+
protocol: string | null
|
|
20
|
+
pool: string | null
|
|
21
|
+
/** 模拟到哪一步:ok 全部成功;buy-failed 买不进(可能还没开放交易、限制了合约地址);sell-failed 卖不出(貔貅盘) */
|
|
22
|
+
stage: ProbeStage | null
|
|
23
|
+
/** 卖不出,或卖出税 ≥ 90% */
|
|
24
|
+
honeypot: boolean | null
|
|
25
|
+
/** 税率(0~1):实际数量比 Router 报价少多少;对应的步骤没跑到时为 null */
|
|
26
|
+
buyTax: number | null
|
|
27
|
+
sellTax: number | null
|
|
28
|
+
transferTax: number | null
|
|
29
|
+
/** 买入 / 卖出消耗的 gas(异常高可能有额外逻辑) */
|
|
30
|
+
buyGas: number | null
|
|
31
|
+
sellGas: number | null
|
|
32
|
+
/** EIP-1967 代理的实现合约(可升级:规则以后可以改);不是代理时为 null */
|
|
33
|
+
implementation: string | null
|
|
34
|
+
/** owner():没有这个方法时为 null;零地址表示已放弃 */
|
|
35
|
+
owner: string | null
|
|
36
|
+
/** 汇总的风险标记:honeypot、cannot-buy、high-buy-tax / high-sell-tax / high-transfer-tax(> 10%)、has-tax(> 0.5%)、proxy、owned、no-pool */
|
|
37
|
+
flags: string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TokenRiskOptions {
|
|
41
|
+
/** 模拟买入用的主币数量(最小单位),默认包装主币储备的 1 / 1000,最多 1 个主币 */
|
|
42
|
+
amount?: bigint
|
|
43
|
+
/** 用哪些 V2 协议的池子,默认这条链内置表里有 Router 的 V2 协议 */
|
|
44
|
+
protocols?: readonly LpProtocol[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const coder = AbiCoder.defaultAbiCoder()
|
|
48
|
+
const factoryInterface = new Interface(['function getPair(address tokenA, address tokenB) view returns (address)'])
|
|
49
|
+
const pairInterface = new Interface(['function token0() view returns (address)', 'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)'])
|
|
50
|
+
const ownerInterface = new Interface(['function owner() view returns (address)'])
|
|
51
|
+
/** EIP-1967 的实现合约槽位:bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) */
|
|
52
|
+
const IMPLEMENTATION_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc'
|
|
53
|
+
const STAGES: readonly ProbeStage[] = ['ok', 'buy-failed', 'transfer-failed', 'approve-failed', 'sell-failed']
|
|
54
|
+
const ZERO = '0x0000000000000000000000000000000000000000'
|
|
55
|
+
|
|
56
|
+
type Node = { call(tx: { from?: string; to?: string; data: string; value?: bigint }): Promise<string>; getStorage?(address: string, slot: string): Promise<string> }
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 检查代币的风险(每个代币一次模拟的 eth_call,加上一次 multicall 找池子、读 owner)。
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const [risk] = await tokenRisks(provider, [token])
|
|
63
|
+
* // { stage: 'ok', honeypot: false, buyTax: 0.05, sellTax: 0.05, transferTax: 0, flags: ['has-tax', 'owned'], … }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export async function tokenRisks(provider: Provider, tokens: readonly string[], options: TokenRiskOptions = {}): Promise<TokenRisk[]> {
|
|
67
|
+
const chainId = await provider.getChainId()
|
|
68
|
+
if (isTronChain(chainId)) throw new Error('tokenRisks is not supported on Tron')
|
|
69
|
+
const wrapped = LP_QUOTE_TOKENS[chainId]?.[0]
|
|
70
|
+
const protocols = (options.protocols ?? LP_PROTOCOLS.filter((p) => p.chainId === chainId)).filter((p) => p.version === 'v2' && p.router && p.factory)
|
|
71
|
+
if (!wrapped || !protocols.length) throw new Error(`No V2 protocol with a router on chain ${chainId}`)
|
|
72
|
+
|
|
73
|
+
// 1. 每个代币在每个 V2 协议里和包装主币的池子,以及 owner()
|
|
74
|
+
const pairCalls = tokens.flatMap((t) => protocols.map((p) => provider.contract(p.factory as string, factoryInterface).getPair(t, wrapped)))
|
|
75
|
+
const ownerCalls = tokens.map((t) => provider.contract(t, ownerInterface).owner())
|
|
76
|
+
const first = await provider.tryAll([...pairCalls, ...ownerCalls])
|
|
77
|
+
const pairs = first.slice(0, pairCalls.length) as Array<string | null>
|
|
78
|
+
const owners = first.slice(pairCalls.length) as Array<string | null>
|
|
79
|
+
const found = pairs.flatMap((pair, i) => (pair && pair.toLowerCase() !== ZERO ? [{ i, pair }] : []))
|
|
80
|
+
const reserves = found.length
|
|
81
|
+
? await provider.tryAll(found.flatMap(({ pair }) => [provider.contract(pair, pairInterface).token0(), provider.contract(pair, pairInterface).getReserves()]))
|
|
82
|
+
: []
|
|
83
|
+
// 每个代币:包装主币一侧最深的池子
|
|
84
|
+
const best: Array<{ protocol: LpProtocol; pair: string; wrappedReserve: bigint } | null> = tokens.map(() => null)
|
|
85
|
+
found.forEach(({ i, pair }, k) => {
|
|
86
|
+
const token0 = reserves[k * 2] as string | null
|
|
87
|
+
const r = reserves[k * 2 + 1] as { 0: bigint; 1: bigint } | null
|
|
88
|
+
if (!token0 || !r) return
|
|
89
|
+
const wrappedReserve = token0.toLowerCase() === wrapped.toLowerCase() ? r[0] : r[1]
|
|
90
|
+
const t = Math.floor(i / protocols.length)
|
|
91
|
+
const current = best[t]
|
|
92
|
+
if (wrappedReserve > 0n && (!current || wrappedReserve > current.wrappedReserve)) best[t] = { protocol: protocols[i % protocols.length] as LpProtocol, pair, wrappedReserve }
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const { runOnNodes } = await import('./write/rpc.js')
|
|
96
|
+
const node = <T>(method: string, fn: (n: Node) => Promise<T>, isFinal?: (err: unknown) => boolean) => runOnNodes(provider.rpc, method, (n) => fn(n as unknown as Node), isFinal)
|
|
97
|
+
const ONE = 10n ** 18n
|
|
98
|
+
|
|
99
|
+
return Promise.all(
|
|
100
|
+
tokens.map(async (token, t): Promise<TokenRisk> => {
|
|
101
|
+
const pick = best[t]
|
|
102
|
+
const owner = owners[t] ?? null
|
|
103
|
+
const implementation = await node('getStorage', (n) => (n.getStorage ? n.getStorage(token, IMPLEMENTATION_SLOT) : Promise.resolve('0x')))
|
|
104
|
+
.then((slot) => (slot && BigInt(slot) !== 0n ? `0x${slot.slice(-40)}` : null))
|
|
105
|
+
.catch(() => null)
|
|
106
|
+
const base = { token, implementation, owner, buyTax: null, sellTax: null, transferTax: null, buyGas: null, sellGas: null }
|
|
107
|
+
if (!pick) return { ...base, protocol: null, pool: null, stage: null, honeypot: null, flags: flagsOf({ ...base, stage: null, honeypot: null }, true) }
|
|
108
|
+
|
|
109
|
+
const amount = options.amount ?? min(pick.wrappedReserve / 1000n, ONE)
|
|
110
|
+
// deployless:没有 to 的 eth_call 执行构造函数;从包装主币合约发起(它持有大量主币,eth_call 不需要签名)
|
|
111
|
+
const data = concat([TOKEN_PROBE_BYTECODE, coder.encode(['address', 'address', 'address'], [pick.protocol.router, token, pick.pair])])
|
|
112
|
+
let decoded: bigint[] | null = null
|
|
113
|
+
try {
|
|
114
|
+
// 探测合约自己不会 revert(每一步都 try);执行出错(如买入时代币合约把调用方拒之门外导致整体 revert)算买不进,
|
|
115
|
+
// 节点、网络、限频的错误照常抛出,不当成代币的风险
|
|
116
|
+
const out = await node('call', (n) => n.call({ from: wrapped, data, value: amount }), isExecutionError)
|
|
117
|
+
decoded = coder.decode(new Array<string>(11).fill('uint256'), out).toArray() as bigint[]
|
|
118
|
+
} catch (err) {
|
|
119
|
+
if (!isExecutionError(err)) throw err
|
|
120
|
+
decoded = null
|
|
121
|
+
}
|
|
122
|
+
if (!decoded) {
|
|
123
|
+
const failed = { ...base, protocol: pick.protocol.id, pool: pick.pair, stage: 'buy-failed' as const, honeypot: null }
|
|
124
|
+
return { ...failed, flags: flagsOf(failed, false) }
|
|
125
|
+
}
|
|
126
|
+
const [stageIndex, buyQuote, bought, transferSent, transferReceived, sellAmount, sellQuote, sold, buyGas, sellGas, pairReceived] = decoded as [bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint, bigint]
|
|
127
|
+
const stage = STAGES[Number(stageIndex)] ?? 'buy-failed'
|
|
128
|
+
const tax = (actual: bigint, expected: bigint) => (expected > 0n ? Math.max(0, 1 - Number((actual * 1_000_000n) / expected) / 1_000_000) : null)
|
|
129
|
+
const result = {
|
|
130
|
+
...base,
|
|
131
|
+
protocol: pick.protocol.id,
|
|
132
|
+
pool: pick.pair,
|
|
133
|
+
stage,
|
|
134
|
+
buyTax: stage === 'buy-failed' ? null : tax(bought, buyQuote),
|
|
135
|
+
transferTax: stage === 'ok' || stage === 'approve-failed' || stage === 'sell-failed' ? tax(transferReceived, transferSent) : null,
|
|
136
|
+
// 卖出税:池子实际收到的代币(转账时扣的税,精确),和实际换回的主币比报价少多少(有的代币卖出时顺便把收来的税也卖进同一个池子,
|
|
137
|
+
// 池子那边看不出来),取较大的
|
|
138
|
+
sellTax: stage === 'ok' ? Math.max(tax(pairReceived, sellAmount) ?? 0, tax(sold, sellQuote) ?? 0) : null,
|
|
139
|
+
buyGas: stage === 'buy-failed' ? null : Number(buyGas),
|
|
140
|
+
sellGas: stage === 'ok' ? Number(sellGas) : null,
|
|
141
|
+
honeypot: stage === 'sell-failed' || stage === 'transfer-failed' || stage === 'approve-failed' ? true : stage === 'ok' ? (tax(sold, sellQuote) ?? 0) >= 0.9 : null,
|
|
142
|
+
}
|
|
143
|
+
return { ...result, flags: flagsOf(result, false) }
|
|
144
|
+
}),
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const min = (a: bigint, b: bigint) => (a < b ? a : b)
|
|
149
|
+
|
|
150
|
+
function flagsOf(r: Omit<TokenRisk, 'flags' | 'protocol' | 'pool'>, noPool: boolean): string[] {
|
|
151
|
+
const flags: string[] = []
|
|
152
|
+
if (noPool) flags.push('no-pool')
|
|
153
|
+
if (r.honeypot) flags.push('honeypot')
|
|
154
|
+
if (r.stage === 'buy-failed') flags.push('cannot-buy')
|
|
155
|
+
for (const [name, value] of [
|
|
156
|
+
['buy', r.buyTax],
|
|
157
|
+
['sell', r.sellTax],
|
|
158
|
+
['transfer', r.transferTax],
|
|
159
|
+
] as const) {
|
|
160
|
+
if (value !== null && value > 0.1) flags.push(`high-${name}-tax`)
|
|
161
|
+
}
|
|
162
|
+
if ([r.buyTax, r.sellTax, r.transferTax].some((v) => v !== null && v > 0.005)) flags.push('has-tax')
|
|
163
|
+
if (r.implementation) flags.push('proxy')
|
|
164
|
+
if (r.owner && r.owner.toLowerCase() !== ZERO) flags.push('owned')
|
|
165
|
+
return flags
|
|
166
|
+
}
|
package/src/rpcNodes.ts
CHANGED
|
@@ -183,6 +183,24 @@ export const DEFAULT_RPC_URLS: Readonly<Record<number, readonly string[]>> = {
|
|
|
183
183
|
'https://rpc-qnd.inkonchain.com',
|
|
184
184
|
'https://ink.api.pocket.network',
|
|
185
185
|
],
|
|
186
|
+
// 星空链(Starry)
|
|
187
|
+
202308: [
|
|
188
|
+
'https://rpc.starryscan.com',
|
|
189
|
+
],
|
|
190
|
+
// conflux
|
|
191
|
+
1030: [
|
|
192
|
+
'https://evm.confluxrpc.com',
|
|
193
|
+
],
|
|
194
|
+
// injective
|
|
195
|
+
1776: [
|
|
196
|
+
'https://sentry.evm-rpc.injective.network',
|
|
197
|
+
'https://injectiveevm-rpc.polkachu.com',
|
|
198
|
+
],
|
|
199
|
+
// pharos
|
|
200
|
+
1672: [
|
|
201
|
+
'https://rpc.pharos.xyz',
|
|
202
|
+
'https://infra.originstake.com/pharos/evm',
|
|
203
|
+
],
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
/** Tron 主网全节点 HTTP API(均支持跨域与 deployless) */
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 授权管理(子路径 `@w3lib/evm-batch-call/approvals`):批量查地址给出的授权(ERC20、Permit2、NFT 的 setApprovalForAll),
|
|
3
|
+
* 按 Approval 事件发现授权过的代币,批量撤销。不放在主入口,不用时打包不会带上这部分代码;发送的代码按需加载。
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { getApprovals, revokeApprovals } from '@w3lib/evm-batch-call/approvals'
|
|
7
|
+
*
|
|
8
|
+
* const list = await getApprovals(user, { chainId: 56, tokens: [USDT, CAKE] })
|
|
9
|
+
* await revokeApprovals(provider, key, list.filter((a) => a.type !== 'nft' && a.unlimited))
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { approvals, type Approval, type ApprovalsQuery } from '../approvals.js'
|
|
13
|
+
import { resolve, type ShortcutProviderOptions } from '../shortcuts.js'
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
PERMIT2_ADDRESS,
|
|
17
|
+
approvals,
|
|
18
|
+
buildRevokes,
|
|
19
|
+
knownSpenders,
|
|
20
|
+
revokeApprovals,
|
|
21
|
+
scanApprovals,
|
|
22
|
+
type Approval,
|
|
23
|
+
type ApprovalScanOptions,
|
|
24
|
+
type ApprovalScanProgress,
|
|
25
|
+
type ApprovalScanResult,
|
|
26
|
+
type ApprovalsQuery,
|
|
27
|
+
type Erc20Approval,
|
|
28
|
+
type KnownSpender,
|
|
29
|
+
type NftApproval,
|
|
30
|
+
type Permit2Approval,
|
|
31
|
+
type RevokeResult,
|
|
32
|
+
type RevokeTx,
|
|
33
|
+
} from '../approvals.js'
|
|
34
|
+
|
|
35
|
+
export interface GetApprovalsOptions extends ApprovalsQuery, ShortcutProviderOptions {}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 查地址给出的授权,节点参数与 getBalances 相同。默认检查已知的 Router / 聚合器 / NFT 市场。
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* await getApprovals(user, { chainId: 56, tokens: [USDT, CAKE] })
|
|
42
|
+
* // [{ type: 'erc20', token: USDT, symbol: 'USDT', spender: '0x1111…', spenderName: '1inch Router v6', amount: '…', unlimited: true }, …]
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export function getApprovals(owner: string, options: GetApprovalsOptions): Promise<Approval[]> {
|
|
46
|
+
const { provider, own } = resolve(options, ['tokens', 'spenders', 'pairs', 'permit2', 'nfts', 'operators', 'nftPairs', 'includeZero'])
|
|
47
|
+
return approvals(provider, owner, own)
|
|
48
|
+
}
|