four-flap-meme-sdk 4.4.2 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -22
- package/dist/bundle/four-meme/approve-tokenmanager.js +2 -2
- package/dist/bundle/four-meme/core/create-token-quote.js +10 -1
- package/dist/bundle/four-meme/private/buy-quote.js +10 -1
- package/dist/bundle/four-meme/swap-sign-helpers.js +6 -1
- package/dist/bundle/four-meme/types/buy-first.d.ts +2 -1
- package/dist/bundle/four-meme/types/config.d.ts +2 -1
- package/dist/bundle/four-meme/types/swap-types.d.ts +2 -1
- package/dist/bundle/four-meme/utils-sweep/erc20-no-hop.js +16 -8
- package/dist/bundle/four-meme/utils-sweep/main.js +15 -1
- package/dist/bundle/four-meme/utils-sweep/native-no-hop.js +17 -9
- package/dist/bundle/index.d.ts +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/internal.d.ts +2 -0
- package/dist/bundle/internal.js +7 -3
- package/dist/bundle/submit/index.d.ts +1 -3
- package/dist/bundle/submit/index.js +1 -3
- package/dist/chains/bsc/index.d.ts +1 -1
- package/dist/chains/bsc/index.js +1 -1
- package/dist/chains/bsc/pancake/bundle-buy-first-types.d.ts +2 -1
- package/dist/chains/bsc/pancake/bundle-swap-types.d.ts +2 -1
- package/dist/chains/bsc/platforms/four/index.d.ts +1 -1
- package/dist/chains/bsc/platforms/four/index.js +1 -1
- package/dist/chains/eni/batch-router/bundle-approve.js +2 -1
- package/dist/chains/eni/batch-router/transfer-disperse.js +2 -1
- package/dist/chains/eni/platforms/dswap/liquidity.js +4 -4
- package/dist/chains/robinhood/sign.js +15 -1
- package/dist/core/clients/blockrazor.js +8 -6
- package/dist/core/clients/blockrazor.test.d.ts +1 -0
- package/dist/core/clients/blockrazor.test.js +23 -0
- package/dist/core/clients/index.d.ts +0 -1
- package/dist/core/clients/index.js +0 -1
- package/dist/core/clients/merkle.js +12 -7
- package/dist/core/clients/merkle.test.d.ts +1 -0
- package/dist/core/clients/merkle.test.js +8 -0
- package/dist/core/gas/bundle-gas.d.ts +8 -0
- package/dist/core/gas/bundle-gas.js +13 -1
- package/dist/core/nonce/nonce-manager.d.ts +2 -0
- package/dist/core/nonce/nonce-manager.js +21 -1
- package/dist/core/tx/wallet-sign-patch.js +61 -4
- package/dist/core/tx/wallet-sign-patch.test.d.ts +1 -0
- package/dist/core/tx/wallet-sign-patch.test.js +21 -0
- package/dist/domains/dex/direct-router-v2-buy.js +16 -2
- package/dist/domains/dex/direct-router-v2-sell.js +3 -2
- package/dist/domains/flap/portal-bundle/helpers.js +1 -1
- package/dist/domains/flap/portal-bundle-merkle/private/buy.js +3 -3
- package/dist/domains/flap/portal-bundle-merkle/private/helpers.d.ts +1 -1
- package/dist/domains/flap/portal-bundle-merkle/private/helpers.js +4 -8
- package/dist/domains/flap/portal-bundle-merkle/private/sell.js +3 -3
- package/dist/domains/flap/portal-bundle-merkle/swap-buy-first-types.d.ts +2 -1
- package/dist/domains/flap/portal-bundle-merkle/swap-helpers.d.ts +2 -1
- package/dist/domains/flap/portal-bundle-merkle/swap-planning.js +10 -4
- package/dist/domains/flap/portal-bundle-merkle/swap-planning.test.d.ts +1 -0
- package/dist/domains/flap/portal-bundle-merkle/swap-planning.test.js +16 -0
- package/dist/domains/flap/portal-bundle-merkle/types-config.d.ts +2 -1
- package/dist/domains/four/contracts/tm-bundle-create.js +13 -1
- package/dist/domains/four/contracts/tm-bundle-helpers.js +1 -1
- package/dist/domains/holders/internal/constants.d.ts +1 -1
- package/dist/domains/holders/internal/constants.js +1 -1
- package/dist/domains/merkle/index.d.ts +1 -1
- package/dist/domains/merkle/index.js +1 -1
- package/dist/exports/root-bundle-and-tooling.d.ts +1 -1
- package/dist/exports/root-bundle-and-tooling.js +1 -1
- package/dist/exports/root-foundations.d.ts +0 -1
- package/dist/exports/root-foundations.js +0 -1
- package/dist/utils/swap-helpers.d.ts +1 -15
- package/dist/utils/swap-helpers.js +0 -80
- package/package.json +1 -1
|
@@ -17,6 +17,20 @@ export async function signRobinhoodTx(params) {
|
|
|
17
17
|
if (maxFeePerGas < floor)
|
|
18
18
|
maxFeePerGas = floor;
|
|
19
19
|
const maxPriorityFeePerGas = priority > maxFeePerGas ? maxFeePerGas : priority;
|
|
20
|
+
let gasLimit = params.gasLimit;
|
|
21
|
+
try {
|
|
22
|
+
const estimated = await provider.estimateGas({
|
|
23
|
+
from: wallet.address,
|
|
24
|
+
to: params.to,
|
|
25
|
+
data: params.data,
|
|
26
|
+
value: params.value ?? 0n,
|
|
27
|
+
});
|
|
28
|
+
if (estimated > 0n)
|
|
29
|
+
gasLimit = estimated;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* 节点没给出上限时用服务端配置 */
|
|
33
|
+
}
|
|
20
34
|
const signedTx = await wallet.signTransaction({
|
|
21
35
|
to: params.to,
|
|
22
36
|
value: params.value ?? 0n,
|
|
@@ -26,7 +40,7 @@ export async function signRobinhoodTx(params) {
|
|
|
26
40
|
type: 2,
|
|
27
41
|
maxFeePerGas,
|
|
28
42
|
maxPriorityFeePerGas,
|
|
29
|
-
gasLimit
|
|
43
|
+
gasLimit,
|
|
30
44
|
});
|
|
31
45
|
const tx = Transaction.from(signedTx);
|
|
32
46
|
return {
|
|
@@ -146,16 +146,18 @@ export class BlockRazorClient {
|
|
|
146
146
|
}
|
|
147
147
|
// 验证 Gas Price(最低 0.05 Gwei)
|
|
148
148
|
for (const rawTx of params.txs) {
|
|
149
|
+
let tx;
|
|
149
150
|
try {
|
|
150
|
-
|
|
151
|
-
const gasPrice = tx.gasPrice || 0n;
|
|
152
|
-
const minGasPrice = ethers.parseUnits(String(MIN_GAS_PRICE_GWEI), 'gwei');
|
|
153
|
-
if (gasPrice < minGasPrice) {
|
|
154
|
-
throw new Error(`Transaction gas price (${ethers.formatUnits(gasPrice, 'gwei')} gwei) is below minimum (${MIN_GAS_PRICE_GWEI} gwei)`);
|
|
155
|
-
}
|
|
151
|
+
tx = Transaction.from(rawTx);
|
|
156
152
|
}
|
|
157
153
|
catch {
|
|
158
154
|
// 解析失败,跳过检查
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const gasPrice = tx.gasPrice && tx.gasPrice > 0n ? tx.gasPrice : tx.maxFeePerGas ?? 0n;
|
|
158
|
+
const minGasPrice = ethers.parseUnits(String(MIN_GAS_PRICE_GWEI), 'gwei');
|
|
159
|
+
if (gasPrice < minGasPrice) {
|
|
160
|
+
throw new Error(`Transaction gas price (${ethers.formatUnits(gasPrice, 'gwei')} gwei) is below minimum (${MIN_GAS_PRICE_GWEI} gwei)`);
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
// 构建请求参数
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Wallet } from 'ethers';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { BlockRazorClient } from './blockrazor.js';
|
|
4
|
+
describe('BlockRazor gas price validation', () => {
|
|
5
|
+
it('低于 0.05 Gwei 的签名交易被拒绝', async () => {
|
|
6
|
+
const client = new BlockRazorClient({
|
|
7
|
+
apiKey: 'test',
|
|
8
|
+
chainId: 56,
|
|
9
|
+
customRpcUrl: 'http://127.0.0.1:1',
|
|
10
|
+
});
|
|
11
|
+
const wallet = Wallet.createRandom();
|
|
12
|
+
const raw = await wallet.signTransaction({
|
|
13
|
+
to: Wallet.createRandom().address,
|
|
14
|
+
value: 0n,
|
|
15
|
+
nonce: 0,
|
|
16
|
+
gasLimit: 21000n,
|
|
17
|
+
gasPrice: 1n,
|
|
18
|
+
chainId: 56,
|
|
19
|
+
type: 0,
|
|
20
|
+
});
|
|
21
|
+
await expect(client.sendBundleRaw({ txs: [raw] })).rejects.toThrow(/below minimum/);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -3,6 +3,5 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { FourClient, buildLoginMessage, buildCreateTokenReq, type FourConfig, type GenerateNonceReq, type LoginReq, type CreateTokenReq, type CreateTokenResp, type FourTokenTaxInfo, type FourRaisedToken, type FourTokenLabel, } from './four.js';
|
|
5
5
|
export { Club48Client, sendBatchPrivateTransactions, sendBackrunBundle, type BundleParams, type BundleStatus, type Club48Config, } from './club48.js';
|
|
6
|
-
export { MerkleClient, createMerkleClient, type MerkleConfig, type BundleParams as MerkleBundleParams, type SendBundleOptions, type BundleResult, type TransactionResult, } from './merkle.js';
|
|
7
6
|
export { BlockRazorClient, createBlockRazorClient, BLOCKRAZOR_BUILDER_EOA, type BlockRazorConfig, type BlockRazorBundleParams, type SendBundleOptions as BlockRazorSendBundleOptions, type BundleResult as BlockRazorBundleResult, type TransactionResult as BlockRazorTransactionResult, type IncentiveTransactionParams, } from './blockrazor.js';
|
|
8
7
|
export { EmitServiceClient, submitToEmitServiceBlockRazor, type EmitServiceClientConfig, type EmitServiceSubmitRequest, type EmitServiceSubmitResponse, type EmitServiceSubmitResponseData, type EmitServiceRouteConfigResponse, } from './emitservice.js';
|
|
@@ -3,6 +3,5 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { FourClient, buildLoginMessage, buildCreateTokenReq, } from './four.js';
|
|
5
5
|
export { Club48Client, sendBatchPrivateTransactions, sendBackrunBundle, } from './club48.js';
|
|
6
|
-
export { MerkleClient, createMerkleClient, } from './merkle.js';
|
|
7
6
|
export { BlockRazorClient, createBlockRazorClient, BLOCKRAZOR_BUILDER_EOA, } from './blockrazor.js';
|
|
8
7
|
export { EmitServiceClient, submitToEmitServiceBlockRazor, } from './emitservice.js';
|
|
@@ -38,12 +38,15 @@ export class MerkleClient {
|
|
|
38
38
|
constructor(config) {
|
|
39
39
|
this.chainId = config.chainId;
|
|
40
40
|
this.chainName = config.chainId === CHAINS.BSC.chainId ? 'bsc' : 'ethereum';
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
? CHAINS.BSC.
|
|
45
|
-
: CHAINS.ETHEREUM.
|
|
46
|
-
const rpcUrl = config.customRpcUrl ||
|
|
41
|
+
// Blink/Merkle 的 bundle RPC 是带 API key 的节点 endpoint;公共 BSC 节点
|
|
42
|
+
// 不支持 eth_sendBundle。customRpcUrl 可显式覆盖(需自带 key)。
|
|
43
|
+
const bundleBase = config.chainId === CHAINS.BSC.chainId
|
|
44
|
+
? CHAINS.BSC.bundleRpcUrl
|
|
45
|
+
: CHAINS.ETHEREUM.bundleRpcUrl;
|
|
46
|
+
const rpcUrl = config.customRpcUrl || (bundleBase ? `${bundleBase}/v1/${config.apiKey}` : '');
|
|
47
|
+
if (!rpcUrl) {
|
|
48
|
+
throw new Error('Merkle bundle RPC endpoint is not configured');
|
|
49
|
+
}
|
|
47
50
|
this.provider = createProvider(rpcUrl, this.chainId, this.chainName);
|
|
48
51
|
}
|
|
49
52
|
/**
|
|
@@ -366,10 +369,12 @@ export class MerkleClient {
|
|
|
366
369
|
*/
|
|
367
370
|
getProviderInfo() {
|
|
368
371
|
const rpcConnection = this.provider._getConnection?.();
|
|
372
|
+
const rawUrl = rpcConnection?.url ?? 'unknown';
|
|
369
373
|
return {
|
|
370
374
|
chainId: this.chainId,
|
|
371
375
|
chainName: this.chainName,
|
|
372
|
-
|
|
376
|
+
// API key 在路径中,调试输出只保留脱敏后的 endpoint。
|
|
377
|
+
rpcUrl: rawUrl.replace(/\/v1\/[^/?#]+/, '/v1/***'),
|
|
373
378
|
};
|
|
374
379
|
}
|
|
375
380
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { MerkleClient } from './merkle.js';
|
|
3
|
+
describe('MerkleClient endpoint', () => {
|
|
4
|
+
it('默认使用带 apiKey 的 Blink BSC bundle RPC', () => {
|
|
5
|
+
const client = new MerkleClient({ apiKey: 'test-key', chainId: 56 });
|
|
6
|
+
expect(client.getProviderInfo().rpcUrl).toContain('https://bsc.blinklabs.xyz/v1/***');
|
|
7
|
+
});
|
|
8
|
+
});
|
|
@@ -52,3 +52,11 @@ export type Eip1559GasFields = {
|
|
|
52
52
|
};
|
|
53
53
|
export type TxGasFields = LegacyGasFields | Eip1559GasFields;
|
|
54
54
|
export declare function buildGasFields(txType: number, gasPrice: bigint): TxGasFields;
|
|
55
|
+
/** 向节点 eth_estimateGas 要这笔 approve 的 gas 上限。节点没返回时才用 fallback。 */
|
|
56
|
+
export declare function estimateApproveGasLimit(provider: {
|
|
57
|
+
estimateGas: (tx: {
|
|
58
|
+
from?: string;
|
|
59
|
+
to?: string;
|
|
60
|
+
data?: string;
|
|
61
|
+
}) => Promise<bigint>;
|
|
62
|
+
}, from: string, to: string, data: string, fallback?: bigint): Promise<bigint>;
|
|
@@ -23,7 +23,7 @@ export async function getOptimizedGasPrice(provider, config) {
|
|
|
23
23
|
return gasPrice;
|
|
24
24
|
}
|
|
25
25
|
export async function estimateGasWithSafety(provider, txRequest, config) {
|
|
26
|
-
const multiplier = config?.multiplier ?? 1
|
|
26
|
+
const multiplier = config?.multiplier ?? 1;
|
|
27
27
|
const fallback = config?.fallbackGasLimit ?? 600000n;
|
|
28
28
|
const allowFailure = config?.allowFailure ?? true;
|
|
29
29
|
try {
|
|
@@ -91,3 +91,15 @@ export function buildGasFields(txType, gasPrice) {
|
|
|
91
91
|
}
|
|
92
92
|
return { type: 0, gasPrice };
|
|
93
93
|
}
|
|
94
|
+
/** 向节点 eth_estimateGas 要这笔 approve 的 gas 上限。节点没返回时才用 fallback。 */
|
|
95
|
+
export async function estimateApproveGasLimit(provider, from, to, data, fallback = 150000n) {
|
|
96
|
+
try {
|
|
97
|
+
const estimated = await provider.estimateGas({ from, to, data });
|
|
98
|
+
if (estimated > 0n)
|
|
99
|
+
return estimated;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* 节点没给出上限 */
|
|
103
|
+
}
|
|
104
|
+
return fallback;
|
|
105
|
+
}
|
|
@@ -14,6 +14,8 @@ export declare class NonceManager {
|
|
|
14
14
|
private readOnchainNonce;
|
|
15
15
|
getNextNonce(wallet: Wallet | string): Promise<number>;
|
|
16
16
|
getNextNonceBatch(wallet: Wallet | string, count: number): Promise<number[]>;
|
|
17
|
+
/** 预填某个地址的下一个 nonce(用于调用方显式 startNonce 的并行场景)。 */
|
|
18
|
+
seedNextNonce(wallet: Wallet | string, nextNonce: number): void;
|
|
17
19
|
clearTemp(): void;
|
|
18
20
|
getTempCachedNonce(address: string): number | undefined;
|
|
19
21
|
getNextNoncesForWallets(wallets: (Wallet | string)[]): Promise<number[]>;
|
|
@@ -157,6 +157,14 @@ export class NonceManager {
|
|
|
157
157
|
return Array.from({ length: count }, (_, i) => startNonce + i);
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
|
+
/** 预填某个地址的下一个 nonce(用于调用方显式 startNonce 的并行场景)。 */
|
|
161
|
+
seedNextNonce(wallet, nextNonce) {
|
|
162
|
+
if (!Number.isInteger(nextNonce) || nextNonce < 0)
|
|
163
|
+
return;
|
|
164
|
+
const address = typeof wallet === 'string' ? wallet : wallet.address;
|
|
165
|
+
const key = address.toLowerCase();
|
|
166
|
+
this.tempNonceCache.set(key, nextNonce);
|
|
167
|
+
}
|
|
160
168
|
clearTemp() {
|
|
161
169
|
this.tempNonceCache.clear();
|
|
162
170
|
}
|
|
@@ -210,10 +218,22 @@ export class NonceManager {
|
|
|
210
218
|
const rawResults = await asBatchSendProvider(this.provider)._send(batchRequests);
|
|
211
219
|
const byId = new Map();
|
|
212
220
|
for (const row of rawResults) {
|
|
221
|
+
if (row.error || row.result == null) {
|
|
222
|
+
throw new Error(`batch eth_getTransactionCount failed: ${row.error?.message || 'missing result'}`);
|
|
223
|
+
}
|
|
213
224
|
const id = Number(row.id);
|
|
225
|
+
if (!Number.isInteger(id))
|
|
226
|
+
throw new Error('batch eth_getTransactionCount returned invalid id');
|
|
214
227
|
byId.set(id, parseRpcQuantity(row.result));
|
|
215
228
|
}
|
|
216
|
-
queryResults = uniqueQueries.map((_, idx) =>
|
|
229
|
+
queryResults = uniqueQueries.map((_, idx) => {
|
|
230
|
+
const latest = byId.get(idx * 2 + 1);
|
|
231
|
+
const pending = byId.get(idx * 2 + 2);
|
|
232
|
+
if (latest === undefined || pending === undefined) {
|
|
233
|
+
throw new Error('batch eth_getTransactionCount returned incomplete result');
|
|
234
|
+
}
|
|
235
|
+
return Math.max(latest, pending);
|
|
236
|
+
});
|
|
217
237
|
}
|
|
218
238
|
catch {
|
|
219
239
|
queryResults = await Promise.all(uniqueQueries.map(({ address }) => this.readOnchainNonce(address)));
|
|
@@ -3,16 +3,73 @@
|
|
|
3
3
|
* 模块加载时 patch Wallet.prototype.signTransaction(全局一次)
|
|
4
4
|
*/
|
|
5
5
|
import { Wallet } from 'ethers';
|
|
6
|
+
function chainGasPrice(fee) {
|
|
7
|
+
const candidates = [fee.maxFeePerGas, fee.gasPrice].filter((v) => v != null && v > 0n);
|
|
8
|
+
if (candidates.length === 0)
|
|
9
|
+
return null;
|
|
10
|
+
return candidates.reduce((a, b) => (a > b ? a : b));
|
|
11
|
+
}
|
|
6
12
|
const _origWalletSign = Wallet.prototype.signTransaction;
|
|
7
13
|
Wallet.prototype.signTransaction = async function (tx) {
|
|
8
|
-
|
|
14
|
+
let signable = tx;
|
|
9
15
|
if (signable.type === 2 && signable.gasPrice != null && signable.maxFeePerGas == null) {
|
|
10
16
|
const { gasPrice: gp, ...rest } = signable;
|
|
11
|
-
|
|
17
|
+
signable = {
|
|
12
18
|
...rest,
|
|
13
19
|
maxFeePerGas: gp,
|
|
14
20
|
maxPriorityFeePerGas: rest.maxPriorityFeePerGas ?? 0n,
|
|
15
|
-
}
|
|
21
|
+
};
|
|
16
22
|
}
|
|
17
|
-
|
|
23
|
+
const provider = this.provider;
|
|
24
|
+
if (provider) {
|
|
25
|
+
const hasPrice = (signable.gasPrice != null && signable.gasPrice > 0n)
|
|
26
|
+
|| (signable.maxFeePerGas != null && signable.maxFeePerGas > 0n);
|
|
27
|
+
if (!hasPrice) {
|
|
28
|
+
try {
|
|
29
|
+
const fee = await provider.getFeeData();
|
|
30
|
+
const chainPrice = chainGasPrice(fee);
|
|
31
|
+
if (chainPrice) {
|
|
32
|
+
if (signable.type === 2) {
|
|
33
|
+
let priority = fee.maxPriorityFeePerGas && fee.maxPriorityFeePerGas > 0n
|
|
34
|
+
? fee.maxPriorityFeePerGas
|
|
35
|
+
: chainPrice / 10n || 1n;
|
|
36
|
+
if (priority > chainPrice)
|
|
37
|
+
priority = chainPrice;
|
|
38
|
+
signable = { ...signable, maxFeePerGas: chainPrice, maxPriorityFeePerGas: priority };
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
signable = { ...signable, gasPrice: chainPrice };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* 节点没给出 gas 价格时保留交易上已有的出价 */
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
let hasGasLimit = false;
|
|
50
|
+
if (signable.gasLimit != null) {
|
|
51
|
+
try {
|
|
52
|
+
hasGasLimit = BigInt(signable.gasLimit) > 0n;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
hasGasLimit = false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!hasGasLimit) {
|
|
59
|
+
try {
|
|
60
|
+
const estimated = await provider.estimateGas({
|
|
61
|
+
from: signable.from ?? this.address,
|
|
62
|
+
to: signable.to,
|
|
63
|
+
data: signable.data,
|
|
64
|
+
value: signable.value,
|
|
65
|
+
});
|
|
66
|
+
if (estimated > 0n)
|
|
67
|
+
signable = { ...signable, gasLimit: estimated };
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
/* 节点没给出 gas 上限时保留原来的上限,避免发不出去 */
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return _origWalletSign.call(this, signable);
|
|
18
75
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './wallet-sign-patch.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Transaction, Wallet } from 'ethers';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import './wallet-sign-patch.js';
|
|
4
|
+
describe('wallet sign patch gasLimit', () => {
|
|
5
|
+
it('保留调用方显式 gasLimit,不被 estimateGas 覆盖', async () => {
|
|
6
|
+
const estimateGas = vi.fn(async () => 21000n);
|
|
7
|
+
const provider = { estimateGas };
|
|
8
|
+
const wallet = new Wallet(Wallet.createRandom().privateKey, provider);
|
|
9
|
+
const raw = await wallet.signTransaction({
|
|
10
|
+
to: Wallet.createRandom().address,
|
|
11
|
+
value: 0n,
|
|
12
|
+
nonce: 0,
|
|
13
|
+
gasLimit: 500000n,
|
|
14
|
+
gasPrice: 1000000000n,
|
|
15
|
+
chainId: 56,
|
|
16
|
+
type: 0,
|
|
17
|
+
});
|
|
18
|
+
expect(Transaction.from(raw).gasLimit).toBe(500000n);
|
|
19
|
+
expect(estimateGas).not.toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 从 direct-router-v2.ts 抽取,direct-router-v2.ts 通过 re-export 保持对外不变。
|
|
4
4
|
*/
|
|
5
5
|
import { ethers, Wallet, Contract } from 'ethers';
|
|
6
|
-
import { getGasLimit } from '../../core/gas/bundle-gas.js';
|
|
6
|
+
import { estimateApproveGasLimit, getGasLimit } from '../../core/gas/bundle-gas.js';
|
|
7
7
|
import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, buildGasFields, } from '../../utils/bundle-helpers.js';
|
|
8
8
|
import { getProfitRecipient, CHAINS, GAS_LIMITS } from '../../core/constants/index.js';
|
|
9
9
|
import { ERC20_ABI } from '../../abis/common.js';
|
|
@@ -71,6 +71,19 @@ export async function directV2BatchBuy(params) {
|
|
|
71
71
|
activeNonces = activeIndices.map((i) => nonces[i]);
|
|
72
72
|
activeBuyAllowances = activeIndices.map((i) => buyAllowances[i]);
|
|
73
73
|
}
|
|
74
|
+
// 0 金额钱包不参与 swap;否则 amountIn=0 会让整包 revert,拖死正常钱包。
|
|
75
|
+
const positiveIndices = activeFlowAmounts
|
|
76
|
+
.map((amount, index) => (amount > 0n ? index : -1))
|
|
77
|
+
.filter((index) => index >= 0);
|
|
78
|
+
if (positiveIndices.length === 0) {
|
|
79
|
+
throw new Error('所有买方金额为 0,无法执行');
|
|
80
|
+
}
|
|
81
|
+
if (positiveIndices.length !== activeWallets.length) {
|
|
82
|
+
activeWallets = positiveIndices.map((i) => activeWallets[i]);
|
|
83
|
+
activeFlowAmounts = positiveIndices.map((i) => activeFlowAmounts[i]);
|
|
84
|
+
activeNonces = positiveIndices.map((i) => activeNonces[i]);
|
|
85
|
+
activeBuyAllowances = positiveIndices.map((i) => activeBuyAllowances[i]);
|
|
86
|
+
}
|
|
74
87
|
const activeTotalFlowWei = activeFlowAmounts.reduce((sum, amt) => sum + amt, 0n);
|
|
75
88
|
const activeBaseProfitWei = calculateProfitAmount(activeTotalFlowWei);
|
|
76
89
|
let activeProfitWei = activeBaseProfitWei > 0n ? activeBaseProfitWei : 0n;
|
|
@@ -88,12 +101,13 @@ export async function directV2BatchBuy(params) {
|
|
|
88
101
|
continue;
|
|
89
102
|
if (activeBuyAllowances[i] < activeFlowAmounts[i]) {
|
|
90
103
|
const approveData = quoteTokenContract.interface.encodeFunctionData('approve', [router, ethers.MaxUint256]);
|
|
104
|
+
const approveGasLimit = await estimateApproveGasLimit(provider, activeWallets[i].address, inputToken, approveData);
|
|
91
105
|
const approveTx = await activeWallets[i].signTransaction({
|
|
92
106
|
to: inputToken,
|
|
93
107
|
data: approveData,
|
|
94
108
|
value: 0n,
|
|
95
109
|
nonce: activeNonces[i],
|
|
96
|
-
gasLimit:
|
|
110
|
+
gasLimit: approveGasLimit,
|
|
97
111
|
...buildGasFields(txType, gasPrice),
|
|
98
112
|
chainId,
|
|
99
113
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 从 direct-router-v2.ts 抽取,direct-router-v2.ts 通过 re-export 保持对外不变。
|
|
4
4
|
*/
|
|
5
5
|
import { ethers, Interface, Wallet, Contract } from 'ethers';
|
|
6
|
-
import { getGasLimit } from '../../core/gas/bundle-gas.js';
|
|
6
|
+
import { estimateApproveGasLimit, getGasLimit } from '../../core/gas/bundle-gas.js';
|
|
7
7
|
import { NonceManager, buildProfitHopTransactions, PROFIT_HOP_COUNT, buildGasFields, } from '../../utils/bundle-helpers.js';
|
|
8
8
|
import { getProfitRecipient, CHAINS, GAS_LIMITS, getMulticall3Address } from '../../core/constants/index.js';
|
|
9
9
|
import { MULTICALL3_ABI } from '../../abis/common.js';
|
|
@@ -124,12 +124,13 @@ export async function directV2BatchSell(params) {
|
|
|
124
124
|
continue;
|
|
125
125
|
if (allowances[i] < sellAmountsWei[i]) {
|
|
126
126
|
const approveData = tokenContract.interface.encodeFunctionData('approve', [router, ethers.MaxUint256]);
|
|
127
|
+
const approveGasLimit = await estimateApproveGasLimit(provider, wallets[i].address, tokenIn, approveData);
|
|
127
128
|
const approveTx = await wallets[i].signTransaction({
|
|
128
129
|
to: tokenIn,
|
|
129
130
|
data: approveData,
|
|
130
131
|
value: 0n,
|
|
131
132
|
nonce: nonces[i],
|
|
132
|
-
gasLimit:
|
|
133
|
+
gasLimit: approveGasLimit,
|
|
133
134
|
...buildGasFields(txType, gasPrice),
|
|
134
135
|
chainId,
|
|
135
136
|
});
|
|
@@ -38,7 +38,7 @@ export function getErrorMessage(key, ...args) {
|
|
|
38
38
|
*/
|
|
39
39
|
export async function estimateGasWithMultiplier(provider, txRequest, multiplier) {
|
|
40
40
|
const estimated = await provider.estimateGas(txRequest);
|
|
41
|
-
const mult = multiplier ?? 1
|
|
41
|
+
const mult = multiplier ?? 1;
|
|
42
42
|
return BigInt(Math.ceil(Number(estimated) * mult));
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
@@ -4,13 +4,13 @@ import { FLAP_PORTAL_ADDRESSES } from '../../constants.js';
|
|
|
4
4
|
import { ZERO_ADDRESS } from '../../../../core/constants/index.js';
|
|
5
5
|
import { GAS_LIMITS } from '../../../../core/constants/index.js';
|
|
6
6
|
import { PORTAL_ABI, getTxType, getGasPriceConfig, shouldExtractProfit, calculateBatchProfit, getProfitRecipient, getBribeAmount, BLOCKRAZOR_BUILDER_EOA, } from '../config.js';
|
|
7
|
-
import {
|
|
7
|
+
import { createPrivateContext, splitBuyAmount, parseMinOutput, resolveGasLimit, resolveBatchMinOutputs, findMaxIndex, signBatchTransactions, } from './helpers.js';
|
|
8
8
|
/**
|
|
9
9
|
* 私有购买(单笔)(Merkle 版本)
|
|
10
10
|
*/
|
|
11
11
|
export async function flapPrivateBuyMerkle(params) {
|
|
12
12
|
const { chain, privateKey, tokenAddress, amountIn, minOutputAmount, config } = params;
|
|
13
|
-
const { chainId, provider } =
|
|
13
|
+
const { chainId, provider } = createPrivateContext(chain, config);
|
|
14
14
|
const wallet = new Wallet(privateKey, provider);
|
|
15
15
|
const portalAddr = FLAP_PORTAL_ADDRESSES[chain];
|
|
16
16
|
const originalAmountWei = ethers.parseEther(amountIn);
|
|
@@ -97,7 +97,7 @@ export async function flapBatchPrivateBuyMerkle(params) {
|
|
|
97
97
|
if (privateKeys.length !== amountsIn.length) {
|
|
98
98
|
throw new Error('privateKeys and amountsIn length mismatch');
|
|
99
99
|
}
|
|
100
|
-
const { chainId, provider } =
|
|
100
|
+
const { chainId, provider } = createPrivateContext(chain, config);
|
|
101
101
|
const portalAddr = FLAP_PORTAL_ADDRESSES[chain];
|
|
102
102
|
const wallets = privateKeys.map((k) => new Wallet(k, provider));
|
|
103
103
|
const originalAmountsWei = amountsIn.map((a) => ethers.parseEther(a));
|
|
@@ -2,7 +2,7 @@ import { ethers, Wallet } from 'ethers';
|
|
|
2
2
|
import type { FlapPrivateBuyMerkleParams } from '../types.js';
|
|
3
3
|
type FlapChain = FlapPrivateBuyMerkleParams['chain'];
|
|
4
4
|
type FlapConfig = FlapPrivateBuyMerkleParams['config'];
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function createPrivateContext(chain: FlapChain, config: FlapConfig): {
|
|
6
6
|
chainId: number;
|
|
7
7
|
provider: ethers.JsonRpcProvider;
|
|
8
8
|
};
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { ethers } from 'ethers';
|
|
2
|
-
import { MerkleClient } from '../../../../core/clients/merkle.js';
|
|
3
2
|
import { createProvider } from '../../../../utils/provider-factory.js';
|
|
4
3
|
import { ZERO_ADDRESS, CHAINS, applySlippageBps } from '../../../../core/constants/index.js';
|
|
5
4
|
import { CHAIN_ID_MAP, getTxType, calculateProfit, } from '../config.js';
|
|
6
5
|
const DEFAULT_GAS_LIMIT = 800000;
|
|
7
|
-
export function
|
|
6
|
+
export function createPrivateContext(chain, config) {
|
|
8
7
|
const chainId = CHAIN_ID_MAP[chain];
|
|
9
8
|
// ✅ 检查是否为 Merkle 支持的链(BSC, ETH)
|
|
10
9
|
// Monad 不支持 Merkle 捆绑服务,使用普通 RPC
|
|
@@ -19,14 +18,11 @@ export function createMerkleContext(chain, config) {
|
|
|
19
18
|
provider: createProvider(config.customRpcUrl, chainId, chain.toLowerCase()),
|
|
20
19
|
};
|
|
21
20
|
}
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
chainId: chainId, // 类型断言为 Merkle 支持的 chainId
|
|
25
|
-
customRpcUrl: config.customRpcUrl,
|
|
26
|
-
});
|
|
21
|
+
const rpcUrl = config.customRpcUrl
|
|
22
|
+
|| (chainId === CHAINS.BSC.chainId ? CHAINS.BSC.rpcUrls.default : CHAINS.ETHEREUM.rpcUrls.default);
|
|
27
23
|
return {
|
|
28
24
|
chainId,
|
|
29
|
-
provider:
|
|
25
|
+
provider: createProvider(rpcUrl, chainId, chain.toLowerCase()),
|
|
30
26
|
};
|
|
31
27
|
}
|
|
32
28
|
export function splitBuyAmount(amount, extractProfit) {
|
|
@@ -4,13 +4,13 @@ import { FLAP_PORTAL_ADDRESSES } from '../../constants.js';
|
|
|
4
4
|
import { ZERO_ADDRESS, applySlippageBps } from '../../../../core/constants/index.js';
|
|
5
5
|
import { GAS_LIMITS } from '../../../../core/constants/index.js';
|
|
6
6
|
import { PORTAL_ABI, getTxType, getGasPriceConfig, shouldExtractProfit, calculateProfit, getProfitRecipient, getBribeAmount, BLOCKRAZOR_BUILDER_EOA, } from '../config.js';
|
|
7
|
-
import {
|
|
7
|
+
import { createPrivateContext, resolveGasLimit, resolveSingleSellOutputs, signBatchTransactions, summarizeSellProfits, } from './helpers.js';
|
|
8
8
|
/**
|
|
9
9
|
* 私有卖出(单笔)(Merkle 版本)
|
|
10
10
|
*/
|
|
11
11
|
export async function flapPrivateSellMerkle(params) {
|
|
12
12
|
const { chain, privateKey, tokenAddress, amount, minOutputAmount, config } = params;
|
|
13
|
-
const { chainId, provider } =
|
|
13
|
+
const { chainId, provider } = createPrivateContext(chain, config);
|
|
14
14
|
const wallet = new Wallet(privateKey, provider);
|
|
15
15
|
const portalAddr = FLAP_PORTAL_ADDRESSES[chain];
|
|
16
16
|
const amountWei = ethers.parseUnits(amount, 18);
|
|
@@ -95,7 +95,7 @@ export async function flapBatchPrivateSellMerkle(params) {
|
|
|
95
95
|
if (privateKeys.length !== amounts.length) {
|
|
96
96
|
throw new Error('privateKeys and amounts length mismatch');
|
|
97
97
|
}
|
|
98
|
-
const { chainId, provider } =
|
|
98
|
+
const { chainId, provider } = createPrivateContext(chain, config);
|
|
99
99
|
const portalAddr = FLAP_PORTAL_ADDRESSES[chain];
|
|
100
100
|
const wallets = privateKeys.map((k) => new Wallet(k, provider));
|
|
101
101
|
const amountsWei = amounts.map((a) => ethers.parseUnits(a, 18));
|
|
@@ -13,7 +13,8 @@ export interface FlapBuyFirstSignConfig extends FlapSignConfig {
|
|
|
13
13
|
userType?: UserType;
|
|
14
14
|
}
|
|
15
15
|
export interface FlapBuyFirstConfig extends CommonBundleConfig {
|
|
16
|
-
|
|
16
|
+
/** @deprecated Merkle 提交已移除;字段仅为旧调用兼容 */
|
|
17
|
+
apiKey?: string;
|
|
17
18
|
customRpcUrl?: string;
|
|
18
19
|
bundleBlockOffset?: number;
|
|
19
20
|
reserveGasETH?: number;
|
|
@@ -49,7 +49,8 @@ export declare const MONAD_WMON: "0x3bd359c1119da7da1d913d1c4d2b7c461115433a";
|
|
|
49
49
|
export declare function getTokenToNativeQuote(provider: ethers.JsonRpcProvider, tokenAddress: string, tokenAmount: bigint, chainId: number): Promise<bigint>;
|
|
50
50
|
export type FlapChain = 'bsc' | 'xlayer' | 'base';
|
|
51
51
|
export interface FlapSwapConfig extends CommonBundleConfig {
|
|
52
|
-
|
|
52
|
+
/** @deprecated Merkle 提交已移除;字段仅为旧调用兼容 */
|
|
53
|
+
apiKey?: string;
|
|
53
54
|
customRpcUrl?: string;
|
|
54
55
|
bundleBlockOffset?: number;
|
|
55
56
|
reserveGasETH?: number;
|
|
@@ -5,6 +5,7 @@ import { ZERO_ADDRESS, calculateBundleSwapRoundTripProfit } from '../../../core/
|
|
|
5
5
|
import { ERC20_ALLOWANCE_ABI } from '../../../core/shared-abis/common.js';
|
|
6
6
|
import { getGasPriceConfig, getTxType, getProfitRecipient, PORTAL_ABI } from './config.js';
|
|
7
7
|
import { getErrorMessageFromUnknown } from '../../../core/index.js';
|
|
8
|
+
import { estimateApproveGasLimit } from '../../../core/gas/bundle-gas.js';
|
|
8
9
|
import { createSignProvider } from '../../../bundle/sign-context-helpers.js';
|
|
9
10
|
import { batchGetBalances } from '../../../bundle/internal.js';
|
|
10
11
|
import { APPROVE_INTERFACE, ERC20_BALANCE_OF_ABI, getChainId, getNativeTokenName, } from './swap-helpers.js';
|
|
@@ -38,12 +39,14 @@ export async function buildApprovalTransaction({ tokenAddress, seller, provider,
|
|
|
38
39
|
const approvalNonce = await nonceManager.getNextNonce(seller);
|
|
39
40
|
const gasPrice = await getOptimizedGasPrice(provider, getGasPriceConfig(config));
|
|
40
41
|
const txType = getTxType(config);
|
|
42
|
+
const approveData = APPROVE_INTERFACE.encodeFunctionData('approve', [portalAddress, ethers.MaxUint256]);
|
|
43
|
+
const gasLimit = await estimateApproveGasLimit(provider, seller.address, tokenAddress, approveData);
|
|
41
44
|
return await seller.signTransaction({
|
|
42
45
|
to: tokenAddress,
|
|
43
|
-
data:
|
|
46
|
+
data: approveData,
|
|
44
47
|
value: 0n,
|
|
45
48
|
nonce: approvalNonce,
|
|
46
|
-
gasLimit
|
|
49
|
+
gasLimit,
|
|
47
50
|
gasPrice,
|
|
48
51
|
chainId,
|
|
49
52
|
type: txType,
|
|
@@ -97,6 +100,9 @@ export async function calculateBuyerNeed({ buyer, quotedNative, reserveGasEth, n
|
|
|
97
100
|
buyerBalance = await erc20.balanceOf(buyer.address);
|
|
98
101
|
}
|
|
99
102
|
const estimatedBuyerNeed = quotedNative;
|
|
103
|
+
if (estimatedBuyerNeed <= 0n) {
|
|
104
|
+
throw new Error('卖出报价为 0,拒绝用买方全部余额买入;请稍后重试或检查池子流动性');
|
|
105
|
+
}
|
|
100
106
|
const buyerNeedTotal = useNativeToken ? estimatedBuyerNeed + reserveGas : estimatedBuyerNeed;
|
|
101
107
|
if (buyerBalance < buyerNeedTotal) {
|
|
102
108
|
if (useNativeToken) {
|
|
@@ -110,7 +116,7 @@ export async function calculateBuyerNeed({ buyer, quotedNative, reserveGasEth, n
|
|
|
110
116
|
` - 实际: ${ethers.formatUnits(buyerBalance, quoteTokenDecimals)}`);
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
|
-
const maxBuyerValue = estimatedBuyerNeed
|
|
119
|
+
const maxBuyerValue = estimatedBuyerNeed;
|
|
114
120
|
return { reserveGas, buyerBalance, buyerNeedTotal, maxBuyerValue };
|
|
115
121
|
}
|
|
116
122
|
export async function validateBalances({ buyerAddress, portalGasCost, provider, chainContext, seller, useNativeToken, }) {
|
|
@@ -165,7 +171,7 @@ export async function planNonces({ seller, buyer, approvalExists, extractProfit,
|
|
|
165
171
|
return { sellerNonce: nonces[0], buyerNonce: nonces[1] };
|
|
166
172
|
}
|
|
167
173
|
export async function buildProfitTransaction({ provider, seller, profitAmount, profitNonce, gasPrice, chainId, txType, }) {
|
|
168
|
-
if (
|
|
174
|
+
if (profitNonce === undefined || profitAmount === 0n) {
|
|
169
175
|
return null;
|
|
170
176
|
}
|
|
171
177
|
const profitHopResult = await buildProfitHopTransactions({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ethers, Wallet } from 'ethers';
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { calculateBuyerNeed } from './swap-planning.js';
|
|
4
|
+
describe('calculateBuyerNeed quote guard', () => {
|
|
5
|
+
it('报价为 0 时拒绝用买方全部余额买入', async () => {
|
|
6
|
+
const provider = {
|
|
7
|
+
getBalance: vi.fn(async () => ethers.parseEther('1')),
|
|
8
|
+
};
|
|
9
|
+
const buyer = new Wallet(Wallet.createRandom().privateKey, provider);
|
|
10
|
+
await expect(calculateBuyerNeed({
|
|
11
|
+
buyer,
|
|
12
|
+
quotedNative: 0n,
|
|
13
|
+
nativeToken: 'BNB',
|
|
14
|
+
})).rejects.toThrow(/卖出报价为 0/);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -62,7 +62,8 @@ export type FlapSignConfig = {
|
|
|
62
62
|
extractProfitOnLaunch?: boolean;
|
|
63
63
|
};
|
|
64
64
|
export type FlapBundleMerkleConfig = {
|
|
65
|
-
|
|
65
|
+
/** @deprecated Merkle 提交已移除;字段仅为旧调用兼容,不再读取 */
|
|
66
|
+
apiKey?: string;
|
|
66
67
|
customRpcUrl?: string;
|
|
67
68
|
bundleBlockOffset?: number;
|
|
68
69
|
minBlockOffset?: number;
|