use crate::SwapInstructionBuilder;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use scematica_core::{dex::program_ids, types::DexKind};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use std::sync::Arc;
pub struct MeteoraBuilder {
pub rpc: Arc<RpcClient>,
}
impl MeteoraBuilder {
pub fn new(rpc: Arc<RpcClient>) -> Self {
Self { rpc }
}
}
const METEORA_SWAP_DISCRIMINATOR: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x27, 0x43];
const DLMM_MIN_DATA_LEN: usize = 834;
mod offsets {
pub const ACTIVE_ID: usize = 76; pub const BIN_STEP: usize = 80; pub const TOKEN_X_MINT: usize = 88; pub const TOKEN_Y_MINT: usize = 120; pub const RESERVE_X: usize = 152; pub const RESERVE_Y: usize = 184; pub const ORACLE: usize = 488; pub const BIN_ARRAY_BITMAP_EXTENSION: usize = 802; }
fn derive_bin_array_pda(pool: &Pubkey, index: i32, program_id: &Pubkey) -> Result<Pubkey> {
let index_bytes = index.to_le_bytes();
let (pda, _) = Pubkey::find_program_address(
&[b"bin_array", pool.as_ref(), &index_bytes],
program_id,
);
Ok(pda)
}
fn read_pubkey(data: &[u8], offset: usize) -> Result<Pubkey> {
Pubkey::try_from(&data[offset..offset + 32])
.map_err(|_| anyhow!("failed to read Pubkey at offset {}", offset))
}
fn read_i32(data: &[u8], offset: usize) -> Result<i32> {
let bytes: [u8; 4] = data[offset..offset + 4]
.try_into()
.map_err(|_| anyhow!("failed to read i32 at offset {}", offset))?;
Ok(i32::from_le_bytes(bytes))
}
fn read_u16(data: &[u8], offset: usize) -> Result<u16> {
let bytes: [u8; 2] = data[offset..offset + 2]
.try_into()
.map_err(|_| anyhow!("failed to read u16 at offset {}", offset))?;
Ok(u16::from_le_bytes(bytes))
}
#[async_trait]
impl SwapInstructionBuilder for MeteoraBuilder {
fn dex(&self) -> DexKind {
DexKind::Meteora
}
async fn build_swap(
&self,
pool: &Pubkey,
owner: &Pubkey,
token_in: &Pubkey,
_token_out: &Pubkey,
ata_in: &Pubkey,
ata_out: &Pubkey,
amount_in: u64,
min_amount_out: u64,
) -> Result<Vec<Instruction>> {
let data = self
.rpc
.get_account_data(pool)
.await
.map_err(|e| anyhow!("RPC error fetching DLMM pool {}: {}", pool, e))?;
if data.len() < DLMM_MIN_DATA_LEN {
return Err(anyhow!(
"DLMM pool data too short: {} < {} bytes",
data.len(),
DLMM_MIN_DATA_LEN
));
}
let active_id = read_i32(&data, offsets::ACTIVE_ID)?;
let _bin_step = read_u16(&data, offsets::BIN_STEP)?;
let token_x_mint = read_pubkey(&data, offsets::TOKEN_X_MINT)?;
let token_y_mint = read_pubkey(&data, offsets::TOKEN_Y_MINT)?;
let reserve_x = read_pubkey(&data, offsets::RESERVE_X)?;
let reserve_y = read_pubkey(&data, offsets::RESERVE_Y)?;
let oracle = read_pubkey(&data, offsets::ORACLE)?;
let bin_array_bitmap_extension = read_pubkey(&data, offsets::BIN_ARRAY_BITMAP_EXTENSION)?;
let swap_for_y = if token_in == &token_x_mint {
true
} else if token_in == &token_y_mint {
false
} else {
return Err(anyhow!(
"token_in {} matches neither token_x_mint {} nor token_y_mint {} for DLMM pool {}",
token_in,
token_x_mint,
token_y_mint,
pool
));
};
const BIN_ARRAY_SIZE: i32 = 70;
let lower_idx = active_id.div_euclid(BIN_ARRAY_SIZE);
let upper_idx = if swap_for_y { lower_idx - 1 } else { lower_idx + 1 };
let bin_array_lower = derive_bin_array_pda(pool, lower_idx, &program_ids::METEORA_DLMM)?;
let bin_array_upper = derive_bin_array_pda(pool, upper_idx, &program_ids::METEORA_DLMM)?;
let mut ix_data = METEORA_SWAP_DISCRIMINATOR.to_vec();
ix_data.extend_from_slice(&amount_in.to_le_bytes());
ix_data.extend_from_slice(&min_amount_out.to_le_bytes());
ix_data.push(swap_for_y as u8);
let accounts = vec![
AccountMeta::new(*pool, false),
AccountMeta::new(bin_array_bitmap_extension, false),
AccountMeta::new(reserve_x, false),
AccountMeta::new(reserve_y, false),
AccountMeta::new(*ata_in, false),
AccountMeta::new(*ata_out, false),
AccountMeta::new_readonly(token_x_mint, false),
AccountMeta::new_readonly(token_y_mint, false),
AccountMeta::new_readonly(oracle, false),
AccountMeta::new(bin_array_lower, false),
AccountMeta::new(bin_array_upper, false),
AccountMeta::new_readonly(*owner, true),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
];
Ok(vec![Instruction {
program_id: program_ids::METEORA_DLMM,
accounts,
data: ix_data,
}])
}
}