four-flap-meme-sdk 4.3.22 → 4.4.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/dist/bundle/four-meme/approve-tokenmanager.js +46 -16
- package/dist/bundle/four-meme/core/create-token-quote.d.ts +5 -1
- package/dist/bundle/four-meme/core/create-token-quote.js +29 -4
- package/dist/bundle/four-meme/core/sell-flow.js +3 -3
- package/dist/bundle/four-meme/pancake-proxy/approve.js +47 -10
- package/dist/bundle/four-meme/private/sell.js +2 -10
- package/dist/bundle/four-meme/swap/bundle.js +4 -4
- package/dist/bundle/four-meme/swap-buy-first/multi-wallet.js +25 -8
- package/dist/bundle/four-meme/swap-buy-first/single.js +30 -9
- package/dist/bundle/four-meme/swap-sign-helpers.js +56 -3
- package/dist/bundle/four-meme/utils-sweep/erc20-no-hop.js +18 -7
- package/dist/bundle/four-meme/utils-sweep/with-hops.js +10 -4
- package/dist/bundle/internal.d.ts +5 -0
- package/dist/bundle/internal.js +36 -0
- package/dist/chains/bsc/pancake/bundle-buy-first-quote.js +24 -8
- package/dist/chains/bsc/pancake/bundle-buy-first-types.d.ts +2 -0
- package/dist/chains/bsc/pancake/bundle-buy-first-validate.d.ts +1 -1
- package/dist/chains/bsc/pancake/bundle-buy-first-validate.js +2 -2
- package/dist/chains/bsc/pancake/bundle-buy-first.js +1 -0
- package/dist/chains/bsc/pancake/bundle-swap-types.d.ts +2 -0
- package/dist/chains/bsc/pancake/quote.d.ts +7 -0
- package/dist/chains/bsc/pancake/quote.js +9 -1
- package/dist/chains/bsc/pancake/swap-batch.js +15 -16
- package/dist/chains/bsc/pancake/swap-build.d.ts +1 -1
- package/dist/chains/bsc/pancake/swap-build.js +15 -9
- package/dist/chains/bsc/pancake/swap-quick.js +4 -4
- package/dist/chains/bsc/pancake/swap-single.js +1 -0
- package/dist/chains/bsc/platforms/iro/token.d.ts +1 -0
- package/dist/chains/bsc/platforms/iro/token.js +36 -1
- package/dist/chains/eni/batch-router/transfer-sweep.js +10 -14
- package/dist/chains/eni/platforms/daoaas/graduation.js +60 -14
- package/dist/chains/eni/platforms/daoaas/meta.d.ts +0 -4
- package/dist/chains/eni/platforms/daoaas/meta.js +112 -27
- package/dist/chains/eni/platforms/daoaas/portal-direct-submit.js +11 -6
- package/dist/chains/eni/platforms/daoaas/portal-direct-tx.js +4 -2
- package/dist/chains/eni/platforms/daoaas/portal.d.ts +5 -0
- package/dist/chains/eni/platforms/daoaas/portal.js +63 -9
- package/dist/chains/eni/platforms/dswap/liquidity.js +144 -49
- package/dist/chains/eni/platforms/fair-launch/launcher.js +2 -10
- package/dist/chains/eni/platforms/fair-launch/pool.d.ts +1 -0
- package/dist/chains/eni/platforms/fair-launch/pool.js +16 -2
- package/dist/chains/eni/platforms/iro/token.d.ts +1 -0
- package/dist/chains/eni/platforms/iro/token.js +36 -1
- package/dist/chains/robinhood/pons-v2/launch.js +49 -18
- package/dist/chains/xlayer/eip7702/bundle-approve.js +8 -8
- package/dist/chains/xlayer/eip7702/bundle-sell.js +3 -4
- package/dist/chains/xlayer/eip7702/bundle-swap-batch.js +4 -2
- package/dist/chains/xlayer/eip7702/bundle-swap-multi.js +3 -3
- package/dist/chains/xlayer/eip7702/multi-hop-transfer-sweep.js +4 -10
- package/dist/chains/xlayer/eip7702/volume-bundle.js +45 -24
- package/dist/chains/xlayer/eoa/eoa-wash-volume.js +4 -14
- package/dist/core/quote/quote-helpers.d.ts +11 -0
- package/dist/core/quote/quote-helpers.js +121 -20
- package/dist/core/quote/quote-helpers.test.js +11 -1
- package/dist/domains/dex/direct-router-v2-buy.js +4 -2
- package/dist/domains/dex/direct-router-v2-sell.js +63 -12
- package/dist/domains/dex/direct-router-v3.js +3 -4
- package/dist/domains/dex/quote.js +15 -5
- package/dist/domains/flap/portal-bundle-merkle/pancake-proxy/approve.js +48 -8
- package/dist/domains/flap/portal-bundle-merkle/swap-batch.js +8 -10
- package/dist/domains/flap/portal-bundle-merkle/swap-buy-first-quote.js +24 -9
- package/dist/domains/flap/portal-bundle-merkle/swap-planning.js +6 -2
- package/dist/domains/flap/portal-bundle-merkle/utils/sweep.js +14 -7
- package/dist/domains/flap/reader.d.ts +6 -0
- package/dist/domains/flap/reader.js +77 -1
- package/dist/domains/four/contracts/tm-bundle-batch.js +2 -10
- package/dist/domains/four/contracts/tm-bundle-create.js +5 -2
- package/dist/domains/four/contracts/tm.d.ts +2 -0
- package/dist/domains/four/contracts/tm.js +59 -2
- package/dist/domains/four/tax-token.js +60 -28
- package/dist/domains/lp-inspect/inspect-tax.js +98 -12
- package/dist/domains/lp-inspect/inspect.js +57 -44
- package/dist/domains/lp-inspect/registry.js +33 -7
- package/dist/domains/openfour/registry.js +15 -5
- package/dist/utils/airdrop-sweep.js +37 -17
- package/dist/utils/swap-helpers.js +35 -7
- package/package.json +1 -1
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* - V3 Quoter 报价(多费率尝试 + 多跳路径)
|
|
7
7
|
* - ERC20 → 原生代币转换报价
|
|
8
8
|
*/
|
|
9
|
-
import { Contract } from 'ethers';
|
|
10
|
-
import { V2_ROUTER_QUOTE_ABI, V3_QUOTER_ABI } from '../../abis/common.js';
|
|
9
|
+
import { Contract, Interface } from 'ethers';
|
|
10
|
+
import { MULTICALL3_ABI, V2_ROUTER_QUOTE_ABI, V3_QUOTER_ABI } from '../../abis/common.js';
|
|
11
|
+
import { getMulticall3Address } from '../constants/addresses.js';
|
|
12
|
+
import { CHAINS } from '../constants/chains.js';
|
|
11
13
|
import { calculateBundleSwapRoundTripProfit } from '../constants/profit.js';
|
|
12
14
|
import { QUOTE_CONFIG, QUOTE_V3_FEE_TIERS, getQuoteChainKey } from '../constants/quote.js';
|
|
13
15
|
const V2_ROUTER_ABI = V2_ROUTER_QUOTE_ABI;
|
|
@@ -62,27 +64,91 @@ export function buildV2QuotePaths(tokenIn, tokenOut, hops) {
|
|
|
62
64
|
function hopTokensForChain(chainUpper) {
|
|
63
65
|
return chainUpper === 'BSC' ? BSC_V2_HOP_TOKENS : [];
|
|
64
66
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
export function selectBestV2Quote(paths, rows) {
|
|
68
|
+
let best;
|
|
69
|
+
for (let i = 0; i < paths.length; i++) {
|
|
70
|
+
const row = rows[i];
|
|
71
|
+
const path = paths[i];
|
|
72
|
+
if (!row?.success || !path || row.amountOut <= 0n)
|
|
73
|
+
continue;
|
|
74
|
+
if (!best || row.amountOut > best.amountOut) {
|
|
75
|
+
best = { amountOut: row.amountOut, path: [...path] };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return best;
|
|
79
|
+
}
|
|
80
|
+
function quoteChainId(chain) {
|
|
81
|
+
const key = chain.toUpperCase();
|
|
82
|
+
return CHAINS[key]?.chainId ?? CHAINS.BSC.chainId;
|
|
83
|
+
}
|
|
84
|
+
function readAggregateRow(row) {
|
|
85
|
+
return { success: Boolean(row.success), returnData: String(row.returnData ?? '0x') };
|
|
86
|
+
}
|
|
87
|
+
/** 多条 getAmountsOut 收成一次 Multicall3 aggregate3。失败的路径 allowFailure,不拖垮整批。 */
|
|
88
|
+
async function quoteV2CallsMulticall(provider, chain, routerAddress, specs) {
|
|
89
|
+
const routerIface = new Interface(V2_ROUTER_ABI);
|
|
90
|
+
const multicall = new Contract(getMulticall3Address(quoteChainId(chain)), MULTICALL3_ABI, provider);
|
|
91
|
+
const calls = specs.map((spec) => ({
|
|
92
|
+
target: routerAddress,
|
|
93
|
+
allowFailure: true,
|
|
94
|
+
callData: routerIface.encodeFunctionData('getAmountsOut', [spec.amountIn, spec.path]),
|
|
95
|
+
}));
|
|
96
|
+
const raw = await multicall.aggregate3.staticCall(calls);
|
|
97
|
+
const rows = raw;
|
|
98
|
+
return rows.map((row) => {
|
|
99
|
+
const decodedRow = readAggregateRow(row);
|
|
100
|
+
if (!decodedRow.success || decodedRow.returnData === '0x') {
|
|
101
|
+
return { success: false, amountOut: 0n };
|
|
102
|
+
}
|
|
67
103
|
try {
|
|
68
|
-
const amounts =
|
|
69
|
-
const amountOut = amounts[amounts.length - 1];
|
|
70
|
-
return {
|
|
104
|
+
const amounts = routerIface.decodeFunctionResult('getAmountsOut', decodedRow.returnData)[0];
|
|
105
|
+
const amountOut = amounts[amounts.length - 1] ?? 0n;
|
|
106
|
+
return { success: amountOut > 0n, amountOut };
|
|
71
107
|
}
|
|
72
108
|
catch {
|
|
73
|
-
return {
|
|
109
|
+
return { success: false, amountOut: 0n };
|
|
74
110
|
}
|
|
75
111
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
112
|
+
}
|
|
113
|
+
async function quoteV2PathsMulticall(provider, chain, routerAddress, amountIn, paths) {
|
|
114
|
+
return quoteV2CallsMulticall(provider, chain, routerAddress, paths.map((path) => ({ amountIn, path })));
|
|
115
|
+
}
|
|
116
|
+
/** 同一条 V2 path、多笔金额,一次 multicall。0 金额直接回 0,不打节点。 */
|
|
117
|
+
export async function quoteV2SamePathAmounts(provider, chain, routerAddress, path, amountsIn) {
|
|
118
|
+
if (amountsIn.length === 0)
|
|
119
|
+
return [];
|
|
120
|
+
const indexed = amountsIn
|
|
121
|
+
.map((amountIn, index) => ({ amountIn, index }))
|
|
122
|
+
.filter((item) => item.amountIn > 0n);
|
|
123
|
+
const rows = await quoteV2CallsMulticall(provider, chain, routerAddress, indexed.map((item) => ({ amountIn: item.amountIn, path })));
|
|
124
|
+
const outs = amountsIn.map(() => 0n);
|
|
125
|
+
indexed.forEach((item, rowIndex) => {
|
|
126
|
+
outs[item.index] = rows[rowIndex]?.amountOut ?? 0n;
|
|
127
|
+
});
|
|
128
|
+
return outs;
|
|
129
|
+
}
|
|
130
|
+
async function bestAmountsOut(provider, chain, routerAddress, amountIn, paths) {
|
|
131
|
+
if (paths.length === 0)
|
|
132
|
+
return undefined;
|
|
133
|
+
try {
|
|
134
|
+
const rows = await quoteV2PathsMulticall(provider, chain, routerAddress, amountIn, paths);
|
|
135
|
+
return selectBestV2Quote(paths, rows);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
const router = new Contract(routerAddress, V2_ROUTER_ABI, provider);
|
|
139
|
+
const quotePromises = paths.map(async (path) => {
|
|
140
|
+
try {
|
|
141
|
+
const amounts = await router.getAmountsOut(amountIn, path);
|
|
142
|
+
const amountOut = amounts[amounts.length - 1];
|
|
143
|
+
return { amountOut, success: amountOut > 0n };
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return { amountOut: 0n, success: false };
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
const results = await Promise.all(quotePromises);
|
|
150
|
+
return selectBestV2Quote(paths, results);
|
|
84
151
|
}
|
|
85
|
-
return best;
|
|
86
152
|
}
|
|
87
153
|
/**
|
|
88
154
|
* V2 报价(直接路径 + 多跳路径)
|
|
@@ -105,8 +171,7 @@ export async function quoteV2(provider, tokenIn, tokenOut, amountIn, chain, rout
|
|
|
105
171
|
return { amountOut: 0n };
|
|
106
172
|
}
|
|
107
173
|
const routerAddress = routerOverride || config.v2Router;
|
|
108
|
-
const
|
|
109
|
-
const stableBest = await bestAmountsOut(router, amountIn, buildV2QuotePaths(tokenIn, tokenOut, config.stableCoins));
|
|
174
|
+
const stableBest = await bestAmountsOut(provider, chain, routerAddress, amountIn, buildV2QuotePaths(tokenIn, tokenOut, config.stableCoins));
|
|
110
175
|
if (stableBest) {
|
|
111
176
|
return { amountOut: stableBest.amountOut, path: stableBest.path };
|
|
112
177
|
}
|
|
@@ -115,12 +180,48 @@ export async function quoteV2(provider, tokenIn, tokenOut, amountIn, chain, rout
|
|
|
115
180
|
return { amountOut: 0n };
|
|
116
181
|
}
|
|
117
182
|
const hopOnly = buildV2QuotePaths(tokenIn, tokenOut, extraHops).filter((path) => path.length >= 3);
|
|
118
|
-
const hopBest = await bestAmountsOut(
|
|
183
|
+
const hopBest = await bestAmountsOut(provider, chain, routerAddress, amountIn, hopOnly);
|
|
119
184
|
if (hopBest) {
|
|
120
185
|
return { amountOut: hopBest.amountOut, path: hopBest.path };
|
|
121
186
|
}
|
|
122
187
|
return { amountOut: 0n };
|
|
123
188
|
}
|
|
189
|
+
/** 多笔金额走同一套 V2 路径搜索,稳定币路径一次 multicall,不够再补一跳。 */
|
|
190
|
+
export async function quoteV2ManyAmounts(provider, tokenIn, tokenOut, amountsIn, chain, routerOverride) {
|
|
191
|
+
const chainUpper = chain.toUpperCase();
|
|
192
|
+
const config = QUOTE_CONFIG[chainUpper];
|
|
193
|
+
if (!config)
|
|
194
|
+
return amountsIn.map(() => 0n);
|
|
195
|
+
const routerAddress = routerOverride || config.v2Router;
|
|
196
|
+
const outs = amountsIn.map(() => 0n);
|
|
197
|
+
const pending = amountsIn.map((amount, index) => ({ amount, index })).filter((item) => item.amount > 0n);
|
|
198
|
+
if (pending.length === 0)
|
|
199
|
+
return outs;
|
|
200
|
+
const fill = async (paths, indexes) => {
|
|
201
|
+
if (paths.length === 0 || indexes.length === 0)
|
|
202
|
+
return indexes;
|
|
203
|
+
const rows = await quoteV2CallsMulticall(provider, chain, routerAddress, indexes.flatMap((index) => paths.map((path) => ({ amountIn: amountsIn[index], path }))));
|
|
204
|
+
const missed = [];
|
|
205
|
+
indexes.forEach((amountIndex, group) => {
|
|
206
|
+
const slice = rows.slice(group * paths.length, (group + 1) * paths.length);
|
|
207
|
+
const best = selectBestV2Quote(paths, slice);
|
|
208
|
+
if (!best || best.amountOut <= 0n)
|
|
209
|
+
missed.push(amountIndex);
|
|
210
|
+
else
|
|
211
|
+
outs[amountIndex] = best.amountOut;
|
|
212
|
+
});
|
|
213
|
+
return missed;
|
|
214
|
+
};
|
|
215
|
+
try {
|
|
216
|
+
const stableMiss = await fill(buildV2QuotePaths(tokenIn, tokenOut, config.stableCoins), pending.map((item) => item.index));
|
|
217
|
+
const hopOnly = buildV2QuotePaths(tokenIn, tokenOut, hopTokensForChain(chainUpper)).filter((path) => path.length >= 3);
|
|
218
|
+
await fill(hopOnly, stableMiss);
|
|
219
|
+
return outs;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
124
225
|
export async function resolveQuotedV2Path(provider, tokenIn, tokenOut, amountIn, chain, routerOverride) {
|
|
125
226
|
if (amountIn <= 0n)
|
|
126
227
|
return [tokenIn, tokenOut];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { buildV2QuotePaths } from './quote-helpers.js';
|
|
2
|
+
import { buildV2QuotePaths, selectBestV2Quote } from './quote-helpers.js';
|
|
3
3
|
const WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
|
|
4
4
|
const SPCXB = '0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1';
|
|
5
5
|
const TOKEN = '0x7Db2F0c15f74aB0Dc0D4f49650dD60C45FFB7777';
|
|
@@ -11,6 +11,16 @@ describe('buildV2QuotePaths', () => {
|
|
|
11
11
|
expect(paths).toContainEqual([WBNB, SPCXB, TOKEN]);
|
|
12
12
|
expect(paths).toContainEqual([WBNB, USDT, TOKEN]);
|
|
13
13
|
});
|
|
14
|
+
it('picks the largest successful getAmountsOut and skips failed paths', () => {
|
|
15
|
+
const paths = [
|
|
16
|
+
[WBNB, TOKEN],
|
|
17
|
+
[WBNB, USDT, TOKEN],
|
|
18
|
+
];
|
|
19
|
+
expect(selectBestV2Quote(paths, [
|
|
20
|
+
{ success: false, amountOut: 0n },
|
|
21
|
+
{ success: true, amountOut: 5n },
|
|
22
|
+
])).toEqual({ amountOut: 5n, path: [WBNB, USDT, TOKEN] });
|
|
23
|
+
});
|
|
14
24
|
it('does not hop through the input token itself', () => {
|
|
15
25
|
const paths = buildV2QuotePaths(SPCXB, TOKEN, [SPCXB, USDT]);
|
|
16
26
|
expect(paths).toEqual([
|
|
@@ -7,6 +7,8 @@ import { getGasLimit } from '../../core/gas/bundle-gas.js';
|
|
|
7
7
|
import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, buildGasFields, } from '../../utils/bundle-helpers.js';
|
|
8
8
|
import { getProfitRecipient, CHAINS, GAS_LIMITS } from '../../core/constants/index.js';
|
|
9
9
|
import { ERC20_ABI } from '../../abis/common.js';
|
|
10
|
+
import { batchCheckAllowances } from '../../core/erc20/allowances.js';
|
|
11
|
+
import { batchGetBalances } from '../../bundle/internal.js';
|
|
10
12
|
import { getTokenToNativeQuote, resolveQuotedV2Path } from '../../core/quote/quote-helpers.js';
|
|
11
13
|
import { requireEvmAddress } from '../../core/erc20/helpers.js';
|
|
12
14
|
import { CHAIN_IDS, DIRECT_ROUTERS, SWAP_ROUTER02_V2_ABI, V2_ROUTER_ABI, buildBribeTransaction, buildProfitTransactionWithHops, calculateProfitAmount, encodeDYORMulticall, encodeDYORRefundETH, encodeDYORSwapExactTokensForTokens, findMaxFlowIndex, filterEniNativeBuyIndices, getBribeAmount, getCachedProvider, getDeadline, getGasPrice, getWrappedNative, isDYORSwap, isNativeToken, isSwapRouter02, } from './direct-router-helpers.js';
|
|
@@ -40,7 +42,7 @@ export async function directV2BatchBuy(params) {
|
|
|
40
42
|
? getTokenToNativeQuote(provider, inputToken, baseProfitWei, chain, 'v2', undefined, router)
|
|
41
43
|
: Promise.resolve(baseProfitWei),
|
|
42
44
|
quoteTokenContract
|
|
43
|
-
?
|
|
45
|
+
? batchCheckAllowances(provider, inputToken, wallets.map((w) => w.address), router)
|
|
44
46
|
: Promise.resolve(wallets.map(() => ethers.MaxUint256)),
|
|
45
47
|
resolveQuotedV2Path(provider, inputToken, tokenOut, probeAmount, chain, router),
|
|
46
48
|
]);
|
|
@@ -55,7 +57,7 @@ export async function directV2BatchBuy(params) {
|
|
|
55
57
|
// ENI 链原生代币买入:链上余额预检,跳过余额不足的钱包
|
|
56
58
|
let activeIndices = null;
|
|
57
59
|
if (useNative && chain.toUpperCase() === 'ENI') {
|
|
58
|
-
const eniBalances = await
|
|
60
|
+
const eniBalances = await batchGetBalances(provider, wallets.map((w) => w.address), undefined, chainId);
|
|
59
61
|
activeIndices = filterEniNativeBuyIndices(eniBalances, flowAmounts, gasLimit, gasPrice, profitWei);
|
|
60
62
|
}
|
|
61
63
|
// 如果有钱包被跳过,重建数组(仅 ENI)
|
|
@@ -2,14 +2,63 @@
|
|
|
2
2
|
* direct-router-v2-sell - V2 批量卖出(直接调用 Router)
|
|
3
3
|
* 从 direct-router-v2.ts 抽取,direct-router-v2.ts 通过 re-export 保持对外不变。
|
|
4
4
|
*/
|
|
5
|
-
import { ethers, Wallet, Contract } from 'ethers';
|
|
5
|
+
import { ethers, Interface, Wallet, Contract } from 'ethers';
|
|
6
6
|
import { getGasLimit } from '../../core/gas/bundle-gas.js';
|
|
7
7
|
import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, buildGasFields, } from '../../utils/bundle-helpers.js';
|
|
8
|
-
import { getProfitRecipient, CHAINS, GAS_LIMITS } from '../../core/constants/index.js';
|
|
8
|
+
import { getProfitRecipient, CHAINS, GAS_LIMITS, getMulticall3Address } from '../../core/constants/index.js';
|
|
9
|
+
import { MULTICALL3_ABI } from '../../abis/common.js';
|
|
9
10
|
import { getErrorMessageFromUnknown } from '../../core/index.js';
|
|
10
11
|
import { ERC20_ABI } from '../../abis/common.js';
|
|
12
|
+
import { batchGetBalances } from '../../bundle/internal.js';
|
|
13
|
+
import { batchCheckAllowances } from '../../core/erc20/allowances.js';
|
|
11
14
|
import { getTokenToNativeQuote, quoteV2, resolveQuotedV2Path } from '../../core/quote/quote-helpers.js';
|
|
12
15
|
import { requireEvmAddress } from '../../core/erc20/helpers.js';
|
|
16
|
+
const SELL_VIEW_ABI = [
|
|
17
|
+
'function decimals() view returns (uint8)',
|
|
18
|
+
'function balanceOf(address) view returns (uint256)',
|
|
19
|
+
'function allowance(address,address) view returns (uint256)',
|
|
20
|
+
];
|
|
21
|
+
/** 卖出前的精度、余额、授权收成一次 aggregate3。 */
|
|
22
|
+
async function readV2SellTokenViews(provider, chainId, tokenIn, owners, spender, knownDecimals) {
|
|
23
|
+
const iface = new Interface(SELL_VIEW_ABI);
|
|
24
|
+
const includeDecimals = knownDecimals === undefined;
|
|
25
|
+
const calls = [
|
|
26
|
+
...(includeDecimals
|
|
27
|
+
? [{ target: tokenIn, allowFailure: true, callData: iface.encodeFunctionData('decimals', []) }]
|
|
28
|
+
: []),
|
|
29
|
+
...owners.map((owner) => ({
|
|
30
|
+
target: tokenIn,
|
|
31
|
+
allowFailure: true,
|
|
32
|
+
callData: iface.encodeFunctionData('balanceOf', [owner]),
|
|
33
|
+
})),
|
|
34
|
+
...owners.map((owner) => ({
|
|
35
|
+
target: tokenIn,
|
|
36
|
+
allowFailure: true,
|
|
37
|
+
callData: iface.encodeFunctionData('allowance', [owner, spender]),
|
|
38
|
+
})),
|
|
39
|
+
];
|
|
40
|
+
const multicall = new Contract(getMulticall3Address(chainId), MULTICALL3_ABI, provider);
|
|
41
|
+
const aggregate3 = multicall.aggregate3;
|
|
42
|
+
if (!aggregate3)
|
|
43
|
+
throw new Error('aggregate3 missing');
|
|
44
|
+
const rows = (await aggregate3.staticCall(calls));
|
|
45
|
+
const decode = (index, name) => {
|
|
46
|
+
const row = rows[index];
|
|
47
|
+
if (!row?.success || row.returnData === '0x')
|
|
48
|
+
return 0n;
|
|
49
|
+
try {
|
|
50
|
+
return iface.decodeFunctionResult(name, row.returnData)[0];
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return 0n;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
let cursor = 0;
|
|
57
|
+
const decimals = includeDecimals ? Number(decode(cursor++, 'decimals') || 18n) : knownDecimals;
|
|
58
|
+
const balances = owners.map(() => decode(cursor++, 'balanceOf'));
|
|
59
|
+
const allowances = owners.map(() => decode(cursor++, 'allowance'));
|
|
60
|
+
return { decimals, balances, allowances };
|
|
61
|
+
}
|
|
13
62
|
import { CHAIN_IDS, DIRECT_ROUTERS, SWAP_ROUTER02_V2_ABI, V2_ROUTER_ABI, buildBribeTransaction, buildProfitTransactionWithHops, calculateProfitAmount, encodeDYORMulticall, encodeDYORSwapExactTokensForTokens, encodeDYORUnwrapWETH9, findMaxFlowIndex, getBribeAmount, getCachedProvider, getDeadline, getGasPrice, getWrappedNative, isDYORSwap, isNativeToken, isSwapRouter02, truncateDecimals, } from './direct-router-helpers.js';
|
|
14
63
|
/**
|
|
15
64
|
* V2 批量卖出(直接调用 Router)
|
|
@@ -27,21 +76,23 @@ export async function directV2BatchSell(params) {
|
|
|
27
76
|
const wallets = privateKeys.map((pk) => new Wallet(pk, provider));
|
|
28
77
|
const tokenContract = new Contract(tokenIn, ERC20_ABI, provider);
|
|
29
78
|
// ✅ 性能优化:并行获取所有独立的 RPC 数据(包括授权额度)
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
79
|
+
const owners = wallets.map((w) => w.address);
|
|
80
|
+
const [sellViews, nonces, gasPrice] = await Promise.all([
|
|
81
|
+
readV2SellTokenViews(provider, chainId, tokenIn, owners, router, inputDecimals).catch(async () => ({
|
|
82
|
+
decimals: inputDecimals ??
|
|
83
|
+
(await tokenContract
|
|
84
|
+
.decimals()
|
|
85
|
+
.then((d) => Number(d))
|
|
86
|
+
.catch(() => 18)),
|
|
87
|
+
balances: await batchGetBalances(provider, owners, tokenIn, chainId),
|
|
88
|
+
allowances: await batchCheckAllowances(provider, tokenIn, owners, router),
|
|
89
|
+
})),
|
|
39
90
|
startNonces && startNonces.length === wallets.length
|
|
40
91
|
? Promise.resolve(startNonces)
|
|
41
92
|
: new NonceManager(provider).getNextNoncesForWallets(wallets),
|
|
42
93
|
getGasPrice(provider, config),
|
|
43
94
|
]);
|
|
44
|
-
const tokenDecimals =
|
|
95
|
+
const { decimals: tokenDecimals, balances, allowances } = sellViews;
|
|
45
96
|
const gasLimit = getGasLimit(config, 300000);
|
|
46
97
|
const txType = config.txType ?? 0;
|
|
47
98
|
const deadline = getDeadline();
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* direct-router-v3 - V3 批量买卖(直接调用 Router)
|
|
3
3
|
*/
|
|
4
|
-
import { ethers, Wallet
|
|
4
|
+
import { ethers, Wallet } from 'ethers';
|
|
5
5
|
import { getGasLimit } from '../../core/gas/bundle-gas.js';
|
|
6
6
|
import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, buildGasFields, } from '../../utils/bundle-helpers.js';
|
|
7
7
|
import { getProfitRecipient, CHAINS, GAS_LIMITS } from '../../core/constants/index.js';
|
|
8
8
|
import { getErrorMessageFromUnknown } from '../../core/index.js';
|
|
9
|
-
import {
|
|
9
|
+
import { batchGetBalances } from '../../bundle/internal.js';
|
|
10
10
|
import { getTokenToNativeQuote } from '../../core/quote/quote-helpers.js';
|
|
11
11
|
import { CHAIN_IDS, V3_ROUTER02_ABI, V3_ROUTER_LEGACY_ABI, buildBribeTransaction, buildProfitTransactionWithHops, calculateProfitAmount, findMaxFlowIndex, getBribeAmount, getCachedProvider, getDeadline, getGasPrice, getWrappedNative, isLegacySwapRouter, isNativeToken, quoteXLayerV3TokenToNativeBySlot0, } from './direct-router-helpers.js';
|
|
12
12
|
// ============================================================================
|
|
@@ -213,10 +213,9 @@ export async function directV3BatchSell(params) {
|
|
|
213
213
|
const useLegacyRouter = isLegacySwapRouter(chain, routerAddress);
|
|
214
214
|
const routerAbi = useLegacyRouter ? V3_ROUTER_LEGACY_ABI : V3_ROUTER02_ABI;
|
|
215
215
|
const wallets = privateKeys.map((pk) => new Wallet(pk, provider));
|
|
216
|
-
const tokenContract = new Contract(tokenAddress, ERC20_ABI, provider);
|
|
217
216
|
// ✅ 并行获取所有 RPC 数据
|
|
218
217
|
const [balances, nonces, gasPrice] = await Promise.all([
|
|
219
|
-
|
|
218
|
+
batchGetBalances(provider, wallets.map((w) => w.address), tokenAddress, chainId),
|
|
220
219
|
startNonces && startNonces.length === wallets.length
|
|
221
220
|
? Promise.resolve(startNonces)
|
|
222
221
|
: new NonceManager(provider).getNextNoncesForWallets(wallets),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Contract } from 'ethers';
|
|
2
|
-
import { ZERO_ADDRESS } from '../../core/constants/index.js';
|
|
1
|
+
import { Contract, Interface } from 'ethers';
|
|
2
|
+
import { CHAINS, ZERO_ADDRESS, getMulticall3Address } from '../../core/constants/index.js';
|
|
3
|
+
import { MULTICALL3_ABI } from '../../abis/common.js';
|
|
3
4
|
import { DIRECT_ROUTERS, V3_FEE_DENOMINATOR, XLAYER_V3_FACTORY_ABI, XLAYER_V3_POOL_ABI, } from './direct-router-types.js';
|
|
4
5
|
// ============================================================================
|
|
5
6
|
// XLAYER V3 报价(slot0 现货价,纯 V3,不串 V2)
|
|
@@ -23,10 +24,19 @@ export async function quoteXLayerV3TokenToNativeBySlot0(params) {
|
|
|
23
24
|
const poolAddr = await factory.getPool?.(tokenIn, wrappedNative, fee);
|
|
24
25
|
if (!poolAddr || poolAddr.toLowerCase() === ZERO_ADDRESS.toLowerCase())
|
|
25
26
|
return 0n;
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const poolIface = new Interface(XLAYER_V3_POOL_ABI);
|
|
28
|
+
const multicall = new Contract(getMulticall3Address(CHAINS.XLAYER.chainId), MULTICALL3_ABI, provider);
|
|
29
|
+
const aggregate3 = multicall.aggregate3;
|
|
30
|
+
if (!aggregate3)
|
|
29
31
|
return 0n;
|
|
32
|
+
const poolRows = (await aggregate3.staticCall(['token0', 'token1', 'slot0'].map((name) => ({
|
|
33
|
+
target: poolAddr,
|
|
34
|
+
allowFailure: false,
|
|
35
|
+
callData: poolIface.encodeFunctionData(name, []),
|
|
36
|
+
}))));
|
|
37
|
+
const t0 = poolIface.decodeFunctionResult('token0', poolRows[0].returnData)[0];
|
|
38
|
+
const t1 = poolIface.decodeFunctionResult('token1', poolRows[1].returnData)[0];
|
|
39
|
+
const slot0 = poolIface.decodeFunctionResult('slot0', poolRows[2].returnData);
|
|
30
40
|
const sqrtPriceX96 = BigInt(slot0[0]);
|
|
31
41
|
if (sqrtPriceX96 <= 0n)
|
|
32
42
|
return 0n;
|
|
@@ -1,9 +1,33 @@
|
|
|
1
|
-
import { ethers, Wallet, Contract, } from 'ethers';
|
|
1
|
+
import { ethers, Wallet, Contract, Interface, } from 'ethers';
|
|
2
2
|
import { getGasLimit, } from '../../../../core/gas/bundle-gas.js';
|
|
3
3
|
import { NonceManager, getOptimizedGasPrice, } from '../../../../utils/bundle-helpers.js';
|
|
4
|
-
import { ERC20_ABI, } from '../../../../core/shared-abis/common.js';
|
|
4
|
+
import { ERC20_ABI, MULTICALL3_ABI, } from '../../../../core/shared-abis/common.js';
|
|
5
|
+
import { getMulticall3Address } from '../../../../core/constants/index.js';
|
|
5
6
|
import { getErrorMessageFromUnknown, } from '../../../../core/index.js';
|
|
6
7
|
import { CHAIN_ID_MAP, getTxType, getGasPriceConfig, } from '../config.js';
|
|
8
|
+
import { batchCheckAllowances } from '../../../../core/erc20/allowances.js';
|
|
9
|
+
async function readDecimalsAndAllowances(provider, chainId, tokenAddress, owners, spender, needDecimals) {
|
|
10
|
+
const iface = new Interface(ERC20_ABI);
|
|
11
|
+
const multicall = new Contract(getMulticall3Address(chainId), MULTICALL3_ABI, provider);
|
|
12
|
+
const aggregate3 = multicall.aggregate3;
|
|
13
|
+
if (!aggregate3)
|
|
14
|
+
throw new Error('aggregate3 missing');
|
|
15
|
+
const rows = (await aggregate3.staticCall([
|
|
16
|
+
...(needDecimals
|
|
17
|
+
? [{ target: tokenAddress, allowFailure: false, callData: iface.encodeFunctionData('decimals', []) }]
|
|
18
|
+
: []),
|
|
19
|
+
...owners.map((owner) => ({
|
|
20
|
+
target: tokenAddress,
|
|
21
|
+
allowFailure: false,
|
|
22
|
+
callData: iface.encodeFunctionData('allowance', [owner, spender]),
|
|
23
|
+
})),
|
|
24
|
+
]));
|
|
25
|
+
const offset = needDecimals ? 1 : 0;
|
|
26
|
+
return {
|
|
27
|
+
decimals: needDecimals ? Number(iface.decodeFunctionResult('decimals', rows[0].returnData)[0]) : 18,
|
|
28
|
+
allowances: owners.map((_, index) => iface.decodeFunctionResult('allowance', rows[index + offset].returnData)[0]),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
7
31
|
import { getApprovalTarget, getCachedProvider, getTokenDecimals, } from './helpers.js';
|
|
8
32
|
/**
|
|
9
33
|
* ✅ 授权代币给 PancakeSwap Router
|
|
@@ -14,8 +38,19 @@ export async function approvePancakeProxy(params) {
|
|
|
14
38
|
const provider = getCachedProvider(chain, rpcUrl);
|
|
15
39
|
const wallet = new Wallet(privateKey, provider);
|
|
16
40
|
const token = new Contract(tokenAddress, ERC20_ABI, wallet);
|
|
17
|
-
const
|
|
18
|
-
const
|
|
41
|
+
const needDecimals = amount !== 'max';
|
|
42
|
+
const chainId = CHAIN_ID_MAP[chain];
|
|
43
|
+
let decimals;
|
|
44
|
+
let currentAllowance;
|
|
45
|
+
try {
|
|
46
|
+
const views = await readDecimalsAndAllowances(provider, chainId, tokenAddress, [wallet.address], approvalTarget, needDecimals);
|
|
47
|
+
decimals = views.decimals;
|
|
48
|
+
currentAllowance = views.allowances[0];
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
decimals = needDecimals ? await getTokenDecimals(tokenAddress, provider) : 18;
|
|
52
|
+
currentAllowance = await token.allowance(wallet.address, approvalTarget);
|
|
53
|
+
}
|
|
19
54
|
const amountBigInt = amount === 'max' ? ethers.MaxUint256 : ethers.parseUnits(amount, decimals);
|
|
20
55
|
if (currentAllowance >= amountBigInt) {
|
|
21
56
|
return { txHash: '', approved: true };
|
|
@@ -38,12 +73,17 @@ export async function approvePancakeProxyBatch(params) {
|
|
|
38
73
|
const approvalTarget = getApprovalTarget(routeType || 'v2');
|
|
39
74
|
const chainId = CHAIN_ID_MAP[chain];
|
|
40
75
|
const provider = getCachedProvider(chain, config.rpcUrl, chainId);
|
|
41
|
-
const gasPrice = await getOptimizedGasPrice(provider, getGasPriceConfig(config));
|
|
42
|
-
const decimals = await getTokenDecimals(tokenAddress, provider);
|
|
43
76
|
const wallets = privateKeys.map((k) => new Wallet(k, provider));
|
|
77
|
+
const needDecimals = amounts.some((amount) => amount !== 'max');
|
|
78
|
+
const [gasPrice, views] = await Promise.all([
|
|
79
|
+
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
80
|
+
readDecimalsAndAllowances(provider, chainId, tokenAddress, wallets.map((wallet) => wallet.address), approvalTarget, needDecimals).catch(async () => ({
|
|
81
|
+
decimals: needDecimals ? await getTokenDecimals(tokenAddress, provider) : 18,
|
|
82
|
+
allowances: await batchCheckAllowances(provider, tokenAddress, wallets.map((wallet) => wallet.address), approvalTarget),
|
|
83
|
+
})),
|
|
84
|
+
]);
|
|
85
|
+
const { decimals, allowances } = views;
|
|
44
86
|
const amountsBigInt = amounts.map((a) => (a === 'max' ? ethers.MaxUint256 : ethers.parseUnits(a, decimals)));
|
|
45
|
-
const tokens = wallets.map((w) => new Contract(tokenAddress, ERC20_ABI, w));
|
|
46
|
-
const allowances = await Promise.all(tokens.map((token, i) => token.allowance(wallets[i].address, approvalTarget)));
|
|
47
87
|
const needApproval = wallets.filter((_, i) => allowances[i] < amountsBigInt[i]);
|
|
48
88
|
const needApprovalAmounts = amountsBigInt.filter((amount, i) => allowances[i] < amount);
|
|
49
89
|
if (needApproval.length === 0) {
|
|
@@ -6,10 +6,11 @@ import { getGasLimit } from '../../../core/gas/bundle-gas.js';
|
|
|
6
6
|
import { calculateSellAmount } from '../../../utils/swap-helpers.js';
|
|
7
7
|
import { NonceManager, getOptimizedGasPrice } from '../../../utils/bundle-helpers.js';
|
|
8
8
|
import { ZERO_ADDRESS } from '../../../core/constants/index.js';
|
|
9
|
+
import { batchGetBalances } from '../../../bundle/internal.js';
|
|
9
10
|
import { quoteRoundTripProfitToNativeByChainId } from '../../../core/quote/quote-helpers.js';
|
|
10
11
|
import { getGasPriceConfig, getTxType, getBribeAmount, BLOCKRAZOR_BUILDER_EOA, PORTAL_ABI, } from './config.js';
|
|
11
12
|
import { buildTransactionRequest } from '../../../core/tx/build-request.js';
|
|
12
|
-
import { BRIBE_GAS_LIMIT,
|
|
13
|
+
import { BRIBE_GAS_LIMIT, } from './swap-helpers.js';
|
|
13
14
|
import { buildApprovalTransaction, buildProfitTransaction, calculateProfitAmount, createChainContext, grossUpSellProceedsToBuySide, quoteSellOutput, } from './swap-planning.js';
|
|
14
15
|
/**
|
|
15
16
|
* Flap 内盘批量换手(一卖多买)
|
|
@@ -83,23 +84,20 @@ export async function flapBatchSwapMerkle(params) {
|
|
|
83
84
|
}
|
|
84
85
|
// ✅ 并行验证所有买方余额
|
|
85
86
|
const reserveGas = ethers.parseEther((config.reserveGasETH || 0.0005).toString());
|
|
86
|
-
const
|
|
87
|
-
|
|
87
|
+
const buyerBalances = await batchGetBalances(chainContext.provider, buyers.map((buyer) => buyer.address), useNativeToken ? undefined : quoteToken, chainContext.chainId);
|
|
88
|
+
buyerBalances.forEach((buyerBalance, i) => {
|
|
88
89
|
const buyAmount = buyAmountsWei[i];
|
|
89
90
|
if (useNativeToken) {
|
|
90
|
-
const buyerBalance = await buyer.provider.getBalance(buyer.address);
|
|
91
91
|
const required = buyAmount + reserveGas;
|
|
92
92
|
if (buyerBalance < required) {
|
|
93
93
|
throw new Error(`买方 ${i + 1} 余额不足: 需要 ${ethers.formatEther(required)} ${chainContext.nativeToken}, 实际 ${ethers.formatEther(buyerBalance)}`);
|
|
94
94
|
}
|
|
95
|
+
return;
|
|
95
96
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (buyerBalance < buyAmount) {
|
|
99
|
-
throw new Error(`买方 ${i + 1} 代币余额不足: 需要 ${ethers.formatUnits(buyAmount, quoteTokenDecimals)}, 实际 ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`);
|
|
100
|
-
}
|
|
97
|
+
if (buyerBalance < buyAmount) {
|
|
98
|
+
throw new Error(`买方 ${i + 1} 代币余额不足: 需要 ${ethers.formatUnits(buyAmount, quoteTokenDecimals)}, 实际 ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`);
|
|
101
99
|
}
|
|
102
|
-
})
|
|
100
|
+
});
|
|
103
101
|
// ✅ 计算利润。换手按卖出代币的买侧计价收万六,避免曲线折价后只剩万五点多
|
|
104
102
|
const profitNotional = await grossUpSellProceedsToBuySide({
|
|
105
103
|
provider: chainContext.provider,
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* Flap BuyFirst 报价/资金/余额 helper(从 swap-buy-first-helpers.ts 抽取)
|
|
3
3
|
*/
|
|
4
4
|
import { ethers, Contract } from 'ethers';
|
|
5
|
-
import { ZERO_ADDRESS } from '../../../core/constants/index.js';
|
|
5
|
+
import { ZERO_ADDRESS, getMulticall3Address } from '../../../core/constants/index.js';
|
|
6
6
|
import { ERC20_BALANCE_ABI } from '../../../core/shared-abis/common.js';
|
|
7
|
+
import { MULTICALL3_ABI } from '../../../abis/common.js';
|
|
7
8
|
import { getErrorMessageFromUnknown } from '../../../core/index.js';
|
|
8
9
|
import { PORTAL_ABI } from './config.js';
|
|
9
10
|
import { getTokenToNativeQuoteByChainId } from '../../../core/quote/quote-helpers.js';
|
|
11
|
+
import { batchGetTokenAndNativeBalances } from '../../../bundle/internal.js';
|
|
10
12
|
/**
|
|
11
13
|
* 获取 ERC20 代币 → 原生代币的报价
|
|
12
14
|
* @param provider - Provider 实例
|
|
@@ -18,19 +20,18 @@ import { getTokenToNativeQuoteByChainId } from '../../../core/quote/quote-helper
|
|
|
18
20
|
export async function getTokenToNativeQuote(provider, tokenAddress, tokenAmount, chainId) {
|
|
19
21
|
return getTokenToNativeQuoteByChainId(provider, tokenAddress, tokenAmount, chainId);
|
|
20
22
|
}
|
|
21
|
-
// ✅ 使用公共模块中的 ERC20_BALANCE_ABI
|
|
22
|
-
const ERC20_BALANCE_OF_ABI = ERC20_BALANCE_ABI;
|
|
23
23
|
export async function calculateBuyerFunds({ buyer, buyerFunds, buyerFundsPercentage, reserveGasEth, nativeToken, useNativeToken = true, quoteToken, quoteTokenDecimals = 18, provider, }) {
|
|
24
24
|
const reserveGas = ethers.parseEther((reserveGasEth || 0.0005).toString());
|
|
25
25
|
// ✅ 根据是否使用原生代币获取不同的余额
|
|
26
26
|
let buyerBalance;
|
|
27
|
+
let buyerNativeBalance = 0n;
|
|
27
28
|
if (useNativeToken) {
|
|
28
29
|
buyerBalance = await buyer.provider.getBalance(buyer.address);
|
|
29
30
|
}
|
|
30
31
|
else {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
const paired = await batchGetTokenAndNativeBalances((provider || buyer.provider), [buyer.address], quoteToken, 56);
|
|
33
|
+
buyerBalance = paired.token[0] ?? 0n;
|
|
34
|
+
buyerNativeBalance = paired.native[0] ?? 0n;
|
|
34
35
|
}
|
|
35
36
|
let buyerFundsWei;
|
|
36
37
|
if (buyerFunds !== undefined) {
|
|
@@ -63,7 +64,7 @@ export async function calculateBuyerFunds({ buyer, buyerFunds, buyerFundsPercent
|
|
|
63
64
|
throw new Error(`买方代币余额不足: 需要 ${ethers.formatUnits(buyerFundsWei, quoteTokenDecimals)},实际 ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`);
|
|
64
65
|
}
|
|
65
66
|
// ✅ ERC20 购买时,还需要检查买方是否有足够 BNB 支付 Gas
|
|
66
|
-
const buyerBnbBalance =
|
|
67
|
+
const buyerBnbBalance = buyerNativeBalance;
|
|
67
68
|
if (buyerBnbBalance < reserveGas) {
|
|
68
69
|
throw new Error(`买方 BNB 余额不足 (用于支付 Gas): 需要 ${ethers.formatEther(reserveGas)} ${nativeToken},实际 ${ethers.formatEther(buyerBnbBalance)} ${nativeToken}`);
|
|
69
70
|
}
|
|
@@ -96,8 +97,22 @@ export async function quoteBuyerOutput({ portalAddress, tokenAddress, buyerFunds
|
|
|
96
97
|
return { quotedToken, sellAmountWei };
|
|
97
98
|
}
|
|
98
99
|
export async function ensureSellerBalance({ tokenAddress, provider, seller, sellAmountWei, skipBalanceCheck, }) {
|
|
99
|
-
const
|
|
100
|
-
const
|
|
100
|
+
const ercIface = new ethers.Interface(ERC20_BALANCE_ABI);
|
|
101
|
+
const chainId = Number((await provider.getNetwork()).chainId);
|
|
102
|
+
const multicall = new Contract(getMulticall3Address(chainId), MULTICALL3_ABI, provider);
|
|
103
|
+
const aggregate3 = multicall.aggregate3;
|
|
104
|
+
if (!aggregate3)
|
|
105
|
+
throw new Error('aggregate3 missing');
|
|
106
|
+
const rows = (await aggregate3.staticCall([
|
|
107
|
+
{
|
|
108
|
+
target: tokenAddress,
|
|
109
|
+
allowFailure: false,
|
|
110
|
+
callData: ercIface.encodeFunctionData('balanceOf', [seller.address]),
|
|
111
|
+
},
|
|
112
|
+
{ target: tokenAddress, allowFailure: false, callData: ercIface.encodeFunctionData('decimals', []) },
|
|
113
|
+
]));
|
|
114
|
+
const sellerTokenBal = ercIface.decodeFunctionResult('balanceOf', rows[0].returnData)[0];
|
|
115
|
+
const decimals = Number(ercIface.decodeFunctionResult('decimals', rows[1].returnData)[0]);
|
|
101
116
|
if (!skipBalanceCheck && sellerTokenBal < sellAmountWei) {
|
|
102
117
|
throw new Error(`卖方代币余额不足: 需要 ${ethers.formatUnits(sellAmountWei, decimals)},实际 ${ethers.formatUnits(sellerTokenBal, decimals)}`);
|
|
103
118
|
}
|
|
@@ -6,6 +6,7 @@ import { ERC20_ALLOWANCE_ABI } from '../../../core/shared-abis/common.js';
|
|
|
6
6
|
import { getGasPriceConfig, getTxType, getProfitRecipient, PORTAL_ABI } from './config.js';
|
|
7
7
|
import { getErrorMessageFromUnknown } from '../../../core/index.js';
|
|
8
8
|
import { createSignProvider } from '../../../bundle/sign-context-helpers.js';
|
|
9
|
+
import { batchGetBalances } from '../../../bundle/internal.js';
|
|
9
10
|
import { APPROVE_INTERFACE, ERC20_BALANCE_OF_ABI, getChainId, getNativeTokenName, } from './swap-helpers.js';
|
|
10
11
|
const providerCache = new Map();
|
|
11
12
|
const PROVIDER_CACHE_TTL_MS = 60 * 1000;
|
|
@@ -113,9 +114,12 @@ export async function calculateBuyerNeed({ buyer, quotedNative, reserveGasEth, n
|
|
|
113
114
|
return { reserveGas, buyerBalance, buyerNeedTotal, maxBuyerValue };
|
|
114
115
|
}
|
|
115
116
|
export async function validateBalances({ buyerAddress, portalGasCost, provider, chainContext, seller, useNativeToken, }) {
|
|
116
|
-
const
|
|
117
|
+
const nativeBalances = useNativeToken
|
|
118
|
+
? [await provider.getBalance(seller.address)]
|
|
119
|
+
: await batchGetBalances(provider, [seller.address, buyerAddress], undefined, chainContext.chainId);
|
|
120
|
+
const sellerBalance = nativeBalances[0] ?? 0n;
|
|
117
121
|
if (!useNativeToken) {
|
|
118
|
-
const buyerBnbBalance =
|
|
122
|
+
const buyerBnbBalance = nativeBalances[1] ?? 0n;
|
|
119
123
|
const buyerGasCost = portalGasCost;
|
|
120
124
|
if (buyerBnbBalance < buyerGasCost) {
|
|
121
125
|
throw new Error(`买方 BNB 余额不足 (用于支付 Gas):\n` +
|