four-flap-meme-sdk 4.3.21 → 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.
Files changed (82) hide show
  1. package/dist/bundle/four-meme/approve-tokenmanager.js +46 -16
  2. package/dist/bundle/four-meme/core/create-token-quote.d.ts +5 -1
  3. package/dist/bundle/four-meme/core/create-token-quote.js +29 -4
  4. package/dist/bundle/four-meme/core/sell-flow.js +3 -3
  5. package/dist/bundle/four-meme/pancake-proxy/approve.js +47 -10
  6. package/dist/bundle/four-meme/private/sell.js +2 -10
  7. package/dist/bundle/four-meme/swap/bundle.js +4 -4
  8. package/dist/bundle/four-meme/swap-buy-first/multi-wallet.js +25 -8
  9. package/dist/bundle/four-meme/swap-buy-first/single.js +30 -9
  10. package/dist/bundle/four-meme/swap-sign-helpers.js +56 -3
  11. package/dist/bundle/four-meme/utils-sweep/erc20-no-hop.js +18 -7
  12. package/dist/bundle/four-meme/utils-sweep/with-hops.js +10 -4
  13. package/dist/bundle/internal.d.ts +5 -0
  14. package/dist/bundle/internal.js +36 -0
  15. package/dist/chains/bsc/pancake/bundle-buy-first-quote.js +24 -8
  16. package/dist/chains/bsc/pancake/bundle-buy-first-types.d.ts +2 -0
  17. package/dist/chains/bsc/pancake/bundle-buy-first-validate.d.ts +1 -1
  18. package/dist/chains/bsc/pancake/bundle-buy-first-validate.js +2 -2
  19. package/dist/chains/bsc/pancake/bundle-buy-first.js +1 -0
  20. package/dist/chains/bsc/pancake/bundle-swap-types.d.ts +2 -0
  21. package/dist/chains/bsc/pancake/quote.d.ts +7 -0
  22. package/dist/chains/bsc/pancake/quote.js +9 -1
  23. package/dist/chains/bsc/pancake/swap-batch.js +15 -16
  24. package/dist/chains/bsc/pancake/swap-build.d.ts +1 -1
  25. package/dist/chains/bsc/pancake/swap-build.js +15 -9
  26. package/dist/chains/bsc/pancake/swap-quick.js +4 -4
  27. package/dist/chains/bsc/pancake/swap-single.js +1 -0
  28. package/dist/chains/bsc/platforms/iro/token.d.ts +1 -0
  29. package/dist/chains/bsc/platforms/iro/token.js +36 -1
  30. package/dist/chains/eni/batch-router/transfer-sweep.js +10 -14
  31. package/dist/chains/eni/platforms/daoaas/graduation.js +60 -14
  32. package/dist/chains/eni/platforms/daoaas/meta.d.ts +0 -4
  33. package/dist/chains/eni/platforms/daoaas/meta.js +112 -27
  34. package/dist/chains/eni/platforms/daoaas/portal-direct-submit.js +11 -6
  35. package/dist/chains/eni/platforms/daoaas/portal-direct-tx.js +4 -2
  36. package/dist/chains/eni/platforms/daoaas/portal.d.ts +5 -0
  37. package/dist/chains/eni/platforms/daoaas/portal.js +63 -9
  38. package/dist/chains/eni/platforms/dswap/liquidity.js +144 -49
  39. package/dist/chains/eni/platforms/fair-launch/launcher.js +2 -10
  40. package/dist/chains/eni/platforms/fair-launch/pool.d.ts +1 -0
  41. package/dist/chains/eni/platforms/fair-launch/pool.js +16 -2
  42. package/dist/chains/eni/platforms/iro/token.d.ts +1 -0
  43. package/dist/chains/eni/platforms/iro/token.js +36 -1
  44. package/dist/chains/robinhood/constants.d.ts +2 -5
  45. package/dist/chains/robinhood/constants.js +8 -7
  46. package/dist/chains/robinhood/longetf/launch.js +4 -4
  47. package/dist/chains/robinhood/pons-v2/launch.js +51 -20
  48. package/dist/chains/xlayer/eip7702/bundle-approve.js +8 -8
  49. package/dist/chains/xlayer/eip7702/bundle-sell.js +3 -4
  50. package/dist/chains/xlayer/eip7702/bundle-swap-batch.js +4 -2
  51. package/dist/chains/xlayer/eip7702/bundle-swap-multi.js +3 -3
  52. package/dist/chains/xlayer/eip7702/multi-hop-transfer-sweep.js +4 -10
  53. package/dist/chains/xlayer/eip7702/volume-bundle.js +45 -24
  54. package/dist/chains/xlayer/eoa/eoa-wash-volume.js +4 -14
  55. package/dist/core/constants/gas.d.ts +0 -8
  56. package/dist/core/constants/gas.js +0 -9
  57. package/dist/core/quote/quote-helpers.d.ts +11 -0
  58. package/dist/core/quote/quote-helpers.js +121 -20
  59. package/dist/core/quote/quote-helpers.test.js +11 -1
  60. package/dist/domains/dex/direct-router-v2-buy.js +4 -2
  61. package/dist/domains/dex/direct-router-v2-sell.js +63 -12
  62. package/dist/domains/dex/direct-router-v3.js +3 -4
  63. package/dist/domains/dex/quote.js +15 -5
  64. package/dist/domains/flap/portal-bundle-merkle/pancake-proxy/approve.js +48 -8
  65. package/dist/domains/flap/portal-bundle-merkle/swap-batch.js +8 -10
  66. package/dist/domains/flap/portal-bundle-merkle/swap-buy-first-quote.js +24 -9
  67. package/dist/domains/flap/portal-bundle-merkle/swap-planning.js +6 -2
  68. package/dist/domains/flap/portal-bundle-merkle/utils/sweep.js +14 -7
  69. package/dist/domains/flap/reader.d.ts +6 -0
  70. package/dist/domains/flap/reader.js +77 -1
  71. package/dist/domains/four/contracts/tm-bundle-batch.js +2 -10
  72. package/dist/domains/four/contracts/tm-bundle-create.js +5 -2
  73. package/dist/domains/four/contracts/tm.d.ts +2 -0
  74. package/dist/domains/four/contracts/tm.js +59 -2
  75. package/dist/domains/four/tax-token.js +60 -28
  76. package/dist/domains/lp-inspect/inspect-tax.js +98 -12
  77. package/dist/domains/lp-inspect/inspect.js +57 -44
  78. package/dist/domains/lp-inspect/registry.js +33 -7
  79. package/dist/domains/openfour/registry.js +15 -5
  80. package/dist/utils/airdrop-sweep.js +37 -17
  81. package/dist/utils/swap-helpers.js +35 -7
  82. 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
