Buy token on Raydium AMM v4

Buy token on Raydium AMM v4

Manage alerts

Loading saved threads...

Dan Terry · External communityPost link
External question — Solana Stack Exchange Author: Dan Terry Original post: https://solana.stackexchange.com/questions/23789 License: CC BY-SA 4.0 — https://creativecommons.org/licenses/by-sa/4.0/ Adaptation: HTML converted to plain text; contact email addresses removed. I have this code to buy a token on Raydium AMM v4 but wanted advice from people who already worked on these types of code because it is pretty hard to find a clear documentation with all the instructions and the orders of account. Could anyone check this and give me some advice on what is good and what is bad about my code? Thanks a lot, have a nice day! const RAYDIUM_PROGRAM: &str = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8"; const RAYDIUM_SWAP_DISCRIMINATOR: u8 = 0x09; const SERUM_PROGRAM_ID_V3: &str = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"; #[derive(Debug, Clone)] struct RaydiumPoolInfo { pool_id: Pubkey, authority: Pubkey, open_orders: Pubkey, coin_vault: Pubkey, // AMM coin vault pc_vault: Pubkey, // AMM pc vault market_program: Pubkey, market: Pubkey, bids: Pubkey, asks: Pubkey, event_queue: Pubkey, vault_signer: Pubkey, market_coin_vault: Pubkey, // Market coin vault market_pc_vault: Pubkey, // Market pc vault target_orders: Pubkey, } #[derive(Debug, Clone)] struct MarketAccounts { bids: Pubkey, asks: Pubkey, event_queue: Pubkey, vault_signer: Pubkey, coin_vault: Pubkey, pc_vault: Pubkey, } async fn buy_raydium( &self, wallet: &Keypair, mint: &Pubkey, amount: u64 ) -> Result<(), Box<dyn std::error::Error>> { let start = Instant::now(); let pool_info = self.fetch_raydium_pool_info(mint).await?; let program_id = Pubkey::try_from(RAYDIUM_PROGRAM)?; let wsol_mint = Pubkey::try_from("So11111111111111111111111111111111111111112")?; let user_wsol_account = get_associated_token_address(&wallet.pubkey(), &wsol_mint); let user_token_account = get_associated_token_address(&wallet.pubkey(), mint); let mut instructions = Vec::new(); use spl_associated_token_account::instruction::create_associated_token_account_idempotent; let create_wsol_ata = create_associated_token_account_idempotent( &wallet.pubkey(), &wallet.pubkey(), &wsol_mint, &spl_token::id(), ); instructions.push(create_wsol_ata); let transfer_to_wsol = system_instruction::transfer( &wallet.pubkey(), &user_wsol_account, amount, ); instructions.push(transfer_to_wsol); let sync_native = spl_token::instruction::sync_native(&spl_token::id(), &user_wsol_account)?; instructions.push(sync_native); let create_token_ata = create_associated_token_account_idempotent( &wallet.pubkey(), &wallet.pubkey(), mint, &spl_token::id(), ); instructions.push(create_token_ata); let current_price = self.get_token_price(mint).await.unwrap_or(0.0); let slippage_percent = self.preset.buy_slippage.parse::<f64>().unwrap_or(100.0); let min_tokens_out = if current_price > 0.0 { let expected_tokens = (amount as f64 / LAMPORTS_PER_SOL as f64) / current_price; let min_tokens = expected_tokens * (1.0 - slippage_percent / 100.0); min_tokens.max(1.0) as u64 } else { 1 }; let mut swap_data = Vec::new(); swap_data.push(RAYDIUM_SWAP_DISCRIMINATOR); swap_data.extend_from_slice(&amount.to_le_bytes()); swap_data.extend_from_slice(&min_tokens_out.to_le_bytes()); let accounts = vec![ // spl token AccountMeta::new_readonly(spl_token::id(), false), // amm AccountMeta::new(pool_info.pool_id, false), AccountMeta::new_readonly(pool_info.authority, false), AccountMeta::new(pool_info.open_orders, false), AccountMeta::new(pool_info.coin_vault, false), AccountMeta::new(pool_info.pc_vault, false), // market AccountMeta::new_readonly(pool_info.market_program, false), AccountMeta::new(pool_info.market, false), AccountMeta::new(pool_info.bids, false), AccountMeta::new(pool_info.asks, false), AccountMeta::new(pool_info.event_queue, false), AccountMeta::new(pool_info.market_coin_vault, false), AccountMeta::new(pool_info.market_pc_vault, false), AccountMeta::new_readonly(pool_info.vault_signer, false), // user AccountMeta::new(user_wsol_account, false), AccountMeta::new(user_token_account, false), AccountMeta::new_readonly(wallet.pubkey(), true), ]; let swap_instruction = Instruction { program_id, accounts, data: swap_data, }; instructions.push(swap_instruction); if self.preset.close_account == "yes" { let close_wsol = spl_token::instruction::close_account( &spl_token::id(), &user_wsol_account, &wallet.pubkey(), &wallet.pubkey(), &[], )?; instructions.push(close_wsol); } Ok(()) } async fn fetch_raydium_pool_info(&self, mint: &Pubkey) -> Result<RaydiumPoolInfo, Box<dyn std::error::Error>> { let pool_address = self.derive_raydium_pool_address(mint)?; // check cache if let Some(account_info) = self.grpc_account_cache.lock().await.get(&pool_address.to_string()) { return self.parse_and_complete_pool_info(&pool_address, &account_info.data).await; } self.tracked_accounts.lock().await.insert(pool_address.to_string()); let grpc_cache = self.grpc_account_cache.clone(); let pool_addr_str = pool_address.to_string(); let grpc_task = async move { for _ in 0..5 { tokio::time::sleep(Duration::from_millis(10)).await; if let Some(account_info) = grpc_cache.lock().await.get(&pool_addr_str) { return Some(account_info.clone()); } } None }; let http_task = self.fetch_raydium_pool_info_http(mint); tokio::select! { Some(grpc_info) = grpc_task => { self.parse_and_complete_pool_info(&pool_address, &grpc_info.data).await } http_result = http_task => { http_result } } } fn derive_raydium_pool_address( &self, mint: &Pubkey ) -> Result<Pubkey, Box<dyn std::error::Error>> { let program_id = Pubkey::try_from(RAYDIUM_PROGRAM)?; let wsol = Pubkey::try_from("So11111111111111111111111111111111111111112")?; let (pool_address, _) = Pubkey::find_program_address( &[ b"amm_associated_seed", mint.as_ref(), wsol.as_ref(), ], &program_id, ); Ok(pool_address) } fn parse_raydium_pool( &self, data: &[u8] ) -> Result<RaydiumPoolInfo, Box<dyn std::error::Error>> { const MIN_SIZE: usize = 752; if data.len() < MIN_SIZE { return Err(format!( "Invalid pool data: {} bytes (expected at least {})", data.len(), MIN_SIZE ).into()); } if data[320..352].iter().all(|&b| b == 0) { return Err("Pool data is all zeros - not initialized".into()); } fn safe_pubkey(data: &[u8], start: usize, field: &str) -> Result<Pubkey, Box<dyn std::error::Error>> { let slice = data.get(start..start+32) .ok_or_else(|| format!("Failed to extract {}: out of bounds at offset {}", field, start))?; let array: [u8; 32] = slice.try_into() .map_err(|_| format!("Failed to convert {} to pubkey", field))?; Ok(Pubkey::new_from_array(array)) } Ok(RaydiumPoolInfo { pool_id: Pubkey::default(), authority: Pubkey::default(), open_orders: safe_pubkey(data, 480, "open_orders")?, coin_vault: safe_pubkey(data, 320, "coin_vault")?, pc_vault: safe_pubkey(data, 352, "pc_vault")?, market_program: safe_pubkey(data, 544, "market_program")?, market: safe_pubkey(data, 512, "market")?, target_orders: safe_pubkey(data, 576, "target_orders")?, bids: Pubkey::default(), asks: Pubkey::default(), event_queue: Pubkey::default(), vault_signer: Pubkey::default(), }) } async fn parse_and_complete_pool_info( &self, pool_address: &Pubkey, data: &[u8] ) -> Result<RaydiumPoolInfo, Box<dyn std::error::Error>> { let mut pool_info = self.parse_raydium_pool(data)?; let (authority, _bump) = Pubkey::find_program_address( &[b"amm authority"], &Pubkey::try_from(RAYDIUM_PROGRAM)? ); pool_info.authority = authority; pool_info.pool_id = *pool_address; let tracked = self.tracked_accounts.clone(); let coin = pool_info.coin_vault.to_string(); let pc = pool_info.pc_vault.to_string(); tokio::spawn(async move { let mut t = tracked.lock().await; t.insert(coin); t.insert(pc); }); tokio::spawn(async move { let mut t = tracked.lock().await; t.insert(base); t.insert(quote); }); if pool_info.market != Pubkey::default() { match self.fetch_market_accounts(&pool_info.market).await { Ok(market_accounts) => { pool_info.bids = market_accounts.bids; pool_info.asks = market_accounts.asks; pool_info.event_queue = market_accounts.event_queue; pool_info.vault_signer = market_accounts.vault_signer; pool_info.market_coin_vault = market_accounts.coin_vault; pool_info.market_pc_vault = market_accounts.pc_vault; } Err(e) => { log_message(&format!("Failed to fetch market accounts: {}", e), LogColor::Warning); // Fallback: dériver le vault_signer comme PDA let (vault_signer, _) = Pubkey::find_program_address( &[pool_address.as_ref()], &Pubkey::try_from(RAYDIUM_PROGRAM)? ); pool_info.vault_signer = vault_signer; } } } Ok(pool_info) } async fn fetch_market_accounts(&self, market: &Pubkey) -> Result<MarketAccounts, Box<dyn std::error::Error>> { if let Some(account_info) = self.grpc_account_cache.lock().await.get(&market.to_string()) { return self.parse_serum_market(&account_info.data); } let account = self.rpc_client.get_account_with_commitment( market, CommitmentConfig::confirmed() )?.value.ok_or("Market not found")?; self.parse_serum_market(&account.data) } fn check_market_flags(data: &[u8]) -> Result<(), Box<dyn std::error::Error>> { if data.len() < 13 { return Err("Market data too short".into()); } let account_flags = u64::from_le_bytes( data[5..13].try_into()? ); // AccountFlag::Disabled = 1u64 << 7 = 128 if account_flags & 128 != 0 { return Err("Market is disabled".into()); } Ok(()) } fn parse_serum_market(&self, data: &[u8]) -> Result<MarketAccounts, Box<dyn std::error::Error>> { if data.len() < 388 { return Err(format!("Invalid market account size: {} bytes", data.len()).into()); } self.check_market_flags(data)?; fn safe_pubkey_at(data: &[u8], offset: usize, field: &str) -> Result<Pubkey, Box<dyn std::error::Error>> { let slice = data.get(offset..offset+32) .ok_or_else(|| format!("Failed to extract {} at offset {}", field, offset))?; let array: [u8; 32] = slice.try_into() .map_err(|_| format!("Failed to convert {} to pubkey", field))?; Ok(Pubkey::new_from_array(array)) } let own_address = safe_pubkey_at(data, 13, "own_address")?; let coin_vault = safe_pubkey_at(data, 117, "coin_vault")?; let pc_vault = safe_pubkey_at(data, 165, "pc_vault")?; let event_queue = safe_pubkey_at(data, 253, "event_queue")?; let bids = safe_pubkey_at(data, 285, "bids")?; let asks = safe_pubkey_at(data, 317, "asks")?; let nonce = data.get(45..53)?; let serum_program = Pubkey::try_from(SERUM_PROGRAM_ID_V3)?; let vault_signer_seeds = [own_address.as_ref(), &nonce.to_le_bytes()]; let (vault_signer, _) = Pubkey::find_program_address(&vault_signer_seeds, &serum_program); Ok(MarketAccounts { bids, asks, event_queue, vault_signer, coin_vault, pc_vault, }) } ```
Quote
Report
Aseneca · External communityPost link
External answer — Solana Stack Exchange Author: Aseneca Original post: https://solana.stackexchange.com/a/23872 License: CC BY-SA 4.0 — https://creativecommons.org/licenses/by-sa/4.0/ Adaptation: HTML converted to plain text; contact email addresses removed. So far, you have got a good architectural delineation and have cleanly separated the pool accounts, the market accounts, accounts from GRPC/RPC fallbacks and swap instructions. A couple of things I can point out here that could be critical issues are: You are using the wrong AMM V3 instruction layout. Typically Radyium pools differ by version so they do not all behave alike. If you are using V4, ensure you have the correct instruction layout. You also have some accounts missing in your swap instruction and will cause most of your swaps to fail. Take a look here at the correct accounts required and take note of the account order. Also take note of the correct flags: https://github.com/raydium-io/raydium-amm/blob/master/program/src/instruction.rs , https://github.com/raydium-io/raydium-amm/blob/master/program/src/processor.rs You set slippage_percent = 100% . This is not advisable as you can be frontrun for full balance. Like I mentioned above, different Raydium pools have different layouts so their offsets typically depend on the Raydium pool version. Best to decode the state of the pool instead of using hardcoded offsets as you did above
Quote
Report

Post Reply

Checking account access…