diff --git a/docs/MIN_TIP_AMOUNT.md b/docs/MIN_TIP_AMOUNT.md new file mode 100644 index 0000000..b4f934c --- /dev/null +++ b/docs/MIN_TIP_AMOUNT.md @@ -0,0 +1,118 @@ +# Node1 最小小费金额限制 + +## 概述 + +Node1 节点要求最小小费金额为 **0.002 SOL**,低于此金额的交易可能会被拒绝。 + +**重要**:这个限制**仅对 Node1** 生效,其他 swqos(Jito、BlockRazor、Astralane 等)不受影响。 + +sol-trade-sdk 已自动添加智能检测,只对 Node1 的 tip_account 应用最小小费金额检查。 + +## 实现位置 + +**文件**: `sol-trade-sdk/src/trading/common/transaction_builder.rs` + +**修改内容**: +```rust +// Add tip transfer instruction +if with_tip && tip_amount > 0.0 { + // 🔧 Node1 最小小费金额限制:0.002 SOL(仅限 Node1) + const MIN_TIP_AMOUNT: f64 = 0.002; + + // 检查是否是 Node1 的 tip_account + let is_node1 = NODE1_TIP_ACCOUNTS.iter().any(|&account| account == *tip_account); + + let actual_tip_amount = if is_node1 && tip_amount < MIN_TIP_AMOUNT { + // Node1 要求最小 0.002 SOL + MIN_TIP_AMOUNT + } else { + // 其他 swqos 使用原始金额 + tip_amount + }; + + instructions.push(transfer( + &payer.pubkey(), + tip_account, + sol_str_to_lamports(actual_tip_amount.to_string().as_str()).unwrap_or(0), + )); +} +``` + +## 工作原理 + +1. **识别 Node1**: 检查 tip_account 是否属于 Node1 的账户列表 +2. **条件检查**: 只对 Node1 且 `tip_amount < 0.002 SOL` 时才调整 +3. **自动调整**: Node1 的小费金额提升到 `0.002 SOL` +4. **其他保持**: 其他 swqos 使用原始配置的小费金额 +5. **透明处理**: 对上层调用者透明,无需修改配置 + +## 示例 + +### Node1: 配置的小费 < 0.002 SOL +```yaml +# config/app.prod.yaml +trading: + gas_fee: + global_buy_tip: 0.001 # 0.001 SOL(低于 Node1 最小值) +``` + +**Node1 实际执行**: 自动调整为 `0.002 SOL` +**其他 swqos 实际执行**: 保持 `0.001 SOL`(不调整) + +### Node1: 配置的小费 >= 0.002 SOL +```yaml +# config/app.prod.yaml +trading: + gas_fee: + global_buy_tip: 0.005 # 0.005 SOL(高于 Node1 最小值) +``` + +**Node1 实际执行**: 使用 `0.005 SOL`(不调整) +**其他 swqos 实际执行**: 使用 `0.005 SOL`(不调整) + +## 影响范围 + +这个修改**只影响使用 Node1** 的交易: + +- ✅ Node1 买入交易(小费 < 0.002 时自动调整) +- ✅ Node1 卖出交易(小费 < 0.002 时自动调整) +- ❌ 其他 swqos 交易(保持原始小费金额) + +## 日志示例 + +当小费金额被自动调整时,交易仍会正常执行,不会有额外日志输出。 + +这是因为调整是在 SDK 内部完成的,对调用者完全透明。 + +## 注意事项 + +1. **最小值固定**: `MIN_TIP_AMOUNT = 0.002 SOL` 是硬编码的常量 +2. **只增不减**: 只会向上调整小费,不会减少配置的小费金额 +3. **Node1 专用**: 这个限制是 Node1 节点的要求 + +## 如果需要修改最小值 + +如果将来 Node1 调整最小小费要求,只需修改: + +```rust +// 修改这个常量即可 +const MIN_TIP_AMOUNT: f64 = 0.002; // 改为新的最小值 +``` + +**文件位置**: `sol-trade-sdk/src/trading/common/transaction_builder.rs:49` + +## 相关配置 + +主项目配置文件中的小费设置: + +```yaml +# config/app.prod.yaml +trading: + gas_fee: + global_buy_tip: 0.001 # 买入小费(Node1 会自动调整为 0.002) + global_sell_tip: 0.0001 # 卖出小费(Node1 会自动调整为 0.002) +``` + +**建议**: +- 如果主要使用 Node1,可以直接配置为 `>= 0.002 SOL` +- 如果混合使用多个 swqos,配置任意值即可,SDK 会智能处理 diff --git a/src/common/fast_fn.rs b/src/common/fast_fn.rs index 23f7cf2..48bc5f4 100644 --- a/src/common/fast_fn.rs +++ b/src/common/fast_fn.rs @@ -100,7 +100,7 @@ pub fn _create_associated_token_account_idempotent_fast( }; // Only use seed if the mint address is not wSOL or SOL - // token 2022 测试不成功(TODO) + // 🔧 修复:Token-2022 也支持 seed 方式(白名单方式更安全) if use_seed && !mint.eq(&crate::constants::WSOL_TOKEN_ACCOUNT) && !mint.eq(&crate::constants::SOL_TOKEN_ACCOUNT) @@ -236,7 +236,7 @@ fn _get_associated_token_address_with_program_id_fast( // Slow path: compute new ATA // Only use seed if the token mint address is not wSOL or SOL - // token 2022 测试不成功(TODO) + // 🔧 修复:Token-2022 也支持 seed 方式(白名单方式更安全) let ata = if use_seed && !token_mint_address.eq(&crate::constants::WSOL_TOKEN_ACCOUNT) && !token_mint_address.eq(&crate::constants::SOL_TOKEN_ACCOUNT) diff --git a/src/common/seed.rs b/src/common/seed.rs index de22f78..b9481c5 100644 --- a/src/common/seed.rs +++ b/src/common/seed.rs @@ -74,9 +74,12 @@ pub fn create_associated_token_account_use_seed( }; } let seed = unsafe { std::str::from_utf8_unchecked(&buf) }; + // 🔧 修复:使用传入的 token_program 生成地址(支持 Token 和 Token-2022) + // 买入和卖出只要都使用事件中的 token_program,地址自然一致 let ata_like = Pubkey::create_with_seed(payer, seed, token_program)?; let len = 165; + // 但账户的 owner 仍然使用正确的 token_program(Token 或 Token-2022) let create_acc = create_account_with_seed(payer, &ata_like, owner, seed, rent, len, token_program); @@ -106,13 +109,9 @@ pub fn get_associated_token_address_with_program_id_use_seed( _ => b'a' + (nibble - 10), }; } - let is_2022_token = token_program_id == &crate::constants::TOKEN_PROGRAM_2022; let seed = unsafe { std::str::from_utf8_unchecked(&buf) }; - let token_program = if is_2022_token { - &crate::constants::TOKEN_PROGRAM_2022 - } else { - &crate::constants::TOKEN_PROGRAM - }; - let ata_like = Pubkey::create_with_seed(wallet_address, seed, token_program)?; + // 🔧 修复:使用传入的 token_program_id 生成地址(支持 Token 和 Token-2022) + // 买入和卖出只要都使用事件中的 token_program_id,地址自然一致 + let ata_like = Pubkey::create_with_seed(wallet_address, seed, token_program_id)?; Ok(ata_like) } diff --git a/src/instruction/pumpswap.rs b/src/instruction/pumpswap.rs index 6cb9a88..eaa87cc 100755 --- a/src/instruction/pumpswap.rs +++ b/src/instruction/pumpswap.rs @@ -200,11 +200,13 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { data[16..24].copy_from_slice(&token_amount.to_le_bytes()); } - instructions.push(Instruction { + let buy_instruction = Instruction { program_id: accounts::AMM_PROGRAM, - accounts, + accounts: accounts.clone(), data: data.to_vec(), - }); + }; + + instructions.push(buy_instruction); if close_wsol_ata { // Close wSOL ATA account, reclaim rent instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey())); @@ -375,11 +377,13 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { data[16..24].copy_from_slice(&token_amount.to_le_bytes()); } - instructions.push(Instruction { + let sell_instruction = Instruction { program_id: accounts::AMM_PROGRAM, - accounts, + accounts: accounts.clone(), data: data.to_vec(), - }); + }; + + instructions.push(sell_instruction); if close_wsol_ata { instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey())); diff --git a/src/instruction/utils/pumpswap.rs b/src/instruction/utils/pumpswap.rs index 31409df..17977c7 100644 --- a/src/instruction/utils/pumpswap.rs +++ b/src/instruction/utils/pumpswap.rs @@ -230,10 +230,17 @@ pub async fn find_by_base_mint( if accounts.is_empty() { return Err(anyhow!("No pool found for mint {}", base_mint)); } + let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts let mut pools: Vec<_> = accounts .into_iter() .filter_map(|(addr, acc)| pool_decode(&acc.data).map(|pool| (addr, pool))) .collect(); + + // 🔧 修复:检查过滤后的 pools 是否为空(accounts 可能不为空但解码全部失败) + if pools.is_empty() { + return Err(anyhow!("No valid pool decoded for mint {} (found {} accounts but all decode failed)", base_mint, accounts_count)); + } + pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply)); let (address, pool) = pools[0].clone(); Ok((address, pool)) @@ -266,10 +273,17 @@ pub async fn find_by_quote_mint( if accounts.is_empty() { return Err(anyhow!("No pool found for mint {}", quote_mint)); } + let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts let mut pools: Vec<_> = accounts .into_iter() .filter_map(|(addr, acc)| pool_decode(&acc.data).map(|pool| (addr, pool))) .collect(); + + // 🔧 修复:检查过滤后的 pools 是否为空(accounts 可能不为空但解码全部失败) + if pools.is_empty() { + return Err(anyhow!("No valid pool decoded for quote_mint {} (found {} accounts but all decode failed)", quote_mint, accounts_count)); + } + pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply)); let (address, pool) = pools[0].clone(); Ok((address, pool)) diff --git a/src/swqos/common.rs b/src/swqos/common.rs index fca5de3..882bf79 100755 --- a/src/swqos/common.rs +++ b/src/swqos/common.rs @@ -28,7 +28,7 @@ impl FormatBase64VersionedTransaction for VersionedTransaction { } pub async fn poll_transaction_confirmation(rpc: &SolanaRpcClient, txt_sig: Signature) -> Result { - let timeout: Duration = Duration::from_secs(10); + let timeout: Duration = Duration::from_secs(15); // 🔧 增加到15秒,避免网络拥堵时超时 let interval: Duration = Duration::from_millis(1000); let start: Instant = Instant::now(); diff --git a/src/trading/common/transaction_builder.rs b/src/trading/common/transaction_builder.rs index 5224a07..a384af5 100755 --- a/src/trading/common/transaction_builder.rs +++ b/src/trading/common/transaction_builder.rs @@ -11,6 +11,7 @@ use super::{ }; use crate::{ common::{nonce_cache::DurableNonceInfo, SolanaRpcClient}, + constants::swqos::NODE1_TIP_ACCOUNTS, trading::{MiddlewareManager, core::transaction_pool::{acquire_builder, release_builder}}, }; @@ -45,10 +46,24 @@ pub async fn build_transaction( // Add tip transfer instruction if with_tip && tip_amount > 0.0 { + // 🔧 Node1 最小小费金额限制:0.002 SOL(仅限 Node1) + const MIN_TIP_AMOUNT: f64 = 0.002; + + // 检查是否是 Node1 的 tip_account + let is_node1 = NODE1_TIP_ACCOUNTS.iter().any(|&account| account == *tip_account); + + let actual_tip_amount = if is_node1 && tip_amount < MIN_TIP_AMOUNT { + // Node1 要求最小 0.002 SOL + MIN_TIP_AMOUNT + } else { + // 其他 swqos 使用原始金额 + tip_amount + }; + instructions.push(transfer( &payer.pubkey(), tip_account, - sol_str_to_lamports(tip_amount.to_string().as_str()).unwrap_or(0), + sol_str_to_lamports(actual_tip_amount.to_string().as_str()).unwrap_or(0), )); }