0.7.43-E5C

This commit is contained in:
2026-05-27 11:28:36 +02:00
parent 69c8f6c957
commit d9558a5c16
28 changed files with 4451 additions and 325 deletions

View File

@@ -178,6 +178,43 @@ pub const METEORA_DBC_PROGRAM_ID: &str = "dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4Du
/// DLMM = Dynamic Liquidity Market Maker.
pub const METEORA_DLMM_PROGRAM_ID: &str = "LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo";
/// MetaDAO META active token mint identifier from MetaDAO documentation.
pub const METADAO_META_MINT_ID: &str = "METAwkXcqyXKy1AtsSgJ8JiUHwGCafnZL38n3vYmeta";
/// MetaDAO METAC legacy token mint identifier from MetaDAO documentation.
pub const METADAO_METAC_LEGACY_MINT_ID: &str = "METADDFL6wWMWEoKTFJwcThTbUmtarRJZjRpzUvkxhr";
/// MetaDAO-linked P2P token mint candidate observed on Solscan.
///
/// This is a token mint, not a DEX program id. It is exposed for discovery only.
pub const METADAO_P2P_MINT_ID: &str = "P2PXup1ZvMpCDkJn3PQxtBYgxeCSfH39SFeurGSmeta";
/// MetaDAO Launchpad v0.7.0 program id from MetaDAO documentation and Solscan.
pub const METADAO_LAUNCHPAD_V0_7_0_PROGRAM_ID: &str =
"moontUzsdepotRGe5xsfip7vLPTJnVuafqdUWexVnPM";
/// MetaDAO Bid Wall v0.7.0 program id from MetaDAO documentation.
pub const METADAO_BID_WALL_V0_7_0_PROGRAM_ID: &str =
"WALL8ucBuUyL46QYxwYJjidaFYhdvxUFrgvBxPshERx";
/// MetaDAO Futarchy v0.6.0 program id from MetaDAO documentation.
pub const METADAO_FUTARCHY_V0_6_0_PROGRAM_ID: &str =
"FUTARELBfJfQ8RDGhg1wdhddq1odMAJUePHFuBYfUxKq";
/// MetaDAO AMM v0.5.0 program id from MetaDAO documentation.
pub const METADAO_AMM_V0_5_0_PROGRAM_ID: &str =
"AMMJdEiCCa8mdugg6JPF7gFirmmxisTfDJoSNSUi5zDJ";
/// Printr program id candidate observed on Solscan.
///
/// This remains a discovery target until a local corpus confirms the instruction semantics.
pub const PRINTR_PROGRAM_ID: &str = "T8HsGYv7sMk3kTnyaRqZrbRPuntYzdh12evXBkprint";
/// Zora program id candidate observed on Solscan.
///
/// This remains a discovery target until a local corpus confirms the instruction semantics.
pub const ZORA_PROGRAM_ID: &str = "zoRabwLGd5zXaV7Gxacppw8tcceXEiTrSKyNLSaSTUc";
/// Orca Whirlpools program id. ("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc").
pub const ORCA_WHIRLPOOLS_PROGRAM_ID: &str = "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc";

View File

