feat: pass is_cashback_coin from events; PumpFun examples use sol-parser-sdk only

- PumpFunParams/BondingCurve: from_trade/from_dev_trade take is_cashback_coin parameter
- pumpfun_copy_trading and pumpfun_sniper_trading use sol-parser-sdk for gRPC, pass e.is_cashback_coin
- address_lookup/nonce_cache call sites updated; README EN/CN Cashback and example notes

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Wood
2026-02-19 01:31:50 +08:00
parent 9a8e3ffb2d
commit c27e479659
10 changed files with 276 additions and 237 deletions
+13 -9
View File
@@ -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<SwqosConfig> = 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. CreateEvents `is_cashback_enabled` or BondingCurves `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
+13 -9
View File
@@ -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<SwqosConfig> = 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 保护服务
+2 -1
View File
@@ -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,
+1
View File
@@ -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,
+1 -1
View File
@@ -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"] }
+115 -130
View File
@@ -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<dyn std::error::Error>> {
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<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
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<SolanaTrade> {
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<SwqosConfig> = 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::<u64>().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(())
}
+1 -1
View File
@@ -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"] }
+114 -84
View File
@@ -1,3 +1,15 @@
//! PumpFun 狙击示例(仅使用 sol-parser-sdk 订阅 gRPC 事件)
//!
//! 监听创建者首次买入(Create 后同笔/首笔 Buyis_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<dyn std::error::Error>> {
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<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
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<SolanaTrade> {
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<SwqosConfig> = 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_trademax_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::<u64>().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(())
}
+8 -2
View File
@@ -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,
}
}
+8
View File
@@ -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<bool>,
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<bool>,
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),