fix(pumpswap): harden quotes and low-latency examples
This commit is contained in:
@@ -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"] }
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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` 设为零来处理滑点错误。
|
||||
|
||||
@@ -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(¶ms)?;
|
||||
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)]
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
Reference in New Issue
Block a user