four-flap-meme-sdk 4.3.13 → 4.3.15
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/core/create-openfour.js +19 -2
- package/dist/chains/bsc/pancake/bundle-swap-types.d.ts +2 -0
- package/dist/chains/bsc/pancake/quote.d.ts +14 -0
- package/dist/chains/bsc/pancake/quote.js +31 -1
- package/dist/chains/bsc/pancake/quote.test.js +21 -1
- package/dist/chains/bsc/pancake/swap-batch.d.ts +1 -0
- package/dist/chains/bsc/pancake/swap-batch.js +14 -8
- package/dist/chains/bsc/pancake/swap-quick.js +16 -12
- package/dist/chains/bsc/pancake/swap-single.js +15 -9
- package/dist/domains/four/tax-token.test.js +2 -0
- package/dist/domains/openfour/create-arg.d.ts +5 -0
- package/dist/domains/openfour/create-arg.js +7 -0
- package/dist/domains/openfour/create-arg.test.d.ts +1 -0
- package/dist/domains/openfour/create-arg.test.js +81 -0
- package/dist/domains/openfour/index.d.ts +1 -1
- package/dist/domains/openfour/index.js +1 -1
- package/dist/exports/root-foundations.d.ts +1 -1
- package/dist/exports/root-foundations.js +1 -1
- package/dist/utils/hop-chains.d.ts +4 -1
- package/dist/utils/hop-chains.js +13 -4
- package/dist/utils/hop-chains.test.d.ts +1 -0
- package/dist/utils/hop-chains.test.js +16 -0
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ import { getTxType, getGasPriceConfig, getBribeAmount, calculateProfit, getProfi
|
|
|
11
11
|
import { signBribeTransaction } from '../../../core/sign-context-helpers.js';
|
|
12
12
|
import { createChainContext, loginFourClient, resolveTokenImage } from '../core-helpers.js';
|
|
13
13
|
import { OPENFOUR_CORE_ABI } from '../../../abis/openfour.js';
|
|
14
|
-
import { computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, loadOpenFourAddresses, } from '../../../domains/openfour/index.js';
|
|
14
|
+
import { OPENFOUR_CREATE_EXPIRED_MESSAGE, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, loadOpenFourAddresses, shouldRefreshOpenFourCreateArg, } from '../../../domains/openfour/index.js';
|
|
15
15
|
import { signQuoteApproveIfNeeded } from './create-token-quote.js';
|
|
16
16
|
const BSC_SUBMIT_FEE_DUST_WEI = 10n ** 12n;
|
|
17
17
|
export function openFourSubmitProfitWei(nativeFlowWei) {
|
|
@@ -32,6 +32,23 @@ function signedNativeFlowWei(signedTxs) {
|
|
|
32
32
|
}
|
|
33
33
|
}, 0n);
|
|
34
34
|
}
|
|
35
|
+
async function readChainTimestamp(provider) {
|
|
36
|
+
const block = await provider.getBlock('latest');
|
|
37
|
+
return BigInt(block?.timestamp ?? Math.floor(Date.now() / 1000));
|
|
38
|
+
}
|
|
39
|
+
async function fetchFreshOpenFourCreateArg(fourClient, accessToken, body, provider) {
|
|
40
|
+
const pull = () => fourClient.createOpenFourToken(accessToken, body);
|
|
41
|
+
let createResp = await pull();
|
|
42
|
+
let decoded = decodeOpenFourCreateArg(createResp.createArg);
|
|
43
|
+
if (shouldRefreshOpenFourCreateArg(decoded.validTimestamp, await readChainTimestamp(provider))) {
|
|
44
|
+
createResp = await pull();
|
|
45
|
+
decoded = decodeOpenFourCreateArg(createResp.createArg);
|
|
46
|
+
}
|
|
47
|
+
if (shouldRefreshOpenFourCreateArg(decoded.validTimestamp, await readChainTimestamp(provider))) {
|
|
48
|
+
throw new Error(OPENFOUR_CREATE_EXPIRED_MESSAGE);
|
|
49
|
+
}
|
|
50
|
+
return createResp;
|
|
51
|
+
}
|
|
35
52
|
function toRaisedToken(cfg) {
|
|
36
53
|
return {
|
|
37
54
|
symbol: cfg.symbol,
|
|
@@ -84,8 +101,8 @@ export async function createOpenFourTokenMerkle(params) {
|
|
|
84
101
|
totalSupply: cfg.totalSupply,
|
|
85
102
|
raisedAmount: cfg.raisedAmount,
|
|
86
103
|
};
|
|
87
|
-
const createResp = await fourClient.createOpenFourToken(accessToken, body);
|
|
88
104
|
const addrs = await loadOpenFourAddresses(provider);
|
|
105
|
+
const createResp = await fetchFreshOpenFourCreateArg(fourClient, accessToken, body, provider);
|
|
89
106
|
const decoded = decodeOpenFourCreateArg(createResp.createArg);
|
|
90
107
|
const valueWei = computeOpenFourCreateValueWei(decoded, addrs.wrappedNative);
|
|
91
108
|
const gasPrice = await getOptimizedGasPrice(provider, getGasPriceConfig(config));
|
|
@@ -138,6 +138,8 @@ export interface PancakeBundleSwapSignParams {
|
|
|
138
138
|
quoteToken?: string;
|
|
139
139
|
quoteTokenDecimals?: number;
|
|
140
140
|
startNonces?: number[];
|
|
141
|
+
/** 税币基点。卖出按扣税后数量报价,买入 minOut 再扣税。 */
|
|
142
|
+
taxRateBps?: number;
|
|
141
143
|
}
|
|
142
144
|
export interface PancakeBundleSwapParams {
|
|
143
145
|
sellerPrivateKey: string;
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { ethers } from 'ethers';
|
|
2
2
|
import { applySlippageBps } from '../../../core/constants/index.js';
|
|
3
3
|
import { type QuoteBNBResult, type QuoteSellParams, type RouteParams, type UserType } from './bundle-swap-types.js';
|
|
4
|
+
/** 路由卖出端的计价代币(path 最后一项 / v3TokenOut)。 */
|
|
5
|
+
export declare function routeQuoteToken(routeParams: RouteParams): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* 股票池 / USDT 池 path 不是 WBNB 时,即使调用方漏传 quoteToken 也不能走 ETH 买卖。
|
|
8
|
+
* Pancake swapExactETH* 要求 path[0]==WETH,否则 INVALID_PATH。
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolvePancakeQuoteMode(params: {
|
|
11
|
+
quoteToken?: string;
|
|
12
|
+
routeParams: RouteParams;
|
|
13
|
+
wrappedNative: string;
|
|
14
|
+
}): {
|
|
15
|
+
useNativeToken: boolean;
|
|
16
|
+
quoteToken?: string;
|
|
17
|
+
};
|
|
4
18
|
/** Pancake 买卖 minOut;实现见 core applySlippageBps。 */
|
|
5
19
|
export declare const applyPancakeSlippage: typeof applySlippageBps;
|
|
6
20
|
export declare function splitQuotedMins(quotedTotal: bigint, amounts: bigint[], slippageBps?: number): bigint[];
|
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import { ADDRESSES, applySlippageBps, getProfitRateBps as getSharedProfitRateBps, calculateBundleSwapRoundTripProfit } from '../../../core/constants/index.js';
|
|
2
2
|
import { quoteV2, quoteV3 } from '../../../core/quote/quote-helpers.js';
|
|
3
|
-
import { WBNB_ADDRESS, } from './bundle-swap-types.js';
|
|
3
|
+
import { WBNB_ADDRESS, ZERO_ADDRESS, } from './bundle-swap-types.js';
|
|
4
|
+
function normalizeAddr(addr) {
|
|
5
|
+
return String(addr || '').trim().toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
/** 路由卖出端的计价代币(path 最后一项 / v3TokenOut)。 */
|
|
8
|
+
export function routeQuoteToken(routeParams) {
|
|
9
|
+
if (routeParams.routeType === 'v2' && routeParams.v2Path?.length) {
|
|
10
|
+
return routeParams.v2Path[routeParams.v2Path.length - 1];
|
|
11
|
+
}
|
|
12
|
+
if (routeParams.routeType === 'v3-single') {
|
|
13
|
+
return routeParams.v3TokenOut;
|
|
14
|
+
}
|
|
15
|
+
const multiPath = routeParams.v2Path;
|
|
16
|
+
if (multiPath?.length)
|
|
17
|
+
return multiPath[multiPath.length - 1];
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 股票池 / USDT 池 path 不是 WBNB 时,即使调用方漏传 quoteToken 也不能走 ETH 买卖。
|
|
22
|
+
* Pancake swapExactETH* 要求 path[0]==WETH,否则 INVALID_PATH。
|
|
23
|
+
*/
|
|
24
|
+
export function resolvePancakeQuoteMode(params) {
|
|
25
|
+
const wrapped = normalizeAddr(params.wrappedNative);
|
|
26
|
+
const explicit = normalizeAddr(params.quoteToken);
|
|
27
|
+
const token = explicit && explicit !== normalizeAddr(ZERO_ADDRESS) ? params.quoteToken.trim() : routeQuoteToken(params.routeParams);
|
|
28
|
+
const tokenNorm = normalizeAddr(token);
|
|
29
|
+
if (!tokenNorm || tokenNorm === normalizeAddr(ZERO_ADDRESS) || tokenNorm === wrapped) {
|
|
30
|
+
return { useNativeToken: true };
|
|
31
|
+
}
|
|
32
|
+
return { useNativeToken: false, quoteToken: token };
|
|
33
|
+
}
|
|
4
34
|
/** Pancake 买卖 minOut;实现见 core applySlippageBps。 */
|
|
5
35
|
export const applyPancakeSlippage = applySlippageBps;
|
|
6
36
|
export function splitQuotedMins(quotedTotal, amounts, slippageBps = 100) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { reverseBuyRouteParams } from './bundle-buy-first-quote.js';
|
|
3
|
-
import { applyPancakeSlippage, applyTaxBps, taxedSellQuoteInput, isSameWalletBuySell, resolveBuyFirstSellMins, sequentialBuyAmountOutMins, sequentialPrefixOutputs, pairSameWalletSellAmounts, splitQuotedMins, } from './quote.js';
|
|
3
|
+
import { applyPancakeSlippage, applyTaxBps, taxedSellQuoteInput, isSameWalletBuySell, resolveBuyFirstSellMins, sequentialBuyAmountOutMins, sequentialPrefixOutputs, pairSameWalletSellAmounts, splitQuotedMins, resolvePancakeQuoteMode, } from './quote.js';
|
|
4
4
|
describe('applyPancakeSlippage', () => {
|
|
5
5
|
it('drops 1% by default', () => {
|
|
6
6
|
expect(applyPancakeSlippage(1000000n)).toBe(990000n);
|
|
@@ -36,6 +36,26 @@ describe('taxedSellQuoteInput', () => {
|
|
|
36
36
|
expect(taxedSellQuoteInput(10000n, 100)).toBe(9900n);
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
|
+
describe('resolvePancakeQuoteMode', () => {
|
|
40
|
+
const wbnb = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c';
|
|
41
|
+
const spcxb = '0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1';
|
|
42
|
+
const token = '0x7db2f0c15f74ab0dc0d4f49650dd60c45ffb7777';
|
|
43
|
+
it('treats omitted quoteToken as ERC20 when v2 path ends at a stock quote', () => {
|
|
44
|
+
const mode = resolvePancakeQuoteMode({
|
|
45
|
+
routeParams: { routeType: 'v2', v2Path: [token, spcxb] },
|
|
46
|
+
wrappedNative: wbnb,
|
|
47
|
+
});
|
|
48
|
+
expect(mode.useNativeToken).toBe(false);
|
|
49
|
+
expect(mode.quoteToken?.toLowerCase()).toBe(spcxb.toLowerCase());
|
|
50
|
+
});
|
|
51
|
+
it('keeps native mode when v2 path ends at wrapped BNB', () => {
|
|
52
|
+
const mode = resolvePancakeQuoteMode({
|
|
53
|
+
routeParams: { routeType: 'v2', v2Path: [token, wbnb] },
|
|
54
|
+
wrappedNative: wbnb,
|
|
55
|
+
});
|
|
56
|
+
expect(mode.useNativeToken).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
39
59
|
describe('pairSameWalletSellAmounts', () => {
|
|
40
60
|
it('pairs each wash wallet sell to that wallet guaranteed buy out', () => {
|
|
41
61
|
expect(pairSameWalletSellAmounts(true, [900n, 700n], 2)).toEqual([900n, 700n]);
|
|
@@ -6,7 +6,7 @@ import { getGasLimit } from '../../../core/gas/bundle-gas.js';
|
|
|
6
6
|
import { ethers, Contract, Wallet } from 'ethers';
|
|
7
7
|
import { calculateSellAmount } from '../../../utils/swap-helpers.js';
|
|
8
8
|
import { NonceManager, getDeadline, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
|
|
9
|
-
import { BRIBE_GAS_LIMIT, ERC20_BALANCE_OF_ABI, FLAT_FEE, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS,
|
|
9
|
+
import { BRIBE_GAS_LIMIT, ERC20_BALANCE_OF_ABI, FLAT_FEE, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS, calculateProfitAmount, createPancakeContext, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, quoteSellOutput, quoteBuyOutput, applyPancakeSlippage, resolvePancakeQuoteMode, taxedSellQuoteInput, sequentialBuyAmountOutMins, } from './bundle-swap-helpers.js';
|
|
10
10
|
/**
|
|
11
11
|
* PancakeSwap 批量换手(一卖多买)
|
|
12
12
|
*
|
|
@@ -17,7 +17,7 @@ import { BRIBE_GAS_LIMIT, ERC20_BALANCE_OF_ABI, FLAT_FEE, PANCAKE_V2_ROUTER_ABI,
|
|
|
17
17
|
*/
|
|
18
18
|
export async function pancakeBatchSwapMerkle(params) {
|
|
19
19
|
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, buyerAmounts, tokenAddress, routeParams, config, quoteToken, quoteTokenDecimals = 18, startNonces, // ✅ 可选:前端预获取的 nonces
|
|
20
|
-
|
|
20
|
+
taxRateBps = 0, } = params;
|
|
21
21
|
// ✅ 校验买方数量(最多 24 个)
|
|
22
22
|
const MAX_BUYERS = 24;
|
|
23
23
|
if (buyerPrivateKeys.length === 0) {
|
|
@@ -26,7 +26,13 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
26
26
|
if (buyerPrivateKeys.length > MAX_BUYERS) {
|
|
27
27
|
throw new Error(`买方钱包数量超过限制: ${buyerPrivateKeys.length} > ${MAX_BUYERS}`);
|
|
28
28
|
}
|
|
29
|
-
const
|
|
29
|
+
const quoteMode = resolvePancakeQuoteMode({
|
|
30
|
+
quoteToken,
|
|
31
|
+
routeParams,
|
|
32
|
+
wrappedNative: WBNB_ADDRESS,
|
|
33
|
+
});
|
|
34
|
+
const useNativeToken = quoteMode.useNativeToken;
|
|
35
|
+
const effectiveQuoteToken = quoteMode.quoteToken;
|
|
30
36
|
const context = createPancakeContext(config);
|
|
31
37
|
const seller = new Wallet(sellerPrivateKey, context.provider);
|
|
32
38
|
const buyers = buyerPrivateKeys.map((pk) => new Wallet(pk, context.provider));
|
|
@@ -42,7 +48,7 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
42
48
|
const { amount: sellAmountWei, decimals } = sellAmountResult;
|
|
43
49
|
const quoteResult = await quoteSellOutput({
|
|
44
50
|
routeParams,
|
|
45
|
-
sellAmountWei,
|
|
51
|
+
sellAmountWei: taxedSellQuoteInput(sellAmountWei, taxRateBps),
|
|
46
52
|
provider: context.provider,
|
|
47
53
|
v2RouterAddress: config.v2RouterAddress,
|
|
48
54
|
});
|
|
@@ -70,7 +76,7 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
70
76
|
}
|
|
71
77
|
// ✅ 并行验证所有买方余额
|
|
72
78
|
const reserveGas = ethers.parseEther((config.reserveGasBNB ?? 0.0005).toString());
|
|
73
|
-
const erc20Contract = useNativeToken ? null : new Contract(
|
|
79
|
+
const erc20Contract = useNativeToken ? null : new Contract(effectiveQuoteToken, ERC20_BALANCE_OF_ABI, context.provider);
|
|
74
80
|
await Promise.all(buyers.map(async (buyer, i) => {
|
|
75
81
|
const buyAmount = buyAmountsWei[i];
|
|
76
82
|
if (useNativeToken) {
|
|
@@ -96,7 +102,7 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
96
102
|
provider: context.provider,
|
|
97
103
|
v2RouterAddress: config.v2RouterAddress,
|
|
98
104
|
})));
|
|
99
|
-
const buyMinOuts = buyQuotedOuts
|
|
105
|
+
const buyMinOuts = sequentialBuyAmountOutMins(buyQuotedOuts, taxRateBps, slippageBps);
|
|
100
106
|
const deadline = getDeadline();
|
|
101
107
|
const v3RouterIface = new ethers.Interface(PANCAKE_V3_ROUTER_ABI);
|
|
102
108
|
const batchV2Addr = config.v2RouterAddress || PANCAKE_V2_ROUTER_ADDRESS;
|
|
@@ -219,7 +225,7 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
219
225
|
tokenAddress,
|
|
220
226
|
sellAmountWei,
|
|
221
227
|
sellProceedsWei: estimatedBNBOut,
|
|
222
|
-
quoteTokenIn: useNativeToken ? WBNB_ADDRESS :
|
|
228
|
+
quoteTokenIn: useNativeToken ? WBNB_ADDRESS : effectiveQuoteToken,
|
|
223
229
|
version,
|
|
224
230
|
fee,
|
|
225
231
|
v2RouterAddress: config.v2RouterAddress,
|
|
@@ -229,7 +235,7 @@ export async function pancakeBatchSwapMerkle(params) {
|
|
|
229
235
|
profitAmount = calculateProfitAmount(profitNotional, config.userType);
|
|
230
236
|
}
|
|
231
237
|
else {
|
|
232
|
-
const estimatedBNBValue = await getERC20ToNativeQuote(context.provider,
|
|
238
|
+
const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, effectiveQuoteToken, profitNotional, version, fee);
|
|
233
239
|
profitAmount = calculateProfitAmount(estimatedBNBValue, config.userType);
|
|
234
240
|
}
|
|
235
241
|
// 计算利润 nonce
|
|
@@ -7,7 +7,7 @@ import { ethers, Contract, Wallet } from 'ethers';
|
|
|
7
7
|
import { calculateSellAmount } from '../../../utils/swap-helpers.js';
|
|
8
8
|
import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
|
|
9
9
|
import { ERC20_ABI } from '../../../abis/common.js';
|
|
10
|
-
import { BRIBE_GAS_LIMIT, FLAT_FEE, NATIVE_TRANSFER_GAS_LIMIT, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS,
|
|
10
|
+
import { BRIBE_GAS_LIMIT, FLAT_FEE, NATIVE_TRANSFER_GAS_LIMIT, PANCAKE_V2_ROUTER_ABI, PANCAKE_V2_ROUTER_ADDRESS, PANCAKE_V3_ROUTER_ABI, PANCAKE_V3_ROUTER_ADDRESS, WBNB_ADDRESS, buildBNBHopChainForERC20, buildERC20HopChain, buildNativeHopChain, calculateProfitAmount, createPancakeContext, generateDisperseHopPaths, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, quoteSellOutput, quoteBuyOutput, applyPancakeSlippage, sequentialBuyAmountOutMins, taxedSellQuoteInput, resolvePancakeQuoteMode, } from './bundle-swap-helpers.js';
|
|
11
11
|
/**
|
|
12
12
|
* PancakeSwap 快捷批量换手(资金自动流转)
|
|
13
13
|
*
|
|
@@ -26,13 +26,17 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
26
26
|
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, buyerRatios, buyerAmounts, tokenAddress, routeParams, config, quoteToken, quoteTokenDecimals = 18, disperseHopCount = 0, // ✅ 转账多跳数(默认0=直接转账)
|
|
27
27
|
startNonces, // ✅ 可选:前端预获取的 nonces
|
|
28
28
|
taxRateBps = 0, } = params;
|
|
29
|
-
// ✅ 判断是否使用原生代币
|
|
30
|
-
const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
|
|
31
|
-
// ✅ 根据 chainId 获取正确的 Wrapped Native Token 地址
|
|
32
29
|
const chainId = config.chainId ?? CHAINS.BSC.chainId;
|
|
33
30
|
const WRAPPED_NATIVE = chainId === CHAINS.XLAYER.chainId
|
|
34
|
-
? ADDRESSES.XLAYER.WOKB.toLowerCase()
|
|
35
|
-
: ADDRESSES.BSC.WBNB.toLowerCase();
|
|
31
|
+
? ADDRESSES.XLAYER.WOKB.toLowerCase()
|
|
32
|
+
: ADDRESSES.BSC.WBNB.toLowerCase();
|
|
33
|
+
const quoteMode = resolvePancakeQuoteMode({
|
|
34
|
+
quoteToken,
|
|
35
|
+
routeParams,
|
|
36
|
+
wrappedNative: WRAPPED_NATIVE,
|
|
37
|
+
});
|
|
38
|
+
const useNativeToken = quoteMode.useNativeToken;
|
|
39
|
+
const effectiveQuoteToken = quoteMode.quoteToken;
|
|
36
40
|
// ✅ 动态计算最大买方数量(根据多跳数)
|
|
37
41
|
// 固定开销: 贿赂(1) + 卖出(1) + 利润多跳(PROFIT_HOP_COUNT + 1)
|
|
38
42
|
// BNB 模式(无多跳): 转账(N) + 买入(N) = 2N
|
|
@@ -98,8 +102,8 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
98
102
|
}
|
|
99
103
|
else {
|
|
100
104
|
// ERC20 模式:输出必须是指定的 quoteToken
|
|
101
|
-
if (!sellOutputToken || sellOutputToken.toLowerCase() !==
|
|
102
|
-
throw new Error(`ERC20 模式要求卖出路径以 ${
|
|
105
|
+
if (!sellOutputToken || sellOutputToken.toLowerCase() !== effectiveQuoteToken.toLowerCase()) {
|
|
106
|
+
throw new Error(`ERC20 模式要求卖出路径以 ${effectiveQuoteToken} 结尾(当前输出: ${sellOutputToken || '未知'})。`);
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
// ✅ 创建共享资源
|
|
@@ -127,7 +131,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
127
131
|
tokenAddress,
|
|
128
132
|
sellAmountWei,
|
|
129
133
|
sellProceedsWei: estimatedOutput,
|
|
130
|
-
quoteTokenIn: useNativeToken ? WBNB_ADDRESS :
|
|
134
|
+
quoteTokenIn: useNativeToken ? WBNB_ADDRESS : effectiveQuoteToken,
|
|
131
135
|
version,
|
|
132
136
|
fee,
|
|
133
137
|
v2RouterAddress: config.v2RouterAddress,
|
|
@@ -137,7 +141,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
137
141
|
profitAmount = calculateProfitAmount(profitNotional, config.userType);
|
|
138
142
|
}
|
|
139
143
|
else {
|
|
140
|
-
const estimatedBNBValue = await getERC20ToNativeQuote(context.provider,
|
|
144
|
+
const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, effectiveQuoteToken, profitNotional, version, fee);
|
|
141
145
|
profitAmount = calculateProfitAmount(estimatedBNBValue, config.userType);
|
|
142
146
|
}
|
|
143
147
|
const distributableAmount = estimatedOutput - (useNativeToken ? profitAmount : 0n);
|
|
@@ -295,7 +299,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
295
299
|
transferTxs = await Promise.all(buyers.map((buyer, i) => {
|
|
296
300
|
const transferData = erc20Interface.encodeFunctionData('transfer', [buyer.address, transferAmountsWei[i]]);
|
|
297
301
|
return seller.signTransaction({
|
|
298
|
-
to:
|
|
302
|
+
to: effectiveQuoteToken,
|
|
299
303
|
data: transferData,
|
|
300
304
|
value: 0n,
|
|
301
305
|
nonce: transferNonces[i],
|
|
@@ -333,7 +337,7 @@ export async function pancakeQuickBatchSwapMerkle(params) {
|
|
|
333
337
|
// 2. 构建 ERC20 多跳链
|
|
334
338
|
const erc20HopChains = await Promise.all(hopPaths.map((path, i) => {
|
|
335
339
|
const payerNonce = sellerNonce + i; // ✅ payer 每个 buyer 只用 1 个 nonce
|
|
336
|
-
return buildERC20HopChain(seller, path,
|
|
340
|
+
return buildERC20HopChain(seller, path, effectiveQuoteToken, transferAmountsWei[i], gasPrice, context.chainId, txType, payerNonce);
|
|
337
341
|
}));
|
|
338
342
|
const erc20Txs = erc20HopChains.flat();
|
|
339
343
|
sellerNonce += buyers.length; // ✅ payer 只增加 buyers.length 个 nonce
|
|
@@ -6,12 +6,17 @@ import { getGasLimit } from '../../../core/gas/bundle-gas.js';
|
|
|
6
6
|
import { ethers, Wallet } from 'ethers';
|
|
7
7
|
import { calculateSellAmount } from '../../../utils/swap-helpers.js';
|
|
8
8
|
import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
|
|
9
|
-
import { BRIBE_GAS_LIMIT,
|
|
9
|
+
import { BRIBE_GAS_LIMIT, buildNoncePlanFromNonces, buildSwapTransactions, WBNB_ADDRESS, calculateBuyerBudget, calculateProfitAmount, createPancakeContext, getERC20ToNativeQuote, getGasPrice, grossUpPancakeSellProceedsToBuySide, planNonces, quoteSellOutput, applyPancakeSlippage, quoteBuyOutput, resolvePancakeQuoteMode, taxedSellQuoteInput, sequentialBuyAmountOutMins, validateFinalBalances, } from './bundle-swap-helpers.js';
|
|
10
10
|
export async function pancakeBundleSwapMerkle(params) {
|
|
11
11
|
const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKey, tokenAddress, routeParams, config, quoteToken, quoteTokenDecimals = 18, startNonces, // ✅ 可选:前端预获取的 nonces
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
taxRateBps = 0, } = params;
|
|
13
|
+
const quoteMode = resolvePancakeQuoteMode({
|
|
14
|
+
quoteToken,
|
|
15
|
+
routeParams,
|
|
16
|
+
wrappedNative: WBNB_ADDRESS,
|
|
17
|
+
});
|
|
18
|
+
const useNativeToken = quoteMode.useNativeToken;
|
|
19
|
+
const effectiveQuoteToken = quoteMode.quoteToken;
|
|
15
20
|
const context = createPancakeContext(config);
|
|
16
21
|
const seller = new Wallet(sellerPrivateKey, context.provider);
|
|
17
22
|
const buyer = new Wallet(buyerPrivateKey, context.provider);
|
|
@@ -28,7 +33,7 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
28
33
|
const { amount: sellAmountWei, decimals } = sellAmountResult;
|
|
29
34
|
const quoteResult = await quoteSellOutput({
|
|
30
35
|
routeParams,
|
|
31
|
-
sellAmountWei,
|
|
36
|
+
sellAmountWei: taxedSellQuoteInput(sellAmountWei, taxRateBps),
|
|
32
37
|
provider: context.provider,
|
|
33
38
|
v2RouterAddress: config.v2RouterAddress,
|
|
34
39
|
});
|
|
@@ -37,7 +42,7 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
37
42
|
quotedBNBOut: quoteResult.estimatedBNBOut,
|
|
38
43
|
reserveGasBNB: config.reserveGasBNB,
|
|
39
44
|
useNativeToken,
|
|
40
|
-
quoteToken,
|
|
45
|
+
quoteToken: effectiveQuoteToken,
|
|
41
46
|
quoteTokenDecimals,
|
|
42
47
|
provider: context.provider,
|
|
43
48
|
});
|
|
@@ -48,6 +53,7 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
48
53
|
v2RouterAddress: config.v2RouterAddress,
|
|
49
54
|
});
|
|
50
55
|
const slippageBps = config.slippageBps ?? 100;
|
|
56
|
+
const buyMinOut = sequentialBuyAmountOutMins([quotedBuyOut], taxRateBps, slippageBps)[0] ?? 0n;
|
|
51
57
|
const swapUnsigned = await buildSwapTransactions({
|
|
52
58
|
routeParams,
|
|
53
59
|
sellAmountWei,
|
|
@@ -58,7 +64,7 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
58
64
|
useNativeToken,
|
|
59
65
|
v2RouterAddress: config.v2RouterAddress,
|
|
60
66
|
sellAmountOutMin: applyPancakeSlippage(quoteResult.estimatedBNBOut, slippageBps),
|
|
61
|
-
buyAmountOutMin:
|
|
67
|
+
buyAmountOutMin: buyMinOut,
|
|
62
68
|
});
|
|
63
69
|
const version = routeParams.routeType === 'v2' ? 'v2' : 'v3';
|
|
64
70
|
const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined;
|
|
@@ -67,7 +73,7 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
67
73
|
tokenAddress,
|
|
68
74
|
sellAmountWei,
|
|
69
75
|
sellProceedsWei: quoteResult.estimatedBNBOut,
|
|
70
|
-
quoteTokenIn: useNativeToken ? WBNB_ADDRESS :
|
|
76
|
+
quoteTokenIn: useNativeToken ? WBNB_ADDRESS : effectiveQuoteToken,
|
|
71
77
|
version,
|
|
72
78
|
fee,
|
|
73
79
|
v2RouterAddress: config.v2RouterAddress,
|
|
@@ -77,7 +83,7 @@ export async function pancakeBundleSwapMerkle(params) {
|
|
|
77
83
|
profitAmount = calculateProfitAmount(profitNotional, config.userType);
|
|
78
84
|
}
|
|
79
85
|
else {
|
|
80
|
-
const estimatedBNBValue = await getERC20ToNativeQuote(context.provider,
|
|
86
|
+
const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, effectiveQuoteToken, profitNotional, version, fee);
|
|
81
87
|
profitAmount = calculateProfitAmount(estimatedBNBValue, config.userType);
|
|
82
88
|
}
|
|
83
89
|
// ✅ 获取贿赂金额
|
|
@@ -73,6 +73,7 @@ describe('OpenFour create value', () => {
|
|
|
73
73
|
createFee: parseEther('0.01'),
|
|
74
74
|
presaleQuote: parseEther('0.2'),
|
|
75
75
|
quoteAsset: wbnb,
|
|
76
|
+
validTimestamp: 0n,
|
|
76
77
|
};
|
|
77
78
|
expect(isOpenFourPresaleNative({ ...decoded, wrappedNative: wbnb })).toBe(true);
|
|
78
79
|
expect(computeOpenFourCreateValueWei(decoded, wbnb)).toBe(parseEther('0.21'));
|
|
@@ -82,6 +83,7 @@ describe('OpenFour create value', () => {
|
|
|
82
83
|
createFee: parseEther('0.01'),
|
|
83
84
|
presaleQuote: parseEther('100'),
|
|
84
85
|
quoteAsset: usdt,
|
|
86
|
+
validTimestamp: 0n,
|
|
85
87
|
};
|
|
86
88
|
expect(isOpenFourPresaleNative({ ...decoded, wrappedNative: wbnb })).toBe(false);
|
|
87
89
|
expect(computeOpenFourCreateValueWei(decoded, wbnb)).toBe(parseEther('0.01'));
|
|
@@ -2,8 +2,13 @@ export type OpenFourDecodedCreateArg = {
|
|
|
2
2
|
createFee: bigint;
|
|
3
3
|
presaleQuote: bigint;
|
|
4
4
|
quoteAsset: string;
|
|
5
|
+
validTimestamp: bigint;
|
|
5
6
|
};
|
|
7
|
+
/** Four 签名在链上只活到 validTimestamp;提交前至少留这么多秒,否则重拉 createArg。 */
|
|
8
|
+
export declare const OPENFOUR_CREATE_MIN_REMAIN_SEC = 15n;
|
|
9
|
+
export declare const OPENFOUR_CREATE_EXPIRED_MESSAGE = "OpenFour \u53D1\u5E01\u7B7E\u540D\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u518D\u70B9\u4E00\u6B21\u53D1\u5C04\u3002Four \u7684 validTimestamp \u5F88\u77ED\uFF0C\u63A5\u53E3\u6162\u65F6\u7B7E\u540D\u5230\u8FBE\u94FE\u4E0A\u5C31\u5DF2\u7ECF\u5931\u6548\u3002";
|
|
6
10
|
export declare function decodeOpenFourCreateArg(rawCreateArg: string): OpenFourDecodedCreateArg;
|
|
11
|
+
export declare function shouldRefreshOpenFourCreateArg(validTimestamp: bigint, chainTimestamp: bigint, minRemainSec?: bigint): boolean;
|
|
7
12
|
export declare function isOpenFourPresaleNative(params: {
|
|
8
13
|
presaleQuote: bigint;
|
|
9
14
|
quoteAsset: string;
|
|
@@ -40,6 +40,9 @@ const ABI_13 = [
|
|
|
40
40
|
}),
|
|
41
41
|
];
|
|
42
42
|
const coder = AbiCoder.defaultAbiCoder();
|
|
43
|
+
/** Four 签名在链上只活到 validTimestamp;提交前至少留这么多秒,否则重拉 createArg。 */
|
|
44
|
+
export const OPENFOUR_CREATE_MIN_REMAIN_SEC = 15n;
|
|
45
|
+
export const OPENFOUR_CREATE_EXPIRED_MESSAGE = 'OpenFour 发币签名已过期,请再点一次发射。Four 的 validTimestamp 很短,接口慢时签名到达链上就已经失效。';
|
|
43
46
|
function toBigInt(value) {
|
|
44
47
|
if (value == null)
|
|
45
48
|
return 0n;
|
|
@@ -60,8 +63,12 @@ export function decodeOpenFourCreateArg(rawCreateArg) {
|
|
|
60
63
|
createFee: toBigInt(row.createFee),
|
|
61
64
|
presaleQuote: toBigInt(row.presaleQuote),
|
|
62
65
|
quoteAsset: String(row.initParams?.quoteAsset ?? ''),
|
|
66
|
+
validTimestamp: toBigInt(row.validTimestamp),
|
|
63
67
|
};
|
|
64
68
|
}
|
|
69
|
+
export function shouldRefreshOpenFourCreateArg(validTimestamp, chainTimestamp, minRemainSec = OPENFOUR_CREATE_MIN_REMAIN_SEC) {
|
|
70
|
+
return validTimestamp < chainTimestamp + minRemainSec;
|
|
71
|
+
}
|
|
65
72
|
export function isOpenFourPresaleNative(params) {
|
|
66
73
|
if (params.presaleQuote <= 0n)
|
|
67
74
|
return false;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AbiCoder, ParamType, ZeroAddress, ZeroHash } from 'ethers';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { OPENFOUR_CREATE_MIN_REMAIN_SEC, decodeOpenFourCreateArg, shouldRefreshOpenFourCreateArg, } from './create-arg.js';
|
|
4
|
+
const INIT = [
|
|
5
|
+
{ name: 'name', type: 'string' },
|
|
6
|
+
{ name: 'symbol', type: 'string' },
|
|
7
|
+
{ name: 'tokenUri', type: 'string' },
|
|
8
|
+
{ name: 'maxSupply', type: 'uint256' },
|
|
9
|
+
{ name: 'saleAmount', type: 'uint256' },
|
|
10
|
+
{ name: 'raiseAmount', type: 'uint256' },
|
|
11
|
+
{ name: 'quoteAsset', type: 'address' },
|
|
12
|
+
{ name: 'tokenSalt', type: 'bytes32' },
|
|
13
|
+
{ name: 'tokenParams', type: 'bytes' },
|
|
14
|
+
{ name: 'vaultParams', type: 'bytes' },
|
|
15
|
+
{ name: 'curveParams', type: 'bytes' },
|
|
16
|
+
{ name: 'tradeParams', type: 'bytes' },
|
|
17
|
+
{ name: 'migrateParams', type: 'bytes' },
|
|
18
|
+
{ name: 'customDataParams', type: 'bytes' },
|
|
19
|
+
];
|
|
20
|
+
const ABI = [
|
|
21
|
+
ParamType.from({
|
|
22
|
+
type: 'tuple',
|
|
23
|
+
components: [
|
|
24
|
+
{ name: 'requestId', type: 'uint256' },
|
|
25
|
+
{ name: 'presetId', type: 'uint256' },
|
|
26
|
+
{ name: 'validTimestamp', type: 'uint256' },
|
|
27
|
+
{ name: 'createFee', type: 'uint256' },
|
|
28
|
+
{ name: 'presaleQuote', type: 'uint256' },
|
|
29
|
+
{ name: 'antiSniperEnabled', type: 'bool' },
|
|
30
|
+
{ name: 'initParams', type: 'tuple', components: INIT },
|
|
31
|
+
],
|
|
32
|
+
}),
|
|
33
|
+
];
|
|
34
|
+
function encodeArg(validTimestamp, quoteAsset = ZeroAddress) {
|
|
35
|
+
return AbiCoder.defaultAbiCoder().encode(ABI, [
|
|
36
|
+
[
|
|
37
|
+
1n,
|
|
38
|
+
2n,
|
|
39
|
+
validTimestamp,
|
|
40
|
+
0n,
|
|
41
|
+
10000000000000000n,
|
|
42
|
+
false,
|
|
43
|
+
[
|
|
44
|
+
'test',
|
|
45
|
+
'test',
|
|
46
|
+
'https://example.com/t.json',
|
|
47
|
+
1n,
|
|
48
|
+
1n,
|
|
49
|
+
1n,
|
|
50
|
+
quoteAsset,
|
|
51
|
+
ZeroHash,
|
|
52
|
+
'0x',
|
|
53
|
+
'0x',
|
|
54
|
+
'0x',
|
|
55
|
+
'0x',
|
|
56
|
+
'0x',
|
|
57
|
+
'0x',
|
|
58
|
+
],
|
|
59
|
+
],
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
describe('decodeOpenFourCreateArg', () => {
|
|
63
|
+
it('reads validTimestamp and quoteAsset', () => {
|
|
64
|
+
const quote = '0xbe9D156892E55e7154BcD3cB0FEA677F9D3103E1';
|
|
65
|
+
const decoded = decodeOpenFourCreateArg(encodeArg(1789979444n, quote));
|
|
66
|
+
expect(decoded.validTimestamp).toBe(1789979444n);
|
|
67
|
+
expect(decoded.presaleQuote).toBe(10000000000000000n);
|
|
68
|
+
expect(decoded.quoteAsset.toLowerCase()).toBe(quote.toLowerCase());
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('shouldRefreshOpenFourCreateArg', () => {
|
|
72
|
+
it('refreshes when the deadline is already past', () => {
|
|
73
|
+
expect(shouldRefreshOpenFourCreateArg(100n, 101n)).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
it('refreshes when remaining time is shorter than the submit buffer', () => {
|
|
76
|
+
expect(shouldRefreshOpenFourCreateArg(100n + OPENFOUR_CREATE_MIN_REMAIN_SEC - 1n, 100n)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
it('keeps a signature with enough remaining time', () => {
|
|
79
|
+
expect(shouldRefreshOpenFourCreateArg(100n + OPENFOUR_CREATE_MIN_REMAIN_SEC, 100n)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { OpenFourAddresses, OpenFourCreateReq, OpenFourInitParams, OpenFourModuleSchema, OpenFourParamDescriptor, OpenFourPresetSchemas, OpenFourTemplate, OpenFourTemplateConfig, } from './types.js';
|
|
2
2
|
export { collectOpenFourSchemaParams, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, } from './schema.js';
|
|
3
|
-
export { computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, type OpenFourDecodedCreateArg, } from './create-arg.js';
|
|
3
|
+
export { OPENFOUR_CREATE_EXPIRED_MESSAGE, OPENFOUR_CREATE_MIN_REMAIN_SEC, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, shouldRefreshOpenFourCreateArg, type OpenFourDecodedCreateArg, } from './create-arg.js';
|
|
4
4
|
export { loadOpenFourAddresses, loadOpenFourPresetSchemas } from './registry.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { collectOpenFourSchemaParams, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, } from './schema.js';
|
|
2
|
-
export { computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, } from './create-arg.js';
|
|
2
|
+
export { OPENFOUR_CREATE_EXPIRED_MESSAGE, OPENFOUR_CREATE_MIN_REMAIN_SEC, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, isOpenFourPresaleNative, shouldRefreshOpenFourCreateArg, } from './create-arg.js';
|
|
3
3
|
export { loadOpenFourAddresses, loadOpenFourPresetSchemas } from './registry.js';
|
|
@@ -15,7 +15,7 @@ export { ensureSellApprovalV1, checkSellApprovalV1, ensureSellApprovalV2, checkS
|
|
|
15
15
|
export { parseFourError, type FourErrorCode } from '../utils/errors.js';
|
|
16
16
|
export { getTokenManagerV1, getTokenManagerV2, getTokenManagerHelper3, getTokenManagerV1Writer, getTokenManagerV2Writer, getTokenManagerHelper3Writer, getTokenManagerAddress, type ChainName, } from '../utils/contract-factory.js';
|
|
17
17
|
export { FourClient, buildLoginMessage, buildCreateTokenReq, type FourConfig, type GenerateNonceReq, type LoginReq, type CreateTokenReq, type CreateTokenCustom, type CreateTokenResp, type FourTokenTaxInfo, type FourRaisedToken, type FourTokenLabel, } from '../core/clients/four.js';
|
|
18
|
-
export { collectOpenFourSchemaParams, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, isOpenFourPresaleNative, loadOpenFourAddresses, loadOpenFourPresetSchemas, type OpenFourAddresses, type OpenFourCreateReq, type OpenFourDecodedCreateArg, type OpenFourInitParams, type OpenFourModuleSchema, type OpenFourParamDescriptor, type OpenFourPresetSchemas, type OpenFourTemplate, type OpenFourTemplateConfig, } from '../domains/openfour/index.js';
|
|
18
|
+
export { OPENFOUR_CREATE_EXPIRED_MESSAGE, OPENFOUR_CREATE_MIN_REMAIN_SEC, collectOpenFourSchemaParams, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, isOpenFourPresaleNative, shouldRefreshOpenFourCreateArg, loadOpenFourAddresses, loadOpenFourPresetSchemas, type OpenFourAddresses, type OpenFourCreateReq, type OpenFourDecodedCreateArg, type OpenFourInitParams, type OpenFourModuleSchema, type OpenFourParamDescriptor, type OpenFourPresetSchemas, type OpenFourTemplate, type OpenFourTemplateConfig, } from '../domains/openfour/index.js';
|
|
19
19
|
export { FOUR_TOKEN_LABELS, FOUR_BNB_RAISED_TOKEN_FALLBACK, parseRaisedTokens, publishedRaisedTokens, pickRaisedToken, isNativeRaisedToken, computeFourCreateValueWei, raisedTokenTradingFeeBps, } from '../domains/four/raised-token.js';
|
|
20
20
|
export { createTokenOnChain, tryBuy, trySell, buyTokenWithFunds, sellToken, tradeBuy, tradeSell, type CreateOnChainParams, type TryBuyResult, type TrySellResult, } from '../domains/four/contracts/tm.js';
|
|
21
21
|
export { TM1, type FourChainV1 } from '../domains/four/contracts/tm1.js';
|
|
@@ -50,7 +50,7 @@ checkAllowanceRaw, approveTokenRaw, checkAllowanceBatchRaw, approveTokenBatchRaw
|
|
|
50
50
|
export { parseFourError } from '../utils/errors.js';
|
|
51
51
|
export { getTokenManagerV1, getTokenManagerV2, getTokenManagerHelper3, getTokenManagerV1Writer, getTokenManagerV2Writer, getTokenManagerHelper3Writer, getTokenManagerAddress, } from '../utils/contract-factory.js';
|
|
52
52
|
export { FourClient, buildLoginMessage, buildCreateTokenReq, } from '../core/clients/four.js';
|
|
53
|
-
export { collectOpenFourSchemaParams, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, isOpenFourPresaleNative, loadOpenFourAddresses, loadOpenFourPresetSchemas, } from '../domains/openfour/index.js';
|
|
53
|
+
export { OPENFOUR_CREATE_EXPIRED_MESSAGE, OPENFOUR_CREATE_MIN_REMAIN_SEC, collectOpenFourSchemaParams, computeOpenFourCreateValueWei, decodeOpenFourCreateArg, defaultOpenFourFormFromSchema, emptyOpenFourInitParams, encodeOpenFourInitParams, encodeOpenFourModuleParams, isOpenFourPresaleNative, shouldRefreshOpenFourCreateArg, loadOpenFourAddresses, loadOpenFourPresetSchemas, } from '../domains/openfour/index.js';
|
|
54
54
|
export { FOUR_TOKEN_LABELS, FOUR_BNB_RAISED_TOKEN_FALLBACK, parseRaisedTokens, publishedRaisedTokens, pickRaisedToken, isNativeRaisedToken, computeFourCreateValueWei, raisedTokenTradingFeeBps, } from '../domains/four/raised-token.js';
|
|
55
55
|
export { createTokenOnChain, tryBuy, trySell, buyTokenWithFunds, sellToken, tradeBuy, tradeSell, } from '../domains/four/contracts/tm.js';
|
|
56
56
|
export { TM1 } from '../domains/four/contracts/tm1.js';
|
|
@@ -10,6 +10,9 @@ export type { DisperseHopPath } from './types/hop-chains.js';
|
|
|
10
10
|
export declare const NATIVE_TRANSFER_GAS_LIMIT: 21055n;
|
|
11
11
|
export declare const ERC20_TRANSFER_GAS_LIMIT_HOP: 120000n;
|
|
12
12
|
export declare const BRIBE_GAS_LIMIT: 21000n;
|
|
13
|
+
/** 末跳且买地址自备 gas 时,hop 不再转发原生币,ERC20 必须从 nonce 0 起。 */
|
|
14
|
+
export declare function hopForwardsNativeGas(index: number, hopCount: number, finalGasAmount: bigint): boolean;
|
|
15
|
+
export declare function hopErc20Nonce(index: number, hopCount: number, finalGasAmount: bigint): number;
|
|
13
16
|
/**
|
|
14
17
|
* 给交易对象附加 gasPrice/maxFeePerGas(根据 txType)
|
|
15
18
|
*/
|
|
@@ -25,7 +28,7 @@ export declare function buildNativeHopChain(payer: Wallet, path: DisperseHopPath
|
|
|
25
28
|
/**
|
|
26
29
|
* 构建 ERC20 多跳转账链(需要配合 BNB 多跳使用)
|
|
27
30
|
*/
|
|
28
|
-
export declare function buildERC20HopChain(payer: Wallet, path: DisperseHopPath, erc20Address: string, erc20Amount: bigint, gasPrice: bigint, chainId: number, txType: 0 | 2, payerNonce: number): Promise<string[]>;
|
|
31
|
+
export declare function buildERC20HopChain(payer: Wallet, path: DisperseHopPath, erc20Address: string, erc20Amount: bigint, gasPrice: bigint, chainId: number, txType: 0 | 2, payerNonce: number, finalNativeForwardAmount?: bigint): Promise<string[]>;
|
|
29
32
|
/**
|
|
30
33
|
* 构建 BNB 多跳转账链(为 ERC20 中间钱包预留 gas)
|
|
31
34
|
*/
|
package/dist/utils/hop-chains.js
CHANGED
|
@@ -11,6 +11,15 @@ import { generateWallets } from './wallet.js';
|
|
|
11
11
|
export const NATIVE_TRANSFER_GAS_LIMIT = GAS_LIMITS.NATIVE_TRANSFER;
|
|
12
12
|
export const ERC20_TRANSFER_GAS_LIMIT_HOP = GAS_LIMITS.ERC20_TRANSFER;
|
|
13
13
|
export const BRIBE_GAS_LIMIT = GAS_LIMITS.BRIBE;
|
|
14
|
+
/** 末跳且买地址自备 gas 时,hop 不再转发原生币,ERC20 必须从 nonce 0 起。 */
|
|
15
|
+
export function hopForwardsNativeGas(index, hopCount, finalGasAmount) {
|
|
16
|
+
if (hopCount <= 0 || index < 0 || index >= hopCount)
|
|
17
|
+
return false;
|
|
18
|
+
return !(index === hopCount - 1 && finalGasAmount === 0n);
|
|
19
|
+
}
|
|
20
|
+
export function hopErc20Nonce(index, hopCount, finalGasAmount) {
|
|
21
|
+
return hopForwardsNativeGas(index, hopCount, finalGasAmount) ? 1 : 0;
|
|
22
|
+
}
|
|
14
23
|
/**
|
|
15
24
|
* 给交易对象附加 gasPrice/maxFeePerGas(根据 txType)
|
|
16
25
|
*/
|
|
@@ -107,7 +116,7 @@ export async function buildNativeHopChain(payer, path, finalAmount, gasPrice, ch
|
|
|
107
116
|
/**
|
|
108
117
|
* 构建 ERC20 多跳转账链(需要配合 BNB 多跳使用)
|
|
109
118
|
*/
|
|
110
|
-
export async function buildERC20HopChain(payer, path, erc20Address, erc20Amount, gasPrice, chainId, txType, payerNonce) {
|
|
119
|
+
export async function buildERC20HopChain(payer, path, erc20Address, erc20Amount, gasPrice, chainId, txType, payerNonce, finalNativeForwardAmount = 0n) {
|
|
111
120
|
const signedTxs = [];
|
|
112
121
|
const hopCount = path.hopWallets.length;
|
|
113
122
|
const erc20Interface = new ethers.Interface(ERC20_ABI);
|
|
@@ -139,7 +148,7 @@ export async function buildERC20HopChain(payer, path, erc20Address, erc20Amount,
|
|
|
139
148
|
};
|
|
140
149
|
applyTxTypeGas(firstTx, gasPrice, txType);
|
|
141
150
|
signedTxs.push(await payer.signTransaction(firstTx));
|
|
142
|
-
// hop1 → hop2 → ... → target
|
|
151
|
+
// hop1 → hop2 → ... → target。末跳若不再转发 BNB,ERC20 用 nonce=0。
|
|
143
152
|
for (let i = 0; i < hopCount; i++) {
|
|
144
153
|
const fromWallet = path.hopWallets[i];
|
|
145
154
|
const toAddress = i === hopCount - 1 ? path.targetAddress : path.hopWallets[i + 1].address;
|
|
@@ -148,7 +157,7 @@ export async function buildERC20HopChain(payer, path, erc20Address, erc20Amount,
|
|
|
148
157
|
to: erc20Address,
|
|
149
158
|
data,
|
|
150
159
|
value: 0n,
|
|
151
|
-
nonce:
|
|
160
|
+
nonce: hopErc20Nonce(i, hopCount, finalNativeForwardAmount),
|
|
152
161
|
gasLimit: ERC20_TRANSFER_GAS_LIMIT_HOP,
|
|
153
162
|
chainId,
|
|
154
163
|
type: txType,
|
|
@@ -197,7 +206,7 @@ export async function buildBNBHopChainForERC20(payer, path, finalGasAmount, gasP
|
|
|
197
206
|
const fromWallet = path.hopWallets[i];
|
|
198
207
|
const toAddress = i === hopCount - 1 ? path.targetAddress : path.hopWallets[i + 1].address;
|
|
199
208
|
const amount = i === hopCount - 1 ? finalGasAmount : amountsPerHop[i + 1];
|
|
200
|
-
if (i
|
|
209
|
+
if (!hopForwardsNativeGas(i, hopCount, finalGasAmount))
|
|
201
210
|
continue;
|
|
202
211
|
const tx = {
|
|
203
212
|
to: toAddress,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { hopErc20Nonce, hopForwardsNativeGas } from './hop-chains.js';
|
|
3
|
+
describe('hopErc20Nonce', () => {
|
|
4
|
+
it('uses nonce 0 on a single hop when the buyer keeps its own gas', () => {
|
|
5
|
+
expect(hopForwardsNativeGas(0, 1, 0n)).toBe(false);
|
|
6
|
+
expect(hopErc20Nonce(0, 1, 0n)).toBe(0);
|
|
7
|
+
});
|
|
8
|
+
it('keeps nonce 1 on intermediate hops and 0 on the last hop', () => {
|
|
9
|
+
expect(hopErc20Nonce(0, 2, 0n)).toBe(1);
|
|
10
|
+
expect(hopErc20Nonce(1, 2, 0n)).toBe(0);
|
|
11
|
+
});
|
|
12
|
+
it('uses nonce 1 on the last hop when it also forwards native gas', () => {
|
|
13
|
+
expect(hopForwardsNativeGas(0, 1, 1n)).toBe(true);
|
|
14
|
+
expect(hopErc20Nonce(0, 1, 1n)).toBe(1);
|
|
15
|
+
});
|
|
16
|
+
});
|