four-flap-meme-sdk 1.6.25 → 1.6.28

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,16 @@
1
+ /**
2
+ * ECDH + AES-GCM 加密工具(浏览器兼容)
3
+ * 用于将签名交易用服务器公钥加密
4
+ */
5
+ /**
6
+ * 用服务器公钥加密签名交易(ECDH + AES-GCM)
7
+ *
8
+ * @param signedTransactions 签名后的交易数组
9
+ * @param publicKeyBase64 服务器提供的公钥(Base64 格式)
10
+ * @returns JSON 字符串 {e: 临时公钥, i: IV, d: 密文}
11
+ */
12
+ export declare function encryptWithPublicKey(signedTransactions: string[], publicKeyBase64: string): Promise<string>;
13
+ /**
14
+ * 验证公钥格式(Base64)
15
+ */
16
+ export declare function validatePublicKey(publicKeyBase64: string): boolean;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * ECDH + AES-GCM 加密工具(浏览器兼容)
3
+ * 用于将签名交易用服务器公钥加密
4
+ */
5
+ /**
6
+ * 获取全局 crypto 对象(最简单直接的方式)
7
+ */
8
+ function getCryptoAPI() {
9
+ // 尝试所有可能的全局对象,优先浏览器环境
10
+ const cryptoObj = (typeof window !== 'undefined' && window.crypto) ||
11
+ (typeof self !== 'undefined' && self.crypto) ||
12
+ (typeof global !== 'undefined' && global.crypto) ||
13
+ (typeof globalThis !== 'undefined' && globalThis.crypto);
14
+ if (!cryptoObj) {
15
+ const env = typeof window !== 'undefined' ? 'Browser' : 'Node.js';
16
+ const protocol = typeof location !== 'undefined' ? location.protocol : 'unknown';
17
+ throw new Error(`❌ Crypto API 不可用。环境: ${env}, 协议: ${protocol}. ` +
18
+ '请确保在 HTTPS 或 localhost 下运行');
19
+ }
20
+ return cryptoObj;
21
+ }
22
+ /**
23
+ * 获取 SubtleCrypto(用于加密操作)
24
+ */
25
+ function getSubtleCrypto() {
26
+ const crypto = getCryptoAPI();
27
+ if (!crypto.subtle) {
28
+ const protocol = typeof location !== 'undefined' ? location.protocol : 'unknown';
29
+ const hostname = typeof location !== 'undefined' ? location.hostname : 'unknown';
30
+ throw new Error(`❌ SubtleCrypto API 不可用。协议: ${protocol}, 主机: ${hostname}. ` +
31
+ '请确保:1) 使用 HTTPS (或 localhost);2) 浏览器支持 Web Crypto API;' +
32
+ '3) 不在无痕/隐私浏览模式下');
33
+ }
34
+ return crypto.subtle;
35
+ }
36
+ /**
37
+ * Base64 转 ArrayBuffer(优先使用浏览器 API)
38
+ */
39
+ function base64ToArrayBuffer(base64) {
40
+ // 浏览器环境(优先)
41
+ if (typeof atob !== 'undefined') {
42
+ const binaryString = atob(base64);
43
+ const bytes = new Uint8Array(binaryString.length);
44
+ for (let i = 0; i < binaryString.length; i++) {
45
+ bytes[i] = binaryString.charCodeAt(i);
46
+ }
47
+ return bytes.buffer;
48
+ }
49
+ // Node.js 环境(fallback)
50
+ if (typeof Buffer !== 'undefined') {
51
+ return Buffer.from(base64, 'base64').buffer;
52
+ }
53
+ throw new Error('❌ Base64 解码不可用');
54
+ }
55
+ /**
56
+ * ArrayBuffer 转 Base64(优先使用浏览器 API)
57
+ */
58
+ function arrayBufferToBase64(buffer) {
59
+ // 浏览器环境(优先)
60
+ if (typeof btoa !== 'undefined') {
61
+ const bytes = new Uint8Array(buffer);
62
+ let binary = '';
63
+ for (let i = 0; i < bytes.length; i++) {
64
+ binary += String.fromCharCode(bytes[i]);
65
+ }
66
+ return btoa(binary);
67
+ }
68
+ // Node.js 环境(fallback)
69
+ if (typeof Buffer !== 'undefined') {
70
+ return Buffer.from(buffer).toString('base64');
71
+ }
72
+ throw new Error('❌ Base64 编码不可用');
73
+ }
74
+ /**
75
+ * 生成随机 Hex 字符串
76
+ */
77
+ function randomHex(length) {
78
+ const crypto = getCryptoAPI();
79
+ const array = new Uint8Array(length);
80
+ crypto.getRandomValues(array);
81
+ return Array.from(array)
82
+ .map(b => b.toString(16).padStart(2, '0'))
83
+ .join('');
84
+ }
85
+ /**
86
+ * 用服务器公钥加密签名交易(ECDH + AES-GCM)
87
+ *
88
+ * @param signedTransactions 签名后的交易数组
89
+ * @param publicKeyBase64 服务器提供的公钥(Base64 格式)
90
+ * @returns JSON 字符串 {e: 临时公钥, i: IV, d: 密文}
91
+ */
92
+ export async function encryptWithPublicKey(signedTransactions, publicKeyBase64) {
93
+ try {
94
+ // 0. 获取 SubtleCrypto 和 Crypto API
95
+ const subtle = getSubtleCrypto();
96
+ const crypto = getCryptoAPI();
97
+ // 1. 准备数据
98
+ const payload = {
99
+ signedTransactions,
100
+ timestamp: Date.now(),
101
+ nonce: randomHex(8)
102
+ };
103
+ const plaintext = JSON.stringify(payload);
104
+ // 2. 生成临时 ECDH 密钥对
105
+ const ephemeralKeyPair = await subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey']);
106
+ // 3. 导入服务器公钥
107
+ const publicKeyBuffer = base64ToArrayBuffer(publicKeyBase64);
108
+ const publicKey = await subtle.importKey('raw', publicKeyBuffer, { name: 'ECDH', namedCurve: 'P-256' }, false, []);
109
+ // 4. 派生共享密钥(AES-256)
110
+ const sharedKey = await subtle.deriveKey({ name: 'ECDH', public: publicKey }, ephemeralKeyPair.privateKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt']);
111
+ // 5. AES-GCM 加密
112
+ const iv = crypto.getRandomValues(new Uint8Array(12));
113
+ const encrypted = await subtle.encrypt({ name: 'AES-GCM', iv }, sharedKey, new TextEncoder().encode(plaintext));
114
+ // 6. 导出临时公钥
115
+ const ephemeralPublicKeyRaw = await subtle.exportKey('raw', ephemeralKeyPair.publicKey);
116
+ // 7. 返回加密包(JSON 格式)
117
+ return JSON.stringify({
118
+ e: arrayBufferToBase64(ephemeralPublicKeyRaw), // 临时公钥
119
+ i: arrayBufferToBase64(iv.buffer), // IV
120
+ d: arrayBufferToBase64(encrypted) // 密文
121
+ });
122
+ }
123
+ catch (error) {
124
+ throw new Error(`加密失败: ${error?.message || String(error)}`);
125
+ }
126
+ }
127
+ /**
128
+ * 验证公钥格式(Base64)
129
+ */
130
+ export function validatePublicKey(publicKeyBase64) {
131
+ try {
132
+ if (!publicKeyBase64)
133
+ return false;
134
+ // Base64 字符集验证
135
+ if (!/^[A-Za-z0-9+/=]+$/.test(publicKeyBase64))
136
+ return false;
137
+ // ECDH P-256 公钥固定长度 65 字节(未压缩)
138
+ // Base64 编码后约 88 字符
139
+ if (publicKeyBase64.length < 80 || publicKeyBase64.length > 100)
140
+ return false;
141
+ return true;
142
+ }
143
+ catch {
144
+ return false;
145
+ }
146
+ }
@@ -147,6 +147,26 @@ export async function buildDisperseFromSingleOwnerOpsWithProfit(params) {
147
147
  const fnFixed = aaManager.buildUserOpWithFixedGas;
148
148
  if (typeof fnFixed !== 'function')
149
149
  throw new Error('AA disperse:SDK 不支持 buildUserOpWithFixedGas');
150
+ // ✅ 关键修复:如果有多个 UserOps,需要确保 sender 有足够余额支付所有 prefund
151
+ // 多个 UserOps 在同一个 handleOps 中提交时,prefund 在 validation 阶段同时扣除
152
+ if (chunks.length > 1 && params.kind === 'native') {
153
+ const feeData = await provider.getFeeData();
154
+ const gasPrice = feeData.gasPrice ?? 5000000000n;
155
+ const estimatePrefundPerOp = (listLength, deployed) => {
156
+ const base = 180000n;
157
+ const per = 45000n;
158
+ const callGas = base + per * BigInt(listLength);
159
+ const verifyGas = deployed ? 250000n : 800000n;
160
+ const preVerifyGas = 60000n;
161
+ return (callGas + verifyGas + preVerifyGas) * gasPrice * 120n / 100n;
162
+ };
163
+ let totalPrefundNeeded = 0n;
164
+ for (let i = 0; i < chunks.length; i++) {
165
+ const deployed = i === 0 ? deployed0 : true;
166
+ totalPrefundNeeded += estimatePrefundPerOp(chunks[i].length, deployed);
167
+ }
168
+ console.log(`[AA 分发] 多批次分发:${chunks.length} 批,总 prefund 预估: ${ethers.formatEther(totalPrefundNeeded)} OKB`);
169
+ }
150
170
  const ops = [];
151
171
  for (let i = 0; i < chunks.length; i++) {
152
172
  const list = chunks[i];
@@ -268,6 +288,19 @@ export async function buildTransfersWithProfit(params) {
268
288
  initCodeUsed.add(k);
269
289
  return aaManager.generateInitCode(ownerAddress);
270
290
  };
291
+ // ✅ 关键修复:计算每个 sender 执行 UserOp 需要的 prefund(gas 费用)
292
+ // 在 Native 模式下,需要确保留足够的金额支付 prefund
293
+ const feeData = await provider.getFeeData();
294
+ const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n;
295
+ const estimateTransferPrefund = (deployed) => {
296
+ // 归集操作的 gas:callGasLimit + verificationGasLimit + preVerificationGas
297
+ const callGas = 120000n; // Native 转账
298
+ const verifyGas = deployed ? 250000n : 800000n;
299
+ const preVerifyGas = 60000n;
300
+ const totalGas = callGas + verifyGas + preVerifyGas;
301
+ // 加 30% buffer 以确保足够(归集操作需要更保守)
302
+ return (totalGas * gasPrice * 130n) / 100n;
303
+ };
271
304
  // ✅ 第一步:计算每个钱包的归集金额和总利润
272
305
  // 利润从每个钱包的归集金额中扣除,但只由第一个有效钱包统一转账给利润地址
273
306
  const transferInfos = [];
@@ -292,10 +325,18 @@ export async function buildTransfersWithProfit(params) {
292
325
  if (amountWei <= 0n)
293
326
  continue;
294
327
  const profitWei = calcProfitWei(amountWei, PROFIT_CONFIG.RATE_BPS);
295
- const toWei = amountWei - profitWei;
328
+ const deployed = accountInfos[i]?.deployed ?? false;
329
+ const prefundWei = params.kind === 'native' ? estimateTransferPrefund(deployed) : 0n;
330
+ // ✅ 关键修复:toWei 需要扣除 max(profitWei, prefundWei),确保留足够的 gas
331
+ // 在 Native 模式下,如果 profitWei < prefundWei,需要留更多余额
332
+ const reserveWei = params.kind === 'native'
333
+ ? (profitWei > prefundWei ? profitWei : prefundWei)
334
+ : profitWei;
335
+ const toWei = amountWei > reserveWei ? (amountWei - reserveWei) : 0n;
296
336
  totalProfitWei += profitWei;
297
- transferInfos.push({ walletIndex: i, sender, to, amountWei, profitWei, toWei });
337
+ transferInfos.push({ walletIndex: i, sender, to, amountWei, profitWei, toWei, prefundWei });
298
338
  }
339
+ console.log(`[AA 归集] 钱包数量: ${transferInfos.length}, 总利润: ${ethers.formatEther(totalProfitWei)} OKB`);
299
340
  const unsigned = [];
300
341
  const opOwnerIndex = [];
301
342
  // ✅ 第二步:构建 UserOps
@@ -1555,7 +1555,7 @@ export class BundleExecutor {
1555
1555
  nonce: nonceMap.next(payerAccount.sender),
1556
1556
  initCode: payerAccount.deployed ? '0x' : (await aaManager.generateInitCode(payerWallet.address)),
1557
1557
  deployed: payerAccount.deployed,
1558
- fixedGas: { callGasLimit: 800000n } // 发币较重,给 80W
1558
+ fixedGas: { callGasLimit: 960000n } // 发币较重,给 80W
1559
1559
  });
1560
1560
  const signedCreateOp = await aaManager.signUserOp(createOpRes.userOp, payerWallet);
1561
1561
  ops1.push(signedCreateOp.userOp);
@@ -1575,7 +1575,9 @@ export class BundleExecutor {
1575
1575
  // ✅ 并行构建和签名所有内盘买入 UserOps(降低并发数避免 RPC 限制)
1576
1576
  // ✅ 使用 encodeBuyCallV3(支持 extensionData),与 V4 代币创建保持一致
1577
1577
  const extensionData = params.extensionData ?? '0x';
1578
- const signedCurveBuyOps = await mapWithConcurrency(curveBuyData, 2, async (data) => {
1578
+ // 预分配 nonce(并发前同步分配,避免 race condition)
1579
+ const curveBuyNonces = curveBuyData.map((data) => nonceMap.next(data.info.sender));
1580
+ const signedCurveBuyOps = await mapWithConcurrency(curveBuyData, 2, async (data, i) => {
1579
1581
  const { wallet, info, buyWei, gasLimit } = data;
1580
1582
  const buyData = encodeBuyCallV3(tokenAddress, buyWei, 0n, extensionData);
1581
1583
  const buyCallData = encodeExecute(FLAP_PORTAL, buyWei, buyData);
@@ -1583,7 +1585,7 @@ export class BundleExecutor {
1583
1585
  ownerWallet: wallet,
1584
1586
  sender: info.sender,
1585
1587
  callData: buyCallData,
1586
- nonce: nonceMap.next(info.sender),
1588
+ nonce: curveBuyNonces[i], // ✅ 使用预分配的 nonce
1587
1589
  initCode: info.deployed ? '0x' : (await aaManager.generateInitCode(wallet.address)),
1588
1590
  deployed: info.deployed,
1589
1591
  fixedGas: { callGasLimit: gasLimit }
@@ -1610,8 +1612,10 @@ export class BundleExecutor {
1610
1612
  return { wallet, info: dexBuyerInfos[i], buyWei };
1611
1613
  });
1612
1614
  totalDexBuyWei = dexBuyData.reduce((sum, d) => sum + d.buyWei, 0n);
1615
+ // ✅ 预分配 nonce(并发前同步分配,避免 race condition)
1616
+ const dexBuyNonces = dexBuyData.map((data) => nonceMap.next(data.info.sender));
1613
1617
  // ✅ 并行构建和签名所有外盘买入 UserOps(降低并发数避免 RPC 限制)
1614
- const signedDexBuyOps = await mapWithConcurrency(dexBuyData, 2, async (data) => {
1618
+ const signedDexBuyOps = await mapWithConcurrency(dexBuyData, 2, async (data, i) => {
1615
1619
  const { wallet, info, buyWei } = data;
1616
1620
  // ✅ 外盘买入:使用 DEX Router 进行交换
1617
1621
  let swapData;
@@ -1638,7 +1642,7 @@ export class BundleExecutor {
1638
1642
  ownerWallet: wallet,
1639
1643
  sender: info.sender,
1640
1644
  callData: dexBuyCallData,
1641
- nonce: nonceMap.next(info.sender),
1645
+ nonce: dexBuyNonces[i], // ✅ 使用预分配的 nonce
1642
1646
  initCode: info.deployed ? '0x' : (await aaManager.generateInitCode(wallet.address)),
1643
1647
  deployed: info.deployed,
1644
1648
  fixedGas: { callGasLimit: 500000n } // 外盘买入:50W
@@ -581,8 +581,29 @@ export class AADexSwapExecutor {
581
581
  if (capitalMode && buyerSenders.length > 0 && totalBuyWei > 0n) {
582
582
  if (hopCount <= 0) {
583
583
  console.log('[AA DEX 批量换手] 进入直接分发模式 (hopCount <= 0)');
584
- // ✅ 直接分发模式:将利润接收者加入分发列表(AA 内部刮取)
585
- const items = buyerSenders.map((to, i) => ({ to, value: buyAmountsWei[i] ?? 0n })).filter(x => x.value > 0n);
584
+ // ✅ 关键修复:计算每个买方执行买入 UserOp 需要的 prefund(gas 费用)
585
+ // EntryPoint validation 阶段会扣除 prefund,此时分发还没执行
586
+ // 因此需要为每个买方额外预留 prefund
587
+ const feeData = await this.aaManager.getFeeData();
588
+ const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n; // 5 gwei fallback
589
+ const estimateBuyerPrefund = (deployed) => {
590
+ // 买入操作的 gas:V3 swap 约 650,000,部署需要额外 verificationGas
591
+ const callGas = 650000n;
592
+ const verifyGas = deployed ? 250000n : 800000n;
593
+ const preVerifyGas = 60000n;
594
+ const totalGas = callGas + verifyGas + preVerifyGas;
595
+ // 加 20% buffer 以确保足够
596
+ return (totalGas * gasPrice * 120n) / 100n;
597
+ };
598
+ // 计算每个买方需要的 prefund
599
+ const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefund(ai.deployed));
600
+ const totalBuyerPrefund = buyerPrefunds.reduce((a, b) => a + b, 0n);
601
+ console.log(`[AA DEX 直接分发] 买方数量: ${buyerSenders.length}, 总 prefund 预估: ${ethers.formatEther(totalBuyerPrefund)} OKB`);
602
+ // ✅ 直接分发模式:分发金额 = 买入金额 + 买方 prefund
603
+ const items = buyerSenders.map((to, i) => ({
604
+ to,
605
+ value: (buyAmountsWei[i] ?? 0n) + (buyerPrefunds[i] ?? 0n) // ✅ 包含 prefund
606
+ })).filter(x => x.value > 0n);
586
607
  // 添加利润接收者到分发列表
587
608
  if (extractProfit && profitWei > 0n) {
588
609
  items.push({ to: profitRecipient, value: profitWei });
@@ -390,8 +390,29 @@ export class AAPortalSwapExecutor {
390
390
  if (capitalMode && buyerSenders.length > 0 && totalBuyWei > 0n) {
391
391
  if (hopCount <= 0) {
392
392
  console.log('[AA Portal 批量换手] 进入直接分发模式 (hopCount <= 0)');
393
- // ✅ 直接分发模式:将利润接收者加入分发列表(AA 内部刮取)
394
- const items = buyerSenders.map((to, i) => ({ to, value: buyAmountsWei[i] ?? 0n })).filter(x => x.value > 0n);
393
+ // ✅ 关键修复:计算每个买方执行买入 UserOp 需要的 prefund(gas 费用)
394
+ // EntryPoint validation 阶段会扣除 prefund,此时分发还没执行
395
+ // 因此需要为每个买方额外预留 prefund
396
+ const feeData = await this.aaManager.getFeeData();
397
+ const gasPrice = feeData.gasPrice ?? feeData.maxFeePerGas ?? 5000000000n; // 5 gwei fallback
398
+ const estimateBuyerPrefund = (deployed) => {
399
+ // 买入操作的 gas:Portal 买入约 450,000,部署需要额外 verificationGas
400
+ const callGas = 450000n;
401
+ const verifyGas = deployed ? 250000n : 800000n;
402
+ const preVerifyGas = 60000n;
403
+ const totalGas = callGas + verifyGas + preVerifyGas;
404
+ // 加 20% buffer 以确保足够
405
+ return (totalGas * gasPrice * 120n) / 100n;
406
+ };
407
+ // 计算每个买方需要的 prefund
408
+ const buyerPrefunds = buyerAis.map(ai => estimateBuyerPrefund(ai.deployed));
409
+ const totalBuyerPrefund = buyerPrefunds.reduce((a, b) => a + b, 0n);
410
+ console.log(`[AA Portal 直接分发] 买方数量: ${buyerSenders.length}, 总 prefund 预估: ${ethers.formatEther(totalBuyerPrefund)} OKB`);
411
+ // ✅ 直接分发模式:分发金额 = 买入金额 + 买方 prefund
412
+ const items = buyerSenders.map((to, i) => ({
413
+ to,
414
+ value: (buyAmountsWei[i] ?? 0n) + (buyerPrefunds[i] ?? 0n) // ✅ 包含 prefund
415
+ })).filter(x => x.value > 0n);
395
416
  if (extractProfit && profitWei > 0n) {
396
417
  items.push({ to: profitRecipient, value: profitWei });
397
418
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.6.25",
3
+ "version": "1.6.28",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",