four-flap-meme-sdk 4.3.14 → 4.3.16
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/bundle/four-meme/utils-disperse.js +2 -2
- package/dist/bundle/four-meme/utils-pairwise.js +2 -2
- package/dist/bundle/four-meme/utils-sweep/erc20-no-hop.js +2 -2
- package/dist/bundle/four-meme/utils-sweep/with-hops.js +3 -3
- package/dist/bundle/internal.d.ts +1 -1
- package/dist/bundle/internal.js +8 -11
- 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/core/constants/gas-erc20-floor.test.d.ts +1 -0
- package/dist/core/constants/gas-erc20-floor.test.js +24 -0
- package/dist/core/constants/gas.d.ts +2 -0
- package/dist/core/constants/gas.js +5 -0
- package/dist/core/constants/index.d.ts +1 -1
- package/dist/core/constants/index.js +1 -1
- package/dist/domains/four/tax-token.test.js +2 -0
- package/dist/domains/openfour/create-arg.d.ts +7 -0
- package/dist/domains/openfour/create-arg.js +11 -0
- package/dist/domains/openfour/create-arg.test.d.ts +1 -0
- package/dist/domains/openfour/create-arg.test.js +91 -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/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, isOpenFourCreateArgExpired, 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 (isOpenFourCreateArgExpired(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));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ethers, Wallet } from 'ethers';
|
|
2
2
|
import { getOptimizedGasPrice, NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../utils/bundle-helpers.js';
|
|
3
|
-
import { calculateTransferFee, GAS_LIMITS, isBscChain } from '../../core/constants/index.js';
|
|
3
|
+
import { calculateTransferFee, floorErc20TransferGasLimit, GAS_LIMITS, isBscChain } from '../../core/constants/index.js';
|
|
4
4
|
import { getGasPriceConfig, shouldExtractProfit, getProfitRecipient, getBribeAmount } from '../config-helpers.js';
|
|
5
5
|
import { getErc20Decimals as _getErc20Decimals, generateHopWallets as _generateHopWallets, normalizeAmounts as _normalizeAmounts, calculateGasLimit as _calculateGasLimit, isNativeTokenAddress as _isNativeTokenAddress, } from '../internal.js';
|
|
6
6
|
import { normalizeItemsOrRecipientsAmounts, resolveHopCountInput, resolveProvidedHops, resolveSignContext, signBribeTransaction, } from '../../core/sign-context-helpers.js';
|
|
@@ -138,7 +138,7 @@ export async function disperseWithBundleMerkle(params) {
|
|
|
138
138
|
// - 原生代币转账:贿赂档
|
|
139
139
|
// - ERC20 转账:走 GAS_LIMITS(含股票币代理)
|
|
140
140
|
const nativeTransferGasLimit = GAS_LIMITS.BRIBE;
|
|
141
|
-
const erc20TransferGasLimit =
|
|
141
|
+
const erc20TransferGasLimit = floorErc20TransferGasLimit(finalGasLimit);
|
|
142
142
|
// ✅ 原生代币多跳的 gas 费(每跳只需要 21000)
|
|
143
143
|
const nativeHopGasFee = nativeTransferGasLimit * gasPrice;
|
|
144
144
|
// ✅ ERC20 多跳时,中转钱包需要执行 2 笔交易(转 gas + 转 ERC20)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ethers, Wallet } from 'ethers';
|
|
2
2
|
import { getOptimizedGasPrice, NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, buildGasFields, } from '../../utils/bundle-helpers.js';
|
|
3
|
-
import { BLOCKRAZOR_BUILDER_EOA, calculateTransferFee, CHAINS,
|
|
3
|
+
import { BLOCKRAZOR_BUILDER_EOA, calculateTransferFee, CHAINS, floorErc20TransferGasLimit, isBscChain, } from '../../core/constants/index.js';
|
|
4
4
|
import { getTxType, getGasPriceConfig, shouldExtractProfit, getProfitRecipient, getBribeAmount, } from '../config-helpers.js';
|
|
5
5
|
import { getErc20Decimals as _getErc20Decimals, generateHopWallets as _generateHopWallets, calculateGasLimit as _calculateGasLimit, isNativeTokenAddress as _isNativeTokenAddress, } from '../internal.js';
|
|
6
6
|
import { createSignProvider, resolveProvidedHops } from '../../core/sign-context-helpers.js';
|
|
@@ -48,7 +48,7 @@ export async function pairwiseTransferWithBundleMerkle(params) {
|
|
|
48
48
|
const finalGasLimit = _calculateGasLimit(config, isNative, hasHops, maxHopCount);
|
|
49
49
|
const nativeGasLimit = (config.prefer21000ForNative ?? false) ? 21000n : finalGasLimit;
|
|
50
50
|
const nativeTransferGasLimit = 21000n;
|
|
51
|
-
const erc20TransferGasLimit =
|
|
51
|
+
const erc20TransferGasLimit = floorErc20TransferGasLimit(config.gasLimit);
|
|
52
52
|
const [gasPrice, decimals] = await Promise.all([
|
|
53
53
|
getOptimizedGasPrice(provider, getGasPriceConfig(config)),
|
|
54
54
|
isNative
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { ethers, Wallet } from 'ethers';
|
|
5
5
|
import { NonceManager, getOptimizedGasPrice, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
|
|
6
6
|
import { getGasPriceConfig } from '../../config-helpers.js';
|
|
7
|
-
import { calculateTransferFee } from '../../../core/constants/index.js';
|
|
7
|
+
import { calculateTransferFee, floorErc20TransferGasLimit } from '../../../core/constants/index.js';
|
|
8
8
|
import { batchGetBalances as _batchGetBalances, getErc20Decimals as _getErc20Decimals } from '../../internal.js';
|
|
9
9
|
import { clampRatio } from './context.js';
|
|
10
10
|
export async function sweepErc20NoHop(ctx) {
|
|
@@ -134,7 +134,7 @@ export async function sweepErc20NoHop(ctx) {
|
|
|
134
134
|
value: 0n,
|
|
135
135
|
nonce,
|
|
136
136
|
gasPrice,
|
|
137
|
-
gasLimit: finalGasLimit,
|
|
137
|
+
gasLimit: floorErc20TransferGasLimit(finalGasLimit),
|
|
138
138
|
chainId: BigInt(chainIdNum),
|
|
139
139
|
type: txType,
|
|
140
140
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { ethers, Wallet } from 'ethers';
|
|
5
5
|
import { NonceManager, getOptimizedGasPrice, buildProfitHopTransactions, PROFIT_HOP_COUNT, } from '../../../utils/bundle-helpers.js';
|
|
6
6
|
import { getGasPriceConfig } from '../../config-helpers.js';
|
|
7
|
-
import { calculateTransferFee, GAS_LIMITS } from '../../../core/constants/index.js';
|
|
7
|
+
import { calculateTransferFee, floorErc20TransferGasLimit, GAS_LIMITS } from '../../../core/constants/index.js';
|
|
8
8
|
import { batchGetBalances as _batchGetBalances, getErc20Decimals as _getErc20Decimals } from '../../internal.js';
|
|
9
9
|
import { clampRatio } from './context.js';
|
|
10
10
|
export async function sweepWithHops(ctx, preparedHops) {
|
|
@@ -38,7 +38,7 @@ export async function sweepWithHops(ctx, preparedHops) {
|
|
|
38
38
|
const iface = isNative ? null : new ethers.Interface(['function transfer(address,uint256) returns (bool)']);
|
|
39
39
|
// ✅ Gas limit 设置(与分散函数保持一致)
|
|
40
40
|
const nativeTransferGasLimit = GAS_LIMITS.BRIBE;
|
|
41
|
-
const erc20TransferGasLimit =
|
|
41
|
+
const erc20TransferGasLimit = floorErc20TransferGasLimit(finalGasLimit);
|
|
42
42
|
// ✅ 原生代币多跳的 gas 费(每跳只需要 21000)
|
|
43
43
|
const nativeHopGasFee = nativeTransferGasLimit * gasPrice;
|
|
44
44
|
// ✅ ERC20 多跳时,中转钱包需要执行 2 笔交易(转 gas + 转 ERC20)
|
|
@@ -124,7 +124,7 @@ export async function sweepWithHops(ctx, preparedHops) {
|
|
|
124
124
|
value: 0n,
|
|
125
125
|
nonce,
|
|
126
126
|
gasPrice,
|
|
127
|
-
gasLimit:
|
|
127
|
+
gasLimit: erc20TransferGasLimit,
|
|
128
128
|
chainId: BigInt(chainIdNum),
|
|
129
129
|
type: txType,
|
|
130
130
|
});
|
|
@@ -11,6 +11,6 @@ export declare function normalizeAmounts(recipients: string[], amount?: AmountLi
|
|
|
11
11
|
export declare function batchGetBalances(provider: JsonRpcProvider, addresses: string[], tokenAddress?: string, chainId?: number): Promise<bigint[]>;
|
|
12
12
|
export declare function estimateErc20TransferGas(provider: JsonRpcProvider, tokenAddress: string, from: string, to: string, amount: bigint, bufferPercent?: number): Promise<bigint>;
|
|
13
13
|
export declare function estimateMaxErc20TransferGas(provider: JsonRpcProvider, tokenAddress: string, from: string, recipients: string[], amounts: bigint[], bufferPercent?: number): Promise<bigint>;
|
|
14
|
-
export declare function calculateGasLimit(config: BundleGasConfig, isNative: boolean,
|
|
14
|
+
export declare function calculateGasLimit(config: BundleGasConfig, isNative: boolean, _hasHops: boolean, _hopCount?: number): bigint;
|
|
15
15
|
export declare function isNativeTokenAddress(tokenAddress?: string): boolean;
|
|
16
16
|
export type { AmountLike, BundleGasConfig } from './types/index.js';
|
package/dist/bundle/internal.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ethers, Wallet } from 'ethers';
|
|
2
2
|
import { MULTICALL3_ABI, ERC20_BALANCE_ABI } from '../abis/common.js';
|
|
3
|
-
import { CHAINS, GAS_LIMITS, getMulticall3Address } from '../core/constants/index.js';
|
|
3
|
+
import { CHAINS, GAS_LIMITS, floorErc20TransferGasLimit, getMulticall3Address } from '../core/constants/index.js';
|
|
4
4
|
export { getErc20Decimals, getErc20DecimalsMerkle } from '../core/erc20/decimals.js';
|
|
5
5
|
function mapAggregate3Results(results, decode, fallback) {
|
|
6
6
|
return results.map((result) => {
|
|
@@ -129,20 +129,17 @@ export async function estimateMaxErc20TransferGas(provider, tokenAddress, from,
|
|
|
129
129
|
return 52000n;
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
-
export function calculateGasLimit(config, isNative,
|
|
132
|
+
export function calculateGasLimit(config, isNative, _hasHops, _hopCount = 0) {
|
|
133
|
+
if (!isNative) {
|
|
134
|
+
return floorErc20TransferGasLimit(config.gasLimit);
|
|
135
|
+
}
|
|
133
136
|
if (config.gasLimit !== undefined) {
|
|
134
137
|
return BigInt(config.gasLimit);
|
|
135
138
|
}
|
|
136
139
|
const isXLayer = config.chainId === CHAINS.XLAYER.chainId;
|
|
137
|
-
const baseGas =
|
|
138
|
-
?
|
|
139
|
-
|
|
140
|
-
: Number(GAS_LIMITS.NATIVE_TRANSFER)
|
|
141
|
-
: 46815;
|
|
142
|
-
if (hasHops && hopCount > 0) {
|
|
143
|
-
const multiplier = config.gasLimitMultiplier ?? 1.1;
|
|
144
|
-
return BigInt(Math.ceil(baseGas * multiplier));
|
|
145
|
-
}
|
|
140
|
+
const baseGas = isXLayer
|
|
141
|
+
? Number(GAS_LIMITS.XLAYER_NATIVE_TRANSFER)
|
|
142
|
+
: Number(GAS_LIMITS.NATIVE_TRANSFER);
|
|
146
143
|
const multiplier = config.gasLimitMultiplier ?? 1.1;
|
|
147
144
|
return BigInt(Math.ceil(baseGas * multiplier));
|
|
148
145
|
}
|
|
@@ -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
|
// ✅ 获取贿赂金额
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { calculateGasLimit } from '../../bundle/internal.js';
|
|
3
|
+
import { GAS_LIMITS, floorErc20TransferGasLimit } from './gas.js';
|
|
4
|
+
describe('floorErc20TransferGasLimit', () => {
|
|
5
|
+
it('raises USDT-era 65000 to SPCXB-safe 120000', () => {
|
|
6
|
+
expect(floorErc20TransferGasLimit(65000)).toBe(GAS_LIMITS.ERC20_TRANSFER);
|
|
7
|
+
expect(floorErc20TransferGasLimit(65000n)).toBe(120000n);
|
|
8
|
+
});
|
|
9
|
+
it('keeps higher user limits for tax tokens', () => {
|
|
10
|
+
expect(floorErc20TransferGasLimit(150000)).toBe(150000n);
|
|
11
|
+
expect(floorErc20TransferGasLimit(300000n)).toBe(300000n);
|
|
12
|
+
});
|
|
13
|
+
it('defaults to ERC20_TRANSFER', () => {
|
|
14
|
+
expect(floorErc20TransferGasLimit()).toBe(120000n);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('bundle calculateGasLimit ERC20 floor', () => {
|
|
18
|
+
it('does not honor 65000 for ERC20 collect', () => {
|
|
19
|
+
expect(calculateGasLimit({ gasLimit: 65000 }, false, false)).toBe(120000n);
|
|
20
|
+
});
|
|
21
|
+
it('keeps native collect at 21055', () => {
|
|
22
|
+
expect(calculateGasLimit({ gasLimit: 21055 }, true, false)).toBe(21055n);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -82,3 +82,5 @@ export declare function calculateGasLimit(chainId: number, isNative: boolean, ha
|
|
|
82
82
|
* @param isNative 是否原生代币
|
|
83
83
|
*/
|
|
84
84
|
export declare function getTransferGasLimit(chainId: number, isNative: boolean): bigint;
|
|
85
|
+
/** 归集/分发 ERC20:UI 常传 65000(USDT),SPCXB/税币 transfer 会 OOG。 */
|
|
86
|
+
export declare function floorErc20TransferGasLimit(requested?: number | bigint): bigint;
|
|
@@ -119,3 +119,8 @@ export function getTransferGasLimit(chainId, isNative) {
|
|
|
119
119
|
}
|
|
120
120
|
return GAS_LIMITS.ERC20_TRANSFER;
|
|
121
121
|
}
|
|
122
|
+
/** 归集/分发 ERC20:UI 常传 65000(USDT),SPCXB/税币 transfer 会 OOG。 */
|
|
123
|
+
export function floorErc20TransferGasLimit(requested) {
|
|
124
|
+
const n = requested === undefined ? GAS_LIMITS.ERC20_TRANSFER : BigInt(requested);
|
|
125
|
+
return n < GAS_LIMITS.ERC20_TRANSFER ? GAS_LIMITS.ERC20_TRANSFER : n;
|
|
126
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* const wbnb = ADDRESSES.BSC.WBNB;
|
|
10
10
|
*/
|
|
11
11
|
export { CHAINS, CHAIN_ID_MAP, getChainById, getChainName, getDefaultRpcUrl, isKnownChain, isBscChain, isXLayerChain, isMonadChain, isEniChain, isRobinhoodChain, getConfirmationTimeout, type ChainKey, type ChainConfig, } from './chains.js';
|
|
12
|
-
export { GAS_LIMITS, GAS_CONFIG, V3_FEE_TIERS, calculateGasLimit, getTransferGasLimit, type GasLimitKey, type V3FeeTier, } from './gas.js';
|
|
12
|
+
export { GAS_LIMITS, GAS_CONFIG, V3_FEE_TIERS, calculateGasLimit, getTransferGasLimit, floorErc20TransferGasLimit, type GasLimitKey, type V3FeeTier, } from './gas.js';
|
|
13
13
|
export { QUOTE_CONFIG, QUOTE_V3_FEE_TIERS, getQuoteChainKey, type QuoteConfigChain } from './quote.js';
|
|
14
14
|
export { applySlippageBps, reverseSlippageBps } from './slippage.js';
|
|
15
15
|
/** Flap 链标识(与 `FlapPortal` 一致,供子路径 `./constants` 使用) */
|
|
@@ -15,7 +15,7 @@ export { CHAINS, CHAIN_ID_MAP, getChainById, getChainName, getDefaultRpcUrl, isK
|
|
|
15
15
|
// ============================================================================
|
|
16
16
|
// Gas 常量
|
|
17
17
|
// ============================================================================
|
|
18
|
-
export { GAS_LIMITS, GAS_CONFIG, V3_FEE_TIERS, calculateGasLimit, getTransferGasLimit, } from './gas.js';
|
|
18
|
+
export { GAS_LIMITS, GAS_CONFIG, V3_FEE_TIERS, calculateGasLimit, getTransferGasLimit, floorErc20TransferGasLimit, } from './gas.js';
|
|
19
19
|
export { QUOTE_CONFIG, QUOTE_V3_FEE_TIERS, getQuoteChainKey } from './quote.js';
|
|
20
20
|
export { applySlippageBps, reverseSlippageBps } from './slippage.js';
|
|
21
21
|
// ============================================================================
|
|
@@ -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,15 @@ 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 = 90n;
|
|
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;
|
|
12
|
+
/** 合约是 block.timestamp > validTimestamp 才回滚;相等这一秒仍有效。 */
|
|
13
|
+
export declare function isOpenFourCreateArgExpired(validTimestamp: bigint, chainTimestamp: bigint): boolean;
|
|
7
14
|
export declare function isOpenFourPresaleNative(params: {
|
|
8
15
|
presaleQuote: bigint;
|
|
9
16
|
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 = 90n;
|
|
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,16 @@ 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
|
+
}
|
|
72
|
+
/** 合约是 block.timestamp > validTimestamp 才回滚;相等这一秒仍有效。 */
|
|
73
|
+
export function isOpenFourCreateArgExpired(validTimestamp, chainTimestamp) {
|
|
74
|
+
return validTimestamp < chainTimestamp;
|
|
75
|
+
}
|
|
65
76
|
export function isOpenFourPresaleNative(params) {
|
|
66
77
|
if (params.presaleQuote <= 0n)
|
|
67
78
|
return false;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AbiCoder, ParamType, ZeroAddress, ZeroHash } from 'ethers';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { OPENFOUR_CREATE_MIN_REMAIN_SEC, decodeOpenFourCreateArg, isOpenFourCreateArgExpired, 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
|
+
});
|
|
82
|
+
describe('isOpenFourCreateArgExpired', () => {
|
|
83
|
+
it('treats equal timestamps as still valid', () => {
|
|
84
|
+
expect(isOpenFourCreateArgExpired(100n, 100n)).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
it('treats the dumped Royalty+SPCXB deadline as expired at 17:30 CST', () => {
|
|
87
|
+
const validTimestamp = 1789982212n;
|
|
88
|
+
const reportedAt = BigInt(Math.floor(Date.parse('2026-09-21T09:30:00.000Z') / 1000));
|
|
89
|
+
expect(isOpenFourCreateArgExpired(validTimestamp, reportedAt)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -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, isOpenFourCreateArgExpired, 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, isOpenFourCreateArgExpired, 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';
|