diff --git a/README.md b/README.md index c8dff99..baf51f2 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ - [📋 Example Usage](#-example-usage) - [⚡ Trading Parameters](#-trading-parameters) - [📊 Usage Examples Summary Table](#-usage-examples-summary-table) - - [⚙️ SWQOS Service Configuration](#️-swqos-service-configuration) + - [⚙️ SWQoS Service Configuration](#️-swqos-service-configuration) - [🔧 Middleware System](#-middleware-system) - [🔍 Address Lookup Tables](#-address-lookup-tables) - [🔍 Nonce Cache](#-nonce-cache) @@ -72,7 +72,7 @@ 8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail 9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations 10. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution -11. **Shared Infrastructure**: Share expensive RPC and SWQOS clients across multiple wallets for reduced resource usage +11. **Shared Infrastructure**: Share expensive RPC and SWQoS clients across multiple wallets for reduced resource usage ## 📦 Installation @@ -114,7 +114,7 @@ let payer = Keypair::from_base58_string("use_your_payer_keypair_here"); // RPC URL let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); let commitment = CommitmentConfig::processed(); -// Multiple SWQOS services can be configured +// Multiple SWQoS services can be configured let swqos_configs: Vec = vec![ SwqosConfig::Default(rpc_url.clone()), SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None), @@ -223,16 +223,16 @@ Please ensure that the parameters your trading logic depends on are available in | Seed trading example | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) | | Gas fee strategy example | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) | -### ⚙️ SWQOS Service Configuration +### ⚙️ SWQoS Service Configuration -When configuring SWQOS services, note the different parameter requirements for each service: +When configuring SWQoS services, note the different parameter requirements for each service: - **Jito**: The first parameter is UUID (if no UUID, pass an empty string `""`) - **Other MEV services**: The first parameter is the API Token #### Custom URL Support -Each SWQOS service now supports an optional custom URL parameter: +Each SWQoS service now supports an optional custom URL parameter: ```rust // Using custom URL (third parameter) @@ -280,10 +280,14 @@ Use Durable Nonce to implement transaction replay protection and optimize transa ## 💰 Cashback Support (PumpFun / PumpSwap) -PumpFun and PumpSwap support **cashback** for eligible tokens: part of the trading fee can be returned to the user. When you use this SDK to execute `buy` or `sell`, the transaction is submitted as usual; if the token has cashback enabled, the protocol will credit cashback according to its rules. +PumpFun and PumpSwap support **cashback** for eligible tokens: part of the trading fee can be returned to the user. The SDK **must know** whether the token has cashback enabled so that buy/sell instructions include the correct accounts (e.g. `UserVolumeAccumulator` as remaining account for cashback coins). -- **Trading**: No change to your code—use `TradeBuyParams` / `TradeSellParams` as normal. Cashback is handled on-chain. -- **Event parsing**: If you consume chain events (e.g. via [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk)), trade events can expose cashback-related fields (e.g. `cashback_fee_basis_points`, `cashback`, `is_cashback_enabled`) so your strategy or analytics can be cashback-aware. +- **When params come from RPC**: If you use `PumpFunParams::from_mint_by_rpc` or `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc`, the SDK reads `is_cashback_coin` from chain—no extra step. +- **When params come from event/parser**: If you build params from trade events (e.g. [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk)), you **must** pass the cashback flag into the SDK: + - **PumpFun**: `PumpFunParams::from_trade(..., is_cashback_coin)` and `PumpFunParams::from_dev_trade(..., is_cashback_coin)` take an `is_cashback_coin` parameter. Set it from the parsed event (e.g. CreateEvent’s `is_cashback_enabled` or BondingCurve’s `is_cashback_coin`). + - **PumpSwap**: `PumpSwapParams` has a field `is_cashback_coin`. When constructing params manually (e.g. from pool/trade events), set it from the parsed pool or event data. +- The **pumpfun_copy_trading** and **pumpfun_sniper_trading** examples use sol-parser-sdk for gRPC subscription and pass `e.is_cashback_coin` when building params. +- **Claim**: Use `client.claim_cashback_pumpfun()` and `client.claim_cashback_pumpswap(...)` to claim accumulated cashback. ## 🛡️ MEV Protection Services diff --git a/README_CN.md b/README_CN.md index e7d99e3..9d215dd 100755 --- a/README_CN.md +++ b/README_CN.md @@ -47,7 +47,7 @@ - [📋 使用示例](#-使用示例) - [⚡ 交易参数](#-交易参数) - [📊 使用示例汇总表格](#-使用示例汇总表格) - - [⚙️ SWQOS 服务配置说明](#️-swqos-服务配置说明) + - [⚙️ SWQoS 服务配置说明](#️-swqos-服务配置说明) - [🔧 中间件系统说明](#-中间件系统说明) - [🔍 地址查找表](#-地址查找表) - [🔍 Nonce 缓存](#-nonce-缓存) @@ -72,7 +72,7 @@ 8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败 9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作 10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除 -11. **共享基础设施**: 多钱包可共享同一套 RPC 与 SWQOS 客户端,降低资源占用 +11. **共享基础设施**: 多钱包可共享同一套 RPC 与 SWQoS 客户端,降低资源占用 ## 📦 安装 @@ -114,7 +114,7 @@ let payer = Keypair::from_base58_string("use_your_payer_keypair_here"); // RPC 地址 let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); let commitment = CommitmentConfig::processed(); -// 可配置多个 SWQOS 服务 +// 可配置多个 SWQoS 服务 let swqos_configs: Vec = vec![ SwqosConfig::Default(rpc_url.clone()), SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None), @@ -222,16 +222,16 @@ client.buy(buy_params).await?; | Seed 优化交易示例 | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) | | Gas费用策略示例 | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) | -### ⚙️ SWQOS 服务配置说明 +### ⚙️ SWQoS 服务配置说明 -在配置 SWQOS 服务时,需要注意不同服务的参数要求: +在配置 SWQoS 服务时,需要注意不同服务的参数要求: - **Jito**: 第一个参数为 UUID(如无 UUID 请传入空字符串 `""`) - 其他的MEV服务,第一个参数为 API Token #### 自定义 URL 支持 -每个 SWQOS 服务现在都支持可选的自定义 URL 参数: +每个 SWQoS 服务现在都支持可选的自定义 URL 参数: ```rust // 使用自定义 URL(第三个参数) @@ -279,10 +279,14 @@ let middleware_manager = MiddlewareManager::new() ## 💰 Cashback 支持(PumpFun / PumpSwap) -PumpFun 与 PumpSwap 支持**返现(Cashback)**:部分手续费可返还给用户。使用本 SDK 执行 `buy` / `sell` 时,按正常方式提交交易即可;若代币已开启返现,协议会按规则自动结算返现。 +PumpFun 与 PumpSwap 支持**返现(Cashback)**:部分手续费可返还给用户。SDK **必须知道**该代币是否开启返现,才能为 buy/sell 指令传入正确的账户(例如返现代币需要把 `UserVolumeAccumulator` 作为 remaining account)。 -- **交易侧**:无需改代码,照常使用 `TradeBuyParams` / `TradeSellParams`,返现由链上处理。 -- **事件解析**:若通过事件驱动(如 [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk))消费链上事件,可获取返现相关字段(如 `cashback_fee_basis_points`、`cashback`、`is_cashback_enabled`),便于策略或统计与返现逻辑结合。 +- **参数来自 RPC 时**:使用 `PumpFunParams::from_mint_by_rpc` 或 `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc` 时,SDK 会从链上读取 `is_cashback_coin`,无需额外传入。 +- **参数来自事件/解析器时**:若根据交易事件(如 [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk))构建参数,**必须**把返现标志传给 SDK: + - **PumpFun**:`PumpFunParams::from_trade(..., is_cashback_coin)` 与 `PumpFunParams::from_dev_trade(..., is_cashback_coin)` 最后一个参数为 `is_cashback_coin`。从解析出的事件传入(如 sol-parser-sdk 的 `PumpFunTradeEvent.is_cashback_coin`)。 + - **PumpSwap**:`PumpSwapParams` 有字段 `is_cashback_coin`。手动构造参数(如从池/交易事件)时,从解析到的池或事件数据中设置该字段。 +- **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`。 +- **领取返现**:使用 `client.claim_cashback_pumpfun()` 和 `client.claim_cashback_pumpswap(...)` 领取累计的返现。 ## 🛡️ MEV 保护服务 diff --git a/examples/address_lookup/src/main.rs b/examples/address_lookup/src/main.rs index 341bb60..869a856 100644 --- a/examples/address_lookup/src/main.rs +++ b/examples/address_lookup/src/main.rs @@ -143,13 +143,14 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul trade_info.mint, trade_info.creator, trade_info.creator_vault, - trade_info.virtual_sol_reserves, + trade_info.virtual_token_reserves, trade_info.virtual_sol_reserves, trade_info.real_token_reserves, trade_info.real_sol_reserves, None, trade_info.fee_recipient, trade_info.token_program, + false, // is_cashback_coin: set from event/parser when available )), address_lookup_table_account: address_lookup_table_account, wait_transaction_confirmed: true, diff --git a/examples/nonce_cache/src/main.rs b/examples/nonce_cache/src/main.rs index df531d6..8727fc2 100644 --- a/examples/nonce_cache/src/main.rs +++ b/examples/nonce_cache/src/main.rs @@ -150,6 +150,7 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul None, trade_info.fee_recipient, trade_info.token_program, + false, // is_cashback_coin: set from event/parser when available )), address_lookup_table_account: None, wait_transaction_confirmed: true, diff --git a/examples/pumpfun_copy_trading/Cargo.toml b/examples/pumpfun_copy_trading/Cargo.toml index 9ced16a..a89fe30 100644 --- a/examples/pumpfun_copy_trading/Cargo.toml +++ b/examples/pumpfun_copy_trading/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] sol-trade-sdk = { path = "../.." } -solana-streamer-sdk = "0.5.0" +sol-parser-sdk = { path = "../../../sol-parser-sdk" } solana-sdk = "3.0.0" solana-commitment-config = { version = "3.0.0", features = ["serde"] } tokio = { version = "1", features = ["full"] } diff --git a/examples/pumpfun_copy_trading/src/main.rs b/examples/pumpfun_copy_trading/src/main.rs index 9603131..532d910 100644 --- a/examples/pumpfun_copy_trading/src/main.rs +++ b/examples/pumpfun_copy_trading/src/main.rs @@ -1,8 +1,14 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; +//! PumpFun 跟单示例(仅使用 sol-parser-sdk 订阅 gRPC 事件) +//! +//! 收到 PumpFun 买卖事件后,用事件中的参数(含 is_cashback_coin)构造交易并执行一次买+卖。 +use std::sync::{atomic::{AtomicBool, Ordering}, Arc}; + +use sol_parser_sdk::grpc::{ + AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol, + TransactionFilter, YellowstoneGrpc, +}; +use sol_parser_sdk::DexEvent; use sol_trade_sdk::common::{ fast_fn::get_associated_token_address_with_program_id_fast_use_seed, TradeConfig, }; @@ -16,143 +22,128 @@ use sol_trade_sdk::{ use solana_commitment_config::CommitmentConfig; use solana_sdk::signature::Keypair; use solana_sdk::signer::Signer; -use solana_streamer_sdk::match_event; -use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter; -use solana_streamer_sdk::streaming::event_parser::common::EventType; -use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::parser::PUMPFUN_PROGRAM_ID; -use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent; -use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent}; -use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter}; -use solana_streamer_sdk::streaming::YellowstoneGrpc; -// Global static flag to ensure transaction is executed only once static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false); #[tokio::main] async fn main() -> Result<(), Box> { - println!("Subscribing to GRPC events..."); + let _ = rustls::crypto::ring::default_provider().install_default(); + println!("PumpFun 跟单示例(sol-parser-sdk gRPC)..."); - let grpc = YellowstoneGrpc::new( - "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), - None, - )?; - - let callback = create_event_callback(); - let protocols = vec![Protocol::PumpFun]; - // Filter accounts - let account_include = vec![ - PUMPFUN_PROGRAM_ID.to_string(), // Listen to pumpfun program ID - ]; - let account_exclude = vec![]; - let account_required = vec![]; - - // Listen to transaction data - let transaction_filter = TransactionFilter { - account_include: account_include.clone(), - account_exclude, - account_required, + let config = ClientConfig { + enable_metrics: false, + connection_timeout_ms: 10000, + request_timeout_ms: 30000, + enable_tls: true, + order_mode: OrderMode::Unordered, + ..Default::default() }; - // Listen to account data belonging to owner programs -> account event monitoring - let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] }; + let grpc_endpoint = std::env::var("GRPC_ENDPOINT") + .unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()); + let grpc = YellowstoneGrpc::new_with_config( + grpc_endpoint.clone(), + std::env::var("GRPC_AUTH_TOKEN").ok(), + config, + )?; - // listen to specific event type - let event_type_filter = - EventTypeFilter { include: vec![EventType::PumpFunBuy, EventType::PumpFunSell] }; + let protocols = vec![Protocol::PumpFun]; + let transaction_filter = TransactionFilter::for_protocols(&protocols); + let account_filter = AccountFilter::for_protocols(&protocols); + let event_filter = EventTypeFilter::include_only(vec![ + EventType::PumpFunBuy, + EventType::PumpFunSell, + EventType::PumpFunBuyExactSolIn, + EventType::PumpFunTrade, + ]); - grpc.subscribe_events_immediate( - protocols, - None, - vec![transaction_filter], - vec![account_filter], - Some(event_type_filter), - None, - callback, - ) - .await?; + let queue = grpc + .subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter)) + .await?; + + println!("订阅已启动,等待一条 PumpFun 交易后执行跟单(仅一次)...\n"); + + loop { + if let Some(event) = queue.pop() { + let run = match &event { + DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => { + if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) { + Some(e.clone()) + } else { + None + } + } + DexEvent::PumpFunTrade(e) => { + if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) { + Some(e.clone()) + } else { + None + } + } + _ => None, + }; + if let Some(e) = run { + tokio::spawn(async move { + if let Err(err) = pumpfun_copy_trade(e).await { + eprintln!("跟单执行错误: {:?}", err); + std::process::exit(1); + } + std::process::exit(0); + }); + break; + } + } else { + tokio::time::sleep(tokio::time::Duration::from_millis(5)).await; + } + } tokio::signal::ctrl_c().await?; - Ok(()) } -/// Create an event callback function that handles different types of events -fn create_event_callback() -> impl Fn(Box) { - |event: Box| { - match_event!(event, { - PumpFunTradeEvent => |e: PumpFunTradeEvent| { - // Test code, only test one transaction - if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) { - let event_clone = e.clone(); - tokio::spawn(async move { - if let Err(err) = pumpfun_copy_trade_with_grpc(event_clone).await { - eprintln!("Error in copy trade: {:?}", err); - std::process::exit(0); - } - }); - } - }, - }); - } -} - -/// Create SolanaTrade client -/// Initializes a new SolanaTrade client with configuration async fn create_solana_trade_client() -> AnyResult { - println!("🚀 Initializing SolanaTrade client..."); let payer = Keypair::from_base58_string("use_your_payer_keypair_here"); - let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); + let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); - let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; - println!("✅ SolanaTrade client initialized successfully!"); - Ok(solana_trade) + Ok(SolanaTrade::new(Arc::new(payer), trade_config).await) } -/// PumpFun sniper trade -/// This function demonstrates how to snipe a new token from a PumpFun trade event -async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResult<()> { - println!("Testing PumpFun trading..."); - +async fn pumpfun_copy_trade( + e: sol_parser_sdk::core::events::PumpFunTradeEvent, +) -> AnyResult<()> { let client = create_solana_trade_client().await?; - let mint_pubkey = trade_info.mint; - let slippage_basis_points = Some(100); + let mint_pubkey = e.mint; + let slippage_basis_points = Some(100u64); let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?; let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new(); - gas_fee_strategy.set_global_fee_strategy( - 150000, - 150000, - 500000, - 500000, - 0.001, - 0.001, - ); + gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001); - // Buy tokens - println!("Buying tokens from PumpFun..."); - let buy_sol_amount = 100_000; + // 买入:使用事件参数,含 is_cashback_coin(来自 sol-parser-sdk 解析) + let buy_sol_amount = 100_000u64; let buy_params = sol_trade_sdk::TradeBuyParams { dex_type: DexType::PumpFun, input_token_type: TradeTokenType::SOL, mint: mint_pubkey, input_token_amount: buy_sol_amount, - slippage_basis_points: slippage_basis_points, + slippage_basis_points, recent_blockhash: Some(recent_blockhash), extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade( - trade_info.bonding_curve, - trade_info.associated_bonding_curve, - trade_info.mint, - trade_info.creator, - trade_info.creator_vault, - trade_info.virtual_token_reserves, - trade_info.virtual_sol_reserves, - trade_info.real_token_reserves, - trade_info.real_sol_reserves, + e.bonding_curve, + e.associated_bonding_curve, + e.mint, + e.creator, + e.creator_vault, + e.virtual_token_reserves, + e.virtual_sol_reserves, + e.real_token_reserves, + e.real_sol_reserves, None, - trade_info.fee_recipient, - trade_info.token_program, + e.fee_recipient, + e.token_program, + e.is_cashback_coin, )), address_lookup_table_account: None, wait_transaction_confirmed: true, @@ -167,43 +158,40 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul }; client.buy(buy_params).await?; - // Sell tokens - println!("Selling tokens from PumpFun..."); - + // 卖出:查询余额后卖出,同样传入 is_cashback_coin let rpc = client.infrastructure.rpc.clone(); let payer = client.payer.pubkey(); let account = get_associated_token_address_with_program_id_fast_use_seed( &payer, &mint_pubkey, - &trade_info.token_program, + &e.token_program, client.use_seed_optimize, ); let balance = rpc.get_token_account_balance(&account).await?; - println!("Balance: {:?}", balance); let amount_token = balance.amount.parse::().unwrap(); - println!("Selling {} tokens", amount_token); let sell_params = sol_trade_sdk::TradeSellParams { dex_type: DexType::PumpFun, output_token_type: TradeTokenType::SOL, mint: mint_pubkey, input_token_amount: amount_token, - slippage_basis_points: slippage_basis_points, + slippage_basis_points, recent_blockhash: Some(recent_blockhash), with_tip: false, extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade( - trade_info.bonding_curve, - trade_info.associated_bonding_curve, - trade_info.mint, - trade_info.creator, - trade_info.creator_vault, - trade_info.virtual_token_reserves, - trade_info.virtual_sol_reserves, - trade_info.real_token_reserves, - trade_info.real_sol_reserves, + e.bonding_curve, + e.associated_bonding_curve, + e.mint, + e.creator, + e.creator_vault, + e.virtual_token_reserves, + e.virtual_sol_reserves, + e.real_token_reserves, + e.real_sol_reserves, Some(true), - trade_info.fee_recipient, - trade_info.token_program, + e.fee_recipient, + e.token_program, + e.is_cashback_coin, )), address_lookup_table_account: None, wait_transaction_confirmed: true, @@ -212,14 +200,11 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul close_mint_token_ata: false, durable_nonce: None, fixed_output_token_amount: None, - gas_fee_strategy: gas_fee_strategy, + gas_fee_strategy, simulate: false, }; client.sell(sell_params).await?; - // PumpFunParams can also be set as PumpFunParams::immediate_sell(creator_vault, close_token_account_when_sell) - // creator_vault can be obtained from the trade event - - // Exit program - std::process::exit(0); + println!("跟单一次买+卖完成"); + Ok(()) } diff --git a/examples/pumpfun_sniper_trading/Cargo.toml b/examples/pumpfun_sniper_trading/Cargo.toml index 0dbb2c4..c72830b 100644 --- a/examples/pumpfun_sniper_trading/Cargo.toml +++ b/examples/pumpfun_sniper_trading/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] sol-trade-sdk = { path = "../.." } -solana-streamer-sdk = "0.5.0" +sol-parser-sdk = { path = "../../../sol-parser-sdk" } solana-sdk = "3.0.0" solana-commitment-config = { version = "3.0.0", features = ["serde"] } tokio = { version = "1", features = ["full"] } diff --git a/examples/pumpfun_sniper_trading/src/main.rs b/examples/pumpfun_sniper_trading/src/main.rs index 78e5bcf..8e2e732 100644 --- a/examples/pumpfun_sniper_trading/src/main.rs +++ b/examples/pumpfun_sniper_trading/src/main.rs @@ -1,3 +1,15 @@ +//! PumpFun 狙击示例(仅使用 sol-parser-sdk 订阅 gRPC 事件) +//! +//! 监听创建者首次买入(Create 后同笔/首笔 Buy,is_created_buy == true), +//! 用事件参数(含 is_cashback_coin)构造 from_dev_trade 并执行一次买+卖。 + +use std::sync::{atomic::{AtomicBool, Ordering}, Arc}; + +use sol_parser_sdk::grpc::{ + AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol, + TransactionFilter, YellowstoneGrpc, +}; +use sol_parser_sdk::DexEvent; use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address; use sol_trade_sdk::common::TradeConfig; use sol_trade_sdk::TradeTokenType; @@ -10,108 +22,120 @@ use sol_trade_sdk::{ use solana_commitment_config::CommitmentConfig; use solana_sdk::signature::Keypair; use solana_sdk::signer::Signer; -use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter; -use solana_streamer_sdk::streaming::event_parser::common::EventType; -use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent; -use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent}; -use solana_streamer_sdk::{match_event, streaming::ShredStreamGrpc}; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; -/// Atomic flag to ensure the sniper trade is executed only once static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false); -/// Main entry point - subscribes to PumpFun events and executes sniper trades on token creation #[tokio::main] async fn main() -> Result<(), Box> { - println!("Subscribing to ShredStream events..."); - let shred_stream = ShredStreamGrpc::new("use_your_shred_stream_url_here".to_string()).await?; - let callback = create_event_callback(); - let protocols = vec![Protocol::PumpFun]; - let event_type_filter = EventTypeFilter { - include: vec![EventType::PumpFunBuy, EventType::PumpFunSell, EventType::PumpFunCreateToken], + let _ = rustls::crypto::ring::default_provider().install_default(); + println!("PumpFun 狙击示例(sol-parser-sdk gRPC)..."); + + let config = ClientConfig { + enable_metrics: false, + connection_timeout_ms: 10000, + request_timeout_ms: 30000, + enable_tls: true, + order_mode: OrderMode::Unordered, + ..Default::default() }; - println!("Starting to listen for events, press Ctrl+C to stop..."); - shred_stream.shredstream_subscribe(protocols, None, Some(event_type_filter), callback).await?; + + let grpc_endpoint = std::env::var("GRPC_ENDPOINT") + .unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string()); + let grpc = YellowstoneGrpc::new_with_config( + grpc_endpoint, + std::env::var("GRPC_AUTH_TOKEN").ok(), + config, + )?; + + let protocols = vec![Protocol::PumpFun]; + let transaction_filter = TransactionFilter::for_protocols(&protocols); + let account_filter = AccountFilter::for_protocols(&protocols); + let event_filter = EventTypeFilter::include_only(vec![ + EventType::PumpFunCreate, + EventType::PumpFunBuy, + EventType::PumpFunBuyExactSolIn, + ]); + + let queue = grpc + .subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter)) + .await?; + + println!("订阅已启动,等待创建者首次买入(is_created_buy)后执行狙击(仅一次)...\n"); + + loop { + if let Some(event) = queue.pop() { + let run = match &event { + DexEvent::PumpFunBuy(e) | DexEvent::PumpFunBuyExactSolIn(e) => { + if e.is_created_buy && !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) { + Some(e.clone()) + } else { + None + } + } + _ => None, + }; + if let Some(e) = run { + tokio::spawn(async move { + if let Err(err) = pumpfun_sniper_trade(e).await { + eprintln!("狙击执行错误: {:?}", err); + std::process::exit(1); + } + std::process::exit(0); + }); + break; + } + } else { + tokio::time::sleep(tokio::time::Duration::from_millis(5)).await; + } + } + tokio::signal::ctrl_c().await?; Ok(()) } -/// Create an event callback function that handles different types of events -fn create_event_callback() -> impl Fn(Box) { - |event: Box| { - match_event!(event, { - PumpFunTradeEvent => |e: PumpFunTradeEvent| { - // Only process developer token creation events - if !e.is_dev_create_token_trade { - return; - } - // Ensure we only execute the trade once using atomic compare-and-swap - if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) { - let event_clone = e.clone(); - // Spawn a new task to handle the trading operation - tokio::spawn(async move { - if let Err(err) = pumpfun_sniper_trade_with_shreds(event_clone).await { - eprintln!("Error in copy trade: {:?}", err); - std::process::exit(0); - } - }); - } - }, - }); - } -} - -/// Create SolanaTrade client -/// Initializes a new SolanaTrade client with configuration async fn create_solana_trade_client() -> AnyResult { - println!("🚀 Initializing SolanaTrade client..."); let payer = Keypair::from_base58_string("use_your_payer_keypair_here"); - let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); + let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string()); let commitment = CommitmentConfig::confirmed(); let swqos_configs: Vec = vec![SwqosConfig::Default(rpc_url.clone())]; let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); - let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; - println!("✅ SolanaTrade client initialized successfully!"); - Ok(solana_trade) + Ok(SolanaTrade::new(Arc::new(payer), trade_config).await) } -/// Execute PumpFun sniper trading strategy based on received token creation event -/// This function buys tokens immediately after creation and then sells all tokens -async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyResult<()> { - println!("Testing PumpFun trading..."); - +async fn pumpfun_sniper_trade( + e: sol_parser_sdk::core::events::PumpFunTradeEvent, +) -> AnyResult<()> { let client = create_solana_trade_client().await?; - let mint_pubkey = trade_info.mint; - let slippage_basis_points = Some(300); + let mint_pubkey = e.mint; + let slippage_basis_points = Some(300u64); let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?; let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new(); gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001); - // Buy tokens - println!("Buying tokens from PumpFun..."); - let buy_sol_amount = 100_000; + // 创建者首次买入:用 from_dev_trade,max_sol_cost 用事件中的 sol_amount(可酌情加滑点) + let buy_sol_amount = 100_000u64; + let max_sol_cost = e.sol_amount.saturating_add(e.sol_amount / 10); // 约 +10% 作为上限 + let buy_params = sol_trade_sdk::TradeBuyParams { dex_type: DexType::PumpFun, input_token_type: TradeTokenType::SOL, mint: mint_pubkey, input_token_amount: buy_sol_amount, - slippage_basis_points: slippage_basis_points, + slippage_basis_points, recent_blockhash: Some(recent_blockhash), extension_params: DexParamEnum::PumpFun(PumpFunParams::from_dev_trade( - trade_info.mint, - trade_info.token_amount, - trade_info.max_sol_cost, - trade_info.creator, - trade_info.bonding_curve, - trade_info.associated_bonding_curve, - trade_info.creator_vault, + e.mint, + e.token_amount, + max_sol_cost, + e.creator, + e.bonding_curve, + e.associated_bonding_curve, + e.creator_vault, None, - trade_info.fee_recipient, - trade_info.token_program, + e.fee_recipient, + e.token_program, + e.is_cashback_coin, )), address_lookup_table_account: None, wait_transaction_confirmed: true, @@ -126,26 +150,35 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR }; client.buy(buy_params).await?; - // Sell tokens - println!("Selling tokens from PumpFun..."); - let rpc = client.infrastructure.rpc.clone(); let payer = client.payer.pubkey(); let account = get_associated_token_address(&payer, &mint_pubkey); let balance = rpc.get_token_account_balance(&account).await?; - println!("Balance: {:?}", balance); let amount_token = balance.amount.parse::().unwrap(); - println!("Selling {} tokens", amount_token); let sell_params = sol_trade_sdk::TradeSellParams { dex_type: DexType::PumpFun, output_token_type: TradeTokenType::SOL, mint: mint_pubkey, input_token_amount: amount_token, - slippage_basis_points: slippage_basis_points, + slippage_basis_points, recent_blockhash: Some(recent_blockhash), with_tip: false, - extension_params: DexParamEnum::PumpFun(PumpFunParams::immediate_sell(trade_info.creator_vault, trade_info.token_program, true)), + extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade( + e.bonding_curve, + e.associated_bonding_curve, + e.mint, + e.creator, + e.creator_vault, + e.virtual_token_reserves, + e.virtual_sol_reserves, + e.real_token_reserves, + e.real_sol_reserves, + Some(true), + e.fee_recipient, + e.token_program, + e.is_cashback_coin, + )), address_lookup_table_account: None, wait_transaction_confirmed: true, create_output_token_ata: true, @@ -153,14 +186,11 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR close_mint_token_ata: false, durable_nonce: None, fixed_output_token_amount: None, - gas_fee_strategy: gas_fee_strategy, + gas_fee_strategy, simulate: false, }; client.sell(sell_params).await?; - // PumpFunParams can also be set as PumpFunParams::immediate_sell(creator_vault, close_token_account_when_sell) - // creator_vault can be obtained from the trade event - - // Exit program after completing the trade - std::process::exit(0); + println!("狙击一次买+卖完成"); + Ok(()) } diff --git a/src/common/bonding_curve.rs b/src/common/bonding_curve.rs index ba397b2..b6785d2 100755 --- a/src/common/bonding_curve.rs +++ b/src/common/bonding_curve.rs @@ -65,6 +65,8 @@ pub struct BondingCurveAccount { } impl BondingCurveAccount { + /// When building from event/parser data (e.g. sol-parser-sdk), pass the token's cashback flag + /// so that sell instructions include the correct remaining accounts. From RPC use `from_mint_by_rpc` instead. pub fn from_dev_trade( bonding_curve: Pubkey, mint: &Pubkey, @@ -72,6 +74,7 @@ impl BondingCurveAccount { dev_sol_amount: u64, creator: Pubkey, is_mayhem_mode: bool, + is_cashback_coin: bool, ) -> Self { let account = if bonding_curve != Pubkey::default() { bonding_curve @@ -89,10 +92,12 @@ impl BondingCurveAccount { complete: false, creator: creator, is_mayhem_mode: is_mayhem_mode, - is_cashback_coin: false, + is_cashback_coin, } } + /// When building from event/parser data (e.g. sol-parser-sdk), pass the token's cashback flag + /// so that sell instructions include the correct remaining accounts. From RPC use `from_mint_by_rpc` instead. pub fn from_trade( bonding_curve: Pubkey, mint: Pubkey, @@ -102,6 +107,7 @@ impl BondingCurveAccount { real_token_reserves: u64, real_sol_reserves: u64, is_mayhem_mode: bool, + is_cashback_coin: bool, ) -> Self { let account = if bonding_curve != Pubkey::default() { bonding_curve @@ -119,7 +125,7 @@ impl BondingCurveAccount { complete: false, creator: creator, is_mayhem_mode: is_mayhem_mode, - is_cashback_coin: false, + is_cashback_coin, } } diff --git a/src/trading/core/params.rs b/src/trading/core/params.rs index dc12594..2e40fb3 100755 --- a/src/trading/core/params.rs +++ b/src/trading/core/params.rs @@ -107,6 +107,8 @@ impl PumpFunParams { } } + /// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event + /// so that sell instructions include the correct remaining accounts for cashback. pub fn from_dev_trade( mint: Pubkey, token_amount: u64, @@ -118,6 +120,7 @@ impl PumpFunParams { close_token_account_when_sell: Option, fee_recipient: Pubkey, token_program: Pubkey, + is_cashback_coin: bool, ) -> Self { let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT; let bonding_curve_account = BondingCurveAccount::from_dev_trade( @@ -127,6 +130,7 @@ impl PumpFunParams { max_sol_cost, creator, is_mayhem_mode, + is_cashback_coin, ); Self { bonding_curve: Arc::new(bonding_curve_account), @@ -137,6 +141,8 @@ impl PumpFunParams { } } + /// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event + /// so that sell instructions include the correct remaining accounts for cashback. pub fn from_trade( bonding_curve: Pubkey, associated_bonding_curve: Pubkey, @@ -150,6 +156,7 @@ impl PumpFunParams { close_token_account_when_sell: Option, fee_recipient: Pubkey, token_program: Pubkey, + is_cashback_coin: bool, ) -> Self { let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT; let bonding_curve = BondingCurveAccount::from_trade( @@ -161,6 +168,7 @@ impl PumpFunParams { real_token_reserves, real_sol_reserves, is_mayhem_mode, + is_cashback_coin, ); Self { bonding_curve: Arc::new(bonding_curve),