1525 lines
59 KiB
Rust
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"),
|
|
}
|
|
}
|
|
}
|