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.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/xlayer/aa-account.d.ts +21 -0
- package/dist/xlayer/aa-account.js +52 -0
- package/dist/xlayer/bundle.d.ts +1 -44
- package/dist/xlayer/bundle.js +1 -396
- package/dist/xlayer/dex-bundle-swap.d.ts +30 -0
- package/dist/xlayer/dex-bundle-swap.js +295 -0
- package/dist/xlayer/index.d.ts +22 -5
- package/dist/xlayer/index.js +33 -17
- package/dist/xlayer/portal-bundle-swap.d.ts +26 -0
- package/dist/xlayer/portal-bundle-swap.js +277 -0
- package/dist/xlayer/types.d.ts +31 -0
- package/dist/xlayer/types.js +50 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/xlayer/types.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/xlayer/types.js
CHANGED
|
@@ -3,4 +3,53 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 包含 ERC-4337 UserOperation、AA 账户、交易参数等核心类型
|
|
5
5
|
*/
|
|
6
|
-
|
|
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
|
+
}
|