fix(pumpswap): harden quotes and low-latency examples
This commit is contained in:
+3
-6
@@ -75,6 +75,9 @@ futures = "0.3.31"
|
||||
futures-util = "0.3.31"
|
||||
base64 = "0.22.1"
|
||||
bs58 = "0.5.1"
|
||||
# `five8` 1.0 permits five8_core 0.1.x, whose DecodeError implements
|
||||
# std::error::Error only with this feature. Solana keypair decoding requires it.
|
||||
five8_core = { version = "0.1.2", features = ["std"] }
|
||||
rand = "0.9.0"
|
||||
bincode = "1.3.3"
|
||||
anyhow = "1.0.90"
|
||||
@@ -123,12 +126,6 @@ memmap2 = "0.9"
|
||||
num_cpus = "1.16"
|
||||
libc = "0.2"
|
||||
|
||||
# solana-keypair 3.1.2 assumes five8's DecodeError implements std::error::Error.
|
||||
# Mixed Solana dependency graphs can resolve five8 1.0 against five8_core 0.1,
|
||||
# so use the patched upstream source that wraps decode failures safely.
|
||||
[patch.crates-io]
|
||||
solana-keypair = { path = "patches/solana-keypair" }
|
||||
|
||||
# 🚀 编译器优化配置 - 平衡性能与编译速度
|
||||
[profile.release]
|
||||
opt-level = 3 # 最高优化级别(不影响编译速度)
|
||||
|
||||
@@ -20,10 +20,12 @@ Do not initialize clients, synchronously fetch a blockhash, query balances, or s
|
||||
| Sell an exact token amount | `SellAmount::ExactInput` |
|
||||
|
||||
`WithMaxInput` still enforces slippage. Never use `min_out = 0` as routine error handling.
|
||||
Exact-output support is protocol- and pool-direction-specific. PumpSwap exposes exact output through its on-chain `buy` instruction, but its `sell` instruction accepts exact base input plus minimum quote output; the SDK rejects `SellAmount::ExactOutput` when that direction would require `sell`.
|
||||
|
||||
Use post-trade event reserves. Preserve PumpFun quote mint, creator/vault, token program, cashback, and mayhem fields. PumpSwap event integrations should use `from_trade_with_fee_basis_points`. Refresh delayed sells because the triggering trade and your own buy both change pool state. Durable nonce extends transaction validity, not quote validity.
|
||||
|
||||
For `BuySlippageBelowMinBaseAmountOut`, discard the old transaction, obtain newer reserves and fee rates, enforce a quote-age limit, and rebuild only within a bounded retry policy.
|
||||
After a submit timeout or ambiguous relay error, reconcile the signature and position before retrying. A retry policy may rebuild quotes automatically only when the previous transaction is known not to have been submitted.
|
||||
|
||||
Reference examples:
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2021"
|
||||
rust-version = "1.81.0"
|
||||
name = "solana-keypair"
|
||||
version = "3.1.2"
|
||||
authors = ["Anza Maintainers <maintainers@anza.xyz>"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Concrete implementation of a Solana `Signer`."
|
||||
homepage = "https://anza.xyz/"
|
||||
documentation = "https://docs.rs/solana-keypair"
|
||||
readme = false
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/anza-xyz/solana-sdk"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg=docsrs"]
|
||||
|
||||
[features]
|
||||
seed-derivable = [
|
||||
"dep:solana-derivation-path",
|
||||
"dep:solana-seed-derivable",
|
||||
"dep:ed25519-dalek-bip32",
|
||||
]
|
||||
|
||||
[lib]
|
||||
name = "solana_keypair"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies.ed25519-dalek]
|
||||
version = "2.1.1"
|
||||
features = ["rand_core"]
|
||||
|
||||
[dependencies.ed25519-dalek-bip32]
|
||||
version = "0.3.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.five8]
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies.five8_core]
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.9.2"
|
||||
|
||||
[dependencies.solana-address]
|
||||
version = "2.2.0"
|
||||
features = ["decode"]
|
||||
|
||||
[dependencies.solana-derivation-path]
|
||||
version = "3.0.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.solana-seed-derivable]
|
||||
version = "3.0.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.solana-seed-phrase]
|
||||
version = "3.0.0"
|
||||
|
||||
[dependencies.solana-signature]
|
||||
version = "3.3.0"
|
||||
features = [
|
||||
"std",
|
||||
"verify",
|
||||
]
|
||||
default-features = false
|
||||
|
||||
[dependencies.solana-signer]
|
||||
version = "3.0.0"
|
||||
|
||||
[dev-dependencies.serde_json]
|
||||
version = "1.0.139"
|
||||
|
||||
[dev-dependencies.static_assertions]
|
||||
version = "1.1.0"
|
||||
|
||||
[dev-dependencies.tiny-bip39]
|
||||
version = "2.0.0"
|
||||
@@ -1,444 +0,0 @@
|
||||
//! Concrete implementation of a Solana `Signer` from raw bytes
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
use {
|
||||
ed25519_dalek::Signer as DalekSigner,
|
||||
solana_seed_phrase::generate_seed_from_seed_phrase_and_passphrase,
|
||||
solana_signer::SignerError,
|
||||
std::{
|
||||
error,
|
||||
io::{Read, Write},
|
||||
path::Path,
|
||||
},
|
||||
};
|
||||
pub use {
|
||||
solana_address::Address,
|
||||
solana_signature::{error::Error as SignatureError, Signature},
|
||||
solana_signer::{EncodableKey, EncodableKeypair, Signer},
|
||||
};
|
||||
|
||||
#[cfg(feature = "seed-derivable")]
|
||||
pub mod seed_derivable;
|
||||
pub mod signable;
|
||||
|
||||
/// A vanilla Ed25519 key pair
|
||||
#[derive(Debug)]
|
||||
pub struct Keypair(ed25519_dalek::SigningKey);
|
||||
|
||||
pub const KEYPAIR_LENGTH: usize = 64;
|
||||
|
||||
impl Keypair {
|
||||
/// Can be used for generating a Keypair without a dependency on `rand` types
|
||||
pub const SECRET_KEY_LENGTH: usize = 32;
|
||||
|
||||
/// Constructs a new, random `Keypair` using `OsRng`
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
let secret_bytes = rand::random::<[u8; Self::SECRET_KEY_LENGTH]>();
|
||||
Self(ed25519_dalek::SigningKey::from_bytes(&secret_bytes))
|
||||
}
|
||||
|
||||
/// Constructs a new `Keypair` using secret key bytes
|
||||
pub fn new_from_array(secret_key: [u8; 32]) -> Self {
|
||||
Self(ed25519_dalek::SigningKey::from(secret_key))
|
||||
}
|
||||
|
||||
/// Returns this `Keypair` as a byte array
|
||||
pub fn to_bytes(&self) -> [u8; KEYPAIR_LENGTH] {
|
||||
self.0.to_keypair_bytes()
|
||||
}
|
||||
|
||||
/// Recovers a `Keypair` from a base58-encoded string
|
||||
pub fn try_from_base58_string(s: &str) -> Result<Self, SignatureError> {
|
||||
let mut buf = [0u8; ed25519_dalek::KEYPAIR_LENGTH];
|
||||
// Mixed dependency graphs may resolve five8 1.x against five8_core 0.1.x,
|
||||
// whose DecodeError does not implement std::error::Error.
|
||||
five8::decode_64(s, &mut buf).map_err(|e| {
|
||||
SignatureError::from_source(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!("base58 decode keypair: {e:?}"),
|
||||
))
|
||||
})?;
|
||||
Self::try_from(&buf[..])
|
||||
}
|
||||
|
||||
/// Recovers a `Keypair` from a base58-encoded string
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if given a malformed base58 string, or if the contents of the
|
||||
/// encoded string is invalid Keypair data.
|
||||
pub fn from_base58_string(s: &str) -> Self {
|
||||
Self::try_from_base58_string(s).unwrap()
|
||||
}
|
||||
|
||||
/// Returns this `Keypair` as a base58-encoded string
|
||||
pub fn to_base58_string(&self) -> String {
|
||||
let mut out = [0u8; five8::BASE58_ENCODED_64_MAX_LEN];
|
||||
let len = five8::encode_64(&self.to_bytes(), &mut out);
|
||||
unsafe { String::from_utf8_unchecked(out[..len as usize].to_vec()) }
|
||||
}
|
||||
|
||||
/// Gets this `Keypair`'s secret key bytes
|
||||
pub fn secret_bytes(&self) -> &[u8; Self::SECRET_KEY_LENGTH] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
|
||||
/// Allows Keypair cloning
|
||||
///
|
||||
/// Note that the `Clone` trait is intentionally unimplemented because making a
|
||||
/// second copy of sensitive secret keys in memory is usually a bad idea.
|
||||
///
|
||||
/// Only use this in tests or when strictly required. Consider using [`std::sync::Arc<Keypair>`]
|
||||
/// instead.
|
||||
pub fn insecure_clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Keypair {
|
||||
type Error = SignatureError;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let keypair_bytes: &[u8; ed25519_dalek::KEYPAIR_LENGTH] =
|
||||
bytes.try_into().map_err(|_| {
|
||||
SignatureError::from_source(String::from(
|
||||
"candidate keypair byte array is the wrong length",
|
||||
))
|
||||
})?;
|
||||
ed25519_dalek::SigningKey::from_keypair_bytes(keypair_bytes)
|
||||
.map_err(|_| {
|
||||
SignatureError::from_source(String::from(
|
||||
"keypair bytes do not specify same pubkey as derived from their secret key",
|
||||
))
|
||||
})
|
||||
.map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
static_assertions::const_assert_eq!(Keypair::SECRET_KEY_LENGTH, ed25519_dalek::SECRET_KEY_LENGTH);
|
||||
|
||||
impl Signer for Keypair {
|
||||
#[inline]
|
||||
fn pubkey(&self) -> Address {
|
||||
Address::from(self.0.verifying_key().to_bytes())
|
||||
}
|
||||
|
||||
fn try_pubkey(&self) -> Result<Address, SignerError> {
|
||||
Ok(self.pubkey())
|
||||
}
|
||||
|
||||
fn sign_message(&self, message: &[u8]) -> Signature {
|
||||
Signature::from(self.0.sign(message).to_bytes())
|
||||
}
|
||||
|
||||
fn try_sign_message(&self, message: &[u8]) -> Result<Signature, SignerError> {
|
||||
Ok(self.sign_message(message))
|
||||
}
|
||||
|
||||
fn is_interactive(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PartialEq<T> for Keypair
|
||||
where
|
||||
T: Signer,
|
||||
{
|
||||
fn eq(&self, other: &T) -> bool {
|
||||
self.pubkey() == other.pubkey()
|
||||
}
|
||||
}
|
||||
|
||||
impl EncodableKey for Keypair {
|
||||
fn read<R: Read>(reader: &mut R) -> Result<Self, Box<dyn error::Error>> {
|
||||
read_keypair(reader)
|
||||
}
|
||||
|
||||
fn write<W: Write>(&self, writer: &mut W) -> Result<String, Box<dyn error::Error>> {
|
||||
write_keypair(self, writer)
|
||||
}
|
||||
}
|
||||
|
||||
impl EncodableKeypair for Keypair {
|
||||
type Pubkey = Address;
|
||||
|
||||
/// Returns the associated pubkey. Use this function specifically for settings that involve
|
||||
/// reading or writing pubkeys. For other settings, use `Signer::pubkey()` instead.
|
||||
fn encodable_pubkey(&self) -> Self::Pubkey {
|
||||
self.pubkey()
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a JSON-encoded `Keypair` from a `Reader` implementor
|
||||
pub fn read_keypair<R: Read>(reader: &mut R) -> Result<Keypair, Box<dyn error::Error>> {
|
||||
let mut buffer = String::new();
|
||||
reader.read_to_string(&mut buffer)?;
|
||||
let trimmed = buffer.trim();
|
||||
if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
"Input must be a JSON array",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
// we already checked that the string has at least two chars,
|
||||
// so 1..trimmed.len() - 1 won't be out of bounds
|
||||
#[allow(clippy::arithmetic_side_effects)]
|
||||
let contents = &trimmed[1..trimmed.len() - 1];
|
||||
let elements_vec: Vec<&str> = contents.split(',').map(|s| s.trim()).collect();
|
||||
let len = elements_vec.len();
|
||||
let elements: [&str; ed25519_dalek::KEYPAIR_LENGTH] =
|
||||
elements_vec.try_into().map_err(|_| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"Expected {} elements, found {}",
|
||||
ed25519_dalek::KEYPAIR_LENGTH,
|
||||
len
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let mut out = [0u8; ed25519_dalek::KEYPAIR_LENGTH];
|
||||
for (idx, element) in elements.into_iter().enumerate() {
|
||||
let parsed: u8 = element.parse()?;
|
||||
out[idx] = parsed;
|
||||
}
|
||||
Keypair::try_from(&out[..]).map_err(|e| std::io::Error::other(e.to_string()).into())
|
||||
}
|
||||
|
||||
/// Reads a `Keypair` from a file
|
||||
pub fn read_keypair_file<F: AsRef<Path>>(path: F) -> Result<Keypair, Box<dyn error::Error>> {
|
||||
Keypair::read_from_file(path)
|
||||
}
|
||||
|
||||
/// Writes a `Keypair` to a `Write` implementor with JSON-encoding
|
||||
pub fn write_keypair<W: Write>(
|
||||
keypair: &Keypair,
|
||||
writer: &mut W,
|
||||
) -> Result<String, Box<dyn error::Error>> {
|
||||
let keypair_bytes = keypair.to_bytes();
|
||||
let mut result = Vec::with_capacity(64 * 4 + 2); // Estimate capacity: 64 numbers * (up to 3 digits + 1 comma) + 2 brackets
|
||||
|
||||
result.push(b'['); // Opening bracket
|
||||
|
||||
for (i, &num) in keypair_bytes.iter().enumerate() {
|
||||
if i > 0 {
|
||||
result.push(b','); // Comma separator for all elements except the first
|
||||
}
|
||||
|
||||
// Convert number to string and then to bytes
|
||||
let num_str = num.to_string();
|
||||
result.extend_from_slice(num_str.as_bytes());
|
||||
}
|
||||
|
||||
result.push(b']'); // Closing bracket
|
||||
writer.write_all(&result)?;
|
||||
let as_string = String::from_utf8(result)?;
|
||||
Ok(as_string)
|
||||
}
|
||||
|
||||
/// Writes a `Keypair` to a file with JSON-encoding
|
||||
pub fn write_keypair_file<F: AsRef<Path>>(
|
||||
keypair: &Keypair,
|
||||
outfile: F,
|
||||
) -> Result<String, Box<dyn error::Error>> {
|
||||
keypair.write_to_file(outfile)
|
||||
}
|
||||
|
||||
/// Constructs a `Keypair` from caller-provided seed entropy
|
||||
pub fn keypair_from_seed(seed: &[u8]) -> Result<Keypair, Box<dyn error::Error>> {
|
||||
if seed.len() < ed25519_dalek::SECRET_KEY_LENGTH {
|
||||
return Err("Seed is too short".into());
|
||||
}
|
||||
// this won't fail as we've already checked the length
|
||||
let secret_key = ed25519_dalek::SecretKey::try_from(&seed[..ed25519_dalek::SECRET_KEY_LENGTH])?;
|
||||
Ok(Keypair(ed25519_dalek::SigningKey::from(secret_key)))
|
||||
}
|
||||
|
||||
pub fn keypair_from_seed_phrase_and_passphrase(
|
||||
seed_phrase: &str,
|
||||
passphrase: &str,
|
||||
) -> Result<Keypair, Box<dyn core::error::Error>> {
|
||||
keypair_from_seed(&generate_seed_from_seed_phrase_and_passphrase(
|
||||
seed_phrase,
|
||||
passphrase,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {
|
||||
super::*,
|
||||
bip39::{Language, Mnemonic, MnemonicType, Seed},
|
||||
solana_signer::unique_signers,
|
||||
std::{
|
||||
fs::{self, File},
|
||||
mem,
|
||||
},
|
||||
};
|
||||
|
||||
fn tmp_file_path(name: &str) -> String {
|
||||
use std::env;
|
||||
let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string());
|
||||
let keypair = Keypair::new();
|
||||
|
||||
format!("{}/tmp/{}-{}", out_dir, name, keypair.pubkey())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_keypair_file() {
|
||||
let outfile = tmp_file_path("test_write_keypair_file.json");
|
||||
let serialized_keypair = write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
||||
let keypair_vec: Vec<u8> = serde_json::from_str(&serialized_keypair).unwrap();
|
||||
assert!(Path::new(&outfile).exists());
|
||||
assert_eq!(
|
||||
keypair_vec,
|
||||
read_keypair_file(&outfile).unwrap().to_bytes().to_vec()
|
||||
);
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
assert_eq!(
|
||||
File::open(&outfile)
|
||||
.expect("open")
|
||||
.metadata()
|
||||
.expect("metadata")
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o777,
|
||||
0o600
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
read_keypair_file(&outfile).unwrap().pubkey().as_ref().len(),
|
||||
mem::size_of::<Address>()
|
||||
);
|
||||
fs::remove_file(&outfile).unwrap();
|
||||
assert!(!Path::new(&outfile).exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_keypair_file_overwrite_ok() {
|
||||
let outfile = tmp_file_path("test_write_keypair_file_overwrite_ok.json");
|
||||
|
||||
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
||||
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_keypair_file_truncate() {
|
||||
let outfile = tmp_file_path("test_write_keypair_file_truncate.json");
|
||||
|
||||
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
||||
read_keypair_file(&outfile).unwrap();
|
||||
|
||||
// Ensure outfile is truncated
|
||||
{
|
||||
let mut f = File::create(&outfile).unwrap();
|
||||
f.write_all(String::from_utf8([b'a'; 2048].to_vec()).unwrap().as_bytes())
|
||||
.unwrap();
|
||||
}
|
||||
write_keypair_file(&Keypair::new(), &outfile).unwrap();
|
||||
read_keypair_file(&outfile).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keypair_from_seed() {
|
||||
let good_seed = vec![0; 32];
|
||||
assert!(keypair_from_seed(&good_seed).is_ok());
|
||||
|
||||
let too_short_seed = vec![0; 31];
|
||||
assert!(keypair_from_seed(&too_short_seed).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keypair() {
|
||||
let keypair = keypair_from_seed(&[0u8; 32]).unwrap();
|
||||
let pubkey = keypair.pubkey();
|
||||
let data = [1u8];
|
||||
let sig = keypair.sign_message(&data);
|
||||
|
||||
// Signer
|
||||
assert_eq!(keypair.try_pubkey().unwrap(), pubkey);
|
||||
assert_eq!(keypair.pubkey(), pubkey);
|
||||
assert_eq!(keypair.try_sign_message(&data).unwrap(), sig);
|
||||
assert_eq!(keypair.sign_message(&data), sig);
|
||||
|
||||
// PartialEq
|
||||
let keypair2 = keypair_from_seed(&[0u8; 32]).unwrap();
|
||||
assert_eq!(keypair, keypair2);
|
||||
}
|
||||
|
||||
fn pubkeys(signers: &[&dyn Signer]) -> Vec<Address> {
|
||||
signers.iter().map(|x| x.pubkey()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unique_signers() {
|
||||
let alice = Keypair::new();
|
||||
let bob = Keypair::new();
|
||||
assert_eq!(
|
||||
pubkeys(&unique_signers(vec![&alice, &bob, &alice])),
|
||||
pubkeys(&[&alice, &bob])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_containers() {
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
struct Foo<S: Signer> {
|
||||
#[allow(unused)]
|
||||
signer: S,
|
||||
}
|
||||
|
||||
fn foo(_s: impl Signer) {}
|
||||
|
||||
let _arc_signer = Foo {
|
||||
signer: Arc::new(Keypair::new()),
|
||||
};
|
||||
foo(Arc::new(Keypair::new()));
|
||||
|
||||
let _rc_signer = Foo {
|
||||
signer: Rc::new(Keypair::new()),
|
||||
};
|
||||
foo(Rc::new(Keypair::new()));
|
||||
|
||||
let _ref_signer = Foo {
|
||||
signer: &Keypair::new(),
|
||||
};
|
||||
foo(Keypair::new());
|
||||
|
||||
let _box_signer = Foo {
|
||||
signer: Box::new(Keypair::new()),
|
||||
};
|
||||
foo(Box::new(Keypair::new()));
|
||||
|
||||
let _signer = Foo {
|
||||
signer: Keypair::new(),
|
||||
};
|
||||
foo(Keypair::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keypair_from_seed_phrase_and_passphrase() {
|
||||
let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
|
||||
let passphrase = "42";
|
||||
let seed = Seed::new(&mnemonic, passphrase);
|
||||
let expected_keypair = keypair_from_seed(seed.as_bytes()).unwrap();
|
||||
let keypair =
|
||||
keypair_from_seed_phrase_and_passphrase(mnemonic.phrase(), passphrase).unwrap();
|
||||
assert_eq!(keypair.pubkey(), expected_keypair.pubkey());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_base58() {
|
||||
let keypair = keypair_from_seed(&[0u8; 32]).unwrap();
|
||||
let as_base58 = keypair.to_base58_string();
|
||||
let parsed = Keypair::from_base58_string(&as_base58);
|
||||
assert_eq!(keypair, parsed);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
//! Implementation of the SeedDerivable trait for Keypair
|
||||
|
||||
use {
|
||||
crate::{keypair_from_seed, keypair_from_seed_phrase_and_passphrase, Keypair},
|
||||
ed25519_dalek_bip32::Error as Bip32Error,
|
||||
solana_derivation_path::DerivationPath,
|
||||
solana_seed_derivable::SeedDerivable,
|
||||
std::error,
|
||||
};
|
||||
|
||||
impl SeedDerivable for Keypair {
|
||||
fn from_seed(seed: &[u8]) -> Result<Self, Box<dyn error::Error>> {
|
||||
keypair_from_seed(seed)
|
||||
}
|
||||
|
||||
fn from_seed_and_derivation_path(
|
||||
seed: &[u8],
|
||||
derivation_path: Option<DerivationPath>,
|
||||
) -> Result<Self, Box<dyn error::Error>> {
|
||||
keypair_from_seed_and_derivation_path(seed, derivation_path)
|
||||
}
|
||||
|
||||
fn from_seed_phrase_and_passphrase(
|
||||
seed_phrase: &str,
|
||||
passphrase: &str,
|
||||
) -> Result<Self, Box<dyn error::Error>> {
|
||||
keypair_from_seed_phrase_and_passphrase(seed_phrase, passphrase)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a Keypair using Bip32 Hierarchical Derivation if derivation-path is provided;
|
||||
/// otherwise generates the base Bip44 Solana keypair from the seed
|
||||
pub fn keypair_from_seed_and_derivation_path(
|
||||
seed: &[u8],
|
||||
derivation_path: Option<DerivationPath>,
|
||||
) -> Result<Keypair, Box<dyn error::Error>> {
|
||||
let derivation_path = derivation_path.unwrap_or_default();
|
||||
bip32_derived_keypair(seed, derivation_path).map_err(|err| err.to_string().into())
|
||||
}
|
||||
|
||||
/// Generates a Keypair using Bip32 Hierarchical Derivation
|
||||
fn bip32_derived_keypair(
|
||||
seed: &[u8],
|
||||
derivation_path: DerivationPath,
|
||||
) -> Result<Keypair, Bip32Error> {
|
||||
let extended = ed25519_dalek_bip32::ExtendedSigningKey::from_seed(seed)
|
||||
.and_then(|extended| extended.derive(&derivation_path))?;
|
||||
Ok(Keypair(extended.signing_key))
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use {
|
||||
crate::Keypair,
|
||||
solana_address::Address,
|
||||
solana_signature::Signature,
|
||||
solana_signer::Signer,
|
||||
std::borrow::{Borrow, Cow},
|
||||
};
|
||||
|
||||
pub trait Signable {
|
||||
fn sign(&mut self, keypair: &Keypair) {
|
||||
let signature = keypair.sign_message(self.signable_data().borrow());
|
||||
self.set_signature(signature);
|
||||
}
|
||||
fn verify(&self) -> bool {
|
||||
self.get_signature()
|
||||
.verify(self.pubkey().as_ref(), self.signable_data().borrow())
|
||||
}
|
||||
|
||||
fn pubkey(&self) -> Address;
|
||||
fn signable_data(&self) -> Cow<'_, [u8]>;
|
||||
fn get_signature(&self) -> Signature;
|
||||
fn set_signature(&mut self, signature: Signature);
|
||||
}
|
||||
+284
-48
@@ -2,7 +2,7 @@ use crate::{
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::pumpswap_ix_data::{
|
||||
encode_pumpswap_buy_exact_quote_in_ix_data, encode_pumpswap_buy_ix_data,
|
||||
encode_pumpswap_buy_two_args, encode_pumpswap_sell_ix_data,
|
||||
encode_pumpswap_sell_ix_data,
|
||||
},
|
||||
instruction::{
|
||||
token_account_setup::{
|
||||
@@ -34,6 +34,38 @@ use solana_sdk::{
|
||||
/// Instruction builder for PumpSwap protocol
|
||||
pub struct PumpSwapInstructionBuilder;
|
||||
|
||||
#[inline]
|
||||
fn request_mint_matches_pool(actual: Pubkey, expected: Pubkey) -> bool {
|
||||
actual == expected
|
||||
|| (expected == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
&& actual == crate::constants::SOL_TOKEN_ACCOUNT)
|
||||
}
|
||||
|
||||
fn push_cashback_remaining_accounts(
|
||||
accounts: &mut Vec<AccountMeta>,
|
||||
user: &Pubkey,
|
||||
quote_mint: &Pubkey,
|
||||
quote_token_program: &Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
is_buy_instruction: bool,
|
||||
) -> Result<()> {
|
||||
if !is_cashback_coin {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let quote_ata = get_user_volume_accumulator_quote_ata(user, quote_mint, quote_token_program)
|
||||
.ok_or_else(|| anyhow!("user volume accumulator quote ATA derivation failed"))?;
|
||||
accounts.push(AccountMeta::new(quote_ata, false));
|
||||
|
||||
if !is_buy_instruction {
|
||||
let accumulator = get_user_volume_accumulator_pda(user)
|
||||
.ok_or_else(|| anyhow!("user volume accumulator PDA derivation failed"))?;
|
||||
accounts.push(AccountMeta::new(accumulator, false));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
@@ -49,6 +81,9 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
if params.input_amount.unwrap_or(0) == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
}
|
||||
if params.fixed_output_amount == Some(0) {
|
||||
return Err(anyhow!("Fixed output amount cannot be zero"));
|
||||
}
|
||||
|
||||
let pool = protocol_params.pool;
|
||||
let base_mint = protocol_params.base_mint;
|
||||
@@ -83,15 +118,28 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
// ========================================
|
||||
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
if params.fixed_output_amount.is_some() && !quote_is_wsol_or_usdc {
|
||||
return Err(anyhow!(
|
||||
"PumpSwap exact-output buy is unsupported when the pool requires a sell instruction"
|
||||
));
|
||||
}
|
||||
let input_stable_mint = if quote_is_wsol_or_usdc { quote_mint } else { base_mint };
|
||||
let input_stable_token_program =
|
||||
if quote_is_wsol_or_usdc { quote_token_program } else { base_token_program };
|
||||
let output_trade_mint = if quote_is_wsol_or_usdc { base_mint } else { quote_mint };
|
||||
let output_trade_token_program =
|
||||
if quote_is_wsol_or_usdc { base_token_program } else { quote_token_program };
|
||||
if !request_mint_matches_pool(params.input_mint, input_stable_mint)
|
||||
|| !request_mint_matches_pool(params.output_mint, output_trade_mint)
|
||||
{
|
||||
return Err(anyhow!("PumpSwap buy request mints do not match the supplied pool"));
|
||||
}
|
||||
let fee_basis_points = protocol_params.fee_basis_points;
|
||||
|
||||
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
|
||||
if output_amount >= pool_base_token_reserves {
|
||||
return Err(anyhow!("Exact base output must be below the pool base reserve"));
|
||||
}
|
||||
(output_amount, params.input_amount.unwrap_or(0))
|
||||
} else if quote_is_wsol_or_usdc {
|
||||
let result = buy_quote_input_internal_with_fees(
|
||||
@@ -142,7 +190,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let recipient = get_protocol_fee_recipient_random();
|
||||
(recipient, AccountMeta::new_readonly(recipient, false))
|
||||
};
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint, quote_token_program);
|
||||
|
||||
// ========================================
|
||||
// Build instructions
|
||||
@@ -210,12 +258,14 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
accounts.push(accounts::FEE_CONFIG_META);
|
||||
accounts.push(accounts::FEE_PROGRAM_META);
|
||||
// Cashback: remaining_accounts[0] = WSOL ATA of UserVolumeAccumulator (after named accounts per IDL)
|
||||
if protocol_params.is_cashback_coin {
|
||||
if let Some(wsol_ata) = get_user_volume_accumulator_wsol_ata(¶ms.payer.pubkey()) {
|
||||
accounts.push(AccountMeta::new(wsol_ata, false));
|
||||
}
|
||||
}
|
||||
push_cashback_remaining_accounts(
|
||||
&mut accounts,
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
"e_token_program,
|
||||
protocol_params.is_cashback_coin,
|
||||
quote_is_wsol_or_usdc,
|
||||
)?;
|
||||
// `pool-v2` only when coin_creator ≠ default (@pump-fun/pump-swap-sdk remainingAccounts);
|
||||
// 否则多出的一格会把 buyback pubkey 错位,触发 BuybackFeeRecipientNotAuthorized(6053)。
|
||||
if protocol_params.coin_creator != Pubkey::default() {
|
||||
@@ -228,12 +278,16 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let protocol_extra = get_protocol_extra_fee_recipient_random();
|
||||
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
|
||||
accounts.push(AccountMeta::new(
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(
|
||||
protocol_extra,
|
||||
quote_mint,
|
||||
quote_token_program,
|
||||
),
|
||||
false,
|
||||
));
|
||||
|
||||
// buy / buy_exact_quote_in:栈上 `[u8;25]` + `new_with_bytes`,避免每笔 `Vec` 堆分配。
|
||||
let track_volume: u8 = if protocol_params.is_cashback_coin { 1 } else { 0 };
|
||||
let track_volume = 1_u8;
|
||||
if quote_is_wsol_or_usdc {
|
||||
let ix_data = if params.fixed_output_amount.is_some() {
|
||||
encode_pumpswap_buy_ix_data(token_amount, sol_amount, track_volume)
|
||||
@@ -314,20 +368,34 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
if params.input_amount.unwrap_or_default() == 0 {
|
||||
return Err(anyhow!("Token amount must be greater than zero"));
|
||||
}
|
||||
if params.fixed_output_amount == Some(0) {
|
||||
return Err(anyhow!("Fixed output amount cannot be zero"));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
// ========================================
|
||||
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
if params.fixed_output_amount.is_some() && quote_is_wsol_or_usdc {
|
||||
return Err(anyhow!(
|
||||
"PumpSwap exact-output sell is unsupported when the pool requires a sell instruction"
|
||||
));
|
||||
}
|
||||
let output_stable_mint = if quote_is_wsol_or_usdc { quote_mint } else { base_mint };
|
||||
let output_stable_token_program =
|
||||
if quote_is_wsol_or_usdc { quote_token_program } else { base_token_program };
|
||||
let input_trade_mint = if quote_is_wsol_or_usdc { base_mint } else { quote_mint };
|
||||
if !request_mint_matches_pool(params.input_mint, input_trade_mint)
|
||||
|| !request_mint_matches_pool(params.output_mint, output_stable_mint)
|
||||
{
|
||||
return Err(anyhow!("PumpSwap sell request mints do not match the supplied pool"));
|
||||
}
|
||||
let fee_basis_points = protocol_params.fee_basis_points;
|
||||
|
||||
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
|
||||
if quote_is_wsol_or_usdc && output_amount > pool_quote_token_reserves {
|
||||
return Err(anyhow!("Minimum quote output exceeds the real quote-vault balance"));
|
||||
if output_amount >= pool_base_token_reserves {
|
||||
return Err(anyhow!("Exact base output must be below the pool base reserve"));
|
||||
}
|
||||
(params.input_amount.unwrap(), output_amount)
|
||||
} else if quote_is_wsol_or_usdc {
|
||||
@@ -364,7 +432,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let recipient = get_protocol_fee_recipient_random();
|
||||
(recipient, AccountMeta::new_readonly(recipient, false))
|
||||
};
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint, quote_token_program);
|
||||
|
||||
let user_base_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
@@ -427,20 +495,14 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
accounts.push(accounts::FEE_CONFIG_META);
|
||||
accounts.push(accounts::FEE_PROGRAM_META);
|
||||
// Cashback sell: 官方 remainingAccounts = [accumulator 的 quote_mint ATA, accumulator PDA, poolV2](用 quote_mint 非固定 WSOL)
|
||||
if protocol_params.is_cashback_coin {
|
||||
if let (Some(quote_ata), Some(accumulator)) = (
|
||||
get_user_volume_accumulator_quote_ata(
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
"e_token_program,
|
||||
),
|
||||
get_user_volume_accumulator_pda(¶ms.payer.pubkey()),
|
||||
) {
|
||||
accounts.push(AccountMeta::new(quote_ata, false));
|
||||
accounts.push(AccountMeta::new(accumulator, false));
|
||||
}
|
||||
}
|
||||
push_cashback_remaining_accounts(
|
||||
&mut accounts,
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
"e_token_program,
|
||||
protocol_params.is_cashback_coin,
|
||||
!quote_is_wsol_or_usdc,
|
||||
)?;
|
||||
if protocol_params.coin_creator != Pubkey::default() {
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
|
||||
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
|
||||
@@ -450,18 +512,46 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let protocol_extra = get_protocol_extra_fee_recipient_random();
|
||||
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
|
||||
accounts.push(AccountMeta::new(
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(
|
||||
protocol_extra,
|
||||
quote_mint,
|
||||
quote_token_program,
|
||||
),
|
||||
false,
|
||||
));
|
||||
|
||||
// 栈数组 + `new_with_bytes`,避免 `data.to_vec()`。
|
||||
let ix_data = if quote_is_wsol_or_usdc {
|
||||
encode_pumpswap_sell_ix_data(token_amount, sol_amount)
|
||||
let track_volume = 1_u8;
|
||||
if quote_is_wsol_or_usdc {
|
||||
let ix_data = encode_pumpswap_sell_ix_data(token_amount, sol_amount);
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
} else if params.fixed_output_amount.is_some() {
|
||||
let ix_data = encode_pumpswap_buy_ix_data(sol_amount, token_amount, track_volume);
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
} else {
|
||||
encode_pumpswap_buy_two_args(sol_amount, token_amount)
|
||||
};
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::AMM_PROGRAM, &ix_data, accounts));
|
||||
let min_base_amount_out = crate::utils::calc::common::calculate_with_slippage_sell(
|
||||
sol_amount,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let ix_data = encode_pumpswap_buy_exact_quote_in_ix_data(
|
||||
params.input_amount.unwrap_or(0),
|
||||
min_base_amount_out,
|
||||
track_volume,
|
||||
);
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
}
|
||||
|
||||
if close_wsol_ata {
|
||||
push_close_wsol_if_needed(
|
||||
@@ -558,6 +648,27 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn reverse_pumpswap_params() -> PumpSwapParams {
|
||||
PumpSwapParams::new(
|
||||
pk(1),
|
||||
crate::constants::USDC_TOKEN_ACCOUNT,
|
||||
pk(2),
|
||||
pk(3),
|
||||
pk(4),
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
0,
|
||||
pk(5),
|
||||
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
crate::constants::TOKEN_PROGRAM,
|
||||
accounts::PROTOCOL_FEE_RECIPIENT,
|
||||
Pubkey::default(),
|
||||
false,
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
fn swap_params(trade_type: TradeType, fixed_output_amount: Option<u64>) -> SwapParams {
|
||||
let (input_mint, output_mint) = if trade_type == TradeType::Sell {
|
||||
(pk(2), crate::constants::WSOL_TOKEN_ACCOUNT)
|
||||
@@ -616,29 +727,154 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_sell_fixed_output_uses_min_quote_directly() {
|
||||
let instructions = PumpSwapInstructionBuilder
|
||||
async fn pumpswap_sell_fixed_output_rejects_unsupported_sell_instruction() {
|
||||
let error = PumpSwapInstructionBuilder
|
||||
.build_sell_instructions(&swap_params(TradeType::Sell, Some(42)))
|
||||
.await
|
||||
.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::SELL_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 100_000);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"PumpSwap exact-output sell is unsupported when the pool requires a sell instruction"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_sell_fixed_output_rejects_real_vault_overflow() {
|
||||
async fn pumpswap_reverse_sell_fixed_output_uses_current_buy_layout() {
|
||||
let mut params = swap_params(TradeType::Sell, Some(42));
|
||||
let DexParamEnum::PumpSwap(protocol_params) = &mut params.protocol_params else {
|
||||
unreachable!();
|
||||
};
|
||||
protocol_params.pool_quote_token_reserves = 41;
|
||||
params.protocol_params = DexParamEnum::PumpSwap(reverse_pumpswap_params());
|
||||
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
let error = PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap_err();
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(error.to_string(), "Minimum quote output exceeds the real quote-vault balance");
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::BUY_DISCRIMINATOR);
|
||||
assert_eq!(ix.data.len(), 25);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
|
||||
assert_eq!(ix.data[24], 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_reverse_sell_exact_input_never_increases_token_spend() {
|
||||
let mut params = swap_params(TradeType::Sell, None);
|
||||
params.protocol_params = DexParamEnum::PumpSwap(reverse_pumpswap_params());
|
||||
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&ix.data[..8],
|
||||
crate::instruction::utils::pumpswap::BUY_EXACT_QUOTE_IN_DISCRIMINATOR
|
||||
);
|
||||
assert_eq!(ix.data.len(), 25);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 100_000);
|
||||
|
||||
let quote = crate::utils::calc::pumpswap::buy_quote_input_internal_with_fees(
|
||||
100_000,
|
||||
100,
|
||||
1_000_000_000,
|
||||
2_000_000_000,
|
||||
0,
|
||||
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(25, 5, 0),
|
||||
)
|
||||
.unwrap();
|
||||
let expected_min =
|
||||
crate::utils::calc::common::calculate_with_slippage_sell(quote.base, 100);
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), expected_min);
|
||||
assert_eq!(ix.data[24], 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_buy_cashback_accounts_match_actual_instruction_direction() {
|
||||
let mut params = swap_params(TradeType::Buy, None);
|
||||
let mut protocol_params = pumpswap_params();
|
||||
protocol_params.is_cashback_coin = true;
|
||||
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
let expected_ata = get_user_volume_accumulator_quote_ata(
|
||||
¶ms.payer.pubkey(),
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
&ix.data[..8],
|
||||
crate::instruction::utils::pumpswap::BUY_EXACT_QUOTE_IN_DISCRIMINATOR
|
||||
);
|
||||
assert_eq!(ix.accounts[23].pubkey, expected_ata);
|
||||
assert_eq!(ix.data[24], 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_reverse_buy_uses_sell_cashback_account_layout() {
|
||||
let mut params = swap_params(TradeType::Buy, None);
|
||||
let mut protocol_params = reverse_pumpswap_params();
|
||||
protocol_params.is_cashback_coin = true;
|
||||
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
let expected_ata = get_user_volume_accumulator_quote_ata(
|
||||
¶ms.payer.pubkey(),
|
||||
&pk(2),
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey()).unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::SELL_DISCRIMINATOR);
|
||||
assert_eq!(ix.accounts[21].pubkey, expected_ata);
|
||||
assert_eq!(ix.accounts[22].pubkey, expected_accumulator);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_fee_atas_use_the_quote_token_program() {
|
||||
let quote_token_program = pk(99);
|
||||
let mut params = swap_params(TradeType::Sell, None);
|
||||
let mut protocol_params = reverse_pumpswap_params();
|
||||
protocol_params.quote_token_program = quote_token_program;
|
||||
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
|
||||
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
let instructions =
|
||||
PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
let expected_fee_ata =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&ix.accounts[9].pubkey,
|
||||
&pk(2),
|
||||
"e_token_program,
|
||||
);
|
||||
let buyback_recipient_index = ix.accounts.len() - 2;
|
||||
let expected_buyback_ata =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&ix.accounts[buyback_recipient_index].pubkey,
|
||||
&pk(2),
|
||||
"e_token_program,
|
||||
);
|
||||
|
||||
assert_eq!(ix.accounts[10].pubkey, expected_fee_ata);
|
||||
assert_eq!(ix.accounts[buyback_recipient_index + 1].pubkey, expected_buyback_ata);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_rejects_request_mints_from_another_pool() {
|
||||
let mut params = swap_params(TradeType::Buy, None);
|
||||
params.output_mint = pk(88);
|
||||
|
||||
let error = PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap_err();
|
||||
|
||||
assert_eq!(error.to_string(), "PumpSwap buy request mints do not match the supplied pool");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -4,15 +4,6 @@ use crate::instruction::utils::pumpswap::{
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||
};
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_two_args(base_amount_out: u64, max_quote_amount_in: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_out.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes());
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_ix_data(
|
||||
base_amount_out: u64,
|
||||
|
||||
@@ -2,8 +2,8 @@ use crate::{
|
||||
common::{
|
||||
spl_associated_token_account::get_associated_token_address_with_program_id, SolanaRpcClient,
|
||||
},
|
||||
constants::{TOKEN_PROGRAM, WSOL_TOKEN_ACCOUNT},
|
||||
instruction::utils::pumpswap_types::{pool_decode, Pool},
|
||||
constants::WSOL_TOKEN_ACCOUNT,
|
||||
instruction::utils::pumpswap_types::{pool_decode, Pool, POOL_DISCRIMINATOR},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -241,6 +241,8 @@ const U8_LEN: usize = 1;
|
||||
const BOOL_LEN: usize = 1;
|
||||
const GLOBAL_CONFIG_DISCRIMINATOR_LEN: usize = 8;
|
||||
const FEE_CONFIG_DISCRIMINATOR_LEN: usize = 8;
|
||||
const GLOBAL_CONFIG_DISCRIMINATOR: [u8; 8] = [149, 8, 156, 202, 160, 252, 176, 217];
|
||||
const FEE_CONFIG_DISCRIMINATOR: [u8; 8] = [143, 52, 146, 187, 219, 123, 76, 155];
|
||||
const FEE_CONFIG_BUMP_LEN: usize = 1;
|
||||
const FEE_TIER_LEN: usize = 16 + U64_LEN * 3;
|
||||
|
||||
@@ -301,6 +303,9 @@ fn read_u32(data: &[u8], offset: usize) -> Option<u32> {
|
||||
}
|
||||
|
||||
fn decode_global_config(data: &[u8]) -> Option<GlobalConfig> {
|
||||
if data.get(..GLOBAL_CONFIG_DISCRIMINATOR_LEN)? != GLOBAL_CONFIG_DISCRIMINATOR {
|
||||
return None;
|
||||
}
|
||||
let mut offset = GLOBAL_CONFIG_DISCRIMINATOR_LEN;
|
||||
offset += PUBKEY_LEN; // admin
|
||||
let lp_fee_basis_points = read_u64(data, offset)?;
|
||||
@@ -364,6 +369,9 @@ fn decode_fee_tiers(data: &[u8], offset: &mut usize) -> Option<Vec<PumpSwapFeeTi
|
||||
}
|
||||
|
||||
pub fn decode_fee_config(data: &[u8]) -> Option<PumpSwapFeeConfig> {
|
||||
if data.get(..FEE_CONFIG_DISCRIMINATOR_LEN)? != FEE_CONFIG_DISCRIMINATOR {
|
||||
return None;
|
||||
}
|
||||
let mut offset = FEE_CONFIG_DISCRIMINATOR_LEN;
|
||||
offset += FEE_CONFIG_BUMP_LEN;
|
||||
offset += PUBKEY_LEN; // admin
|
||||
@@ -399,6 +407,14 @@ async fn refresh_global_config_once(rpc: &SolanaRpcClient) -> Option<GlobalConfi
|
||||
}
|
||||
};
|
||||
|
||||
if account.owner != accounts::AMM_PROGRAM {
|
||||
warn!(
|
||||
target: "pumpswap_global_config",
|
||||
owner = %account.owner,
|
||||
"PumpSwap GlobalConfig owner 无效"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let Some(config) = decode_global_config(&account.data) else {
|
||||
warn!(
|
||||
target: "pumpswap_global_config",
|
||||
@@ -435,6 +451,14 @@ async fn refresh_fee_config_once(rpc: &SolanaRpcClient) -> Option<PumpSwapFeeCon
|
||||
}
|
||||
};
|
||||
|
||||
if account.owner != accounts::FEE_PROGRAM {
|
||||
warn!(
|
||||
target: "pumpswap_fee_config",
|
||||
owner = %account.owner,
|
||||
"PumpSwap FeeConfig owner 无效"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
let Some(config) = decode_fee_config(&account.data) else {
|
||||
warn!(
|
||||
target: "pumpswap_fee_config",
|
||||
@@ -665,22 +689,30 @@ pub(crate) fn coin_creator_vault_authority(coin_creator: Pubkey) -> Pubkey {
|
||||
pump_pool_authority
|
||||
}
|
||||
|
||||
pub(crate) fn coin_creator_vault_ata(coin_creator: Pubkey, quote_mint: Pubkey) -> Pubkey {
|
||||
pub(crate) fn coin_creator_vault_ata(
|
||||
coin_creator: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
) -> Pubkey {
|
||||
let creator_vault_authority = coin_creator_vault_authority(coin_creator);
|
||||
let associated_token_creator_vault_authority = get_associated_token_address_with_program_id(
|
||||
&creator_vault_authority,
|
||||
"e_mint,
|
||||
&TOKEN_PROGRAM,
|
||||
"e_token_program,
|
||||
);
|
||||
associated_token_creator_vault_authority
|
||||
}
|
||||
|
||||
pub(crate) fn fee_recipient_ata(fee_recipient: Pubkey, quote_mint: Pubkey) -> Pubkey {
|
||||
pub(crate) fn fee_recipient_ata(
|
||||
fee_recipient: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
) -> Pubkey {
|
||||
let associated_token_fee_recipient =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&fee_recipient,
|
||||
"e_mint,
|
||||
&TOKEN_PROGRAM,
|
||||
"e_token_program,
|
||||
);
|
||||
associated_token_fee_recipient
|
||||
}
|
||||
@@ -733,11 +765,21 @@ pub async fn fetch_pool(
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Pool, anyhow::Error> {
|
||||
let account = rpc.get_account(pool_address).await?;
|
||||
decode_pool_account(&account).map_err(anyhow::Error::msg)
|
||||
}
|
||||
|
||||
fn decode_pool_account(account: &solana_sdk::account::Account) -> Result<Pool, String> {
|
||||
if account.owner != accounts::AMM_PROGRAM {
|
||||
return Err(anyhow!("Account is not owned by PumpSwap program"));
|
||||
return Err("Account is not owned by PumpSwap program".to_string());
|
||||
}
|
||||
let pool = pool_decode(&account.data[8..]).ok_or_else(|| anyhow!("Failed to decode pool"))?;
|
||||
Ok(pool)
|
||||
let discriminator = account
|
||||
.data
|
||||
.get(..8)
|
||||
.ok_or_else(|| "Pool account is shorter than its discriminator".to_string())?;
|
||||
if discriminator != POOL_DISCRIMINATOR {
|
||||
return Err("Account discriminator is not PumpSwap Pool".to_string());
|
||||
}
|
||||
pool_decode(&account.data[8..]).ok_or_else(|| "Failed to decode pool".to_string())
|
||||
}
|
||||
|
||||
/// Known allocated Pool account sizes. Current accounts may be serialized to
|
||||
@@ -756,6 +798,9 @@ async fn get_program_accounts_known_sizes(
|
||||
let make_config = |data_size: u64| solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
filters: Some(vec![
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(data_size),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(0, &POOL_DISCRIMINATOR),
|
||||
),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(memcmp_offset, mint.as_ref()),
|
||||
),
|
||||
@@ -777,10 +822,22 @@ async fn get_program_accounts_known_sizes(
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_PADDED)),
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_EXTENDED)),
|
||||
);
|
||||
let mut all = legacy_result.unwrap_or_default();
|
||||
all.extend(current_result.unwrap_or_default());
|
||||
all.extend(padded_result.unwrap_or_default());
|
||||
all.extend(extended_result.unwrap_or_default());
|
||||
let results = [legacy_result, current_result, padded_result, extended_result];
|
||||
let mut all = Vec::new();
|
||||
let mut errors = Vec::new();
|
||||
for (size, result) in
|
||||
[POOL_DATA_LEN_LEGACY, POOL_DATA_LEN_CURRENT, POOL_DATA_LEN_PADDED, POOL_DATA_LEN_EXTENDED]
|
||||
.into_iter()
|
||||
.zip(results)
|
||||
{
|
||||
match result {
|
||||
Ok(accounts) => all.extend(accounts),
|
||||
Err(error) => errors.push(format!("dataSize={size}: {error}")),
|
||||
}
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
return Err(anyhow!("Incomplete PumpSwap pool query: {}", errors.join("; ")));
|
||||
}
|
||||
Ok(all)
|
||||
}
|
||||
|
||||
@@ -789,13 +846,7 @@ fn decode_pool_accounts(
|
||||
) -> Vec<(Pubkey, Pool)> {
|
||||
accounts
|
||||
.into_iter()
|
||||
.filter_map(|(addr, acc)| {
|
||||
if acc.data.len() > 8 {
|
||||
pool_decode(&acc.data[8..]).map(|pool| (addr, pool))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.filter_map(|(addr, acc)| decode_pool_account(&acc).ok().map(|pool| (addr, pool)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -885,17 +936,111 @@ pub async fn find_by_mint(
|
||||
Err(anyhow!("No pool found for mint {}. diag: {}", mint, diag_str))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PoolRpcSnapshot {
|
||||
pub base_reserve: u64,
|
||||
pub quote_reserve: u64,
|
||||
pub base_token_program: Pubkey,
|
||||
pub quote_token_program: Pubkey,
|
||||
pub base_mint_supply: u64,
|
||||
}
|
||||
|
||||
const TOKEN_ACCOUNT_MINT_END: usize = 32;
|
||||
const TOKEN_ACCOUNT_AMOUNT_OFFSET: usize = 64;
|
||||
const TOKEN_ACCOUNT_AMOUNT_END: usize = 72;
|
||||
const TOKEN_ACCOUNT_STATE_OFFSET: usize = 108;
|
||||
const MINT_SUPPLY_OFFSET: usize = 36;
|
||||
const MINT_SUPPLY_END: usize = 44;
|
||||
const MINT_INITIALIZED_OFFSET: usize = 45;
|
||||
|
||||
fn supported_token_program(program: &Pubkey) -> bool {
|
||||
*program == crate::constants::TOKEN_PROGRAM || *program == crate::constants::TOKEN_PROGRAM_2022
|
||||
}
|
||||
|
||||
fn decode_token_account_amount(
|
||||
account: &solana_sdk::account::Account,
|
||||
expected_mint: &Pubkey,
|
||||
) -> Result<(u64, Pubkey), anyhow::Error> {
|
||||
if !supported_token_program(&account.owner) {
|
||||
return Err(anyhow!("Pool vault is not owned by a supported token program"));
|
||||
}
|
||||
let mint = account
|
||||
.data
|
||||
.get(..TOKEN_ACCOUNT_MINT_END)
|
||||
.ok_or_else(|| anyhow!("Pool vault data is too short"))?;
|
||||
if mint != expected_mint.as_ref() {
|
||||
return Err(anyhow!("Pool vault mint does not match Pool account"));
|
||||
}
|
||||
if account.data.get(TOKEN_ACCOUNT_STATE_OFFSET).copied() != Some(1) {
|
||||
return Err(anyhow!("Pool vault is not initialized"));
|
||||
}
|
||||
let amount = account
|
||||
.data
|
||||
.get(TOKEN_ACCOUNT_AMOUNT_OFFSET..TOKEN_ACCOUNT_AMOUNT_END)
|
||||
.and_then(|bytes| bytes.try_into().ok())
|
||||
.map(u64::from_le_bytes)
|
||||
.ok_or_else(|| anyhow!("Pool vault amount is missing"))?;
|
||||
Ok((amount, account.owner))
|
||||
}
|
||||
|
||||
fn decode_mint_supply(
|
||||
account: &solana_sdk::account::Account,
|
||||
expected_token_program: &Pubkey,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
if account.owner != *expected_token_program {
|
||||
return Err(anyhow!("Base mint and base vault use different token programs"));
|
||||
}
|
||||
if account.data.get(MINT_INITIALIZED_OFFSET).copied() != Some(1) {
|
||||
return Err(anyhow!("Base mint is not initialized"));
|
||||
}
|
||||
account
|
||||
.data
|
||||
.get(MINT_SUPPLY_OFFSET..MINT_SUPPLY_END)
|
||||
.and_then(|bytes| bytes.try_into().ok())
|
||||
.map(u64::from_le_bytes)
|
||||
.ok_or_else(|| anyhow!("Base mint supply is missing"))
|
||||
}
|
||||
|
||||
pub async fn get_pool_rpc_snapshot(
|
||||
pool: &Pool,
|
||||
rpc: &SolanaRpcClient,
|
||||
) -> Result<PoolRpcSnapshot, anyhow::Error> {
|
||||
let addresses = [pool.pool_base_token_account, pool.pool_quote_token_account, pool.base_mint];
|
||||
let accounts = rpc.get_multiple_accounts(&addresses).await?;
|
||||
let base_vault = accounts
|
||||
.first()
|
||||
.and_then(Option::as_ref)
|
||||
.ok_or_else(|| anyhow!("PumpSwap base vault account was not found"))?;
|
||||
let quote_vault = accounts
|
||||
.get(1)
|
||||
.and_then(Option::as_ref)
|
||||
.ok_or_else(|| anyhow!("PumpSwap quote vault account was not found"))?;
|
||||
let base_mint = accounts
|
||||
.get(2)
|
||||
.and_then(Option::as_ref)
|
||||
.ok_or_else(|| anyhow!("PumpSwap base mint account was not found"))?;
|
||||
let (base_reserve, base_token_program) =
|
||||
decode_token_account_amount(base_vault, &pool.base_mint)?;
|
||||
let (quote_reserve, quote_token_program) =
|
||||
decode_token_account_amount(quote_vault, &pool.quote_mint)?;
|
||||
let base_mint_supply = decode_mint_supply(base_mint, &base_token_program)?;
|
||||
|
||||
Ok(PoolRpcSnapshot {
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
base_mint_supply,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_token_balances(
|
||||
pool: &Pool,
|
||||
rpc: &SolanaRpcClient,
|
||||
) -> Result<(u64, u64), anyhow::Error> {
|
||||
let base_balance = rpc.get_token_account_balance(&pool.pool_base_token_account).await?;
|
||||
let quote_balance = rpc.get_token_account_balance(&pool.pool_quote_token_account).await?;
|
||||
let snapshot = get_pool_rpc_snapshot(pool, rpc).await?;
|
||||
|
||||
let base_amount = base_balance.amount.parse::<u64>().map_err(|e| anyhow!(e))?;
|
||||
let quote_amount = quote_balance.amount.parse::<u64>().map_err(|e| anyhow!(e))?;
|
||||
|
||||
Ok((base_amount, quote_amount))
|
||||
Ok((snapshot.base_reserve, snapshot.quote_reserve))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -909,7 +1054,40 @@ pub fn get_fee_config_pda() -> Option<Pubkey> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use crate::instruction::utils::pumpswap_types;
|
||||
use solana_sdk::{account::Account, pubkey::Pubkey};
|
||||
|
||||
fn pool_account(virtual_quote_reserves: i128) -> Account {
|
||||
let mut data = Vec::with_capacity(8 + pumpswap_types::POOL_SIZE);
|
||||
data.extend_from_slice(&POOL_DISCRIMINATOR);
|
||||
data.push(7);
|
||||
data.extend_from_slice(&42u16.to_le_bytes());
|
||||
for seed in 1..=6 {
|
||||
data.extend_from_slice(Pubkey::new_from_array([seed; 32]).as_ref());
|
||||
}
|
||||
data.extend_from_slice(&123_456u64.to_le_bytes());
|
||||
data.extend_from_slice(Pubkey::new_from_array([7; 32]).as_ref());
|
||||
data.push(1);
|
||||
data.push(0);
|
||||
data.extend_from_slice(&virtual_quote_reserves.to_le_bytes());
|
||||
Account { data, owner: accounts::AMM_PROGRAM, ..Account::default() }
|
||||
}
|
||||
|
||||
fn token_account(mint: Pubkey, owner: Pubkey, amount: u64) -> Account {
|
||||
let mut data = vec![0; 165];
|
||||
data[..32].copy_from_slice(mint.as_ref());
|
||||
data[TOKEN_ACCOUNT_AMOUNT_OFFSET..TOKEN_ACCOUNT_AMOUNT_END]
|
||||
.copy_from_slice(&amount.to_le_bytes());
|
||||
data[TOKEN_ACCOUNT_STATE_OFFSET] = 1;
|
||||
Account { data, owner, ..Account::default() }
|
||||
}
|
||||
|
||||
fn mint_account(owner: Pubkey, supply: u64) -> Account {
|
||||
let mut data = vec![0; 82];
|
||||
data[MINT_SUPPLY_OFFSET..MINT_SUPPLY_END].copy_from_slice(&supply.to_le_bytes());
|
||||
data[MINT_INITIALIZED_OFFSET] = 1;
|
||||
Account { data, owner, ..Account::default() }
|
||||
}
|
||||
|
||||
fn fee_config_fixture() -> PumpSwapFeeConfig {
|
||||
PumpSwapFeeConfig {
|
||||
@@ -936,6 +1114,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_decoders_require_official_account_discriminators() {
|
||||
let global_len = 8 + 32 + 8 + 8 + 1 + 32 * 8 + 8 + 32 + 32 + 32 + 1 + 32 * 7 + 1 + 32 * 8;
|
||||
let mut global_data = vec![0; global_len];
|
||||
global_data[..8].copy_from_slice(&GLOBAL_CONFIG_DISCRIMINATOR);
|
||||
assert!(decode_global_config(&global_data).is_some());
|
||||
global_data[0] ^= 0xff;
|
||||
assert!(decode_global_config(&global_data).is_none());
|
||||
|
||||
let mut fee_data = Vec::with_capacity(8 + 1 + 32 + 24 + 4 + 4);
|
||||
fee_data.extend_from_slice(&FEE_CONFIG_DISCRIMINATOR);
|
||||
fee_data.extend_from_slice(&[0; 1 + 32 + 24]);
|
||||
fee_data.extend_from_slice(&0_u32.to_le_bytes());
|
||||
fee_data.extend_from_slice(&0_u32.to_le_bytes());
|
||||
assert!(decode_fee_config(&fee_data).is_some());
|
||||
fee_data[0] ^= 0xff;
|
||||
assert!(decode_fee_config(&fee_data).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpswap_user_volume_accumulator_pda_deterministic() {
|
||||
let user = Pubkey::new_unique();
|
||||
@@ -1001,4 +1198,70 @@ mod tests {
|
||||
assert_eq!(POOL_DATA_LEN_PADDED, 300);
|
||||
assert_eq!(POOL_DATA_LEN_EXTENDED, 643);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_account_validation_checks_owner_length_and_discriminator() {
|
||||
let account = pool_account(-123_456);
|
||||
assert_eq!(decode_pool_account(&account).unwrap().virtual_quote_reserves, -123_456);
|
||||
|
||||
let mut wrong_owner = account.clone();
|
||||
wrong_owner.owner = Pubkey::new_unique();
|
||||
assert_eq!(
|
||||
decode_pool_account(&wrong_owner).unwrap_err(),
|
||||
"Account is not owned by PumpSwap program"
|
||||
);
|
||||
|
||||
let mut short = account.clone();
|
||||
short.data.truncate(7);
|
||||
assert_eq!(
|
||||
decode_pool_account(&short).unwrap_err(),
|
||||
"Pool account is shorter than its discriminator"
|
||||
);
|
||||
|
||||
let mut wrong_discriminator = account;
|
||||
wrong_discriminator.data[0] ^= 0xff;
|
||||
assert_eq!(
|
||||
decode_pool_account(&wrong_discriminator).unwrap_err(),
|
||||
"Account discriminator is not PumpSwap Pool"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_snapshot_decoders_validate_token_ownership_and_layout() {
|
||||
let mint = Pubkey::new_unique();
|
||||
let token_program = crate::constants::TOKEN_PROGRAM_2022;
|
||||
let vault = token_account(mint, token_program, 987_654_321);
|
||||
assert_eq!(
|
||||
decode_token_account_amount(&vault, &mint).unwrap(),
|
||||
(987_654_321, token_program)
|
||||
);
|
||||
assert_eq!(
|
||||
decode_mint_supply(&mint_account(token_program, 42), &token_program).unwrap(),
|
||||
42
|
||||
);
|
||||
|
||||
let wrong_mint = Pubkey::new_unique();
|
||||
assert_eq!(
|
||||
decode_token_account_amount(&vault, &wrong_mint).unwrap_err().to_string(),
|
||||
"Pool vault mint does not match Pool account"
|
||||
);
|
||||
|
||||
let unsupported = token_account(mint, Pubkey::new_unique(), 1);
|
||||
assert_eq!(
|
||||
decode_token_account_amount(&unsupported, &mint).unwrap_err().to_string(),
|
||||
"Pool vault is not owned by a supported token program"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coin_creator_vault_ata_uses_quote_token_program() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let token_program = crate::constants::TOKEN_PROGRAM_2022;
|
||||
let authority = coin_creator_vault_authority(creator);
|
||||
let expected =
|
||||
get_associated_token_address_with_program_id(&authority, &mint, &token_program);
|
||||
|
||||
assert_eq!(coin_creator_vault_ata(creator, mint, token_program), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ use borsh::BorshDeserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub const POOL_DISCRIMINATOR: [u8; 8] = [241, 154, 109, 4, 17, 177, 109, 188];
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct Pool {
|
||||
pub pool_bump: u8,
|
||||
@@ -26,7 +28,9 @@ pub struct Pool {
|
||||
/// Minimum Borsh payload length for the current Pool layout, excluding the
|
||||
/// 8-byte Anchor account discriminator.
|
||||
pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1 + 16;
|
||||
const LEGACY_POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1;
|
||||
const LEGACY_POOL_FIELDS_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1;
|
||||
/// Legacy Pool accounts were allocated with seven trailing padding bytes.
|
||||
pub const LEGACY_POOL_SIZE: usize = LEGACY_POOL_FIELDS_SIZE + 7;
|
||||
|
||||
#[derive(BorshDeserialize)]
|
||||
struct LegacyPool {
|
||||
@@ -69,8 +73,10 @@ pub fn pool_decode(data: &[u8]) -> Option<Pool> {
|
||||
return borsh::from_slice::<Pool>(&data[..POOL_SIZE]).ok();
|
||||
}
|
||||
|
||||
if data.len() >= LEGACY_POOL_SIZE {
|
||||
return borsh::from_slice::<LegacyPool>(&data[..LEGACY_POOL_SIZE]).ok().map(Into::into);
|
||||
if data.len() == LEGACY_POOL_SIZE {
|
||||
return borsh::from_slice::<LegacyPool>(&data[..LEGACY_POOL_FIELDS_SIZE])
|
||||
.ok()
|
||||
.map(Into::into);
|
||||
}
|
||||
|
||||
None
|
||||
@@ -121,13 +127,21 @@ mod tests {
|
||||
#[test]
|
||||
fn decodes_legacy_pool_with_zero_virtual_quote_reserves() {
|
||||
let mut data = pool_payload(0);
|
||||
data.truncate(LEGACY_POOL_SIZE);
|
||||
data.truncate(LEGACY_POOL_FIELDS_SIZE);
|
||||
data.extend_from_slice(&[0; 7]);
|
||||
|
||||
let pool = pool_decode(&data).unwrap();
|
||||
assert_eq!(pool.virtual_quote_reserves, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_partial_current_pool_layout() {
|
||||
let current = pool_payload(987_654_321);
|
||||
for len in (LEGACY_POOL_SIZE + 1)..POOL_SIZE {
|
||||
assert!(pool_decode(¤t[..len]).is_none(), "accepted body length {len}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_reserves_support_signed_virtual_amounts_and_reject_invalid_sums() {
|
||||
assert_eq!(effective_quote_reserves(1_000, 250), Some(1_250));
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::instruction::utils::pumpswap::{
|
||||
accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP, PumpSwapFeeBasisPoints,
|
||||
};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
const SPL_MINT_SUPPLY_OFFSET: usize = 36;
|
||||
const SPL_MINT_SUPPLY_LEN: usize = 8;
|
||||
|
||||
/// PumpSwap Protocol Specific Parameters
|
||||
///
|
||||
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
|
||||
@@ -267,17 +263,9 @@ impl PumpSwapParams {
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No pool found for mint"));
|
||||
}
|
||||
let (pool_address, pool) =
|
||||
crate::instruction::utils::pumpswap::find_by_mint(rpc, mint).await?;
|
||||
Self::from_pool_data(rpc, &pool_address, &pool).await
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
@@ -298,8 +286,10 @@ impl PumpSwapParams {
|
||||
pool_address: &Pubkey,
|
||||
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let (pool_base_token_reserves, pool_quote_token_reserves) =
|
||||
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
|
||||
let snapshot =
|
||||
crate::instruction::utils::pumpswap::get_pool_rpc_snapshot(pool_data, rpc).await?;
|
||||
let pool_base_token_reserves = snapshot.base_reserve;
|
||||
let pool_quote_token_reserves = snapshot.quote_reserve;
|
||||
let effective_quote_token_reserves =
|
||||
crate::instruction::utils::pumpswap_types::effective_quote_reserves(
|
||||
pool_quote_token_reserves,
|
||||
@@ -312,7 +302,7 @@ impl PumpSwapParams {
|
||||
pool_data.virtual_quote_reserves
|
||||
)
|
||||
})?;
|
||||
let base_mint_supply = fetch_mint_supply(rpc, &pool_data.base_mint).await.ok();
|
||||
let base_mint_supply = Some(snapshot.base_mint_supply);
|
||||
let fee_config = crate::instruction::utils::pumpswap::fetch_fee_config(rpc).await;
|
||||
let raw_fee_basis_points = crate::instruction::utils::pumpswap::compute_fee_basis_points(
|
||||
fee_config.as_ref(),
|
||||
@@ -331,21 +321,11 @@ impl PumpSwapParams {
|
||||
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
|
||||
creator,
|
||||
pool_data.quote_mint,
|
||||
snapshot.quote_token_program,
|
||||
);
|
||||
let coin_creator_vault_authority =
|
||||
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
|
||||
|
||||
let base_token_program_ata = get_associated_token_address_with_program_id(
|
||||
pool_address,
|
||||
&pool_data.base_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
let quote_token_program_ata = get_associated_token_address_with_program_id(
|
||||
pool_address,
|
||||
&pool_data.quote_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
pool: *pool_address,
|
||||
base_mint: pool_data.base_mint,
|
||||
@@ -357,17 +337,9 @@ impl PumpSwapParams {
|
||||
virtual_quote_reserves: pool_data.virtual_quote_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
base_token_program: snapshot.base_token_program,
|
||||
is_cashback_coin: pool_data.is_cashback_coin,
|
||||
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
quote_token_program: snapshot.quote_token_program,
|
||||
is_mayhem_mode: pool_data.is_mayhem_mode,
|
||||
pool_creator: pool_data.creator,
|
||||
coin_creator: pool_data.coin_creator,
|
||||
@@ -381,13 +353,3 @@ impl PumpSwapParams {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_mint_supply(data: &[u8]) -> Option<u64> {
|
||||
let bytes = data.get(SPL_MINT_SUPPLY_OFFSET..SPL_MINT_SUPPLY_OFFSET + SPL_MINT_SUPPLY_LEN)?;
|
||||
Some(u64::from_le_bytes(bytes.try_into().ok()?))
|
||||
}
|
||||
|
||||
async fn fetch_mint_supply(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result<u64, anyhow::Error> {
|
||||
let account = rpc.get_account(mint).await?;
|
||||
decode_mint_supply(&account.data).ok_or_else(|| anyhow::anyhow!("Failed to decode mint supply"))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,18 @@
|
||||
/// * fee_basis_points = 100 -> 1% fee
|
||||
#[inline(always)]
|
||||
pub const fn compute_fee(amount: u128, fee_basis_points: u128) -> u128 {
|
||||
ceil_div(amount * fee_basis_points, 10_000)
|
||||
let whole = match (amount / 10_000).checked_mul(fee_basis_points) {
|
||||
Some(value) => value,
|
||||
None => return u128::MAX,
|
||||
};
|
||||
let remainder_product = match (amount % 10_000).checked_mul(fee_basis_points) {
|
||||
Some(value) => value,
|
||||
None => return u128::MAX,
|
||||
};
|
||||
match whole.checked_add(ceil_div(remainder_product, 10_000)) {
|
||||
Some(value) => value,
|
||||
None => u128::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
/// Ceiling division implementation
|
||||
@@ -25,7 +36,12 @@ pub const fn compute_fee(amount: u128, fee_basis_points: u128) -> u128 {
|
||||
/// Returns the ceiling result of a/b
|
||||
#[inline(always)]
|
||||
pub const fn ceil_div(a: u128, b: u128) -> u128 {
|
||||
(a + b - 1) / b
|
||||
let quotient = a / b;
|
||||
if a % b == 0 {
|
||||
quotient
|
||||
} else {
|
||||
quotient + 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum slippage in basis points (99.99% = 9999 bps)
|
||||
@@ -55,7 +71,12 @@ pub const fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64
|
||||
} else {
|
||||
basis_points
|
||||
};
|
||||
amount + (amount * bps / 10000)
|
||||
let result = amount as u128 + (amount as u128 * bps as u128 / 10_000);
|
||||
if result > u64::MAX as u128 {
|
||||
u64::MAX
|
||||
} else {
|
||||
result as u64
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate sell amount with slippage protection
|
||||
@@ -75,9 +96,35 @@ pub const fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64
|
||||
if amount == 0 {
|
||||
return 0;
|
||||
}
|
||||
if amount <= basis_points / 10000 {
|
||||
1
|
||||
let bps = if basis_points > MAX_SLIPPAGE_BASIS_POINTS {
|
||||
MAX_SLIPPAGE_BASIS_POINTS
|
||||
} else {
|
||||
amount - (amount * basis_points / 10000)
|
||||
basis_points
|
||||
};
|
||||
amount - (amount as u128 * bps as u128 / 10_000) as u64
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ceil_div_handles_u128_max_without_addition_overflow() {
|
||||
assert_eq!(ceil_div(u128::MAX, u128::MAX), 1);
|
||||
assert_eq!(ceil_div(u128::MAX, 2), u128::MAX / 2 + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_fee_handles_large_amounts_without_multiplication_overflow() {
|
||||
assert_eq!(compute_fee(u128::MAX, 1), ceil_div(u128::MAX, 10_000));
|
||||
assert_eq!(compute_fee(u128::MAX, 10_000), u128::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slippage_helpers_are_deterministic_at_numeric_boundaries() {
|
||||
assert_eq!(calculate_with_slippage_buy(u64::MAX, 100), u64::MAX);
|
||||
assert_eq!(calculate_with_slippage_sell(u64::MAX, 100), 18_262_276_632_972_456_099);
|
||||
assert_eq!(calculate_with_slippage_sell(10_000, u64::MAX), 1);
|
||||
assert_eq!(calculate_with_slippage_sell(1, u64::MAX), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,9 @@ pub fn get_sell_sol_amount_from_token_amount(
|
||||
let amount_128 = amount as u128;
|
||||
|
||||
// Calculate SOL amount received from selling tokens using constant product formula
|
||||
let numerator = amount_128.checked_mul(virtual_sol_reserves).unwrap_or(0);
|
||||
let Some(numerator) = amount_128.checked_mul(virtual_sol_reserves) else {
|
||||
return u64::MAX;
|
||||
};
|
||||
let denominator = virtual_token_reserves.checked_add(amount_128).unwrap_or(1);
|
||||
|
||||
let sol_cost = numerator.checked_div(denominator).unwrap_or(0);
|
||||
@@ -101,5 +103,5 @@ pub fn get_sell_sol_amount_from_token_amount(
|
||||
// Calculate transaction fee
|
||||
let fee = compute_fee(sol_cost, total_fee_basis_points_128);
|
||||
|
||||
sol_cost.saturating_sub(fee) as u64
|
||||
sol_cost.saturating_sub(fee).min(u64::MAX as u128) as u64
|
||||
}
|
||||
|
||||
+193
-42
@@ -30,10 +30,22 @@ fn effective_quote_reserve(
|
||||
pub(crate) fn creator_side_fee_basis_points(
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> u64 {
|
||||
) -> Result<u64, String> {
|
||||
let creator_bps =
|
||||
if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS };
|
||||
creator_bps.saturating_add(cashback_fee_basis_points)
|
||||
creator_bps
|
||||
.checked_add(cashback_fee_basis_points)
|
||||
.ok_or_else(|| "Coin creator fee basis points overflow.".to_string())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn checked_u64(value: u128, name: &str) -> Result<u64, String> {
|
||||
u64::try_from(value).map_err(|_| format!("Calculated {name} exceeds u64."))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn checked_fee(amount: u64, basis_points: u64, name: &str) -> Result<u64, String> {
|
||||
checked_u64(compute_fee(amount as u128, basis_points as u128), name)
|
||||
}
|
||||
|
||||
/// Result for buying base tokens with base amount input
|
||||
@@ -111,7 +123,7 @@ pub fn buy_base_input_internal(
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -140,19 +152,23 @@ pub fn buy_base_input_internal_with_fees(
|
||||
return Err("Pool would be depleted; denominator is zero.".to_string());
|
||||
}
|
||||
|
||||
let quote_amount_in = ceil_div(numerator, denominator as u128) as u64;
|
||||
let quote_amount_in =
|
||||
checked_u64(ceil_div(numerator, denominator as u128), "raw quote amount")?;
|
||||
|
||||
// Calculate fees
|
||||
let lp_fee =
|
||||
compute_fee(quote_amount_in as u128, fee_basis_points.lp_fee_basis_points as u128) as u64;
|
||||
let lp_fee = checked_fee(quote_amount_in, fee_basis_points.lp_fee_basis_points, "LP fee")?;
|
||||
let protocol_fee =
|
||||
compute_fee(quote_amount_in as u128, fee_basis_points.protocol_fee_basis_points as u128)
|
||||
as u64;
|
||||
let coin_creator_fee = compute_fee(
|
||||
quote_amount_in as u128,
|
||||
fee_basis_points.coin_creator_fee_basis_points as u128,
|
||||
) as u64;
|
||||
let total_quote = quote_amount_in + lp_fee + protocol_fee + coin_creator_fee;
|
||||
checked_fee(quote_amount_in, fee_basis_points.protocol_fee_basis_points, "protocol fee")?;
|
||||
let coin_creator_fee = checked_fee(
|
||||
quote_amount_in,
|
||||
fee_basis_points.coin_creator_fee_basis_points,
|
||||
"coin creator fee",
|
||||
)?;
|
||||
let total_quote = quote_amount_in
|
||||
.checked_add(lp_fee)
|
||||
.and_then(|amount| amount.checked_add(protocol_fee))
|
||||
.and_then(|amount| amount.checked_add(coin_creator_fee))
|
||||
.ok_or_else(|| "Total quote amount exceeds u64.".to_string())?;
|
||||
|
||||
// Calculate max quote with slippage
|
||||
let max_quote = calculate_with_slippage_buy(total_quote, slippage_basis_points);
|
||||
@@ -195,7 +211,7 @@ pub fn buy_quote_input_internal(
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -216,9 +232,12 @@ pub fn buy_quote_input_internal_with_fees(
|
||||
// Calculate total fee basis points
|
||||
let total_fee_bps = fee_basis_points
|
||||
.lp_fee_basis_points
|
||||
.saturating_add(fee_basis_points.protocol_fee_basis_points)
|
||||
.saturating_add(fee_basis_points.coin_creator_fee_basis_points);
|
||||
let denominator = 10_000 + total_fee_bps;
|
||||
.checked_add(fee_basis_points.protocol_fee_basis_points)
|
||||
.and_then(|fees| fees.checked_add(fee_basis_points.coin_creator_fee_basis_points))
|
||||
.ok_or_else(|| "Fee basis points overflow.".to_string())?;
|
||||
let denominator = 10_000_u64
|
||||
.checked_add(total_fee_bps)
|
||||
.ok_or_else(|| "Fee denominator overflow.".to_string())?;
|
||||
|
||||
// Calculate effective quote amount after fees
|
||||
let mut effective_quote = (quote as u128 * 10_000) / denominator as u128;
|
||||
@@ -227,11 +246,19 @@ pub fn buy_quote_input_internal_with_fees(
|
||||
compute_fee(effective_quote, fee_basis_points.protocol_fee_basis_points as u128);
|
||||
let coin_creator_fee =
|
||||
compute_fee(effective_quote, fee_basis_points.coin_creator_fee_basis_points as u128);
|
||||
let total_with_fees = effective_quote + lp_fee + protocol_fee + coin_creator_fee;
|
||||
let total_with_fees = effective_quote
|
||||
.checked_add(lp_fee)
|
||||
.and_then(|amount| amount.checked_add(protocol_fee))
|
||||
.and_then(|amount| amount.checked_add(coin_creator_fee))
|
||||
.ok_or_else(|| "Total quote amount exceeds u128.".to_string())?;
|
||||
if total_with_fees > quote as u128 {
|
||||
effective_quote = effective_quote.saturating_sub(total_with_fees - quote as u128);
|
||||
effective_quote = effective_quote
|
||||
.checked_sub(total_with_fees - quote as u128)
|
||||
.ok_or_else(|| "Quote input is too small to cover fees.".to_string())?;
|
||||
}
|
||||
let input_amount = effective_quote.saturating_sub(1);
|
||||
let input_amount = effective_quote
|
||||
.checked_sub(1)
|
||||
.ok_or_else(|| "Quote input is too small after fees.".to_string())?;
|
||||
|
||||
// Calculate base amount out using constant product formula
|
||||
let numerator = (base_reserve as u128) * input_amount;
|
||||
@@ -241,14 +268,14 @@ pub fn buy_quote_input_internal_with_fees(
|
||||
return Err("Pool would be depleted; denominator is zero.".to_string());
|
||||
}
|
||||
|
||||
let base_amount_out = (numerator / denominator_effective) as u64;
|
||||
let base_amount_out = checked_u64(numerator / denominator_effective, "base amount")?;
|
||||
|
||||
// Calculate max quote with slippage
|
||||
let max_quote = calculate_with_slippage_buy(quote, slippage_basis_points);
|
||||
|
||||
Ok(BuyQuoteInputResult {
|
||||
base: base_amount_out,
|
||||
internal_quote_without_fees: effective_quote as u64,
|
||||
internal_quote_without_fees: checked_u64(effective_quote, "effective quote amount")?,
|
||||
max_quote,
|
||||
})
|
||||
}
|
||||
@@ -284,7 +311,7 @@ pub fn sell_base_input_internal(
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -303,22 +330,27 @@ pub fn sell_base_input_internal_with_fees(
|
||||
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||
|
||||
// Calculate quote amount out using constant product formula
|
||||
let quote_amount_out = ((effective_quote_reserve as u128) * (base as u128)
|
||||
/ ((base_reserve as u128) + (base as u128))) as u64;
|
||||
let quote_amount_out = checked_u64(
|
||||
(effective_quote_reserve as u128) * (base as u128)
|
||||
/ ((base_reserve as u128) + (base as u128)),
|
||||
"raw quote amount",
|
||||
)?;
|
||||
|
||||
// Calculate fees
|
||||
let lp_fee =
|
||||
compute_fee(quote_amount_out as u128, fee_basis_points.lp_fee_basis_points as u128) as u64;
|
||||
let lp_fee = checked_fee(quote_amount_out, fee_basis_points.lp_fee_basis_points, "LP fee")?;
|
||||
let protocol_fee =
|
||||
compute_fee(quote_amount_out as u128, fee_basis_points.protocol_fee_basis_points as u128)
|
||||
as u64;
|
||||
let coin_creator_fee = compute_fee(
|
||||
quote_amount_out as u128,
|
||||
fee_basis_points.coin_creator_fee_basis_points as u128,
|
||||
) as u64;
|
||||
checked_fee(quote_amount_out, fee_basis_points.protocol_fee_basis_points, "protocol fee")?;
|
||||
let coin_creator_fee = checked_fee(
|
||||
quote_amount_out,
|
||||
fee_basis_points.coin_creator_fee_basis_points,
|
||||
"coin creator fee",
|
||||
)?;
|
||||
|
||||
// Calculate final quote after fees
|
||||
let total_fees = lp_fee + protocol_fee + coin_creator_fee;
|
||||
let total_fees = lp_fee
|
||||
.checked_add(protocol_fee)
|
||||
.and_then(|fees| fees.checked_add(coin_creator_fee))
|
||||
.ok_or_else(|| "Total fees exceed u64.".to_string())?;
|
||||
if total_fees > quote_amount_out {
|
||||
return Err("Fees exceed total output; final quote is negative.".to_string());
|
||||
}
|
||||
@@ -395,7 +427,7 @@ pub fn sell_quote_input_internal(
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -424,9 +456,11 @@ pub fn sell_quote_input_internal_with_fees(
|
||||
fee_basis_points.coin_creator_fee_basis_points,
|
||||
)?;
|
||||
|
||||
let lp_fee =
|
||||
compute_fee(raw_quote as u128, fee_basis_points.lp_fee_basis_points as u128) as u64;
|
||||
if raw_quote.saturating_sub(lp_fee) > quote_reserve {
|
||||
let lp_fee = checked_fee(raw_quote, fee_basis_points.lp_fee_basis_points, "LP fee")?;
|
||||
let quote_vault_outflow = raw_quote
|
||||
.checked_sub(lp_fee)
|
||||
.ok_or_else(|| "LP fee exceeds raw quote output.".to_string())?;
|
||||
if quote_vault_outflow > quote_reserve {
|
||||
return Err("Insufficient real quote reserves to cover the sell output.".to_string());
|
||||
}
|
||||
|
||||
@@ -435,10 +469,13 @@ pub fn sell_quote_input_internal_with_fees(
|
||||
return Err("Invalid input: Desired quote amount exceeds available reserve.".to_string());
|
||||
}
|
||||
|
||||
let base_amount_in = ceil_div(
|
||||
(base_reserve as u128) * (raw_quote as u128),
|
||||
(effective_quote_reserve - raw_quote) as u128,
|
||||
) as u64;
|
||||
let base_amount_in = checked_u64(
|
||||
ceil_div(
|
||||
(base_reserve as u128) * (raw_quote as u128),
|
||||
(effective_quote_reserve - raw_quote) as u128,
|
||||
),
|
||||
"base amount",
|
||||
)?;
|
||||
|
||||
// Calculate min quote with slippage
|
||||
let min_quote = calculate_with_slippage_sell(quote, slippage_basis_points);
|
||||
@@ -531,4 +568,118 @@ mod tests {
|
||||
|
||||
assert_eq!(error, "Invalid effective quote reserves: raw=1000000, virtual=-1000000.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quote_modes_match_official_integer_formulas() {
|
||||
let fees = PumpSwapFeeBasisPoints::new(20, 5, 30);
|
||||
let base_reserve = 800_000_000_000_000;
|
||||
let quote_reserve = 100_000_000_000;
|
||||
let virtual_quote_reserves = 5_000_000_000;
|
||||
let slippage_basis_points = 125;
|
||||
|
||||
let buy_base = buy_base_input_internal_with_fees(
|
||||
123_456_789_000,
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&fees,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(buy_base.internal_quote_amount, 16_206_205);
|
||||
assert_eq!(buy_base.ui_quote, 16_295_341);
|
||||
assert_eq!(buy_base.max_quote, 16_499_032);
|
||||
|
||||
let buy_quote = buy_quote_input_internal_with_fees(
|
||||
1_500_000_000,
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&fees,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(buy_quote.internal_quote_without_fees, 1_491_795_125);
|
||||
assert_eq!(buy_quote.base, 11_206_836_149_304);
|
||||
assert_eq!(buy_quote.max_quote, 1_518_750_000);
|
||||
|
||||
let sell_base = sell_base_input_internal_with_fees(
|
||||
123_456_789_000,
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&fees,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sell_base.internal_quote_amount_out, 16_201_203);
|
||||
assert_eq!(sell_base.ui_quote, 16_112_095);
|
||||
assert_eq!(sell_base.min_quote, 15_910_694);
|
||||
|
||||
let sell_quote = sell_quote_input_internal_with_fees(
|
||||
500_000_000,
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&fees,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(sell_quote.internal_raw_quote, 502_765_209);
|
||||
assert_eq!(sell_quote.base, 3_849_022_110_532);
|
||||
assert_eq!(sell_quote.min_quote, 493_750_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_quote_results_return_errors_instead_of_truncating() {
|
||||
let no_fees = PumpSwapFeeBasisPoints::new(0, 0, 0);
|
||||
|
||||
let buy_error =
|
||||
buy_base_input_internal_with_fees(u64::MAX - 1, 0, u64::MAX, u64::MAX, 0, &no_fees)
|
||||
.unwrap_err();
|
||||
assert_eq!(buy_error, "Calculated raw quote amount exceeds u64.");
|
||||
|
||||
let sell_error =
|
||||
sell_quote_input_internal_with_fees(u64::MAX - 1, 0, u64::MAX, u64::MAX, 0, &no_fees)
|
||||
.unwrap_err();
|
||||
assert_eq!(sell_error, "Calculated base amount exceeds u64.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_fee_boundaries_return_errors() {
|
||||
let overflowing_fees = PumpSwapFeeBasisPoints::new(u64::MAX, 1, 0);
|
||||
let error = buy_quote_input_internal_with_fees(
|
||||
10_000,
|
||||
0,
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
0,
|
||||
&overflowing_fees,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error, "Fee basis points overflow.");
|
||||
|
||||
let oversized_fee = PumpSwapFeeBasisPoints::new(u64::MAX, 0, 0);
|
||||
let error = sell_base_input_internal_with_fees(
|
||||
1_000_000,
|
||||
0,
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
0,
|
||||
&oversized_fee,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(error, "Calculated LP fee exceeds u64.");
|
||||
|
||||
let error = creator_side_fee_basis_points(&Pubkey::new_unique(), u64::MAX).unwrap_err();
|
||||
assert_eq!(error, "Coin creator fee basis points overflow.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buy_quote_rejects_amount_too_small_after_fees() {
|
||||
let error =
|
||||
buy_quote_input_internal_with_fees(1, 0, 1_000_000, 1_000_000, 0, &fees()).unwrap_err();
|
||||
|
||||
assert_eq!(error, "Quote input is too small after fees.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user