Files
khadhroony-bobobot/kb_lib/src/dex/raydium_stable_swap.rs
2026-06-11 17:22:55 +02:00

1525 lines
59 KiB
Rust

// file: kb_lib/src/dex/raydium_stable_swap.rs
//! Raydium Stable Swap decoder.
//!
//! Raydium Stable Swap uses a legacy one-byte instruction discriminator layout.
//! The decoder below follows the Pinax/Substreams layout and keeps Anchor-like
//! eight-byte discriminants as upstream discovery evidence only.
/// Raydium Stable Swap `initialize` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_INITIALIZE_DISCRIMINATOR_HEX: &str = "00";
/// Raydium Stable Swap `init_model_data` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_INIT_MODEL_DATA_DISCRIMINATOR_HEX: &str = "01";
/// Raydium Stable Swap `update_model_data` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_UPDATE_MODEL_DATA_DISCRIMINATOR_HEX: &str = "02";
/// Raydium Stable Swap `deposit` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_DEPOSIT_DISCRIMINATOR_HEX: &str = "03";
/// Raydium Stable Swap `withdraw` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_WITHDRAW_DISCRIMINATOR_HEX: &str = "04";
/// Raydium Stable Swap `monitor_step` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_MONITOR_STEP_DISCRIMINATOR_HEX: &str = "05";
/// Raydium Stable Swap `set_params` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_SET_PARAMS_DISCRIMINATOR_HEX: &str = "06";
/// Raydium Stable Swap `withdraw_pnl` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_WITHDRAW_PNL_DISCRIMINATOR_HEX: &str = "07";
/// Raydium Stable Swap `withdraw_srm` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_WITHDRAW_SRM_DISCRIMINATOR_HEX: &str = "08";
/// Raydium Stable Swap `swap_base_in` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_SWAP_BASE_IN_DISCRIMINATOR_HEX: &str = "09";
/// Raydium Stable Swap `pre_initialize` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_PRE_INITIALIZE_DISCRIMINATOR_HEX: &str = "0a";
/// Raydium Stable Swap `swap_base_out` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_SWAP_BASE_OUT_DISCRIMINATOR_HEX: &str = "0b";
/// Raydium Stable Swap `simulate_info` one-byte discriminator.
const RAYDIUM_STABLE_SWAP_SIMULATE_INFO_DISCRIMINATOR_HEX: &str = "0c";
/// Raydium Stable Swap `admin_cancel_orders` one-byte discriminator observed locally.
const RAYDIUM_STABLE_SWAP_ADMIN_CANCEL_ORDERS_DISCRIMINATOR_HEX: &str = "0d";
/// Raydium Stable Swap `SwapEvent` discriminator reused by Raydium CPMM logs.
const RAYDIUM_STABLE_SWAP_SWAP_EVENT_DISCRIMINATOR_HEX: &str = "40c6cde8260871e2";
/// Raydium Stable Swap decoded event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum RaydiumStableSwapDecodedEvent {
/// Known Stable Swap instruction decoded from the local one-byte layout.
Instruction(std::boxed::Box<RaydiumStableSwapInstructionDecoded>),
/// Program-data swap event retained as decoded-only audit evidence.
SwapEvent(std::boxed::Box<RaydiumStableSwapSwapEventDecoded>),
}
/// Decoded Raydium Stable Swap instruction.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RaydiumStableSwapInstructionDecoded {
/// Parent transaction id.
pub transaction_id: i64,
/// Parent instruction id.
pub instruction_id: i64,
/// Transaction signature.
pub signature: std::string::String,
/// Program id.
pub program_id: std::string::String,
/// Local decoded event kind.
pub event_kind: std::string::String,
/// Upstream/local instruction name.
pub instruction_name: std::string::String,
/// One-byte instruction discriminator in hexadecimal form.
pub discriminator_hex: std::string::String,
/// Optional pool/AMM account.
pub pool_account: std::option::Option<std::string::String>,
/// Optional market account.
pub market_account: std::option::Option<std::string::String>,
/// Optional LP mint account.
pub lp_mint: std::option::Option<std::string::String>,
/// Optional normalized base mint.
pub token_a_mint: std::option::Option<std::string::String>,
/// Optional normalized quote mint.
pub token_b_mint: std::option::Option<std::string::String>,
/// Optional normalized base vault.
pub base_vault: std::option::Option<std::string::String>,
/// Optional normalized quote vault.
pub quote_vault: std::option::Option<std::string::String>,
/// Decoded and enrichment-ready payload.
pub payload_json: serde_json::Value,
}
/// Decoded Raydium Stable Swap program-data swap event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RaydiumStableSwapSwapEventDecoded {
/// Event discriminator in hexadecimal form.
pub event_discriminator_hex: std::string::String,
/// Raydium stable-swap event-local dex flag.
pub dex: u8,
/// Raw input amount emitted by the event.
pub amount_in_raw: std::string::String,
/// Raw output amount emitted by the event.
pub amount_out_raw: std::string::String,
/// Whether the event can materialize as a trade.
pub trade_candidate: bool,
/// Whether the event can materialize as a candle.
pub candle_candidate: bool,
/// Reason why trade materialization is skipped.
pub skip_trade_reason: std::string::String,
/// Reason why candle materialization is skipped.
pub skip_candle_reason: std::string::String,
}
/// Raydium Stable Swap decoder.
#[derive(Debug, Clone, Default)]
pub struct RaydiumStableSwapDecoder;
impl RaydiumStableSwapDecoder {
/// Creates a new Raydium Stable Swap decoder.
pub fn new() -> Self {
return Self;
}
/// Decodes all Raydium Stable Swap instructions in one projected transaction.
pub fn decode_transaction(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::RaydiumStableSwapDecodedEvent>, crate::Error> {
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"chain transaction '{}' has no internal id",
transaction.signature
)));
},
};
let transaction_json_value = parse_json_value(transaction.transaction_json.as_str());
let meta_json_value = parse_optional_json_value(transaction.meta_json.as_deref());
let account_keys = extract_transaction_account_keys(
transaction_json_value.as_ref(),
meta_json_value.as_ref(),
);
let token_mints_by_account = extract_token_mints_by_account(
transaction_json_value.as_ref(),
meta_json_value.as_ref(),
account_keys.as_slice(),
);
let token_balance_records = extract_token_balance_records(
transaction_json_value.as_ref(),
meta_json_value.as_ref(),
account_keys.as_slice(),
);
let mut decoded_events = std::vec::Vec::new();
for instruction in instructions {
let program_id = match instruction.program_id.as_ref() {
Some(program_id) => program_id,
None => continue,
};
if program_id.as_str() != crate::RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID {
continue;
}
let instruction_id = match instruction.id {
Some(instruction_id) => instruction_id,
None => continue,
};
let decoded = decode_raydium_stable_swap_instruction_with_context(
transaction_id,
instruction_id,
transaction.signature.as_str(),
program_id.as_str(),
instruction.accounts_json.as_str(),
instruction.data_json.as_deref(),
&token_mints_by_account,
token_balance_records.as_slice(),
);
for event in decoded {
decoded_events.push(event);
}
}
return Ok(decoded_events);
}
}
impl RaydiumStableSwapDecodedEvent {
/// Returns the local decoded event kind.
pub fn event_kind(&self) -> &str {
match self {
Self::Instruction(event) => return event.event_kind.as_str(),
Self::SwapEvent(_) => return "raydium_stable_swap.swap_event",
}
}
/// Returns the optional pool account.
pub fn pool_account(&self) -> std::option::Option<&str> {
match self {
Self::Instruction(event) => return event.pool_account.as_deref(),
Self::SwapEvent(_) => return None,
}
}
/// Returns the optional market account.
pub fn market_account(&self) -> std::option::Option<&str> {
match self {
Self::Instruction(event) => return event.market_account.as_deref(),
Self::SwapEvent(_) => return None,
}
}
/// Returns the optional normalized base mint.
pub fn base_mint(&self) -> std::option::Option<&str> {
match self {
Self::Instruction(event) => return event.token_a_mint.as_deref(),
Self::SwapEvent(_) => return None,
}
}
/// Returns the optional normalized quote mint.
pub fn quote_mint(&self) -> std::option::Option<&str> {
match self {
Self::Instruction(event) => return event.token_b_mint.as_deref(),
Self::SwapEvent(_) => return None,
}
}
/// Returns the optional LP mint.
pub fn lp_mint(&self) -> std::option::Option<&str> {
match self {
Self::Instruction(event) => return event.lp_mint.as_deref(),
Self::SwapEvent(_) => return None,
}
}
/// Serializes the decoded payload as JSON text.
pub fn to_payload_json(&self) -> std::option::Option<std::string::String> {
match self {
Self::Instruction(event) => {
let result = serde_json::to_string(&event.payload_json);
match result {
Ok(payload) => return Some(payload),
Err(_) => return None,
}
},
Self::SwapEvent(event) => {
let result = serde_json::to_string(event);
match result {
Ok(payload) => return Some(payload),
Err(_) => return None,
}
},
}
}
/// Returns the parent instruction id when available.
pub fn instruction_id(&self) -> std::option::Option<i64> {
match self {
Self::Instruction(event) => return Some(event.instruction_id),
Self::SwapEvent(_) => return None,
}
}
}
/// Classifies one Raydium Stable Swap instruction data payload.
pub fn classify_raydium_stable_swap_instruction_data(
data_json: &str,
) -> std::option::Option<&'static str> {
let data = match decode_data_json_base58(data_json) {
Some(data) => data,
None => return None,
};
let discriminator = match data.first() {
Some(discriminator) => *discriminator,
None => return None,
};
return stable_instruction_name(discriminator);
}
/// Decodes Raydium Stable Swap `Program data:` event payloads.
pub fn decode_raydium_stable_swap_program_data_event(
data_base64: &str,
) -> std::option::Option<crate::RaydiumStableSwapDecodedEvent> {
use base64::Engine as _;
let decoded_result = base64::engine::general_purpose::STANDARD.decode(data_base64.as_bytes());
let data = match decoded_result {
Ok(data) => data,
Err(_) => return None,
};
if data.len() < 25 {
return None;
}
let discriminator_hex = bytes_to_hex(&data[0..8]);
if discriminator_hex != RAYDIUM_STABLE_SWAP_SWAP_EVENT_DISCRIMINATOR_HEX {
return None;
}
let dex = data[8];
let amount_in = match read_u64_le(data.as_slice(), 9) {
Some(amount_in) => amount_in,
None => return None,
};
let amount_out = match read_u64_le(data.as_slice(), 17) {
Some(amount_out) => amount_out,
None => return None,
};
return Some(crate::RaydiumStableSwapDecodedEvent::SwapEvent(std::boxed::Box::new(
RaydiumStableSwapSwapEventDecoded {
event_discriminator_hex: discriminator_hex,
dex,
amount_in_raw: amount_in.to_string(),
amount_out_raw: amount_out.to_string(),
trade_candidate: false,
candle_candidate: false,
skip_trade_reason: "raydium_stable_swap_swap_event_decoded_only".to_string(),
skip_candle_reason: "raydium_stable_swap_swap_event_decoded_only".to_string(),
},
)));
}
#[allow(clippy::too_many_arguments)]
fn decode_raydium_stable_swap_instruction_with_context(
transaction_id: i64,
instruction_id: i64,
signature: &str,
program_id: &str,
accounts_json: &str,
data_json: std::option::Option<&str>,
token_mints_by_account: &std::collections::BTreeMap<std::string::String, std::string::String>,
token_balance_records: &[TokenBalanceRecord],
) -> std::vec::Vec<crate::RaydiumStableSwapDecodedEvent> {
let data_json = match data_json {
Some(data_json) => data_json,
None => return std::vec::Vec::new(),
};
let accounts = match parse_accounts_json(accounts_json) {
Some(accounts) => accounts,
None => return std::vec::Vec::new(),
};
let data = match decode_data_json_base58(data_json) {
Some(data) => data,
None => return std::vec::Vec::new(),
};
let discriminator = match data.first() {
Some(discriminator) => *discriminator,
None => return std::vec::Vec::new(),
};
let instruction_name = match stable_instruction_name(discriminator) {
Some(instruction_name) => instruction_name,
None => return std::vec::Vec::new(),
};
let decoded = build_stable_instruction_event(
transaction_id,
instruction_id,
signature,
program_id,
instruction_name,
discriminator,
accounts.as_slice(),
data.as_slice(),
token_mints_by_account,
token_balance_records,
);
let decoded = match decoded {
Some(decoded) => decoded,
None => return std::vec::Vec::new(),
};
return vec![crate::RaydiumStableSwapDecodedEvent::Instruction(std::boxed::Box::new(decoded))];
}
#[allow(clippy::too_many_arguments)]
fn build_stable_instruction_event(
transaction_id: i64,
instruction_id: i64,
signature: &str,
program_id: &str,
instruction_name: &str,
discriminator: u8,
accounts: &[std::string::String],
data: &[u8],
token_mints_by_account: &std::collections::BTreeMap<std::string::String, std::string::String>,
token_balance_records: &[TokenBalanceRecord],
) -> std::option::Option<RaydiumStableSwapInstructionDecoded> {
let event_kind = format!("raydium_stable_swap.{}", instruction_name);
let discriminator_hex = match stable_instruction_discriminator_hex(discriminator) {
Some(discriminator_hex) => discriminator_hex.to_string(),
None => format!("{:02x}", discriminator),
};
let mut payload = serde_json::Map::new();
payload.insert(
"instructionName".to_string(),
serde_json::Value::String(instruction_name.to_string()),
);
payload.insert("eventKind".to_string(), serde_json::Value::String(event_kind.clone()));
payload.insert(
"discriminatorHex".to_string(),
serde_json::Value::String(discriminator_hex.clone()),
);
payload.insert("programId".to_string(), serde_json::Value::String(program_id.to_string()));
payload.insert(
"stableLayout".to_string(),
serde_json::Value::String("pinax_one_byte".to_string()),
);
let pool_account: std::option::Option<std::string::String>;
let market_account: std::option::Option<std::string::String>;
let lp_mint: std::option::Option<std::string::String>;
let token_a_mint: std::option::Option<std::string::String>;
let token_b_mint: std::option::Option<std::string::String>;
let base_vault: std::option::Option<std::string::String>;
let quote_vault: std::option::Option<std::string::String>;
match instruction_name {
"initialize" => {
pool_account = account_at(accounts, 3);
lp_mint = account_at(accounts, 6);
let coin_mint = account_at(accounts, 7);
let pc_mint = account_at(accounts, 8);
let coin_vault = account_at(accounts, 9);
let pc_vault = account_at(accounts, 10);
market_account = account_at(accounts, 14);
let normalized = normalize_pair_with_vaults(
coin_mint.clone(),
pc_mint.clone(),
coin_vault.clone(),
pc_vault.clone(),
);
token_a_mint = normalized.base_mint;
token_b_mint = normalized.quote_mint;
base_vault = normalized.base_vault;
quote_vault = normalized.quote_vault;
insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
insert_optional_string(&mut payload, "lpMint", lp_mint.clone());
insert_optional_string(&mut payload, "coinMint", coin_mint);
insert_optional_string(&mut payload, "pcMint", pc_mint);
insert_optional_string(&mut payload, "baseVault", base_vault.clone());
insert_optional_string(&mut payload, "quoteVault", quote_vault.clone());
insert_optional_string(&mut payload, "marketAccount", market_account.clone());
insert_optional_u8(&mut payload, "nonce", data.get(1).copied());
insert_optional_u64(&mut payload, "openTimeRaw", read_u64_le(data, 2));
},
"pre_initialize" => {
pool_account = None;
lp_mint = account_at(accounts, 5);
let coin_mint = account_at(accounts, 6);
let pc_mint = account_at(accounts, 7);
let coin_vault = account_at(accounts, 8);
let pc_vault = account_at(accounts, 9);
market_account = account_at(accounts, 10);
let normalized = normalize_pair_with_vaults(
coin_mint.clone(),
pc_mint.clone(),
coin_vault.clone(),
pc_vault.clone(),
);
token_a_mint = normalized.base_mint;
token_b_mint = normalized.quote_mint;
base_vault = normalized.base_vault;
quote_vault = normalized.quote_vault;
insert_optional_string(&mut payload, "lpMint", lp_mint.clone());
insert_optional_string(&mut payload, "coinMint", coin_mint);
insert_optional_string(&mut payload, "pcMint", pc_mint);
insert_optional_string(&mut payload, "baseVault", base_vault.clone());
insert_optional_string(&mut payload, "quoteVault", quote_vault.clone());
insert_optional_string(&mut payload, "marketAccount", market_account.clone());
insert_optional_u8(&mut payload, "nonce", data.get(1).copied());
payload.insert(
"skipLifecycleReason".to_string(),
serde_json::Value::String("stable_pre_initialize_partial_pool_context".to_string()),
);
},
"deposit" => {
pool_account = account_at(accounts, 1);
lp_mint = account_at(accounts, 5);
let coin_vault = account_at(accounts, 6);
let pc_vault = account_at(accounts, 7);
market_account = account_at(accounts, 9);
let coin_mint = mint_for_account(coin_vault.as_deref(), token_mints_by_account);
let pc_mint = mint_for_account(pc_vault.as_deref(), token_mints_by_account);
let normalized = normalize_pair_with_vaults(
coin_mint.clone(),
pc_mint.clone(),
coin_vault.clone(),
pc_vault.clone(),
);
token_a_mint = normalized.base_mint;
token_b_mint = normalized.quote_mint;
base_vault = normalized.base_vault;
quote_vault = normalized.quote_vault;
insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
insert_optional_string(&mut payload, "lpMint", lp_mint.clone());
insert_optional_string(&mut payload, "baseVault", base_vault.clone());
insert_optional_string(&mut payload, "quoteVault", quote_vault.clone());
insert_optional_string(&mut payload, "marketAccount", market_account.clone());
insert_optional_u64_string(&mut payload, "maxCoinAmountRaw", read_u64_le(data, 1));
insert_optional_u64_string(&mut payload, "maxPcAmountRaw", read_u64_le(data, 9));
insert_optional_u64_string(&mut payload, "baseSideRaw", read_u64_le(data, 17));
},
"withdraw" => {
pool_account = account_at(accounts, 1);
lp_mint = account_at(accounts, 5);
let coin_vault = account_at(accounts, 6);
let pc_vault = account_at(accounts, 7);
market_account = account_at(accounts, 10);
let coin_mint = mint_for_account(coin_vault.as_deref(), token_mints_by_account);
let pc_mint = mint_for_account(pc_vault.as_deref(), token_mints_by_account);
let normalized = normalize_pair_with_vaults(
coin_mint.clone(),
pc_mint.clone(),
coin_vault.clone(),
pc_vault.clone(),
);
token_a_mint = normalized.base_mint;
token_b_mint = normalized.quote_mint;
base_vault = normalized.base_vault;
quote_vault = normalized.quote_vault;
insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
insert_optional_string(&mut payload, "lpMint", lp_mint.clone());
insert_optional_string(&mut payload, "baseVault", base_vault.clone());
insert_optional_string(&mut payload, "quoteVault", quote_vault.clone());
insert_optional_string(&mut payload, "marketAccount", market_account.clone());
insert_optional_u64_string(&mut payload, "lpAmountRaw", read_u64_le(data, 1));
},
"init_model_data" | "update_model_data" => {
pool_account = account_at(accounts, 0);
market_account = None;
lp_mint = None;
token_a_mint = None;
token_b_mint = None;
base_vault = None;
quote_vault = None;
insert_optional_string(&mut payload, "modelDataAccount", pool_account.clone());
payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert(
"skipTradeReason".to_string(),
serde_json::Value::String("stable_model_data_instruction_decoded_only".to_string()),
);
payload.insert(
"skipLiquidityReason".to_string(),
serde_json::Value::String("stable_model_data_instruction_decoded_only".to_string()),
);
payload.insert(
"skipLifecycleReason".to_string(),
serde_json::Value::String("stable_model_data_instruction_decoded_only".to_string()),
);
},
"monitor_step" | "admin_cancel_orders" => {
pool_account = account_at(accounts, 1);
market_account = account_at(accounts, 5);
lp_mint = None;
token_a_mint = None;
token_b_mint = None;
base_vault = None;
quote_vault = None;
insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
insert_optional_string(&mut payload, "marketAccount", market_account.clone());
payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert(
"skipTradeReason".to_string(),
serde_json::Value::String("stable_orderbook_instruction_decoded_only".to_string()),
);
payload.insert(
"skipLiquidityReason".to_string(),
serde_json::Value::String("stable_orderbook_instruction_decoded_only".to_string()),
);
payload.insert(
"skipLifecycleReason".to_string(),
serde_json::Value::String("stable_orderbook_instruction_decoded_only".to_string()),
);
},
"set_params" => {
pool_account = account_at(accounts, 1);
market_account = None;
lp_mint = None;
token_a_mint = None;
token_b_mint = None;
base_vault = None;
quote_vault = None;
insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert(
"skipTradeReason".to_string(),
serde_json::Value::String("stable_admin_instruction_decoded_only".to_string()),
);
payload.insert(
"skipLiquidityReason".to_string(),
serde_json::Value::String("stable_admin_instruction_decoded_only".to_string()),
);
payload.insert(
"skipLifecycleReason".to_string(),
serde_json::Value::String("stable_admin_instruction_decoded_only".to_string()),
);
},
"withdraw_pnl" | "withdraw_srm" => {
pool_account = account_at(accounts, 1);
market_account = account_at(accounts, 4);
lp_mint = None;
token_a_mint = None;
token_b_mint = None;
base_vault = None;
quote_vault = None;
insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
insert_optional_string(&mut payload, "marketAccount", market_account.clone());
payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert(
"skipTradeReason".to_string(),
serde_json::Value::String("stable_fee_instruction_decoded_only".to_string()),
);
payload.insert(
"skipLiquidityReason".to_string(),
serde_json::Value::String("stable_fee_instruction_decoded_only".to_string()),
);
payload.insert(
"skipLifecycleReason".to_string(),
serde_json::Value::String("stable_fee_instruction_decoded_only".to_string()),
);
},
"simulate_info" => {
pool_account = account_at(accounts, 1);
market_account = None;
lp_mint = None;
token_a_mint = None;
token_b_mint = None;
base_vault = None;
quote_vault = None;
insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert(
"skipTradeReason".to_string(),
serde_json::Value::String("stable_simulate_info_decoded_only".to_string()),
);
payload.insert(
"skipLiquidityReason".to_string(),
serde_json::Value::String("stable_simulate_info_decoded_only".to_string()),
);
payload.insert(
"skipLifecycleReason".to_string(),
serde_json::Value::String("stable_simulate_info_decoded_only".to_string()),
);
},
"swap_base_in" | "swap_base_out" => {
pool_account = account_at(accounts, 1);
lp_mint = None;
let coin_vault = account_at(accounts, 4);
let pc_vault = account_at(accounts, 5);
market_account = account_at(accounts, 8);
let input_token_account = account_at(accounts, 15);
let output_token_account = account_at(accounts, 16);
let coin_mint = mint_for_account(coin_vault.as_deref(), token_mints_by_account);
let pc_mint = mint_for_account(pc_vault.as_deref(), token_mints_by_account);
let input_mint =
mint_for_account(input_token_account.as_deref(), token_mints_by_account);
let output_mint =
mint_for_account(output_token_account.as_deref(), token_mints_by_account);
let normalized = normalize_pair_with_vaults(
coin_mint.clone(),
pc_mint.clone(),
coin_vault.clone(),
pc_vault.clone(),
);
token_a_mint = normalized.base_mint;
token_b_mint = normalized.quote_mint;
base_vault = normalized.base_vault;
quote_vault = normalized.quote_vault;
insert_optional_string(&mut payload, "poolAccount", pool_account.clone());
insert_optional_string(&mut payload, "baseVault", base_vault.clone());
insert_optional_string(&mut payload, "quoteVault", quote_vault.clone());
insert_optional_string(&mut payload, "marketAccount", market_account.clone());
insert_optional_string(&mut payload, "inputTokenAccount", input_token_account);
insert_optional_string(&mut payload, "outputTokenAccount", output_token_account);
insert_optional_string(&mut payload, "inputMint", input_mint.clone());
insert_optional_string(&mut payload, "outputMint", output_mint.clone());
let amount_in_or_max = read_u64_le(data, 1);
let amount_out_or_min = read_u64_le(data, 9);
if instruction_name == "swap_base_in" {
insert_optional_u64_string(&mut payload, "amountInRaw", amount_in_or_max);
insert_optional_u64_string(&mut payload, "minimumAmountOutRaw", amount_out_or_min);
} else {
insert_optional_u64_string(&mut payload, "maxAmountInRaw", amount_in_or_max);
insert_optional_u64_string(&mut payload, "amountOutRaw", amount_out_or_min);
}
let instruction_amounts = map_swap_amounts(
instruction_name,
amount_in_or_max,
amount_out_or_min,
input_mint.as_deref(),
output_mint.as_deref(),
token_a_mint.as_deref(),
token_b_mint.as_deref(),
);
if let Some(instruction_amounts) = instruction_amounts {
insert_optional_string(
&mut payload,
"instructionTradeSide",
Some(instruction_amounts.trade_side.clone()),
);
insert_optional_string(
&mut payload,
"instructionBoundBaseAmountRaw",
Some(instruction_amounts.base_amount_raw),
);
insert_optional_string(
&mut payload,
"instructionBoundQuoteAmountRaw",
Some(instruction_amounts.quote_amount_raw),
);
}
let exact_amounts = resolve_stable_swap_vault_delta_amounts(
base_vault.as_deref(),
quote_vault.as_deref(),
token_balance_records,
);
match exact_amounts {
Some(exact_amounts) => {
insert_optional_string(
&mut payload,
"tradeSide",
Some(exact_amounts.trade_side),
);
insert_optional_string(
&mut payload,
"baseAmountRaw",
Some(exact_amounts.base_amount_raw),
);
insert_optional_string(
&mut payload,
"quoteAmountRaw",
Some(exact_amounts.quote_amount_raw),
);
payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(true));
payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(true));
payload.insert(
"amountSource".to_string(),
serde_json::Value::String("stable_swap_vault_balance_delta".to_string()),
);
payload.insert("skipTradeReason".to_string(), serde_json::Value::Null);
payload.insert("skipCandleReason".to_string(), serde_json::Value::Null);
},
None => {
payload.insert("tradeCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert("candleCandidate".to_string(), serde_json::Value::Bool(false));
payload.insert(
"amountSource".to_string(),
serde_json::Value::String(
"stable_swap_instruction_bounds_only".to_string(),
),
);
payload.insert(
"skipTradeReason".to_string(),
serde_json::Value::String(
"stable_swap_exact_amounts_unresolved".to_string(),
),
);
payload.insert(
"skipCandleReason".to_string(),
serde_json::Value::String(
"stable_swap_exact_amounts_unresolved".to_string(),
),
);
},
}
},
_ => return None,
}
insert_optional_string(&mut payload, "tokenAMint", token_a_mint.clone());
insert_optional_string(&mut payload, "tokenBMint", token_b_mint.clone());
return Some(RaydiumStableSwapInstructionDecoded {
transaction_id,
instruction_id,
signature: signature.to_string(),
program_id: program_id.to_string(),
event_kind,
instruction_name: instruction_name.to_string(),
discriminator_hex,
pool_account,
market_account,
lp_mint,
token_a_mint,
token_b_mint,
base_vault,
quote_vault,
payload_json: serde_json::Value::Object(payload),
});
}
fn stable_instruction_name(discriminator: u8) -> std::option::Option<&'static str> {
match discriminator {
0 => return Some("initialize"),
1 => return Some("init_model_data"),
2 => return Some("update_model_data"),
3 => return Some("deposit"),
4 => return Some("withdraw"),
5 => return Some("monitor_step"),
6 => return Some("set_params"),
7 => return Some("withdraw_pnl"),
8 => return Some("withdraw_srm"),
9 => return Some("swap_base_in"),
10 => return Some("pre_initialize"),
11 => return Some("swap_base_out"),
12 => return Some("simulate_info"),
13 => return Some("admin_cancel_orders"),
_ => return None,
}
}
fn stable_instruction_discriminator_hex(discriminator: u8) -> std::option::Option<&'static str> {
match discriminator {
0 => return Some(RAYDIUM_STABLE_SWAP_INITIALIZE_DISCRIMINATOR_HEX),
1 => return Some(RAYDIUM_STABLE_SWAP_INIT_MODEL_DATA_DISCRIMINATOR_HEX),
2 => return Some(RAYDIUM_STABLE_SWAP_UPDATE_MODEL_DATA_DISCRIMINATOR_HEX),
3 => return Some(RAYDIUM_STABLE_SWAP_DEPOSIT_DISCRIMINATOR_HEX),
4 => return Some(RAYDIUM_STABLE_SWAP_WITHDRAW_DISCRIMINATOR_HEX),
5 => return Some(RAYDIUM_STABLE_SWAP_MONITOR_STEP_DISCRIMINATOR_HEX),
6 => return Some(RAYDIUM_STABLE_SWAP_SET_PARAMS_DISCRIMINATOR_HEX),
7 => return Some(RAYDIUM_STABLE_SWAP_WITHDRAW_PNL_DISCRIMINATOR_HEX),
8 => return Some(RAYDIUM_STABLE_SWAP_WITHDRAW_SRM_DISCRIMINATOR_HEX),
9 => return Some(RAYDIUM_STABLE_SWAP_SWAP_BASE_IN_DISCRIMINATOR_HEX),
10 => return Some(RAYDIUM_STABLE_SWAP_PRE_INITIALIZE_DISCRIMINATOR_HEX),
11 => return Some(RAYDIUM_STABLE_SWAP_SWAP_BASE_OUT_DISCRIMINATOR_HEX),
12 => return Some(RAYDIUM_STABLE_SWAP_SIMULATE_INFO_DISCRIMINATOR_HEX),
13 => return Some(RAYDIUM_STABLE_SWAP_ADMIN_CANCEL_ORDERS_DISCRIMINATOR_HEX),
_ => return None,
}
}
#[derive(Debug, Clone)]
struct NormalizedVaultPair {
base_mint: std::option::Option<std::string::String>,
quote_mint: std::option::Option<std::string::String>,
base_vault: std::option::Option<std::string::String>,
quote_vault: std::option::Option<std::string::String>,
}
fn normalize_pair_with_vaults(
coin_mint: std::option::Option<std::string::String>,
pc_mint: std::option::Option<std::string::String>,
coin_vault: std::option::Option<std::string::String>,
pc_vault: std::option::Option<std::string::String>,
) -> NormalizedVaultPair {
let coin_mint_ref = match coin_mint.as_ref() {
Some(coin_mint) => coin_mint.as_str(),
None => {
return NormalizedVaultPair {
base_mint: None,
quote_mint: None,
base_vault: None,
quote_vault: None,
};
},
};
let pc_mint_ref = match pc_mint.as_ref() {
Some(pc_mint) => pc_mint.as_str(),
None => {
return NormalizedVaultPair {
base_mint: None,
quote_mint: None,
base_vault: None,
quote_vault: None,
};
},
};
let coin_is_quote = is_quote_mint(coin_mint_ref);
let pc_is_quote = is_quote_mint(pc_mint_ref);
let pc_is_normalized_quote = if pc_is_quote && !coin_is_quote {
true
} else if coin_is_quote && !pc_is_quote {
false
} else {
pc_mint_ref <= coin_mint_ref
};
if pc_is_normalized_quote {
return NormalizedVaultPair {
base_mint: coin_mint,
quote_mint: pc_mint,
base_vault: coin_vault,
quote_vault: pc_vault,
};
}
return NormalizedVaultPair {
base_mint: pc_mint,
quote_mint: coin_mint,
base_vault: pc_vault,
quote_vault: coin_vault,
};
}
fn is_quote_mint(mint: &str) -> bool {
return mint == crate::WSOL_MINT_ID
|| mint == crate::USDC_MINT_ID
|| mint == crate::USDT_MINT_ID;
}
#[derive(Debug, Clone)]
struct StableSwapAmounts {
trade_side: std::string::String,
base_amount_raw: std::string::String,
quote_amount_raw: std::string::String,
}
fn map_swap_amounts(
instruction_name: &str,
first_amount: std::option::Option<u64>,
second_amount: std::option::Option<u64>,
input_mint: std::option::Option<&str>,
output_mint: std::option::Option<&str>,
base_mint: std::option::Option<&str>,
quote_mint: std::option::Option<&str>,
) -> std::option::Option<StableSwapAmounts> {
let first_amount = match first_amount {
Some(first_amount) => first_amount,
None => return None,
};
let second_amount = match second_amount {
Some(second_amount) => second_amount,
None => return None,
};
let input_mint = match input_mint {
Some(input_mint) => input_mint,
None => return None,
};
if output_mint.is_none() {
return None;
}
let base_mint = match base_mint {
Some(base_mint) => base_mint,
None => return None,
};
let quote_mint = match quote_mint {
Some(quote_mint) => quote_mint,
None => return None,
};
if instruction_name == "swap_base_in" {
if input_mint == base_mint {
return Some(StableSwapAmounts {
trade_side: "sell".to_string(),
base_amount_raw: first_amount.to_string(),
quote_amount_raw: second_amount.to_string(),
});
}
if input_mint == quote_mint {
return Some(StableSwapAmounts {
trade_side: "buy".to_string(),
base_amount_raw: second_amount.to_string(),
quote_amount_raw: first_amount.to_string(),
});
}
}
if instruction_name == "swap_base_out" {
if input_mint == base_mint {
return Some(StableSwapAmounts {
trade_side: "sell".to_string(),
base_amount_raw: first_amount.to_string(),
quote_amount_raw: second_amount.to_string(),
});
}
if input_mint == quote_mint {
return Some(StableSwapAmounts {
trade_side: "buy".to_string(),
base_amount_raw: second_amount.to_string(),
quote_amount_raw: first_amount.to_string(),
});
}
}
return None;
}
#[derive(Debug, Clone)]
struct StableSwapExactAmounts {
trade_side: std::string::String,
base_amount_raw: std::string::String,
quote_amount_raw: std::string::String,
}
#[derive(Debug, Clone)]
struct TokenBalanceRecord {
account_address: std::option::Option<std::string::String>,
pre_amount_raw: std::option::Option<std::string::String>,
post_amount_raw: std::option::Option<std::string::String>,
}
#[derive(Debug, Clone)]
struct TokenBalanceAccumulator {
account_index: std::option::Option<u64>,
account_address: std::option::Option<std::string::String>,
mint: std::string::String,
pre_amount_raw: std::option::Option<std::string::String>,
post_amount_raw: std::option::Option<std::string::String>,
}
impl TokenBalanceRecord {
fn delta_raw(&self) -> std::option::Option<i128> {
let pre = parse_i128_or_zero(self.pre_amount_raw.as_deref());
let post = parse_i128_or_zero(self.post_amount_raw.as_deref());
match (pre, post) {
(Some(pre), Some(post)) => return Some(post - pre),
_ => return None,
}
}
}
fn resolve_stable_swap_vault_delta_amounts(
base_vault: std::option::Option<&str>,
quote_vault: std::option::Option<&str>,
token_balance_records: &[TokenBalanceRecord],
) -> std::option::Option<StableSwapExactAmounts> {
let base_vault = match base_vault {
Some(base_vault) => base_vault,
None => return None,
};
let quote_vault = match quote_vault {
Some(quote_vault) => quote_vault,
None => return None,
};
let base_record = token_balance_record_for_account(token_balance_records, base_vault);
let base_record = match base_record {
Some(base_record) => base_record,
None => return None,
};
let quote_record = token_balance_record_for_account(token_balance_records, quote_vault);
let quote_record = match quote_record {
Some(quote_record) => quote_record,
None => return None,
};
let base_delta = match base_record.delta_raw() {
Some(base_delta) => base_delta,
None => return None,
};
let quote_delta = match quote_record.delta_raw() {
Some(quote_delta) => quote_delta,
None => return None,
};
if base_delta == 0 || quote_delta == 0 {
return None;
}
if base_delta < 0 && quote_delta > 0 {
return Some(StableSwapExactAmounts {
trade_side: "buy".to_string(),
base_amount_raw: (-base_delta).to_string(),
quote_amount_raw: quote_delta.to_string(),
});
}
if base_delta > 0 && quote_delta < 0 {
return Some(StableSwapExactAmounts {
trade_side: "sell".to_string(),
base_amount_raw: base_delta.to_string(),
quote_amount_raw: (-quote_delta).to_string(),
});
}
return None;
}
fn token_balance_record_for_account<'a>(
token_balance_records: &'a [TokenBalanceRecord],
account: &str,
) -> std::option::Option<&'a TokenBalanceRecord> {
for record in token_balance_records {
if record.account_address.as_deref() == Some(account) {
return Some(record);
}
}
return None;
}
fn parse_i128_or_zero(value: std::option::Option<&str>) -> std::option::Option<i128> {
let value = match value {
Some(value) => value.trim(),
None => return Some(0),
};
if value.is_empty() {
return Some(0);
}
let parsed = value.parse::<i128>();
match parsed {
Ok(parsed) => return Some(parsed),
Err(_) => return None,
}
}
fn parse_accounts_json(
accounts_json: &str,
) -> std::option::Option<std::vec::Vec<std::string::String>> {
let value = match serde_json::from_str::<serde_json::Value>(accounts_json) {
Ok(value) => value,
Err(_) => return None,
};
let array = match value.as_array() {
Some(array) => array,
None => return None,
};
let mut accounts = std::vec::Vec::new();
for item in array {
if let Some(text) = item.as_str() {
accounts.push(text.to_string());
} else if let Some(pubkey) = item.get("pubkey").and_then(|value| return value.as_str()) {
accounts.push(pubkey.to_string());
}
}
return Some(accounts);
}
fn decode_data_json_base58(data_json: &str) -> std::option::Option<std::vec::Vec<u8>> {
let value = match serde_json::from_str::<serde_json::Value>(data_json) {
Ok(value) => value,
Err(_) => return None,
};
let data_base58 = if let Some(text) = value.as_str() {
text.to_string()
} else if let Some(text) = value.get("data").and_then(|value| return value.as_str()) {
text.to_string()
} else {
return None;
};
let decoded = bs58::decode(data_base58.as_str()).into_vec();
match decoded {
Ok(decoded) => return Some(decoded),
Err(_) => return None,
}
}
fn account_at(
accounts: &[std::string::String],
index: usize,
) -> std::option::Option<std::string::String> {
return accounts.get(index).cloned();
}
fn read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
let end = offset.saturating_add(8);
if data.len() < end {
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));
}
fn parse_json_value(input: &str) -> std::option::Option<serde_json::Value> {
let parsed = serde_json::from_str::<serde_json::Value>(input);
match parsed {
Ok(parsed) => return Some(parsed),
Err(_) => return None,
}
}
fn parse_optional_json_value(
input: std::option::Option<&str>,
) -> std::option::Option<serde_json::Value> {
let input = match input {
Some(input) => input,
None => return None,
};
return parse_json_value(input);
}
fn extract_transaction_account_keys(
transaction_json: std::option::Option<&serde_json::Value>,
meta_json: std::option::Option<&serde_json::Value>,
) -> std::vec::Vec<std::string::String> {
let mut account_keys = std::vec::Vec::new();
let message = transaction_json
.and_then(|value| return value.get("transaction"))
.and_then(|value| return value.get("message"));
if let Some(message) = message {
extract_account_keys_from_message(message, &mut account_keys);
}
if account_keys.is_empty() {
let message = transaction_json.and_then(|value| return value.get("message"));
if let Some(message) = message {
extract_account_keys_from_message(message, &mut account_keys);
}
}
if account_keys.is_empty() {
let loaded = meta_json.and_then(|value| return value.get("loadedAddresses"));
if let Some(loaded) = loaded {
append_loaded_addresses(loaded, &mut account_keys);
}
}
return account_keys;
}
fn extract_account_keys_from_message(
message: &serde_json::Value,
account_keys: &mut std::vec::Vec<std::string::String>,
) {
let keys = match message.get("accountKeys").and_then(|value| return value.as_array()) {
Some(keys) => keys,
None => return,
};
for key in keys {
if let Some(text) = key.as_str() {
account_keys.push(text.to_string());
} else if let Some(pubkey) = key.get("pubkey").and_then(|value| return value.as_str()) {
account_keys.push(pubkey.to_string());
}
}
if let Some(loaded) = message.get("loadedAddresses") {
append_loaded_addresses(loaded, account_keys);
}
}
fn append_loaded_addresses(
loaded: &serde_json::Value,
account_keys: &mut std::vec::Vec<std::string::String>,
) {
for section in ["writable", "readonly"] {
let array = loaded.get(section).and_then(|value| return value.as_array());
let array = match array {
Some(array) => array,
None => continue,
};
for item in array {
if let Some(text) = item.as_str() {
account_keys.push(text.to_string());
}
}
}
}
fn extract_token_balance_records(
transaction_json: std::option::Option<&serde_json::Value>,
meta_json: std::option::Option<&serde_json::Value>,
account_keys: &[std::string::String],
) -> std::vec::Vec<TokenBalanceRecord> {
let mut accumulators = std::vec::Vec::new();
let meta_candidates = [
meta_json,
transaction_json.and_then(|value| return value.get("meta")),
transaction_json
.and_then(|value| return value.get("transaction"))
.and_then(|value| return value.get("meta")),
];
for meta in meta_candidates.iter().flatten() {
collect_token_balance_side(
meta.get("preTokenBalances"),
account_keys,
true,
&mut accumulators,
);
collect_token_balance_side(
meta.get("postTokenBalances"),
account_keys,
false,
&mut accumulators,
);
}
let mut records = std::vec::Vec::new();
for accumulator in accumulators {
records.push(TokenBalanceRecord {
account_address: accumulator.account_address,
pre_amount_raw: accumulator.pre_amount_raw,
post_amount_raw: accumulator.post_amount_raw,
});
}
return records;
}
fn collect_token_balance_side(
balances: std::option::Option<&serde_json::Value>,
account_keys: &[std::string::String],
is_pre: bool,
accumulators: &mut std::vec::Vec<TokenBalanceAccumulator>,
) {
let array = balances.and_then(|value| return value.as_array());
let array = match array {
Some(array) => array,
None => return,
};
for balance in array {
let account_index = balance.get("accountIndex").and_then(|value| return value.as_u64());
let mint = match balance.get("mint").and_then(|value| return value.as_str()) {
Some(mint) => mint.to_string(),
None => continue,
};
let amount = balance
.get("uiTokenAmount")
.and_then(|value| return value.get("amount"))
.and_then(|value| return value.as_str())
.map(|value| return value.to_string());
let account_address = account_address_by_index(account_keys, account_index);
let accumulator_index =
find_token_balance_accumulator(accumulators.as_slice(), account_index, mint.as_str());
let index = match accumulator_index {
Some(index) => index,
None => {
accumulators.push(TokenBalanceAccumulator {
account_index,
account_address,
mint,
pre_amount_raw: None,
post_amount_raw: None,
});
accumulators.len() - 1
},
};
if is_pre {
accumulators[index].pre_amount_raw = amount;
} else {
accumulators[index].post_amount_raw = amount;
}
}
}
fn find_token_balance_accumulator(
accumulators: &[TokenBalanceAccumulator],
account_index: std::option::Option<u64>,
mint: &str,
) -> std::option::Option<usize> {
let mut index = 0usize;
while index < accumulators.len() {
let accumulator = &accumulators[index];
if accumulator.account_index == account_index && accumulator.mint == mint {
return Some(index);
}
index += 1;
}
return None;
}
fn account_address_by_index(
account_keys: &[std::string::String],
account_index: std::option::Option<u64>,
) -> std::option::Option<std::string::String> {
let account_index = match account_index {
Some(account_index) => account_index,
None => return None,
};
let index = match usize::try_from(account_index) {
Ok(index) => index,
Err(_) => return None,
};
return account_keys.get(index).cloned();
}
fn extract_token_mints_by_account(
transaction_json: std::option::Option<&serde_json::Value>,
meta_json: std::option::Option<&serde_json::Value>,
account_keys: &[std::string::String],
) -> std::collections::BTreeMap<std::string::String, std::string::String> {
let mut map = std::collections::BTreeMap::new();
let meta_candidates = [
meta_json,
transaction_json.and_then(|value| return value.get("meta")),
transaction_json
.and_then(|value| return value.get("transaction"))
.and_then(|value| return value.get("meta")),
];
for meta in meta_candidates.iter().flatten() {
append_token_balance_mints(meta.get("preTokenBalances"), account_keys, &mut map);
append_token_balance_mints(meta.get("postTokenBalances"), account_keys, &mut map);
}
return map;
}
fn append_token_balance_mints(
balances: std::option::Option<&serde_json::Value>,
account_keys: &[std::string::String],
map: &mut std::collections::BTreeMap<std::string::String, std::string::String>,
) {
let array = balances.and_then(|value| return value.as_array());
let array = match array {
Some(array) => array,
None => return,
};
for balance in array {
let account_index = balance.get("accountIndex").and_then(|value| return value.as_u64());
let mint = balance.get("mint").and_then(|value| return value.as_str());
let owner = balance.get("owner").and_then(|value| return value.as_str());
let account_index = match account_index {
Some(account_index) => account_index,
None => continue,
};
let mint = match mint {
Some(mint) => mint,
None => continue,
};
let index_result = usize::try_from(account_index);
let index = match index_result {
Ok(index) => index,
Err(_) => continue,
};
if let Some(account) = account_keys.get(index) {
map.insert(account.clone(), mint.to_string());
}
if let Some(owner) = owner {
map.insert(owner.to_string(), mint.to_string());
}
}
}
fn mint_for_account(
account: std::option::Option<&str>,
token_mints_by_account: &std::collections::BTreeMap<std::string::String, std::string::String>,
) -> std::option::Option<std::string::String> {
let account = match account {
Some(account) => account,
None => return None,
};
return token_mints_by_account.get(account).cloned();
}
fn insert_optional_string(
object: &mut serde_json::Map<std::string::String, serde_json::Value>,
key: &str,
value: std::option::Option<std::string::String>,
) {
if let Some(value) = value {
object.insert(key.to_string(), serde_json::Value::String(value));
}
}
fn insert_optional_u8(
object: &mut serde_json::Map<std::string::String, serde_json::Value>,
key: &str,
value: std::option::Option<u8>,
) {
if let Some(value) = value {
object.insert(key.to_string(), serde_json::Value::Number(serde_json::Number::from(value)));
}
}
fn insert_optional_u64(
object: &mut serde_json::Map<std::string::String, serde_json::Value>,
key: &str,
value: std::option::Option<u64>,
) {
if let Some(value) = value {
object.insert(key.to_string(), serde_json::Value::Number(serde_json::Number::from(value)));
}
}
fn insert_optional_u64_string(
object: &mut serde_json::Map<std::string::String, serde_json::Value>,
key: &str,
value: std::option::Option<u64>,
) {
if let Some(value) = value {
object.insert(key.to_string(), serde_json::Value::String(value.to_string()));
}
}
fn bytes_to_hex(data: &[u8]) -> std::string::String {
let mut output = std::string::String::new();
for byte in data {
output.push_str(format!("{:02x}", byte).as_str());
}
return output;
}
#[cfg(test)]
mod tests {
fn encode_data(data: &[u8]) -> std::string::String {
return bs58::encode(data).into_string();
}
#[test]
fn classifies_one_byte_stable_swap_discriminators() {
let initialize = serde_json::json!(encode_data(&[0_u8, 1_u8, 0, 0, 0, 0, 0, 0, 0, 0]));
let deposit = serde_json::json!(encode_data(&[
3_u8, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
]));
let initialize_text = initialize.to_string();
let deposit_text = deposit.to_string();
assert_eq!(
super::classify_raydium_stable_swap_instruction_data(initialize_text.as_str()),
Some("initialize")
);
assert_eq!(
super::classify_raydium_stable_swap_instruction_data(deposit_text.as_str()),
Some("deposit")
);
}
#[test]
fn decodes_stable_swap_initialize_with_normalized_pair() {
let accounts = serde_json::json!([
"TokenProgram1111111111111111111111111111111111",
"System111111111111111111111111111111111111",
"Rent111111111111111111111111111111111111111",
"StablePool11111111111111111111111111111111111",
"Authority111111111111111111111111111111111111",
"OpenOrders1111111111111111111111111111111111",
"LpMint11111111111111111111111111111111111111",
crate::USDT_MINT_ID,
crate::USDC_MINT_ID,
"CoinVault1111111111111111111111111111111111",
"PcVault11111111111111111111111111111111111",
"TargetOrders11111111111111111111111111111111",
"ModelData1111111111111111111111111111111111",
"SerumProgram111111111111111111111111111111",
"SerumMarket1111111111111111111111111111111",
"UserLp111111111111111111111111111111111111",
"Wallet111111111111111111111111111111111111"
]);
let mut data = vec![0_u8, 1_u8];
data.extend_from_slice(&123_u64.to_le_bytes());
let events = super::decode_raydium_stable_swap_instruction_with_context(
1,
2,
"sig",
crate::RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID,
accounts.to_string().as_str(),
Some(serde_json::json!(encode_data(data.as_slice())).to_string().as_str()),
&std::collections::BTreeMap::new(),
&[],
);
assert_eq!(events.len(), 1);
match &events[0] {
crate::RaydiumStableSwapDecodedEvent::Instruction(event) => {
assert_eq!(event.event_kind, "raydium_stable_swap.initialize");
assert_eq!(
event.pool_account.as_deref(),
Some("StablePool11111111111111111111111111111111111")
);
assert_eq!(event.token_b_mint.as_deref(), Some(crate::USDC_MINT_ID));
},
_ => panic!("expected instruction event"),
}
}
}