four-flap-meme-sdk 1.4.12 → 1.4.13

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.
@@ -129,6 +129,8 @@ export interface PancakeQuickBatchSwapParams {
129
129
  tokenAddress: string;
130
130
  routeParams: RouteParams;
131
131
  config: PancakeSwapSignConfig;
132
+ quoteToken?: string;
133
+ quoteTokenDecimals?: number;
132
134
  }
133
135
  /**
134
136
  * 快捷批量换手结果
@@ -139,9 +141,10 @@ export interface PancakeQuickBatchSwapResult {
139
141
  sellerAddress: string;
140
142
  buyerAddresses: string[];
141
143
  sellAmount: string;
142
- estimatedBNBOut: string;
144
+ estimatedOutput: string;
143
145
  transferAmounts: string[];
144
146
  profitAmount?: string;
147
+ useNativeToken: boolean;
145
148
  };
146
149
  }
147
150
  /**
@@ -150,9 +153,10 @@ export interface PancakeQuickBatchSwapResult {
150
153
  * 流程:[贿赂] → [卖出] → [转账1, 转账2, ...] → [买入1, 买入2, ...] → [利润]
151
154
  *
152
155
  * 特点:
153
- * - 子钱包不需要预先有 BNB 余额
156
+ * - 子钱包不需要预先有余额
154
157
  * - 资金来自主钱包卖出代币所得
155
158
  * - 提升资金利用率
159
+ * - 支持 BNB 和 ERC20(如 USDT)两种模式
156
160
  *
157
161
  * 限制:最多 23 个买方(转账 + 买入 = 46 笔,加上贿赂/卖出/利润 = 50 笔限制)
158
162
  */