@@ -221,7 +221,76 @@ impl MeteoraDammV1Decoder {
continue;
}
if instruction_kind == MeteoraDammV1InstructionKind::Swap {
let trade_side = infer_trade_side(&log_messages);
let decoded_amounts_result =
crate::meteora_swap_amount_inference::infer_meteora_swap_amounts_from_inner_transfers(
transaction,
instructions,
instruction,
pool_account.as_deref(),
);
let inner_transfer_amounts = match decoded_amounts_result {
Ok(decoded_amounts) => decoded_amounts,
Err(error) => return Err(error),
};
let mut amount_resolution_source = "none";
let decoded_amounts = match inner_transfer_amounts {
Some(decoded_amounts) => {
amount_resolution_source = "flattened_cpi_pool_transfer_window";
Some(decoded_amounts)
},
None => {
let event_log_amounts_result =
crate::meteora_swap_amount_inference::infer_meteora_damm_v1_swap_amounts_from_event_log(
transaction,
accounts.as_slice(),
log_messages.as_slice(),
pool_account.as_deref(),
);
match event_log_amounts_result {
Ok(event_log_amounts) => match event_log_amounts {
Some(event_log_amounts) => {
amount_resolution_source = "meteora_damm_v1_swap_event_log";
Some(event_log_amounts)
},
None => None,
},
Err(error) => return Err(error),
}
},
};
let fallback_trade_side = infer_trade_side(&log_messages);
let trade_side = match decoded_amounts.as_ref() {
Some(decoded_amounts) => decoded_amounts.trade_side,
None => fallback_trade_side,
};
let effective_token_a_mint = match decoded_amounts.as_ref() {
Some(decoded_amounts) => Some(decoded_amounts.base_token_mint.clone()),
None => token_a_mint.clone(),
};
let effective_token_b_mint = match decoded_amounts.as_ref() {
Some(decoded_amounts) => Some(decoded_amounts.quote_token_mint.clone()),
None => token_b_mint.clone(),
};
let base_vault = match decoded_amounts.as_ref() {
Some(decoded_amounts) => decoded_amounts.base_vault_address.clone(),
None => None,
};
let quote_vault = match decoded_amounts.as_ref() {
Some(decoded_amounts) => decoded_amounts.quote_vault_address.clone(),
None => None,
};
let base_amount_raw = match decoded_amounts.as_ref() {
Some(decoded_amounts) => {
serde_json::Value::String(decoded_amounts.base_amount_raw.clone())
},
None => serde_json::Value::Null,
};
let quote_amount_raw = match decoded_amounts.as_ref() {
Some(decoded_amounts) => {
serde_json::Value::String(decoded_amounts.quote_amount_raw.clone())
},
None => serde_json::Value::Null,
};
let payload_json = serde_json::json!({
"decoder": "meteora_damm_v1",
"eventKind": "swap",
@@ -236,8 +305,15 @@ impl MeteoraDammV1Decoder {
"parsed": parsed_json,
"logMessages": log_messages,
"poolAccount": pool_account,
"tokenAMint": token_a_mint,
"tokenBMint": token_b_mint,
"tokenAMint": effective_token_a_mint.clone(),
"tokenBMint": effective_token_b_mint.clone(),
"inputTokenAccount": extract_account(&accounts, 1),
"outputTokenAccount": extract_account(&accounts, 2),
"baseVault": base_vault,
"quoteVault": quote_vault,
"baseAmountRaw": base_amount_raw,
"quoteAmountRaw": quote_amount_raw,
"amountResolutionSource": amount_resolution_source,
"tradeSide": format!("{:?}", trade_side)
});
decoded_events.push(crate::MeteoraDammV1DecodedEvent::Swap(
@@ -248,8 +324,8 @@ impl MeteoraDammV1Decoder {
program_id: program_id.clone(),
trade_side,
pool_account,
token_a_mint,
token_b_mint,
token_a_mint: effective_token_a_mint,
token_b_mint: effective_token_b_mint,
payload_json,
},
));

View File

@@ -238,9 +238,27 @@ impl MeteoraDammV2Decoder {
if instruction_kind == MeteoraDammV2InstructionKind::Swap {
let used_swap2 = log_messages_contain_keyword(&log_messages, "swap2")
|| value_contains_any_key(parsed_json.as_ref(), &["swap2", "isSwap2"]);
let trade_side = infer_trade_side(&log_messages);
let has_trade_amount_payload =
let decoded_amounts_result =
crate::meteora_swap_amount_inference::infer_meteora_swap_amounts_from_inner_transfers(
transaction,
instructions,
instruction,
pool_account.as_deref(),
);
let decoded_amounts = match decoded_amounts_result {
Ok(decoded_amounts) => decoded_amounts,
Err(error) => return Err(error),
};
let fallback_trade_side = infer_trade_side(&log_messages);
let trade_side = match decoded_amounts.as_ref() {
Some(decoded_amounts) => decoded_amounts.trade_side,
None => fallback_trade_side,
};
let has_direct_amount_payload =
parsed_json_has_trade_amount_or_price_payload(parsed_json.as_ref());
let has_inferred_amount_payload = decoded_amounts.is_some();
let has_trade_amount_payload =
has_direct_amount_payload || has_inferred_amount_payload;
let event_actionability = if has_trade_amount_payload {
"trade_candidate"
} else {
@@ -251,6 +269,41 @@ impl MeteoraDammV2Decoder {
} else {
serde_json::Value::String("swap_without_amount_payload".to_string())
};
let effective_token_a_mint = match decoded_amounts.as_ref() {
Some(decoded_amounts) => Some(decoded_amounts.base_token_mint.clone()),
None => token_a_mint.clone(),
};
let effective_token_b_mint = match decoded_amounts.as_ref() {
Some(decoded_amounts) => Some(decoded_amounts.quote_token_mint.clone()),
None => token_b_mint.clone(),
};
let base_vault = match decoded_amounts.as_ref() {
Some(decoded_amounts) => decoded_amounts.base_vault_address.clone(),
None => None,
};
let quote_vault = match decoded_amounts.as_ref() {
Some(decoded_amounts) => decoded_amounts.quote_vault_address.clone(),
None => None,
};
let base_amount_raw = match decoded_amounts.as_ref() {
Some(decoded_amounts) => {
serde_json::Value::String(decoded_amounts.base_amount_raw.clone())
},
None => serde_json::Value::Null,
};
let quote_amount_raw = match decoded_amounts.as_ref() {
Some(decoded_amounts) => {
serde_json::Value::String(decoded_amounts.quote_amount_raw.clone())
},
None => serde_json::Value::Null,
};
let amount_resolution_source = if has_inferred_amount_payload {
"flattened_cpi_pool_transfer_window"
} else if has_direct_amount_payload {
"parsed_payload"
} else {
"none"
};
let payload_json = serde_json::json!({
"decoder": "meteora_damm_v2",
"eventKind": "swap",
@@ -265,6 +318,7 @@ impl MeteoraDammV2Decoder {
"candleCandidate": has_trade_amount_payload,
"nonTradeUseful": false,
"materializationSkipReason": materialization_skip_reason,
"amountResolutionSource": amount_resolution_source,
"signature": transaction.signature,
"instructionId": instruction_id,
"instructionIndex": instruction.instruction_index,
@@ -272,8 +326,12 @@ impl MeteoraDammV2Decoder {
"parsed": parsed_json,
"logMessages": log_messages,
"poolAccount": pool_account,
"tokenAMint": token_a_mint,
"tokenBMint": token_b_mint,
"tokenAMint": effective_token_a_mint.clone(),
"tokenBMint": effective_token_b_mint.clone(),
"baseVault": base_vault,
"quoteVault": quote_vault,
"baseAmountRaw": base_amount_raw,
"quoteAmountRaw": quote_amount_raw,
"tradeSide": format!("{:?}", trade_side)
});
decoded_events.push(crate::MeteoraDammV2DecodedEvent::Swap(
@@ -284,8 +342,8 @@ impl MeteoraDammV2Decoder {
program_id: program_id.clone(),
trade_side,
pool_account,
token_a_mint,
token_b_mint,
token_a_mint: effective_token_a_mint,
token_b_mint: effective_token_b_mint,
used_swap2,
payload_json,
},

View File

@@ -215,9 +215,27 @@ impl MeteoraDbcDecoder {
continue;
}
if instruction_kind == MeteoraDbcInstructionKind::Swap {
let trade_side = infer_trade_side(&log_messages);
let has_trade_amount_payload =
let decoded_amounts_result =
crate::meteora_swap_amount_inference::infer_meteora_swap_amounts_from_inner_transfers(
transaction,
instructions,
instruction,
pool_account.as_deref(),
);
let decoded_amounts = match decoded_amounts_result {
Ok(decoded_amounts) => decoded_amounts,
Err(error) => return Err(error),
};
let fallback_trade_side = infer_trade_side(&log_messages);
let trade_side = match decoded_amounts.as_ref() {
Some(decoded_amounts) => decoded_amounts.trade_side,
None => fallback_trade_side,
};
let has_direct_amount_payload =
parsed_json_has_trade_amount_or_price_payload(parsed_json.as_ref());
let has_inferred_amount_payload = decoded_amounts.is_some();
let has_trade_amount_payload =
has_direct_amount_payload || has_inferred_amount_payload;
let event_actionability = if has_trade_amount_payload {
"trade_candidate"
} else {
@@ -228,6 +246,41 @@ impl MeteoraDbcDecoder {
} else {
serde_json::Value::String("swap_without_amount_payload".to_string())
};
let effective_token_a_mint = match decoded_amounts.as_ref() {
Some(decoded_amounts) => Some(decoded_amounts.base_token_mint.clone()),
None => token_a_mint.clone(),
};
let effective_token_b_mint = match decoded_amounts.as_ref() {
Some(decoded_amounts) => Some(decoded_amounts.quote_token_mint.clone()),
None => token_b_mint.clone(),
};
let base_vault = match decoded_amounts.as_ref() {
Some(decoded_amounts) => decoded_amounts.base_vault_address.clone(),
None => None,
};
let quote_vault = match decoded_amounts.as_ref() {
Some(decoded_amounts) => decoded_amounts.quote_vault_address.clone(),
None => None,
};
let base_amount_raw = match decoded_amounts.as_ref() {
Some(decoded_amounts) => {
serde_json::Value::String(decoded_amounts.base_amount_raw.clone())
},
None => serde_json::Value::Null,
};
let quote_amount_raw = match decoded_amounts.as_ref() {
Some(decoded_amounts) => {
serde_json::Value::String(decoded_amounts.quote_amount_raw.clone())
},
None => serde_json::Value::Null,
};
let amount_resolution_source = if has_inferred_amount_payload {
"flattened_cpi_pool_transfer_window"
} else if has_direct_amount_payload {
"parsed_payload"
} else {
"none"
};
let payload_json = serde_json::json!({
"decoder": "meteora_dbc",
"eventKind": "swap",
@@ -242,6 +295,7 @@ impl MeteoraDbcDecoder {
"candleCandidate": has_trade_amount_payload,
"nonTradeUseful": false,
"materializationSkipReason": materialization_skip_reason,
"amountResolutionSource": amount_resolution_source,
"signature": transaction.signature,
"instructionId": instruction_id,
"instructionIndex": instruction.instruction_index,
@@ -249,8 +303,12 @@ impl MeteoraDbcDecoder {
"parsed": parsed_json,
"logMessages": log_messages,
"poolAccount": pool_account,
"tokenAMint": token_a_mint,
"tokenBMint": token_b_mint,
"tokenAMint": effective_token_a_mint.clone(),
"tokenBMint": effective_token_b_mint.clone(),
"baseVault": base_vault,
"quoteVault": quote_vault,
"baseAmountRaw": base_amount_raw,
"quoteAmountRaw": quote_amount_raw,
"tradeSide": format!("{:?}", trade_side)
});
decoded_events.push(crate::MeteoraDbcDecodedEvent::Swap(
@@ -261,8 +319,8 @@ impl MeteoraDbcDecoder {
program_id: program_id.clone(),
trade_side,
pool_account,
token_a_mint,
token_b_mint,
token_a_mint: effective_token_a_mint,
token_b_mint: effective_token_b_mint,
payload_json,
},
));

View File

@@ -82,9 +82,9 @@ pub struct RaydiumAmmV4SwapDecoded {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum RaydiumAmmV4DecodedEvent {
/// `initialize2` pool creation-like event.
Initialize2Pool(RaydiumAmmV4Initialize2PoolDecoded),
Initialize2Pool(std::boxed::Box<RaydiumAmmV4Initialize2PoolDecoded>),
/// Swap event decoded from a direct or inner Raydium AMM v4 instruction.
Swap(RaydiumAmmV4SwapDecoded),
Swap(std::boxed::Box<RaydiumAmmV4SwapDecoded>),
}
/// Raydium AmmV4 decoder.
@@ -165,8 +165,9 @@ impl RaydiumAmmV4Decoder {
log_messages.as_slice(),
);
if let Some(initialize_event) = initialize_event {
decoded_events
.push(crate::RaydiumAmmV4DecodedEvent::Initialize2Pool(initialize_event));
decoded_events.push(crate::RaydiumAmmV4DecodedEvent::Initialize2Pool(
std::boxed::Box::new(initialize_event),
));
continue;
}
}
@@ -181,7 +182,8 @@ impl RaydiumAmmV4Decoder {
token_balances.as_slice(),
);
match swap_result {
Ok(Some(swap)) => decoded_events.push(crate::RaydiumAmmV4DecodedEvent::Swap(swap)),
Ok(Some(swap)) => decoded_events
.push(crate::RaydiumAmmV4DecodedEvent::Swap(std::boxed::Box::new(swap))),
Ok(None) => {},
Err(error) => return Err(error),
}
@@ -429,8 +431,8 @@ fn extract_transaction_account_keys(
let mut account_keys = std::vec::Vec::new();
let values = transaction
.get("transaction")
.and_then(|value| value.get("message"))
.and_then(|value| value.get("accountKeys"))
.and_then(|value| return value.get("message"))
.and_then(|value| return value.get("accountKeys"))
.and_then(serde_json::Value::as_array);
if let Some(values) = values {
let mut index = 0usize;
@@ -467,8 +469,8 @@ fn append_loaded_addresses(
key: &str,
) {
let addresses = meta
.and_then(|value| value.get("loadedAddresses"))
.and_then(|value| value.get(key))
.and_then(|value| return value.get("loadedAddresses"))
.and_then(|value| return value.get(key))
.and_then(serde_json::Value::as_array);
let addresses = match addresses {
Some(addresses) => addresses,
@@ -511,7 +513,9 @@ fn collect_token_balance_side(
is_pre: bool,
accumulators: &mut std::vec::Vec<TokenBalanceAccumulator>,
) {
let values = meta.and_then(|value| value.get(key)).and_then(serde_json::Value::as_array);
let values = meta
.and_then(|value| return value.get(key))
.and_then(serde_json::Value::as_array);
let values = match values {
Some(values) => values,
None => return,
@@ -525,12 +529,12 @@ fn collect_token_balance_side(
let owner = value
.get("owner")
.and_then(serde_json::Value::as_str)
.map(|text| text.to_string());
.map(|text| return text.to_string());
let amount = value
.get("uiTokenAmount")
.and_then(|amount| amount.get("amount"))
.and_then(|amount| return amount.get("amount"))
.and_then(serde_json::Value::as_str)
.map(|text| text.to_string());
.map(|text| return text.to_string());
let account_address = match account_index {
Some(account_index) => account_address_by_index(account_keys, account_index),
None => None,
@@ -652,7 +656,7 @@ fn infer_authority_owned_vault_pair(
}
if candidates
.iter()
.any(|candidate: &TokenBalanceRecord| candidate.mint == record.mint)
.any(|candidate: &TokenBalanceRecord| return candidate.mint == record.mint)
{
continue;
}

View File

@@ -708,8 +708,8 @@ fn extract_transaction_account_keys(
let mut account_keys = std::vec::Vec::new();
let values = transaction
.get("transaction")
.and_then(|value| value.get("message"))
.and_then(|value| value.get("accountKeys"))
.and_then(|value| return value.get("message"))
.and_then(|value| return value.get("accountKeys"))
.and_then(serde_json::Value::as_array);
if let Some(values) = values {
let mut index = 0usize;
@@ -746,8 +746,8 @@ fn append_loaded_addresses(
key: &str,
) {
let addresses = meta
.and_then(|value| value.get("loadedAddresses"))
.and_then(|value| value.get(key))
.and_then(|value| return value.get("loadedAddresses"))
.and_then(|value| return value.get(key))
.and_then(serde_json::Value::as_array);
let addresses = match addresses {
Some(addresses) => addresses,
@@ -789,7 +789,9 @@ fn collect_token_balance_side(
is_pre: bool,
accumulators: &mut std::vec::Vec<TokenBalanceAccumulator>,
) {
let values = meta.and_then(|value| value.get(key)).and_then(serde_json::Value::as_array);
let values = meta
.and_then(|value| return value.get(key))
.and_then(serde_json::Value::as_array);
let values = match values {
Some(values) => values,
None => return,
@@ -802,9 +804,9 @@ fn collect_token_balance_side(
};
let amount = value
.get("uiTokenAmount")
.and_then(|amount| amount.get("amount"))
.and_then(|amount| return amount.get("amount"))
.and_then(serde_json::Value::as_str)
.map(|text| text.to_string());
.map(|text| return text.to_string());
let account_address = match account_index {
Some(account_index) => account_address_by_index(account_keys, account_index),
None => None,

View File

@@ -131,6 +131,14 @@ impl DexDecodeService {
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.preserve_unmatched_meteora_instruction_audits(&transaction, &instructions)
.await,
);
if let Err(error) = append_result {
return Err(error);
}
let append_result = append_persisted_events_result(
&mut persisted,
self.decode_and_persist_orca_whirlpools_events(&transaction, &instructions)
@@ -1069,15 +1077,15 @@ impl DexDecodeService {
instruction.accounts_json.as_str(),
);
let token_a_mint = candidate_raydium_mapped_account(
mapped_spec.and_then(|spec| spec.token_a_mint_index),
mapped_spec.and_then(|spec| return spec.token_a_mint_index),
accounts.as_slice(),
);
let token_b_mint = candidate_raydium_mapped_account(
mapped_spec.and_then(|spec| spec.token_b_mint_index),
mapped_spec.and_then(|spec| return spec.token_b_mint_index),
accounts.as_slice(),
);
let lp_mint = candidate_raydium_mapped_account(
mapped_spec.and_then(|spec| spec.lp_mint_index),
mapped_spec.and_then(|spec| return spec.lp_mint_index),
accounts.as_slice(),
);
let persist_result = self
@@ -1105,6 +1113,95 @@ impl DexDecodeService {
return Ok(persisted);
}
async fn preserve_unmatched_meteora_instruction_audits(
&self,
transaction: &crate::ChainTransactionDto,
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let transaction_id = match transaction.id {
Some(transaction_id) => transaction_id,
None => {
return Err(crate::Error::InvalidState(format!(
"transaction '{}' has no internal id",
transaction.signature
)));
},
};
let decoded_events_result = crate::query_dex_decoded_events_list_by_transaction_id(
self.database.as_ref(),
transaction_id,
)
.await;
let decoded_events = match decoded_events_result {
Ok(decoded_events) => decoded_events,
Err(error) => return Err(error),
};
let mut decoded_instruction_ids = std::collections::HashSet::<i64>::new();
for decoded_event in &decoded_events {
if !decoded_event.protocol_name.starts_with("meteora_") {
continue;
}
if decoded_event.event_kind.ends_with(".instruction_audit") {
continue;
}
let instruction_id = match decoded_event.instruction_id {
Some(instruction_id) => instruction_id,
None => continue,
};
decoded_instruction_ids.insert(instruction_id);
}
let mut persisted = std::vec::Vec::new();
for instruction in instructions {
let program_id = match instruction.program_id.as_ref() {
Some(program_id) => program_id,
None => continue,
};
let audit_spec = match meteora_instruction_audit_spec(program_id.as_str()) {
Some(audit_spec) => audit_spec,
None => continue,
};
let instruction_id = match instruction.id {
Some(instruction_id) => instruction_id,
None => continue,
};
if decoded_instruction_ids.contains(&instruction_id) {
continue;
}
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());
let payload = build_meteora_instruction_audit_payload(
transaction,
instruction,
audit_spec.protocol_name,
audit_spec.event_kind,
program_id.as_str(),
);
let pool_account =
candidate_meteora_audit_pool_account(audit_spec, accounts.as_slice());
let persist_result = self
.materialize_named_dex_event(
transaction,
transaction_id,
instruction_id,
audit_spec.protocol_name,
program_id.clone(),
audit_spec.event_kind,
pool_account,
None,
None,
None,
None,
payload,
)
.await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
}
return Ok(persisted);
}
async fn decode_and_persist_pump_fun_events(
&self,
transaction: &crate::ChainTransactionDto,
@@ -1318,6 +1415,13 @@ struct RaydiumInstructionAuditSpec {
candidate_pool_account_index: usize,
}
#[derive(Clone, Copy)]
struct MeteoraInstructionAuditSpec {
protocol_name: &'static str,
event_kind: &'static str,
candidate_pool_account_index: std::option::Option<usize>,
}
#[derive(Clone, Copy)]
struct RaydiumMappedNonTradeInstructionSpec {
instruction_name: &'static str,
@@ -1624,6 +1728,94 @@ fn read_u128_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u1
return Some(u128::from_le_bytes(bytes));
}
fn meteora_instruction_audit_spec(
program_id: &str,
) -> std::option::Option<MeteoraInstructionAuditSpec> {
if program_id == crate::METEORA_DBC_PROGRAM_ID {
return Some(MeteoraInstructionAuditSpec {
protocol_name: "meteora_dbc",
event_kind: "meteora_dbc.instruction_audit",
candidate_pool_account_index: Some(1),
});
}
if program_id == crate::METEORA_DLMM_PROGRAM_ID {
return Some(MeteoraInstructionAuditSpec {
protocol_name: "meteora_dlmm",
event_kind: "meteora_dlmm.instruction_audit",
candidate_pool_account_index: Some(0),
});
}
if program_id == crate::METEORA_DAMM_V1_PROGRAM_ID {
return Some(MeteoraInstructionAuditSpec {
protocol_name: "meteora_damm_v1",
event_kind: "meteora_damm_v1.instruction_audit",
candidate_pool_account_index: Some(0),
});
}
if program_id == crate::METEORA_DAMM_V2_PROGRAM_ID {
return Some(MeteoraInstructionAuditSpec {
protocol_name: "meteora_damm_v2",
event_kind: "meteora_damm_v2.instruction_audit",
candidate_pool_account_index: Some(1),
});
}
return None;
}
fn candidate_meteora_audit_pool_account(
audit_spec: MeteoraInstructionAuditSpec,
accounts: &[std::string::String],
) -> std::option::Option<std::string::String> {
let index = match audit_spec.candidate_pool_account_index {
Some(index) => index,
None => return None,
};
return accounts.get(index).cloned();
}
fn build_meteora_instruction_audit_payload(
transaction: &crate::ChainTransactionDto,
instruction: &crate::ChainInstructionDto,
protocol_name: &str,
event_kind: &str,
program_id: &str,
) -> serde_json::Value {
let accounts = parse_instruction_accounts_value(instruction.accounts_json.as_str());
let account_count = match accounts.as_array() {
Some(items) => items.len(),
None => 0,
};
let data_base58 = parse_instruction_data_base58(instruction.data_json.as_deref());
let discriminator_hex = discriminator_hex_from_base58(data_base58.as_deref());
let data_prefix = data_base58
.as_ref()
.map(|value| return value.chars().take(16).collect::<std::string::String>());
return serde_json::json!({
"decoder": protocol_name,
"eventKind": event_kind,
"signature": transaction.signature,
"instructionId": instruction.id,
"instructionIndex": instruction.instruction_index,
"innerInstructionIndex": instruction.inner_instruction_index,
"innerInstruction": instruction.inner_instruction_index.is_some(),
"parentInstructionId": instruction.parent_instruction_id,
"programId": program_id,
"programFamily": "meteora",
"accounts": accounts,
"accountCount": account_count,
"data": data_base58,
"dataPrefix": data_prefix,
"discriminatorHex": discriminator_hex,
"auditReason": "meteora_instruction_not_decoded_by_specific_decoder",
"proofStatus": "unclassified_local_corpus_instruction",
"tradeCandidate": false,
"candleCandidate": false,
"nonTradeUseful": false,
"skipTradeReason": "instruction_audit_only",
"skipCandleReason": "instruction_audit_only"
});
}
fn raydium_instruction_audit_event_kind_by_protocol(
protocol_name: &str,
) -> std::option::Option<&'static str> {
@@ -2774,7 +2966,6 @@ mod tests {
assert_eq!(decrease.pool_account_index, Some(3));
assert_eq!(decrease.token_a_mint_index, Some(14));
assert_eq!(decrease.token_b_mint_index, Some(15));
let increase = super::raydium_mapped_non_trade_instruction_spec(
"raydium_clmm",
Some("851d59df45eeb00a"),
@@ -2800,7 +2991,6 @@ mod tests {
None => panic!("collect_creator_fee discriminator must be mapped"),
};
assert_eq!(collect_creator_fee.event_kind, "raydium_cpmm.collect_creator_fee");
let withdraw = super::raydium_mapped_non_trade_instruction_spec(
"raydium_cpmm",
Some("b712469c946da122"),
@@ -2811,7 +3001,6 @@ mod tests {
None => panic!("withdraw discriminator must be mapped"),
};
assert_eq!(withdraw.event_kind, "raydium_cpmm.withdraw");
let initialize = super::raydium_mapped_non_trade_instruction_spec(
"raydium_cpmm",
Some("afaf6d1f0d989bed"),

View File

@@ -375,7 +375,7 @@ impl DexDetectService {
decoded_event: &crate::DexDecodedEventDto,
) -> Result<crate::DexPoolDetectionResult, crate::Error> {
return self
.detect_materialized_pool_from_decoded_event(
.detect_materialized_pool_from_decoded_event_with_payload_vaults(
transaction,
decoded_event,
"meteora_dbc",
@@ -446,7 +446,7 @@ impl DexDetectService {
decoded_event: &crate::DexDecodedEventDto,
) -> Result<crate::DexPoolDetectionResult, crate::Error> {
return self
.detect_materialized_pool_from_decoded_event(
.detect_materialized_pool_from_decoded_event_with_payload_vaults(
transaction,
decoded_event,
"meteora_damm_v1",
@@ -463,7 +463,7 @@ impl DexDetectService {
decoded_event: &crate::DexDecodedEventDto,
) -> Result<crate::DexPoolDetectionResult, crate::Error> {
return self
.detect_materialized_pool_from_decoded_event(
.detect_materialized_pool_from_decoded_event_with_payload_vaults(
transaction,
decoded_event,
"meteora_damm_v2",
@@ -743,6 +743,64 @@ impl DexDetectService {
return Ok(detection_result);
}
async fn detect_materialized_pool_from_decoded_event_with_payload_vaults(
&self,
transaction: &crate::ChainTransactionDto,
decoded_event: &crate::DexDecodedEventDto,
dex_code: &str,
pool_kind: crate::PoolKind,
pool_status: crate::PoolStatus,
signal_prefix: &str,
) -> Result<crate::DexPoolDetectionResult, crate::Error> {
let dex_id_result =
crate::dex_catalog::ensure_known_dex(self.database.as_ref(), dex_code).await;
let dex_id = match dex_id_result {
Ok(dex_id) => dex_id,
Err(error) => return Err(error),
};
let payload_value_result = parse_payload_json(decoded_event.payload_json.as_str());
let payload_value = match payload_value_result {
Ok(payload_value) => payload_value,
Err(error) => return Err(error),
};
let base_vault = extract_payload_string_field(&payload_value, "baseVault");
let quote_vault = extract_payload_string_field(&payload_value, "quoteVault");
let input_result =
crate::dex_pool_materialization::DexPoolMaterializationInput::from_decoded_event(
decoded_event,
dex_id,
pool_kind,
pool_status,
crate::dex_pool_materialization::DexPoolTokenOrder::ChooseBaseQuoteFromTokenAB,
base_vault,
quote_vault,
transaction.source_endpoint_name.clone(),
);
let input = match input_result {
Ok(input) => input,
Err(error) => return Err(error),
};
let detection_result =
crate::dex_pool_materialization::materialize_dex_pool(self.database.as_ref(), &input)
.await;
let detection_result = match detection_result {
Ok(detection_result) => detection_result,
Err(error) => return Err(error),
};
let signal_result = self
.record_pool_detection_signals(
transaction,
signal_prefix,
&detection_result,
payload_value,
)
.await;
if let Err(error) = signal_result {
return Err(error);
}
return Ok(detection_result);
}
async fn record_detection_signal(
&self,
transaction: &crate::ChainTransactionDto,

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,8 @@ mod local_pipeline_diagnostics;
mod local_pipeline_replay;
/// Local pipeline validation helpers for non-regression runs.
mod local_pipeline_validation;
/// Meteora swap amount inference from flattened CPI token transfers.
mod meteora_swap_amount_inference;
/// Useful non-trade DEX event materialization service.
mod non_trade_event_materialization;
/// On-chain DEX pair/pool discovery helpers used by Demo3.
@@ -182,6 +184,20 @@ pub use constants::JUP_MINT_ID;
/// Loader V4 program identifier. ("LoaderV411111111111111111111111111111111111").
/// @see solana_sdk::pubkey::Pubkey = solana_sdk_ids::loader_v4::ID
pub use constants::LOADER_V4_PROGRAM_ID;
/// MetaDAO AMM v0.5.0 program id.
pub use constants::METADAO_AMM_V0_5_0_PROGRAM_ID;
/// MetaDAO Bid Wall v0.7.0 program id.
pub use constants::METADAO_BID_WALL_V0_7_0_PROGRAM_ID;
/// MetaDAO Futarchy v0.6.0 program id.
pub use constants::METADAO_FUTARCHY_V0_6_0_PROGRAM_ID;
/// MetaDAO Launchpad v0.7.0 program id.
pub use constants::METADAO_LAUNCHPAD_V0_7_0_PROGRAM_ID;
/// MetaDAO META active token mint identifier.
pub use constants::METADAO_META_MINT_ID;
/// MetaDAO METAC legacy token mint identifier.
pub use constants::METADAO_METAC_LEGACY_MINT_ID;
/// MetaDAO-linked P2P token mint candidate.
pub use constants::METADAO_P2P_MINT_ID;
/// Meteora DAMM v1 program id. ("Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB").
pub use constants::METEORA_DAMM_V1_PROGRAM_ID;
/// Meteora DAMM v2 program id. ("cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG").
@@ -195,6 +211,8 @@ pub use constants::METEORA_DLMM_PROGRAM_ID;
pub use constants::NATIVE_LOADER_PROGRAM_ID;
/// Orca Whirlpools program id. ("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc").
pub use constants::ORCA_WHIRLPOOLS_PROGRAM_ID;
/// Printr program id candidate observed on Solscan.
pub use constants::PRINTR_PROGRAM_ID;
/// Pump.fun program id. ("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P").
pub use constants::PUMP_FUN_PROGRAM_ID;
/// PumpSwap / PumpAMM program id. ("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA").
@@ -289,6 +307,8 @@ pub use constants::ZK_ELGAMAL_PROOF_PROGRAM_ID;
/// Zk Token Proof program identifier. ("ZkTokenProof1111111111111111111111111111111").
/// @see solana_sdk::pubkey::Pubkey = solana_sdk_ids::zk_token_proof_program::ID
pub use constants::ZK_TOKEN_PROOF_PROGRAM_ID;
/// Zora program id candidate observed on Solscan.
pub use constants::ZORA_PROGRAM_ID;
/// Application-facing analysis signal DTO.
pub use db::AnalysisSignalDto;
/// Persisted analysis signal row.
@@ -1114,6 +1134,10 @@ pub use local_pipeline_validation::LocalPipelineValidationRunDto;
pub use local_pipeline_validation::LocalPipelineValidationService;
/// Validates a diagnostics summary without performing database access.
pub use local_pipeline_validation::validate_local_pipeline_diagnostics_summary;
/// Result of non-trade event materialization for one transaction.
pub use non_trade_event_materialization::NonTradeEventMaterializationResult;
/// Materializes useful non-trade decoded DEX events.
pub use non_trade_event_materialization::NonTradeEventMaterializationService;
/// Candidate account inferred from generic transaction evidence.
pub use onchain_dex_pair_discovery::OnchainDexCandidateAccountDto;
/// Candidate transaction/instruction observed on-chain for one DEX program id.
@@ -1124,6 +1148,8 @@ pub use onchain_dex_pair_discovery::OnchainDexPairDiscoveryRequestDto;
pub use onchain_dex_pair_discovery::OnchainDexPairDiscoveryResultDto;
/// On-chain pair/pool discovery service.
pub use onchain_dex_pair_discovery::OnchainDexPairDiscoveryService;
/// Rejected on-chain DEX candidate summary DTO.
pub use onchain_dex_pair_discovery::OnchainDexRejectedCandidateSummaryDto;
/// Token-balance delta observed in one transaction through Solana transaction metadata.
pub use onchain_dex_pair_discovery::OnchainDexTokenBalanceDeltaDto;
/// One pair-analytic-signal recording result.
@@ -1248,8 +1274,3 @@ pub use ws_manager::WsManagedEndpointSnapshot;
pub use ws_manager::WsManager;
/// Snapshot of the whole manager state.
pub use ws_manager::WsManagerSnapshot;
/// Result of non-trade event materialization for one transaction.
pub use non_trade_event_materialization::NonTradeEventMaterializationResult;
/// Materializes useful non-trade decoded DEX events.
pub use non_trade_event_materialization::NonTradeEventMaterializationService;

View File

@@ -344,6 +344,30 @@ impl LocalPipelineValidationConfig {
return config;
}
/// Builds the `0.7.43` Meteora effective-surface validation config.
///
/// This profile restarts from the established Meteora-family validation but
/// keeps missing variants as warnings while a fresh corpus is being built.
/// It is intentionally strict on global trade/candle invariants: successful
/// trade candidates still need materialized trades, failed transactions stay
/// non-actionable, and non-trade Meteora events must not feed trades/candles.
pub fn v0_7_43_meteora_effective_surfaces() -> Self {
let mut config = Self::v0_7_36_meteora_family_consolidation();
config.profile_code = "0.7.43_meteora_effective_surfaces".to_string();
config.expected_dex_codes = vec![
"meteora_dbc".to_string(),
"meteora_damm_v1".to_string(),
"meteora_damm_v2".to_string(),
"meteora_dlmm".to_string(),
];
config.require_all_expected_dexes = false;
config.allow_unexpected_dexes = true;
config.require_trade_events_per_dex = false;
config.require_candles_per_dex = false;
config.require_pair_trading_readiness_semantics = false;
return config;
}
/// Builds the legacy `0.7.39` launch-surface validation alias.
///
/// The implementation now delegates to the DEX-first profile so callers that
@@ -474,7 +498,9 @@ impl LocalPipelineValidationService {
) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
let diagnostics_service =
crate::LocalPipelineDiagnosticsService::new(self.database.clone());
let summary_result = if config.profile_code == "0.7.42_raydium_family_event_coverage" {
let summary_result = if config.profile_code == "0.7.42_raydium_family_event_coverage"
|| config.profile_code == "0.7.43_meteora_effective_surfaces"
{
diagnostics_service.diagnose_for_validation().await
} else {
diagnostics_service.diagnose().await
@@ -617,6 +643,14 @@ impl LocalPipelineValidationService {
let config = crate::LocalPipelineValidationConfig::v0_7_42_raydium_family_event_coverage();
return self.validate_current_database(&config).await;
}
/// Diagnoses the current database with the `0.7.43` Meteora effective-surface profile.
pub async fn validate_v0_7_43_current_database(
&self,
) -> Result<crate::LocalPipelineValidationRunDto, crate::Error> {
let config = crate::LocalPipelineValidationConfig::v0_7_43_meteora_effective_surfaces();
return self.validate_current_database(&config).await;
}
}
/// Validates a diagnostics summary without performing database access.
@@ -743,7 +777,8 @@ pub fn validate_local_pipeline_diagnostics_summary(
|| config.profile_code == "0.7.39_dex_first_effective_swap_surfaces"
|| config.profile_code == "0.7.39_launch_surface_origin_baseline"
|| config.profile_code == "0.7.40_raydium_effective_surfaces"
|| config.profile_code == "0.7.41_raydium_amm_v4_swap_decoder";
|| config.profile_code == "0.7.41_raydium_amm_v4_swap_decoder"
|| config.profile_code == "0.7.43_meteora_effective_surfaces";
if config.require_all_expected_dexes || missing_expected_dex_is_warning {
for expected_dex_code in &expected_dex_codes {
if !observed_dex_codes.contains(expected_dex_code) {
@@ -1022,8 +1057,7 @@ fn validate_dex_support_matrix_semantics(
});
}
if entry.surface_role == "to_verify"
&& (entry.program_id.is_some()
|| entry.program_id_status != "to_verify"
&& (entry.program_id_status != "to_verify"
|| entry.catalog_enabled
|| entry.decoded
|| entry.materialized
@@ -1033,7 +1067,7 @@ fn validate_dex_support_matrix_semantics(
issues.push(crate::LocalPipelineValidationIssueDto {
code: "to_verify_matrix_entry_promoted_without_proof".to_string(),
message: format!(
"to-verify surface '{}' must not expose program id, catalog activation, decoding or trade/candle materialization without corpus proof",
"to-verify surface '{}' must not expose catalog activation, decoding or trade/candle materialization without corpus proof",
entry_code
),
subject: Some(entry_code.clone()),
@@ -1599,6 +1633,31 @@ mod tests {
assert!(report.expected_dex_codes.contains(&"raydium_amm_v4".to_string()));
}
#[test]
fn validation_accepts_0_7_43_meteora_effective_surfaces_partial_corpus() {
let mut summary = make_0_7_28_summary_with_meteora();
summary.dex_summaries.retain(|dex_summary| {
return dex_summary.dex_code == "meteora_dlmm"
|| dex_summary.dex_code == "meteora_damm_v1";
});
summary.decoded_non_trade_useful_event_count = 7;
summary.liquidity_event_count = 4;
summary.pool_lifecycle_event_count = 1;
summary.fee_event_count = 2;
let config = crate::LocalPipelineValidationConfig::v0_7_43_meteora_effective_surfaces();
let report = crate::validate_local_pipeline_diagnostics_summary(&summary, &config);
assert!(report.validation_passed);
assert_eq!(report.validation_profile_code, "0.7.43_meteora_effective_surfaces");
assert_eq!(report.blocking_issue_count, 0);
assert!(report.warning_count >= 2);
assert!(report.expected_dex_codes.contains(&"meteora_dbc".to_string()));
assert!(report.expected_dex_codes.contains(&"meteora_damm_v1".to_string()));
assert!(report.expected_dex_codes.contains(&"meteora_damm_v2".to_string()));
assert!(report.expected_dex_codes.contains(&"meteora_dlmm".to_string()));
assert_eq!(report.liquidity_event_count, 4);
assert_eq!(report.fee_event_count, 2);
}
#[test]
fn validation_rejects_0_7_33_pair_trading_readiness_mismatch() {
let mut summary = make_0_7_28_summary_with_meteora();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff