1use crate::{SwapInstructionBuilder, StateDecoder};
2use anyhow::Result;
3use async_trait::async_trait;
4use borsh::BorshDeserialize;
5use crate::raydium_state::RaydiumAmmV4;
6use scematica_core::{dex::program_ids, types::DexKind};
7use solana_client::nonblocking::rpc_client::RpcClient;
8use solana_sdk::{
9 instruction::{AccountMeta, Instruction},
10 pubkey::Pubkey,
11};
12use std::sync::Arc;
13
14pub struct RaydiumBuilder {
16 rpc: Arc<RpcClient>,
17}
18
19impl RaydiumBuilder {
20 pub fn new(rpc: Arc<RpcClient>) -> Self {
21 Self { rpc }
22 }
23}
24
25impl StateDecoder for RaydiumBuilder {
26 fn decode_pool_state(&self, data: &[u8]) -> Result<(u64, u64)> {
27 let mut slice = data;
28 let state = RaydiumAmmV4::deserialize(&mut slice)?;
29 Ok((state.lp_reserve, 0))
30 }
31}
32
33fn raydium_swap_data(amount_in: u64, min_amount_out: u64) -> Vec<u8> {
35 let mut data = vec![9u8];
36 data.extend_from_slice(&amount_in.to_le_bytes());
37 data.extend_from_slice(&min_amount_out.to_le_bytes());
38 data
39}
40
41mod serum_offsets {
66 pub const VAULT_SIGNER_NONCE: usize = 45;
67 pub const BASE_VAULT: usize = 117;
68 pub const PC_VAULT: usize = 165;
69 pub const EVENT_QUEUE: usize = 253;
70 pub const BIDS: usize = 285;
71 pub const ASKS: usize = 317;
72}
73
74fn read_pubkey(data: &[u8], offset: usize) -> Result<Pubkey> {
76 Pubkey::try_from(&data[offset..offset + 32])
77 .map_err(|_| anyhow::anyhow!("failed to read Pubkey at serum offset {}", offset))
78}
79
80fn derive_vault_signer(market: &Pubkey, nonce: u64, market_program: &Pubkey) -> Result<Pubkey> {
82 let nonce_bytes = nonce.to_le_bytes();
83 Pubkey::create_program_address(
85 &[market.as_ref(), &nonce_bytes],
86 market_program,
87 ).map_err(|e| anyhow::anyhow!("vault signer derivation failed: {}", e))
88}
89
90#[async_trait]
91impl SwapInstructionBuilder for RaydiumBuilder {
92 fn dex(&self) -> DexKind {
93 DexKind::Raydium
94 }
95
96 async fn build_swap(
97 &self,
98 pool: &Pubkey,
99 owner: &Pubkey,
100 _token_in: &Pubkey,
101 _token_out: &Pubkey,
102 ata_in: &Pubkey,
103 ata_out: &Pubkey,
104 amount_in: u64,
105 min_amount_out: u64,
106 ) -> Result<Vec<Instruction>> {
107 use scematica_core::dex::raydium_v4 as offsets;
108
109 let pool_data = self.rpc.get_account_data(pool).await
111 .map_err(|e| anyhow::anyhow!("RPC error fetching pool {}: {}", pool, e))?;
112
113 if pool_data.len() < offsets::POOL_STATE_SIZE {
114 anyhow::bail!("pool data too short: {} bytes for {}", pool_data.len(), pool);
115 }
116
117 let status = u64::from_le_bytes(pool_data[0..8].try_into()?);
119 if status == 0 {
120 anyhow::bail!("pool {} is uninitialized", pool);
121 }
122
123 let pool_base_vault = read_pubkey(&pool_data, offsets::BASE_VAULT_OFFSET)?;
125 let pool_quote_vault = read_pubkey(&pool_data, offsets::QUOTE_VAULT_OFFSET)?;
126 let open_orders = read_pubkey(&pool_data, offsets::OPEN_ORDERS_OFFSET)?;
127 let target_orders = read_pubkey(&pool_data, offsets::TARGET_ORDERS_OFFSET)?;
128 let market_id = read_pubkey(&pool_data, offsets::MARKET_ID_OFFSET)?;
129 let market_program_id = read_pubkey(&pool_data, offsets::MARKET_PROGRAM_OFFSET)?;
130
131 let market_data = self.rpc.get_account_data(&market_id).await
133 .map_err(|e| anyhow::anyhow!("RPC error fetching market {}: {}", market_id, e))?;
134
135 const MIN_MARKET_LEN: usize = serum_offsets::PC_VAULT + 32;
136 if market_data.len() < MIN_MARKET_LEN {
137 anyhow::bail!("market data too short: {} < {} for market {}", market_data.len(), MIN_MARKET_LEN, market_id);
138 }
139
140 let bids = read_pubkey(&market_data, serum_offsets::BIDS)?;
141 let asks = read_pubkey(&market_data, serum_offsets::ASKS)?;
142 let event_queue = read_pubkey(&market_data, serum_offsets::EVENT_QUEUE)?;
143 let mkt_base_vault = read_pubkey(&market_data, serum_offsets::BASE_VAULT)?;
144 let mkt_pc_vault = read_pubkey(&market_data, serum_offsets::PC_VAULT)?;
145
146 let nonce_bytes: [u8; 8] = market_data[serum_offsets::VAULT_SIGNER_NONCE
147 ..serum_offsets::VAULT_SIGNER_NONCE + 8]
148 .try_into()
149 .map_err(|_| anyhow::anyhow!("failed to read vault signer nonce"))?;
150 let vault_signer_nonce = u64::from_le_bytes(nonce_bytes);
151 let vault_signer = derive_vault_signer(&market_id, vault_signer_nonce, &market_program_id)?;
152
153 let (amm_authority, _) = Pubkey::find_program_address(
154 &[b"amm authority"],
155 &program_ids::RAYDIUM_AMM_V4,
156 );
157
158 let accounts = vec![
159 AccountMeta::new_readonly(spl_token::id(), false),
160 AccountMeta::new(*pool, false),
161 AccountMeta::new_readonly(amm_authority, false),
162 AccountMeta::new(open_orders, false),
163 AccountMeta::new(target_orders, false),
164 AccountMeta::new(pool_base_vault, false),
165 AccountMeta::new(pool_quote_vault, false),
166 AccountMeta::new_readonly(market_program_id, false),
167 AccountMeta::new(market_id, false),
168 AccountMeta::new(bids, false),
169 AccountMeta::new(asks, false),
170 AccountMeta::new(event_queue, false),
171 AccountMeta::new(mkt_base_vault, false),
172 AccountMeta::new(mkt_pc_vault, false),
173 AccountMeta::new_readonly(vault_signer, false),
174 AccountMeta::new(*ata_in, false),
175 AccountMeta::new(*ata_out, false),
176 AccountMeta::new_readonly(*owner, true),
177 ];
178
179 Ok(vec![Instruction {
180 program_id: program_ids::RAYDIUM_AMM_V4,
181 accounts,
182 data: raydium_swap_data(amount_in, min_amount_out),
183 }])
184 }
185}