Skip to main content

scematica_executor/
raydium.rs

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
14/// Raydium AMM V4 swap instruction builder
15pub 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
33/// Raydium V4 swap instruction data: discriminator 9 + amount_in + min_amount_out
34fn 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
41/// Serum/OpenBook V1 market state layout (verified against openbook-dex/openbook-v1 state.rs).
42///
43/// [0..5]    padding ("serum" magic)
44/// [5..13]   accountFlags        u64
45/// [13..45]  ownAddress          Pubkey
46/// [45..53]  vaultSignerNonce    u64
47/// [53..85]  baseMint            Pubkey
48/// [85..117] quoteMint           Pubkey
49/// [117..149] baseVault          Pubkey
50/// [149..157] baseDepositsTotal  u64
51/// [157..165] baseFeesAccrued    u64
52/// [165..197] quoteVault         Pubkey
53/// [197..205] quoteDepositsTotal u64
54/// [205..213] quoteFeesAccrued   u64
55/// [213..221] quoteDustThreshold u64
56/// [221..253] requestQueue       Pubkey
57/// [253..285] eventQueue         Pubkey
58/// [285..317] bids               Pubkey
59/// [317..349] asks               Pubkey
60/// [349..357] baseLotSize        u64
61/// [357..365] quoteLotSize       u64
62/// [365..373] feeRateBps         u64
63/// [373..381] referrerRebatesAccrued u64
64/// [381..388] trailing padding (7 bytes)  — total 388 bytes
65mod 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
74/// Parse a Pubkey from a byte slice at the given offset.
75fn 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
80/// Derive the Serum vault signer PDA from the market address and nonce.
81fn derive_vault_signer(market: &Pubkey, nonce: u64, market_program: &Pubkey) -> Result<Pubkey> {
82    let nonce_bytes = nonce.to_le_bytes();
83    // Serum vault signer uses create_program_address (not find), nonce is the bump
84    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        // Fetch pool state — 752 bytes on-chain
110        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        // Read status (u64 LE at byte 0)
118        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        // Read pool accounts directly from verified byte offsets (avoids Borsh struct misalignment)
124        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        // Fetch OpenBook/Serum market state
132        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}