@@ -743,22 +743,30 @@ export async function pancakeBatchSwapMerkle(params) {
743
743
  * 流程:[贿赂] → [卖出] → [转账1, 转账2, ...] → [买入1, 买入2, ...] → [利润]
744
744
  *
745
745
  * 特点:
746
- * - 子钱包不需要预先有 BNB 余额
746
+ * - 子钱包不需要预先有余额
747
747
  * - 资金来自主钱包卖出代币所得
748
748
  * - 提升资金利用率
749
+ * - 支持 BNB 和 ERC20(如 USDT)两种模式
749
750
  *
750
751
  * 限制:最多 23 个买方(转账 + 买入 = 46 笔,加上贿赂/卖出/利润 = 50 笔限制)
751
752
  */
752
753
  export async function pancakeQuickBatchSwapMerkle(params) {
753
- const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, buyerRatios, buyerAmounts, tokenAddress, routeParams, config } = params;
754
- // ✅ 校验买方数量(最多 23 个:贿赂(1) + 卖出(1) + 转账(N) + 买入(N) + 利润(1) ≤ 50)
755
- // 2N + 3 50 → N ≤ 23
756
- const MAX_BUYERS = 23;
754
+ const { sellerPrivateKey, sellAmount, sellPercentage, buyerPrivateKeys, buyerRatios, buyerAmounts, tokenAddress, routeParams, config, quoteToken, quoteTokenDecimals = 18 } = params;
755
+ // ✅ 判断是否使用原生代币
756
+ const useNativeToken = !quoteToken || quoteToken === ZERO_ADDRESS;
757
+ const WBNB = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c'.toLowerCase();
758
+ // ✅ 校验买方数量
759
+ // BNB 模式:贿赂(1) + 卖出(1) + 转账(N) + 买入(N) + 利润(1) ≤ 50 → 2N + 3 ≤ 50 → N ≤ 23
760
+ // ERC20 模式:贿赂(1) + 卖出(1) + ERC20转账(N) + BNB Gas转账(N) + 买入(N) + 利润(1) ≤ 50 → 3N + 3 ≤ 50 → N ≤ 15
761
+ const MAX_BUYERS_NATIVE = 23;
762
+ const MAX_BUYERS_ERC20 = 15;
763
+ const MAX_BUYERS = useNativeToken ? MAX_BUYERS_NATIVE : MAX_BUYERS_ERC20;
757
764
  if (buyerPrivateKeys.length === 0) {
758
765
  throw new Error('至少需要一个买方钱包');
759
766
  }
760
767
  if (buyerPrivateKeys.length > MAX_BUYERS) {
761
- throw new Error(`资金利用率模式买方钱包数量超过限制: ${buyerPrivateKeys.length} > ${MAX_BUYERS}`);
768
+ const mode = useNativeToken ? 'BNB' : 'ERC20';
769
+ throw new Error(`资金利用率模式(${mode})买方钱包数量超过限制: ${buyerPrivateKeys.length} > ${MAX_BUYERS}`);
762
770
  }
763
771
  // ✅ 校验分配模式
764
772
  if (!buyerRatios && !buyerAmounts) {
@@ -772,20 +780,51 @@ export async function pancakeQuickBatchSwapMerkle(params) {
772
780
  }
773
781
  const context = createPancakeContext(config);
774
782
  const seller = new Wallet(sellerPrivateKey, context.provider);
775
- // ✅ 买方需要私钥(因为买方自己执行买入交易)
776
783
  const buyers = buyerPrivateKeys.map(pk => new Wallet(pk, context.provider));
784
+ // ✅ 校验卖出路径输出代币是否匹配
785
+ let sellOutputToken;
786
+ if (routeParams.routeType === 'v2') {
787
+ const { v2Path } = routeParams;
788
+ sellOutputToken = v2Path[v2Path.length - 1];
789
+ }
790
+ else if (routeParams.routeType === 'v3-single') {
791
+ const { v3TokenOut } = routeParams;
792
+ sellOutputToken = v3TokenOut;
793
+ }
794
+ else if (routeParams.routeType === 'v3-multi') {
795
+ const { v2Path } = routeParams;
796
+ if (v2Path && v2Path.length > 0) {
797
+ sellOutputToken = v2Path[v2Path.length - 1];
798
+ }
799
+ }
800
+ // 校验输出代币
801
+ if (useNativeToken) {
802
+ // 原生代币模式:输出必须是 WBNB
803
+ if (!sellOutputToken || sellOutputToken.toLowerCase() !== WBNB) {
804
+ throw new Error(`原生代币模式要求卖出路径以 WBNB 结尾(当前输出: ${sellOutputToken || '未知'})。` +
805
+ `请切换交易对或使用 ERC20 模式。`);
806
+ }
807
+ }
808
+ else {
809
+ // ERC20 模式:输出必须是指定的 quoteToken
810
+ if (!sellOutputToken || sellOutputToken.toLowerCase() !== quoteToken.toLowerCase()) {
811
+ throw new Error(`ERC20 模式要求卖出路径以 ${quoteToken} 结尾(当前输出: ${sellOutputToken || '未知'})。`);
812
+ }
813
+ }
777
814
  // ✅ 创建共享资源
778
815
  const nonceManager = new NonceManager(context.provider);
779
816
  const finalGasLimit = getGasLimit(config);
780
817
  const txType = config.txType ?? 0;
818
+ const ERC20_TRANSFER_GAS = 65000n; // ERC20 transfer 的 gas 限制
781
819
  // ✅ 并行获取 gasPrice 和卖出数量
782
820
  const [gasPrice, sellAmountResult] = await Promise.all([
783
821
  getGasPrice(context.provider, config),
784
822
  calculateSellAmount(context.provider, tokenAddress, seller.address, sellAmount, sellPercentage)
785
823
  ]);
786
824
  const { amount: sellAmountWei, decimals } = sellAmountResult;
787
- // ✅ 调试日志:打印卖出数量
788
- console.log(`[pancakeQuickBatchSwapMerkle] 卖出数量: ${ethers.formatUnits(sellAmountWei, decimals)} (${sellAmountWei} wei, decimals=${decimals})`);
825
+ // ✅ 调试日志
826
+ console.log(`[pancakeQuickBatchSwapMerkle] 模式: ${useNativeToken ? 'BNB' : 'ERC20'}`);
827
+ console.log(`[pancakeQuickBatchSwapMerkle] 卖出数量: ${ethers.formatUnits(sellAmountWei, decimals)}`);
789
828
  console.log(`[pancakeQuickBatchSwapMerkle] routeParams:`, JSON.stringify({
790
829
  routeType: routeParams.routeType,
791
830
  v2Path: routeParams.v2Path,
@@ -799,31 +838,44 @@ export async function pancakeQuickBatchSwapMerkle(params) {
799
838
  sellAmountWei,
800
839
  provider: context.provider
801
840
  });
802
- const estimatedBNBOut = quoteResult.estimatedBNBOut;
803
- console.log(`[pancakeQuickBatchSwapMerkle] 预估卖出所得: ${ethers.formatEther(estimatedBNBOut)} BNB (${estimatedBNBOut} wei)`);
804
- // ✅ 安全检查:如果报价超过 10 BNB,警告可能有问题
805
- if (estimatedBNBOut > ethers.parseEther('10')) {
806
- console.warn(`[pancakeQuickBatchSwapMerkle] ⚠️ 报价异常高: ${ethers.formatEther(estimatedBNBOut)} BNB,请检查路径和数量!`);
807
- }
808
- // ✅ 计算利润(从卖出所得中预扣)
809
- const profitAmount = calculateProfitAmount(estimatedBNBOut);
810
- const distributableBNB = estimatedBNBOut - profitAmount; // 可分配金额
811
- // ✅ 计算每个买方分到的 BNB(用于转账和买入)
841
+ const estimatedOutput = quoteResult.estimatedBNBOut;
842
+ const outputFormatted = useNativeToken
843
+ ? ethers.formatEther(estimatedOutput)
844
+ : ethers.formatUnits(estimatedOutput, quoteTokenDecimals);
845
+ console.log(`[pancakeQuickBatchSwapMerkle] 预估卖出所得: ${outputFormatted} ${useNativeToken ? 'BNB' : 'ERC20'}`);
846
+ // ✅ 计算利润(基于 BNB 价值)
847
+ let profitAmount;
848
+ if (useNativeToken) {
849
+ profitAmount = calculateProfitAmount(estimatedOutput);
850
+ }
851
+ else {
852
+ // ERC20 模式:需要将 ERC20 价值转换为 BNB
853
+ const version = routeParams.routeType === 'v2' ? 'v2' : 'v3';
854
+ const fee = routeParams.routeType === 'v3-single' ? routeParams.v3Fee : undefined;
855
+ const estimatedBNBValue = await getERC20ToNativeQuote(context.provider, quoteToken, estimatedOutput, version, fee);
856
+ profitAmount = calculateProfitAmount(estimatedBNBValue);
857
+ console.log(`[pancakeQuickBatchSwapMerkle] ERC20→BNB 报价: ${outputFormatted} → ${ethers.formatEther(estimatedBNBValue)} BNB`);
858
+ }
859
+ const distributableAmount = estimatedOutput - (useNativeToken ? profitAmount : 0n);
860
+ // ✅ 计算每个买方分到的金额
812
861
  let transferAmountsWei;
813
862
  if (buyerAmounts && buyerAmounts.length === buyers.length) {
814
- // 数量模式:使用指定的金额
815
- transferAmountsWei = buyerAmounts.map(amt => ethers.parseEther(amt));
816
- // 校验总金额不超过可分配金额
863
+ // 数量模式
864
+ transferAmountsWei = buyerAmounts.map(amt => useNativeToken
865
+ ? ethers.parseEther(amt)
866
+ : ethers.parseUnits(amt, quoteTokenDecimals));
817
867
  const totalTransfer = transferAmountsWei.reduce((a, b) => a + b, 0n);
818
- if (totalTransfer > distributableBNB) {
819
- throw new Error(`指定的买入总金额 (${ethers.formatEther(totalTransfer)} BNB) 超过可分配金额 (${ethers.formatEther(distributableBNB)} BNB)`);
868
+ if (totalTransfer > distributableAmount) {
869
+ const formatted = useNativeToken
870
+ ? ethers.formatEther(distributableAmount)
871
+ : ethers.formatUnits(distributableAmount, quoteTokenDecimals);
872
+ throw new Error(`指定的买入总金额超过可分配金额 (${formatted})`);
820
873
  }
821
874
  }
822
875
  else if (buyerRatios && buyerRatios.length === buyers.length) {
823
- // 比例模式:按比例分配可分配金额
876
+ // 比例模式
824
877
  transferAmountsWei = buyerRatios.map(ratio => {
825
- const amount = (distributableBNB * BigInt(Math.round(ratio * 10000))) / 10000n;
826
- return amount;
878
+ return (distributableAmount * BigInt(Math.round(ratio * 10000))) / 10000n;
827
879
  });
828
880
  }
829
881
  else {
@@ -833,16 +885,27 @@ export async function pancakeQuickBatchSwapMerkle(params) {
833
885
  const bribeAmount = config.bribeAmount && config.bribeAmount > 0
834
886
  ? ethers.parseEther(String(config.bribeAmount))
835
887
  : 0n;
836
- // ✅ 验证主钱包余额(需要支付 Gas 费用 + 贿赂)
888
+ // ✅ 验证主钱包余额
837
889
  const sellerBalance = await seller.provider.getBalance(seller.address);
838
- // 卖方需要的 Gas:贿赂(21000) + 卖出(gasLimit) + N个转账(21000 each) + 利润(21000)
839
- const sellerGasCost = gasPrice * (21000n + finalGasLimit + 21000n * BigInt(buyers.length) + 21000n);
840
- const sellerRequired = bribeAmount + sellerGasCost;
890
+ let sellerGasCost;
891
+ let sellerRequired;
892
+ if (useNativeToken) {
893
+ // BNB 模式:贿赂(21000) + 卖出(gasLimit) + N个原生转账(21000 each) + 利润(21000)
894
+ sellerGasCost = gasPrice * (21000n + finalGasLimit + 21000n * BigInt(buyers.length) + 21000n);
895
+ sellerRequired = bribeAmount + sellerGasCost;
896
+ }
897
+ else {
898
+ // ERC20 模式:
899
+ // - 贿赂(21000) + 卖出(gasLimit) + N个ERC20转账(65000 each) + N个BNB Gas转账(21000 each) + 利润(21000)
900
+ // - 还需要给买家转 BNB 用于支付 FLAT_FEE 和买入 Gas
901
+ sellerGasCost = gasPrice * (21000n + finalGasLimit + (ERC20_TRANSFER_GAS + 21000n) * BigInt(buyers.length) + 21000n);
902
+ const buyerGasNeeded = (gasPrice * finalGasLimit + FLAT_FEE) * BigInt(buyers.length);
903
+ sellerRequired = bribeAmount + sellerGasCost + buyerGasNeeded;
904
+ }
841
905
  if (sellerBalance < sellerRequired) {
842
906
  throw new Error(`主钱包 BNB 余额不足: 需要约 ${ethers.formatEther(sellerRequired)} BNB (贿赂: ${ethers.formatEther(bribeAmount)}, Gas: ${ethers.formatEther(sellerGasCost)}), 实际 ${ethers.formatEther(sellerBalance)} BNB`);
843
907
  }
844
908
  // ==================== 规划 Nonce ====================
845
- // 流程: [贿赂] → [卖出] → [转账1, 转账2, ...] → [买入1, 买入2, ...] → [利润]
846
909
  let sellerNonce = await nonceManager.getNextNonce(seller);
847
910
  const deadline = Math.floor(Date.now() / 1000) + 600;
848
911
  // ==================== 1. 贿赂交易 ====================
@@ -884,32 +947,71 @@ export async function pancakeQuickBatchSwapMerkle(params) {
884
947
  type: txType
885
948
  });
886
949
  console.log(`[pancakeQuickBatchSwapMerkle] 卖出交易已签名`);
887
- // ==================== 3. 转账交易(卖方 → 各买方)====================
950
+ // ==================== 3. 转账交易 ====================
888
951
  const transferTxs = [];
889
- for (let i = 0; i < buyers.length; i++) {
890
- // 转账金额 = 买入金额 + FLAT_FEE + 买方 Gas 费用
891
- const buyerGasCost = gasPrice * finalGasLimit;
892
- const transferValue = transferAmountsWei[i] + FLAT_FEE + buyerGasCost;
893
- const transferTx = await seller.signTransaction({
894
- to: buyers[i].address,
895
- value: transferValue,
896
- nonce: sellerNonce++,
897
- gasPrice,
898
- gasLimit: 21000n,
899
- chainId: context.chainId,
900
- type: txType
901
- });
902
- transferTxs.push(transferTx);
952
+ if (useNativeToken) {
953
+ // 原生代币模式:直接 BNB 转账
954
+ for (let i = 0; i < buyers.length; i++) {
955
+ const buyerGasCost = gasPrice * finalGasLimit;
956
+ const transferValue = transferAmountsWei[i] + FLAT_FEE + buyerGasCost;
957
+ const transferTx = await seller.signTransaction({
958
+ to: buyers[i].address,
959
+ value: transferValue,
960
+ nonce: sellerNonce++,
961
+ gasPrice,
962
+ gasLimit: 21000n,
963
+ chainId: context.chainId,
964
+ type: txType
965
+ });
966
+ transferTxs.push(transferTx);
967
+ }
968
+ }
969
+ else {
970
+ // ✅ ERC20 模式:ERC20 transfer 调用
971
+ const erc20Interface = new ethers.Interface([
972
+ 'function transfer(address to, uint256 amount) returns (bool)'
973
+ ]);
974
+ for (let i = 0; i < buyers.length; i++) {
975
+ const transferData = erc20Interface.encodeFunctionData('transfer', [
976
+ buyers[i].address,
977
+ transferAmountsWei[i]
978
+ ]);
979
+ const transferTx = await seller.signTransaction({
980
+ to: quoteToken,
981
+ data: transferData,
982
+ value: 0n,
983
+ nonce: sellerNonce++,
984
+ gasPrice,
985
+ gasLimit: ERC20_TRANSFER_GAS,
986
+ chainId: context.chainId,
987
+ type: txType
988
+ });
989
+ transferTxs.push(transferTx);
990
+ }
991
+ // ERC20 模式:额外转账 Gas 费用给买家(用于支付 FLAT_FEE 和买入 Gas)
992
+ for (let i = 0; i < buyers.length; i++) {
993
+ const buyerGasCost = gasPrice * finalGasLimit + FLAT_FEE;
994
+ const gasTx = await seller.signTransaction({
995
+ to: buyers[i].address,
996
+ value: buyerGasCost,
997
+ nonce: sellerNonce++,
998
+ gasPrice,
999
+ gasLimit: 21000n,
1000
+ chainId: context.chainId,
1001
+ type: txType
1002
+ });
1003
+ transferTxs.push(gasTx);
1004
+ }
903
1005
  }
904
1006
  console.log(`[pancakeQuickBatchSwapMerkle] ${transferTxs.length} 笔转账交易已签名`);
905
- // ==================== 4. 买入交易(各买方执行)====================
906
- // 并行获取所有买方的 nonce
1007
+ // ==================== 4. 买入交易 ====================
907
1008
  const buyerNonces = await Promise.all(buyers.map(buyer => nonceManager.getNextNonce(buyer)));
908
- // 并行签名所有买入交易
909
1009
  const signedBuys = await Promise.all(buyers.map(async (buyer, i) => {
910
1010
  const buyAmount = transferAmountsWei[i];
911
1011
  const proxyBuyer = new Contract(PANCAKE_PROXY_ADDRESS, PANCAKE_PROXY_ABI, buyer);
912
- const buyValue = buyAmount + FLAT_FEE;
1012
+ // BNB 模式:value = buyAmount + FLAT_FEE
1013
+ // ERC20 模式:value = FLAT_FEE(买入金额是 ERC20,通过授权支付)
1014
+ const buyValue = useNativeToken ? buyAmount + FLAT_FEE : FLAT_FEE;
913
1015
  let buyUnsigned;
914
1016
  if (routeParams.routeType === 'v2') {
915
1017
  const { v2Path } = routeParams;
@@ -951,7 +1053,6 @@ export async function pancakeQuickBatchSwapMerkle(params) {
951
1053
  }
952
1054
  nonceManager.clearTemp();
953
1055
  // ==================== 组装交易数组 ====================
954
- // 顺序:贿赂 → 卖出 → 转账1, 转账2, ... → 买入1, 买入2, ... → 利润
955
1056
  const signedTransactions = [];
956
1057
  if (bribeTx)
957
1058
  signedTransactions.push(bribeTx);
@@ -966,15 +1067,21 @@ export async function pancakeQuickBatchSwapMerkle(params) {
966
1067
  console.log(` - 转账: ${transferTxs.length}`);
967
1068
  console.log(` - 买入: ${signedBuys.length}`);
968
1069
  console.log(` - 利润: ${profitTx ? 1 : 0}`);
1070
+ const outputUnit = useNativeToken ? 'BNB' : 'ERC20';
969
1071
  return {
970
1072
  signedTransactions,
971
1073
  metadata: {
972
1074
  sellerAddress: seller.address,
973
1075
  buyerAddresses: buyers.map(b => b.address),
974
1076
  sellAmount: ethers.formatUnits(sellAmountWei, decimals),
975
- estimatedBNBOut: ethers.formatEther(estimatedBNBOut),
976
- transferAmounts: transferAmountsWei.map(amt => ethers.formatEther(amt)),
977
- profitAmount: profitAmount > 0n ? ethers.formatEther(profitAmount) : undefined
1077
+ estimatedOutput: useNativeToken
1078
+ ? ethers.formatEther(estimatedOutput)
1079
+ : ethers.formatUnits(estimatedOutput, quoteTokenDecimals),
1080
+ transferAmounts: transferAmountsWei.map(amt => useNativeToken
1081
+ ? ethers.formatEther(amt)
1082
+ : ethers.formatUnits(amt, quoteTokenDecimals)),
1083
+ profitAmount: profitAmount > 0n ? ethers.formatEther(profitAmount) : undefined,
1084
+ useNativeToken
978
1085
  }
979
1086
  };
980
1087
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-flap-meme-sdk",
3
- "version": "1.4.12",
3
+ "version": "1.4.13",
4
4
  "description": "SDK for Flap bonding curve and four.meme TokenManager",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",