fix(pumpswap): harden quotes and low-latency examples

This commit is contained in:
0xfnzero
2026-07-17 03:22:52 +08:00
parent fb1ff176d0
commit dc3d2b8deb
34 changed files with 1133 additions and 972 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.4.14"
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+22 -30
View File
@@ -15,13 +15,12 @@ use sol_trade_sdk::{
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
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::bonk::parser::BONK_PROGRAM_ID;
use solana_streamer_sdk::streaming::event_parser::protocols::bonk::BonkTradeEvent;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
use solana_streamer_sdk::streaming::yellowstone_grpc::TransactionFilter;
use solana_streamer_sdk::streaming::YellowstoneGrpc;
// Global static flag to ensure transaction is executed only once
@@ -52,24 +51,19 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
account_required,
};
// Listen to account data belonging to owner programs -> account event monitoring
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
// listen to specific event type
let event_type_filter = EventTypeFilter {
include: vec![
EventType::BonkBuyExactIn,
EventType::BonkSellExactIn,
EventType::BonkBuyExactOut,
EventType::BonkSellExactOut,
],
};
let event_type_filter = EventTypeFilter::include_only(vec![
EventType::BonkBuyExactIn,
EventType::BonkSellExactIn,
EventType::BonkBuyExactOut,
EventType::BonkSellExactOut,
]);
grpc.subscribe_events_immediate(
protocols,
None,
vec![transaction_filter],
vec![account_filter],
vec![],
Some(event_type_filter),
None,
callback,
@@ -77,27 +71,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?;
tokio::signal::ctrl_c().await?;
grpc.stop().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, {
BonkTradeEvent => |e: BonkTradeEvent| {
// 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) = bonk_copy_trade_with_grpc(event_clone).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(1);
}
});
fn create_event_callback() -> impl Fn(DexEvent) {
|event: DexEvent| {
let DexEvent::BonkTradeEvent(event) = event else {
return;
};
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
tokio::spawn(async move {
if let Err(err) = bonk_copy_trade_with_grpc(event).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(1);
}
},
});
});
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+25 -32
View File
@@ -12,8 +12,8 @@ use solana_commitment_config::CommitmentConfig;
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::bonk::BonkTradeEvent;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::{match_event, streaming::ShredStreamGrpc};
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
use solana_streamer_sdk::streaming::ShredStreamGrpc;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -29,16 +29,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let shred_stream = ShredStreamGrpc::new("use_your_shred_stream_url_here".to_string()).await?;
let callback = create_event_callback();
let protocols = vec![Protocol::Bonk];
let event_type_filter = EventTypeFilter {
include: vec![
EventType::BonkBuyExactIn,
EventType::BonkBuyExactOut,
EventType::BonkSellExactIn,
EventType::BonkSellExactOut,
EventType::BonkInitialize,
EventType::BonkInitializeV2,
],
};
let event_type_filter = EventTypeFilter::include_only(vec![
EventType::BonkBuyExactIn,
EventType::BonkBuyExactOut,
EventType::BonkSellExactIn,
EventType::BonkSellExactOut,
EventType::BonkInitialize,
EventType::BonkInitializeV2,
]);
println!("Starting to listen for events, press Ctrl+C to stop...");
shred_stream.shredstream_subscribe(protocols, None, Some(event_type_filter), callback).await?;
tokio::signal::ctrl_c().await?;
@@ -46,27 +44,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
/// 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, {
BonkTradeEvent => |e: BonkTradeEvent| {
// Only process developer token creation events
if !e.is_dev_create_token_trade {
return;
fn create_event_callback() -> impl Fn(DexEvent) {
|event: DexEvent| {
let DexEvent::BonkTradeEvent(event) = event else {
return;
};
if !event.is_dev_create_token_trade {
return;
}
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
tokio::spawn(async move {
if let Err(err) = bonk_sniper_trade_with_shreds(event).await {
eprintln!("Error in sniper trade: {:?}", err);
std::process::exit(1);
}
// 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) = bonk_sniper_trade_with_shreds(event_clone).await {
eprintln!("Error in sniper trade: {:?}", err);
std::process::exit(1);
}
});
}
},
});
});
}
}
}
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+2 -2
View File
@@ -5,8 +5,8 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0.79"
anyhow = "1.0.79"
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.4.14"
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
spl-associated-token-account = "7.0.0"
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.4.14"
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = "0.4.14"
sol-parser-sdk = { version = "0.6.0", git = "https://github.com/0xfnzero/sol-parser-sdk", rev = "995d88991b56234a23fc1d0911fdd33caa063c67" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "85c6cc901ad3f1bf8fe8010d79b92ecdd0be02b4" }
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+2 -1
View File
@@ -22,7 +22,8 @@ cargo run --release --package pumpswap_trading
- The buy uses `BuyAmount::WithMaxInput`, which applies slippage to maximum quote cost and is appropriate when fill priority matters.
- Buy parameters use post-trade reserves and LP/protocol/creator fee bps from the event.
- The event's raw and virtual quote reserves come from the same transaction snapshot. The hot path does not fetch the Pool account, avoiding both added latency and mixed-slot quotes.
- The example records the pre-buy balance and sells only the confirmed balance increase. It refreshes pool state and blockhash before selling.
- The first matching event asynchronously records the pre-buy balance; the next fresh event performs the trade without a balance RPC in the submission hot path. It sells only the confirmed balance increase and refreshes pool state and blockhash before selling.
- Use `BuyAmount::ExactInput` when the quote spend must be exact. That mode protects minimum output and can fail more often in an active pool.
- If baseline warmup fails, the example waits for another event. Once transaction execution starts, an error keeps the one-shot guard locked because submission or position state may be uncertain; inspect the signatures and account state before retrying.
Production bots should also add durable signature deduplication, a position state machine, SWQoS configuration, and bounded requoting. Do not solve slippage errors by setting `min_out` to zero.
+2 -1
View File
@@ -20,7 +20,8 @@ cargo run --release --package pumpswap_trading
- 买入使用 `BuyAmount::WithMaxInput`,适合优先成交的跟单/狙击场景,滑点限制最大 quote 成本。
- 买入参数使用事件中的成交后储备和 LP/protocol/creator fee bps。
- 原始 quote 储备和虚拟 quote 储备均来自同一笔交易的事件快照;热路径不再查询 Pool 账户,避免额外延迟和跨 slot 混合报价。
- 示例记录买前余额,只卖出确认后的余额增量卖出前重新获取池状态和 blockhash。
- 第一个匹配事件用于异步记录买前余额,下一条新鲜事件才会交易,因此提交热路径不再查询余额;示例只卖出确认后的余额增量,并在卖出前重新获取池状态和 blockhash。
- 若业务必须精确花费 quote,应改用 `BuyAmount::ExactInput`。这会启用最小输出保护,在活跃池中更容易因状态变化而失败。
- 基线预热失败时会等待后续事件;一旦进入交易阶段,错误会保持单次执行锁定,因为提交状态或持仓可能不确定,必须先核对签名和账户状态再重试。
生产机器人还应增加持久化签名去重、持仓状态机、SWQoS 配置和有限次数的重新报价。不要通过把 `min_out` 设为零来处理滑点错误。
+161 -29
View File
@@ -24,7 +24,7 @@ use solana_streamer_sdk::streaming::YellowstoneGrpc;
use std::str::FromStr;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
Arc, RwLock,
};
use std::time::{Duration, Instant};
use tokio::sync::watch;
@@ -91,6 +91,18 @@ struct CachedBlockhash {
fetched_at: Instant,
}
#[derive(Clone, Copy)]
struct PositionBaseline {
mint: Pubkey,
token_program: Pubkey,
amount: u64,
}
enum EventAction {
BaselineWarmed,
TradeCompleted,
}
#[derive(Clone)]
struct BlockhashCache {
receiver: watch::Receiver<CachedBlockhash>,
@@ -139,6 +151,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let trade_client = Arc::new(create_solana_trade_client().await?);
let blockhash_cache = BlockhashCache::start(trade_client.infrastructure.rpc.clone()).await?;
let position_baseline = Arc::new(RwLock::new(None));
let grpc = YellowstoneGrpc::new(
std::env::var("GRPC_ENDPOINT")
@@ -146,7 +159,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
std::env::var("GRPC_AUTH_TOKEN").ok(),
)?;
let callback = create_event_callback(trade_client, blockhash_cache, selection);
let callback =
create_event_callback(trade_client, blockhash_cache, position_baseline, selection);
let protocols = vec![Protocol::PumpSwap];
// Filter accounts
let account_include = vec![
@@ -192,6 +206,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
fn create_event_callback(
client: Arc<SolanaTrade>,
blockhash_cache: BlockhashCache,
position_baseline: Arc<RwLock<Option<PositionBaseline>>>,
selection: EventSelection,
) -> impl Fn(DexEvent) {
move |event: DexEvent| match event {
@@ -207,15 +222,38 @@ fn create_event_callback(
return;
}
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
if ALREADY_EXECUTED
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
let client = client.clone();
let blockhash_cache = blockhash_cache.clone();
let position_baseline = position_baseline.clone();
let was_preparing =
position_baseline.read().map(|baseline| baseline.is_none()).unwrap_or(true);
tokio::spawn(async move {
if let Err(err) =
pumpswap_trade_with_grpc_buy_event(client, blockhash_cache, e).await
match pumpswap_trade_with_grpc_buy_event(
client,
blockhash_cache,
position_baseline,
selection,
e,
)
.await
{
eprintln!("Error in trade: {:?}", err);
std::process::exit(1);
Ok(EventAction::BaselineWarmed) => {
ALREADY_EXECUTED.store(false, Ordering::Release);
}
Ok(EventAction::TradeCompleted) => {}
Err(err) if was_preparing => {
eprintln!("baseline warmup failed; waiting for a later event: {err:?}");
ALREADY_EXECUTED.store(false, Ordering::Release);
}
Err(err) => {
eprintln!(
"trade failed after entering execution state: {err:?}; automatic retry is disabled because submission status or position state may be uncertain"
);
}
}
});
}
@@ -232,15 +270,38 @@ fn create_event_callback(
return;
}
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
if ALREADY_EXECUTED
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
let client = client.clone();
let blockhash_cache = blockhash_cache.clone();
let position_baseline = position_baseline.clone();
let was_preparing =
position_baseline.read().map(|baseline| baseline.is_none()).unwrap_or(true);
tokio::spawn(async move {
if let Err(err) =
pumpswap_trade_with_grpc_sell_event(client, blockhash_cache, e).await
match pumpswap_trade_with_grpc_sell_event(
client,
blockhash_cache,
position_baseline,
selection,
e,
)
.await
{
eprintln!("Error in trade: {:?}", err);
std::process::exit(1);
Ok(EventAction::BaselineWarmed) => {
ALREADY_EXECUTED.store(false, Ordering::Release);
}
Ok(EventAction::TradeCompleted) => {}
Err(err) if was_preparing => {
eprintln!("baseline warmup failed; waiting for a later event: {err:?}");
ALREADY_EXECUTED.store(false, Ordering::Release);
}
Err(err) => {
eprintln!(
"trade failed after entering execution state: {err:?}; automatic retry is disabled because submission status or position state may be uncertain"
);
}
}
});
}
@@ -274,8 +335,10 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
async fn pumpswap_trade_with_grpc_buy_event(
client: Arc<SolanaTrade>,
blockhash_cache: BlockhashCache,
position_baseline: Arc<RwLock<Option<PositionBaseline>>>,
selection: EventSelection,
trade_info: PumpSwapBuyEvent,
) -> AnyResult<()> {
) -> AnyResult<EventAction> {
let params = PumpSwapParams::from_trade_with_fee_basis_points(
trade_info.pool,
trade_info.base_mint,
@@ -305,16 +368,25 @@ async fn pumpswap_trade_with_grpc_buy_event(
} else {
trade_info.base_mint
};
pumpswap_trade_with_grpc(&client, &blockhash_cache, trade_info.metadata.recv_us, mint, params)
.await?;
Ok(())
pumpswap_trade_with_grpc(
&client,
&blockhash_cache,
&position_baseline,
trade_info.metadata.recv_us,
selection.max_event_age_ms,
mint,
params,
)
.await
}
async fn pumpswap_trade_with_grpc_sell_event(
client: Arc<SolanaTrade>,
blockhash_cache: BlockhashCache,
position_baseline: Arc<RwLock<Option<PositionBaseline>>>,
selection: EventSelection,
trade_info: PumpSwapSellEvent,
) -> AnyResult<()> {
) -> AnyResult<EventAction> {
let params = PumpSwapParams::from_trade_with_fee_basis_points(
trade_info.pool,
trade_info.base_mint,
@@ -344,24 +416,33 @@ async fn pumpswap_trade_with_grpc_sell_event(
} else {
trade_info.base_mint
};
pumpswap_trade_with_grpc(&client, &blockhash_cache, trade_info.metadata.recv_us, mint, params)
.await?;
Ok(())
pumpswap_trade_with_grpc(
&client,
&blockhash_cache,
&position_baseline,
trade_info.metadata.recv_us,
selection.max_event_age_ms,
mint,
params,
)
.await
}
async fn pumpswap_trade_with_grpc(
client: &SolanaTrade,
blockhash_cache: &BlockhashCache,
position_baseline: &Arc<RwLock<Option<PositionBaseline>>>,
grpc_recv_us: i64,
max_event_age_ms: u64,
mint_pubkey: Pubkey,
params: PumpSwapParams,
) -> AnyResult<()> {
) -> AnyResult<EventAction> {
println!("Testing PumpSwap trading...");
validate_pumpswap_snapshot(&params)?;
if !is_event_fresh(grpc_recv_us, now_micros(), max_event_age_ms) {
anyhow::bail!("event became stale before transaction construction");
}
let slippage_basis_points = Some(500);
let recent_blockhash = blockhash_cache.latest()?;
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);
let is_sol = params.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| params.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
@@ -372,8 +453,35 @@ async fn pumpswap_trade_with_grpc(
} else {
anyhow::bail!("target mint {} does not belong to pool {}", mint_pubkey, params.pool);
};
let balance_before =
client.get_payer_token_balance_with_program(&mint_pubkey, &program_id).await?;
let baseline = position_baseline
.read()
.map_err(|_| anyhow::anyhow!("position baseline lock is poisoned"))?
.as_ref()
.copied();
let balance_before = if let Some(baseline) = baseline {
if baseline.mint != mint_pubkey || baseline.token_program != program_id {
anyhow::bail!("cached position baseline belongs to a different mint or token program");
}
baseline.amount
} else {
let amount = client.get_payer_token_balance_with_program(&mint_pubkey, &program_id).await?;
let mut baseline = position_baseline
.write()
.map_err(|_| anyhow::anyhow!("position baseline lock is poisoned"))?;
*baseline = Some(PositionBaseline { mint: mint_pubkey, token_program: program_id, amount });
println!(
"Position baseline warmed at {} base units; waiting for the next fresh matching event",
amount
);
return Ok(EventAction::BaselineWarmed);
};
let recent_blockhash = blockhash_cache.latest()?;
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);
if !is_event_fresh(grpc_recv_us, now_micros(), max_event_age_ms) {
anyhow::bail!("event became stale while preparing the transaction");
}
// Buy tokens
println!("Buying tokens from PumpSwap...");
@@ -431,8 +539,32 @@ async fn pumpswap_trade_with_grpc(
anyhow::bail!("sell failed: {:?}; signatures: {:?}", err, sigs);
}
// Exit program
std::process::exit(0);
println!("Round-trip example completed; further matching events remain locked out");
Ok(EventAction::TradeCompleted)
}
fn validate_pumpswap_snapshot(params: &PumpSwapParams) -> AnyResult<()> {
let required = [
("pool", params.pool),
("base_mint", params.base_mint),
("quote_mint", params.quote_mint),
("pool_base_token_account", params.pool_base_token_account),
("pool_quote_token_account", params.pool_quote_token_account),
("coin_creator_vault_ata", params.coin_creator_vault_ata),
("coin_creator_vault_authority", params.coin_creator_vault_authority),
("base_token_program", params.base_token_program),
("quote_token_program", params.quote_token_program),
];
for (name, value) in required {
if value == Pubkey::default() {
anyhow::bail!("event snapshot is missing {name}");
}
}
if params.pool_base_token_reserves == 0 || params.pool_quote_token_reserves == 0 {
anyhow::bail!("event snapshot has an empty raw pool reserve");
}
params.effective_quote_reserves()?;
Ok(())
}
#[cfg(test)]
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+21 -27
View File
@@ -12,12 +12,10 @@ use solana_commitment_config::CommitmentConfig;
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::raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
use solana_streamer_sdk::streaming::event_parser::protocols::raydium_amm_v4::RaydiumAmmV4SwapEvent;
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
use solana_streamer_sdk::streaming::yellowstone_grpc::TransactionFilter;
use solana_streamer_sdk::streaming::YellowstoneGrpc;
use solana_streamer_sdk::{
match_event, streaming::event_parser::protocols::raydium_amm_v4::RaydiumAmmV4SwapEvent,
};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -51,19 +49,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
account_required,
};
// Listen to account data belonging to owner programs -> account event monitoring
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
// listen to specific event type
let event_type_filter = EventTypeFilter {
include: vec![EventType::RaydiumAmmV4SwapBaseIn, EventType::RaydiumAmmV4SwapBaseOut],
};
let event_type_filter = EventTypeFilter::include_only(vec![
EventType::RaydiumAmmV4SwapBaseIn,
EventType::RaydiumAmmV4SwapBaseOut,
]);
grpc.subscribe_events_immediate(
protocols,
None,
vec![transaction_filter],
vec![account_filter],
vec![],
Some(event_type_filter),
None,
callback,
@@ -71,27 +67,25 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?;
tokio::signal::ctrl_c().await?;
grpc.stop().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, {
RaydiumAmmV4SwapEvent => |e: RaydiumAmmV4SwapEvent| {
// 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) = raydium_amm_v4_copy_trade_with_grpc(event_clone).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(1);
}
});
fn create_event_callback() -> impl Fn(DexEvent) {
|event: DexEvent| {
let DexEvent::RaydiumAmmV4SwapEvent(event) = event else {
return;
};
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
tokio::spawn(async move {
if let Err(err) = raydium_amm_v4_copy_trade_with_grpc(event).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(1);
}
},
});
});
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+28 -32
View File
@@ -8,12 +8,10 @@ use solana_commitment_config::CommitmentConfig;
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::raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
use solana_streamer_sdk::streaming::event_parser::protocols::raydium_cpmm::RaydiumCpmmSwapEvent;
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
use solana_streamer_sdk::streaming::yellowstone_grpc::TransactionFilter;
use solana_streamer_sdk::streaming::YellowstoneGrpc;
use solana_streamer_sdk::{
match_event, streaming::event_parser::protocols::raydium_cpmm::RaydiumCpmmSwapEvent,
};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -47,19 +45,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
account_required,
};
// Listen to account data belonging to owner programs -> account event monitoring
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
// listen to specific event type
let event_type_filter = EventTypeFilter {
include: vec![EventType::RaydiumCpmmSwapBaseInput, EventType::RaydiumCpmmSwapBaseOutput],
};
let event_type_filter = EventTypeFilter::include_only(vec![
EventType::RaydiumCpmmSwapBaseInput,
EventType::RaydiumCpmmSwapBaseOutput,
]);
grpc.subscribe_events_immediate(
protocols,
None,
vec![transaction_filter],
vec![account_filter],
vec![],
Some(event_type_filter),
None,
callback,
@@ -67,32 +63,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.await?;
tokio::signal::ctrl_c().await?;
grpc.stop().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, {
RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| {
let is_wsol = e.input_token_mint == WSOL_TOKEN_ACCOUNT || e.output_token_mint == WSOL_TOKEN_ACCOUNT;
let is_usdc = e.input_token_mint == USDC_TOKEN_ACCOUNT || e.output_token_mint == USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
fn create_event_callback() -> impl Fn(DexEvent) {
|event: DexEvent| {
let DexEvent::RaydiumCpmmSwapEvent(event) = event else {
return;
};
let is_wsol = event.input_token_mint == WSOL_TOKEN_ACCOUNT
|| event.output_token_mint == WSOL_TOKEN_ACCOUNT;
let is_usdc = event.input_token_mint == USDC_TOKEN_ACCOUNT
|| event.output_token_mint == USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
}
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
tokio::spawn(async move {
if let Err(err) = raydium_cpmm_copy_trade_with_grpc(event).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(1);
}
// 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) = raydium_cpmm_copy_trade_with_grpc(event_clone).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(1);
}
});
}
},
});
});
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "f1c6aecb3d4a4ebb2cd3c9f6a58de20b019418e2" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }