0.7.51
This commit is contained in:
@@ -40,6 +40,12 @@ pub struct RaydiumAmmV4SwapDecoded {
|
||||
pub signature: std::string::String,
|
||||
/// Program id.
|
||||
pub program_id: std::string::String,
|
||||
/// Local decoded event kind derived from the AMM v4 instruction discriminator.
|
||||
pub event_kind: std::string::String,
|
||||
/// Upstream instruction name derived from the AMM v4 instruction discriminator.
|
||||
pub instruction_name: std::string::String,
|
||||
/// One-byte Raydium AMM v4 instruction discriminator in hex form.
|
||||
pub discriminator_hex: std::string::String,
|
||||
/// AMM pool/state account.
|
||||
pub pool_account: std::string::String,
|
||||
/// Raydium AMM authority account.
|
||||
@@ -78,6 +84,49 @@ pub struct RaydiumAmmV4SwapDecoded {
|
||||
pub payload_json: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Decoded Raydium AmmV4 non-swap or decoded-only instruction event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct RaydiumAmmV4InstructionDecoded {
|
||||
/// 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 derived from the AMM v4 instruction discriminator.
|
||||
pub event_kind: std::string::String,
|
||||
/// Upstream instruction name derived from the AMM v4 instruction discriminator.
|
||||
pub instruction_name: std::string::String,
|
||||
/// One-byte Raydium AMM v4 instruction discriminator in hex form.
|
||||
pub discriminator_hex: std::string::String,
|
||||
/// Optional AMM pool/state account.
|
||||
pub pool_account: std::option::Option<std::string::String>,
|
||||
/// Optional Raydium AMM authority account.
|
||||
pub authority: std::option::Option<std::string::String>,
|
||||
/// Optional AMM open-orders account.
|
||||
pub open_orders: std::option::Option<std::string::String>,
|
||||
/// Optional AMM target-orders account.
|
||||
pub target_orders: std::option::Option<std::string::String>,
|
||||
/// Optional market program account.
|
||||
pub market_program: 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 token A mint after best-effort account layout extraction.
|
||||
pub token_a_mint: std::option::Option<std::string::String>,
|
||||
/// Optional token B mint after best-effort account layout extraction.
|
||||
pub token_b_mint: std::option::Option<std::string::String>,
|
||||
/// Optional AMM coin/base vault account.
|
||||
pub base_vault: std::option::Option<std::string::String>,
|
||||
/// Optional AMM pc/quote vault account.
|
||||
pub quote_vault: std::option::Option<std::string::String>,
|
||||
/// Decoded payload.
|
||||
pub payload_json: serde_json::Value,
|
||||
}
|
||||
|
||||
/// Decoded Raydium AmmV4 event.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub enum RaydiumAmmV4DecodedEvent {
|
||||
@@ -85,6 +134,8 @@ pub enum RaydiumAmmV4DecodedEvent {
|
||||
Initialize2Pool(std::boxed::Box<RaydiumAmmV4Initialize2PoolDecoded>),
|
||||
/// Swap event decoded from a direct or inner Raydium AMM v4 instruction.
|
||||
Swap(std::boxed::Box<RaydiumAmmV4SwapDecoded>),
|
||||
/// Known Raydium AMM v4 instruction decoded without direct trade materialization.
|
||||
Instruction(std::boxed::Box<RaydiumAmmV4InstructionDecoded>),
|
||||
}
|
||||
|
||||
/// Raydium AmmV4 decoder.
|
||||
@@ -182,11 +233,27 @@ impl RaydiumAmmV4Decoder {
|
||||
token_balances.as_slice(),
|
||||
);
|
||||
match swap_result {
|
||||
Ok(Some(swap)) => decoded_events
|
||||
.push(crate::RaydiumAmmV4DecodedEvent::Swap(std::boxed::Box::new(swap))),
|
||||
Ok(Some(swap)) => {
|
||||
decoded_events
|
||||
.push(crate::RaydiumAmmV4DecodedEvent::Swap(std::boxed::Box::new(swap)));
|
||||
continue;
|
||||
},
|
||||
Ok(None) => {},
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
let instruction_event = decode_known_instruction_event(
|
||||
transaction,
|
||||
transaction_id,
|
||||
instruction,
|
||||
instruction_id,
|
||||
program_id,
|
||||
accounts.as_slice(),
|
||||
);
|
||||
if let Some(instruction_event) = instruction_event {
|
||||
decoded_events.push(crate::RaydiumAmmV4DecodedEvent::Instruction(
|
||||
std::boxed::Box::new(instruction_event),
|
||||
));
|
||||
}
|
||||
}
|
||||
return Ok(decoded_events);
|
||||
}
|
||||
@@ -209,9 +276,15 @@ fn decode_initialize2_event(
|
||||
let token_a_mint = extract_account(accounts, 8);
|
||||
let token_b_mint = extract_account(accounts, 9);
|
||||
let market_account = extract_account(accounts, 16);
|
||||
let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref());
|
||||
let discriminator_hex = raydium_amm_v4_instruction_discriminator_hex(data_base58.as_deref());
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "raydium_amm_v4",
|
||||
"eventKind": "initialize2_pool",
|
||||
"instructionName": "initialize2",
|
||||
"upstreamInstructionName": "initialize2",
|
||||
"discriminatorHex": discriminator_hex,
|
||||
"instructionDiscriminatorHex": discriminator_hex,
|
||||
"signature": transaction.signature,
|
||||
"instructionId": instruction_id,
|
||||
"instructionIndex": instruction.instruction_index,
|
||||
@@ -251,6 +324,11 @@ fn decode_swap_event(
|
||||
if accounts.len() < 8 {
|
||||
return Ok(None);
|
||||
}
|
||||
let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref());
|
||||
let swap_instruction = match raydium_amm_v4_swap_instruction(data_base58.as_deref()) {
|
||||
Some(swap_instruction) => swap_instruction,
|
||||
None => return Ok(None),
|
||||
};
|
||||
let pool_account = match extract_account(accounts, 1) {
|
||||
Some(pool_account) => pool_account,
|
||||
None => return Ok(None),
|
||||
@@ -294,11 +372,14 @@ fn decode_swap_event(
|
||||
normalized_pair.quote_vault.as_str(),
|
||||
);
|
||||
let parent_program = parent_program_id_for_instruction(instruction, transaction_instructions);
|
||||
let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref());
|
||||
let trade_candidate = base_amount_raw.is_some() && quote_amount_raw.is_some();
|
||||
let payload_json = serde_json::json!({
|
||||
"decoder": "raydium_amm_v4",
|
||||
"eventKind": "swap",
|
||||
"eventKind": swap_instruction.event_kind,
|
||||
"instructionName": swap_instruction.instruction_name,
|
||||
"upstreamInstructionName": swap_instruction.instruction_name,
|
||||
"discriminatorHex": swap_instruction.discriminator_hex,
|
||||
"instructionDiscriminatorHex": swap_instruction.discriminator_hex,
|
||||
"signature": transaction.signature,
|
||||
"instructionId": instruction_id,
|
||||
"instructionIndex": instruction.instruction_index,
|
||||
@@ -337,6 +418,9 @@ fn decode_swap_event(
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
program_id: program_id.to_string(),
|
||||
event_kind: swap_instruction.event_kind.to_string(),
|
||||
instruction_name: swap_instruction.instruction_name.to_string(),
|
||||
discriminator_hex: swap_instruction.discriminator_hex.to_string(),
|
||||
pool_account,
|
||||
authority,
|
||||
token_a_mint: normalized_pair.base_mint,
|
||||
@@ -358,6 +442,439 @@ fn decode_swap_event(
|
||||
}));
|
||||
}
|
||||
|
||||
fn decode_known_instruction_event(
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
transaction_id: i64,
|
||||
instruction: &crate::ChainInstructionDto,
|
||||
instruction_id: i64,
|
||||
program_id: &str,
|
||||
accounts: &[std::string::String],
|
||||
) -> std::option::Option<crate::RaydiumAmmV4InstructionDecoded> {
|
||||
let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref());
|
||||
let data_bytes = raydium_amm_v4_instruction_data_bytes(data_base58.as_deref());
|
||||
let identity = match raydium_amm_v4_instruction_identity(data_base58.as_deref()) {
|
||||
Some(identity) => identity,
|
||||
None => return None,
|
||||
};
|
||||
let account_layout = raydium_amm_v4_account_layout(identity.discriminator_hex);
|
||||
let pool_account = extract_account_by_index(accounts, account_layout.pool_account_index);
|
||||
let authority = extract_account_by_index(accounts, account_layout.authority_index);
|
||||
let open_orders = extract_account_by_index(accounts, account_layout.open_orders_index);
|
||||
let target_orders = extract_account_by_index(accounts, account_layout.target_orders_index);
|
||||
let market_program = extract_account_by_index(accounts, account_layout.market_program_index);
|
||||
let market_account = extract_account_by_index(accounts, account_layout.market_account_index);
|
||||
let lp_mint = extract_account_by_index(accounts, account_layout.lp_mint_index);
|
||||
let token_a_mint = extract_account_by_index(accounts, account_layout.token_a_mint_index);
|
||||
let token_b_mint = extract_account_by_index(accounts, account_layout.token_b_mint_index);
|
||||
let base_vault = extract_account_by_index(accounts, account_layout.base_vault_index);
|
||||
let quote_vault = extract_account_by_index(accounts, account_layout.quote_vault_index);
|
||||
let mut payload_json = serde_json::json!({
|
||||
"decoder": "raydium_amm_v4",
|
||||
"eventKind": identity.event_kind,
|
||||
"instructionName": identity.instruction_name,
|
||||
"upstreamInstructionName": identity.instruction_name,
|
||||
"discriminatorHex": identity.discriminator_hex,
|
||||
"instructionDiscriminatorHex": identity.discriminator_hex,
|
||||
"signature": transaction.signature,
|
||||
"instructionId": instruction_id,
|
||||
"instructionIndex": instruction.instruction_index,
|
||||
"innerInstructionIndex": instruction.inner_instruction_index,
|
||||
"innerInstruction": instruction.inner_instruction_index.is_some(),
|
||||
"parentInstructionId": instruction.parent_instruction_id,
|
||||
"programId": program_id,
|
||||
"accounts": accounts,
|
||||
"data": data_base58,
|
||||
"instructionDataLength": data_bytes.as_ref().map(std::vec::Vec::len),
|
||||
"poolAccount": pool_account.clone(),
|
||||
"authority": authority.clone(),
|
||||
"openOrders": open_orders.clone(),
|
||||
"targetOrders": target_orders.clone(),
|
||||
"marketProgram": market_program.clone(),
|
||||
"marketAccount": market_account.clone(),
|
||||
"lpMint": lp_mint.clone(),
|
||||
"tokenAMint": token_a_mint.clone(),
|
||||
"tokenBMint": token_b_mint.clone(),
|
||||
"baseVault": base_vault.clone(),
|
||||
"quoteVault": quote_vault.clone(),
|
||||
"tradeCandidate": false,
|
||||
"candleCandidate": false,
|
||||
"skipTradeReason": "decoded_only_instruction",
|
||||
"skipCandleReason": "decoded_only_instruction"
|
||||
});
|
||||
enrich_known_instruction_payload(&mut payload_json, identity.discriminator_hex, data_bytes.as_deref());
|
||||
return Some(crate::RaydiumAmmV4InstructionDecoded {
|
||||
transaction_id,
|
||||
instruction_id,
|
||||
signature: transaction.signature.clone(),
|
||||
program_id: program_id.to_string(),
|
||||
event_kind: identity.event_kind.to_string(),
|
||||
instruction_name: identity.instruction_name.to_string(),
|
||||
discriminator_hex: identity.discriminator_hex.to_string(),
|
||||
pool_account,
|
||||
authority,
|
||||
open_orders,
|
||||
target_orders,
|
||||
market_program,
|
||||
market_account,
|
||||
lp_mint,
|
||||
token_a_mint,
|
||||
token_b_mint,
|
||||
base_vault,
|
||||
quote_vault,
|
||||
payload_json,
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct RaydiumAmmV4AccountLayout {
|
||||
pool_account_index: std::option::Option<usize>,
|
||||
authority_index: std::option::Option<usize>,
|
||||
open_orders_index: std::option::Option<usize>,
|
||||
target_orders_index: std::option::Option<usize>,
|
||||
market_program_index: std::option::Option<usize>,
|
||||
market_account_index: std::option::Option<usize>,
|
||||
lp_mint_index: std::option::Option<usize>,
|
||||
token_a_mint_index: std::option::Option<usize>,
|
||||
token_b_mint_index: std::option::Option<usize>,
|
||||
base_vault_index: std::option::Option<usize>,
|
||||
quote_vault_index: std::option::Option<usize>,
|
||||
}
|
||||
|
||||
fn raydium_amm_v4_empty_account_layout() -> RaydiumAmmV4AccountLayout {
|
||||
return RaydiumAmmV4AccountLayout {
|
||||
pool_account_index: None,
|
||||
authority_index: None,
|
||||
open_orders_index: None,
|
||||
target_orders_index: None,
|
||||
market_program_index: None,
|
||||
market_account_index: None,
|
||||
lp_mint_index: None,
|
||||
token_a_mint_index: None,
|
||||
token_b_mint_index: None,
|
||||
base_vault_index: None,
|
||||
quote_vault_index: None,
|
||||
};
|
||||
}
|
||||
|
||||
fn raydium_amm_v4_account_layout(discriminator_hex: &str) -> RaydiumAmmV4AccountLayout {
|
||||
let mut layout = raydium_amm_v4_empty_account_layout();
|
||||
match discriminator_hex {
|
||||
"00" => {
|
||||
layout.pool_account_index = Some(3);
|
||||
},
|
||||
"01" => {
|
||||
layout.pool_account_index = Some(4);
|
||||
layout.authority_index = Some(5);
|
||||
layout.open_orders_index = Some(6);
|
||||
layout.lp_mint_index = Some(7);
|
||||
layout.token_a_mint_index = Some(8);
|
||||
layout.token_b_mint_index = Some(9);
|
||||
layout.base_vault_index = Some(10);
|
||||
layout.quote_vault_index = Some(11);
|
||||
layout.target_orders_index = Some(12);
|
||||
layout.market_program_index = Some(15);
|
||||
layout.market_account_index = Some(16);
|
||||
},
|
||||
"02" => {
|
||||
layout.pool_account_index = Some(3);
|
||||
layout.authority_index = Some(4);
|
||||
layout.open_orders_index = Some(5);
|
||||
layout.target_orders_index = Some(6);
|
||||
layout.base_vault_index = Some(7);
|
||||
layout.quote_vault_index = Some(8);
|
||||
layout.market_program_index = Some(9);
|
||||
layout.market_account_index = Some(10);
|
||||
},
|
||||
"03" => {
|
||||
layout.pool_account_index = Some(1);
|
||||
layout.authority_index = Some(2);
|
||||
layout.open_orders_index = Some(3);
|
||||
layout.target_orders_index = Some(4);
|
||||
layout.lp_mint_index = Some(5);
|
||||
layout.base_vault_index = Some(6);
|
||||
layout.quote_vault_index = Some(7);
|
||||
layout.market_account_index = Some(8);
|
||||
},
|
||||
"04" => {
|
||||
layout.pool_account_index = Some(1);
|
||||
layout.authority_index = Some(2);
|
||||
layout.open_orders_index = Some(3);
|
||||
layout.target_orders_index = Some(4);
|
||||
layout.lp_mint_index = Some(5);
|
||||
layout.base_vault_index = Some(6);
|
||||
layout.quote_vault_index = Some(7);
|
||||
layout.market_program_index = Some(8);
|
||||
layout.market_account_index = Some(9);
|
||||
},
|
||||
"05" => {
|
||||
layout.pool_account_index = Some(3);
|
||||
layout.authority_index = Some(4);
|
||||
layout.open_orders_index = Some(5);
|
||||
layout.base_vault_index = Some(6);
|
||||
layout.quote_vault_index = Some(7);
|
||||
layout.target_orders_index = Some(8);
|
||||
layout.market_program_index = Some(9);
|
||||
layout.market_account_index = Some(10);
|
||||
},
|
||||
"06" => {
|
||||
layout.pool_account_index = Some(1);
|
||||
layout.authority_index = Some(2);
|
||||
layout.open_orders_index = Some(3);
|
||||
layout.target_orders_index = Some(4);
|
||||
layout.base_vault_index = Some(5);
|
||||
layout.quote_vault_index = Some(6);
|
||||
layout.market_program_index = Some(7);
|
||||
layout.market_account_index = Some(8);
|
||||
},
|
||||
"07" => {
|
||||
layout.pool_account_index = Some(1);
|
||||
layout.authority_index = Some(3);
|
||||
layout.open_orders_index = Some(4);
|
||||
layout.base_vault_index = Some(5);
|
||||
layout.quote_vault_index = Some(6);
|
||||
layout.target_orders_index = Some(10);
|
||||
layout.market_program_index = Some(11);
|
||||
layout.market_account_index = Some(12);
|
||||
},
|
||||
"08" => {
|
||||
layout.pool_account_index = Some(1);
|
||||
layout.authority_index = Some(3);
|
||||
},
|
||||
"09" | "0b" => {
|
||||
layout.pool_account_index = Some(1);
|
||||
layout.authority_index = Some(2);
|
||||
layout.open_orders_index = Some(3);
|
||||
layout.target_orders_index = Some(4);
|
||||
layout.base_vault_index = Some(5);
|
||||
layout.quote_vault_index = Some(6);
|
||||
layout.market_program_index = Some(7);
|
||||
layout.market_account_index = Some(8);
|
||||
},
|
||||
"0a" => {
|
||||
layout.pool_account_index = Some(4);
|
||||
},
|
||||
"0c" => {
|
||||
layout.pool_account_index = Some(1);
|
||||
},
|
||||
"0d" => {
|
||||
layout.pool_account_index = Some(1);
|
||||
},
|
||||
"10" | "11" => {
|
||||
layout.pool_account_index = Some(1);
|
||||
layout.authority_index = Some(2);
|
||||
layout.base_vault_index = Some(3);
|
||||
layout.quote_vault_index = Some(4);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
fn extract_account_by_index(
|
||||
accounts: &[std::string::String],
|
||||
index: std::option::Option<usize>,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let index = match index {
|
||||
Some(index) => index,
|
||||
None => return None,
|
||||
};
|
||||
return extract_account(accounts, index);
|
||||
}
|
||||
|
||||
fn raydium_amm_v4_instruction_data_bytes(
|
||||
data_base58: std::option::Option<&str>,
|
||||
) -> std::option::Option<std::vec::Vec<u8>> {
|
||||
let data_base58 = match data_base58 {
|
||||
Some(data_base58) => data_base58,
|
||||
None => return None,
|
||||
};
|
||||
let bytes_result = bs58::decode(data_base58).into_vec();
|
||||
match bytes_result {
|
||||
Ok(bytes) => return Some(bytes),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn enrich_known_instruction_payload(
|
||||
payload_json: &mut serde_json::Value,
|
||||
discriminator_hex: &str,
|
||||
data: std::option::Option<&[u8]>,
|
||||
) {
|
||||
let data = match data {
|
||||
Some(data) => data,
|
||||
None => return,
|
||||
};
|
||||
match discriminator_hex {
|
||||
"00" => {
|
||||
insert_u8(payload_json, "nonce", data, 1);
|
||||
insert_u64_string(payload_json, "openTime", data, 2);
|
||||
},
|
||||
"01" => {
|
||||
insert_u8(payload_json, "nonce", data, 1);
|
||||
insert_u64_string(payload_json, "openTime", data, 2);
|
||||
insert_u64_string(payload_json, "initPcAmount", data, 10);
|
||||
insert_u64_string(payload_json, "initCoinAmount", data, 18);
|
||||
insert_u64_string(payload_json, "tokenAAmount", data, 18);
|
||||
insert_u64_string(payload_json, "tokenBAmount", data, 10);
|
||||
},
|
||||
"02" => {
|
||||
insert_u16(payload_json, "planOrderLimit", data, 1);
|
||||
insert_u16(payload_json, "placeOrderLimit", data, 3);
|
||||
insert_u16(payload_json, "cancelOrderLimit", data, 5);
|
||||
},
|
||||
"03" => {
|
||||
insert_u64_string(payload_json, "maxCoinAmount", data, 1);
|
||||
insert_u64_string(payload_json, "maxPcAmount", data, 9);
|
||||
insert_u64_string(payload_json, "baseSide", data, 17);
|
||||
insert_u64_string(payload_json, "otherAmountMin", data, 25);
|
||||
insert_u64_string(payload_json, "tokenAAmount", data, 1);
|
||||
insert_u64_string(payload_json, "tokenBAmount", data, 9);
|
||||
},
|
||||
"04" => {
|
||||
insert_u64_string(payload_json, "lpAmountRaw", data, 1);
|
||||
insert_u64_string(payload_json, "liquidity", data, 1);
|
||||
insert_u64_string(payload_json, "minCoinAmount", data, 9);
|
||||
insert_u64_string(payload_json, "minPcAmount", data, 17);
|
||||
},
|
||||
"06" => {
|
||||
insert_u8(payload_json, "configParam", data, 1);
|
||||
insert_u64_string(payload_json, "configValue", data, 2);
|
||||
insert_fixed_hex(payload_json, "configPubkeyHex", data, 2, 32);
|
||||
insert_u64_string(payload_json, "lastOrderNumerator", data, 2);
|
||||
insert_u64_string(payload_json, "lastOrderDenominator", data, 10);
|
||||
},
|
||||
"08" => {
|
||||
insert_u64_string(payload_json, "amountRaw", data, 1);
|
||||
},
|
||||
"09" | "10" => {
|
||||
insert_u64_string(payload_json, "amountIn", data, 1);
|
||||
insert_u64_string(payload_json, "minimumAmountOut", data, 9);
|
||||
},
|
||||
"0a" => {
|
||||
insert_u8(payload_json, "nonce", data, 1);
|
||||
},
|
||||
"0b" | "11" => {
|
||||
insert_u64_string(payload_json, "maxAmountIn", data, 1);
|
||||
insert_u64_string(payload_json, "amountOut", data, 9);
|
||||
},
|
||||
"0c" => {
|
||||
insert_u8(payload_json, "simulateParam", data, 1);
|
||||
insert_u64_string(payload_json, "amountIn", data, 2);
|
||||
insert_u64_string(payload_json, "minimumAmountOut", data, 10);
|
||||
insert_u64_string(payload_json, "maxAmountIn", data, 2);
|
||||
insert_u64_string(payload_json, "amountOut", data, 10);
|
||||
},
|
||||
"0d" => {
|
||||
insert_u16(payload_json, "orderCancelLimit", data, 1);
|
||||
},
|
||||
"0f" => {
|
||||
insert_u8(payload_json, "configParam", data, 1);
|
||||
insert_fixed_hex(payload_json, "configOwnerHex", data, 2, 32);
|
||||
insert_u64_string(payload_json, "createPoolFee", data, 2);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_u8(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) {
|
||||
let value = match read_u8(data, offset) {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
insert_json_value(
|
||||
payload_json,
|
||||
key,
|
||||
serde_json::Value::Number(serde_json::Number::from(value as u64)),
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_u16(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) {
|
||||
let value = match read_u16_le(data, offset) {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
insert_json_value(
|
||||
payload_json,
|
||||
key,
|
||||
serde_json::Value::Number(serde_json::Number::from(value as u64)),
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_u64_string(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) {
|
||||
let value = match read_u64_le(data, offset) {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
insert_json_value(
|
||||
payload_json,
|
||||
key,
|
||||
serde_json::Value::String(value.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
fn insert_fixed_hex(
|
||||
payload_json: &mut serde_json::Value,
|
||||
key: &str,
|
||||
data: &[u8],
|
||||
offset: usize,
|
||||
len: usize,
|
||||
) {
|
||||
if data.len() < offset + len {
|
||||
return;
|
||||
}
|
||||
let slice = &data[offset..offset + len];
|
||||
insert_json_value(payload_json, key, serde_json::Value::String(bytes_to_hex(slice)));
|
||||
}
|
||||
|
||||
fn insert_json_value(payload_json: &mut serde_json::Value, key: &str, value: serde_json::Value) {
|
||||
let object_option = payload_json.as_object_mut();
|
||||
let object = match object_option {
|
||||
Some(object) => object,
|
||||
None => return,
|
||||
};
|
||||
object.insert(key.to_string(), value);
|
||||
}
|
||||
|
||||
fn read_u8(data: &[u8], offset: usize) -> std::option::Option<u8> {
|
||||
if data.len() < offset + 1 {
|
||||
return None;
|
||||
}
|
||||
return Some(data[offset]);
|
||||
}
|
||||
|
||||
fn read_u16_le(data: &[u8], offset: usize) -> std::option::Option<u16> {
|
||||
if data.len() < offset + 2 {
|
||||
return None;
|
||||
}
|
||||
let bytes = [data[offset], data[offset + 1]];
|
||||
return Some(u16::from_le_bytes(bytes));
|
||||
}
|
||||
|
||||
fn read_u64_le(data: &[u8], offset: usize) -> std::option::Option<u64> {
|
||||
if data.len() < offset + 8 {
|
||||
return None;
|
||||
}
|
||||
let bytes = [
|
||||
data[offset],
|
||||
data[offset + 1],
|
||||
data[offset + 2],
|
||||
data[offset + 3],
|
||||
data[offset + 4],
|
||||
data[offset + 5],
|
||||
data[offset + 6],
|
||||
data[offset + 7],
|
||||
];
|
||||
return Some(u64::from_le_bytes(bytes));
|
||||
}
|
||||
|
||||
fn bytes_to_hex(bytes: &[u8]) -> std::string::String {
|
||||
let mut output = std::string::String::new();
|
||||
for byte in bytes {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TokenBalanceRecord {
|
||||
account_address: std::option::Option<std::string::String>,
|
||||
@@ -401,6 +918,183 @@ struct NormalizedVaultPair {
|
||||
quote_mint: std::string::String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: &'static str,
|
||||
event_kind: &'static str,
|
||||
discriminator_hex: &'static str,
|
||||
}
|
||||
|
||||
fn raydium_amm_v4_swap_instruction(
|
||||
data_base58: std::option::Option<&str>,
|
||||
) -> std::option::Option<RaydiumAmmV4InstructionIdentity> {
|
||||
let identity = match raydium_amm_v4_instruction_identity(data_base58) {
|
||||
Some(identity) => identity,
|
||||
None => return None,
|
||||
};
|
||||
match identity.discriminator_hex {
|
||||
"09" | "0b" | "10" | "11" => return Some(identity),
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn raydium_amm_v4_instruction_identity(
|
||||
data_base58: std::option::Option<&str>,
|
||||
) -> std::option::Option<RaydiumAmmV4InstructionIdentity> {
|
||||
let discriminator_hex = match raydium_amm_v4_instruction_discriminator_hex(data_base58) {
|
||||
Some(discriminator_hex) => discriminator_hex,
|
||||
None => return None,
|
||||
};
|
||||
match discriminator_hex.as_str() {
|
||||
"00" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "initialize",
|
||||
event_kind: "raydium_amm_v4.initialize",
|
||||
discriminator_hex: "00",
|
||||
});
|
||||
},
|
||||
"01" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "initialize2",
|
||||
event_kind: "raydium_amm_v4.initialize2_pool",
|
||||
discriminator_hex: "01",
|
||||
});
|
||||
},
|
||||
"02" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "monitor_step",
|
||||
event_kind: "raydium_amm_v4.monitor_step",
|
||||
discriminator_hex: "02",
|
||||
});
|
||||
},
|
||||
"03" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "deposit",
|
||||
event_kind: "raydium_amm_v4.deposit",
|
||||
discriminator_hex: "03",
|
||||
});
|
||||
},
|
||||
"04" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "withdraw",
|
||||
event_kind: "raydium_amm_v4.withdraw",
|
||||
discriminator_hex: "04",
|
||||
});
|
||||
},
|
||||
"05" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "migrate_to_open_book",
|
||||
event_kind: "raydium_amm_v4.migrate_to_open_book",
|
||||
discriminator_hex: "05",
|
||||
});
|
||||
},
|
||||
"06" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "set_params",
|
||||
event_kind: "raydium_amm_v4.set_params",
|
||||
discriminator_hex: "06",
|
||||
});
|
||||
},
|
||||
"07" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "withdraw_pnl",
|
||||
event_kind: "raydium_amm_v4.withdraw_pnl",
|
||||
discriminator_hex: "07",
|
||||
});
|
||||
},
|
||||
"08" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "withdraw_srm",
|
||||
event_kind: "raydium_amm_v4.withdraw_srm",
|
||||
discriminator_hex: "08",
|
||||
});
|
||||
},
|
||||
"09" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "swap_base_in",
|
||||
event_kind: "raydium_amm_v4.swap_base_in",
|
||||
discriminator_hex: "09",
|
||||
});
|
||||
},
|
||||
"0a" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "pre_initialize",
|
||||
event_kind: "raydium_amm_v4.pre_initialize",
|
||||
discriminator_hex: "0a",
|
||||
});
|
||||
},
|
||||
"0b" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "swap_base_out",
|
||||
event_kind: "raydium_amm_v4.swap_base_out",
|
||||
discriminator_hex: "0b",
|
||||
});
|
||||
},
|
||||
"0c" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "simulate_info",
|
||||
event_kind: "raydium_amm_v4.simulate_info",
|
||||
discriminator_hex: "0c",
|
||||
});
|
||||
},
|
||||
"0d" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "admin_cancel_orders",
|
||||
event_kind: "raydium_amm_v4.admin_cancel_orders",
|
||||
discriminator_hex: "0d",
|
||||
});
|
||||
},
|
||||
"0e" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "create_config_account",
|
||||
event_kind: "raydium_amm_v4.create_config_account",
|
||||
discriminator_hex: "0e",
|
||||
});
|
||||
},
|
||||
"0f" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "update_config_account",
|
||||
event_kind: "raydium_amm_v4.update_config_account",
|
||||
discriminator_hex: "0f",
|
||||
});
|
||||
},
|
||||
"10" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "swap_base_in_v2",
|
||||
event_kind: "raydium_amm_v4.swap_base_in_v2",
|
||||
discriminator_hex: "10",
|
||||
});
|
||||
},
|
||||
"11" => {
|
||||
return Some(RaydiumAmmV4InstructionIdentity {
|
||||
instruction_name: "swap_base_out_v2",
|
||||
event_kind: "raydium_amm_v4.swap_base_out_v2",
|
||||
discriminator_hex: "11",
|
||||
});
|
||||
},
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn raydium_amm_v4_instruction_discriminator_hex(
|
||||
data_base58: std::option::Option<&str>,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let data_base58 = match data_base58 {
|
||||
Some(data_base58) => data_base58,
|
||||
None => return None,
|
||||
};
|
||||
let bytes_result = bs58::decode(data_base58).into_vec();
|
||||
let bytes = match bytes_result {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let first_byte = match bytes.first() {
|
||||
Some(first_byte) => first_byte,
|
||||
None => return None,
|
||||
};
|
||||
return Some(format!("{first_byte:02x}"));
|
||||
}
|
||||
|
||||
fn parse_transaction_meta_value(
|
||||
transaction: &crate::ChainTransactionDto,
|
||||
transaction_json: &serde_json::Value,
|
||||
@@ -1209,6 +1903,14 @@ mod tests {
|
||||
}
|
||||
|
||||
fn make_swap_instruction() -> crate::ChainInstructionDto {
|
||||
return make_swap_instruction_with_data("63SfuT4qF7xK35jRTGqxuUT");
|
||||
}
|
||||
|
||||
fn make_swap_v2_instruction() -> crate::ChainInstructionDto {
|
||||
return make_swap_instruction_with_data("9rj8cBJgMm4L1xvzfy5AUsy");
|
||||
}
|
||||
|
||||
fn make_swap_instruction_with_data(data_base58: &str) -> crate::ChainInstructionDto {
|
||||
let mut dto = crate::ChainInstructionDto::new(
|
||||
100,
|
||||
Some(55),
|
||||
@@ -1228,7 +1930,7 @@ mod tests {
|
||||
"UserQuote111"
|
||||
])
|
||||
.to_string(),
|
||||
Some(serde_json::json!("9rj8cBJgMm4L1xvzfy5AUsy").to_string()),
|
||||
Some(serde_json::json!(data_base58).to_string()),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@@ -1305,6 +2007,9 @@ mod tests {
|
||||
assert_eq!(event.transaction_id, 100);
|
||||
assert_eq!(event.instruction_id, 200);
|
||||
assert_eq!(event.pool_account, "Pool111".to_string());
|
||||
assert_eq!(event.event_kind, "raydium_amm_v4.swap_base_in".to_string());
|
||||
assert_eq!(event.instruction_name, "swap_base_in".to_string());
|
||||
assert_eq!(event.discriminator_hex, "09".to_string());
|
||||
assert_eq!(event.token_a_mint, "BaseMint111".to_string());
|
||||
assert_eq!(event.token_b_mint, crate::WSOL_MINT_ID.to_string());
|
||||
assert_eq!(event.base_vault, "BaseVault111".to_string());
|
||||
@@ -1320,4 +2025,29 @@ mod tests {
|
||||
_ => panic!("expected swap event"),
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn raydium_amm_v4_swap_base_in_v2_is_decoded_from_inner_instruction_and_vault_deltas() {
|
||||
let decoder = crate::RaydiumAmmV4Decoder::new();
|
||||
let transaction = make_swap_transaction();
|
||||
let instructions = vec![make_swap_v2_instruction()];
|
||||
let decoded_result = decoder.decode_transaction(&transaction, &instructions);
|
||||
let decoded = match decoded_result {
|
||||
Ok(decoded) => decoded,
|
||||
Err(error) => panic!("decode must succeed: {}", error),
|
||||
};
|
||||
assert_eq!(decoded.len(), 1);
|
||||
match &decoded[0] {
|
||||
crate::RaydiumAmmV4DecodedEvent::Swap(event) => {
|
||||
assert_eq!(event.event_kind, "raydium_amm_v4.swap_base_in_v2".to_string());
|
||||
assert_eq!(event.instruction_name, "swap_base_in_v2".to_string());
|
||||
assert_eq!(event.discriminator_hex, "10".to_string());
|
||||
assert_eq!(event.pool_account, "Pool111".to_string());
|
||||
assert_eq!(event.base_amount_raw, Some("100".to_string()));
|
||||
assert_eq!(event.quote_amount_raw, Some("100".to_string()));
|
||||
assert_eq!(event.trade_side, Some("BuyBase".to_string()));
|
||||
},
|
||||
_ => panic!("expected swap event"),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user