Skip to content

Instantly share code, notes, and snippets.

@allenhark
Last active March 9, 2026 00:54
Show Gist options
  • Select an option

  • Save allenhark/85356636eda0a0c4aef9653ac098e03c to your computer and use it in GitHub Desktop.

Select an option

Save allenhark/85356636eda0a0c4aef9653ac098e03c to your computer and use it in GitHub Desktop.
pumpfun_amm.rs
/// Standalone PumpFun AMM buy-then-sell example.
///
/// 1. Resolves pool accounts on-chain for the given token
/// 2. Buys 0.001 SOL worth of the token
/// 3. Waits 2 seconds
/// 4. Checks on-chain token balance
/// 5. Sells all tokens back for SOL
///
/// Usage:
/// RUST_LOG=debug cargo run --example pumpfun_amm # simulate only
/// RUST_LOG=info cargo run --example pumpfun_amm -- --send # ACTUALLY SEND
use std::str::FromStr;
use anyhow::{Context, Result};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_client::rpc_config::RpcSimulateTransactionConfig;
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::compute_budget::ComputeBudgetInstruction;
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::message::{VersionedMessage, v0};
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer;
#[allow(deprecated)]
use solana_sdk::system_program;
use solana_sdk::transaction::VersionedTransaction;
use tracing::{debug, error, info, warn};
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const TOKEN_MINT: &str = "a3W4qutoEJA4232T2gwZUfgYJTetr96pU4SJMwppump";
const BUY_AMOUNT_SOL: f64 = 0.001;
const COMPUTE_UNIT_LIMIT: u32 = 400_000;
const PRIORITY_FEE_MICRO_LAMPORTS: u64 = 100_000;
// ---------------------------------------------------------------------------
// Program IDs
// ---------------------------------------------------------------------------
fn pumpfun_program_id() -> Pubkey {
Pubkey::from_str("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA").unwrap()
}
fn ata_program_id() -> Pubkey {
Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap()
}
fn wsol_mint() -> Pubkey {
solana_sdk::pubkey!("So11111111111111111111111111111111111111112")
}
// ---------------------------------------------------------------------------
// ATA derivation
// ---------------------------------------------------------------------------
fn derive_ata(wallet: &Pubkey, mint: &Pubkey) -> Pubkey {
derive_ata_with_program(wallet, mint, &spl_token::ID)
}
fn derive_ata_with_program(wallet: &Pubkey, mint: &Pubkey, token_program: &Pubkey) -> Pubkey {
let (ata, _) = Pubkey::find_program_address(
&[wallet.as_ref(), token_program.as_ref(), mint.as_ref()],
&ata_program_id(),
);
ata
}
fn anchor_discriminator(name: &str) -> [u8; 8] {
let full = format!("global:{name}");
let hash = solana_sdk::hash::hash(full.as_bytes());
hash.to_bytes()[..8].try_into().unwrap()
}
fn pubkey_at(data: &[u8], offset: usize) -> Result<Pubkey> {
anyhow::ensure!(
data.len() >= offset + 32,
"data too short at offset {offset}"
);
Ok(Pubkey::new_from_array(
data[offset..offset + 32].try_into()?,
))
}
// ---------------------------------------------------------------------------
// Pool account resolution
// ---------------------------------------------------------------------------
#[derive(Debug, Clone)]
struct PumpFunPoolAccounts {
protocol_fee_recipient: Pubkey,
coin_creator: Pubkey,
base_token_program: Pubkey,
base_reserve: u64,
quote_reserve: u64,
is_cashback_coin: bool,
}
/// Pool fields we read from the discovered_pools DB row (or hardcode for this example).
#[derive(Debug, Clone)]
struct PoolInfo {
address: Pubkey,
base_mint: Pubkey, // the non-SOL token
quote_mint: Pubkey, // wSOL
base_vault: Pubkey,
quote_vault: Pubkey,
}
const PUMPFUN_POOL_BASE_VAULT_OFFSET: usize = 139;
const PUMPFUN_POOL_QUOTE_VAULT_OFFSET: usize = 171;
async fn resolve_pool_accounts(
rpc: &RpcClient,
pool_address: &Pubkey,
base_mint: &Pubkey,
) -> Result<PumpFunPoolAccounts> {
let program_id = pumpfun_program_id();
let (global_config, _) = Pubkey::find_program_address(&[b"global_config"], &program_id);
info!("Resolving PumpFun pool {pool_address}...");
let (pool_data, config_data) = tokio::try_join!(
async {
rpc.get_account_data(pool_address)
.await
.map_err(|e| anyhow::anyhow!("Failed to fetch pool: {e}"))
},
async {
rpc.get_account_data(&global_config)
.await
.map_err(|e| anyhow::anyhow!("Failed to fetch global_config: {e}"))
}
)?;
anyhow::ensure!(
pool_data.len() >= 245,
"Pool data too short: {}",
pool_data.len()
);
anyhow::ensure!(
config_data.len() >= 89,
"GlobalConfig too short: {}",
config_data.len()
);
let coin_creator = pubkey_at(&pool_data, 211)?;
let is_cashback_coin = pool_data[244] != 0;
let protocol_fee_recipient = pubkey_at(&config_data, 57)?;
let base_vault = pubkey_at(&pool_data, PUMPFUN_POOL_BASE_VAULT_OFFSET)?;
let quote_vault = pubkey_at(&pool_data, PUMPFUN_POOL_QUOTE_VAULT_OFFSET)?;
let base_token_program = rpc
.get_account(base_mint)
.await
.ok()
.map(|acc| acc.owner)
.unwrap_or(spl_token::ID);
let (base_reserve, quote_reserve) = tokio::try_join!(
async {
rpc.get_token_account_balance(&base_vault)
.await
.map(|b| b.amount.parse::<u64>().unwrap_or(0))
.map_err(|e| anyhow::anyhow!("base vault balance: {e}"))
},
async {
rpc.get_token_account_balance(&quote_vault)
.await
.map(|b| b.amount.parse::<u64>().unwrap_or(0))
.map_err(|e| anyhow::anyhow!("quote vault balance: {e}"))
}
)?;
info!(
" fee_recipient={}, coin_creator={}, cashback={}, base_program={}",
&protocol_fee_recipient.to_string()[..8],
&coin_creator.to_string()[..8],
is_cashback_coin,
&base_token_program.to_string()[..8],
);
info!(
" base_reserve={base_reserve}, quote_reserve={quote_reserve} ({:.4} SOL)",
quote_reserve as f64 / 1e9,
);
Ok(PumpFunPoolAccounts {
protocol_fee_recipient,
coin_creator,
base_token_program,
base_reserve,
quote_reserve,
is_cashback_coin,
})
}
/// Discover the PumpFun pool for the given token via RPC (no DB needed).
///
/// Uses `getProgramAccounts` with memcmp filters on the pool's base_mint field
/// (offset 43 in PumpFun pool layout) to find the pool on-chain.
/// Falls back to checking quote_mint (offset 75) if not found as base.
async fn find_pool_via_rpc(rpc: &RpcClient, token_mint: &Pubkey) -> Result<PoolInfo> {
use solana_account_decoder::UiAccountEncoding;
use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
use solana_client::rpc_filter::{Memcmp, RpcFilterType};
let program_id = pumpfun_program_id();
let sol = wsol_mint();
// PumpFun pool layout (after 8-byte discriminator):
// offset 43: base_mint (32 bytes)
// offset 75: quote_mint (32 bytes)
// offset 139: pool_base_token_account (vault_a, 32 bytes)
// offset 171: pool_quote_token_account (vault_b, 32 bytes)
info!("Searching for PumpFun pool with base_mint={token_mint} via RPC...");
let config = RpcProgramAccountsConfig {
filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
43,
token_mint.to_bytes().to_vec(),
))]),
account_config: RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
..Default::default()
},
..Default::default()
};
let accounts = rpc
.get_program_accounts_with_config(&program_id, config)
.await
.map_err(|e| anyhow::anyhow!("getProgramAccounts failed: {e}"))?;
if let Some((pool_address, account)) = accounts.into_iter().next() {
let data = &account.data;
anyhow::ensure!(data.len() >= 203, "Pool data too short: {}", data.len());
let base_mint = pubkey_at(data, 43)?;
let quote_mint = pubkey_at(data, 75)?;
let base_vault = pubkey_at(data, PUMPFUN_POOL_BASE_VAULT_OFFSET)?;
let quote_vault = pubkey_at(data, PUMPFUN_POOL_QUOTE_VAULT_OFFSET)?;
info!(
"Found pool: {pool_address} (base={}, quote={})",
&base_mint.to_string()[..8],
&quote_mint.to_string()[..8]
);
return Ok(PoolInfo {
address: pool_address,
base_mint,
quote_mint,
base_vault,
quote_vault,
});
}
// Token might be quote_mint instead of base — try offset 75
info!("Not found as base_mint, trying quote_mint...");
let config2 = RpcProgramAccountsConfig {
filters: Some(vec![RpcFilterType::Memcmp(Memcmp::new_raw_bytes(
75,
token_mint.to_bytes().to_vec(),
))]),
account_config: RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
..Default::default()
},
..Default::default()
};
let accounts2 = rpc
.get_program_accounts_with_config(&program_id, config2)
.await
.map_err(|e| anyhow::anyhow!("getProgramAccounts (quote) failed: {e}"))?;
if let Some((pool_address, account)) = accounts2.into_iter().next() {
let data = &account.data;
anyhow::ensure!(data.len() >= 203, "Pool data too short: {}", data.len());
let mint_a = pubkey_at(data, 43)?;
let mint_b = pubkey_at(data, 75)?;
let vault_a = pubkey_at(data, PUMPFUN_POOL_BASE_VAULT_OFFSET)?;
let vault_b = pubkey_at(data, PUMPFUN_POOL_QUOTE_VAULT_OFFSET)?;
// Swap so base = non-SOL token
let (base_mint, quote_mint, base_vault, quote_vault) = if mint_a == sol {
(mint_b, mint_a, vault_b, vault_a)
} else {
(mint_a, mint_b, vault_a, vault_b)
};
info!(
"Found pool: {pool_address} (base={}, quote={})",
&base_mint.to_string()[..8],
&quote_mint.to_string()[..8]
);
return Ok(PoolInfo {
address: pool_address,
base_mint,
quote_mint,
base_vault,
quote_vault,
});
}
anyhow::bail!("No PumpFun pool found for token {token_mint}")
}
// ---------------------------------------------------------------------------
// Instruction builders
// ---------------------------------------------------------------------------
fn build_init_volume_accumulator_ix(payer: &Pubkey) -> Instruction {
let pump_id = pumpfun_program_id();
let (user_vol_acc, _) =
Pubkey::find_program_address(&[b"user_volume_accumulator", payer.as_ref()], &pump_id);
let (event_auth, _) = Pubkey::find_program_address(&[b"__event_authority"], &pump_id);
Instruction {
program_id: pump_id,
accounts: vec![
AccountMeta::new(*payer, true),
AccountMeta::new_readonly(*payer, false),
AccountMeta::new(user_vol_acc, false),
AccountMeta::new_readonly(system_program::ID, false),
AccountMeta::new_readonly(event_auth, false),
AccountMeta::new_readonly(pump_id, false),
],
data: anchor_discriminator("init_user_volume_accumulator").to_vec(),
}
}
fn build_buy_ix(
pool: &PoolInfo,
payer: &Pubkey,
max_sol_in: u64,
min_tokens_out: u64,
resolved: &PumpFunPoolAccounts,
) -> Result<Instruction> {
let program_id = pumpfun_program_id();
let user_base_ata =
derive_ata_with_program(payer, &pool.base_mint, &resolved.base_token_program);
let user_quote_ata = derive_ata(payer, &pool.quote_mint);
let protocol_fee_recipient_ata = derive_ata(&resolved.protocol_fee_recipient, &pool.quote_mint);
let (coin_creator_vault_authority, _) = Pubkey::find_program_address(
&[b"creator_vault", resolved.coin_creator.as_ref()],
&program_id,
);
let coin_creator_vault_ata = derive_ata(&coin_creator_vault_authority, &pool.quote_mint);
let (global_volume_accumulator, _) =
Pubkey::find_program_address(&[b"global_volume_accumulator"], &program_id);
let (user_volume_accumulator, _) =
Pubkey::find_program_address(&[b"user_volume_accumulator", payer.as_ref()], &program_id);
let (global_config, _) = Pubkey::find_program_address(&[b"global_config"], &program_id);
let (event_authority, _) = Pubkey::find_program_address(&[b"__event_authority"], &program_id);
let fee_program = Pubkey::from_str("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ").unwrap();
let key: [u8; 32] = [
12, 20, 222, 252, 130, 94, 198, 118, 148, 37, 8, 24, 187, 101, 64, 101, 244, 41, 141, 49,
86, 213, 113, 180, 212, 248, 9, 12, 24, 233, 168, 99,
];
let (fee_config, _) = Pubkey::find_program_address(&[b"fee_config", &key], &fee_program);
let (pool_v2, _) =
Pubkey::find_program_address(&[b"pool-v2", pool.base_mint.as_ref()], &program_id);
// buy_exact_quote_in(spendable_quote_in: u64, min_base_amount_out: u64)
let disc: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112];
let mut data = Vec::with_capacity(24);
data.extend_from_slice(&disc);
data.extend_from_slice(&max_sol_in.to_le_bytes());
data.extend_from_slice(&min_tokens_out.to_le_bytes());
#[allow(deprecated)]
let mut accounts = vec![
AccountMeta::new(pool.address, false), // 0: pool
AccountMeta::new(*payer, true), // 1: user
AccountMeta::new_readonly(global_config, false), // 2: global_config
AccountMeta::new_readonly(pool.base_mint, false), // 3: base_mint
AccountMeta::new_readonly(pool.quote_mint, false), // 4: quote_mint
AccountMeta::new(user_base_ata, false), // 5: user_base_token
AccountMeta::new(user_quote_ata, false), // 6: user_quote_token
AccountMeta::new(pool.base_vault, false), // 7: pool_base_vault
AccountMeta::new(pool.quote_vault, false), // 8: pool_quote_vault
AccountMeta::new_readonly(resolved.protocol_fee_recipient, false), // 9
AccountMeta::new(protocol_fee_recipient_ata, false), // 10
AccountMeta::new_readonly(resolved.base_token_program, false), // 11
AccountMeta::new_readonly(spl_token::ID, false), // 12: quote_token_program
AccountMeta::new_readonly(system_program::ID, false), // 13
AccountMeta::new_readonly(ata_program_id(), false), // 14
AccountMeta::new_readonly(event_authority, false), // 15
AccountMeta::new_readonly(program_id, false), // 16: program
AccountMeta::new(coin_creator_vault_ata, false), // 17
AccountMeta::new_readonly(coin_creator_vault_authority, false), // 18
// BUY-only: volume accumulators + fee
AccountMeta::new(global_volume_accumulator, false), // 19 [WRITABLE]
AccountMeta::new(user_volume_accumulator, false), // 20 [WRITABLE]
AccountMeta::new_readonly(fee_config, false), // 21
AccountMeta::new_readonly(fee_program, false), // 22
];
if resolved.is_cashback_coin {
let user_volume_accumulator_wsol_ata =
derive_ata(&user_volume_accumulator, &pool.quote_mint);
accounts.push(AccountMeta::new(user_volume_accumulator_wsol_ata, false)); // 23
}
accounts.push(AccountMeta::new(pool_v2, false)); // 24 (or 23 if not cashback)
Ok(Instruction {
program_id,
accounts,
data,
})
}
fn build_sell_ix(
pool: &PoolInfo,
payer: &Pubkey,
token_amount_in: u64,
min_sol_out: u64,
resolved: &PumpFunPoolAccounts,
) -> Result<Instruction> {
let program_id = pumpfun_program_id();
let user_base_ata =
derive_ata_with_program(payer, &pool.base_mint, &resolved.base_token_program);
let user_quote_ata = derive_ata(payer, &pool.quote_mint);
let protocol_fee_recipient_ata = derive_ata(&resolved.protocol_fee_recipient, &pool.quote_mint);
let (coin_creator_vault_authority, _) = Pubkey::find_program_address(
&[b"creator_vault", resolved.coin_creator.as_ref()],
&program_id,
);
let coin_creator_vault_ata = derive_ata(&coin_creator_vault_authority, &pool.quote_mint);
let (global_config, _) = Pubkey::find_program_address(&[b"global_config"], &program_id);
let (event_authority, _) = Pubkey::find_program_address(&[b"__event_authority"], &program_id);
let fee_program = Pubkey::from_str("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ").unwrap();
let key: [u8; 32] = [
12, 20, 222, 252, 130, 94, 198, 118, 148, 37, 8, 24, 187, 101, 64, 101, 244, 41, 141, 49,
86, 213, 113, 180, 212, 248, 9, 12, 24, 233, 168, 99,
];
let (fee_config, _) = Pubkey::find_program_address(&[b"fee_config", &key], &fee_program);
let (pool_v2, _) =
Pubkey::find_program_address(&[b"pool-v2", pool.base_mint.as_ref()], &program_id);
// sell(base_amount_in: u64, min_quote_amount_out: u64) — no track_volume
let disc: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
let mut data = Vec::with_capacity(24);
data.extend_from_slice(&disc);
data.extend_from_slice(&token_amount_in.to_le_bytes());
data.extend_from_slice(&min_sol_out.to_le_bytes());
#[allow(deprecated)]
let mut accounts = vec![
AccountMeta::new(pool.address, false), // 0: pool
AccountMeta::new(*payer, true), // 1: user
AccountMeta::new_readonly(global_config, false), // 2: global_config
AccountMeta::new_readonly(pool.base_mint, false), // 3: base_mint
AccountMeta::new_readonly(pool.quote_mint, false), // 4: quote_mint
AccountMeta::new(user_base_ata, false), // 5: user_base_token
AccountMeta::new(user_quote_ata, false), // 6: user_quote_token
AccountMeta::new(pool.base_vault, false), // 7: pool_base_vault
AccountMeta::new(pool.quote_vault, false), // 8: pool_quote_vault
AccountMeta::new_readonly(resolved.protocol_fee_recipient, false), // 9
AccountMeta::new(protocol_fee_recipient_ata, false), // 10
AccountMeta::new_readonly(resolved.base_token_program, false), // 11
AccountMeta::new_readonly(spl_token::ID, false), // 12: quote_token_program
AccountMeta::new_readonly(system_program::ID, false), // 13
AccountMeta::new_readonly(ata_program_id(), false), // 14
AccountMeta::new_readonly(event_authority, false), // 15
AccountMeta::new_readonly(program_id, false), // 16: program
AccountMeta::new(coin_creator_vault_ata, false), // 17
AccountMeta::new_readonly(coin_creator_vault_authority, false), // 18
// SELL-only: fee (no volume accumulators)
AccountMeta::new_readonly(fee_config, false), // 19
AccountMeta::new_readonly(fee_program, false), // 20
];
if resolved.is_cashback_coin {
let (user_volume_accumulator, _) = Pubkey::find_program_address(
&[b"user_volume_accumulator", payer.as_ref()],
&program_id,
);
let user_volume_accumulator_wsol_ata =
derive_ata(&user_volume_accumulator, &pool.quote_mint);
accounts.push(AccountMeta::new(user_volume_accumulator_wsol_ata, false)); // 21
accounts.push(AccountMeta::new(user_volume_accumulator, false)); // 22
}
accounts.push(AccountMeta::new(pool_v2, false)); // 23 (or 21)
Ok(Instruction {
program_id,
accounts,
data,
})
}
// ---------------------------------------------------------------------------
// TX building + sending helpers
// ---------------------------------------------------------------------------
async fn build_and_send(
rpc: &RpcClient,
wallet: &Keypair,
instructions: Vec<Instruction>,
label: &str,
send_for_real: bool,
) -> Result<bool> {
let payer = wallet.pubkey();
let blockhash = rpc.get_latest_blockhash().await?;
let message = v0::Message::try_compile(&payer, &instructions, &[], blockhash)?;
let tx = VersionedTransaction::try_new(VersionedMessage::V0(message), &[wallet])?;
let size = bincode::serialize(&tx).map(|b| b.len()).unwrap_or(0);
info!(
"[{label}] TX size: {size} bytes | IXs: {}",
instructions.len()
);
// Simulate first
info!("[{label}] Simulating...");
let sim_config = RpcSimulateTransactionConfig {
sig_verify: false,
commitment: Some(CommitmentConfig::confirmed()),
replace_recent_blockhash: true,
..Default::default()
};
let sim = rpc
.simulate_transaction_with_config(&tx, sim_config)
.await?;
let result = &sim.value;
if let Some(ref err) = result.err {
error!("[{label}] SIMULATION FAILED: {err:?}");
if let Some(ref logs) = result.logs {
for log in logs {
if log.contains("Error") || log.contains("error") || log.contains("failed") {
warn!(" {log}");
} else {
debug!(" {log}");
}
}
}
return Ok(false);
}
if let Some(units) = result.units_consumed {
info!("[{label}] Simulation OK — {units} CUs consumed");
}
if send_for_real {
info!("[{label}] SENDING TX...");
match rpc.send_and_confirm_transaction_with_spinner(&tx).await {
Ok(sig) => {
info!("[{label}] TX CONFIRMED: {sig}");
return Ok(true);
}
Err(e) => {
error!("[{label}] TX SEND FAILED: {e}");
return Ok(false);
}
}
} else {
info!("[{label}] Simulation-only mode (use --send to execute)");
}
Ok(true)
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
#[tokio::main]
async fn main() -> Result<()> {
dotenv::dotenv().ok();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "pumpfun_amm=debug,solana_client=warn".into()),
)
.init();
let send_for_real = std::env::args().any(|a| a == "--send");
if send_for_real {
warn!("--send flag: transactions WILL be sent to the network!");
}
// 1. Load config
let rpc_url = std::env::var("DISCOVERY_RPC_URL")
.or_else(|_| std::env::var("RPC_URL"))
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".into());
let wallet_path = std::env::var("WALLET_PATH").unwrap_or_else(|_| "wallet.json".into());
let wallet_json = std::fs::read_to_string(&wallet_path)
.with_context(|| format!("Failed to read wallet: {wallet_path}"))?;
let wallet_bytes: Vec<u8> =
serde_json::from_str(&wallet_json).context("Invalid wallet JSON")?;
let wallet = Keypair::try_from(wallet_bytes.as_slice()).map_err(|e| anyhow::anyhow!("{e}"))?;
let payer = wallet.pubkey();
info!("Wallet: {payer}");
info!("RPC: {rpc_url}");
info!("Token: {TOKEN_MINT}");
info!("Buy amount: {BUY_AMOUNT_SOL} SOL");
// 2. Connect
let rpc = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());
let balance = rpc.get_balance(&payer).await?;
info!("Wallet balance: {:.9} SOL", balance as f64 / 1e9);
// 3. Find pool via RPC and resolve accounts
let token_mint = Pubkey::from_str(TOKEN_MINT)?;
let pool = find_pool_via_rpc(&rpc, &token_mint).await?;
let resolved = resolve_pool_accounts(&rpc, &pool.address, &pool.base_mint).await?;
let buy_lamports = (BUY_AMOUNT_SOL * 1e9) as u64;
// Estimate tokens out using constant-product formula
if resolved.quote_reserve == 0 || resolved.base_reserve == 0 {
anyhow::bail!("Pool has zero reserves — cannot trade");
}
let num = buy_lamports as u128 * resolved.base_reserve as u128;
let den = resolved.quote_reserve as u128 + buy_lamports as u128;
let raw_tokens = (num / den) as u64;
// CRITICAL: PumpFun program does `base_amount_out * quote_reserve` in u64 checked math.
// If this overflows, the TX fails with error 6023 (Overflow).
// Cap base_amount_out so the multiplication stays within u64 bounds.
let overflow_cap = u64::MAX / resolved.quote_reserve.max(1);
let capped_tokens = raw_tokens.min(overflow_cap);
let estimated_tokens = capped_tokens * 98 / 100; // 2% slippage buffer
if capped_tokens < raw_tokens {
warn!(
"Capped base_amount_out from {raw_tokens} to {capped_tokens} to avoid u64 overflow \
(quote_reserve={}, cap={})",
resolved.quote_reserve, overflow_cap,
);
}
info!("Estimated buy: {buy_lamports} lamports → ~{estimated_tokens} tokens (with 2% slippage)",);
// -----------------------------------------------------------------------
// 4. BUY transaction
// -----------------------------------------------------------------------
let mut buy_ixs: Vec<Instruction> = Vec::new();
// Compute budget
buy_ixs.push(ComputeBudgetInstruction::set_compute_unit_limit(
COMPUTE_UNIT_LIMIT,
));
buy_ixs.push(ComputeBudgetInstruction::set_compute_unit_price(
PRIORITY_FEE_MICRO_LAMPORTS,
));
// Create ATAs if needed
buy_ixs.push(
spl_associated_token_account::instruction::create_associated_token_account_idempotent(
&payer,
&payer,
&pool.base_mint,
&resolved.base_token_program,
),
);
buy_ixs.push(
spl_associated_token_account::instruction::create_associated_token_account_idempotent(
&payer,
&payer,
&pool.quote_mint,
&spl_token::ID,
),
);
let user_quote_ata = derive_ata(&payer, &pool.quote_mint);
// Transfer SOL to the wSOL ATA and Sync Native to wrap it
buy_ixs.push(solana_sdk::system_instruction::transfer(
&payer,
&user_quote_ata,
buy_lamports,
));
buy_ixs.push(spl_token::instruction::sync_native(&spl_token::ID, &user_quote_ata).unwrap());
// Init volume accumulator (idempotent)
buy_ixs.push(build_init_volume_accumulator_ix(&payer));
// Buy swap
buy_ixs.push(build_buy_ix(
&pool,
&payer,
buy_lamports,
estimated_tokens,
&resolved,
)?);
info!("=== BUY: {BUY_AMOUNT_SOL} SOL → ~{estimated_tokens} tokens ===");
// OVERRIDE: Skip executing the buy for now to test the sell instruction.
let buy_ok = build_and_send(&rpc, &wallet, buy_ixs, "BUY", send_for_real).await?;
//let buy_ok = true;
if !buy_ok {
error!("Buy failed — aborting sell.");
return Ok(());
}
// -----------------------------------------------------------------------
// 5. Wait + check balance
// -----------------------------------------------------------------------
info!("Waiting 2 seconds...");
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let user_token_ata =
derive_ata_with_program(&payer, &pool.base_mint, &resolved.base_token_program);
let token_balance = match rpc.get_token_account_balance(&user_token_ata).await {
Ok(bal) => {
let amount = bal.amount.parse::<u64>().unwrap_or(0);
info!("Token balance: {} (ui: {})", amount, bal.ui_amount_string,);
amount
}
Err(e) => {
warn!("Could not fetch token balance: {e}");
0
}
};
if token_balance == 0 {
error!("No tokens to sell — balance is 0");
return Ok(());
}
let sell_amount = token_balance;
if sell_amount == 0 {
info!("Nothing to sell. Done.");
return Ok(());
}
// -----------------------------------------------------------------------
// 6. SELL transaction
// -----------------------------------------------------------------------
// Re-resolve pool accounts for fresh reserves
let resolved = resolve_pool_accounts(&rpc, &pool.address, &pool.base_mint).await?;
let mut sell_ixs: Vec<Instruction> = Vec::new();
// Compute budget
sell_ixs.push(ComputeBudgetInstruction::set_compute_unit_limit(
COMPUTE_UNIT_LIMIT,
));
sell_ixs.push(ComputeBudgetInstruction::set_compute_unit_price(
PRIORITY_FEE_MICRO_LAMPORTS,
));
// Sell swap (min_sol_out = 0 for simplicity / max slippage)
sell_ixs.push(build_sell_ix(&pool, &payer, sell_amount, 0, &resolved)?);
info!("=== SELL: {sell_amount} tokens → SOL ===");
build_and_send(&rpc, &wallet, sell_ixs, "SELL", send_for_real).await?;
info!("Done.");
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment