Files
khadhroony-bobobot/kb_lib/src/dex/raydium_cpmm.rs
2026-05-10 00:33:01 +02:00

444 lines
16 KiB
Rust

// file: kb_lib/src/dex/raydium_cpmm.rs
//! Raydium CPMM decoder.
//!
//! This module decodes Raydium constant product swap instructions from
//! already-projected Solana transaction instructions.
/// Raydium CPMM `swap_base_input` discriminator.
const RAYDIUM_CPMM_SWAP_BASE_INPUT_DISCRIMINATOR: [u8; 8] = [143, 190, 90, 218, 196, 30, 51, 222];
/// Raydium CPMM `swap_base_output` discriminator.
const RAYDIUM_CPMM_SWAP_BASE_OUTPUT_DISCRIMINATOR: [u8; 8] = [55, 217, 98, 86, 163, 74, 180, 173];
/// Raydium CPMM decoded event.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
pub enum RaydiumCpmmDecodedEvent {
/// Swap where the user fixes the input amount.
SwapBaseInput(RaydiumCpmmSwapDecoded),
/// Swap where the user fixes the output amount.
SwapBaseOutput(RaydiumCpmmSwapDecoded),
}
/// Raydium CPMM swap mode.
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
pub enum RaydiumCpmmSwapMode {
/// Fixed input swap.
BaseInput,
/// Fixed output swap.
BaseOutput,
}
/// Raydium CPMM decoded swap.
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
pub struct RaydiumCpmmSwapDecoded {
/// Instruction mode.
pub swap_mode: RaydiumCpmmSwapMode,
/// User or payer account.
pub payer: std::string::String,
/// Raydium authority account.
pub authority: std::string::String,
/// AMM config account.
pub amm_config: std::string::String,
/// CPMM pool state account.
pub pool_state: std::string::String,
/// User input token account.
pub input_token_account: std::string::String,
/// User output token account.
pub output_token_account: std::string::String,
/// Pool input vault.
pub input_vault: std::string::String,
/// Pool output vault.
pub output_vault: std::string::String,
/// Input token program.
pub input_token_program: std::string::String,
/// Output token program.
pub output_token_program: std::string::String,
/// Input token mint.
pub input_token_mint: std::string::String,
/// Output token mint.
pub output_token_mint: std::string::String,
/// Observation state.
pub observation_state: std::string::String,
/// Normalized base mint.
pub base_mint: std::string::String,
/// Normalized quote mint.
pub quote_mint: std::string::String,
/// Normalized base vault.
pub base_vault: std::string::String,
/// Normalized quote vault.
pub quote_vault: std::string::String,
/// True when input mint is the normalized base mint.
pub input_is_base: bool,
/// Trade side from the normalized pair perspective.
pub trade_side: std::string::String,
/// Fixed input amount, for swap_base_input.
pub amount_in_raw: std::option::Option<std::string::String>,
/// Minimum output amount, for swap_base_input.
pub minimum_amount_out_raw: std::option::Option<std::string::String>,
/// Maximum input amount, for swap_base_output.
pub max_amount_in_raw: std::option::Option<std::string::String>,
/// Fixed output amount, for swap_base_output.
pub amount_out_raw: std::option::Option<std::string::String>,
}
impl RaydiumCpmmDecodedEvent {
/// Returns the storage event kind.
pub fn event_kind(&self) -> &'static str {
match self {
RaydiumCpmmDecodedEvent::SwapBaseInput(_) => return "raydium_cpmm.swap_base_input",
RaydiumCpmmDecodedEvent::SwapBaseOutput(_) => return "raydium_cpmm.swap_base_output",
}
}
/// Returns the pool account.
pub fn pool_account(&self) -> &str {
match self {
RaydiumCpmmDecodedEvent::SwapBaseInput(event) => return event.pool_state.as_str(),
RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => return event.pool_state.as_str(),
}
}
/// Returns the normalized base mint.
pub fn base_mint(&self) -> &str {
match self {
RaydiumCpmmDecodedEvent::SwapBaseInput(event) => return event.base_mint.as_str(),
RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => return event.base_mint.as_str(),
}
}
/// Returns the normalized quote mint.
pub fn quote_mint(&self) -> &str {
match self {
RaydiumCpmmDecodedEvent::SwapBaseInput(event) => return event.quote_mint.as_str(),
RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => return event.quote_mint.as_str(),
}
}
/// Converts the decoded event to JSON payload.
pub fn to_payload_json(&self) -> std::option::Option<std::string::String> {
match self {
crate::RaydiumCpmmDecodedEvent::SwapBaseInput(event) => {
let result = serde_json::to_string(event);
match result {
Ok(payload) => return Some(payload),
Err(_) => return None,
}
},
crate::RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => {
let result = serde_json::to_string(event);
match result {
Ok(payload) => return Some(payload),
Err(_) => return None,
}
},
}
}
}
/// Decodes one Raydium CPMM instruction from projected instruction fields.
pub fn decode_raydium_cpmm_instruction(
accounts_json: &str,
data_json: &str,
) -> std::vec::Vec<RaydiumCpmmDecodedEvent> {
let accounts = match parse_accounts_json(accounts_json) {
Some(accounts) => accounts,
None => return std::vec::Vec::new(),
};
let data_base58 = match parse_data_json_as_base58(data_json) {
Some(data_base58) => data_base58,
None => return std::vec::Vec::new(),
};
let data = match bs58::decode(data_base58.as_str()).into_vec() {
Ok(data) => data,
Err(_) => return std::vec::Vec::new(),
};
if data.len() < 24 {
return std::vec::Vec::new();
}
let discriminator = [data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]];
if discriminator == RAYDIUM_CPMM_SWAP_BASE_INPUT_DISCRIMINATOR {
let amount_in = match read_u64_le(data.as_slice(), 8) {
Some(value) => value,
None => return std::vec::Vec::new(),
};
let minimum_amount_out = match read_u64_le(data.as_slice(), 16) {
Some(value) => value,
None => return std::vec::Vec::new(),
};
let swap = match build_raydium_cpmm_swap(
RaydiumCpmmSwapMode::BaseInput,
accounts.as_slice(),
Some(amount_in.to_string()),
Some(minimum_amount_out.to_string()),
None,
None,
) {
Some(swap) => swap,
None => return std::vec::Vec::new(),
};
return vec![RaydiumCpmmDecodedEvent::SwapBaseInput(swap)];
}
if discriminator == RAYDIUM_CPMM_SWAP_BASE_OUTPUT_DISCRIMINATOR {
let max_amount_in = match read_u64_le(data.as_slice(), 8) {
Some(value) => value,
None => return std::vec::Vec::new(),
};
let amount_out = match read_u64_le(data.as_slice(), 16) {
Some(value) => value,
None => return std::vec::Vec::new(),
};
let swap = match build_raydium_cpmm_swap(
RaydiumCpmmSwapMode::BaseOutput,
accounts.as_slice(),
None,
None,
Some(max_amount_in.to_string()),
Some(amount_out.to_string()),
) {
Some(swap) => swap,
None => return std::vec::Vec::new(),
};
return vec![RaydiumCpmmDecodedEvent::SwapBaseOutput(swap)];
}
return std::vec::Vec::new();
}
fn build_raydium_cpmm_swap(
swap_mode: RaydiumCpmmSwapMode,
accounts: &[std::string::String],
amount_in_raw: std::option::Option<std::string::String>,
minimum_amount_out_raw: std::option::Option<std::string::String>,
max_amount_in_raw: std::option::Option<std::string::String>,
amount_out_raw: std::option::Option<std::string::String>,
) -> std::option::Option<RaydiumCpmmSwapDecoded> {
if accounts.len() < 13 {
return None;
}
let input_token_mint = accounts[10].clone();
let output_token_mint = accounts[11].clone();
let input_vault = accounts[6].clone();
let output_vault = accounts[7].clone();
let normalized = normalize_raydium_cpmm_pair(
input_token_mint.as_str(),
output_token_mint.as_str(),
input_vault.as_str(),
output_vault.as_str(),
);
let input_is_base = normalized.input_is_base;
let trade_side = if input_is_base { "sell".to_string() } else { "buy".to_string() };
return Some(RaydiumCpmmSwapDecoded {
swap_mode,
payer: accounts[0].clone(),
authority: accounts[1].clone(),
amm_config: accounts[2].clone(),
pool_state: accounts[3].clone(),
input_token_account: accounts[4].clone(),
output_token_account: accounts[5].clone(),
input_vault,
output_vault,
input_token_program: accounts[8].clone(),
output_token_program: accounts[9].clone(),
input_token_mint,
output_token_mint,
observation_state: accounts[12].clone(),
base_mint: normalized.base_mint,
quote_mint: normalized.quote_mint,
base_vault: normalized.base_vault,
quote_vault: normalized.quote_vault,
input_is_base,
trade_side,
amount_in_raw,
minimum_amount_out_raw,
max_amount_in_raw,
amount_out_raw,
});
}
struct RaydiumCpmmNormalizedPair {
base_mint: std::string::String,
quote_mint: std::string::String,
base_vault: std::string::String,
quote_vault: std::string::String,
input_is_base: bool,
}
fn normalize_raydium_cpmm_pair(
input_mint: &str,
output_mint: &str,
input_vault: &str,
output_vault: &str,
) -> RaydiumCpmmNormalizedPair {
if is_quote_mint(output_mint) && !is_quote_mint(input_mint) {
return RaydiumCpmmNormalizedPair {
base_mint: input_mint.to_string(),
quote_mint: output_mint.to_string(),
base_vault: input_vault.to_string(),
quote_vault: output_vault.to_string(),
input_is_base: true,
};
}
if is_quote_mint(input_mint) && !is_quote_mint(output_mint) {
return RaydiumCpmmNormalizedPair {
base_mint: output_mint.to_string(),
quote_mint: input_mint.to_string(),
base_vault: output_vault.to_string(),
quote_vault: input_vault.to_string(),
input_is_base: false,
};
}
if input_mint <= output_mint {
return RaydiumCpmmNormalizedPair {
base_mint: input_mint.to_string(),
quote_mint: output_mint.to_string(),
base_vault: input_vault.to_string(),
quote_vault: output_vault.to_string(),
input_is_base: true,
};
}
return RaydiumCpmmNormalizedPair {
base_mint: output_mint.to_string(),
quote_mint: input_mint.to_string(),
base_vault: output_vault.to_string(),
quote_vault: input_vault.to_string(),
input_is_base: false,
};
}
fn is_quote_mint(mint: &str) -> bool {
if mint == crate::WSOL_MINT_ID {
return true;
}
if mint == "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" {
return true;
}
if mint == "Es9vMFrzaCERmJfrF4H2FYD4KMZbsJyNeYrBbqD6CbK" {
return true;
}
if mint == "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB" {
return true;
}
return false;
}
fn parse_accounts_json(
accounts_json: &str,
) -> std::option::Option<std::vec::Vec<std::string::String>> {
let result = serde_json::from_str::<std::vec::Vec<std::string::String>>(accounts_json);
match result {
Ok(accounts) => return Some(accounts),
Err(_) => return None,
}
}
fn parse_data_json_as_base58(data_json: &str) -> std::option::Option<std::string::String> {
let json_string_result = serde_json::from_str::<std::string::String>(data_json);
if let Ok(value) = json_string_result {
return Some(value);
}
let trimmed = data_json.trim();
if trimmed.is_empty() {
return None;
}
let without_quotes = trimmed.trim_matches('"');
if without_quotes.is_empty() {
return None;
}
return Some(without_quotes.to_string());
}
fn read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
if data.len() < offset + 8 {
return None;
}
let bytes = [
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
data[offset + 4],
data[offset + 5],
data[offset + 6],
data[offset + 7],
];
return Some(u64::from_le_bytes(bytes));
}
#[cfg(test)]
mod tests {
#[test]
fn decodes_swap_base_input() {
let accounts_json = r#"[
"ARu4n5mFdZogZAravu7CcizaojWnS6oqka37gdLT5SZn",
"GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL",
"GtdipqAcw8eAYxGcANs3vpN7UaN7UH7u89Kd8wPqLThd",
"2ErXvV1tKtG3wiHqdofDjMou7Jusdsfasvfh8HrTj5oV",
"3tBz6MmUTaf2QdnAnYYJGDpgbTXdANP9v4CtUL42GZaZ",
"C1qMskwQhQ4cn1M6RG8uWfUuMrq6ysRPEHccGxk4tjw8",
"8aHJfDQBmngBpFwLysD1bhj8LKh7ywbxfUxsy7VQkx22",
"A4jPLDGrkAogPgm8KqQzZQ4FoNJuKuKMCbNqgQkpT5Ci",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"Pf9aSicGu3g6tTUBqrRbjNsGape9HopibspX5KSbonk",
"USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB",
"9fzQ17bEnqJSsyHL5CodJqY2sdjZJBQCQLbgFZ1BYTWn"
]"#;
let events = crate::decode_raydium_cpmm_instruction(
accounts_json,
r#""E73fXHPWvSRF4Q2ZQFvPoeJBVGDUEMmxB""#,
);
assert_eq!(events.len(), 1);
match &events[0] {
crate::RaydiumCpmmDecodedEvent::SwapBaseInput(event) => {
assert_eq!(event.pool_state, "2ErXvV1tKtG3wiHqdofDjMou7Jusdsfasvfh8HrTj5oV");
assert_eq!(event.base_mint, "Pf9aSicGu3g6tTUBqrRbjNsGape9HopibspX5KSbonk");
assert_eq!(event.quote_mint, "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB");
assert!(event.input_is_base);
assert_eq!(event.trade_side, "sell");
assert!(event.amount_in_raw.is_some());
assert!(event.minimum_amount_out_raw.is_some());
},
_ => {
panic!("expected swap base input");
},
}
}
#[test]
fn decodes_swap_base_output() {
let accounts_json = r#"[
"JD6rVaerbyz6wjQ433nrw6bFTgFrp46MiYmi8EtUAfsG",
"GpMZbSM2GgvTKHJirzeGfMFoaZ8UR2X7F4v8vHTvxFbL",
"GtdipqAcw8eAYxGcANs3vpN7UaN7UH7u89Kd8wPqLThd",
"2ErXvV1tKtG3wiHqdofDjMou7Jusdsfasvfh8HrTj5oV",
"4Fg913oAKp7BiW6WvUF32RvACsBYusMTQ16nmA6Sd9tp",
"Cvcy56pbaDms71KyxAWFTQ24n5KbCy8KNa7zMhKGcJa6",
"A4jPLDGrkAogPgm8KqQzZQ4FoNJuKuKMCbNqgQkpT5Ci",
"8aHJfDQBmngBpFwLysD1bhj8LKh7ywbxfUxsy7VQkx22",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB",
"Pf9aSicGu3g6tTUBqrRbjNsGape9HopibspX5KSbonk",
"9fzQ17bEnqJSsyHL5CodJqY2sdjZJBQCQLbgFZ1BYTWn"
]"#;
let events = crate::decode_raydium_cpmm_instruction(
accounts_json,
r#""66JafaVu7KNEtTngqQyD7ETVurF3rxJ47""#,
);
assert_eq!(events.len(), 1);
match &events[0] {
crate::RaydiumCpmmDecodedEvent::SwapBaseOutput(event) => {
assert_eq!(event.pool_state, "2ErXvV1tKtG3wiHqdofDjMou7Jusdsfasvfh8HrTj5oV");
assert_eq!(event.base_mint, "Pf9aSicGu3g6tTUBqrRbjNsGape9HopibspX5KSbonk");
assert_eq!(event.quote_mint, "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB");
assert!(!event.input_is_base);
assert_eq!(event.trade_side, "buy");
assert!(event.max_amount_in_raw.is_some());
assert!(event.amount_out_raw.is_some());
},
_ => {
panic!("expected swap base output");
},
}
}
}