four-flap-meme-sdk 1.5.52 → 1.5.53
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/xlayer/account-fetcher.d.ts +27 -0
- package/dist/xlayer/account-fetcher.js +27 -0
- package/dist/xlayer/constants.d.ts +4 -4
- package/dist/xlayer/constants.js +4 -10
- package/dist/xlayer/dex-buy-first.d.ts +2 -6
- package/dist/xlayer/dex-buy-first.js +127 -178
- package/dist/xlayer/dex-helpers.d.ts +20 -0
- package/dist/xlayer/dex-helpers.js +67 -0
- package/dist/xlayer/dex.d.ts +4 -0
- package/dist/xlayer/dex.js +8 -0
- package/dist/xlayer/router-manager.d.ts +38 -0
- package/dist/xlayer/router-manager.js +69 -0
- package/dist/xlayer/types.d.ts +11 -1
- package/dist/xlayer/userOp-builder.d.ts +51 -0
- package/dist/xlayer/userOp-builder.js +97 -0
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer AA 账户信息批量获取器
|
|
3
|
+
*/
|
|
4
|
+
import type { Wallet } from 'ethers';
|
|
5
|
+
import type { AAAccountManager } from './aa-account.js';
|
|
6
|
+
import type { AAAccount } from './types.js';
|
|
7
|
+
export interface AccountInfoBatch {
|
|
8
|
+
buyerAis: AAAccount[];
|
|
9
|
+
sellerAis: AAAccount[];
|
|
10
|
+
sellerSenders: string[];
|
|
11
|
+
sellerTokenBalances: Map<string, bigint>;
|
|
12
|
+
sellerAllowances: Map<string, bigint>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* ✅ 优化 1: 并行获取所有账户信息
|
|
16
|
+
*/
|
|
17
|
+
export declare function fetchAccountInfoBatch(params: {
|
|
18
|
+
buyers: Wallet[];
|
|
19
|
+
sellers: Wallet[];
|
|
20
|
+
tokenAddress: string;
|
|
21
|
+
effectiveRouter: string;
|
|
22
|
+
aaManager: AAAccountManager;
|
|
23
|
+
portalQuery: {
|
|
24
|
+
getMultipleTokenBalances: (token: string, addresses: string[]) => Promise<Map<string, bigint>>;
|
|
25
|
+
getMultipleAllowances: (token: string, owners: string[], spender: string) => Promise<Map<string, bigint>>;
|
|
26
|
+
};
|
|
27
|
+
}): Promise<AccountInfoBatch>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer AA 账户信息批量获取器
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* ✅ 优化 1: 并行获取所有账户信息
|
|
6
|
+
*/
|
|
7
|
+
export async function fetchAccountInfoBatch(params) {
|
|
8
|
+
const { buyers, sellers, tokenAddress, effectiveRouter, aaManager, portalQuery } = params;
|
|
9
|
+
// ✅ 并行获取买方和卖方的账户信息
|
|
10
|
+
const [buyerAis, sellerAis] = await Promise.all([
|
|
11
|
+
aaManager.getMultipleAccountInfo(buyers.map((w) => w.address)),
|
|
12
|
+
aaManager.getMultipleAccountInfo(sellers.map((w) => w.address))
|
|
13
|
+
]);
|
|
14
|
+
const sellerSenders = sellerAis.map((ai) => ai.sender);
|
|
15
|
+
// ✅ 并行获取卖方的代币余额和授权额度
|
|
16
|
+
const [sellerTokenBalances, sellerAllowances] = await Promise.all([
|
|
17
|
+
portalQuery.getMultipleTokenBalances(tokenAddress, sellerSenders),
|
|
18
|
+
portalQuery.getMultipleAllowances(tokenAddress, sellerSenders, effectiveRouter)
|
|
19
|
+
]);
|
|
20
|
+
return {
|
|
21
|
+
buyerAis,
|
|
22
|
+
sellerAis,
|
|
23
|
+
sellerSenders,
|
|
24
|
+
sellerTokenBalances,
|
|
25
|
+
sellerAllowances
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -24,6 +24,10 @@ export declare const FLAP_PORTAL = "0xb30D8c4216E1f21F27444D2FfAee3ad577808678";
|
|
|
24
24
|
export declare const FLAP_TOKEN_IMPL = "0x12Dc83157Bf1cfCB8Db5952b3ba5bb56Cc38f8C9";
|
|
25
25
|
/** WOKB 原生包装代币 */
|
|
26
26
|
export declare const WOKB = "0xe538905cf8410324e03a5a23c1c177a474d59b2b";
|
|
27
|
+
/** ✅ USDT 代币地址(XLayer,6 位精度) */
|
|
28
|
+
export declare const USDT = "0x1e4a5963abfd975d8c9021ce480b42188849d41d";
|
|
29
|
+
/** ✅ 零地址(表示原生代币) */
|
|
30
|
+
export declare const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
27
31
|
/** Multicall3 合约 */
|
|
28
32
|
export declare const MULTICALL3 = "0xca11bde05977b3631167028862be2a173976ca11";
|
|
29
33
|
/** PotatoSwap V2 Router */
|
|
@@ -34,14 +38,10 @@ export declare const POTATOSWAP_SWAP_ROUTER02 = "0xB45D0149249488333E3F3f9F35980
|
|
|
34
38
|
export declare const POTATOSWAP_V3_ROUTER = "0xBB069e9465BcabC4F488d21e793BDEf0F2d41D41";
|
|
35
39
|
/** PotatoSwap V3 Factory */
|
|
36
40
|
export declare const POTATOSWAP_V3_FACTORY = "0xa1415fAe79c4B196d087F02b8aD5a622B8A827E5";
|
|
37
|
-
/** USDT (6位精度) */
|
|
38
|
-
export declare const USDT = "0x1e4a5963abfd975d8c9021ce480b42188849d41d";
|
|
39
41
|
/** USDC (6位精度) */
|
|
40
42
|
export declare const USDC = "0x74b7f16337b8972027f6196a17a631ac6de26d22";
|
|
41
43
|
/** USD₮0 / USDT0 (6位精度) - Flap xLayer 支持的稳定币计价 */
|
|
42
44
|
export declare const USDT0 = "0x779ded0c9e1022225f8e0630b35a9b54be713736";
|
|
43
|
-
/** 零地址(表示原生代币 OKB) */
|
|
44
|
-
export declare const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
45
45
|
/** 默认 Gas Price 兜底值(0.1 gwei) */
|
|
46
46
|
export declare const DEFAULT_GAS_PRICE = 100000000n;
|
|
47
47
|
/** 默认 Salt(用于 AA 账户派生) */
|
package/dist/xlayer/constants.js
CHANGED
|
@@ -36,6 +36,10 @@ export const FLAP_TOKEN_IMPL = '0x12Dc83157Bf1cfCB8Db5952b3ba5bb56Cc38f8C9';
|
|
|
36
36
|
// ============================================================================
|
|
37
37
|
/** WOKB 原生包装代币 */
|
|
38
38
|
export const WOKB = '0xe538905cf8410324e03a5a23c1c177a474d59b2b';
|
|
39
|
+
/** ✅ USDT 代币地址(XLayer,6 位精度) */
|
|
40
|
+
export const USDT = '0x1e4a5963abfd975d8c9021ce480b42188849d41d';
|
|
41
|
+
/** ✅ 零地址(表示原生代币) */
|
|
42
|
+
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
39
43
|
/** Multicall3 合约 */
|
|
40
44
|
export const MULTICALL3 = '0xca11bde05977b3631167028862be2a173976ca11';
|
|
41
45
|
/** PotatoSwap V2 Router */
|
|
@@ -46,21 +50,11 @@ export const POTATOSWAP_SWAP_ROUTER02 = '0xB45D0149249488333E3F3f9F359807F4b810C
|
|
|
46
50
|
export const POTATOSWAP_V3_ROUTER = '0xBB069e9465BcabC4F488d21e793BDEf0F2d41D41';
|
|
47
51
|
/** PotatoSwap V3 Factory */
|
|
48
52
|
export const POTATOSWAP_V3_FACTORY = '0xa1415fAe79c4B196d087F02b8aD5a622B8A827E5';
|
|
49
|
-
// ============================================================================
|
|
50
|
-
// 稳定币地址
|
|
51
|
-
// ============================================================================
|
|
52
|
-
/** USDT (6位精度) */
|
|
53
|
-
export const USDT = '0x1e4a5963abfd975d8c9021ce480b42188849d41d';
|
|
54
53
|
/** USDC (6位精度) */
|
|
55
54
|
export const USDC = '0x74b7f16337b8972027f6196a17a631ac6de26d22';
|
|
56
55
|
/** USD₮0 / USDT0 (6位精度) - Flap xLayer 支持的稳定币计价 */
|
|
57
56
|
export const USDT0 = '0x779ded0c9e1022225f8e0630b35a9b54be713736';
|
|
58
57
|
// ============================================================================
|
|
59
|
-
// 零地址
|
|
60
|
-
// ============================================================================
|
|
61
|
-
/** 零地址(表示原生代币 OKB) */
|
|
62
|
-
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
63
|
-
// ============================================================================
|
|
64
58
|
// Gas 配置
|
|
65
59
|
// ============================================================================
|
|
66
60
|
/** 默认 Gas Price 兜底值(0.1 gwei) */
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* XLayer AA Buy-First(外盘 / PotatoSwap V2)
|
|
3
|
-
*
|
|
4
|
-
* 对齐 BSC buy-first 思路:
|
|
5
|
-
* - 买方先买入(OKB -> token)
|
|
6
|
-
* - 卖方再卖出等值 token(token -> OKB,卖方需要预持仓)
|
|
7
|
-
* - ✅ 利润:在卖出后归集阶段刮取
|
|
3
|
+
* ✅ 优化版本:并行化 RPC、批量签名、合并 approve+sell
|
|
8
4
|
*/
|
|
9
5
|
import type { XLayerConfig } from './types.js';
|
|
10
6
|
import type { BuyFirstParams, BuyFirstResult } from './types.js';
|
|
@@ -14,11 +10,11 @@ export declare class AADexBuyFirstExecutor {
|
|
|
14
10
|
private dexQuery;
|
|
15
11
|
private config;
|
|
16
12
|
constructor(config?: XLayerConfig);
|
|
17
|
-
private getEffectiveRouter;
|
|
18
13
|
private runHandleOps;
|
|
19
14
|
private pickWallets;
|
|
20
15
|
private buildWithdrawUserOp;
|
|
21
16
|
private safeQuoteBuy;
|
|
17
|
+
private initNonceAndInitCode;
|
|
22
18
|
execute(params: BuyFirstParams): Promise<BuyFirstResult>;
|
|
23
19
|
}
|
|
24
20
|
export declare function createAADexBuyFirstExecutor(config?: XLayerConfig): AADexBuyFirstExecutor;
|
|
@@ -1,72 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* XLayer AA Buy-First(外盘 / PotatoSwap V2)
|
|
3
|
-
*
|
|
4
|
-
* 对齐 BSC buy-first 思路:
|
|
5
|
-
* - 买方先买入(OKB -> token)
|
|
6
|
-
* - 卖方再卖出等值 token(token -> OKB,卖方需要预持仓)
|
|
7
|
-
* - ✅ 利润:在卖出后归集阶段刮取
|
|
3
|
+
* ✅ 优化版本:并行化 RPC、批量签名、合并 approve+sell
|
|
8
4
|
*/
|
|
9
5
|
import { Contract, Interface, Wallet, ethers } from 'ethers';
|
|
10
6
|
import { AANonceMap } from './types.js';
|
|
11
|
-
import { ENTRYPOINT_ABI, DEFAULT_WITHDRAW_RESERVE, POTATOSWAP_V2_ROUTER, WOKB, } from './constants.js';
|
|
7
|
+
import { ENTRYPOINT_ABI, DEFAULT_WITHDRAW_RESERVE, POTATOSWAP_V2_ROUTER, WOKB, ZERO_ADDRESS, } from './constants.js';
|
|
12
8
|
import { AAAccountManager, encodeExecute } from './aa-account.js';
|
|
13
|
-
import { DexQuery
|
|
14
|
-
import { PortalQuery,
|
|
9
|
+
import { DexQuery } from './dex.js';
|
|
10
|
+
import { PortalQuery, parseOkb } from './portal-ops.js';
|
|
15
11
|
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
16
|
-
import {
|
|
17
|
-
|
|
12
|
+
import { getDexDeadline, resolveProfitSettings, calculateProfitWei, splitAmount, getEffectiveRouter } from './dex-helpers.js';
|
|
13
|
+
import { fetchAccountInfoBatch } from './account-fetcher.js';
|
|
14
|
+
import { buildBuyUserOps, buildSellUserOps } from './userOp-builder.js';
|
|
18
15
|
const DEFAULT_CALL_GAS_LIMIT_BUY = 450000n;
|
|
19
16
|
const DEFAULT_CALL_GAS_LIMIT_SELL = 450000n;
|
|
20
17
|
const DEFAULT_CALL_GAS_LIMIT_APPROVE = 200000n;
|
|
21
18
|
const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
|
|
22
|
-
function getDexDeadline(minutes = 20) {
|
|
23
|
-
return Math.floor(Date.now() / 1000) + minutes * 60;
|
|
24
|
-
}
|
|
25
|
-
function resolveProfitSettings(config) {
|
|
26
|
-
const extractProfit = config?.extractProfit !== false; // 默认 true
|
|
27
|
-
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
|
|
28
|
-
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
29
|
-
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
30
|
-
return { extractProfit, profitBps, profitRecipient };
|
|
31
|
-
}
|
|
32
|
-
function calculateProfitWei(amountWei, profitBps) {
|
|
33
|
-
if (amountWei <= 0n)
|
|
34
|
-
return 0n;
|
|
35
|
-
if (!Number.isFinite(profitBps) || profitBps <= 0)
|
|
36
|
-
return 0n;
|
|
37
|
-
return (amountWei * BigInt(profitBps)) / 10000n;
|
|
38
|
-
}
|
|
39
|
-
function shuffle(arr) {
|
|
40
|
-
const a = arr.slice();
|
|
41
|
-
for (let i = a.length - 1; i > 0; i--) {
|
|
42
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
43
|
-
[a[i], a[j]] = [a[j], a[i]];
|
|
44
|
-
}
|
|
45
|
-
return a;
|
|
46
|
-
}
|
|
47
|
-
function splitAmount(totalAmount, count) {
|
|
48
|
-
if (count <= 0)
|
|
49
|
-
throw new Error('拆分份数必须大于 0');
|
|
50
|
-
if (count === 1)
|
|
51
|
-
return [totalAmount];
|
|
52
|
-
if (totalAmount <= 0n)
|
|
53
|
-
return new Array(count).fill(0n);
|
|
54
|
-
const weights = [];
|
|
55
|
-
for (let i = 0; i < count; i++) {
|
|
56
|
-
const w = BigInt(500000 + Math.floor(Math.random() * 1000000)); // [0.5,1.5)
|
|
57
|
-
weights.push(w);
|
|
58
|
-
}
|
|
59
|
-
const totalWeight = weights.reduce((a, b) => a + b, 0n);
|
|
60
|
-
const amounts = [];
|
|
61
|
-
let allocated = 0n;
|
|
62
|
-
for (let i = 0; i < count - 1; i++) {
|
|
63
|
-
const amt = (totalAmount * weights[i]) / totalWeight;
|
|
64
|
-
amounts.push(amt);
|
|
65
|
-
allocated += amt;
|
|
66
|
-
}
|
|
67
|
-
amounts.push(totalAmount - allocated);
|
|
68
|
-
return shuffle(amounts);
|
|
69
|
-
}
|
|
70
19
|
export class AADexBuyFirstExecutor {
|
|
71
20
|
constructor(config = {}) {
|
|
72
21
|
this.config = config;
|
|
@@ -74,14 +23,6 @@ export class AADexBuyFirstExecutor {
|
|
|
74
23
|
this.portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
|
|
75
24
|
this.dexQuery = new DexQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
|
|
76
25
|
}
|
|
77
|
-
getEffectiveRouter(params) {
|
|
78
|
-
if (params.routerAddress)
|
|
79
|
-
return params.routerAddress;
|
|
80
|
-
if (params.dexKey === 'DYORSWAP') {
|
|
81
|
-
return '0xfb001fbbace32f09cb6d3c449b935183de53ee96';
|
|
82
|
-
}
|
|
83
|
-
return POTATOSWAP_V2_ROUTER;
|
|
84
|
-
}
|
|
85
26
|
async runHandleOps(label, ops, bundlerSigner, beneficiary) {
|
|
86
27
|
const provider = this.aaManager.getProvider();
|
|
87
28
|
const entryPointAddress = this.aaManager.getEntryPointAddress();
|
|
@@ -153,9 +94,8 @@ export class AADexBuyFirstExecutor {
|
|
|
153
94
|
: 0n;
|
|
154
95
|
if (withdrawAmount <= 0n)
|
|
155
96
|
return null;
|
|
156
|
-
const { extractProfit, profitBps
|
|
97
|
+
const { extractProfit, profitBps } = resolveProfitSettings(effConfig);
|
|
157
98
|
const profitWei = extractProfit ? calculateProfitWei(withdrawAmount, profitBps) : 0n;
|
|
158
|
-
const toOwnerWei = withdrawAmount - profitWei;
|
|
159
99
|
const callData = encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
|
|
160
100
|
const { userOp } = gasPolicy === 'fixed'
|
|
161
101
|
? await this.aaManager.buildUserOpWithFixedGas({
|
|
@@ -189,12 +129,49 @@ export class AADexBuyFirstExecutor {
|
|
|
189
129
|
return 0n;
|
|
190
130
|
}
|
|
191
131
|
}
|
|
132
|
+
initNonceAndInitCode(params) {
|
|
133
|
+
const { buyers, sellers, buyerAis, sellerAis } = params;
|
|
134
|
+
const nonceMap = new AANonceMap();
|
|
135
|
+
// ✅ 使用 Set 去重,避免重复初始化
|
|
136
|
+
const uniqueSenders = new Set();
|
|
137
|
+
for (const ai of [...buyerAis, ...sellerAis]) {
|
|
138
|
+
const senderLower = ai.sender.toLowerCase();
|
|
139
|
+
if (!uniqueSenders.has(senderLower)) {
|
|
140
|
+
nonceMap.init(ai.sender, ai.nonce);
|
|
141
|
+
uniqueSenders.add(senderLower);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const initCodeBySenderLower = new Map();
|
|
145
|
+
const initIfNeeded = (sender, deployed, ownerAddress) => {
|
|
146
|
+
const k = sender.toLowerCase();
|
|
147
|
+
if (initCodeBySenderLower.has(k))
|
|
148
|
+
return;
|
|
149
|
+
initCodeBySenderLower.set(k, deployed ? '0x' : this.aaManager.generateInitCode(ownerAddress));
|
|
150
|
+
};
|
|
151
|
+
const consumeInitCode = (sender) => {
|
|
152
|
+
const k = sender.toLowerCase();
|
|
153
|
+
const cur = initCodeBySenderLower.get(k);
|
|
154
|
+
if (cur === undefined)
|
|
155
|
+
throw new Error(`initCode not initialized for sender: ${sender}`);
|
|
156
|
+
if (cur !== '0x')
|
|
157
|
+
initCodeBySenderLower.set(k, '0x');
|
|
158
|
+
return cur;
|
|
159
|
+
};
|
|
160
|
+
for (let i = 0; i < buyers.length; i++)
|
|
161
|
+
initIfNeeded(buyerAis[i].sender, buyerAis[i].deployed, buyers[i].address);
|
|
162
|
+
for (let i = 0; i < sellers.length; i++)
|
|
163
|
+
initIfNeeded(sellerAis[i].sender, sellerAis[i].deployed, sellers[i].address);
|
|
164
|
+
return { nonceMap, consumeInitCode };
|
|
165
|
+
}
|
|
192
166
|
async execute(params) {
|
|
193
|
-
const { tradeType = 'V2',
|
|
167
|
+
const { tradeType = 'V2', routerVersion = 'V2', // ✅ 新增
|
|
168
|
+
dexKey, routerAddress, deadlineMinutes = 20, tokenAddress, buyerPrivateKeys, sellerPrivateKeys, buyerFunds, buyCount, sellCount, slippageBps = 0, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, quoteToken, // ✅ 新增
|
|
169
|
+
quoteTokenDecimals = 18, // ✅ 新增
|
|
170
|
+
} = params;
|
|
194
171
|
if (tradeType !== 'V2') {
|
|
195
172
|
throw new Error('AADexBuyFirstExecutor 仅支持 tradeType=V2(外盘)');
|
|
196
173
|
}
|
|
197
|
-
const effectiveRouter =
|
|
174
|
+
const effectiveRouter = getEffectiveRouter({ dexKey, routerAddress, routerVersion }, POTATOSWAP_V2_ROUTER);
|
|
198
175
|
const deadline = getDexDeadline(deadlineMinutes);
|
|
199
176
|
const buyers = this.pickWallets(buyerPrivateKeys, buyCount);
|
|
200
177
|
const sellers = this.pickWallets(sellerPrivateKeys, sellCount);
|
|
@@ -204,49 +181,60 @@ export class AADexBuyFirstExecutor {
|
|
|
204
181
|
throw new Error('sellCount=0 或 sellerPrivateKeys 为空');
|
|
205
182
|
const effConfig = { ...(this.config ?? {}), ...(config ?? {}) };
|
|
206
183
|
const profitSettings = resolveProfitSettings(effConfig);
|
|
207
|
-
const
|
|
184
|
+
const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
|
|
185
|
+
// ✅ 解析买入金额(根据 quoteToken 精度)
|
|
186
|
+
const totalBuyWei = useNativeToken
|
|
187
|
+
? parseOkb(String(buyerFunds))
|
|
188
|
+
: ethers.parseUnits(String(buyerFunds), quoteTokenDecimals);
|
|
208
189
|
if (totalBuyWei <= 0n)
|
|
209
190
|
throw new Error('buyerFunds 需要 > 0');
|
|
210
191
|
const buyAmountsWei = splitAmount(totalBuyWei, buyers.length);
|
|
211
|
-
|
|
192
|
+
// ✅ 并行报价(支持 OKB 或 USDT)
|
|
193
|
+
const quotedPerBuy = await mapWithConcurrency(buyAmountsWei, 6, async (amt) => {
|
|
194
|
+
if (useNativeToken) {
|
|
195
|
+
return await this.safeQuoteBuy(tokenAddress, amt);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// USDT → Token 报价
|
|
199
|
+
try {
|
|
200
|
+
return await this.dexQuery.quoteTokenToToken(quoteToken, tokenAddress, amt);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return 0n;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
212
207
|
const quotedTotalTokenOut = quotedPerBuy.reduce((a, b) => a + (b ?? 0n), 0n);
|
|
213
208
|
const estimatedTokenOutWei = (quotedTotalTokenOut * BigInt(10000 - Math.max(0, Math.min(10000, slippageBps)))) / 10000n;
|
|
214
209
|
if (estimatedTokenOutWei <= 0n) {
|
|
215
210
|
throw new Error('买入报价为 0,无法规划 sellAmount(可能无流动性)');
|
|
216
211
|
}
|
|
217
212
|
const sellAmountsWei = splitAmount(estimatedTokenOutWei, sellers.length);
|
|
218
|
-
//
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
for (let i = 0; i < buyers.length; i++)
|
|
244
|
-
initIfNeeded(buyerAis[i].sender, buyerAis[i].deployed, buyers[i].address);
|
|
245
|
-
for (let i = 0; i < sellers.length; i++)
|
|
246
|
-
initIfNeeded(sellerAis[i].sender, sellerAis[i].deployed, sellers[i].address);
|
|
247
|
-
// 卖方预持仓检查
|
|
248
|
-
const sellerSenders = sellerAis.map((ai) => ai.sender);
|
|
249
|
-
const sellerTokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, sellerSenders);
|
|
213
|
+
// ✅ 优化 1: 并行获取所有账户信息
|
|
214
|
+
const accountInfo = await fetchAccountInfoBatch({
|
|
215
|
+
buyers,
|
|
216
|
+
sellers,
|
|
217
|
+
tokenAddress,
|
|
218
|
+
effectiveRouter,
|
|
219
|
+
aaManager: this.aaManager,
|
|
220
|
+
portalQuery: this.portalQuery
|
|
221
|
+
});
|
|
222
|
+
const { buyerAis, sellerAis, sellerSenders, sellerTokenBalances, sellerAllowances } = accountInfo;
|
|
223
|
+
// ✅ 验证买方余额(OKB 或 USDT)
|
|
224
|
+
if (!useNativeToken) {
|
|
225
|
+
const buyerSenders = buyerAis.map(ai => ai.sender);
|
|
226
|
+
const buyerQuoteBalances = await this.portalQuery.getMultipleTokenBalances(quoteToken, buyerSenders);
|
|
227
|
+
for (let i = 0; i < buyers.length; i++) {
|
|
228
|
+
const sender = buyerSenders[i];
|
|
229
|
+
const needBuy = buyAmountsWei[i] ?? 0n;
|
|
230
|
+
const bal = buyerQuoteBalances.get(sender) ?? 0n;
|
|
231
|
+
if (needBuy > bal) {
|
|
232
|
+
const tokenName = quoteTokenDecimals === 6 ? 'USDT' : 'ERC20';
|
|
233
|
+
throw new Error(`买方 ${tokenName} 余额不足: sender=${sender} need=${needBuy.toString()} bal=${bal.toString()}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// 验证卖方余额
|
|
250
238
|
for (let i = 0; i < sellers.length; i++) {
|
|
251
239
|
const sender = sellerSenders[i];
|
|
252
240
|
const needSell = sellAmountsWei[i] ?? 0n;
|
|
@@ -255,80 +243,41 @@ export class AADexBuyFirstExecutor {
|
|
|
255
243
|
throw new Error(`卖方预持仓不足: sender=${sender} need=${needSell.toString()} bal=${bal.toString()}`);
|
|
256
244
|
}
|
|
257
245
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (sellWei <= 0n)
|
|
294
|
-
continue;
|
|
295
|
-
const allowance = sellerAllowances.get(sender) ?? 0n;
|
|
296
|
-
if (allowance < sellWei) {
|
|
297
|
-
const approveCallData = encodeExecute(tokenAddress, 0n, encodeApproveCall(effectiveRouter, ethers.MaxUint256));
|
|
298
|
-
const { userOp: approveOp, prefundWei } = await this.aaManager.buildUserOpWithFixedGas({
|
|
299
|
-
ownerWallet: w,
|
|
300
|
-
sender,
|
|
301
|
-
nonce: nonceMap.next(sender),
|
|
302
|
-
initCode: consumeInitCode(sender),
|
|
303
|
-
callData: approveCallData,
|
|
304
|
-
deployed: ai.deployed,
|
|
305
|
-
fixedGas: {
|
|
306
|
-
...(effConfig.fixedGas ?? {}),
|
|
307
|
-
callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_APPROVE,
|
|
308
|
-
},
|
|
309
|
-
});
|
|
310
|
-
await this.aaManager.ensureSenderBalance(w, sender, prefundWei + parseOkb('0.0001'), `buyFirstDex/seller${i + 1}/approve-fund`);
|
|
311
|
-
const signedApprove = await this.aaManager.signUserOp(approveOp, w);
|
|
312
|
-
ops.push(signedApprove.userOp);
|
|
313
|
-
}
|
|
314
|
-
const sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellWei, 0n, [tokenAddress, WOKB], sender, deadline);
|
|
315
|
-
const sellCallData = encodeExecute(effectiveRouter, 0n, sellSwapData);
|
|
316
|
-
const { userOp: sellOp, prefundWei: sellPrefund } = await this.aaManager.buildUserOpWithFixedGas({
|
|
317
|
-
ownerWallet: w,
|
|
318
|
-
sender,
|
|
319
|
-
nonce: nonceMap.next(sender),
|
|
320
|
-
initCode: consumeInitCode(sender),
|
|
321
|
-
callData: sellCallData,
|
|
322
|
-
deployed: ai.deployed,
|
|
323
|
-
fixedGas: {
|
|
324
|
-
...(effConfig.fixedGas ?? {}),
|
|
325
|
-
callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_SELL,
|
|
326
|
-
},
|
|
327
|
-
});
|
|
328
|
-
await this.aaManager.ensureSenderBalance(w, sender, sellPrefund + parseOkb('0.0001'), `buyFirstDex/seller${i + 1}/sell-fund`);
|
|
329
|
-
const signedSell = await this.aaManager.signUserOp(sellOp, w);
|
|
330
|
-
ops.push(signedSell.userOp);
|
|
331
|
-
}
|
|
246
|
+
const { nonceMap, consumeInitCode } = this.initNonceAndInitCode({ buyers, sellers, buyerAis, sellerAis });
|
|
247
|
+
// ✅ 优化 2 + 3: 并行构建买入和卖出 UserOp(卖出合并 approve+sell)
|
|
248
|
+
const [buyOps, sellOps] = await Promise.all([
|
|
249
|
+
buildBuyUserOps({
|
|
250
|
+
buyers,
|
|
251
|
+
buyerAis,
|
|
252
|
+
buyAmountsWei,
|
|
253
|
+
tokenAddress,
|
|
254
|
+
effectiveRouter,
|
|
255
|
+
deadline,
|
|
256
|
+
wokbAddress: WOKB,
|
|
257
|
+
nonceMap,
|
|
258
|
+
consumeInitCode,
|
|
259
|
+
aaManager: this.aaManager,
|
|
260
|
+
config: effConfig,
|
|
261
|
+
defaultCallGasLimit: DEFAULT_CALL_GAS_LIMIT_BUY
|
|
262
|
+
}),
|
|
263
|
+
buildSellUserOps({
|
|
264
|
+
sellers,
|
|
265
|
+
sellerAis,
|
|
266
|
+
sellAmountsWei,
|
|
267
|
+
tokenAddress,
|
|
268
|
+
effectiveRouter,
|
|
269
|
+
deadline,
|
|
270
|
+
wokbAddress: WOKB,
|
|
271
|
+
sellerAllowances,
|
|
272
|
+
nonceMap,
|
|
273
|
+
consumeInitCode,
|
|
274
|
+
aaManager: this.aaManager,
|
|
275
|
+
config: effConfig,
|
|
276
|
+
defaultApproveGasLimit: DEFAULT_CALL_GAS_LIMIT_APPROVE,
|
|
277
|
+
defaultSellGasLimit: DEFAULT_CALL_GAS_LIMIT_SELL
|
|
278
|
+
})
|
|
279
|
+
]);
|
|
280
|
+
const ops = [...buyOps, ...sellOps];
|
|
332
281
|
if (ops.length === 0)
|
|
333
282
|
throw new Error('本轮没有生成任何 UserOp');
|
|
334
283
|
const payerWallet = buyers[0];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer AA DEX 工具函数
|
|
3
|
+
*/
|
|
4
|
+
export declare function getDexDeadline(minutes?: number): number;
|
|
5
|
+
export declare function calculateProfitWei(amountWei: bigint, profitBps: number): bigint;
|
|
6
|
+
export declare function resolveProfitSettings(config?: {
|
|
7
|
+
extractProfit?: boolean;
|
|
8
|
+
profitBps?: number;
|
|
9
|
+
profitRecipient?: string;
|
|
10
|
+
}): {
|
|
11
|
+
extractProfit: boolean;
|
|
12
|
+
profitBps: number;
|
|
13
|
+
profitRecipient: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function splitAmount(totalAmount: bigint, count: number): bigint[];
|
|
16
|
+
export declare function getEffectiveRouter(params: {
|
|
17
|
+
dexKey?: string;
|
|
18
|
+
routerAddress?: string;
|
|
19
|
+
routerVersion?: 'V2' | 'V3';
|
|
20
|
+
}, defaultRouter: string): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer AA DEX 工具函数
|
|
3
|
+
*/
|
|
4
|
+
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
5
|
+
export function getDexDeadline(minutes = 20) {
|
|
6
|
+
return Math.floor(Date.now() / 1000) + minutes * 60;
|
|
7
|
+
}
|
|
8
|
+
export function calculateProfitWei(amountWei, profitBps) {
|
|
9
|
+
if (amountWei <= 0n)
|
|
10
|
+
return 0n;
|
|
11
|
+
if (!Number.isFinite(profitBps) || profitBps <= 0)
|
|
12
|
+
return 0n;
|
|
13
|
+
return (amountWei * BigInt(profitBps)) / 10000n;
|
|
14
|
+
}
|
|
15
|
+
export function resolveProfitSettings(config) {
|
|
16
|
+
const extractProfit = config?.extractProfit !== false;
|
|
17
|
+
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
|
|
18
|
+
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
19
|
+
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
20
|
+
return { extractProfit, profitBps, profitRecipient };
|
|
21
|
+
}
|
|
22
|
+
function shuffle(arr) {
|
|
23
|
+
const a = arr.slice();
|
|
24
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
25
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
26
|
+
[a[i], a[j]] = [a[j], a[i]];
|
|
27
|
+
}
|
|
28
|
+
return a;
|
|
29
|
+
}
|
|
30
|
+
export function splitAmount(totalAmount, count) {
|
|
31
|
+
if (count <= 0)
|
|
32
|
+
throw new Error('拆分份数必须大于 0');
|
|
33
|
+
if (count === 1)
|
|
34
|
+
return [totalAmount];
|
|
35
|
+
if (totalAmount <= 0n)
|
|
36
|
+
return new Array(count).fill(0n);
|
|
37
|
+
const weights = [];
|
|
38
|
+
for (let i = 0; i < count; i++) {
|
|
39
|
+
const w = BigInt(500000 + Math.floor(Math.random() * 1000000));
|
|
40
|
+
weights.push(w);
|
|
41
|
+
}
|
|
42
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0n);
|
|
43
|
+
const amounts = [];
|
|
44
|
+
let allocated = 0n;
|
|
45
|
+
for (let i = 0; i < count - 1; i++) {
|
|
46
|
+
const amt = (totalAmount * weights[i]) / totalWeight;
|
|
47
|
+
amounts.push(amt);
|
|
48
|
+
allocated += amt;
|
|
49
|
+
}
|
|
50
|
+
amounts.push(totalAmount - allocated);
|
|
51
|
+
return shuffle(amounts);
|
|
52
|
+
}
|
|
53
|
+
export function getEffectiveRouter(params, defaultRouter) {
|
|
54
|
+
// 优先使用显式指定的地址
|
|
55
|
+
if (params.routerAddress)
|
|
56
|
+
return params.routerAddress;
|
|
57
|
+
// 根据 dexKey 选择
|
|
58
|
+
if (params.dexKey === 'DYORSWAP') {
|
|
59
|
+
return '0xfb001fbbace32f09cb6d3c449b935183de53ee96';
|
|
60
|
+
}
|
|
61
|
+
// 根据 routerVersion 选择
|
|
62
|
+
if (params.routerVersion === 'V3') {
|
|
63
|
+
return '0xBB069e9465BcabC4F488d21e793BDEf0F2d41D41'; // POTATOSWAP_V3_ROUTER
|
|
64
|
+
}
|
|
65
|
+
// 默认使用传入的 defaultRouter(通常是 V2)
|
|
66
|
+
return defaultRouter;
|
|
67
|
+
}
|
package/dist/xlayer/dex.d.ts
CHANGED
|
@@ -64,6 +64,10 @@ export declare class DexQuery {
|
|
|
64
64
|
* 获取 Token -> OKB 的预期输出
|
|
65
65
|
*/
|
|
66
66
|
quoteTokenToOkb(tokenAmount: bigint, tokenAddress: string): Promise<bigint>;
|
|
67
|
+
/**
|
|
68
|
+
* ✅ 获取 Token → Token 的预期输出(支持 USDT → Token)
|
|
69
|
+
*/
|
|
70
|
+
quoteTokenToToken(inputToken: string, outputToken: string, inputAmount: bigint): Promise<bigint>;
|
|
67
71
|
/**
|
|
68
72
|
* 获取 WOKB 地址
|
|
69
73
|
*/
|
package/dist/xlayer/dex.js
CHANGED
|
@@ -129,6 +129,14 @@ export class DexQuery {
|
|
|
129
129
|
const amounts = await this.getAmountsOut(tokenAmount, path);
|
|
130
130
|
return amounts[amounts.length - 1];
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* ✅ 获取 Token → Token 的预期输出(支持 USDT → Token)
|
|
134
|
+
*/
|
|
135
|
+
async quoteTokenToToken(inputToken, outputToken, inputAmount) {
|
|
136
|
+
const path = [inputToken, outputToken];
|
|
137
|
+
const amounts = await this.getAmountsOut(inputAmount, path);
|
|
138
|
+
return amounts[amounts.length - 1];
|
|
139
|
+
}
|
|
132
140
|
/**
|
|
133
141
|
* 获取 WOKB 地址
|
|
134
142
|
*/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer Router 管理器
|
|
3
|
+
* 统一管理 V2/V3 Router 地址和授权目标选择逻辑
|
|
4
|
+
*/
|
|
5
|
+
import type { RouterVersion, ApprovalTarget } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* ✅ 获取 Router 地址
|
|
8
|
+
* @param params.routerVersion - Router 版本(V2 或 V3)
|
|
9
|
+
* @param params.dexKey - DEX 标识(如 DYORSWAP)
|
|
10
|
+
* @param params.routerAddress - 显式指定的 Router 地址(优先级最高)
|
|
11
|
+
* @returns Router 地址
|
|
12
|
+
*/
|
|
13
|
+
export declare function getRouterAddress(params: {
|
|
14
|
+
routerVersion?: RouterVersion;
|
|
15
|
+
dexKey?: string;
|
|
16
|
+
routerAddress?: string;
|
|
17
|
+
}): string;
|
|
18
|
+
/**
|
|
19
|
+
* ✅ 获取授权目标地址
|
|
20
|
+
* @param target - 授权目标类型
|
|
21
|
+
* @returns 目标合约地址
|
|
22
|
+
*/
|
|
23
|
+
export declare function getApprovalTargetAddress(target: ApprovalTarget): string;
|
|
24
|
+
/**
|
|
25
|
+
* ✅ 获取所有授权目标地址
|
|
26
|
+
* @param targets - 授权目标类型数组
|
|
27
|
+
* @returns 目标信息数组
|
|
28
|
+
*/
|
|
29
|
+
export declare function getAllApprovalTargets(targets: ApprovalTarget[]): Array<{
|
|
30
|
+
target: ApprovalTarget;
|
|
31
|
+
address: string;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* ✅ 根据交易类型推断授权目标
|
|
35
|
+
* @param tradeType - 交易类型(FLAP/V2/V3)
|
|
36
|
+
* @returns 授权目标类型
|
|
37
|
+
*/
|
|
38
|
+
export declare function inferApprovalTarget(tradeType: string): ApprovalTarget;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer Router 管理器
|
|
3
|
+
* 统一管理 V2/V3 Router 地址和授权目标选择逻辑
|
|
4
|
+
*/
|
|
5
|
+
import { POTATOSWAP_V2_ROUTER, POTATOSWAP_V3_ROUTER, FLAP_PORTAL, } from './constants.js';
|
|
6
|
+
/**
|
|
7
|
+
* ✅ 获取 Router 地址
|
|
8
|
+
* @param params.routerVersion - Router 版本(V2 或 V3)
|
|
9
|
+
* @param params.dexKey - DEX 标识(如 DYORSWAP)
|
|
10
|
+
* @param params.routerAddress - 显式指定的 Router 地址(优先级最高)
|
|
11
|
+
* @returns Router 地址
|
|
12
|
+
*/
|
|
13
|
+
export function getRouterAddress(params) {
|
|
14
|
+
// 1. 优先使用显式指定的地址
|
|
15
|
+
if (params.routerAddress)
|
|
16
|
+
return params.routerAddress;
|
|
17
|
+
// 2. 根据 dexKey 选择
|
|
18
|
+
if (params.dexKey === 'DYORSWAP') {
|
|
19
|
+
return '0xfb001fbbace32f09cb6d3c449b935183de53ee96';
|
|
20
|
+
}
|
|
21
|
+
// 3. 根据 routerVersion 选择
|
|
22
|
+
const version = params.routerVersion ?? 'V2';
|
|
23
|
+
if (version === 'V3') {
|
|
24
|
+
return POTATOSWAP_V3_ROUTER;
|
|
25
|
+
}
|
|
26
|
+
// 4. 默认 V2
|
|
27
|
+
return POTATOSWAP_V2_ROUTER;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* ✅ 获取授权目标地址
|
|
31
|
+
* @param target - 授权目标类型
|
|
32
|
+
* @returns 目标合约地址
|
|
33
|
+
*/
|
|
34
|
+
export function getApprovalTargetAddress(target) {
|
|
35
|
+
switch (target) {
|
|
36
|
+
case 'FLAP_PORTAL':
|
|
37
|
+
return FLAP_PORTAL;
|
|
38
|
+
case 'V2_ROUTER':
|
|
39
|
+
return POTATOSWAP_V2_ROUTER;
|
|
40
|
+
case 'V3_ROUTER':
|
|
41
|
+
return POTATOSWAP_V3_ROUTER;
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(`Unknown approval target: ${target}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* ✅ 获取所有授权目标地址
|
|
48
|
+
* @param targets - 授权目标类型数组
|
|
49
|
+
* @returns 目标信息数组
|
|
50
|
+
*/
|
|
51
|
+
export function getAllApprovalTargets(targets) {
|
|
52
|
+
return targets.map(target => ({
|
|
53
|
+
target,
|
|
54
|
+
address: getApprovalTargetAddress(target)
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* ✅ 根据交易类型推断授权目标
|
|
59
|
+
* @param tradeType - 交易类型(FLAP/V2/V3)
|
|
60
|
+
* @returns 授权目标类型
|
|
61
|
+
*/
|
|
62
|
+
export function inferApprovalTarget(tradeType) {
|
|
63
|
+
const type = String(tradeType || '').toUpperCase();
|
|
64
|
+
if (type === 'V2')
|
|
65
|
+
return 'V2_ROUTER';
|
|
66
|
+
if (type === 'V3')
|
|
67
|
+
return 'V3_ROUTER';
|
|
68
|
+
return 'FLAP_PORTAL'; // 默认内盘
|
|
69
|
+
}
|
package/dist/xlayer/types.d.ts
CHANGED
|
@@ -661,8 +661,14 @@ export interface VolumeResult {
|
|
|
661
661
|
totalVolume: bigint;
|
|
662
662
|
}
|
|
663
663
|
export type BuyFirstTradeType = 'FLAP' | 'V2';
|
|
664
|
+
/** ✅ Router 版本类型 */
|
|
665
|
+
export type RouterVersion = 'V2' | 'V3';
|
|
666
|
+
/** ✅ 授权目标类型 */
|
|
667
|
+
export type ApprovalTarget = 'FLAP_PORTAL' | 'V2_ROUTER' | 'V3_ROUTER';
|
|
664
668
|
export interface BuyFirstParams {
|
|
665
669
|
tradeType?: BuyFirstTradeType;
|
|
670
|
+
/** ✅ 新增:Router 版本(V2 或 V3,默认 V2) */
|
|
671
|
+
routerVersion?: RouterVersion;
|
|
666
672
|
/** DEX 标识(仅 V2 时使用,用于选择 Router) */
|
|
667
673
|
dexKey?: string;
|
|
668
674
|
/** 显式指定 Router 地址(仅 V2 时使用) */
|
|
@@ -672,10 +678,14 @@ export interface BuyFirstParams {
|
|
|
672
678
|
tokenAddress: string;
|
|
673
679
|
buyerPrivateKeys: string[];
|
|
674
680
|
sellerPrivateKeys: string[];
|
|
675
|
-
/** 本轮总买入资金(OKB) */
|
|
681
|
+
/** 本轮总买入资金(OKB 或 quoteToken) */
|
|
676
682
|
buyerFunds: string;
|
|
677
683
|
buyCount?: number;
|
|
678
684
|
sellCount?: number;
|
|
685
|
+
/** ✅ 新增:买入使用的代币地址(零地址或不传表示使用 OKB,非零地址表示使用 ERC20 如 USDT) */
|
|
686
|
+
quoteToken?: string;
|
|
687
|
+
/** ✅ 新增:quoteToken 的精度(默认 18,USDT 在 XLayer 上是 6 位) */
|
|
688
|
+
quoteTokenDecimals?: number;
|
|
679
689
|
/** 报价滑点(仅用于估算 sellAmount),默认 0 */
|
|
680
690
|
slippageBps?: number;
|
|
681
691
|
withdrawToOwner?: boolean;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer AA UserOp 批量构建器
|
|
3
|
+
*/
|
|
4
|
+
import type { Wallet } from 'ethers';
|
|
5
|
+
import type { AAAccountManager } from './aa-account.js';
|
|
6
|
+
import type { AANonceMap } from './types.js';
|
|
7
|
+
import type { UserOperation, XLayerConfig } from './types.js';
|
|
8
|
+
export interface BuildBuyOpsParams {
|
|
9
|
+
buyers: Wallet[];
|
|
10
|
+
buyerAis: Array<{
|
|
11
|
+
sender: string;
|
|
12
|
+
deployed: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
buyAmountsWei: bigint[];
|
|
15
|
+
tokenAddress: string;
|
|
16
|
+
effectiveRouter: string;
|
|
17
|
+
deadline: number;
|
|
18
|
+
wokbAddress: string;
|
|
19
|
+
nonceMap: AANonceMap;
|
|
20
|
+
consumeInitCode: (sender: string) => string;
|
|
21
|
+
aaManager: AAAccountManager;
|
|
22
|
+
config: XLayerConfig;
|
|
23
|
+
defaultCallGasLimit: bigint;
|
|
24
|
+
}
|
|
25
|
+
export interface BuildSellOpsParams {
|
|
26
|
+
sellers: Wallet[];
|
|
27
|
+
sellerAis: Array<{
|
|
28
|
+
sender: string;
|
|
29
|
+
deployed: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
sellAmountsWei: bigint[];
|
|
32
|
+
tokenAddress: string;
|
|
33
|
+
effectiveRouter: string;
|
|
34
|
+
deadline: number;
|
|
35
|
+
wokbAddress: string;
|
|
36
|
+
sellerAllowances: Map<string, bigint>;
|
|
37
|
+
nonceMap: AANonceMap;
|
|
38
|
+
consumeInitCode: (sender: string) => string;
|
|
39
|
+
aaManager: AAAccountManager;
|
|
40
|
+
config: XLayerConfig;
|
|
41
|
+
defaultApproveGasLimit: bigint;
|
|
42
|
+
defaultSellGasLimit: bigint;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* ✅ 优化 1: 并行构建所有买入 UserOp
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildBuyUserOps(params: BuildBuyOpsParams): Promise<UserOperation[]>;
|
|
48
|
+
/**
|
|
49
|
+
* ✅ 优化 3: 合并 Approve 和 Sell
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildSellUserOps(params: BuildSellOpsParams): Promise<UserOperation[]>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer AA UserOp 批量构建器
|
|
3
|
+
*/
|
|
4
|
+
import { ethers } from 'ethers';
|
|
5
|
+
import { encodeExecute, encodeExecuteBatch } from './aa-account.js';
|
|
6
|
+
import { encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee } from './dex.js';
|
|
7
|
+
import { encodeApproveCall, parseOkb } from './portal-ops.js';
|
|
8
|
+
/**
|
|
9
|
+
* ✅ 优化 1: 并行构建所有买入 UserOp
|
|
10
|
+
*/
|
|
11
|
+
export async function buildBuyUserOps(params) {
|
|
12
|
+
const { buyers, buyerAis, buyAmountsWei, tokenAddress, effectiveRouter, deadline, wokbAddress, nonceMap, consumeInitCode, aaManager, config, defaultCallGasLimit } = params;
|
|
13
|
+
const buyOpPromises = buyers.map(async (w, i) => {
|
|
14
|
+
const ai = buyerAis[i];
|
|
15
|
+
const sender = ai.sender;
|
|
16
|
+
const buyWei = buyAmountsWei[i] ?? 0n;
|
|
17
|
+
if (buyWei <= 0n)
|
|
18
|
+
return null;
|
|
19
|
+
const swapData = encodeSwapExactETHForTokensSupportingFee(0n, [wokbAddress, tokenAddress], sender, deadline);
|
|
20
|
+
const callData = encodeExecute(effectiveRouter, buyWei, swapData);
|
|
21
|
+
const { userOp, prefundWei } = await aaManager.buildUserOpWithFixedGas({
|
|
22
|
+
ownerWallet: w,
|
|
23
|
+
sender,
|
|
24
|
+
nonce: nonceMap.next(sender),
|
|
25
|
+
initCode: consumeInitCode(sender),
|
|
26
|
+
callData,
|
|
27
|
+
deployed: ai.deployed,
|
|
28
|
+
fixedGas: {
|
|
29
|
+
...(config.fixedGas ?? {}),
|
|
30
|
+
callGasLimit: config.fixedGas?.callGasLimit ?? defaultCallGasLimit,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
await aaManager.ensureSenderBalance(w, sender, buyWei + prefundWei + parseOkb('0.0001'), `buyFirstDex/buyer${i + 1}/fund`);
|
|
34
|
+
const signed = await aaManager.signUserOp(userOp, w);
|
|
35
|
+
return signed.userOp;
|
|
36
|
+
});
|
|
37
|
+
const ops = await Promise.all(buyOpPromises);
|
|
38
|
+
return ops.filter((op) => op !== null);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* ✅ 优化 3: 合并 Approve 和 Sell
|
|
42
|
+
*/
|
|
43
|
+
export async function buildSellUserOps(params) {
|
|
44
|
+
const { sellers, sellerAis, sellAmountsWei, tokenAddress, effectiveRouter, deadline, wokbAddress, sellerAllowances, nonceMap, consumeInitCode, aaManager, config, defaultApproveGasLimit, defaultSellGasLimit } = params;
|
|
45
|
+
const sellOpPromises = sellers.map(async (w, i) => {
|
|
46
|
+
const ai = sellerAis[i];
|
|
47
|
+
const sender = ai.sender;
|
|
48
|
+
const sellWei = sellAmountsWei[i] ?? 0n;
|
|
49
|
+
if (sellWei <= 0n)
|
|
50
|
+
return null;
|
|
51
|
+
const allowance = sellerAllowances.get(sender) ?? 0n;
|
|
52
|
+
const needApprove = allowance < sellWei;
|
|
53
|
+
if (needApprove) {
|
|
54
|
+
// ✅ 合并 approve + sell 到一个 UserOp
|
|
55
|
+
const approveData = encodeApproveCall(effectiveRouter, ethers.MaxUint256);
|
|
56
|
+
const sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellWei, 0n, [tokenAddress, wokbAddress], sender, deadline);
|
|
57
|
+
const batchCallData = encodeExecuteBatch([tokenAddress, effectiveRouter], [0n, 0n], [approveData, sellSwapData]);
|
|
58
|
+
const { userOp, prefundWei } = await aaManager.buildUserOpWithFixedGas({
|
|
59
|
+
ownerWallet: w,
|
|
60
|
+
sender,
|
|
61
|
+
nonce: nonceMap.next(sender),
|
|
62
|
+
initCode: consumeInitCode(sender),
|
|
63
|
+
callData: batchCallData,
|
|
64
|
+
deployed: ai.deployed,
|
|
65
|
+
fixedGas: {
|
|
66
|
+
...(config.fixedGas ?? {}),
|
|
67
|
+
callGasLimit: config.fixedGas?.callGasLimit ?? (defaultApproveGasLimit + defaultSellGasLimit),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
await aaManager.ensureSenderBalance(w, sender, prefundWei + parseOkb('0.0001'), `buyFirstDex/seller${i + 1}/batch-fund`);
|
|
71
|
+
const signed = await aaManager.signUserOp(userOp, w);
|
|
72
|
+
return signed.userOp;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// 只需要 sell
|
|
76
|
+
const sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellWei, 0n, [tokenAddress, wokbAddress], sender, deadline);
|
|
77
|
+
const sellCallData = encodeExecute(effectiveRouter, 0n, sellSwapData);
|
|
78
|
+
const { userOp, prefundWei } = await aaManager.buildUserOpWithFixedGas({
|
|
79
|
+
ownerWallet: w,
|
|
80
|
+
sender,
|
|
81
|
+
nonce: nonceMap.next(sender),
|
|
82
|
+
initCode: consumeInitCode(sender),
|
|
83
|
+
callData: sellCallData,
|
|
84
|
+
deployed: ai.deployed,
|
|
85
|
+
fixedGas: {
|
|
86
|
+
...(config.fixedGas ?? {}),
|
|
87
|
+
callGasLimit: config.fixedGas?.callGasLimit ?? defaultSellGasLimit,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
await aaManager.ensureSenderBalance(w, sender, prefundWei + parseOkb('0.0001'), `buyFirstDex/seller${i + 1}/sell-fund`);
|
|
91
|
+
const signed = await aaManager.signUserOp(userOp, w);
|
|
92
|
+
return signed.userOp;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const ops = await Promise.all(sellOpPromises);
|
|
96
|
+
return ops.filter((op) => op !== null);
|
|
97
|
+
}
|