Files
khadhroony-bobobot/kb_lib/src/dex/meteora_dlmm.rs
2026-05-12 15:04:04 +02:00

1319 lines
48 KiB
Rust

// file: kb_lib/src/dex/meteora_dlmm.rs
//! Meteora DLMM transaction decoder.
//!
//! This first decoder version is intentionally conservative. It only emits
//! decoded events when the projected instruction or transaction logs expose
//! clear DLMM create/swap hints.
const DLMM_DISCRIMINATOR_CLAIM_FEE2: [u8; 8] = [0x70, 0xbf, 0x65, 0xab, 0x1c, 0x90, 0x7f, 0xbb];
const DLMM_DISCRIMINATOR_INITIALIZE_POSITION: [u8; 8] =
[0xdb, 0xc0, 0xea, 0x47, 0xbe, 0xbf, 0x66, 0x50];
const DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR: [u8; 8] =
[0x2d, 0x9a, 0xed, 0xd2, 0xdd, 0x0f, 0xa6, 0x5c];
const DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR2: [u8; 8] =
[0x49, 0x3b, 0x24, 0x78, 0xed, 0x53, 0x6c, 0xc6];
const DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR: [u8; 8] =
[0x2e, 0x27, 0x29, 0x87, 0x6f, 0xb7, 0xc8, 0x40];
const DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR2: [u8; 8] =
[0xf3, 0x49, 0x81, 0x7e, 0x33, 0x13, 0xf1, 0x6b];
const DLMM_DISCRIMINATOR_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8];
const DLMM_DISCRIMINATOR_SWAP2: [u8; 8] = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88];
const DLMM_DISCRIMINATOR_SWAP_EXACT_OUT: [u8; 8] = [0xfa, 0x49, 0x65, 0x21, 0x26, 0xcf, 0x4b, 0xb8];
const DLMM_DISCRIMINATOR_SWAP_EXACT_OUT2: [u8; 8] =
[0x2b, 0xd7, 0xf7, 0x84, 0x89, 0x3c, 0xf3, 0x51];
const DLMM_DISCRIMINATOR_SWAP_WITH_PRICE_IMPACT: [u8; 8] =
[0x38, 0xad, 0xe6, 0xd0, 0xad, 0xe4, 0x9c, 0xcd];
/// Decoded Meteora DLMM create-pool event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDlmmCreatePoolDecoded {
/// 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,
/// Optional DLMM pair/pool account.
pub pool_account: std::option::Option<std::string::String>,
/// Optional token X/base mint.
pub token_a_mint: std::option::Option<std::string::String>,
/// Optional token Y/quote mint.
pub token_b_mint: std::option::Option<std::string::String>,
/// Optional preset/config account.
pub config_account: std::option::Option<std::string::String>,
/// Decoded payload.
pub payload_json: serde_json::Value,
}
/// Decoded Meteora DLMM swap event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDlmmSwapDecoded {
/// 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,
/// Trade side relative to normalized base, when inferable.
pub trade_side: crate::SwapTradeSide,
/// Optional DLMM pair/pool account.
pub pool_account: std::option::Option<std::string::String>,
/// Optional token X/base mint.
pub token_a_mint: std::option::Option<std::string::String>,
/// Optional token Y/quote mint.
pub token_b_mint: std::option::Option<std::string::String>,
/// Optional reserve X token account.
pub reserve_x_account: std::option::Option<std::string::String>,
/// Optional reserve Y token account.
pub reserve_y_account: std::option::Option<std::string::String>,
/// Optional user token-in account.
pub user_token_in_account: std::option::Option<std::string::String>,
/// Optional user token-out account.
pub user_token_out_account: std::option::Option<std::string::String>,
/// Decoded payload.
pub payload_json: serde_json::Value,
}
/// Decoded Meteora DLMM event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum MeteoraDlmmDecodedEvent {
/// DLMM pair/pool creation.
CreatePool(MeteoraDlmmCreatePoolDecoded),
/// DLMM swap.
Swap(MeteoraDlmmSwapDecoded),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MeteoraDlmmInstructionKind {
CreatePool,
Swap,
Ignore,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MeteoraDlmmInstructionName {
InitializeLbPair,
InitializeLbPair2,
InitializeCustomizablePermissionlessLbPair,
InitializeCustomizablePermissionlessLbPair2,
Swap,
Swap2,
SwapExactOut,
SwapExactOut2,
SwapWithPriceImpact,
ClaimFee2,
InitializePosition,
Unknown,
}
impl MeteoraDlmmInstructionName {
fn as_str(&self) -> &'static str {
match self {
Self::InitializeLbPair => return "initialize_lb_pair",
Self::InitializeLbPair2 => return "initialize_lb_pair2",
Self::InitializeCustomizablePermissionlessLbPair => {
return "initialize_customizable_permissionless_lb_pair";
},
Self::InitializeCustomizablePermissionlessLbPair2 => {
return "initialize_customizable_permissionless_lb_pair2";
},
Self::Swap => return "swap",
Self::Swap2 => return "swap2",
Self::SwapExactOut => return "swap_exact_out",
Self::SwapExactOut2 => return "swap_exact_out2",
Self::SwapWithPriceImpact => return "swap_with_price_impact",
Self::ClaimFee2 => return "claim_fee2",
Self::InitializePosition => return "initialize_position",
Self::Unknown => return "unknown",
}
}
fn kind(&self) -> MeteoraDlmmInstructionKind {
match self {
Self::InitializeLbPair
| Self::InitializeLbPair2
| Self::InitializeCustomizablePermissionlessLbPair
| Self::InitializeCustomizablePermissionlessLbPair2 => {
return MeteoraDlmmInstructionKind::CreatePool;
},
Self::Swap
| Self::Swap2
| Self::SwapExactOut
| Self::SwapExactOut2
| Self::SwapWithPriceImpact => return MeteoraDlmmInstructionKind::Swap,
Self::Unknown => return MeteoraDlmmInstructionKind::Unknown,
Self::ClaimFee2 | Self::InitializePosition => {
return MeteoraDlmmInstructionKind::Ignore;
},
}
}
}
/// Meteora DLMM decoder.
#[derive(Debug, Clone, Default)]
pub struct MeteoraDlmmDecoder;
impl MeteoraDlmmDecoder {
/// Creates a new decoder.
pub fn new() -> Self {
return Self;
}
/// Decodes one projected transaction into zero or more Meteora DLMM events.
pub fn decode_transaction(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::MeteoraDlmmDecodedEvent>, 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_result =
serde_json::from_str::<serde_json::Value>(transaction.transaction_json.as_str());
let transaction_json = match transaction_json_result {
Ok(transaction_json) => transaction_json,
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot parse transaction_json for signature '{}': {}",
transaction.signature, error
)));
},
};
let log_messages = extract_log_messages(&transaction_json);
let mut decoded_events = std::vec::Vec::new();
for instruction in instructions {
let program_id = match instruction.program_id.as_deref() {
Some(program_id) => program_id,
None => continue,
};
if program_id != crate::METEORA_DLMM_PROGRAM_ID {
continue;
}
let instruction_id = match instruction.id {
Some(instruction_id) => instruction_id,
None => continue,
};
let accounts_result = parse_accounts_json(instruction.accounts_json.as_str());
let accounts = match accounts_result {
Ok(accounts) => accounts,
Err(error) => return Err(error),
};
let parsed_json_result = parse_optional_parsed_json(instruction.parsed_json.as_ref());
let parsed_json = match parsed_json_result {
Ok(parsed_json) => parsed_json,
Err(error) => return Err(error),
};
let instruction_data_result =
decode_instruction_data_json(instruction.data_json.as_ref());
let instruction_data = match instruction_data_result {
Ok(instruction_data) => instruction_data,
Err(error) => return Err(error),
};
let instruction_name = classify_instruction_name(
parsed_json.as_ref(),
instruction.parsed_type.as_deref(),
instruction_data.as_deref(),
&log_messages,
);
let instruction_kind = instruction_name.kind();
if instruction_kind == MeteoraDlmmInstructionKind::Unknown
|| instruction_kind == MeteoraDlmmInstructionKind::Ignore
{
continue;
}
let pool_account =
resolve_dlmm_pool_account(instruction_name, parsed_json.as_ref(), &accounts);
let token_a_mint =
resolve_dlmm_token_x_mint(instruction_name, parsed_json.as_ref(), &accounts);
let token_b_mint =
resolve_dlmm_token_y_mint(instruction_name, parsed_json.as_ref(), &accounts);
if pool_account.is_none() || token_a_mint.is_none() || token_b_mint.is_none() {
continue;
}
let config_account =
resolve_dlmm_config_account(instruction_name, parsed_json.as_ref(), &accounts);
if instruction_kind == MeteoraDlmmInstructionKind::CreatePool {
let payload_json = serde_json::json!({
"decoder": "meteora_dlmm",
"eventKind": "create_pool",
"decodedInstructionName": instruction_name.as_str(),
"dataDiscriminatorHex": instruction_data
.as_ref()
.and_then(|data| return first_8_bytes_hex(data.as_slice())),
"classifiedInstructionKind": "create_pool",
"signature": transaction.signature,
"instructionId": instruction_id,
"parentInstructionId": instruction.parent_instruction_id,
"instructionIndex": instruction.instruction_index,
"innerInstructionIndex": instruction.inner_instruction_index,
"stackHeight": instruction.stack_height,
"accounts": accounts,
"parsed": parsed_json,
"logMessages": log_messages,
"poolAccount": pool_account,
"tokenAMint": token_a_mint,
"tokenBMint": token_b_mint,
"configAccount": config_account
});
decoded_events.push(crate::MeteoraDlmmDecodedEvent::CreatePool(
crate::MeteoraDlmmCreatePoolDecoded {
transaction_id,
instruction_id,
signature: transaction.signature.clone(),
program_id: program_id.to_string(),
pool_account,
token_a_mint,
token_b_mint,
config_account,
payload_json,
},
));
continue;
}
if instruction_kind == MeteoraDlmmInstructionKind::Swap {
let reserve_x_account = resolve_dlmm_reserve_x_account(
instruction_name,
parsed_json.as_ref(),
&accounts,
);
let reserve_y_account = resolve_dlmm_reserve_y_account(
instruction_name,
parsed_json.as_ref(),
&accounts,
);
let user_token_in_account = resolve_dlmm_user_token_in_account(
instruction_name,
parsed_json.as_ref(),
&accounts,
);
let user_token_out_account = resolve_dlmm_user_token_out_account(
instruction_name,
parsed_json.as_ref(),
&accounts,
);
let trade_side = infer_trade_side(parsed_json.as_ref());
let payload_json = serde_json::json!({
"decoder": "meteora_dlmm",
"eventKind": "swap",
"decodedInstructionName": instruction_name.as_str(),
"dataDiscriminatorHex": instruction_data
.as_ref()
.and_then(|data| return first_8_bytes_hex(data.as_slice())),
"classifiedInstructionKind": "swap",
"signature": transaction.signature,
"instructionId": instruction_id,
"parentInstructionId": instruction.parent_instruction_id,
"instructionIndex": instruction.instruction_index,
"innerInstructionIndex": instruction.inner_instruction_index,
"stackHeight": instruction.stack_height,
"accounts": accounts,
"parsed": parsed_json,
"logMessages": log_messages,
"poolAccount": pool_account,
"tokenAMint": token_a_mint,
"tokenBMint": token_b_mint,
"reserveXAccount": reserve_x_account,
"reserveYAccount": reserve_y_account,
"userTokenInAccount": user_token_in_account,
"userTokenOutAccount": user_token_out_account,
"tradeSide": format!("{:?}", trade_side)
});
decoded_events.push(crate::MeteoraDlmmDecodedEvent::Swap(
crate::MeteoraDlmmSwapDecoded {
transaction_id,
instruction_id,
signature: transaction.signature.clone(),
program_id: program_id.to_string(),
trade_side,
pool_account,
token_a_mint,
token_b_mint,
reserve_x_account,
reserve_y_account,
user_token_in_account,
user_token_out_account,
payload_json,
},
));
}
}
return Ok(decoded_events);
}
}
fn classify_instruction_name(
parsed_json: std::option::Option<&serde_json::Value>,
parsed_type: std::option::Option<&str>,
instruction_data: std::option::Option<&[u8]>,
log_messages: &[std::string::String],
) -> MeteoraDlmmInstructionName {
let from_data = classify_instruction_name_from_data(instruction_data);
if from_data != MeteoraDlmmInstructionName::Unknown {
return from_data;
}
if instruction_data.is_some() {
return MeteoraDlmmInstructionName::Unknown;
}
if contains_create_pool_hint(parsed_type) {
return MeteoraDlmmInstructionName::InitializeLbPair;
}
if contains_swap_hint(parsed_type) {
return MeteoraDlmmInstructionName::Swap;
}
if parsed_type.is_some() {
return MeteoraDlmmInstructionName::Unknown;
}
if let Some(parsed_json) = parsed_json {
if contains_create_pool_hint_in_value(parsed_json) {
return MeteoraDlmmInstructionName::InitializeLbPair;
}
if contains_swap_hint_in_value(parsed_json) {
return MeteoraDlmmInstructionName::Swap;
}
return MeteoraDlmmInstructionName::Unknown;
}
for log_message in log_messages {
if contains_create_pool_hint(Some(log_message.as_str())) {
return MeteoraDlmmInstructionName::InitializeLbPair;
}
if contains_swap_hint(Some(log_message.as_str())) {
return MeteoraDlmmInstructionName::Swap;
}
}
return MeteoraDlmmInstructionName::Unknown;
}
fn classify_instruction_name_from_data(
instruction_data: std::option::Option<&[u8]>,
) -> MeteoraDlmmInstructionName {
let instruction_data = match instruction_data {
Some(instruction_data) => instruction_data,
None => return MeteoraDlmmInstructionName::Unknown,
};
if instruction_data.len() < 8 {
return MeteoraDlmmInstructionName::Unknown;
}
let discriminator = [
instruction_data[0],
instruction_data[1],
instruction_data[2],
instruction_data[3],
instruction_data[4],
instruction_data[5],
instruction_data[6],
instruction_data[7],
];
if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR {
return MeteoraDlmmInstructionName::InitializeLbPair;
}
if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR2 {
return MeteoraDlmmInstructionName::InitializeLbPair2;
}
if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR {
return MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair;
}
if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR2 {
return MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2;
}
if discriminator == DLMM_DISCRIMINATOR_SWAP {
return MeteoraDlmmInstructionName::Swap;
}
if discriminator == DLMM_DISCRIMINATOR_SWAP2 {
return MeteoraDlmmInstructionName::Swap2;
}
if discriminator == DLMM_DISCRIMINATOR_SWAP_EXACT_OUT {
return MeteoraDlmmInstructionName::SwapExactOut;
}
if discriminator == DLMM_DISCRIMINATOR_SWAP_EXACT_OUT2 {
return MeteoraDlmmInstructionName::SwapExactOut2;
}
if discriminator == DLMM_DISCRIMINATOR_SWAP_WITH_PRICE_IMPACT {
return MeteoraDlmmInstructionName::SwapWithPriceImpact;
}
if discriminator == DLMM_DISCRIMINATOR_CLAIM_FEE2 {
return MeteoraDlmmInstructionName::ClaimFee2;
}
if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_POSITION {
return MeteoraDlmmInstructionName::InitializePosition;
}
return MeteoraDlmmInstructionName::Unknown;
}
fn resolve_dlmm_pool_account(
instruction_name: MeteoraDlmmInstructionName,
parsed_json: std::option::Option<&serde_json::Value>,
accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
let parsed_value = extract_string_by_candidate_keys(
parsed_json,
&[
"lbPair",
"lb_pair",
"lbPairAccount",
"pair",
"pairAccount",
"pool",
"poolAccount",
],
);
if parsed_value.is_some() {
return parsed_value;
}
match instruction_name {
MeteoraDlmmInstructionName::InitializeLbPair
| MeteoraDlmmInstructionName::InitializeLbPair2
| MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair
| MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2
| MeteoraDlmmInstructionName::Swap
| MeteoraDlmmInstructionName::Swap2
| MeteoraDlmmInstructionName::SwapExactOut
| MeteoraDlmmInstructionName::SwapExactOut2
| MeteoraDlmmInstructionName::SwapWithPriceImpact => {
return extract_account(accounts, 0);
},
MeteoraDlmmInstructionName::ClaimFee2
| MeteoraDlmmInstructionName::InitializePosition
| MeteoraDlmmInstructionName::Unknown => return None,
}
}
fn resolve_dlmm_token_x_mint(
instruction_name: MeteoraDlmmInstructionName,
parsed_json: std::option::Option<&serde_json::Value>,
accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
let parsed_value = extract_string_by_candidate_keys(
parsed_json,
&[
"tokenXMint",
"token_x_mint",
"tokenMintX",
"token_mint_x",
"mintX",
"mint_x",
"tokenAMint",
"token_a_mint",
"baseMint",
],
);
if parsed_value.is_some() {
return parsed_value;
}
match instruction_name {
MeteoraDlmmInstructionName::InitializeLbPair
| MeteoraDlmmInstructionName::InitializeLbPair2
| MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair
| MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2 => {
return extract_account(accounts, 2);
},
MeteoraDlmmInstructionName::Swap
| MeteoraDlmmInstructionName::Swap2
| MeteoraDlmmInstructionName::SwapExactOut
| MeteoraDlmmInstructionName::SwapExactOut2
| MeteoraDlmmInstructionName::SwapWithPriceImpact => {
return extract_account(accounts, 6);
},
MeteoraDlmmInstructionName::ClaimFee2
| MeteoraDlmmInstructionName::InitializePosition
| MeteoraDlmmInstructionName::Unknown => return None,
}
}
fn resolve_dlmm_token_y_mint(
instruction_name: MeteoraDlmmInstructionName,
parsed_json: std::option::Option<&serde_json::Value>,
accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
let parsed_value = extract_string_by_candidate_keys(
parsed_json,
&[
"tokenYMint",
"token_y_mint",
"tokenMintY",
"token_mint_y",
"mintY",
"mint_y",
"tokenBMint",
"token_b_mint",
"quoteMint",
],
);
if parsed_value.is_some() {
return parsed_value;
}
match instruction_name {
MeteoraDlmmInstructionName::InitializeLbPair
| MeteoraDlmmInstructionName::InitializeLbPair2
| MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair
| MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2 => {
return extract_account(accounts, 3);
},
MeteoraDlmmInstructionName::Swap
| MeteoraDlmmInstructionName::Swap2
| MeteoraDlmmInstructionName::SwapExactOut
| MeteoraDlmmInstructionName::SwapExactOut2
| MeteoraDlmmInstructionName::SwapWithPriceImpact => {
return extract_account(accounts, 7);
},
MeteoraDlmmInstructionName::ClaimFee2
| MeteoraDlmmInstructionName::InitializePosition
| MeteoraDlmmInstructionName::Unknown => return None,
}
}
fn resolve_dlmm_reserve_x_account(
instruction_name: MeteoraDlmmInstructionName,
parsed_json: std::option::Option<&serde_json::Value>,
accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
let parsed_value = extract_string_by_candidate_keys(
parsed_json,
&["reserveX", "reserve_x", "reserveXAccount", "reserve_x_account"],
);
if parsed_value.is_some() {
return parsed_value;
}
match instruction_name {
MeteoraDlmmInstructionName::Swap
| MeteoraDlmmInstructionName::Swap2
| MeteoraDlmmInstructionName::SwapExactOut
| MeteoraDlmmInstructionName::SwapExactOut2
| MeteoraDlmmInstructionName::SwapWithPriceImpact => {
return extract_account(accounts, 2);
},
_ => return None,
}
}
fn resolve_dlmm_reserve_y_account(
instruction_name: MeteoraDlmmInstructionName,
parsed_json: std::option::Option<&serde_json::Value>,
accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
let parsed_value = extract_string_by_candidate_keys(
parsed_json,
&["reserveY", "reserve_y", "reserveYAccount", "reserve_y_account"],
);
if parsed_value.is_some() {
return parsed_value;
}
match instruction_name {
MeteoraDlmmInstructionName::Swap
| MeteoraDlmmInstructionName::Swap2
| MeteoraDlmmInstructionName::SwapExactOut
| MeteoraDlmmInstructionName::SwapExactOut2
| MeteoraDlmmInstructionName::SwapWithPriceImpact => {
return extract_account(accounts, 3);
},
_ => return None,
}
}
fn resolve_dlmm_user_token_in_account(
instruction_name: MeteoraDlmmInstructionName,
parsed_json: std::option::Option<&serde_json::Value>,
accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
let parsed_value = extract_string_by_candidate_keys(
parsed_json,
&["userTokenIn", "user_token_in", "userTokenInAccount", "user_token_in_account"],
);
if parsed_value.is_some() {
return parsed_value;
}
match instruction_name {
MeteoraDlmmInstructionName::Swap
| MeteoraDlmmInstructionName::Swap2
| MeteoraDlmmInstructionName::SwapExactOut
| MeteoraDlmmInstructionName::SwapExactOut2
| MeteoraDlmmInstructionName::SwapWithPriceImpact => {
return extract_account(accounts, 4);
},
_ => return None,
}
}
fn resolve_dlmm_user_token_out_account(
instruction_name: MeteoraDlmmInstructionName,
parsed_json: std::option::Option<&serde_json::Value>,
accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
let parsed_value = extract_string_by_candidate_keys(
parsed_json,
&[
"userTokenOut",
"user_token_out",
"userTokenOutAccount",
"user_token_out_account",
],
);
if parsed_value.is_some() {
return parsed_value;
}
match instruction_name {
MeteoraDlmmInstructionName::Swap
| MeteoraDlmmInstructionName::Swap2
| MeteoraDlmmInstructionName::SwapExactOut
| MeteoraDlmmInstructionName::SwapWithPriceImpact
| MeteoraDlmmInstructionName::SwapExactOut2 => {
return extract_account(accounts, 5);
},
_ => return None,
}
}
fn resolve_dlmm_config_account(
instruction_name: MeteoraDlmmInstructionName,
parsed_json: std::option::Option<&serde_json::Value>,
accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
let parsed_value = extract_string_by_candidate_keys(
parsed_json,
&[
"presetParameter",
"preset_parameter",
"presetParameter2",
"config",
"poolConfig",
],
);
if parsed_value.is_some() {
return parsed_value;
}
match instruction_name {
MeteoraDlmmInstructionName::InitializeLbPair
| MeteoraDlmmInstructionName::InitializeLbPair2
| MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair => {
return extract_account(accounts, 7);
},
MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2 => {
return None;
},
_ => return None,
}
}
fn contains_create_pool_hint(value: std::option::Option<&str>) -> bool {
let value = match value {
Some(value) => value.to_ascii_lowercase(),
None => return false,
};
if value.contains("initializelbpair") {
return true;
}
if value.contains("initialize_lb_pair") {
return true;
}
if value.contains("initializecustomizablepermissionlesslbpair") {
return true;
}
if value.contains("initialize_customizable_permissionless_lb_pair") {
return true;
}
if value.contains("initializepermissionlbpair") {
return true;
}
if value.contains("initialize_permission_lb_pair") {
return true;
}
if value.contains("create_lb_pair") {
return true;
}
return false;
}
fn contains_swap_hint(value: std::option::Option<&str>) -> bool {
let value = match value {
Some(value) => value.to_ascii_lowercase(),
None => return false,
};
if value.contains("swap") {
return true;
}
return false;
}
fn contains_create_pool_hint_in_value(value: &serde_json::Value) -> bool {
return contains_string_hint_in_value(value, contains_create_pool_hint);
}
fn contains_swap_hint_in_value(value: &serde_json::Value) -> bool {
return contains_string_hint_in_value(value, contains_swap_hint);
}
fn contains_string_hint_in_value(
value: &serde_json::Value,
predicate: fn(std::option::Option<&str>) -> bool,
) -> bool {
match value {
serde_json::Value::String(text) => return predicate(Some(text.as_str())),
serde_json::Value::Array(values) => {
for nested in values {
if contains_string_hint_in_value(nested, predicate) {
return true;
}
}
},
serde_json::Value::Object(object) => {
for nested in object.values() {
if contains_string_hint_in_value(nested, predicate) {
return true;
}
}
},
_ => {},
}
return false;
}
fn infer_trade_side(parsed_json: std::option::Option<&serde_json::Value>) -> crate::SwapTradeSide {
let side = extract_string_by_candidate_keys(
parsed_json,
&["tradeSide", "trade_side", "side", "swapSide", "swap_side"],
);
match side.as_deref() {
Some("BuyBase") => return crate::SwapTradeSide::BuyBase,
Some("buy") => return crate::SwapTradeSide::BuyBase,
Some("BUY") => return crate::SwapTradeSide::BuyBase,
Some("SellBase") => return crate::SwapTradeSide::SellBase,
Some("sell") => return crate::SwapTradeSide::SellBase,
Some("SELL") => return crate::SwapTradeSide::SellBase,
_ => return crate::SwapTradeSide::Unknown,
}
}
fn extract_log_messages(
transaction_json: &serde_json::Value,
) -> std::vec::Vec<std::string::String> {
let mut messages = std::vec::Vec::new();
let meta = match transaction_json.get("meta") {
Some(meta) => meta,
None => return messages,
};
let logs = match meta.get("logMessages").and_then(|value| return value.as_array()) {
Some(logs) => logs,
None => return messages,
};
for log in logs {
if let Some(text) = log.as_str() {
messages.push(text.to_string());
}
}
return messages;
}
fn parse_accounts_json(
accounts_json: &str,
) -> Result<std::vec::Vec<std::string::String>, crate::Error> {
let parsed_result = serde_json::from_str::<serde_json::Value>(accounts_json);
let parsed = match parsed_result {
Ok(parsed) => parsed,
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot parse Meteora DLMM accounts_json: {}",
error
)));
},
};
let array = match parsed.as_array() {
Some(array) => array,
None => return Ok(std::vec::Vec::new()),
};
let mut accounts = std::vec::Vec::new();
for item in array {
if let Some(text) = item.as_str() {
accounts.push(text.to_string());
continue;
}
if let Some(pubkey) = item.get("pubkey").and_then(|value| return value.as_str()) {
accounts.push(pubkey.to_string());
}
}
return Ok(accounts);
}
fn parse_optional_parsed_json(
parsed_json: std::option::Option<&std::string::String>,
) -> Result<std::option::Option<serde_json::Value>, crate::Error> {
let parsed_json = match parsed_json {
Some(parsed_json) => parsed_json,
None => return Ok(None),
};
let parsed_result = serde_json::from_str::<serde_json::Value>(parsed_json);
match parsed_result {
Ok(parsed) => return Ok(Some(parsed)),
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot parse Meteora DLMM parsed_json: {}",
error
)));
},
}
}
fn extract_account(
accounts: &[std::string::String],
index: usize,
) -> std::option::Option<std::string::String> {
match accounts.get(index) {
Some(account) => return Some(account.clone()),
None => return None,
}
}
fn extract_string_by_candidate_keys(
value: std::option::Option<&serde_json::Value>,
candidate_keys: &[&str],
) -> std::option::Option<std::string::String> {
let value = match value {
Some(value) => value,
None => return None,
};
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
if let Some(found) = object.get(*candidate_key) {
if let Some(text) = found.as_str() {
return Some(text.to_string());
}
if let Some(number) = found.as_u64() {
return Some(number.to_string());
}
if let Some(number) = found.as_i64() {
return Some(number.to_string());
}
}
}
for nested in object.values() {
let nested_result = extract_string_by_candidate_keys(Some(nested), candidate_keys);
if nested_result.is_some() {
return nested_result;
}
}
}
if let Some(array) = value.as_array() {
for nested in array {
let nested_result = extract_string_by_candidate_keys(Some(nested), candidate_keys);
if nested_result.is_some() {
return nested_result;
}
}
}
return None;
}
fn decode_instruction_data_json(
data_json: std::option::Option<&std::string::String>,
) -> Result<std::option::Option<std::vec::Vec<u8>>, crate::Error> {
let data_json = match data_json {
Some(data_json) => data_json,
None => return Ok(None),
};
let parsed_result = serde_json::from_str::<serde_json::Value>(data_json.as_str());
let parsed = match parsed_result {
Ok(parsed) => parsed,
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot parse Meteora DLMM data_json: {}",
error
)));
},
};
if let serde_json::Value::String(base58_text) = parsed {
let decoded_result = decode_base58(base58_text.as_str());
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => return Err(error),
};
return Ok(Some(decoded));
}
return Ok(None);
}
fn decode_base58(input: &str) -> Result<std::vec::Vec<u8>, crate::Error> {
let decoded_result = bs58::decode(input).into_vec();
match decoded_result {
Ok(decoded) => return Ok(decoded),
Err(error) => {
return Err(crate::Error::Json(format!(
"cannot decode Meteora DLMM instruction data from base58: {}",
error
)));
},
}
}
fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
if bytes.len() < 8 {
return None;
}
return Some(format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
));
}
#[cfg(test)]
mod tests {
fn make_create_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-dlmm-create-1".to_string(),
Some(888101),
Some(1779400001),
Some("helius_primary_http".to_string()),
Some("0".to_string()),
None,
None,
serde_json::json!({
"slot": 888101,
"meta": {
"logMessages": [
"Program log: Instruction: InitializeLbPair"
]
},
"transaction": {
"message": {
"instructions": []
}
}
})
.to_string(),
);
dto.id = Some(401);
return dto;
}
fn make_create_instruction() -> crate::ChainInstructionDto {
let mut dto = crate::ChainInstructionDto::new(
401,
None,
0,
None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()),
Some(1),
serde_json::json!([
"DlmmPair111",
"DlmmTokenX111",
crate::WSOL_MINT_ID,
"DlmmPreset111"
])
.to_string(),
None,
Some("initialize_lb_pair".to_string()),
Some(
serde_json::json!({
"info": {
"lbPair": "DlmmPair111",
"tokenXMint": "DlmmTokenX111",
"tokenYMint": crate::WSOL_MINT_ID,
"presetParameter": "DlmmPreset111"
}
})
.to_string(),
),
);
dto.id = Some(402);
return dto;
}
fn make_swap_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-dlmm-swap-1".to_string(),
Some(888102),
Some(1779400002),
Some("helius_primary_http".to_string()),
Some("0".to_string()),
None,
None,
serde_json::json!({
"slot": 888102,
"meta": {
"logMessages": [
"Program log: Instruction: Swap"
]
},
"transaction": {
"message": {
"instructions": []
}
}
})
.to_string(),
);
dto.id = Some(403);
return dto;
}
fn make_swap_instruction() -> crate::ChainInstructionDto {
let mut dto = crate::ChainInstructionDto::new(
403,
None,
0,
None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()),
Some(1),
serde_json::json!(["DlmmPairSwap111", "DlmmSwapTokenX111", crate::WSOL_MINT_ID])
.to_string(),
None,
Some("swap".to_string()),
Some(
serde_json::json!({
"info": {
"lbPair": "DlmmPairSwap111",
"tokenXMint": "DlmmSwapTokenX111",
"tokenYMint": crate::WSOL_MINT_ID,
"tradeSide": "buy"
}
})
.to_string(),
),
);
dto.id = Some(404);
return dto;
}
#[test]
fn meteora_dlmm_create_pool_is_detected() {
let decoder = crate::MeteoraDlmmDecoder::new();
let transaction = make_create_transaction();
let instructions = vec![make_create_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::MeteoraDlmmDecodedEvent::CreatePool(event) => {
assert_eq!(event.transaction_id, 401);
assert_eq!(event.instruction_id, 402);
assert_eq!(event.pool_account, Some("DlmmPair111".to_string()));
assert_eq!(event.token_a_mint, Some("DlmmTokenX111".to_string()));
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
assert_eq!(event.config_account, Some("DlmmPreset111".to_string()));
},
crate::MeteoraDlmmDecodedEvent::Swap(_) => {
panic!("unexpected swap event");
},
}
}
#[test]
fn meteora_dlmm_swap_is_detected() {
let decoder = crate::MeteoraDlmmDecoder::new();
let transaction = make_swap_transaction();
let instructions = vec![make_swap_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::MeteoraDlmmDecodedEvent::Swap(event) => {
assert_eq!(event.transaction_id, 403);
assert_eq!(event.instruction_id, 404);
assert_eq!(event.pool_account, Some("DlmmPairSwap111".to_string()));
assert_eq!(event.token_a_mint, Some("DlmmSwapTokenX111".to_string()));
assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string()));
assert_eq!(event.trade_side, crate::SwapTradeSide::BuyBase);
},
crate::MeteoraDlmmDecodedEvent::CreatePool(_) => {
panic!("unexpected create event");
},
}
}
#[test]
fn meteora_dlmm_ignores_unclear_instruction() {
let decoder = crate::MeteoraDlmmDecoder::new();
let transaction = make_swap_transaction();
let mut instruction = make_swap_instruction();
instruction.parsed_type = Some("initialize_bin_array".to_string());
instruction.parsed_json = None;
let decoded_result = decoder.decode_transaction(&transaction, &[instruction]);
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 0);
}
#[test]
fn meteora_dlmm_swap2_discriminator_is_detected() {
let instruction_data = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01, 0x02, 0x03];
let name = super::classify_instruction_name_from_data(Some(&instruction_data));
assert_eq!(name, super::MeteoraDlmmInstructionName::Swap2);
assert_eq!(name.kind(), super::MeteoraDlmmInstructionKind::Swap);
}
#[test]
fn meteora_dlmm_swap_accounts_are_mapped_from_carbon_layout() {
let accounts = vec![
"LbPair111".to_string(),
"Bitmap111".to_string(),
"ReserveX111".to_string(),
"ReserveY111".to_string(),
"UserTokenIn111".to_string(),
"UserTokenOut111".to_string(),
"TokenXMint111".to_string(),
"TokenYMint111".to_string(),
];
let pool = super::resolve_dlmm_pool_account(
super::MeteoraDlmmInstructionName::Swap2,
None,
&accounts,
);
let token_x = super::resolve_dlmm_token_x_mint(
super::MeteoraDlmmInstructionName::Swap2,
None,
&accounts,
);
let token_y = super::resolve_dlmm_token_y_mint(
super::MeteoraDlmmInstructionName::Swap2,
None,
&accounts,
);
assert_eq!(pool, Some("LbPair111".to_string()));
assert_eq!(token_x, Some("TokenXMint111".to_string()));
assert_eq!(token_y, Some("TokenYMint111".to_string()));
}
#[test]
fn meteora_dlmm_real_swap2_base58_discriminator_is_decoded() {
let data_json = "\"fx9RHbGFfZ8bH9v4Jv5SQRfuGUq9kGrWfmiZ99\"".to_string();
let decoded_result = super::decode_instruction_data_json(Some(&data_json));
let decoded = match decoded_result {
Ok(Some(decoded)) => decoded,
Ok(None) => panic!("expected decoded data"),
Err(error) => panic!("decode must succeed: {}", error),
};
let name = super::classify_instruction_name_from_data(Some(decoded.as_slice()));
assert_eq!(name, super::MeteoraDlmmInstructionName::Swap2);
}
#[test]
fn meteora_dlmm_inner_swap2_instruction_is_not_skipped() {
let decoder = crate::MeteoraDlmmDecoder::new();
let mut transaction = crate::ChainTransactionDto::new(
"sig-meteora-dlmm-inner-swap2".to_string(),
Some(888103),
Some(1779400003),
Some("helius_primary_http".to_string()),
Some("0".to_string()),
None,
None,
serde_json::json!({
"slot": 888103,
"meta": {
"logMessages": []
},
"transaction": {
"message": {
"instructions": []
}
}
})
.to_string(),
);
transaction.id = Some(405);
let mut instruction = crate::ChainInstructionDto::new(
405,
Some(404),
3,
Some(14),
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()),
Some(2),
serde_json::json!([
"LbPair111",
"Bitmap111",
"ReserveX111",
"ReserveY111",
"UserTokenIn111",
"UserTokenOut111",
"TokenXMint111",
"TokenYMint111",
"Oracle111",
"HostFee111",
"User111",
crate::SPL_TOKEN_PROGRAM_ID,
crate::SPL_TOKEN_PROGRAM_ID,
"EventAuthority111".to_string(),
crate::METEORA_DLMM_PROGRAM_ID,
"BinArray111",
"BinArray222"
])
.to_string(),
Some("\"fx9RHbGFfZ8bH9v4Jv5SQRfuGUq9kGrWfmiZ99\"".to_string()),
None,
None,
);
instruction.id = Some(406);
let decoded_result = decoder.decode_transaction(&transaction, &[instruction]);
let decoded = match decoded_result {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 1);
match &decoded[0] {
crate::MeteoraDlmmDecodedEvent::Swap(event) => {
assert_eq!(event.transaction_id, 405);
assert_eq!(event.instruction_id, 406);
assert_eq!(event.pool_account, Some("LbPair111".to_string()));
assert_eq!(event.token_a_mint, Some("TokenXMint111".to_string()));
assert_eq!(event.token_b_mint, Some("TokenYMint111".to_string()));
assert_eq!(event.reserve_x_account, Some("ReserveX111".to_string()));
assert_eq!(event.reserve_y_account, Some("ReserveY111".to_string()));
assert_eq!(event.user_token_in_account, Some("UserTokenIn111".to_string()));
assert_eq!(event.user_token_out_account, Some("UserTokenOut111".to_string()));
},
crate::MeteoraDlmmDecodedEvent::CreatePool(_) => {
panic!("unexpected create event");
},
}
}
#[test]
fn meteora_dlmm_initialize_position_discriminator_is_ignored() {
let instruction_data = [0xdb, 0xc0, 0xea, 0x47, 0xbe, 0xbf, 0x66, 0x50, 0x01, 0x02, 0x03];
let log_messages = vec!["Program log: Instruction: Swap".to_string()];
let name =
super::classify_instruction_name(None, None, Some(&instruction_data), &log_messages);
assert_eq!(name, super::MeteoraDlmmInstructionName::InitializePosition);
assert_eq!(name.kind(), super::MeteoraDlmmInstructionKind::Ignore);
}
#[test]
fn meteora_dlmm_unknown_data_discriminator_does_not_fallback_to_global_swap_logs() {
let instruction_data = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x01, 0x02, 0x03];
let log_messages = vec!["Program log: Instruction: Swap".to_string()];
let name =
super::classify_instruction_name(None, None, Some(&instruction_data), &log_messages);
assert_eq!(name, super::MeteoraDlmmInstructionName::Unknown);
}
}