This commit is contained in:
2026-05-13 20:11:29 +02:00
parent 693a456e62
commit cfa1ff2289
36 changed files with 2035 additions and 103 deletions

View File

@@ -8,9 +8,17 @@
const DLMM_DISCRIMINATOR_CLAIM_FEE2: [u8; 8] = [0x70, 0xbf, 0x65, 0xab, 0x1c, 0x90, 0x7f, 0xbb];
const DLMM_DISCRIMINATOR_INITIALIZE_BIN_ARRAY: [u8; 8] =
[0x23, 0x56, 0x13, 0xb9, 0x4e, 0xd4, 0x4b, 0xd3];
const DLMM_DISCRIMINATOR_INITIALIZE_POSITION: [u8; 8] =
[0xdb, 0xc0, 0xea, 0x47, 0xbe, 0xbf, 0x66, 0x50];
const DLMM_DISCRIMINATOR_ADD_LIQUIDITY: [u8; 8] = [0xb5, 0x9d, 0x59, 0x43, 0x8f, 0xb6, 0x34, 0x48];
const DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY: [u8; 8] =
[0x50, 0x55, 0xd1, 0x48, 0x18, 0xce, 0xb1, 0x6c];
const DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR: [u8; 8] =
[0x2d, 0x9a, 0xed, 0xd2, 0xdd, 0x0f, 0xa6, 0x5c];
@@ -89,6 +97,60 @@ pub struct MeteoraDlmmSwapDecoded {
pub payload_json: serde_json::Value,
}
/// Decoded Meteora DLMM liquidity lifecycle event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDlmmLiquidityDecoded {
/// 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,
/// Normalized decoded event kind.
pub event_kind: 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 actor wallet or owner account.
pub actor_wallet: std::option::Option<std::string::String>,
/// Optional decoded base/token-X amount.
pub base_amount_raw: std::option::Option<std::string::String>,
/// Optional decoded quote/token-Y amount.
pub quote_amount_raw: std::option::Option<std::string::String>,
/// Optional decoded liquidity amount.
pub liquidity_amount_raw: std::option::Option<std::string::String>,
/// Decoded payload.
pub payload_json: serde_json::Value,
}
/// Decoded Meteora DLMM pool lifecycle event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MeteoraDlmmPoolLifecycleDecoded {
/// 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,
/// Normalized decoded event kind.
pub event_kind: 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>,
/// Decoded payload.
pub payload_json: serde_json::Value,
}
/// Decoded Meteora DLMM event.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum MeteoraDlmmDecodedEvent {
@@ -96,12 +158,20 @@ pub enum MeteoraDlmmDecodedEvent {
CreatePool(MeteoraDlmmCreatePoolDecoded),
/// DLMM swap.
Swap(MeteoraDlmmSwapDecoded),
/// DLMM liquidity lifecycle event.
Liquidity(MeteoraDlmmLiquidityDecoded),
/// DLMM pool lifecycle event that is not the canonical create-pool event.
PoolLifecycle(MeteoraDlmmPoolLifecycleDecoded),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MeteoraDlmmInstructionKind {
CreatePool,
Swap,
LiquidityAdd,
LiquidityRemove,
PositionOpen,
PoolLifecycle,
Ignore,
Unknown,
}
@@ -117,6 +187,9 @@ enum MeteoraDlmmInstructionName {
SwapExactOut,
SwapExactOut2,
SwapWithPriceImpact,
InitializeBinArray,
AddLiquidity,
RemoveLiquidity,
ClaimFee2,
InitializePosition,
Unknown,
@@ -138,6 +211,9 @@ impl MeteoraDlmmInstructionName {
Self::SwapExactOut => return "swap_exact_out",
Self::SwapExactOut2 => return "swap_exact_out2",
Self::SwapWithPriceImpact => return "swap_with_price_impact",
Self::InitializeBinArray => return "initialize_bin_array",
Self::AddLiquidity => return "add_liquidity",
Self::RemoveLiquidity => return "remove_liquidity",
Self::ClaimFee2 => return "claim_fee2",
Self::InitializePosition => return "initialize_position",
Self::Unknown => return "unknown",
@@ -157,8 +233,12 @@ impl MeteoraDlmmInstructionName {
| Self::SwapExactOut
| Self::SwapExactOut2
| Self::SwapWithPriceImpact => return MeteoraDlmmInstructionKind::Swap,
Self::AddLiquidity => return MeteoraDlmmInstructionKind::LiquidityAdd,
Self::RemoveLiquidity => return MeteoraDlmmInstructionKind::LiquidityRemove,
Self::InitializePosition => return MeteoraDlmmInstructionKind::PositionOpen,
Self::InitializeBinArray => return MeteoraDlmmInstructionKind::PoolLifecycle,
Self::Unknown => return MeteoraDlmmInstructionKind::Unknown,
Self::ClaimFee2 | Self::InitializePosition => {
Self::ClaimFee2 => {
return MeteoraDlmmInstructionKind::Ignore;
},
}
@@ -249,12 +329,15 @@ impl MeteoraDlmmDecoder {
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() {
if pool_account.is_none() {
continue;
}
let config_account =
resolve_dlmm_config_account(instruction_name, parsed_json.as_ref(), &accounts);
if instruction_kind == MeteoraDlmmInstructionKind::CreatePool {
if token_a_mint.is_none() || token_b_mint.is_none() {
continue;
}
let payload_json = serde_json::json!({
"decoder": "meteora_dlmm",
"eventKind": "create_pool",
@@ -293,6 +376,9 @@ impl MeteoraDlmmDecoder {
continue;
}
if instruction_kind == MeteoraDlmmInstructionKind::Swap {
if token_a_mint.is_none() || token_b_mint.is_none() {
continue;
}
let reserve_x_account = resolve_dlmm_reserve_x_account(
instruction_name,
parsed_json.as_ref(),
@@ -357,6 +443,127 @@ impl MeteoraDlmmDecoder {
payload_json,
},
));
continue;
}
if instruction_kind == MeteoraDlmmInstructionKind::LiquidityAdd
|| instruction_kind == MeteoraDlmmInstructionKind::LiquidityRemove
|| instruction_kind == MeteoraDlmmInstructionKind::PositionOpen
{
let event_kind = format!("meteora_dlmm.{}", instruction_name.as_str());
let actor_wallet =
resolve_dlmm_actor_wallet(instruction_name, parsed_json.as_ref(), &accounts);
let base_amount_raw = extract_amount_string_by_candidate_keys(
parsed_json.as_ref(),
&[
"baseAmountRaw",
"base_amount_raw",
"tokenXAmount",
"token_x_amount",
"amountX",
"amount_x",
],
);
let quote_amount_raw = extract_amount_string_by_candidate_keys(
parsed_json.as_ref(),
&[
"quoteAmountRaw",
"quote_amount_raw",
"tokenYAmount",
"token_y_amount",
"amountY",
"amount_y",
],
);
let liquidity_amount_raw = extract_amount_string_by_candidate_keys(
parsed_json.as_ref(),
&[
"liquidity",
"liquidityAmount",
"liquidity_amount",
"binLiquidity",
"bin_liquidity",
],
);
let payload_json = serde_json::json!({
"decoder": "meteora_dlmm",
"eventKind": instruction_name.as_str(),
"decodedInstructionName": instruction_name.as_str(),
"dataDiscriminatorHex": instruction_data
.as_ref()
.and_then(|data| return first_8_bytes_hex(data.as_slice())),
"classifiedInstructionKind": crate::classify_dex_event_lifecycle_kind_code(event_kind.as_str()),
"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,
"actorWallet": actor_wallet,
"baseAmountRaw": base_amount_raw,
"quoteAmountRaw": quote_amount_raw,
"liquidityAmountRaw": liquidity_amount_raw
});
decoded_events.push(crate::MeteoraDlmmDecodedEvent::Liquidity(
crate::MeteoraDlmmLiquidityDecoded {
transaction_id,
instruction_id,
signature: transaction.signature.clone(),
program_id: program_id.to_string(),
event_kind,
pool_account,
token_a_mint,
token_b_mint,
actor_wallet,
base_amount_raw,
quote_amount_raw,
liquidity_amount_raw,
payload_json,
},
));
continue;
}
if instruction_kind == MeteoraDlmmInstructionKind::PoolLifecycle {
let event_kind = format!("meteora_dlmm.{}", instruction_name.as_str());
let payload_json = serde_json::json!({
"decoder": "meteora_dlmm",
"eventKind": instruction_name.as_str(),
"decodedInstructionName": instruction_name.as_str(),
"dataDiscriminatorHex": instruction_data
.as_ref()
.and_then(|data| return first_8_bytes_hex(data.as_slice())),
"classifiedInstructionKind": crate::classify_dex_event_lifecycle_kind_code(event_kind.as_str()),
"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
});
decoded_events.push(crate::MeteoraDlmmDecodedEvent::PoolLifecycle(
crate::MeteoraDlmmPoolLifecycleDecoded {
transaction_id,
instruction_id,
signature: transaction.signature.clone(),
program_id: program_id.to_string(),
event_kind,
pool_account,
token_a_mint,
token_b_mint,
payload_json,
},
));
}
}
return Ok(decoded_events);
@@ -382,6 +589,18 @@ fn classify_instruction_name(
if contains_swap_hint(parsed_type) {
return MeteoraDlmmInstructionName::Swap;
}
if contains_add_liquidity_hint(parsed_type) {
return MeteoraDlmmInstructionName::AddLiquidity;
}
if contains_remove_liquidity_hint(parsed_type) {
return MeteoraDlmmInstructionName::RemoveLiquidity;
}
if contains_initialize_position_hint(parsed_type) {
return MeteoraDlmmInstructionName::InitializePosition;
}
if contains_initialize_bin_array_hint(parsed_type) {
return MeteoraDlmmInstructionName::InitializeBinArray;
}
if parsed_type.is_some() {
return MeteoraDlmmInstructionName::Unknown;
}
@@ -392,6 +611,18 @@ fn classify_instruction_name(
if contains_swap_hint_in_value(parsed_json) {
return MeteoraDlmmInstructionName::Swap;
}
if contains_add_liquidity_hint_in_value(parsed_json) {
return MeteoraDlmmInstructionName::AddLiquidity;
}
if contains_remove_liquidity_hint_in_value(parsed_json) {
return MeteoraDlmmInstructionName::RemoveLiquidity;
}
if contains_initialize_position_hint_in_value(parsed_json) {
return MeteoraDlmmInstructionName::InitializePosition;
}
if contains_initialize_bin_array_hint_in_value(parsed_json) {
return MeteoraDlmmInstructionName::InitializeBinArray;
}
return MeteoraDlmmInstructionName::Unknown;
}
for log_message in log_messages {
@@ -401,6 +632,18 @@ fn classify_instruction_name(
if contains_swap_hint(Some(log_message.as_str())) {
return MeteoraDlmmInstructionName::Swap;
}
if contains_add_liquidity_hint(Some(log_message.as_str())) {
return MeteoraDlmmInstructionName::AddLiquidity;
}
if contains_remove_liquidity_hint(Some(log_message.as_str())) {
return MeteoraDlmmInstructionName::RemoveLiquidity;
}
if contains_initialize_position_hint(Some(log_message.as_str())) {
return MeteoraDlmmInstructionName::InitializePosition;
}
if contains_initialize_bin_array_hint(Some(log_message.as_str())) {
return MeteoraDlmmInstructionName::InitializeBinArray;
}
}
return MeteoraDlmmInstructionName::Unknown;
}
@@ -452,6 +695,15 @@ fn classify_instruction_name_from_data(
if discriminator == DLMM_DISCRIMINATOR_SWAP_WITH_PRICE_IMPACT {
return MeteoraDlmmInstructionName::SwapWithPriceImpact;
}
if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_BIN_ARRAY {
return MeteoraDlmmInstructionName::InitializeBinArray;
}
if discriminator == DLMM_DISCRIMINATOR_ADD_LIQUIDITY {
return MeteoraDlmmInstructionName::AddLiquidity;
}
if discriminator == DLMM_DISCRIMINATOR_REMOVE_LIQUIDITY {
return MeteoraDlmmInstructionName::RemoveLiquidity;
}
if discriminator == DLMM_DISCRIMINATOR_CLAIM_FEE2 {
return MeteoraDlmmInstructionName::ClaimFee2;
}
@@ -490,12 +742,19 @@ fn resolve_dlmm_pool_account(
| MeteoraDlmmInstructionName::Swap2
| MeteoraDlmmInstructionName::SwapExactOut
| MeteoraDlmmInstructionName::SwapExactOut2
| MeteoraDlmmInstructionName::SwapWithPriceImpact => {
| MeteoraDlmmInstructionName::SwapWithPriceImpact
| MeteoraDlmmInstructionName::InitializeBinArray => {
return extract_account(accounts, 0);
},
MeteoraDlmmInstructionName::ClaimFee2
| MeteoraDlmmInstructionName::InitializePosition
| MeteoraDlmmInstructionName::Unknown => return None,
MeteoraDlmmInstructionName::AddLiquidity | MeteoraDlmmInstructionName::RemoveLiquidity => {
return extract_account(accounts, 1);
},
MeteoraDlmmInstructionName::InitializePosition => {
return extract_account(accounts, 2);
},
MeteoraDlmmInstructionName::ClaimFee2 | MeteoraDlmmInstructionName::Unknown => {
return None;
},
}
}
@@ -535,7 +794,11 @@ fn resolve_dlmm_token_x_mint(
| MeteoraDlmmInstructionName::SwapWithPriceImpact => {
return extract_account(accounts, 6);
},
MeteoraDlmmInstructionName::AddLiquidity | MeteoraDlmmInstructionName::RemoveLiquidity => {
return extract_account(accounts, 7);
},
MeteoraDlmmInstructionName::ClaimFee2
| MeteoraDlmmInstructionName::InitializeBinArray
| MeteoraDlmmInstructionName::InitializePosition
| MeteoraDlmmInstructionName::Unknown => return None,
}
@@ -577,7 +840,11 @@ fn resolve_dlmm_token_y_mint(
| MeteoraDlmmInstructionName::SwapWithPriceImpact => {
return extract_account(accounts, 7);
},
MeteoraDlmmInstructionName::AddLiquidity | MeteoraDlmmInstructionName::RemoveLiquidity => {
return extract_account(accounts, 8);
},
MeteoraDlmmInstructionName::ClaimFee2
| MeteoraDlmmInstructionName::InitializeBinArray
| MeteoraDlmmInstructionName::InitializePosition
| MeteoraDlmmInstructionName::Unknown => return None,
}
@@ -715,6 +982,86 @@ fn resolve_dlmm_config_account(
}
}
fn resolve_dlmm_actor_wallet(
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,
&["owner", "payer", "sender", "user", "authority", "liquidityProvider"],
);
if parsed_value.is_some() {
return parsed_value;
}
match instruction_name {
MeteoraDlmmInstructionName::AddLiquidity | MeteoraDlmmInstructionName::RemoveLiquidity => {
return extract_account(accounts, 9);
},
MeteoraDlmmInstructionName::InitializePosition => {
return extract_account(accounts, 3);
},
_ => return None,
}
}
fn extract_amount_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(text) = extract_string_by_candidate_keys(Some(value), candidate_keys) {
return Some(text);
}
return extract_number_by_candidate_keys(Some(value), candidate_keys);
}
fn extract_number_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,
};
match value {
serde_json::Value::Object(object) => {
for candidate_key in candidate_keys {
if let Some(candidate) = object.get(*candidate_key) {
if let Some(number) = candidate.as_i64() {
return Some(number.to_string());
}
if let Some(number) = candidate.as_u64() {
return Some(number.to_string());
}
if let Some(number) = candidate.as_f64() {
return Some(number.to_string());
}
}
}
for nested in object.values() {
let result = extract_number_by_candidate_keys(Some(nested), candidate_keys);
if result.is_some() {
return result;
}
}
},
serde_json::Value::Array(values) => {
for nested in values {
let result = extract_number_by_candidate_keys(Some(nested), candidate_keys);
if result.is_some() {
return result;
}
}
},
_ => {},
}
return None;
}
fn contains_create_pool_hint(value: std::option::Option<&str>) -> bool {
let value = match value {
Some(value) => value.to_ascii_lowercase(),
@@ -755,6 +1102,62 @@ fn contains_swap_hint(value: std::option::Option<&str>) -> bool {
return false;
}
fn contains_add_liquidity_hint(value: std::option::Option<&str>) -> bool {
let value = match value {
Some(value) => value.to_ascii_lowercase(),
None => return false,
};
if value.contains("addliquidity") {
return true;
}
if value.contains("add_liquidity") {
return true;
}
return false;
}
fn contains_remove_liquidity_hint(value: std::option::Option<&str>) -> bool {
let value = match value {
Some(value) => value.to_ascii_lowercase(),
None => return false,
};
if value.contains("removeliquidity") {
return true;
}
if value.contains("remove_liquidity") {
return true;
}
return false;
}
fn contains_initialize_position_hint(value: std::option::Option<&str>) -> bool {
let value = match value {
Some(value) => value.to_ascii_lowercase(),
None => return false,
};
if value.contains("initializeposition") {
return true;
}
if value.contains("initialize_position") {
return true;
}
return false;
}
fn contains_initialize_bin_array_hint(value: std::option::Option<&str>) -> bool {
let value = match value {
Some(value) => value.to_ascii_lowercase(),
None => return false,
};
if value.contains("initializebinarray") {
return true;
}
if value.contains("initialize_bin_array") {
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);
}
@@ -763,6 +1166,22 @@ fn contains_swap_hint_in_value(value: &serde_json::Value) -> bool {
return contains_string_hint_in_value(value, contains_swap_hint);
}
fn contains_add_liquidity_hint_in_value(value: &serde_json::Value) -> bool {
return contains_string_hint_in_value(value, contains_add_liquidity_hint);
}
fn contains_remove_liquidity_hint_in_value(value: &serde_json::Value) -> bool {
return contains_string_hint_in_value(value, contains_remove_liquidity_hint);
}
fn contains_initialize_position_hint_in_value(value: &serde_json::Value) -> bool {
return contains_string_hint_in_value(value, contains_initialize_position_hint);
}
fn contains_initialize_bin_array_hint_in_value(value: &serde_json::Value) -> bool {
return contains_string_hint_in_value(value, contains_initialize_bin_array_hint);
}
fn contains_string_hint_in_value(
value: &serde_json::Value,
predicate: fn(std::option::Option<&str>) -> bool,
@@ -1118,6 +1537,10 @@ mod tests {
crate::MeteoraDlmmDecodedEvent::Swap(_) => {
panic!("unexpected swap event");
},
crate::MeteoraDlmmDecodedEvent::Liquidity(_)
| crate::MeteoraDlmmDecodedEvent::PoolLifecycle(_) => {
panic!("unexpected non-trade event");
},
}
}
@@ -1144,11 +1567,15 @@ mod tests {
crate::MeteoraDlmmDecodedEvent::CreatePool(_) => {
panic!("unexpected create event");
},
crate::MeteoraDlmmDecodedEvent::Liquidity(_)
| crate::MeteoraDlmmDecodedEvent::PoolLifecycle(_) => {
panic!("unexpected non-trade event");
},
}
}
#[test]
fn meteora_dlmm_ignores_unclear_instruction() {
fn meteora_dlmm_initialize_bin_array_hint_is_decoded_as_pool_lifecycle() {
let decoder = crate::MeteoraDlmmDecoder::new();
let transaction = make_swap_transaction();
let mut instruction = make_swap_instruction();
@@ -1159,7 +1586,14 @@ mod tests {
Ok(decoded) => decoded,
Err(error) => panic!("decode must succeed: {}", error),
};
assert_eq!(decoded.len(), 0);
assert_eq!(decoded.len(), 1);
match &decoded[0] {
crate::MeteoraDlmmDecodedEvent::PoolLifecycle(event) => {
assert_eq!(event.event_kind, "meteora_dlmm.initialize_bin_array");
assert_eq!(event.pool_account, Some("DlmmPairSwap111".to_string()));
},
_ => panic!("expected pool lifecycle event"),
}
}
#[test]
@@ -1294,17 +1728,89 @@ mod tests {
crate::MeteoraDlmmDecodedEvent::CreatePool(_) => {
panic!("unexpected create event");
},
crate::MeteoraDlmmDecodedEvent::Liquidity(_)
| crate::MeteoraDlmmDecodedEvent::PoolLifecycle(_) => {
panic!("unexpected non-trade event");
},
}
}
#[test]
fn meteora_dlmm_initialize_position_discriminator_is_ignored() {
fn meteora_dlmm_initialize_position_discriminator_is_non_trade_position_open() {
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);
assert_eq!(name.kind(), super::MeteoraDlmmInstructionKind::PositionOpen);
}
#[test]
fn meteora_dlmm_add_remove_liquidity_discriminators_are_non_trade_liquidity() {
let add_data = [0xb5, 0x9d, 0x59, 0x43, 0x8f, 0xb6, 0x34, 0x48, 0x01];
let remove_data = [0x50, 0x55, 0xd1, 0x48, 0x18, 0xce, 0xb1, 0x6c, 0x01];
let add_name = super::classify_instruction_name_from_data(Some(&add_data));
let remove_name = super::classify_instruction_name_from_data(Some(&remove_data));
assert_eq!(add_name, super::MeteoraDlmmInstructionName::AddLiquidity);
assert_eq!(add_name.kind(), super::MeteoraDlmmInstructionKind::LiquidityAdd);
assert_eq!(remove_name, super::MeteoraDlmmInstructionName::RemoveLiquidity);
assert_eq!(remove_name.kind(), super::MeteoraDlmmInstructionKind::LiquidityRemove);
}
#[test]
fn meteora_dlmm_initialize_bin_array_is_pool_lifecycle() {
let data = [0x23, 0x56, 0x13, 0xb9, 0x4e, 0xd4, 0x4b, 0xd3, 0x01];
let name = super::classify_instruction_name_from_data(Some(&data));
assert_eq!(name, super::MeteoraDlmmInstructionName::InitializeBinArray);
assert_eq!(name.kind(), super::MeteoraDlmmInstructionKind::PoolLifecycle);
}
#[test]
fn meteora_dlmm_add_liquidity_is_decoded_as_non_trade_event() {
let decoder = crate::MeteoraDlmmDecoder::new();
let transaction = make_swap_transaction();
let mut instruction = 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!([
"Position111",
"DlmmPairSwap111",
"Bitmap111",
"UserTokenX111",
"UserTokenY111",
"ReserveX111",
"ReserveY111",
"DlmmSwapTokenX111",
crate::WSOL_MINT_ID,
"Owner111"
])
.to_string(),
Some("\"3K5citUwVB6uv\"".to_string()),
None,
None,
);
instruction.id = Some(405);
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::Liquidity(event) => {
assert_eq!(event.event_kind, "meteora_dlmm.add_liquidity");
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.actor_wallet, Some("Owner111".to_string()));
},
_ => panic!("expected liquidity event"),
}
}
#[test]