four-flap-meme-sdk 1.5.28 → 1.5.29

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.
@@ -0,0 +1,277 @@
1
+ /**
2
+ * XLayer AA 内盘 (Flap Portal) 捆绑换手实现
3
+ */
4
+ import { Wallet, ethers } from 'ethers';
5
+ import { AANonceMap, } from './types.js';
6
+ import { FLAP_PORTAL, } from './constants.js';
7
+ import { AAAccountManager, encodeExecute } from './aa-account.js';
8
+ import { encodeBuyCall, encodeSellCall, encodeApproveCall, PortalQuery, parseOkb, } from './portal-ops.js';
9
+ import { BundleExecutor } from './bundle.js';
10
+ /**
11
+ * XLayer AA 内盘换手执行器
12
+ */
13
+ export class AAPortalSwapExecutor {
14
+ constructor(config = {}) {
15
+ this.config = config;
16
+ this.aaManager = new AAAccountManager(config);
17
+ this.portalQuery = new PortalQuery({
18
+ rpcUrl: config.rpcUrl,
19
+ chainId: config.chainId,
20
+ });
21
+ this.bundleExecutor = new BundleExecutor(config);
22
+ }
23
+ /**
24
+ * AA 内盘单钱包换手签名
25
+ */
26
+ async bundleSwapSign(params) {
27
+ const { tokenAddress, sellerPrivateKey, buyerPrivateKey, sellAmount, sellPercent = 100, buyAmountOkb, slippageBps = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
28
+ const provider = this.aaManager.getProvider();
29
+ const sellerOwner = new Wallet(sellerPrivateKey, provider);
30
+ const buyerOwner = new Wallet(buyerPrivateKey, provider);
31
+ const payerWallet = new Wallet(payerPrivateKey || sellerPrivateKey, provider);
32
+ const beneficiary = beneficiaryIn || payerWallet.address;
33
+ const [sellerAi, buyerAi] = await this.aaManager.getMultipleAccountInfo([sellerOwner.address, buyerOwner.address]);
34
+ const sellerSender = sellerAi.sender;
35
+ const buyerSender = buyerAi.sender;
36
+ const nonceMap = new AANonceMap();
37
+ nonceMap.init(sellerSender, sellerAi.nonce);
38
+ nonceMap.init(buyerSender, buyerAi.nonce);
39
+ // initCode:确保“同一 sender”只在第一笔 op 携带(对齐 BSC 的 nonce/顺序管理思想)
40
+ const initCodeBySenderLower = new Map();
41
+ const initIfNeeded = (sender, deployed, ownerAddress) => {
42
+ const k = sender.toLowerCase();
43
+ if (initCodeBySenderLower.has(k))
44
+ return;
45
+ initCodeBySenderLower.set(k, deployed ? '0x' : this.aaManager.generateInitCode(ownerAddress));
46
+ };
47
+ const consumeInitCode = (sender) => {
48
+ const k = sender.toLowerCase();
49
+ const cur = initCodeBySenderLower.get(k);
50
+ if (cur === undefined)
51
+ throw new Error(`initCode not initialized for sender: ${sender}`);
52
+ if (cur !== '0x')
53
+ initCodeBySenderLower.set(k, '0x');
54
+ return cur;
55
+ };
56
+ initIfNeeded(sellerSender, !!sellerAi?.deployed, sellerOwner.address);
57
+ initIfNeeded(buyerSender, !!buyerAi?.deployed, buyerOwner.address);
58
+ // 1. 计算卖出数量
59
+ const decimals = await this.bundleExecutor['getErc20Decimals'](tokenAddress);
60
+ const sellerTokenBal = await this.aaManager.getErc20Balance(tokenAddress, sellerSender);
61
+ const sellAmountWei = sellAmount
62
+ ? ethers.parseUnits(String(sellAmount), decimals)
63
+ : (sellerTokenBal * BigInt(sellPercent)) / 100n;
64
+ if (sellAmountWei <= 0n)
65
+ throw new Error('卖出数量无效');
66
+ // 2. 授权检查
67
+ let needApprove = false;
68
+ if (!skipApprovalCheck) {
69
+ const allowance = await this.aaManager.getErc20Allowance(tokenAddress, sellerSender, FLAP_PORTAL);
70
+ needApprove = allowance < sellAmountWei;
71
+ }
72
+ // 3. 报价买入 OKB
73
+ const quoted = await this.portalQuery.previewSell(tokenAddress, sellAmountWei);
74
+ const finalBuyAmountWei = buyAmountOkb
75
+ ? parseOkb(String(buyAmountOkb))
76
+ : (quoted * BigInt(10000 - slippageBps)) / 10000n;
77
+ // 4. 构建 Ops
78
+ const outOps = [];
79
+ if (needApprove) {
80
+ const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
81
+ ownerWallet: sellerOwner,
82
+ sender: sellerSender,
83
+ nonce: nonceMap.next(sellerSender),
84
+ initCode: consumeInitCode(sellerSender),
85
+ callData: encodeExecute(tokenAddress, 0n, encodeApproveCall(FLAP_PORTAL)),
86
+ deployed: sellerAi.deployed,
87
+ });
88
+ const signedApprove = await this.aaManager.signUserOp(userOp, sellerOwner);
89
+ outOps.push(signedApprove.userOp);
90
+ }
91
+ // Sell op
92
+ const sellSwapData = encodeSellCall(tokenAddress, sellAmountWei, 0n);
93
+ const sellCallData = encodeExecute(FLAP_PORTAL, 0n, sellSwapData);
94
+ const signedSell = await this.aaManager.buildUserOpWithState({
95
+ ownerWallet: sellerOwner,
96
+ sender: sellerSender,
97
+ nonce: nonceMap.next(sellerSender),
98
+ initCode: consumeInitCode(sellerSender),
99
+ callData: sellCallData,
100
+ signOnly: true,
101
+ });
102
+ outOps.push(signedSell.userOp);
103
+ // Buy op
104
+ const buySwapData = encodeBuyCall(tokenAddress, finalBuyAmountWei, 0n);
105
+ const buyCallData = encodeExecute(FLAP_PORTAL, finalBuyAmountWei, buySwapData);
106
+ const signedBuy = await this.aaManager.buildUserOpWithState({
107
+ ownerWallet: buyerOwner,
108
+ sender: buyerSender,
109
+ nonce: nonceMap.next(buyerSender),
110
+ initCode: consumeInitCode(buyerSender),
111
+ callData: buyCallData,
112
+ signOnly: true,
113
+ });
114
+ outOps.push(signedBuy.userOp);
115
+ // 5. 签名 HandleOps
116
+ const signedHandleOps = await this.bundleExecutor['signHandleOpsTx']({
117
+ ops: outOps,
118
+ payerWallet,
119
+ beneficiary,
120
+ nonce: payerStartNonce,
121
+ });
122
+ const signedTransactions = [signedHandleOps];
123
+ if (routeAddress) {
124
+ const tx = ethers.Transaction.from(signedHandleOps);
125
+ const tailTx = await payerWallet.signTransaction({
126
+ to: routeAddress,
127
+ value: 0n,
128
+ nonce: Number(tx.nonce) + 1,
129
+ gasLimit: 21000n,
130
+ gasPrice: tx.gasPrice,
131
+ chainId: tx.chainId,
132
+ type: 0,
133
+ });
134
+ signedTransactions.push(tailTx);
135
+ }
136
+ return {
137
+ signedTransactions,
138
+ metadata: {
139
+ payer: payerWallet.address,
140
+ beneficiary,
141
+ sellerOwner: sellerOwner.address,
142
+ sellerSender,
143
+ buyerOwner: buyerOwner.address,
144
+ buyerSender,
145
+ sellAmountWei: sellAmountWei.toString(),
146
+ buyAmountWei: finalBuyAmountWei.toString(),
147
+ hasApprove: needApprove,
148
+ routeAddress,
149
+ },
150
+ };
151
+ }
152
+ /**
153
+ * AA 内盘批量换手签名 (一卖多买)
154
+ */
155
+ async bundleBatchSwapSign(params) {
156
+ const { tokenAddress, sellerPrivateKey, buyerPrivateKeys, buyAmountsOkb, sellAmount, sellPercent = 100, payerPrivateKey, beneficiary: beneficiaryIn, payerStartNonce, routeAddress, skipApprovalCheck = false, } = params;
157
+ const provider = this.aaManager.getProvider();
158
+ const sellerOwner = new Wallet(sellerPrivateKey, provider);
159
+ const buyerOwners = buyerPrivateKeys.map(pk => new Wallet(pk, provider));
160
+ const payerWallet = new Wallet(payerPrivateKey || sellerPrivateKey, provider);
161
+ const beneficiary = beneficiaryIn || payerWallet.address;
162
+ const accountInfos = await this.aaManager.getMultipleAccountInfo([sellerOwner.address, ...buyerOwners.map(w => w.address)]);
163
+ const sellerAi = accountInfos[0];
164
+ const buyerAis = accountInfos.slice(1);
165
+ const nonceMap = new AANonceMap();
166
+ nonceMap.init(sellerAi.sender, sellerAi.nonce);
167
+ buyerAis.forEach(ai => nonceMap.init(ai.sender, ai.nonce));
168
+ // initCode:确保同一 sender 只在第一笔 op 携带
169
+ const initCodeBySenderLower = new Map();
170
+ const initIfNeeded = (sender, deployed, ownerAddress) => {
171
+ const k = sender.toLowerCase();
172
+ if (initCodeBySenderLower.has(k))
173
+ return;
174
+ initCodeBySenderLower.set(k, deployed ? '0x' : this.aaManager.generateInitCode(ownerAddress));
175
+ };
176
+ const consumeInitCode = (sender) => {
177
+ const k = sender.toLowerCase();
178
+ const cur = initCodeBySenderLower.get(k);
179
+ if (cur === undefined)
180
+ throw new Error(`initCode not initialized for sender: ${sender}`);
181
+ if (cur !== '0x')
182
+ initCodeBySenderLower.set(k, '0x');
183
+ return cur;
184
+ };
185
+ initIfNeeded(sellerAi.sender, sellerAi.deployed, sellerOwner.address);
186
+ for (let i = 0; i < buyerOwners.length; i++) {
187
+ initIfNeeded(buyerAis[i].sender, buyerAis[i].deployed, buyerOwners[i].address);
188
+ }
189
+ const decimals = await this.bundleExecutor['getErc20Decimals'](tokenAddress);
190
+ const sellerTokenBal = await this.aaManager.getErc20Balance(tokenAddress, sellerAi.sender);
191
+ const sellAmountWei = sellAmount
192
+ ? ethers.parseUnits(String(sellAmount), decimals)
193
+ : (sellerTokenBal * BigInt(sellPercent)) / 100n;
194
+ let needApprove = false;
195
+ if (!skipApprovalCheck) {
196
+ const allowance = await this.aaManager.getErc20Allowance(tokenAddress, sellerAi.sender, FLAP_PORTAL);
197
+ needApprove = allowance < sellAmountWei;
198
+ }
199
+ const outOps = [];
200
+ if (needApprove) {
201
+ const { userOp } = await this.aaManager.buildUserOpWithFixedGas({
202
+ ownerWallet: sellerOwner,
203
+ sender: sellerAi.sender,
204
+ nonce: nonceMap.next(sellerAi.sender),
205
+ initCode: consumeInitCode(sellerAi.sender),
206
+ callData: encodeExecute(tokenAddress, 0n, encodeApproveCall(FLAP_PORTAL)),
207
+ deployed: sellerAi.deployed,
208
+ });
209
+ const signedApprove = await this.aaManager.signUserOp(userOp, sellerOwner);
210
+ outOps.push(signedApprove.userOp);
211
+ }
212
+ // Sell op
213
+ const sellSwapData = encodeSellCall(tokenAddress, sellAmountWei, 0n);
214
+ const sellCallData = encodeExecute(FLAP_PORTAL, 0n, sellSwapData);
215
+ const signedSell = await this.aaManager.buildUserOpWithState({
216
+ ownerWallet: sellerOwner,
217
+ sender: sellerAi.sender,
218
+ nonce: nonceMap.next(sellerAi.sender),
219
+ initCode: consumeInitCode(sellerAi.sender),
220
+ callData: sellCallData,
221
+ signOnly: true,
222
+ });
223
+ outOps.push(signedSell.userOp);
224
+ // Batch Buy ops
225
+ const buyAmountsWei = buyAmountsOkb.map(a => parseOkb(a));
226
+ for (let i = 0; i < buyerOwners.length; i++) {
227
+ const ai = buyerAis[i];
228
+ const buyWei = buyAmountsWei[i];
229
+ const buySwapData = encodeBuyCall(tokenAddress, buyWei, 0n);
230
+ const buyCallData = encodeExecute(FLAP_PORTAL, buyWei, buySwapData);
231
+ const signedBuy = await this.aaManager.buildUserOpWithState({
232
+ ownerWallet: buyerOwners[i],
233
+ sender: ai.sender,
234
+ nonce: nonceMap.next(ai.sender),
235
+ initCode: consumeInitCode(ai.sender),
236
+ callData: buyCallData,
237
+ signOnly: true,
238
+ });
239
+ outOps.push(signedBuy.userOp);
240
+ }
241
+ const signedHandleOps = await this.bundleExecutor['signHandleOpsTx']({
242
+ ops: outOps,
243
+ payerWallet,
244
+ beneficiary,
245
+ nonce: payerStartNonce,
246
+ });
247
+ const signedTransactions = [signedHandleOps];
248
+ if (routeAddress) {
249
+ const tx = ethers.Transaction.from(signedHandleOps);
250
+ const tailTx = await payerWallet.signTransaction({
251
+ to: routeAddress,
252
+ value: 0n,
253
+ nonce: Number(tx.nonce) + 1,
254
+ gasLimit: 21000n,
255
+ gasPrice: tx.gasPrice,
256
+ chainId: tx.chainId,
257
+ type: 0,
258
+ });
259
+ signedTransactions.push(tailTx);
260
+ }
261
+ return {
262
+ signedTransactions,
263
+ metadata: {
264
+ payer: payerWallet.address,
265
+ beneficiary,
266
+ sellerOwner: sellerOwner.address,
267
+ sellerSender: sellerAi.sender,
268
+ buyerOwners: buyerOwners.map(w => w.address),
269
+ buyerSenders: buyerAis.map(ai => ai.sender),
270
+ sellAmountWei: sellAmountWei.toString(),
271
+ buyAmountsWei: buyAmountsWei.map(w => w.toString()),
272
+ hasApprove: needApprove,
273
+ routeAddress,
274
+ },
275
+ };
276
+ }
277
+ }
@@ -196,6 +196,12 @@ export interface BundleBuySellParams {
196
196
  * - 两个 UserOp(以及可选 approve)放入同一笔 handleOps
197
197
  */
198
198
  export interface BundleSwapParams {
199
+ /** 交易类型:FLAP (内盘), V2 (外盘 V2), V3 (外盘 V3) */
200
+ tradeType?: 'FLAP' | 'V2' | 'V3';
201
+ /** DEX 标识(用于选择正确的 Router) */
202
+ dexKey?: string;
203
+ /** 显式指定 Router 地址(若不传则由 SDK 根据 dexKey 自动决定) */
204
+ routerAddress?: string;
199
205
  /** 代币地址 */
200
206
  tokenAddress: string;
201
207
  /** 卖方 Owner 私钥 */
@@ -231,6 +237,7 @@ export interface BundleSwapResult {
231
237
  sellAmountWei: string;
232
238
  buyAmountWei: string;
233
239
  hasApprove: boolean;
240
+ routeAddress?: string;
234
241
  };
235
242
  }
236
243
  /**
@@ -276,6 +283,12 @@ export interface BundleSwapSignResult {
276
283
  * 批量捆绑换手参数(一卖多买)
277
284
  */
278
285
  export interface BundleBatchSwapParams {
286
+ /** 交易类型:FLAP (内盘), V2 (外盘 V2), V3 (外盘 V3) */
287
+ tradeType?: 'FLAP' | 'V2' | 'V3';
288
+ /** DEX 标识(用于选择正确的 Router) */
289
+ dexKey?: string;
290
+ /** 显式指定 Router 地址(若不传则由 SDK 根据 dexKey 自动决定) */
291
+ routerAddress?: string;
279
292
  /** 代币地址 */
280
293
  tokenAddress: string;
281
294
  /** 卖方 Owner 私钥 */
@@ -301,6 +314,7 @@ export interface BundleBatchSwapResult {
301
314
  sellAmountWei: string;
302
315
  buyAmountsWei: string[];
303
316
  hasApprove: boolean;
317
+ routeAddress?: string;
304
318
  };
305
319
  }
306
320
  export interface BundleBatchSwapSignParams extends BundleBatchSwapParams {
@@ -448,6 +462,23 @@ export interface DexSwapResult {
448
462
  export type DeepPartial<T> = {
449
463
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
450
464
  };
465
+ /**
466
+ * ✅ 仅用于 ERC-4337 AA(EntryPoint.getNonce(sender, 0))
467
+ *
468
+ * 目标:不要在业务代码里手写 `+1/+2` 推导,而是像 BSC bundle 一样用本地 Map
469
+ * 对“同一 sender 多步流程”连续分配 nonce。
470
+ *
471
+ * 注意:
472
+ * - 这是**同一次 SDK 调用/同一条流程**内的 nonce 分配;不解决多进程/多并发任务同时使用同一 sender 的问题。
473
+ * - 对“可能不生成 op”的分支(例如 withdraw 返回 null),使用 peek + commit,避免提前消耗 nonce。
474
+ */
475
+ export declare class AANonceMap {
476
+ private nextBySenderLower;
477
+ init(sender: string, startNonce: bigint): void;
478
+ peek(sender: string): bigint;
479
+ commit(sender: string, usedNonce: bigint): void;
480
+ next(sender: string): bigint;
481
+ }
451
482
  /**
452
483
  * 必需的配置字段
453
484
  */
@@ -3,4 +3,53 @@
3
3
  *
4
4
  * 包含 ERC-4337 UserOperation、AA 账户、交易参数等核心类型
5
5
  */
6
- export {};
6
+ // ==================== AA Nonce(EntryPoint nonce)本地分配器 ====================
7
+ /**
8
+ * ✅ 仅用于 ERC-4337 AA(EntryPoint.getNonce(sender, 0))
9
+ *
10
+ * 目标:不要在业务代码里手写 `+1/+2` 推导,而是像 BSC bundle 一样用本地 Map
11
+ * 对“同一 sender 多步流程”连续分配 nonce。
12
+ *
13
+ * 注意:
14
+ * - 这是**同一次 SDK 调用/同一条流程**内的 nonce 分配;不解决多进程/多并发任务同时使用同一 sender 的问题。
15
+ * - 对“可能不生成 op”的分支(例如 withdraw 返回 null),使用 peek + commit,避免提前消耗 nonce。
16
+ */
17
+ export class AANonceMap {
18
+ constructor() {
19
+ this.nextBySenderLower = new Map();
20
+ }
21
+ init(sender, startNonce) {
22
+ const k = sender.toLowerCase();
23
+ const cur = this.nextBySenderLower.get(k);
24
+ if (cur === undefined || startNonce > cur) {
25
+ this.nextBySenderLower.set(k, startNonce);
26
+ }
27
+ }
28
+ peek(sender) {
29
+ const k = sender.toLowerCase();
30
+ const cur = this.nextBySenderLower.get(k);
31
+ if (cur === undefined) {
32
+ throw new Error(`AANonceMap: sender not initialized: ${sender}`);
33
+ }
34
+ return cur;
35
+ }
36
+ commit(sender, usedNonce) {
37
+ const k = sender.toLowerCase();
38
+ const cur = this.nextBySenderLower.get(k);
39
+ if (cur === undefined) {
40
+ throw new Error(`AANonceMap: sender not initialized: ${sender}`);
41
+ }
42
+ // 只允许“使用当前 nonce”或“重复 commit”(幂等)
43
+ if (usedNonce !== cur && usedNonce !== cur - 1n) {
44
+ throw new Error(`AANonceMap: nonce mismatch for ${sender}: used=${usedNonce.toString()} cur=${cur.toString()}`);
45
+ }
46
+ if (usedNonce === cur) {
47
+ this.nextBySenderLower.set(k, cur + 1n);
48
+ }
49
+ }
50
+ next(sender) {
51
+ const n = this.peek(sender);
52
+ this.commit(sender, n);
53
+ return n;
54
+ }
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.5.28",
3
+ "version": "1.5.29",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",