Last active
March 9, 2026 00:54
-
-
Save allenhark/85356636eda0a0c4aef9653ac098e03c to your computer and use it in GitHub Desktop.
pumpfun_amm.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// 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("e_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], | |
| "e_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], | |
| "e_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