1319 lines
48 KiB
Rust
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);
|
|
}
|
|
}
|