- async function bestAmountsOut(router, amountIn, paths) {
66
- const quotePromises = paths.map(async (path) => {
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 = await router.getAmountsOut(amountIn, path);
69
- const amountOut = amounts[amounts.length - 1];
70
- return { amountOut, path, success: amountOut > 0n };
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 { amountOut: 0n, path, success: false };
109
+ return { success: false, amountOut: 0n };
74
110
  }
75
111
  });
76
- const results = await Promise.all(quotePromises);
77
- let best;
78
- for (const result of results) {
79
- if (!result.success)
80
- continue;
81
- if (!best || result.amountOut > best.amountOut) {
82
- best = { amountOut: result.amountOut, path: result.path };
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 router = new Contract(routerAddress, V2_ROUTER_ABI, provider);
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(router, amountIn, hopOnly);
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
- ? Promise.all(wallets.map((w) => quoteTokenContract.allowance(w.address, router).catch(() => 0n)))
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 Promise.all(wallets.map((w) => provider.getBalance(w.address)));
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 [fetchedDecimals, balances, allowances, nonces, gasPrice] = await Promise.all([
31
- inputDecimals !== undefined
32
- ? Promise.resolve(inputDecimals)
33
- : tokenContract
34
- .decimals()
35
- .then((d) => Number(d))
36
- .catch(() => 18),
37
- Promise.all(wallets.map((w) => tokenContract.balanceOf(w.address))),
38
- Promise.all(wallets.map((w) => tokenContract.allowance(w.address, router).catch(() => 0n))),
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 = fetchedDecimals;
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, Contract } from 'ethers';
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 { ERC20_ABI } from '../../abis/common.js';
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
- Promise.all(wallets.map((w) => tokenContract.balanceOf(w.address))),
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 pool = new Contract(poolAddr, XLAYER_V3_POOL_ABI, provider);
27
- const [t0, t1, slot0] = await Promise.all([pool.token0?.(), pool.token1?.(), pool.slot0?.()]);
28
- if (!t0 || !t1 || !slot0)
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 decimals = await getTokenDecimals(tokenAddress, provider);
18
- const currentAllowance = await token.allowance(wallet.address, approvalTarget);
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, ERC20_BALANCE_OF_ABI, } from './swap-helpers.js';
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 erc20Contract = useNativeToken ? null : new Contract(quoteToken, ERC20_BALANCE_OF_ABI, chainContext.provider);
87
- await Promise.all(buyers.map(async (buyer, i) => {
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
- else {
97
- const buyerBalance = await erc20Contract.balanceOf(buyer.address);
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
- // ERC20 代币余额
32
- const erc20 = new Contract(quoteToken, ERC20_BALANCE_OF_ABI, provider || buyer.provider);
33
- buyerBalance = await erc20.balanceOf(buyer.address);
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 = await buyer.provider.getBalance(buyer.address);
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 erc20 = new Contract(tokenAddress, ERC20_BALANCE_ABI, provider);
100
- const [sellerTokenBal, decimals] = await Promise.all([erc20.balanceOf(seller.address), erc20.decimals()]);
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 sellerBalance = await provider.getBalance(seller.address);
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 = await provider.getBalance(buyerAddress);
122
+ const buyerBnbBalance = nativeBalances[1] ?? 0n;
119
123
  const buyerGasCost = portalGasCost;
120
124
  if (buyerBnbBalance < buyerGasCost) {
121
125
  throw new Error(`买方 BNB 余额不足 (用于支付 Gas):\n` +