Skip to main content

scematica_executor/
meteora.rs

1use crate::SwapInstructionBuilder;
2use anyhow::{anyhow, Result};
3use async_trait::async_trait;
4use scematica_core::{dex::program_ids, types::DexKind};
5use solana_client::nonblocking::rpc_client::RpcClient;
6use solana_sdk::{
7    instruction::{AccountMeta, Instruction},
8    pubkey::Pubkey,
9};
10use std::sync::Arc;
11
12pub struct MeteoraBuilder {
13    pub rpc: Arc<RpcClient>,
14}
15
16impl MeteoraBuilder {
17    pub fn new(rpc: Arc<RpcClient>) -> Self {
18        Self { rpc }
19    }
20}
21
22/// Meteora DLMM swap discriminator: sha256("global:swap")[0..8]
23/// Correct value is [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x27, 0x43]
24const METEORA_SWAP_DISCRIMINATOR: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x27, 0x43];
25
26/// Minimum byte length required to decode all fields from the LbPair account.
27/// bin_array_bitmap_extension is at offset 802 and is 32 bytes, so we need at least 834 bytes.
28const DLMM_MIN_DATA_LEN: usize = 834;
29
30/// Byte offsets within the LbPair account data (after the 8-byte Anchor discriminator).
31/// All offsets are absolute (from the start of the account data, including the discriminator).
32mod offsets {
33    pub const ACTIVE_ID: usize = 76;       // i32
34    pub const BIN_STEP: usize = 80;        // u16
35    pub const TOKEN_X_MINT: usize = 88;    // Pubkey (32 bytes)
36    pub const TOKEN_Y_MINT: usize = 120;   // Pubkey (32 bytes)
37    pub const RESERVE_X: usize = 152;      // Pubkey (32 bytes)
38    pub const RESERVE_Y: usize = 184;      // Pubkey (32 bytes)
39    pub const ORACLE: usize = 488;         // Pubkey (32 bytes)
40    pub const BIN_ARRAY_BITMAP_EXTENSION: usize = 802; // Pubkey (32 bytes)
41}
42
43/// Derive a Meteora DLMM bin array PDA for a given pool and bin array index.
44fn derive_bin_array_pda(pool: &Pubkey, index: i32, program_id: &Pubkey) -> Result<Pubkey> {
45    let index_bytes = index.to_le_bytes();
46    let (pda, _) = Pubkey::find_program_address(
47        &[b"bin_array", pool.as_ref(), &index_bytes],
48        program_id,
49    );
50    Ok(pda)
51}
52fn read_pubkey(data: &[u8], offset: usize) -> Result<Pubkey> {
53    Pubkey::try_from(&data[offset..offset + 32])
54        .map_err(|_| anyhow!("failed to read Pubkey at offset {}", offset))
55}
56
57/// Decode an `i32` from a 4-byte little-endian slice at the given offset.
58fn read_i32(data: &[u8], offset: usize) -> Result<i32> {
59    let bytes: [u8; 4] = data[offset..offset + 4]
60        .try_into()
61        .map_err(|_| anyhow!("failed to read i32 at offset {}", offset))?;
62    Ok(i32::from_le_bytes(bytes))
63}
64
65/// Decode a `u16` from a 2-byte little-endian slice at the given offset.
66fn read_u16(data: &[u8], offset: usize) -> Result<u16> {
67    let bytes: [u8; 2] = data[offset..offset + 2]
68        .try_into()
69        .map_err(|_| anyhow!("failed to read u16 at offset {}", offset))?;
70    Ok(u16::from_le_bytes(bytes))
71}
72
73#[async_trait]
74impl SwapInstructionBuilder for MeteoraBuilder {
75    fn dex(&self) -> DexKind {
76        DexKind::Meteora
77    }
78
79    async fn build_swap(
80        &self,
81        pool: &Pubkey,
82        owner: &Pubkey,
83        token_in: &Pubkey,
84        _token_out: &Pubkey,
85        ata_in: &Pubkey,
86        ata_out: &Pubkey,
87        amount_in: u64,
88        min_amount_out: u64,
89    ) -> Result<Vec<Instruction>> {
90        // Req 6.2: fetch DLMM LbPair account data
91        let data = self
92            .rpc
93            .get_account_data(pool)
94            .await
95            .map_err(|e| anyhow!("RPC error fetching DLMM pool {}: {}", pool, e))?;
96
97        // Req 6.3: validate minimum data length
98        if data.len() < DLMM_MIN_DATA_LEN {
99            return Err(anyhow!(
100                "DLMM pool data too short: {} < {} bytes",
101                data.len(),
102                DLMM_MIN_DATA_LEN
103            ));
104        }
105
106        // Req 6.4: extract fields from the Anchor-serialized layout (8-byte discriminator already
107        // accounted for in the offset constants above)
108        let active_id = read_i32(&data, offsets::ACTIVE_ID)?;
109        let _bin_step = read_u16(&data, offsets::BIN_STEP)?;
110        let token_x_mint = read_pubkey(&data, offsets::TOKEN_X_MINT)?;
111        let token_y_mint = read_pubkey(&data, offsets::TOKEN_Y_MINT)?;
112        let reserve_x = read_pubkey(&data, offsets::RESERVE_X)?;
113        let reserve_y = read_pubkey(&data, offsets::RESERVE_Y)?;
114        let oracle = read_pubkey(&data, offsets::ORACLE)?;
115        let bin_array_bitmap_extension = read_pubkey(&data, offsets::BIN_ARRAY_BITMAP_EXTENSION)?;
116
117        // Req 6.5 / 6.6: determine swap direction; error if token_in matches neither mint
118        let swap_for_y = if token_in == &token_x_mint {
119            true
120        } else if token_in == &token_y_mint {
121            false
122        } else {
123            return Err(anyhow!(
124                "token_in {} matches neither token_x_mint {} nor token_y_mint {} for DLMM pool {}",
125                token_in,
126                token_x_mint,
127                token_y_mint,
128                pool
129            ));
130        };
131
132        // Derive bin array PDAs from active_id and bin_step.
133        // Each bin array covers BIN_ARRAY_SIZE (70) bins.
134        // lower covers the active bin, upper covers the next array in swap direction.
135        const BIN_ARRAY_SIZE: i32 = 70;
136        let lower_idx = active_id.div_euclid(BIN_ARRAY_SIZE);
137        let upper_idx = if swap_for_y { lower_idx - 1 } else { lower_idx + 1 };
138
139        let bin_array_lower = derive_bin_array_pda(pool, lower_idx, &program_ids::METEORA_DLMM)?;
140        let bin_array_upper = derive_bin_array_pda(pool, upper_idx, &program_ids::METEORA_DLMM)?;
141
142        // Encode instruction data (25 bytes total per Req 7.4 / 11.3):
143        //   [0..8]   METEORA_SWAP_DISCRIMINATOR
144        //   [8..16]  amount_in (u64 LE)
145        //   [16..24] min_amount_out (u64 LE)
146        //   [24]     swap_for_y (1u8 = true, 0u8 = false)
147        let mut ix_data = METEORA_SWAP_DISCRIMINATOR.to_vec();
148        ix_data.extend_from_slice(&amount_in.to_le_bytes());
149        ix_data.extend_from_slice(&min_amount_out.to_le_bytes());
150        ix_data.push(swap_for_y as u8);
151
152        // Account order (14 accounts per Req 7.2 / design):
153        //  0: pool                        writable
154        //  1: bin_array_bitmap_extension  writable
155        //  2: reserve_x                   writable
156        //  3: reserve_y                   writable
157        //  4: ata_in                      writable
158        //  5: ata_out                     writable
159        //  6: token_x_mint                readonly
160        //  7: token_y_mint                readonly
161        //  8: oracle                      readonly
162        //  9: bin_array_lower             writable
163        // 10: bin_array_upper             writable
164        // 11: owner                       signer
165        // 12: spl_token                   readonly
166        // 13: spl_token                   readonly
167        let accounts = vec![
168            AccountMeta::new(*pool, false),
169            AccountMeta::new(bin_array_bitmap_extension, false),
170            AccountMeta::new(reserve_x, false),
171            AccountMeta::new(reserve_y, false),
172            AccountMeta::new(*ata_in, false),
173            AccountMeta::new(*ata_out, false),
174            AccountMeta::new_readonly(token_x_mint, false),
175            AccountMeta::new_readonly(token_y_mint, false),
176            AccountMeta::new_readonly(oracle, false),
177            AccountMeta::new(bin_array_lower, false),
178            AccountMeta::new(bin_array_upper, false),
179            AccountMeta::new_readonly(*owner, true),
180            AccountMeta::new_readonly(spl_token::id(), false),
181            AccountMeta::new_readonly(spl_token::id(), false),
182        ];
183
184        Ok(vec![Instruction {
185            program_id: program_ids::METEORA_DLMM,
186            accounts,
187            data: ix_data,
188        }])
189    }
190}