four-flap-meme-sdk 1.5.31 → 1.5.33
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/bundle.js +80 -7
- package/dist/xlayer/dex-bundle-swap.js +217 -12
- package/dist/xlayer/dex-bundle.d.ts +42 -0
- package/dist/xlayer/dex-bundle.js +378 -0
- package/dist/xlayer/dex-volume.d.ts +30 -0
- package/dist/xlayer/dex-volume.js +80 -0
- package/dist/xlayer/index.d.ts +2 -0
- package/dist/xlayer/index.js +8 -0
- package/dist/xlayer/portal-bundle-swap.js +213 -6
- package/dist/xlayer/types.d.ts +96 -0
- package/package.json +1 -1
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer 外盘(PotatoSwap/DEX)捆绑交易 SDK(AA / ERC-4337)
|
|
3
|
+
*
|
|
4
|
+
* 目标:对齐内盘 `bundle.ts` 的能力,但 Router/报价/授权逻辑走 DEX。
|
|
5
|
+
* - 捆绑买卖(买入 ->(可选授权)-> 卖出 ->(可选归集))
|
|
6
|
+
* - 自动归集 OKB 到 owner
|
|
7
|
+
* - ✅ 支持“刮取利润”:在归集时从可转出金额中按 bps 拆分转到 PROFIT_CONFIG.RECIPIENT
|
|
8
|
+
*/
|
|
9
|
+
import { Wallet, Contract, Interface, ethers } from 'ethers';
|
|
10
|
+
import { ENTRYPOINT_ABI, DEFAULT_CALL_GAS_LIMIT_SELL, DEFAULT_WITHDRAW_RESERVE, POTATOSWAP_V2_ROUTER, WOKB, } from './constants.js';
|
|
11
|
+
import { AAAccountManager, encodeExecute, encodeExecuteBatch } from './aa-account.js';
|
|
12
|
+
import { DexQuery, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee } from './dex.js';
|
|
13
|
+
import { PortalQuery, encodeApproveCall, parseOkb, formatOkb } from './portal-ops.js';
|
|
14
|
+
import { mapWithConcurrency } from '../utils/concurrency.js';
|
|
15
|
+
import { PROFIT_CONFIG } from '../utils/constants.js';
|
|
16
|
+
import { AANonceMap } from './types.js';
|
|
17
|
+
function getDexDeadline(minutes = 20) {
|
|
18
|
+
return Math.floor(Date.now() / 1000) + minutes * 60;
|
|
19
|
+
}
|
|
20
|
+
function resolveProfitSettings(config) {
|
|
21
|
+
const extractProfit = config?.extractProfit !== false; // 默认 true
|
|
22
|
+
const profitBpsRaw = config?.profitBps ?? PROFIT_CONFIG.RATE_BPS_SWAP;
|
|
23
|
+
const profitBps = Math.max(0, Math.min(10000, Number(profitBpsRaw)));
|
|
24
|
+
const profitRecipient = config?.profitRecipient ?? PROFIT_CONFIG.RECIPIENT;
|
|
25
|
+
return { extractProfit, profitBps, profitRecipient };
|
|
26
|
+
}
|
|
27
|
+
function calculateProfitWei(amountWei, profitBps) {
|
|
28
|
+
if (amountWei <= 0n)
|
|
29
|
+
return 0n;
|
|
30
|
+
if (!Number.isFinite(profitBps) || profitBps <= 0)
|
|
31
|
+
return 0n;
|
|
32
|
+
return (amountWei * BigInt(profitBps)) / 10000n;
|
|
33
|
+
}
|
|
34
|
+
// 固定 gas(用于大规模减少 RPC);具体值允许通过 config.fixedGas 覆盖
|
|
35
|
+
const DEFAULT_CALL_GAS_LIMIT_BUY = DEFAULT_CALL_GAS_LIMIT_SELL;
|
|
36
|
+
const DEFAULT_CALL_GAS_LIMIT_APPROVE = 200000n;
|
|
37
|
+
const DEFAULT_CALL_GAS_LIMIT_WITHDRAW = 120000n;
|
|
38
|
+
/**
|
|
39
|
+
* 外盘捆绑交易执行器
|
|
40
|
+
*/
|
|
41
|
+
export class DexBundleExecutor {
|
|
42
|
+
constructor(config = {}) {
|
|
43
|
+
this.config = config;
|
|
44
|
+
this.aaManager = new AAAccountManager(config);
|
|
45
|
+
this.portalQuery = new PortalQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId });
|
|
46
|
+
this.dexQuery = new DexQuery({ rpcUrl: config.rpcUrl, chainId: config.chainId, routerAddress: config.routerAddress });
|
|
47
|
+
}
|
|
48
|
+
getEffectiveRouter(params) {
|
|
49
|
+
if (params.routerAddress)
|
|
50
|
+
return params.routerAddress;
|
|
51
|
+
if (params.dexKey === 'DYORSWAP') {
|
|
52
|
+
return '0xfb001fbbace32f09cb6d3c449b935183de53ee96';
|
|
53
|
+
}
|
|
54
|
+
return POTATOSWAP_V2_ROUTER;
|
|
55
|
+
}
|
|
56
|
+
getDeadlineMinutes(params) {
|
|
57
|
+
return params.deadlineMinutes ?? this.config.deadlineMinutes ?? 20;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 执行 handleOps 并解析结果
|
|
61
|
+
*/
|
|
62
|
+
async runHandleOps(label, ops, bundlerSigner, beneficiary) {
|
|
63
|
+
if (ops.length === 0) {
|
|
64
|
+
console.log(`\n[${label}] 没有 ops,跳过`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const provider = this.aaManager.getProvider();
|
|
68
|
+
const entryPointAddress = this.aaManager.getEntryPointAddress();
|
|
69
|
+
const feeData = await provider.getFeeData();
|
|
70
|
+
console.log(`\n[${label}] 发送 handleOps,ops=${ops.length} ...`);
|
|
71
|
+
const entryPointWithSigner = new Contract(entryPointAddress, ENTRYPOINT_ABI, bundlerSigner);
|
|
72
|
+
const tx = await entryPointWithSigner.handleOps(ops, beneficiary, { gasPrice: feeData.gasPrice ?? 100000000n });
|
|
73
|
+
console.log(`[${label}] txHash:`, tx.hash);
|
|
74
|
+
const receipt = await tx.wait();
|
|
75
|
+
console.log(`[${label}] mined block=${receipt.blockNumber} status=${receipt.status}`);
|
|
76
|
+
const epIface = new Interface(ENTRYPOINT_ABI);
|
|
77
|
+
const userOpEvents = [];
|
|
78
|
+
for (const log of receipt.logs) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = epIface.parseLog(log);
|
|
81
|
+
if (parsed?.name === 'UserOperationEvent') {
|
|
82
|
+
const e = parsed.args;
|
|
83
|
+
userOpEvents.push({
|
|
84
|
+
userOpHash: e.userOpHash,
|
|
85
|
+
sender: e.sender,
|
|
86
|
+
paymaster: e.paymaster,
|
|
87
|
+
success: e.success,
|
|
88
|
+
actualGasCost: e.actualGasCost,
|
|
89
|
+
actualGasUsed: e.actualGasUsed,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch { }
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
txHash: tx.hash,
|
|
97
|
+
blockNumber: receipt.blockNumber,
|
|
98
|
+
status: receipt.status,
|
|
99
|
+
userOpEvents,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async buildWithdrawUserOpWithState(params) {
|
|
103
|
+
const senderBalance = params.senderBalance;
|
|
104
|
+
if (senderBalance <= params.reserveWei) {
|
|
105
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] sender OKB 太少,跳过归集:${formatOkb(senderBalance)} OKB`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// 先估算 prefund(使用空调用)
|
|
109
|
+
const tempCallData = encodeExecute(params.ownerWallet.address, 0n, '0x');
|
|
110
|
+
if (!params.signOnly) {
|
|
111
|
+
await this.aaManager.ensureSenderBalance(params.ownerWallet, params.sender, parseOkb('0.0002'), `${params.ownerName ?? 'owner'}/withdraw-prefund`);
|
|
112
|
+
}
|
|
113
|
+
const effConfig = { ...(this.config ?? {}), ...(params.configOverride ?? {}) };
|
|
114
|
+
const gasPolicyRaw = effConfig.gasPolicy ?? 'bundlerEstimate';
|
|
115
|
+
const gasPolicy = params.signOnly && gasPolicyRaw === 'bundlerEstimate' ? 'fixed' : gasPolicyRaw;
|
|
116
|
+
const { prefundWei } = gasPolicy === 'fixed'
|
|
117
|
+
? await this.aaManager.buildUserOpWithFixedGas({
|
|
118
|
+
ownerWallet: params.ownerWallet,
|
|
119
|
+
sender: params.sender,
|
|
120
|
+
callData: tempCallData,
|
|
121
|
+
nonce: params.nonce,
|
|
122
|
+
initCode: params.initCode,
|
|
123
|
+
deployed: params.initCode === '0x',
|
|
124
|
+
fixedGas: {
|
|
125
|
+
...(effConfig.fixedGas ?? {}),
|
|
126
|
+
callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
: gasPolicy === 'localEstimate'
|
|
130
|
+
? await this.aaManager.buildUserOpWithLocalEstimate({
|
|
131
|
+
ownerWallet: params.ownerWallet,
|
|
132
|
+
sender: params.sender,
|
|
133
|
+
callData: tempCallData,
|
|
134
|
+
nonce: params.nonce,
|
|
135
|
+
initCode: params.initCode,
|
|
136
|
+
})
|
|
137
|
+
: await this.aaManager.buildUserOpWithBundlerEstimate({
|
|
138
|
+
ownerWallet: params.ownerWallet,
|
|
139
|
+
sender: params.sender,
|
|
140
|
+
callData: tempCallData,
|
|
141
|
+
nonce: params.nonce,
|
|
142
|
+
initCode: params.initCode,
|
|
143
|
+
});
|
|
144
|
+
const withdrawAmount = senderBalance > prefundWei + params.reserveWei
|
|
145
|
+
? senderBalance - prefundWei - params.reserveWei
|
|
146
|
+
: 0n;
|
|
147
|
+
if (withdrawAmount <= 0n) {
|
|
148
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] 归集后可转出=0(余额不足以覆盖 prefund+reserve)`);
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const { extractProfit, profitBps, profitRecipient } = resolveProfitSettings(effConfig);
|
|
152
|
+
const profitWei = extractProfit ? calculateProfitWei(withdrawAmount, profitBps) : 0n;
|
|
153
|
+
const toOwnerWei = withdrawAmount - profitWei;
|
|
154
|
+
const callData = extractProfit && profitWei > 0n
|
|
155
|
+
? encodeExecuteBatch([profitRecipient, params.ownerWallet.address], [profitWei, toOwnerWei], ['0x', '0x'])
|
|
156
|
+
: encodeExecute(params.ownerWallet.address, withdrawAmount, '0x');
|
|
157
|
+
const { userOp } = gasPolicy === 'fixed'
|
|
158
|
+
? await this.aaManager.buildUserOpWithFixedGas({
|
|
159
|
+
ownerWallet: params.ownerWallet,
|
|
160
|
+
sender: params.sender,
|
|
161
|
+
callData,
|
|
162
|
+
nonce: params.nonce,
|
|
163
|
+
initCode: params.initCode,
|
|
164
|
+
deployed: params.initCode === '0x',
|
|
165
|
+
fixedGas: {
|
|
166
|
+
...(effConfig.fixedGas ?? {}),
|
|
167
|
+
callGasLimit: effConfig.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_WITHDRAW,
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
: gasPolicy === 'localEstimate'
|
|
171
|
+
? await this.aaManager.buildUserOpWithLocalEstimate({
|
|
172
|
+
ownerWallet: params.ownerWallet,
|
|
173
|
+
sender: params.sender,
|
|
174
|
+
callData,
|
|
175
|
+
nonce: params.nonce,
|
|
176
|
+
initCode: params.initCode,
|
|
177
|
+
})
|
|
178
|
+
: await this.aaManager.buildUserOpWithBundlerEstimate({
|
|
179
|
+
ownerWallet: params.ownerWallet,
|
|
180
|
+
sender: params.sender,
|
|
181
|
+
callData,
|
|
182
|
+
nonce: params.nonce,
|
|
183
|
+
initCode: params.initCode,
|
|
184
|
+
});
|
|
185
|
+
if (extractProfit && profitWei > 0n) {
|
|
186
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(toOwnerWei)} OKB (profit: ${formatOkb(profitWei)} OKB -> ${profitRecipient})`);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.log(`\n[${params.ownerName ?? 'owner'}] withdraw: ${formatOkb(withdrawAmount)} OKB`);
|
|
190
|
+
}
|
|
191
|
+
return { userOp, prefundWei };
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 外盘捆绑买卖(先买后卖)
|
|
195
|
+
*/
|
|
196
|
+
async bundleBuySell(params) {
|
|
197
|
+
const { tokenAddress, privateKeys, buyAmounts, sellPercent = 100, withdrawToOwner = true, withdrawReserve = DEFAULT_WITHDRAW_RESERVE, config, dexKey, routerAddress, deadlineMinutes, } = params;
|
|
198
|
+
if (privateKeys.length !== buyAmounts.length) {
|
|
199
|
+
throw new Error('私钥数量和购买金额数量必须一致');
|
|
200
|
+
}
|
|
201
|
+
const effectiveRouter = this.getEffectiveRouter({ dexKey, routerAddress });
|
|
202
|
+
const deadline = getDexDeadline(this.getDeadlineMinutes({ deadlineMinutes }));
|
|
203
|
+
const sharedProvider = this.aaManager.getProvider();
|
|
204
|
+
const wallets = privateKeys.map((pk) => new Wallet(pk, sharedProvider));
|
|
205
|
+
const bundlerSigner = wallets[0];
|
|
206
|
+
const beneficiary = bundlerSigner.address;
|
|
207
|
+
const reserveWei = parseOkb(withdrawReserve);
|
|
208
|
+
console.log('=== XLayer DEX Bundle Buy -> Sell ===');
|
|
209
|
+
console.log('router:', effectiveRouter);
|
|
210
|
+
console.log('token:', tokenAddress);
|
|
211
|
+
console.log('owners:', wallets.length);
|
|
212
|
+
console.log('sellPercent:', sellPercent);
|
|
213
|
+
const effCfg = { ...(this.config ?? {}), ...(config ?? {}) };
|
|
214
|
+
const profitSettings = resolveProfitSettings(effCfg);
|
|
215
|
+
// 1) accountInfos / nonceMap
|
|
216
|
+
const accountInfos = await this.aaManager.getMultipleAccountInfo(wallets.map((w) => w.address));
|
|
217
|
+
const senders = accountInfos.map((ai) => ai.sender);
|
|
218
|
+
const nonceMap = new AANonceMap();
|
|
219
|
+
for (const ai of accountInfos)
|
|
220
|
+
nonceMap.init(ai.sender, ai.nonce);
|
|
221
|
+
// 2) 买入(批量估算 + 补余额 + 并发签名)
|
|
222
|
+
const buyWeis = buyAmounts.map((a) => parseOkb(a));
|
|
223
|
+
await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
|
|
224
|
+
const buyWei = buyWeis[i] ?? 0n;
|
|
225
|
+
if (buyWei <= 0n)
|
|
226
|
+
return;
|
|
227
|
+
await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWei + parseOkb('0.0003'), `owner${i + 1}/dex-buy-prefund-before-estimate`);
|
|
228
|
+
});
|
|
229
|
+
const buyCallDatas = buyWeis.map((buyWei, i) => {
|
|
230
|
+
const sender = senders[i];
|
|
231
|
+
const swapData = encodeSwapExactETHForTokensSupportingFee(0n, [WOKB, tokenAddress], sender, deadline);
|
|
232
|
+
return encodeExecute(effectiveRouter, buyWei, swapData);
|
|
233
|
+
});
|
|
234
|
+
const initCodes = accountInfos.map((ai, i) => (ai.deployed ? '0x' : this.aaManager.generateInitCode(wallets[i].address)));
|
|
235
|
+
const { userOps: buyUserOps, prefundWeis } = await this.aaManager.buildUserOpsWithBundlerEstimateBatch({
|
|
236
|
+
ops: accountInfos.map((ai, i) => ({
|
|
237
|
+
sender: ai.sender,
|
|
238
|
+
nonce: nonceMap.next(ai.sender),
|
|
239
|
+
callData: buyCallDatas[i],
|
|
240
|
+
initCode: initCodes[i],
|
|
241
|
+
})),
|
|
242
|
+
});
|
|
243
|
+
await mapWithConcurrency(accountInfos, 6, async (ai, i) => {
|
|
244
|
+
const buyWei = buyWeis[i] ?? 0n;
|
|
245
|
+
await this.aaManager.ensureSenderBalance(wallets[i], ai.sender, buyWei + (prefundWeis[i] ?? 0n) + parseOkb('0.0002'), `owner${i + 1}/dex-buy-fund`);
|
|
246
|
+
});
|
|
247
|
+
const signedBuy = await mapWithConcurrency(buyUserOps, 10, async (op, i) => this.aaManager.signUserOp(op, wallets[i]));
|
|
248
|
+
const buyOps = signedBuy.map((s) => s.userOp);
|
|
249
|
+
const buyResult = await this.runHandleOps('dex-buyBundle', buyOps, bundlerSigner, beneficiary);
|
|
250
|
+
if (!buyResult) {
|
|
251
|
+
throw new Error('买入交易失败');
|
|
252
|
+
}
|
|
253
|
+
// 3) 授权 + 卖出(同一笔 handleOps)
|
|
254
|
+
const tokenBalances = await this.portalQuery.getMultipleTokenBalances(tokenAddress, senders);
|
|
255
|
+
const allowances = await this.portalQuery.getMultipleAllowances(tokenAddress, senders, effectiveRouter);
|
|
256
|
+
const sellOps = [];
|
|
257
|
+
const sellPerWallet = await mapWithConcurrency(wallets, 4, async (w, i) => {
|
|
258
|
+
const sender = senders[i];
|
|
259
|
+
const balance = tokenBalances.get(sender) ?? 0n;
|
|
260
|
+
if (balance === 0n) {
|
|
261
|
+
console.log(`[owner${i + 1}] 没买到代币,跳过卖出`);
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
const allowance = allowances.get(sender) ?? 0n;
|
|
265
|
+
const needApprove = allowance < balance;
|
|
266
|
+
const out = [];
|
|
267
|
+
// buy 已经用过一次,因此 initCode = 0x(且 nonce 已更新过)
|
|
268
|
+
const initCode = '0x';
|
|
269
|
+
if (needApprove) {
|
|
270
|
+
const approveNonce = nonceMap.next(sender);
|
|
271
|
+
const approveCallData = encodeExecute(tokenAddress, 0n, encodeApproveCall(effectiveRouter, ethers.MaxUint256));
|
|
272
|
+
const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
|
|
273
|
+
ownerWallet: w,
|
|
274
|
+
sender,
|
|
275
|
+
nonce: approveNonce,
|
|
276
|
+
initCode,
|
|
277
|
+
callData: approveCallData,
|
|
278
|
+
deployed: true,
|
|
279
|
+
fixedGas: {
|
|
280
|
+
...(effCfg.fixedGas ?? {}),
|
|
281
|
+
callGasLimit: effCfg.fixedGas?.callGasLimit ?? DEFAULT_CALL_GAS_LIMIT_APPROVE,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
const signed = await this.aaManager.signUserOp(userOp, w);
|
|
285
|
+
out.push(signed.userOp);
|
|
286
|
+
}
|
|
287
|
+
const sellAmount = (balance * BigInt(sellPercent)) / 100n;
|
|
288
|
+
if (sellAmount === 0n)
|
|
289
|
+
return out;
|
|
290
|
+
const sellNonce = nonceMap.next(sender);
|
|
291
|
+
const sellSwapData = encodeSwapExactTokensForETHSupportingFee(sellAmount, 0n, [tokenAddress, WOKB], sender, deadline);
|
|
292
|
+
const sellCallData = encodeExecute(effectiveRouter, 0n, sellSwapData);
|
|
293
|
+
const signedSell = await this.aaManager.buildUserOpWithState({
|
|
294
|
+
ownerWallet: w,
|
|
295
|
+
sender,
|
|
296
|
+
nonce: sellNonce,
|
|
297
|
+
initCode,
|
|
298
|
+
callData: sellCallData,
|
|
299
|
+
signOnly: false,
|
|
300
|
+
});
|
|
301
|
+
out.push(signedSell.userOp);
|
|
302
|
+
return out;
|
|
303
|
+
});
|
|
304
|
+
for (const ops of sellPerWallet)
|
|
305
|
+
sellOps.push(...ops);
|
|
306
|
+
const sellResult = await this.runHandleOps('dex-sellBundle', sellOps, bundlerSigner, beneficiary);
|
|
307
|
+
if (!sellResult) {
|
|
308
|
+
throw new Error('卖出交易失败');
|
|
309
|
+
}
|
|
310
|
+
// 4) 归集 OKB(sell 后状态)
|
|
311
|
+
let withdrawResult;
|
|
312
|
+
let totalProfitWei = 0n;
|
|
313
|
+
if (withdrawToOwner) {
|
|
314
|
+
const withdrawOps = [];
|
|
315
|
+
const okbBalances = await this.portalQuery.getMultipleOkbBalances(senders);
|
|
316
|
+
const withdrawItems = wallets.map((w, i) => ({
|
|
317
|
+
i,
|
|
318
|
+
ownerWallet: w,
|
|
319
|
+
sender: senders[i],
|
|
320
|
+
senderBalance: okbBalances.get(senders[i]) ?? 0n,
|
|
321
|
+
nonce: nonceMap.peek(senders[i]),
|
|
322
|
+
initCode: '0x',
|
|
323
|
+
}));
|
|
324
|
+
const signedWithdraws = await mapWithConcurrency(withdrawItems, 3, async (it) => {
|
|
325
|
+
const built = await this.buildWithdrawUserOpWithState({
|
|
326
|
+
ownerWallet: it.ownerWallet,
|
|
327
|
+
sender: it.sender,
|
|
328
|
+
nonce: it.nonce,
|
|
329
|
+
initCode: it.initCode,
|
|
330
|
+
senderBalance: it.senderBalance,
|
|
331
|
+
reserveWei,
|
|
332
|
+
ownerName: `owner${it.i + 1}`,
|
|
333
|
+
configOverride: config,
|
|
334
|
+
});
|
|
335
|
+
if (built) {
|
|
336
|
+
nonceMap.commit(it.sender, it.nonce);
|
|
337
|
+
withdrawOps.push(built.userOp);
|
|
338
|
+
if (profitSettings.extractProfit) {
|
|
339
|
+
const withdrawAmount = it.senderBalance > built.prefundWei + reserveWei
|
|
340
|
+
? it.senderBalance - built.prefundWei - reserveWei
|
|
341
|
+
: 0n;
|
|
342
|
+
totalProfitWei += calculateProfitWei(withdrawAmount, profitSettings.profitBps);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
void signedWithdraws; // silence unused (build is applied in loop)
|
|
347
|
+
if (withdrawOps.length > 0) {
|
|
348
|
+
withdrawResult = await this.runHandleOps('dex-withdrawBundle', withdrawOps, bundlerSigner, beneficiary) ?? undefined;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const finalBalances = await this.portalQuery.getMultipleOkbBalances(senders);
|
|
352
|
+
return {
|
|
353
|
+
buyResult,
|
|
354
|
+
sellResult,
|
|
355
|
+
withdrawResult,
|
|
356
|
+
finalBalances,
|
|
357
|
+
profit: withdrawToOwner
|
|
358
|
+
? {
|
|
359
|
+
extractProfit: profitSettings.extractProfit,
|
|
360
|
+
profitBps: profitSettings.profitBps,
|
|
361
|
+
profitRecipient: profitSettings.profitRecipient,
|
|
362
|
+
totalProfitWei: totalProfitWei.toString(),
|
|
363
|
+
}
|
|
364
|
+
: undefined,
|
|
365
|
+
// approveResult: 这里我们把 approve 合并进 sellBundle 了,不单独返回
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// ============================================================================
|
|
370
|
+
// 便捷函数
|
|
371
|
+
// ============================================================================
|
|
372
|
+
export function createDexBundleExecutor(config) {
|
|
373
|
+
return new DexBundleExecutor(config);
|
|
374
|
+
}
|
|
375
|
+
export async function dexBundleBuySell(params) {
|
|
376
|
+
const executor = createDexBundleExecutor(params.config);
|
|
377
|
+
return executor.bundleBuySell(params);
|
|
378
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer 外盘刷量/做市 SDK(AA / ERC-4337)
|
|
3
|
+
*
|
|
4
|
+
* - 与 `src/xlayer/volume.ts`(内盘)分离
|
|
5
|
+
* - 底层使用 `DexBundleExecutor.bundleBuySell`(买入 -> 卖出 -> 归集 -> 刮利润)
|
|
6
|
+
*/
|
|
7
|
+
import type { XLayerConfig, VolumeResult } from './types.js';
|
|
8
|
+
export interface DexVolumeParams {
|
|
9
|
+
tokenAddress: string;
|
|
10
|
+
privateKeys: string[];
|
|
11
|
+
buyAmountPerRound: string;
|
|
12
|
+
rounds: number;
|
|
13
|
+
intervalMs?: number;
|
|
14
|
+
sellImmediately?: boolean;
|
|
15
|
+
sellPercent?: number;
|
|
16
|
+
withdrawToOwner?: boolean;
|
|
17
|
+
withdrawReserve?: string;
|
|
18
|
+
dexKey?: string;
|
|
19
|
+
routerAddress?: string;
|
|
20
|
+
deadlineMinutes?: number;
|
|
21
|
+
config?: Partial<XLayerConfig>;
|
|
22
|
+
}
|
|
23
|
+
export declare class DexVolumeExecutor {
|
|
24
|
+
private dexBundleExecutor;
|
|
25
|
+
private config;
|
|
26
|
+
constructor(config?: XLayerConfig);
|
|
27
|
+
execute(params: DexVolumeParams): Promise<VolumeResult>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createDexVolumeExecutor(config?: XLayerConfig): DexVolumeExecutor;
|
|
30
|
+
export declare function makeDexVolume(params: DexVolumeParams): Promise<VolumeResult>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XLayer 外盘刷量/做市 SDK(AA / ERC-4337)
|
|
3
|
+
*
|
|
4
|
+
* - 与 `src/xlayer/volume.ts`(内盘)分离
|
|
5
|
+
* - 底层使用 `DexBundleExecutor.bundleBuySell`(买入 -> 卖出 -> 归集 -> 刮利润)
|
|
6
|
+
*/
|
|
7
|
+
import { parseOkb, formatOkb } from './portal-ops.js';
|
|
8
|
+
import { DexBundleExecutor } from './dex-bundle.js';
|
|
9
|
+
function sleep(ms) {
|
|
10
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
|
+
}
|
|
12
|
+
export class DexVolumeExecutor {
|
|
13
|
+
constructor(config = {}) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
this.dexBundleExecutor = new DexBundleExecutor(config);
|
|
16
|
+
}
|
|
17
|
+
async execute(params) {
|
|
18
|
+
const { tokenAddress, privateKeys, buyAmountPerRound, rounds, intervalMs = 3000, sellImmediately = true, sellPercent, withdrawToOwner = true, withdrawReserve, dexKey, routerAddress, deadlineMinutes, config, } = params;
|
|
19
|
+
console.log('=== XLayer DEX Volume Maker ===');
|
|
20
|
+
console.log('token:', tokenAddress);
|
|
21
|
+
console.log('owners:', privateKeys.length);
|
|
22
|
+
console.log('rounds:', rounds);
|
|
23
|
+
console.log('buyAmountPerRound:', buyAmountPerRound, 'OKB');
|
|
24
|
+
console.log('intervalMs:', intervalMs);
|
|
25
|
+
console.log('sellImmediately:', sellImmediately);
|
|
26
|
+
const roundResults = [];
|
|
27
|
+
let successRounds = 0;
|
|
28
|
+
let failedRounds = 0;
|
|
29
|
+
let totalVolume = 0n;
|
|
30
|
+
const buyWei = parseOkb(buyAmountPerRound);
|
|
31
|
+
for (let round = 0; round < rounds; round++) {
|
|
32
|
+
console.log(`\n========== DEX Round ${round + 1}/${rounds} ==========`);
|
|
33
|
+
try {
|
|
34
|
+
const buyAmounts = privateKeys.map(() => buyAmountPerRound);
|
|
35
|
+
const effSellPercent = typeof sellPercent === 'number' ? sellPercent : (sellImmediately ? 100 : 0);
|
|
36
|
+
const result = await this.dexBundleExecutor.bundleBuySell({
|
|
37
|
+
tokenAddress,
|
|
38
|
+
privateKeys,
|
|
39
|
+
buyAmounts,
|
|
40
|
+
sellPercent: effSellPercent,
|
|
41
|
+
withdrawToOwner,
|
|
42
|
+
withdrawReserve,
|
|
43
|
+
dexKey,
|
|
44
|
+
routerAddress,
|
|
45
|
+
deadlineMinutes,
|
|
46
|
+
config: { ...(this.config ?? {}), ...(config ?? {}) },
|
|
47
|
+
});
|
|
48
|
+
roundResults.push(result);
|
|
49
|
+
successRounds++;
|
|
50
|
+
totalVolume += buyWei * BigInt(privateKeys.length);
|
|
51
|
+
console.log(`DEX Round ${round + 1} 完成`);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
console.error(`DEX Round ${round + 1} 失败:`, error);
|
|
55
|
+
failedRounds++;
|
|
56
|
+
}
|
|
57
|
+
if (round < rounds - 1 && intervalMs > 0) {
|
|
58
|
+
console.log(`等待 ${intervalMs}ms...`);
|
|
59
|
+
await sleep(intervalMs);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
console.log('\n=== DEX Volume Summary ===');
|
|
63
|
+
console.log('成功轮数:', successRounds);
|
|
64
|
+
console.log('失败轮数:', failedRounds);
|
|
65
|
+
console.log('总交易量:', formatOkb(totalVolume), 'OKB');
|
|
66
|
+
return {
|
|
67
|
+
successRounds,
|
|
68
|
+
failedRounds,
|
|
69
|
+
roundResults,
|
|
70
|
+
totalVolume,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function createDexVolumeExecutor(config) {
|
|
75
|
+
return new DexVolumeExecutor(config);
|
|
76
|
+
}
|
|
77
|
+
export async function makeDexVolume(params) {
|
|
78
|
+
const executor = createDexVolumeExecutor(params.config);
|
|
79
|
+
return executor.execute(params);
|
|
80
|
+
}
|
package/dist/xlayer/index.d.ts
CHANGED
|
@@ -63,6 +63,7 @@ export { BundlerClient, createBundlerClient, type BundlerConfig, type BundlerRec
|
|
|
63
63
|
export { AAAccountManager, createAAAccountManager, predictSender, createWallet, encodeExecute, encodeExecuteBatch, generateAAWallets, generateAAWalletsFromMnemonic, predictSendersFromPrivateKeys, type GeneratedAAWallet, type GenerateAAWalletsParams, type GenerateAAWalletsResult, } from './aa-account.js';
|
|
64
64
|
export { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, PortalQuery, createPortalQuery, applySlippage, formatOkb, parseOkb, formatTokenAmount, parseTokenAmount, type PortalQueryConfig, } from './portal-ops.js';
|
|
65
65
|
export { BundleExecutor, createBundleExecutor, bundleBuy, bundleSell, bundleBuySell, } from './bundle.js';
|
|
66
|
+
export { DexBundleExecutor, createDexBundleExecutor, dexBundleBuySell, type DexBundleConfig, type DexBundleBuySellParams, } from './dex-bundle.js';
|
|
66
67
|
export { AAPortalSwapExecutor, } from './portal-bundle-swap.js';
|
|
67
68
|
export { AADexSwapExecutor, } from './dex-bundle-swap.js';
|
|
68
69
|
/**
|
|
@@ -78,6 +79,7 @@ export declare function bundleBatchSwapSign(params: BundleBatchSwapSignParams &
|
|
|
78
79
|
skipApprovalCheck?: boolean;
|
|
79
80
|
}): Promise<BundleBatchSwapSignResult>;
|
|
80
81
|
export { VolumeExecutor, createVolumeExecutor, makeVolume, singleRoundVolume, } from './volume.js';
|
|
82
|
+
export { DexVolumeExecutor, createDexVolumeExecutor, makeDexVolume, type DexVolumeParams, } from './dex-volume.js';
|
|
81
83
|
export { DexQuery, DexExecutor, createDexQuery, createDexExecutor, quoteOkbToToken, quoteTokenToOkb, encodeSwapExactETHForTokens, encodeSwapExactTokensForETH, encodeSwapExactTokensForTokens, encodeSwapExactETHForTokensSupportingFee, encodeSwapExactTokensForETHSupportingFee, encodeSwapExactTokensForTokensSupportingFee, type DexConfig, } from './dex.js';
|
|
82
84
|
export declare const xlayer: {
|
|
83
85
|
bundleBuy: (params: import("./types.js").BundleBuyParams) => Promise<import("./types.js").BundleBuyResult>;
|
package/dist/xlayer/index.js
CHANGED
|
@@ -83,6 +83,10 @@ export { encodeBuyCall, encodeSellCall, encodeApproveCall, encodeTransferCall, P
|
|
|
83
83
|
// ============================================================================
|
|
84
84
|
export { BundleExecutor, createBundleExecutor, bundleBuy, bundleSell, bundleBuySell, } from './bundle.js';
|
|
85
85
|
// ============================================================================
|
|
86
|
+
// 外盘捆绑交易(DEX Bundle)
|
|
87
|
+
// ============================================================================
|
|
88
|
+
export { DexBundleExecutor, createDexBundleExecutor, dexBundleBuySell, } from './dex-bundle.js';
|
|
89
|
+
// ============================================================================
|
|
86
90
|
// 捆绑换手 (AASwap)
|
|
87
91
|
// ============================================================================
|
|
88
92
|
export { AAPortalSwapExecutor, } from './portal-bundle-swap.js';
|
|
@@ -116,6 +120,10 @@ export async function bundleBatchSwapSign(params) {
|
|
|
116
120
|
// ============================================================================
|
|
117
121
|
export { VolumeExecutor, createVolumeExecutor, makeVolume, singleRoundVolume, } from './volume.js';
|
|
118
122
|
// ============================================================================
|
|
123
|
+
// 外盘刷量/做市(DEX Volume)
|
|
124
|
+
// ============================================================================
|
|
125
|
+
export { DexVolumeExecutor, createDexVolumeExecutor, makeDexVolume, } from './dex-volume.js';
|
|
126
|
+
// ============================================================================
|
|
119
127
|
// 外盘交易 (DEX)
|
|
120
128
|
// ============================================================================
|
|
121
129
|
export { DexQuery, DexExecutor, createDexQuery, createDexExecutor, quoteOkbToToken, quoteTokenToOkb,
|