fix(pumpswap): validate virtual reserve quotes
This commit is contained in:
+7
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sol-trade-sdk"
|
name = "sol-trade-sdk"
|
||||||
version = "4.0.23"
|
version = "5.0.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = [
|
authors = [
|
||||||
"William <byteblock6@gmail.com>",
|
"William <byteblock6@gmail.com>",
|
||||||
@@ -123,6 +123,12 @@ memmap2 = "0.9"
|
|||||||
num_cpus = "1.16"
|
num_cpus = "1.16"
|
||||||
libc = "0.2"
|
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]
|
[profile.release]
|
||||||
opt-level = 3 # 最高优化级别(不影响编译速度)
|
opt-level = 3 # 最高优化级别(不影响编译速度)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sol-trade-sdk = { path = "../.." }
|
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 = "85c6cc901ad3f1bf8fe8010d79b92ecdd0be02b4" }
|
||||||
solana-sdk = "3.0.0"
|
solana-sdk = "3.0.0"
|
||||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ 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.
|
- 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.
|
- Buy parameters use post-trade reserves and LP/protocol/creator fee bps from the event.
|
||||||
- `solana-streamer-sdk 0.5.0` predates the appended `virtual_quote_reserves` event field, so this compatibility example reads that Pool field once before quoting. When the parser exposes the field, pass it directly to `PumpSwapParams::from_trade_with_fee_basis_points` or keep it in a Pool account cache to remove this RPC from the hot path.
|
- 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 example records the pre-buy balance and sells only the confirmed balance increase. It 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.
|
- Use `BuyAmount::ExactInput` when the quote spend must be exact. That mode protects minimum output and can fail more often in an active pool.
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ cargo run --release --package pumpswap_trading
|
|||||||
|
|
||||||
- 买入使用 `BuyAmount::WithMaxInput`,适合优先成交的跟单/狙击场景,滑点限制最大 quote 成本。
|
- 买入使用 `BuyAmount::WithMaxInput`,适合优先成交的跟单/狙击场景,滑点限制最大 quote 成本。
|
||||||
- 买入参数使用事件中的成交后储备和 LP/protocol/creator fee bps。
|
- 买入参数使用事件中的成交后储备和 LP/protocol/creator fee bps。
|
||||||
- `solana-streamer-sdk 0.5.0` 尚未暴露追加的 `virtual_quote_reserves` 事件字段,因此该兼容示例会在报价前读取一次 Pool 字段。解析器暴露该字段后,应直接传给 `PumpSwapParams::from_trade_with_fee_basis_points`,或维护 Pool 账户缓存,以移除热路径中的这次 RPC。
|
- 原始 quote 储备和虚拟 quote 储备均来自同一笔交易的事件快照;热路径不再查询 Pool 账户,避免额外延迟和跨 slot 混合报价。
|
||||||
- 示例记录买前余额,只卖出确认后的余额增量;卖出前重新获取池状态和 blockhash。
|
- 示例记录买前余额,只卖出确认后的余额增量;卖出前重新获取池状态和 blockhash。
|
||||||
- 若业务必须精确花费 quote,应改用 `BuyAmount::ExactInput`。这会启用最小输出保护,在活跃池中更容易因状态变化而失败。
|
- 若业务必须精确花费 quote,应改用 `BuyAmount::ExactInput`。这会启用最小输出保护,在活跃池中更容易因状态变化而失败。
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use sol_trade_sdk::common::{clock::now_micros, SolanaRpcClient, TradeConfig};
|
use sol_trade_sdk::common::{clock::now_micros, SolanaRpcClient, TradeConfig};
|
||||||
use sol_trade_sdk::instruction::utils::pumpswap::fetch_pool;
|
|
||||||
use sol_trade_sdk::TradeTokenType;
|
use sol_trade_sdk::TradeTokenType;
|
||||||
use sol_trade_sdk::{
|
use sol_trade_sdk::{
|
||||||
common::AnyResult,
|
common::AnyResult,
|
||||||
@@ -12,18 +11,16 @@ use sol_trade_sdk::{
|
|||||||
};
|
};
|
||||||
use solana_commitment_config::CommitmentConfig;
|
use solana_commitment_config::CommitmentConfig;
|
||||||
use solana_sdk::{hash::Hash, pubkey::Pubkey};
|
use solana_sdk::{hash::Hash, pubkey::Pubkey};
|
||||||
|
use solana_streamer_sdk::streaming::event_parser::protocols::pumpswap::parser::PUMPSWAP_PROGRAM_ID;
|
||||||
use solana_streamer_sdk::streaming::event_parser::{
|
use solana_streamer_sdk::streaming::event_parser::{
|
||||||
common::filter::EventTypeFilter, protocols::pumpswap::PumpSwapBuyEvent,
|
common::filter::EventTypeFilter, protocols::pumpswap::PumpSwapBuyEvent,
|
||||||
};
|
};
|
||||||
use solana_streamer_sdk::streaming::event_parser::{
|
use solana_streamer_sdk::streaming::event_parser::{
|
||||||
common::EventType, protocols::pumpswap::PumpSwapSellEvent,
|
common::EventType, protocols::pumpswap::PumpSwapSellEvent,
|
||||||
};
|
};
|
||||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
|
||||||
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
|
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
|
||||||
use solana_streamer_sdk::streaming::YellowstoneGrpc;
|
use solana_streamer_sdk::streaming::YellowstoneGrpc;
|
||||||
use solana_streamer_sdk::{
|
|
||||||
match_event, streaming::event_parser::protocols::pumpswap::parser::PUMPSWAP_PROGRAM_ID,
|
|
||||||
};
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::{
|
use std::sync::{
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
@@ -170,8 +167,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
|
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
|
||||||
|
|
||||||
// listen to specific event type
|
// listen to specific event type
|
||||||
let event_type_filter =
|
let event_type_filter = EventTypeFilter {
|
||||||
EventTypeFilter { include: vec![EventType::PumpSwapBuy, EventType::PumpSwapSell] };
|
include: vec![EventType::PumpSwapBuy, EventType::PumpSwapSell],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
grpc.subscribe_events_immediate(
|
grpc.subscribe_events_immediate(
|
||||||
protocols,
|
protocols,
|
||||||
@@ -194,62 +193,59 @@ fn create_event_callback(
|
|||||||
client: Arc<SolanaTrade>,
|
client: Arc<SolanaTrade>,
|
||||||
blockhash_cache: BlockhashCache,
|
blockhash_cache: BlockhashCache,
|
||||||
selection: EventSelection,
|
selection: EventSelection,
|
||||||
) -> impl Fn(Box<dyn UnifiedEvent>) {
|
) -> impl Fn(DexEvent) {
|
||||||
move |event: Box<dyn UnifiedEvent>| {
|
move |event: DexEvent| match event {
|
||||||
match_event!(event, {
|
DexEvent::PumpSwapBuyEvent(e) => {
|
||||||
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
|
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||||
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
|| e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||||
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||||
if !is_wsol && !is_usdc {
|
|| e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
||||||
return;
|
if !is_wsol && !is_usdc {
|
||||||
}
|
return;
|
||||||
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Test code, only test one transaction
|
|
||||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
|
||||||
let event_clone = e.clone();
|
|
||||||
let client = client.clone();
|
|
||||||
let blockhash_cache = blockhash_cache.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = pumpswap_trade_with_grpc_buy_event(
|
|
||||||
client,
|
|
||||||
blockhash_cache,
|
|
||||||
event_clone,
|
|
||||||
).await {
|
|
||||||
eprintln!("Error in trade: {:?}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
|
|
||||||
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
|
||||||
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
|
||||||
if !is_wsol && !is_usdc {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Test code, only test one transaction
|
|
||||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
|
||||||
let event_clone = e.clone();
|
|
||||||
let client = client.clone();
|
|
||||||
let blockhash_cache = blockhash_cache.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = pumpswap_trade_with_grpc_sell_event(
|
|
||||||
client,
|
|
||||||
blockhash_cache,
|
|
||||||
event_clone,
|
|
||||||
).await {
|
|
||||||
eprintln!("Error in trade: {:?}", err);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Test code, only test one transaction
|
||||||
|
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||||
|
let client = client.clone();
|
||||||
|
let blockhash_cache = blockhash_cache.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) =
|
||||||
|
pumpswap_trade_with_grpc_buy_event(client, blockhash_cache, e).await
|
||||||
|
{
|
||||||
|
eprintln!("Error in trade: {:?}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DexEvent::PumpSwapSellEvent(e) => {
|
||||||
|
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||||
|
|| e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||||
|
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||||
|
|| e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
||||||
|
if !is_wsol && !is_usdc {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Test code, only test one transaction
|
||||||
|
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||||
|
let client = client.clone();
|
||||||
|
let blockhash_cache = blockhash_cache.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) =
|
||||||
|
pumpswap_trade_with_grpc_sell_event(client, blockhash_cache, e).await
|
||||||
|
{
|
||||||
|
eprintln!("Error in trade: {:?}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,10 +276,6 @@ async fn pumpswap_trade_with_grpc_buy_event(
|
|||||||
blockhash_cache: BlockhashCache,
|
blockhash_cache: BlockhashCache,
|
||||||
trade_info: PumpSwapBuyEvent,
|
trade_info: PumpSwapBuyEvent,
|
||||||
) -> AnyResult<()> {
|
) -> AnyResult<()> {
|
||||||
// solana-streamer-sdk 0.5.0 predates the appended event field. Read the
|
|
||||||
// Pool value so this compatibility example still prices effective reserves.
|
|
||||||
let virtual_quote_reserves =
|
|
||||||
fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?.virtual_quote_reserves;
|
|
||||||
let params = PumpSwapParams::from_trade_with_fee_basis_points(
|
let params = PumpSwapParams::from_trade_with_fee_basis_points(
|
||||||
trade_info.pool,
|
trade_info.pool,
|
||||||
trade_info.base_mint,
|
trade_info.base_mint,
|
||||||
@@ -292,7 +284,7 @@ async fn pumpswap_trade_with_grpc_buy_event(
|
|||||||
trade_info.pool_quote_token_account,
|
trade_info.pool_quote_token_account,
|
||||||
trade_info.pool_base_token_reserves,
|
trade_info.pool_base_token_reserves,
|
||||||
trade_info.pool_quote_token_reserves,
|
trade_info.pool_quote_token_reserves,
|
||||||
virtual_quote_reserves,
|
trade_info.virtual_quote_reserves,
|
||||||
trade_info.coin_creator_vault_ata,
|
trade_info.coin_creator_vault_ata,
|
||||||
trade_info.coin_creator_vault_authority,
|
trade_info.coin_creator_vault_authority,
|
||||||
trade_info.base_token_program,
|
trade_info.base_token_program,
|
||||||
@@ -300,8 +292,8 @@ async fn pumpswap_trade_with_grpc_buy_event(
|
|||||||
trade_info.protocol_fee_recipient,
|
trade_info.protocol_fee_recipient,
|
||||||
Pubkey::default(),
|
Pubkey::default(),
|
||||||
trade_info.coin_creator,
|
trade_info.coin_creator,
|
||||||
false,
|
trade_info.cashback_fee_basis_points != 0 || trade_info.cashback != 0,
|
||||||
0,
|
trade_info.cashback_fee_basis_points,
|
||||||
trade_info.lp_fee_basis_points,
|
trade_info.lp_fee_basis_points,
|
||||||
trade_info.protocol_fee_basis_points,
|
trade_info.protocol_fee_basis_points,
|
||||||
trade_info.coin_creator_fee_basis_points,
|
trade_info.coin_creator_fee_basis_points,
|
||||||
@@ -323,8 +315,6 @@ async fn pumpswap_trade_with_grpc_sell_event(
|
|||||||
blockhash_cache: BlockhashCache,
|
blockhash_cache: BlockhashCache,
|
||||||
trade_info: PumpSwapSellEvent,
|
trade_info: PumpSwapSellEvent,
|
||||||
) -> AnyResult<()> {
|
) -> AnyResult<()> {
|
||||||
let virtual_quote_reserves =
|
|
||||||
fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?.virtual_quote_reserves;
|
|
||||||
let params = PumpSwapParams::from_trade_with_fee_basis_points(
|
let params = PumpSwapParams::from_trade_with_fee_basis_points(
|
||||||
trade_info.pool,
|
trade_info.pool,
|
||||||
trade_info.base_mint,
|
trade_info.base_mint,
|
||||||
@@ -333,7 +323,7 @@ async fn pumpswap_trade_with_grpc_sell_event(
|
|||||||
trade_info.pool_quote_token_account,
|
trade_info.pool_quote_token_account,
|
||||||
trade_info.pool_base_token_reserves,
|
trade_info.pool_base_token_reserves,
|
||||||
trade_info.pool_quote_token_reserves,
|
trade_info.pool_quote_token_reserves,
|
||||||
virtual_quote_reserves,
|
trade_info.virtual_quote_reserves,
|
||||||
trade_info.coin_creator_vault_ata,
|
trade_info.coin_creator_vault_ata,
|
||||||
trade_info.coin_creator_vault_authority,
|
trade_info.coin_creator_vault_authority,
|
||||||
trade_info.base_token_program,
|
trade_info.base_token_program,
|
||||||
@@ -341,8 +331,8 @@ async fn pumpswap_trade_with_grpc_sell_event(
|
|||||||
trade_info.protocol_fee_recipient,
|
trade_info.protocol_fee_recipient,
|
||||||
Pubkey::default(),
|
Pubkey::default(),
|
||||||
trade_info.coin_creator,
|
trade_info.coin_creator,
|
||||||
false,
|
trade_info.cashback_fee_basis_points != 0 || trade_info.cashback != 0,
|
||||||
0,
|
trade_info.cashback_fee_basis_points,
|
||||||
trade_info.lp_fee_basis_points,
|
trade_info.lp_fee_basis_points,
|
||||||
trade_info.protocol_fee_basis_points,
|
trade_info.protocol_fee_basis_points,
|
||||||
trade_info.coin_creator_fee_basis_points,
|
trade_info.coin_creator_fee_basis_points,
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# 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"
|
||||||
@@ -0,0 +1,444 @@
|
|||||||
|
//! 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
//! 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))
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
+46
-10
@@ -54,7 +54,9 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
let base_mint = protocol_params.base_mint;
|
let base_mint = protocol_params.base_mint;
|
||||||
let quote_mint = protocol_params.quote_mint;
|
let quote_mint = protocol_params.quote_mint;
|
||||||
let pool_base_token_reserves = protocol_params.pool_base_token_reserves;
|
let pool_base_token_reserves = protocol_params.pool_base_token_reserves;
|
||||||
let pool_quote_token_reserves = protocol_params.effective_quote_reserves()?;
|
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
|
||||||
|
let virtual_quote_reserves = protocol_params.virtual_quote_reserves;
|
||||||
|
protocol_params.effective_quote_reserves()?;
|
||||||
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
|
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
|
||||||
let params_coin_creator_vault_authority = protocol_params.coin_creator_vault_authority;
|
let params_coin_creator_vault_authority = protocol_params.coin_creator_vault_authority;
|
||||||
let create_input_ata = params.create_input_mint_ata;
|
let create_input_ata = params.create_input_mint_ata;
|
||||||
@@ -97,9 +99,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||||
pool_base_token_reserves,
|
pool_base_token_reserves,
|
||||||
pool_quote_token_reserves,
|
pool_quote_token_reserves,
|
||||||
|
virtual_quote_reserves,
|
||||||
&fee_basis_points,
|
&fee_basis_points,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.map_err(anyhow::Error::msg)?;
|
||||||
// base_amount_out, max_quote_amount_in
|
// base_amount_out, max_quote_amount_in
|
||||||
(result.base, result.max_quote)
|
(result.base, result.max_quote)
|
||||||
} else {
|
} else {
|
||||||
@@ -108,9 +111,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||||
pool_base_token_reserves,
|
pool_base_token_reserves,
|
||||||
pool_quote_token_reserves,
|
pool_quote_token_reserves,
|
||||||
|
virtual_quote_reserves,
|
||||||
&fee_basis_points,
|
&fee_basis_points,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.map_err(anyhow::Error::msg)?;
|
||||||
// min_quote_amount_out, base_amount_in
|
// min_quote_amount_out, base_amount_in
|
||||||
(result.min_quote, params.input_amount.unwrap_or(0))
|
(result.min_quote, params.input_amount.unwrap_or(0))
|
||||||
};
|
};
|
||||||
@@ -283,7 +287,9 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
let base_mint = protocol_params.base_mint;
|
let base_mint = protocol_params.base_mint;
|
||||||
let quote_mint = protocol_params.quote_mint;
|
let quote_mint = protocol_params.quote_mint;
|
||||||
let pool_base_token_reserves = protocol_params.pool_base_token_reserves;
|
let pool_base_token_reserves = protocol_params.pool_base_token_reserves;
|
||||||
let pool_quote_token_reserves = protocol_params.effective_quote_reserves()?;
|
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
|
||||||
|
let virtual_quote_reserves = protocol_params.virtual_quote_reserves;
|
||||||
|
protocol_params.effective_quote_reserves()?;
|
||||||
let pool_base_token_account = protocol_params.pool_base_token_account;
|
let pool_base_token_account = protocol_params.pool_base_token_account;
|
||||||
let pool_quote_token_account = protocol_params.pool_quote_token_account;
|
let pool_quote_token_account = protocol_params.pool_quote_token_account;
|
||||||
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
|
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
|
||||||
@@ -305,8 +311,8 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
return Err(anyhow!("Pool must contain WSOL or USDC"));
|
return Err(anyhow!("Pool must contain WSOL or USDC"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.input_amount.is_none() {
|
if params.input_amount.unwrap_or_default() == 0 {
|
||||||
return Err(anyhow!("Token amount is not set"));
|
return Err(anyhow!("Token amount must be greater than zero"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -320,6 +326,9 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
let fee_basis_points = protocol_params.fee_basis_points;
|
let fee_basis_points = protocol_params.fee_basis_points;
|
||||||
|
|
||||||
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
|
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"));
|
||||||
|
}
|
||||||
(params.input_amount.unwrap(), output_amount)
|
(params.input_amount.unwrap(), output_amount)
|
||||||
} else if quote_is_wsol_or_usdc {
|
} else if quote_is_wsol_or_usdc {
|
||||||
let result = sell_base_input_internal_with_fees(
|
let result = sell_base_input_internal_with_fees(
|
||||||
@@ -327,9 +336,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||||
pool_base_token_reserves,
|
pool_base_token_reserves,
|
||||||
pool_quote_token_reserves,
|
pool_quote_token_reserves,
|
||||||
|
virtual_quote_reserves,
|
||||||
&fee_basis_points,
|
&fee_basis_points,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.map_err(anyhow::Error::msg)?;
|
||||||
// base_amount_in, min_quote_amount_out
|
// base_amount_in, min_quote_amount_out
|
||||||
(params.input_amount.unwrap(), result.min_quote)
|
(params.input_amount.unwrap(), result.min_quote)
|
||||||
} else {
|
} else {
|
||||||
@@ -338,9 +348,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||||
pool_base_token_reserves,
|
pool_base_token_reserves,
|
||||||
pool_quote_token_reserves,
|
pool_quote_token_reserves,
|
||||||
|
virtual_quote_reserves,
|
||||||
&fee_basis_points,
|
&fee_basis_points,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.map_err(anyhow::Error::msg)?;
|
||||||
// max_quote_amount_in, base_amount_out
|
// max_quote_amount_in, base_amount_out
|
||||||
(result.max_quote, result.base)
|
(result.max_quote, result.base)
|
||||||
};
|
};
|
||||||
@@ -617,6 +628,29 @@ mod tests {
|
|||||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pumpswap_sell_fixed_output_rejects_real_vault_overflow() {
|
||||||
|
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;
|
||||||
|
|
||||||
|
let error = PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error.to_string(), "Minimum quote output exceeds the real quote-vault balance");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pumpswap_sell_rejects_zero_input() {
|
||||||
|
let mut params = swap_params(TradeType::Sell, None);
|
||||||
|
params.input_amount = Some(0);
|
||||||
|
|
||||||
|
let error = PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error.to_string(), "Token amount must be greater than zero");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn pumpswap_usdc_buy_create_input_builds_usdc_ata() {
|
async fn pumpswap_usdc_buy_create_input_builds_usdc_ata() {
|
||||||
let mut params = swap_params(TradeType::Buy, Some(42));
|
let mut params = swap_params(TradeType::Buy, Some(42));
|
||||||
@@ -670,7 +704,8 @@ mod tests {
|
|||||||
1_000_000,
|
1_000_000,
|
||||||
100,
|
100,
|
||||||
1_000_000_000,
|
1_000_000_000,
|
||||||
2_500_000_000,
|
2_000_000_000,
|
||||||
|
500_000_000,
|
||||||
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
|
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -693,7 +728,8 @@ mod tests {
|
|||||||
100_000,
|
100_000,
|
||||||
100,
|
100,
|
||||||
1_000_000_000,
|
1_000_000_000,
|
||||||
2_500_000_000,
|
2_000_000_000,
|
||||||
|
500_000_000,
|
||||||
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
|
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -740,10 +740,11 @@ pub async fn fetch_pool(
|
|||||||
Ok(pool)
|
Ok(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Known allocated Pool account sizes. The July 2026 layout carrying
|
/// Known allocated Pool account sizes. Current accounts may be serialized to
|
||||||
/// `virtual_quote_reserves` is allocated to 300 bytes on-chain.
|
/// exactly 261 bytes or retain a larger historical allocation.
|
||||||
const POOL_DATA_LEN_LEGACY: u64 = 8 + 244;
|
const POOL_DATA_LEN_LEGACY: u64 = 8 + 244;
|
||||||
const POOL_DATA_LEN_CURRENT: u64 = 300;
|
const POOL_DATA_LEN_CURRENT: u64 = 8 + 253;
|
||||||
|
const POOL_DATA_LEN_PADDED: u64 = 300;
|
||||||
const POOL_DATA_LEN_EXTENDED: u64 = 643;
|
const POOL_DATA_LEN_EXTENDED: u64 = 643;
|
||||||
|
|
||||||
/// Run getProgramAccounts with a Memcmp filter, querying known Pool sizes in parallel.
|
/// Run getProgramAccounts with a Memcmp filter, querying known Pool sizes in parallel.
|
||||||
@@ -770,13 +771,15 @@ async fn get_program_accounts_known_sizes(
|
|||||||
};
|
};
|
||||||
let program_id = accounts::AMM_PROGRAM;
|
let program_id = accounts::AMM_PROGRAM;
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
let (legacy_result, current_result, extended_result) = tokio::join!(
|
let (legacy_result, current_result, padded_result, extended_result) = tokio::join!(
|
||||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_LEGACY)),
|
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_LEGACY)),
|
||||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_CURRENT)),
|
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_CURRENT)),
|
||||||
|
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)),
|
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_EXTENDED)),
|
||||||
);
|
);
|
||||||
let mut all = legacy_result.unwrap_or_default();
|
let mut all = legacy_result.unwrap_or_default();
|
||||||
all.extend(current_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());
|
all.extend(extended_result.unwrap_or_default());
|
||||||
Ok(all)
|
Ok(all)
|
||||||
}
|
}
|
||||||
@@ -990,4 +993,12 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(fees, PumpSwapFeeBasisPoints::new(20, 5, 75));
|
assert_eq!(fees, PumpSwapFeeBasisPoints::new(20, 5, 75));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pumpswap_pool_queries_cover_current_serialized_and_padded_sizes() {
|
||||||
|
assert_eq!(POOL_DATA_LEN_LEGACY, 252);
|
||||||
|
assert_eq!(POOL_DATA_LEN_CURRENT, 261);
|
||||||
|
assert_eq!(POOL_DATA_LEN_PADDED, 300);
|
||||||
|
assert_eq!(POOL_DATA_LEN_EXTENDED, 643);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ pub fn pool_decode(data: &[u8]) -> Option<Pool> {
|
|||||||
|
|
||||||
/// Compute the quote reserves used by PumpSwap pricing.
|
/// Compute the quote reserves used by PumpSwap pricing.
|
||||||
///
|
///
|
||||||
/// Returns `None` when the signed sum is negative or cannot fit in a `u64`.
|
/// Returns `None` when the signed sum is non-positive or cannot fit in a `u64`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn effective_quote_reserves(
|
pub fn effective_quote_reserves(
|
||||||
quote_vault_balance: u64,
|
quote_vault_balance: u64,
|
||||||
@@ -87,6 +87,7 @@ pub fn effective_quote_reserves(
|
|||||||
i128::from(quote_vault_balance)
|
i128::from(quote_vault_balance)
|
||||||
.checked_add(virtual_quote_reserves)
|
.checked_add(virtual_quote_reserves)
|
||||||
.and_then(|reserves| u64::try_from(reserves).ok())
|
.and_then(|reserves| u64::try_from(reserves).ok())
|
||||||
|
.filter(|reserves| *reserves != 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -131,6 +132,7 @@ mod tests {
|
|||||||
fn effective_reserves_support_signed_virtual_amounts_and_reject_invalid_sums() {
|
fn effective_reserves_support_signed_virtual_amounts_and_reject_invalid_sums() {
|
||||||
assert_eq!(effective_quote_reserves(1_000, 250), Some(1_250));
|
assert_eq!(effective_quote_reserves(1_000, 250), Some(1_250));
|
||||||
assert_eq!(effective_quote_reserves(1_000, -250), Some(750));
|
assert_eq!(effective_quote_reserves(1_000, -250), Some(750));
|
||||||
|
assert_eq!(effective_quote_reserves(1_000, -1_000), None);
|
||||||
assert_eq!(effective_quote_reserves(100, -101), None);
|
assert_eq!(effective_quote_reserves(100, -101), None);
|
||||||
assert_eq!(effective_quote_reserves(u64::MAX, 1), None);
|
assert_eq!(effective_quote_reserves(u64::MAX, 1), None);
|
||||||
}
|
}
|
||||||
|
|||||||
+163
-18
@@ -7,6 +7,23 @@ use crate::instruction::utils::pumpswap::accounts::{
|
|||||||
use crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints;
|
use crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints;
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn effective_quote_reserve(
|
||||||
|
quote_reserve: u64,
|
||||||
|
virtual_quote_reserves: i128,
|
||||||
|
) -> Result<u64, String> {
|
||||||
|
crate::instruction::utils::pumpswap_types::effective_quote_reserves(
|
||||||
|
quote_reserve,
|
||||||
|
virtual_quote_reserves,
|
||||||
|
)
|
||||||
|
.filter(|reserve| *reserve != 0)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"Invalid effective quote reserves: raw={quote_reserve}, virtual={virtual_quote_reserves}."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Creator-side fee bps: fixed coin-creator fee when a creator vault applies, plus optional
|
/// Creator-side fee bps: fixed coin-creator fee when a creator vault applies, plus optional
|
||||||
/// cashback fee bps for cashback-enabled coins (see Pump AMM / parser event field).
|
/// cashback fee bps for cashback-enabled coins (see Pump AMM / parser event field).
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -69,7 +86,8 @@ pub struct SellQuoteInputResult {
|
|||||||
/// * `base` - Amount of base tokens to buy
|
/// * `base` - Amount of base tokens to buy
|
||||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||||
/// * `base_reserve` - Base token reserves in the pool
|
/// * `base_reserve` - Base token reserves in the pool
|
||||||
/// * `quote_reserve` - Quote token reserves in the pool
|
/// * `quote_reserve` - Raw quote-vault balance
|
||||||
|
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||||
/// * `coin_creator` - Token creator address
|
/// * `coin_creator` - Token creator address
|
||||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins (from on-chain / events); use `0` if unknown
|
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins (from on-chain / events); use `0` if unknown
|
||||||
///
|
///
|
||||||
@@ -80,6 +98,7 @@ pub fn buy_base_input_internal(
|
|||||||
slippage_basis_points: u64,
|
slippage_basis_points: u64,
|
||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
|
virtual_quote_reserves: i128,
|
||||||
coin_creator: &Pubkey,
|
coin_creator: &Pubkey,
|
||||||
cashback_fee_basis_points: u64,
|
cashback_fee_basis_points: u64,
|
||||||
) -> Result<BuyBaseInputResult, String> {
|
) -> Result<BuyBaseInputResult, String> {
|
||||||
@@ -88,6 +107,7 @@ pub fn buy_base_input_internal(
|
|||||||
slippage_basis_points,
|
slippage_basis_points,
|
||||||
base_reserve,
|
base_reserve,
|
||||||
quote_reserve,
|
quote_reserve,
|
||||||
|
virtual_quote_reserves,
|
||||||
&PumpSwapFeeBasisPoints::new(
|
&PumpSwapFeeBasisPoints::new(
|
||||||
LP_FEE_BASIS_POINTS,
|
LP_FEE_BASIS_POINTS,
|
||||||
PROTOCOL_FEE_BASIS_POINTS,
|
PROTOCOL_FEE_BASIS_POINTS,
|
||||||
@@ -101,17 +121,19 @@ pub fn buy_base_input_internal_with_fees(
|
|||||||
slippage_basis_points: u64,
|
slippage_basis_points: u64,
|
||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
|
virtual_quote_reserves: i128,
|
||||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||||
) -> Result<BuyBaseInputResult, String> {
|
) -> Result<BuyBaseInputResult, String> {
|
||||||
if base_reserve == 0 || quote_reserve == 0 {
|
if base_reserve == 0 || quote_reserve == 0 {
|
||||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||||
}
|
}
|
||||||
|
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||||
if base > base_reserve {
|
if base > base_reserve {
|
||||||
return Err("Cannot buy more base tokens than the pool reserves.".to_string());
|
return Err("Cannot buy more base tokens than the pool reserves.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate required quote amount using constant product formula
|
// Calculate required quote amount using constant product formula
|
||||||
let numerator = (quote_reserve as u128) * (base as u128);
|
let numerator = (effective_quote_reserve as u128) * (base as u128);
|
||||||
let denominator = base_reserve - base;
|
let denominator = base_reserve - base;
|
||||||
|
|
||||||
if denominator == 0 {
|
if denominator == 0 {
|
||||||
@@ -148,7 +170,8 @@ pub fn buy_base_input_internal_with_fees(
|
|||||||
/// * `quote` - Amount of quote tokens to spend
|
/// * `quote` - Amount of quote tokens to spend
|
||||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||||
/// * `base_reserve` - Base token reserves in the pool
|
/// * `base_reserve` - Base token reserves in the pool
|
||||||
/// * `quote_reserve` - Quote token reserves in the pool
|
/// * `quote_reserve` - Raw quote-vault balance
|
||||||
|
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||||
/// * `coin_creator` - Token creator address
|
/// * `coin_creator` - Token creator address
|
||||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||||
///
|
///
|
||||||
@@ -159,6 +182,7 @@ pub fn buy_quote_input_internal(
|
|||||||
slippage_basis_points: u64,
|
slippage_basis_points: u64,
|
||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
|
virtual_quote_reserves: i128,
|
||||||
coin_creator: &Pubkey,
|
coin_creator: &Pubkey,
|
||||||
cashback_fee_basis_points: u64,
|
cashback_fee_basis_points: u64,
|
||||||
) -> Result<BuyQuoteInputResult, String> {
|
) -> Result<BuyQuoteInputResult, String> {
|
||||||
@@ -167,6 +191,7 @@ pub fn buy_quote_input_internal(
|
|||||||
slippage_basis_points,
|
slippage_basis_points,
|
||||||
base_reserve,
|
base_reserve,
|
||||||
quote_reserve,
|
quote_reserve,
|
||||||
|
virtual_quote_reserves,
|
||||||
&PumpSwapFeeBasisPoints::new(
|
&PumpSwapFeeBasisPoints::new(
|
||||||
LP_FEE_BASIS_POINTS,
|
LP_FEE_BASIS_POINTS,
|
||||||
PROTOCOL_FEE_BASIS_POINTS,
|
PROTOCOL_FEE_BASIS_POINTS,
|
||||||
@@ -180,11 +205,13 @@ pub fn buy_quote_input_internal_with_fees(
|
|||||||
slippage_basis_points: u64,
|
slippage_basis_points: u64,
|
||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
|
virtual_quote_reserves: i128,
|
||||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||||
) -> Result<BuyQuoteInputResult, String> {
|
) -> Result<BuyQuoteInputResult, String> {
|
||||||
if base_reserve == 0 || quote_reserve == 0 {
|
if base_reserve == 0 || quote_reserve == 0 {
|
||||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||||
}
|
}
|
||||||
|
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||||
|
|
||||||
// Calculate total fee basis points
|
// Calculate total fee basis points
|
||||||
let total_fee_bps = fee_basis_points
|
let total_fee_bps = fee_basis_points
|
||||||
@@ -208,7 +235,7 @@ pub fn buy_quote_input_internal_with_fees(
|
|||||||
|
|
||||||
// Calculate base amount out using constant product formula
|
// Calculate base amount out using constant product formula
|
||||||
let numerator = (base_reserve as u128) * input_amount;
|
let numerator = (base_reserve as u128) * input_amount;
|
||||||
let denominator_effective = (quote_reserve as u128) + input_amount;
|
let denominator_effective = (effective_quote_reserve as u128) + input_amount;
|
||||||
|
|
||||||
if denominator_effective == 0 {
|
if denominator_effective == 0 {
|
||||||
return Err("Pool would be depleted; denominator is zero.".to_string());
|
return Err("Pool would be depleted; denominator is zero.".to_string());
|
||||||
@@ -232,7 +259,8 @@ pub fn buy_quote_input_internal_with_fees(
|
|||||||
/// * `base` - Amount of base tokens to sell
|
/// * `base` - Amount of base tokens to sell
|
||||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||||
/// * `base_reserve` - Base token reserves in the pool
|
/// * `base_reserve` - Base token reserves in the pool
|
||||||
/// * `quote_reserve` - Quote token reserves in the pool
|
/// * `quote_reserve` - Raw quote-vault balance
|
||||||
|
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||||
/// * `coin_creator` - Token creator address
|
/// * `coin_creator` - Token creator address
|
||||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||||
///
|
///
|
||||||
@@ -243,6 +271,7 @@ pub fn sell_base_input_internal(
|
|||||||
slippage_basis_points: u64,
|
slippage_basis_points: u64,
|
||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
|
virtual_quote_reserves: i128,
|
||||||
coin_creator: &Pubkey,
|
coin_creator: &Pubkey,
|
||||||
cashback_fee_basis_points: u64,
|
cashback_fee_basis_points: u64,
|
||||||
) -> Result<SellBaseInputResult, String> {
|
) -> Result<SellBaseInputResult, String> {
|
||||||
@@ -251,6 +280,7 @@ pub fn sell_base_input_internal(
|
|||||||
slippage_basis_points,
|
slippage_basis_points,
|
||||||
base_reserve,
|
base_reserve,
|
||||||
quote_reserve,
|
quote_reserve,
|
||||||
|
virtual_quote_reserves,
|
||||||
&PumpSwapFeeBasisPoints::new(
|
&PumpSwapFeeBasisPoints::new(
|
||||||
LP_FEE_BASIS_POINTS,
|
LP_FEE_BASIS_POINTS,
|
||||||
PROTOCOL_FEE_BASIS_POINTS,
|
PROTOCOL_FEE_BASIS_POINTS,
|
||||||
@@ -264,14 +294,16 @@ pub fn sell_base_input_internal_with_fees(
|
|||||||
slippage_basis_points: u64,
|
slippage_basis_points: u64,
|
||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
|
virtual_quote_reserves: i128,
|
||||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||||
) -> Result<SellBaseInputResult, String> {
|
) -> Result<SellBaseInputResult, String> {
|
||||||
if base_reserve == 0 || quote_reserve == 0 {
|
if base_reserve == 0 || quote_reserve == 0 {
|
||||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||||
}
|
}
|
||||||
|
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||||
|
|
||||||
// Calculate quote amount out using constant product formula
|
// Calculate quote amount out using constant product formula
|
||||||
let quote_amount_out = ((quote_reserve as u128) * (base as u128)
|
let quote_amount_out = ((effective_quote_reserve as u128) * (base as u128)
|
||||||
/ ((base_reserve as u128) + (base as u128))) as u64;
|
/ ((base_reserve as u128) + (base as u128))) as u64;
|
||||||
|
|
||||||
// Calculate fees
|
// Calculate fees
|
||||||
@@ -290,6 +322,10 @@ pub fn sell_base_input_internal_with_fees(
|
|||||||
if total_fees > quote_amount_out {
|
if total_fees > quote_amount_out {
|
||||||
return Err("Fees exceed total output; final quote is negative.".to_string());
|
return Err("Fees exceed total output; final quote is negative.".to_string());
|
||||||
}
|
}
|
||||||
|
let quote_vault_outflow = quote_amount_out - lp_fee;
|
||||||
|
if quote_vault_outflow > quote_reserve {
|
||||||
|
return Err("Insufficient real quote reserves to cover the sell output.".to_string());
|
||||||
|
}
|
||||||
let final_quote = quote_amount_out - total_fees;
|
let final_quote = quote_amount_out - total_fees;
|
||||||
|
|
||||||
// Calculate min quote with slippage
|
// Calculate min quote with slippage
|
||||||
@@ -310,12 +346,22 @@ fn calculate_quote_amount_out(
|
|||||||
lp_fee_basis_points: u64,
|
lp_fee_basis_points: u64,
|
||||||
protocol_fee_basis_points: u64,
|
protocol_fee_basis_points: u64,
|
||||||
coin_creator_fee_basis_points: u64,
|
coin_creator_fee_basis_points: u64,
|
||||||
) -> u64 {
|
) -> Result<u64, String> {
|
||||||
let total_fee_basis_points =
|
let total_fee_basis_points = lp_fee_basis_points
|
||||||
lp_fee_basis_points + protocol_fee_basis_points + coin_creator_fee_basis_points;
|
.checked_add(protocol_fee_basis_points)
|
||||||
let denominator = MAX_FEE_BASIS_POINTS - total_fee_basis_points;
|
.and_then(|fees| fees.checked_add(coin_creator_fee_basis_points))
|
||||||
ceil_div((user_quote_amount_out as u128) * (MAX_FEE_BASIS_POINTS as u128), denominator as u128)
|
.ok_or_else(|| "Fee basis points overflow.".to_string())?;
|
||||||
as u64
|
let denominator = MAX_FEE_BASIS_POINTS
|
||||||
|
.checked_sub(total_fee_basis_points)
|
||||||
|
.ok_or_else(|| "Total fee basis points must be less than 10,000.".to_string())?;
|
||||||
|
if denominator == 0 {
|
||||||
|
return Err("Total fee basis points must be less than 10,000.".to_string());
|
||||||
|
}
|
||||||
|
let raw_quote = ceil_div(
|
||||||
|
(user_quote_amount_out as u128) * (MAX_FEE_BASIS_POINTS as u128),
|
||||||
|
denominator as u128,
|
||||||
|
);
|
||||||
|
u64::try_from(raw_quote).map_err(|_| "Calculated quote amount exceeds u64.".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate base tokens needed to receive a specific amount of quote tokens
|
/// Calculate base tokens needed to receive a specific amount of quote tokens
|
||||||
@@ -324,7 +370,8 @@ fn calculate_quote_amount_out(
|
|||||||
/// * `quote` - Desired amount of quote tokens to receive
|
/// * `quote` - Desired amount of quote tokens to receive
|
||||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||||
/// * `base_reserve` - Base token reserves in the pool
|
/// * `base_reserve` - Base token reserves in the pool
|
||||||
/// * `quote_reserve` - Quote token reserves in the pool
|
/// * `quote_reserve` - Raw quote-vault balance
|
||||||
|
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||||
/// * `coin_creator` - Token creator address
|
/// * `coin_creator` - Token creator address
|
||||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||||
///
|
///
|
||||||
@@ -335,6 +382,7 @@ pub fn sell_quote_input_internal(
|
|||||||
slippage_basis_points: u64,
|
slippage_basis_points: u64,
|
||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
|
virtual_quote_reserves: i128,
|
||||||
coin_creator: &Pubkey,
|
coin_creator: &Pubkey,
|
||||||
cashback_fee_basis_points: u64,
|
cashback_fee_basis_points: u64,
|
||||||
) -> Result<SellQuoteInputResult, String> {
|
) -> Result<SellQuoteInputResult, String> {
|
||||||
@@ -343,6 +391,7 @@ pub fn sell_quote_input_internal(
|
|||||||
slippage_basis_points,
|
slippage_basis_points,
|
||||||
base_reserve,
|
base_reserve,
|
||||||
quote_reserve,
|
quote_reserve,
|
||||||
|
virtual_quote_reserves,
|
||||||
&PumpSwapFeeBasisPoints::new(
|
&PumpSwapFeeBasisPoints::new(
|
||||||
LP_FEE_BASIS_POINTS,
|
LP_FEE_BASIS_POINTS,
|
||||||
PROTOCOL_FEE_BASIS_POINTS,
|
PROTOCOL_FEE_BASIS_POINTS,
|
||||||
@@ -356,6 +405,7 @@ pub fn sell_quote_input_internal_with_fees(
|
|||||||
slippage_basis_points: u64,
|
slippage_basis_points: u64,
|
||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
|
virtual_quote_reserves: i128,
|
||||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||||
) -> Result<SellQuoteInputResult, String> {
|
) -> Result<SellQuoteInputResult, String> {
|
||||||
if base_reserve == 0 || quote_reserve == 0 {
|
if base_reserve == 0 || quote_reserve == 0 {
|
||||||
@@ -364,6 +414,7 @@ pub fn sell_quote_input_internal_with_fees(
|
|||||||
if quote > quote_reserve {
|
if quote > quote_reserve {
|
||||||
return Err("Cannot receive more quote tokens than the pool quote reserves.".to_string());
|
return Err("Cannot receive more quote tokens than the pool quote reserves.".to_string());
|
||||||
}
|
}
|
||||||
|
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||||
|
|
||||||
// Calculate raw quote amount including fees
|
// Calculate raw quote amount including fees
|
||||||
let raw_quote = calculate_quote_amount_out(
|
let raw_quote = calculate_quote_amount_out(
|
||||||
@@ -371,19 +422,113 @@ pub fn sell_quote_input_internal_with_fees(
|
|||||||
fee_basis_points.lp_fee_basis_points,
|
fee_basis_points.lp_fee_basis_points,
|
||||||
fee_basis_points.protocol_fee_basis_points,
|
fee_basis_points.protocol_fee_basis_points,
|
||||||
fee_basis_points.coin_creator_fee_basis_points,
|
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 {
|
||||||
|
return Err("Insufficient real quote reserves to cover the sell output.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate base amount needed using inverse constant product formula
|
// Calculate base amount needed using inverse constant product formula
|
||||||
if raw_quote >= quote_reserve {
|
if raw_quote >= effective_quote_reserve {
|
||||||
return Err("Invalid input: Desired quote amount exceeds available reserve.".to_string());
|
return Err("Invalid input: Desired quote amount exceeds available reserve.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let base_amount_in =
|
let base_amount_in = ceil_div(
|
||||||
ceil_div((base_reserve as u128) * (raw_quote as u128), (quote_reserve - raw_quote) as u128)
|
(base_reserve as u128) * (raw_quote as u128),
|
||||||
as u64;
|
(effective_quote_reserve - raw_quote) as u128,
|
||||||
|
) as u64;
|
||||||
|
|
||||||
// Calculate min quote with slippage
|
// Calculate min quote with slippage
|
||||||
let min_quote = calculate_with_slippage_sell(quote, slippage_basis_points);
|
let min_quote = calculate_with_slippage_sell(quote, slippage_basis_points);
|
||||||
|
|
||||||
Ok(SellQuoteInputResult { internal_raw_quote: raw_quote, base: base_amount_in, min_quote })
|
Ok(SellQuoteInputResult { internal_raw_quote: raw_quote, base: base_amount_in, min_quote })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn fees() -> PumpSwapFeeBasisPoints {
|
||||||
|
PumpSwapFeeBasisPoints::new(20, 5, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn buy_uses_effective_quote_reserves() {
|
||||||
|
let result =
|
||||||
|
buy_quote_input_internal_with_fees(10_000, 100, 1_000_000, 1_000_000, 500_000, &fees())
|
||||||
|
.unwrap();
|
||||||
|
let without_virtual =
|
||||||
|
buy_quote_input_internal_with_fees(10_000, 100, 1_000_000, 1_000_000, 0, &fees())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(result.base < without_virtual.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sell_rejects_output_not_covered_by_real_quote_vault() {
|
||||||
|
let error = sell_base_input_internal_with_fees(
|
||||||
|
1_000_000,
|
||||||
|
100,
|
||||||
|
1_000_000,
|
||||||
|
1_000,
|
||||||
|
1_000_000,
|
||||||
|
&fees(),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error, "Insufficient real quote reserves to cover the sell output.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_quote_sell_uses_effective_reserve_for_denominator() {
|
||||||
|
let result =
|
||||||
|
sell_quote_input_internal_with_fees(500, 100, 1_000_000, 1_000, 1_000_000, &fees())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(result.base < 1_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_quote_sell_rejects_output_above_real_quote_vault() {
|
||||||
|
let error =
|
||||||
|
sell_quote_input_internal_with_fees(1_001, 100, 1_000_000, 1_000, 1_000_000, &fees())
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error, "Cannot receive more quote tokens than the pool quote reserves.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negative_virtual_reserves_are_applied() {
|
||||||
|
let result = buy_quote_input_internal_with_fees(
|
||||||
|
10_000,
|
||||||
|
100,
|
||||||
|
1_000_000,
|
||||||
|
1_000_000,
|
||||||
|
-500_000,
|
||||||
|
&fees(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let without_virtual =
|
||||||
|
buy_quote_input_internal_with_fees(10_000, 100, 1_000_000, 1_000_000, 0, &fees())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(result.base > without_virtual.base);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zero_effective_quote_reserves_are_rejected() {
|
||||||
|
let error = buy_quote_input_internal_with_fees(
|
||||||
|
10_000,
|
||||||
|
100,
|
||||||
|
1_000_000,
|
||||||
|
1_000_000,
|
||||||
|
-1_000_000,
|
||||||
|
&fees(),
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error, "Invalid effective quote reserves: raw=1000000, virtual=-1000000.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user