This commit is contained in:
2026-06-05 14:53:16 +02:00
parent 27e25d5bf4
commit f81e0f3bea
66 changed files with 7655 additions and 214 deletions

View File

@@ -61,6 +61,7 @@ pub use dtos::LocalRaydiumSurfaceDiagnosticSummaryDto;
pub use dtos::LocalTokenMetadataGapDiagnosticSampleDto;
pub use dtos::ObservedTokenDto;
pub use dtos::OnchainObservationDto;
pub use dtos::OrderbookEventDto;
pub use dtos::PairAnalyticSignalDto;
pub use dtos::PairCandleDto;
pub use dtos::PairDto;
@@ -106,6 +107,7 @@ pub use entities::LaunchSurfaceKeyEntity;
pub use entities::LiquidityEventEntity;
pub use entities::ObservedTokenEntity;
pub use entities::OnchainObservationEntity;
pub use entities::OrderbookEventEntity;
pub use entities::PairAnalyticSignalEntity;
pub use entities::PairCandleEntity;
pub use entities::PairEntity;
@@ -152,8 +154,10 @@ pub use queries::query_dex_decode_replay_ledger_get_by_signature;
pub use queries::query_dex_decode_replay_ledger_get_by_transaction;
pub use queries::query_dex_decode_replay_ledger_upsert;
pub use queries::query_dex_decoded_events_delete_by_key;
pub use queries::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches;
pub use queries::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits;
pub use queries::query_dex_decoded_events_delete_related_instruction_audit;
pub use queries::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits;
pub use queries::query_dex_decoded_events_get_by_key;
pub use queries::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint;
pub use queries::query_dex_decoded_events_list_by_transaction_id;
@@ -216,6 +220,7 @@ pub use queries::query_observed_tokens_list;
pub use queries::query_observed_tokens_upsert;
pub use queries::query_onchain_observations_insert;
pub use queries::query_onchain_observations_list_recent;
pub use queries::query_orderbook_events_upsert;
pub use queries::query_pair_analytic_signals_get_by_key;
pub use queries::query_pair_analytic_signals_list_by_pair_id;
pub use queries::query_pair_analytic_signals_upsert;

View File

@@ -24,6 +24,7 @@ mod local_dex_corpus_search;
mod local_pipeline_diagnostics;
mod observed_token;
mod onchain_observation;
mod orderbook_event;
mod pair;
mod pair_analytic_signal;
mod pair_candle;
@@ -117,6 +118,7 @@ pub use local_pipeline_diagnostics::LocalRaydiumSurfaceDiagnosticSummaryDto;
pub use local_pipeline_diagnostics::LocalTokenMetadataGapDiagnosticSampleDto;
pub use observed_token::ObservedTokenDto;
pub use onchain_observation::OnchainObservationDto;
pub use orderbook_event::OrderbookEventDto;
pub use pair::PairDto;
pub use pair_analytic_signal::PairAnalyticSignalDto;
pub use pair_candle::PairCandleDto;

View File

@@ -0,0 +1,190 @@
// file: kb_lib/src/db/dtos/orderbook_event.rs
//! Orderbook event DTO.
/// Application-facing normalized orderbook or limit-order event DTO.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct OrderbookEventDto {
/// Optional numeric primary key.
pub id: std::option::Option<i64>,
/// Related transaction id.
pub transaction_id: i64,
/// Related decoded DEX event id, when available.
pub decoded_event_id: std::option::Option<i64>,
/// Related DEX id, when the DEX row is known.
pub dex_id: std::option::Option<i64>,
/// Related pool id, when the pool row is known.
pub pool_id: std::option::Option<i64>,
/// Related pair id, when the pair row is known.
pub pair_id: std::option::Option<i64>,
/// Transaction signature.
pub signature: std::string::String,
/// Optional slot number.
pub slot: std::option::Option<u64>,
/// Protocol name that emitted the decoded event.
pub protocol_name: std::string::String,
/// Program id that emitted the decoded event.
pub program_id: std::string::String,
/// Stable decoded event kind.
pub event_kind: std::string::String,
/// Normalized orderbook action.
pub order_action: std::string::String,
/// Pool account address, when decoded.
pub pool_account: std::option::Option<std::string::String>,
/// Market/orderbook account, when decoded.
pub market_account: std::option::Option<std::string::String>,
/// Wallet or authority associated with the event, when decoded.
pub actor_wallet: std::option::Option<std::string::String>,
/// Limit/order account, when decoded.
pub order_account: std::option::Option<std::string::String>,
/// Base or token A mint, when decoded.
pub base_token_mint: std::option::Option<std::string::String>,
/// Quote or token B mint, when decoded.
pub quote_token_mint: std::option::Option<std::string::String>,
/// Raw order amount as decimal text, when decoded.
pub amount_raw: std::option::Option<std::string::String>,
/// Raw minimum amount as decimal text, when decoded.
pub amount_min_raw: std::option::Option<std::string::String>,
/// Optional tick index for CLMM limit orders.
pub tick_index: std::option::Option<i64>,
/// Optional zero-for-one side flag.
pub zero_for_one: std::option::Option<bool>,
/// Source decoded payload JSON.
pub payload_json: std::string::String,
/// Execution timestamp.
pub executed_at: chrono::DateTime<chrono::Utc>,
/// Creation timestamp.
pub created_at: chrono::DateTime<chrono::Utc>,
}
impl OrderbookEventDto {
/// Creates a new orderbook event DTO.
#[allow(clippy::too_many_arguments)]
pub fn new(
transaction_id: i64,
decoded_event_id: std::option::Option<i64>,
dex_id: std::option::Option<i64>,
pool_id: std::option::Option<i64>,
pair_id: std::option::Option<i64>,
signature: std::string::String,
slot: std::option::Option<u64>,
protocol_name: std::string::String,
program_id: std::string::String,
event_kind: std::string::String,
order_action: std::string::String,
pool_account: std::option::Option<std::string::String>,
market_account: std::option::Option<std::string::String>,
actor_wallet: std::option::Option<std::string::String>,
order_account: std::option::Option<std::string::String>,
base_token_mint: std::option::Option<std::string::String>,
quote_token_mint: std::option::Option<std::string::String>,
amount_raw: std::option::Option<std::string::String>,
amount_min_raw: std::option::Option<std::string::String>,
tick_index: std::option::Option<i64>,
zero_for_one: std::option::Option<bool>,
payload_json: std::string::String,
) -> Self {
let now = chrono::Utc::now();
return Self {
id: None,
transaction_id,
decoded_event_id,
dex_id,
pool_id,
pair_id,
signature,
slot,
protocol_name,
program_id,
event_kind,
order_action,
pool_account,
market_account,
actor_wallet,
order_account,
base_token_mint,
quote_token_mint,
amount_raw,
amount_min_raw,
tick_index,
zero_for_one,
payload_json,
executed_at: now,
created_at: now,
};
}
}
impl TryFrom<crate::OrderbookEventEntity> for OrderbookEventDto {
type Error = crate::Error;
fn try_from(entity: crate::OrderbookEventEntity) -> Result<Self, Self::Error> {
let executed_at_result = chrono::DateTime::parse_from_rfc3339(&entity.executed_at);
let executed_at = match executed_at_result {
Ok(executed_at) => executed_at.with_timezone(&chrono::Utc),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot parse orderbook event executed_at '{}': {}",
entity.executed_at, error
)));
},
};
let created_at_result = chrono::DateTime::parse_from_rfc3339(&entity.created_at);
let created_at = match created_at_result {
Ok(created_at) => created_at.with_timezone(&chrono::Utc),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot parse orderbook event created_at '{}': {}",
entity.created_at, error
)));
},
};
let slot = match entity.slot {
Some(slot) => {
let slot_result = u64::try_from(slot);
match slot_result {
Ok(slot) => Some(slot),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot convert orderbook event slot '{}' to u64: {}",
slot, error
)));
},
}
},
None => None,
};
let zero_for_one = match entity.zero_for_one {
Some(0) => Some(false),
Some(_) => Some(true),
None => None,
};
return Ok(Self {
id: Some(entity.id),
transaction_id: entity.transaction_id,
decoded_event_id: entity.decoded_event_id,
dex_id: entity.dex_id,
pool_id: entity.pool_id,
pair_id: entity.pair_id,
signature: entity.signature,
slot,
protocol_name: entity.protocol_name,
program_id: entity.program_id,
event_kind: entity.event_kind,
order_action: entity.order_action,
pool_account: entity.pool_account,
market_account: entity.market_account,
actor_wallet: entity.actor_wallet,
order_account: entity.order_account,
base_token_mint: entity.base_token_mint,
quote_token_mint: entity.quote_token_mint,
amount_raw: entity.amount_raw,
amount_min_raw: entity.amount_min_raw,
tick_index: entity.tick_index,
zero_for_one,
payload_json: entity.payload_json,
executed_at,
created_at,
});
}
}

View File

@@ -24,6 +24,7 @@ mod launch_surface_key;
mod liquidity_event;
mod observed_token;
mod onchain_observation;
mod orderbook_event;
mod pair;
mod pair_analytic_signal;
mod pair_candle;
@@ -70,6 +71,7 @@ pub use launch_surface_key::LaunchSurfaceKeyEntity;
pub use liquidity_event::LiquidityEventEntity;
pub use observed_token::ObservedTokenEntity;
pub use onchain_observation::OnchainObservationEntity;
pub use orderbook_event::OrderbookEventEntity;
pub use pair::PairEntity;
pub use pair_analytic_signal::PairAnalyticSignalEntity;
pub use pair_candle::PairCandleEntity;

View File

@@ -0,0 +1,58 @@
// file: kb_lib/src/db/entities/orderbook_event.rs
//! Orderbook event entity.
/// Persisted normalized orderbook or limit-order event row.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
pub struct OrderbookEventEntity {
/// Numeric primary key.
pub id: i64,
/// Related transaction id.
pub transaction_id: i64,
/// Related decoded DEX event id, when available.
pub decoded_event_id: std::option::Option<i64>,
/// Related DEX id, when the DEX row is known.
pub dex_id: std::option::Option<i64>,
/// Related pool id, when the pool row is known.
pub pool_id: std::option::Option<i64>,
/// Related pair id, when the pair row is known.
pub pair_id: std::option::Option<i64>,
/// Transaction signature.
pub signature: std::string::String,
/// Optional slot number.
pub slot: std::option::Option<i64>,
/// Protocol name that emitted the decoded event.
pub protocol_name: std::string::String,
/// Program id that emitted the decoded event.
pub program_id: std::string::String,
/// Stable decoded event kind.
pub event_kind: std::string::String,
/// Normalized orderbook action.
pub order_action: std::string::String,
/// Pool account address, when decoded.
pub pool_account: std::option::Option<std::string::String>,
/// Market/orderbook account, when decoded.
pub market_account: std::option::Option<std::string::String>,
/// Wallet or authority associated with the event, when decoded.
pub actor_wallet: std::option::Option<std::string::String>,
/// Limit/order account, when decoded.
pub order_account: std::option::Option<std::string::String>,
/// Base or token A mint, when decoded.
pub base_token_mint: std::option::Option<std::string::String>,
/// Quote or token B mint, when decoded.
pub quote_token_mint: std::option::Option<std::string::String>,
/// Raw order amount as decimal text, when decoded.
pub amount_raw: std::option::Option<std::string::String>,
/// Raw minimum amount as decimal text, when decoded.
pub amount_min_raw: std::option::Option<std::string::String>,
/// Optional tick index for CLMM limit orders.
pub tick_index: std::option::Option<i64>,
/// Optional zero-for-one side flag, stored as 0/1.
pub zero_for_one: std::option::Option<i64>,
/// Source decoded payload JSON.
pub payload_json: std::string::String,
/// Execution timestamp encoded as RFC3339 UTC text.
pub executed_at: std::string::String,
/// Creation timestamp encoded as RFC3339 UTC text.
pub created_at: std::string::String,
}

View File

@@ -13,9 +13,9 @@ mod dex_decode_replay_ledger;
mod dex_decoded_event;
mod dex_event_coverage_entry;
mod fee_event;
mod instruction_observation;
mod known_http_endpoint;
mod known_ws_endpoint;
mod instruction_observation;
mod launch_attribution;
mod launch_surface;
mod launch_surface_key;
@@ -24,6 +24,7 @@ mod local_dex_corpus_search;
mod local_pipeline_diagnostics;
mod observed_token;
mod onchain_observation;
mod orderbook_event;
mod pair;
mod pair_analytic_signal;
mod pair_candle;
@@ -72,8 +73,10 @@ pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_get_by_signatur
pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_get_by_transaction;
pub use dex_decode_replay_ledger::query_dex_decode_replay_ledger_upsert;
pub use dex_decoded_event::query_dex_decoded_events_delete_by_key;
pub use dex_decoded_event::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches;
pub use dex_decoded_event::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits;
pub use dex_decoded_event::query_dex_decoded_events_delete_related_instruction_audit;
pub use dex_decoded_event::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits;
pub use dex_decoded_event::query_dex_decoded_events_get_by_key;
pub use dex_decoded_event::query_dex_decoded_events_get_latest_pump_fun_create_payload_by_mint;
pub use dex_decoded_event::query_dex_decoded_events_list_by_transaction_id;
@@ -87,14 +90,14 @@ pub use dex_event_coverage_entry::query_dex_event_coverage_entries_upsert;
pub use fee_event::query_fee_events_get_by_decoded_event_id;
pub use fee_event::query_fee_events_list_recent;
pub use fee_event::query_fee_events_upsert;
pub use instruction_observation::query_instruction_observations_list_by_filter;
pub use instruction_observation::query_instruction_observations_upsert;
pub use known_http_endpoint::query_known_http_endpoints_get;
pub use known_http_endpoint::query_known_http_endpoints_list;
pub use known_http_endpoint::query_known_http_endpoints_upsert;
pub use known_ws_endpoint::query_known_ws_endpoints_get;
pub use known_ws_endpoint::query_known_ws_endpoints_list;
pub use known_ws_endpoint::query_known_ws_endpoints_upsert;
pub use instruction_observation::query_instruction_observations_list_by_filter;
pub use instruction_observation::query_instruction_observations_upsert;
pub use launch_attribution::query_launch_attributions_get_by_decoded_event_id;
pub use launch_attribution::query_launch_attributions_list_by_pool_id;
pub use launch_attribution::query_launch_attributions_upsert;
@@ -133,6 +136,7 @@ pub use observed_token::query_observed_tokens_list;
pub use observed_token::query_observed_tokens_upsert;
pub use onchain_observation::query_onchain_observations_insert;
pub use onchain_observation::query_onchain_observations_list_recent;
pub use orderbook_event::query_orderbook_events_upsert;
pub use pair::query_pairs_get_by_pool_id;
pub use pair::query_pairs_list;
pub use pair::query_pairs_update_symbol;

View File

@@ -191,6 +191,185 @@ WHERE transaction_id = ?
}
}
/// Deletes Raydium CLMM instruction-audit rows for locally mapped CLMM instructions.
///
/// The CLMM specialized decoder now emits named `raydium_clmm.*` rows for all
/// locally mapped instruction discriminants. Keeping the former
/// `raydium_clmm.instruction_audit` rows for the same discriminants creates
/// duplicate coverage and makes residual audit diagnostics noisy. Unknown
/// discriminants remain untouched because they are intentionally absent from
/// this allow-list.
pub async fn query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits(
database: &crate::Database,
transaction_id: std::option::Option<i64>,
) -> Result<u64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query(
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE protocol_name = 'raydium_clmm'
AND event_kind = 'raydium_clmm.instruction_audit'
AND (? IS NULL OR transaction_id = ?)
AND COALESCE(
json_extract(payload_json, '$.discriminatorHex'),
json_extract(payload_json, '$.discriminator_hex'),
json_extract(payload_json, '$.instructionDiscriminatorHex'),
json_extract(payload_json, '$.instruction_discriminator_hex')
) IN (
'4c7c800fd55725fa',
'9d20dab7471d1293',
'b19059ecfaba7d63',
'759d3c674231a300',
'7b86510031446262',
'c975989055556cb2',
'a78a4e95dfc2067e',
'8888fcddc2427e59',
'12eda6c52210d590',
'8934edd4d7756c68',
'2b44d4a7592fa401',
'bd0eb5785576e33e',
'3f5794216d230868',
'e992d18ecf6840bc',
'11fb415c88f20ea9',
'a026d06f685b2c01',
'3a7fbc3e4f52c460',
'2e9cf3760dcdfbb2',
'851d59df45eeb00a',
'5f87c0c4f281e644',
'87802f4d0f98f031',
'4db84ad67056f1c7',
'4dffae527d1dc92e',
'7034a74b20c9d389',
'cd4e74215c691a60',
'f8c69e91e17587c8',
'457d73daf5baf2c4',
'2b04ed0b1ac91e62',
'07160c53f22b3079',
'313cae889a1c74c8',
'7f467728bce33d07',
'82576c062ee0757b',
'a3ace0340b9a6adf'
)
"#,
)
.bind(transaction_id)
.bind(transaction_id)
.execute(pool)
.await;
match query_result {
Ok(result) => return Ok(result.rows_affected()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete mapped Raydium CLMM instruction audit events on sqlite: {}",
error
)));
},
}
},
}
}
/// Deletes upstream registry instruction-match rows already covered by specialized local decoders.
///
/// The upstream registry fallback is useful only while an instruction is not yet
/// handled by a protocol-specific decoder. Once `k_sol_dex_event_coverage_entries`
/// has a non-empty `local_event_kind` for the same decoder/entry/discriminator,
/// the fallback row is redundant and must be removed so coverage queries do not
/// report both `upstream_git.instruction_match` and the local `protocol.*` event.
pub async fn query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches(
database: &crate::Database,
upstream_decoder_code: std::option::Option<&str>,
) -> Result<u64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let unlink_result = sqlx::query(
r#"
UPDATE k_sol_instruction_observations
SET decoded_event_id = NULL
WHERE decoded_event_id IN (
SELECT de.id
FROM k_sol_dex_decoded_events de
WHERE de.protocol_name = 'upstream_git'
AND de.event_kind = 'upstream_git.instruction_match'
AND (? IS NULL OR json_extract(de.payload_json, '$.upstreamDecoderCode') = ?)
AND (
COALESCE(json_extract(de.payload_json, '$.upstreamDecoderCode'), '')
|| '|'
|| COALESCE(json_extract(de.payload_json, '$.upstreamEntryName'), '')
|| '|'
|| COALESCE(json_extract(de.payload_json, '$.upstreamDiscriminatorHex'), '')
) IN (
SELECT
ce.decoder_code
|| '|'
|| ce.entry_name
|| '|'
|| ce.discriminator_hex
FROM k_sol_dex_event_coverage_entries ce
WHERE ce.local_event_kind IS NOT NULL
AND ce.local_event_kind <> ''
AND ce.discriminator_hex IS NOT NULL
AND ce.discriminator_hex <> ''
)
)
"#,
)
.bind(upstream_decoder_code)
.bind(upstream_decoder_code)
.execute(pool)
.await;
if let Err(error) = unlink_result {
return Err(crate::Error::Db(format!(
"cannot unlink locally covered upstream instruction matches from instruction observations on sqlite: {}",
error
)));
}
let query_result = sqlx::query(
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE protocol_name = 'upstream_git'
AND event_kind = 'upstream_git.instruction_match'
AND (? IS NULL OR json_extract(payload_json, '$.upstreamDecoderCode') = ?)
AND (
COALESCE(json_extract(payload_json, '$.upstreamDecoderCode'), '')
|| '|'
|| COALESCE(json_extract(payload_json, '$.upstreamEntryName'), '')
|| '|'
|| COALESCE(json_extract(payload_json, '$.upstreamDiscriminatorHex'), '')
) IN (
SELECT
ce.decoder_code
|| '|'
|| ce.entry_name
|| '|'
|| ce.discriminator_hex
FROM k_sol_dex_event_coverage_entries ce
WHERE ce.local_event_kind IS NOT NULL
AND ce.local_event_kind <> ''
AND ce.discriminator_hex IS NOT NULL
AND ce.discriminator_hex <> ''
)
"#,
)
.bind(upstream_decoder_code)
.bind(upstream_decoder_code)
.execute(pool)
.await;
match query_result {
Ok(result) => return Ok(result.rows_affected()),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete locally covered upstream instruction matches on sqlite: {}",
error
)));
},
}
},
}
}
/// Deletes Meteora DLMM Anchor self-CPI swap audit rows already covered by decoded swaps.
///
/// This targets only local-corpus-observed Anchor event discriminators that are

View File

@@ -569,6 +569,41 @@ SET
)
)
)
WHEN expected_db_target = 'k_sol_orderbook_events' THEN (
SELECT COUNT(oe.id)
FROM k_sol_dex_decoded_events de
JOIN k_sol_orderbook_events oe ON oe.decoded_event_id = de.id
WHERE (
(k_sol_dex_event_coverage_entries.program_id IS NULL OR de.program_id = k_sol_dex_event_coverage_entries.program_id)
AND (
(
k_sol_dex_event_coverage_entries.local_event_kind IS NOT NULL
AND k_sol_dex_event_coverage_entries.local_event_kind <> ''
AND de.event_kind = k_sol_dex_event_coverage_entries.local_event_kind
)
OR (
k_sol_dex_event_coverage_entries.entry_name IS NOT NULL
AND (
json_extract(de.payload_json, '$.upstreamEntryName') = k_sol_dex_event_coverage_entries.entry_name
OR json_extract(de.payload_json, '$.upstreamInstructionName') = k_sol_dex_event_coverage_entries.entry_name
OR json_extract(de.payload_json, '$.upstreamEventName') = k_sol_dex_event_coverage_entries.entry_name
OR json_extract(de.payload_json, '$.entryName') = k_sol_dex_event_coverage_entries.entry_name
)
)
OR (
k_sol_dex_event_coverage_entries.discriminator_hex IS NOT NULL
AND k_sol_dex_event_coverage_entries.discriminator_hex <> ''
AND (
json_extract(de.payload_json, '$.upstreamDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
OR json_extract(de.payload_json, '$.instructionDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
OR json_extract(de.payload_json, '$.anchorEventDiscriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
OR json_extract(de.payload_json, '$.anchorEventDiscriminator') = k_sol_dex_event_coverage_entries.discriminator_hex
OR json_extract(de.payload_json, '$.discriminatorHex') = k_sol_dex_event_coverage_entries.discriminator_hex
)
)
)
)
)
ELSE materialized_count
END,
first_signature = (

View File

@@ -0,0 +1,212 @@
// file: kb_lib/src/db/queries/orderbook_event.rs
//! Queries for `k_sol_orderbook_events`.
/// Inserts or updates one normalized orderbook event row.
pub async fn query_orderbook_events_upsert(
database: &crate::Database,
dto: &crate::OrderbookEventDto,
) -> Result<i64, crate::Error> {
let slot_i64 = match dto.slot {
Some(slot) => {
let slot_result = i64::try_from(slot);
match slot_result {
Ok(slot) => Some(slot),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot convert orderbook event slot '{}' to i64: {}",
slot, error
)));
},
}
},
None => None,
};
let zero_for_one_i64 = match dto.zero_for_one {
Some(true) => Some(1_i64),
Some(false) => Some(0_i64),
None => None,
};
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let existing_id = match dto.decoded_event_id {
Some(decoded_event_id) => {
let existing_result = sqlx::query_scalar::<sqlx::Sqlite, i64>(
r#"
SELECT id
FROM k_sol_orderbook_events
WHERE decoded_event_id = ?
LIMIT 1
"#,
)
.bind(decoded_event_id)
.fetch_optional(pool)
.await;
match existing_result {
Ok(existing_id) => existing_id,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot fetch k_sol_orderbook_events id for decoded_event_id '{}' on sqlite: {}",
decoded_event_id, error
)));
},
}
},
None => None,
};
if let Some(id) = existing_id {
let update_result = sqlx::query(
r#"
UPDATE k_sol_orderbook_events
SET
transaction_id = ?,
dex_id = ?,
pool_id = ?,
pair_id = ?,
signature = ?,
slot = ?,
protocol_name = ?,
program_id = ?,
event_kind = ?,
order_action = ?,
pool_account = ?,
market_account = ?,
actor_wallet = ?,
order_account = ?,
base_token_mint = ?,
quote_token_mint = ?,
amount_raw = ?,
amount_min_raw = ?,
tick_index = ?,
zero_for_one = ?,
payload_json = ?,
executed_at = ?
WHERE id = ?
"#,
)
.bind(dto.transaction_id)
.bind(dto.dex_id)
.bind(dto.pool_id)
.bind(dto.pair_id)
.bind(dto.signature.clone())
.bind(slot_i64)
.bind(dto.protocol_name.clone())
.bind(dto.program_id.clone())
.bind(dto.event_kind.clone())
.bind(dto.order_action.clone())
.bind(dto.pool_account.clone())
.bind(dto.market_account.clone())
.bind(dto.actor_wallet.clone())
.bind(dto.order_account.clone())
.bind(dto.base_token_mint.clone())
.bind(dto.quote_token_mint.clone())
.bind(dto.amount_raw.clone())
.bind(dto.amount_min_raw.clone())
.bind(dto.tick_index)
.bind(zero_for_one_i64)
.bind(dto.payload_json.clone())
.bind(dto.executed_at.to_rfc3339())
.bind(id)
.execute(pool)
.await;
if let Err(error) = update_result {
return Err(crate::Error::Db(format!(
"cannot update k_sol_orderbook_events id '{}' on sqlite: {}",
id, error
)));
}
return Ok(id);
}
let insert_result = sqlx::query(
r#"
INSERT INTO k_sol_orderbook_events (
transaction_id,
decoded_event_id,
dex_id,
pool_id,
pair_id,
signature,
slot,
protocol_name,
program_id,
event_kind,
order_action,
pool_account,
market_account,
actor_wallet,
order_account,
base_token_mint,
quote_token_mint,
amount_raw,
amount_min_raw,
tick_index,
zero_for_one,
payload_json,
executed_at,
created_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
"#,
)
.bind(dto.transaction_id)
.bind(dto.decoded_event_id)
.bind(dto.dex_id)
.bind(dto.pool_id)
.bind(dto.pair_id)
.bind(dto.signature.clone())
.bind(slot_i64)
.bind(dto.protocol_name.clone())
.bind(dto.program_id.clone())
.bind(dto.event_kind.clone())
.bind(dto.order_action.clone())
.bind(dto.pool_account.clone())
.bind(dto.market_account.clone())
.bind(dto.actor_wallet.clone())
.bind(dto.order_account.clone())
.bind(dto.base_token_mint.clone())
.bind(dto.quote_token_mint.clone())
.bind(dto.amount_raw.clone())
.bind(dto.amount_min_raw.clone())
.bind(dto.tick_index)
.bind(zero_for_one_i64)
.bind(dto.payload_json.clone())
.bind(dto.executed_at.to_rfc3339())
.bind(dto.created_at.to_rfc3339())
.execute(pool)
.await;
if let Err(error) = insert_result {
return Err(crate::Error::Db(format!(
"cannot insert k_sol_orderbook_events on sqlite: {}",
error
)));
}
let id_result = sqlx::query_scalar::<sqlx::Sqlite, i64>(
r#"
SELECT id
FROM k_sol_orderbook_events
WHERE transaction_id = ?
AND protocol_name = ?
AND event_kind = ?
AND signature = ?
ORDER BY id DESC
LIMIT 1
"#,
)
.bind(dto.transaction_id)
.bind(dto.protocol_name.clone())
.bind(dto.event_kind.clone())
.bind(dto.signature.clone())
.fetch_one(pool)
.await;
match id_result {
Ok(id) => return Ok(id),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot fetch inserted k_sol_orderbook_events id for signature '{}' on sqlite: {}",
dto.signature, error
)));
},
}
},
}
}

View File

@@ -16,7 +16,67 @@ pub async fn query_trade_market_materialization_delete_all(
("k_sol_pair_analytic_signals", "DELETE FROM k_sol_pair_analytic_signals"),
("k_sol_pair_candles", "DELETE FROM k_sol_pair_candles"),
("k_sol_pair_metrics", "DELETE FROM k_sol_pair_metrics"),
("k_sol_orderbook_events", "DELETE FROM k_sol_orderbook_events"),
("k_sol_trade_events", "DELETE FROM k_sol_trade_events"),
(
"locally_covered_upstream_instruction_observation_links",
r#"
UPDATE k_sol_instruction_observations
SET decoded_event_id = NULL
WHERE decoded_event_id IN (
SELECT de.id
FROM k_sol_dex_decoded_events de
WHERE de.protocol_name = 'upstream_git'
AND de.event_kind = 'upstream_git.instruction_match'
AND (
COALESCE(json_extract(de.payload_json, '$.upstreamDecoderCode'), '')
|| '|'
|| COALESCE(json_extract(de.payload_json, '$.upstreamEntryName'), '')
|| '|'
|| COALESCE(json_extract(de.payload_json, '$.upstreamDiscriminatorHex'), '')
) IN (
SELECT
ce.decoder_code
|| '|'
|| ce.entry_name
|| '|'
|| ce.discriminator_hex
FROM k_sol_dex_event_coverage_entries ce
WHERE ce.local_event_kind IS NOT NULL
AND ce.local_event_kind <> ''
AND ce.discriminator_hex IS NOT NULL
AND ce.discriminator_hex <> ''
)
)
"#,
),
(
"locally_covered_upstream_instruction_matches",
r#"
DELETE FROM k_sol_dex_decoded_events
WHERE protocol_name = 'upstream_git'
AND event_kind = 'upstream_git.instruction_match'
AND (
COALESCE(json_extract(payload_json, '$.upstreamDecoderCode'), '')
|| '|'
|| COALESCE(json_extract(payload_json, '$.upstreamEntryName'), '')
|| '|'
|| COALESCE(json_extract(payload_json, '$.upstreamDiscriminatorHex'), '')
) IN (
SELECT
ce.decoder_code
|| '|'
|| ce.entry_name
|| '|'
|| ce.discriminator_hex
FROM k_sol_dex_event_coverage_entries ce
WHERE ce.local_event_kind IS NOT NULL
AND ce.local_event_kind <> ''
AND ce.discriminator_hex IS NOT NULL
AND ce.discriminator_hex <> ''
)
"#,
),
];
let mut deleted_count = 0_u64;
for (table_name, statement) in statements {

View File

@@ -374,6 +374,22 @@ pub(crate) async fn ensure_schema(database: &crate::Database) -> Result<(), crat
if let Err(error) = result {
return Err(error);
}
let result = create_tbl_orderbook_events(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_orderbook_events_transaction_id(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_orderbook_events_pool_id(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_uix_orderbook_events_decoded_event_id(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_tbl_launch_surfaces(pool).await;
if let Err(error) = result {
return Err(error);
@@ -2710,3 +2726,85 @@ WHERE decoded_event_id IS NOT NULL
)
.await;
}
/// Creates `k_sol_orderbook_events`.
async fn create_tbl_orderbook_events(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_tbl_orderbook_events",
r#"
CREATE TABLE IF NOT EXISTS k_sol_orderbook_events (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
transaction_id INTEGER NOT NULL,
decoded_event_id INTEGER NULL,
dex_id INTEGER NULL,
pool_id INTEGER NULL,
pair_id INTEGER NULL,
signature TEXT NOT NULL,
slot INTEGER NULL,
protocol_name TEXT NOT NULL,
program_id TEXT NOT NULL,
event_kind TEXT NOT NULL,
order_action TEXT NOT NULL,
pool_account TEXT NULL,
market_account TEXT NULL,
actor_wallet TEXT NULL,
order_account TEXT NULL,
base_token_mint TEXT NULL,
quote_token_mint TEXT NULL,
amount_raw TEXT NULL,
amount_min_raw TEXT NULL,
tick_index INTEGER NULL,
zero_for_one INTEGER NULL,
payload_json TEXT NOT NULL,
executed_at TEXT NOT NULL,
created_at TEXT NOT NULL
)
"#,
)
.await;
}
/// Creates index on `k_sol_orderbook_events(transaction_id)`.
async fn create_idx_orderbook_events_transaction_id(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_orderbook_events_transaction_id",
r#"
CREATE INDEX IF NOT EXISTS idx_orderbook_events_transaction_id
ON k_sol_orderbook_events (transaction_id)
"#,
)
.await;
}
/// Creates index on `k_sol_orderbook_events(pool_id)`.
async fn create_idx_orderbook_events_pool_id(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_orderbook_events_pool_id",
r#"
CREATE INDEX IF NOT EXISTS idx_orderbook_events_pool_id
ON k_sol_orderbook_events (pool_id)
"#,
)
.await;
}
/// Creates partial unique index on `k_sol_orderbook_events(decoded_event_id)`.
async fn create_uix_orderbook_events_decoded_event_id(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_uix_orderbook_events_decoded_event_id",
r#"
CREATE UNIQUE INDEX IF NOT EXISTS uix_orderbook_events_decoded_event_id
ON k_sol_orderbook_events (decoded_event_id)
WHERE decoded_event_id IS NOT NULL
"#,
)
.await;
}

View File

@@ -70,12 +70,16 @@ pub use raydium_amm_v4::RaydiumAmmV4DecodedEvent;
pub use raydium_amm_v4::RaydiumAmmV4Decoder;
pub use raydium_amm_v4::RaydiumAmmV4Initialize2PoolDecoded;
pub use raydium_amm_v4::RaydiumAmmV4SwapDecoded;
pub use raydium_clmm::RaydiumClmmCollectProtocolFeeDecoded;
pub use raydium_clmm::RaydiumClmmCreatePoolDecoded;
pub use raydium_clmm::RaydiumClmmDecodedEvent;
pub use raydium_clmm::RaydiumClmmDecodedInstructionEvent;
pub use raydium_clmm::RaydiumClmmProgramDataEventDecoded;
pub use raydium_clmm::RaydiumClmmDecoder;
pub use raydium_clmm::RaydiumClmmSwapLegacyDecoded;
pub use raydium_clmm::RaydiumClmmSwapV2Decoded;
pub use raydium_clmm::decode_raydium_clmm_instruction;
pub use raydium_clmm::decode_raydium_clmm_program_data_event;
pub use raydium_cpmm::RaydiumCpmmDecodedEvent;
pub use raydium_cpmm::RaydiumCpmmLpChangeEventDecoded;
pub use raydium_cpmm::RaydiumCpmmSwapDecoded;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -148,7 +148,7 @@ fn prepare_payload_for_transaction_status(
transaction: &crate::ChainTransactionDto,
payload_json: serde_json::Value,
) -> serde_json::Value {
if transaction.err_json.is_none() {
if !transaction_has_effective_error(transaction) {
return payload_json;
}
let mut object = match payload_json {
@@ -177,3 +177,17 @@ fn prepare_payload_for_transaction_status(
);
return serde_json::Value::Object(object);
}
fn transaction_has_effective_error(transaction: &crate::ChainTransactionDto) -> bool {
let err_json = match transaction.err_json.as_ref() {
Some(err_json) => err_json.trim(),
None => return false,
};
if err_json.is_empty() {
return false;
}
if err_json == "null" {
return false;
}
return true;
}

View File

@@ -238,9 +238,15 @@ pub fn classify_dex_event_actionability(
if trade_candidate {
return DexEventActionability::TradeCandidate;
}
if is_dex_informational_event_kind(event_kind) {
return DexEventActionability::Informational;
}
if is_dex_trade_event_kind(event_kind) {
return DexEventActionability::NonActionableTrade;
}
if is_dex_orderbook_event_kind(event_kind) {
return DexEventActionability::NonTradeUseful;
}
let category = classify_dex_event_category(event_kind);
match category {
DexEventCategory::Liquidity => return DexEventActionability::NonTradeUseful,
@@ -323,6 +329,12 @@ pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".lp_change_event") {
return true;
}
if event_kind.contains(".liquidity_change_event") {
return true;
}
if event_kind.contains(".liquidity_calculate_event") {
return true;
}
if event_kind.contains(".withdraw") {
return true;
}
@@ -341,6 +353,9 @@ pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".initialize_position") {
return true;
}
if event_kind.contains(".create_personal_position_event") {
return true;
}
if event_kind.contains(".open_position") {
return true;
}
@@ -440,6 +455,38 @@ pub fn is_dex_reward_event_kind(event_kind: &str) -> bool {
return false;
}
/// Returns true for orderbook or limit-order events that must not become candles.
pub fn is_dex_orderbook_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".order_place") {
return true;
}
if event_kind.contains(".order_cancel") {
return true;
}
if event_kind.contains(".order_fill") {
return true;
}
if event_kind.contains(".settle_funds") {
return true;
}
if event_kind.contains(".open_limit_order") {
return true;
}
if event_kind.contains(".increase_limit_order") {
return true;
}
if event_kind.contains(".decrease_limit_order") {
return true;
}
if event_kind.contains(".close_limit_order") {
return true;
}
if event_kind.contains(".settle_limit_order") {
return true;
}
return false;
}
/// Returns true for pool, pair, launch, mint, burn or migration lifecycle events.
pub fn is_dex_pool_lifecycle_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".create_lock_escrow") {
@@ -539,6 +586,9 @@ pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".create_pool") {
return true;
}
if event_kind.contains(".pool_created_event") {
return true;
}
if event_kind.contains(".create_amm") {
return true;
}

View File

@@ -214,6 +214,9 @@ fn infer_expected_db_target_for_entry(
if decoder_code == "raydium_cpmm" && entry_name == "swap_event" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
}
if decoder_code == "raydium_clmm" && entry_name == "initialize_reward" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_REWARD_EVENTS.to_string());
}
return infer_expected_db_target(event_family, entry_kind);
}
@@ -238,8 +241,8 @@ fn infer_expected_db_target(
"liquidity" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
"liquidity_add" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
"liquidity_remove" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
"position_open" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
"position_close" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
"position_open" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
"position_close" => crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS,
"fee" => crate::DexEventCoverageEntryDto::DB_TARGET_FEE_EVENTS,
"reward" => crate::DexEventCoverageEntryDto::DB_TARGET_REWARD_EVENTS,
"admin_config" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_ADMIN_EVENTS,
@@ -283,6 +286,7 @@ fn infer_event_family(
return Some("swap".to_string());
}
if contains_any(normalized.as_str(), &["create_pool", "initialize_pool", "initialize2"])
|| normalized == "create_customizable_pool"
|| normalized == "initialize"
|| normalized.starts_with("initialize_")
{
@@ -306,11 +310,13 @@ fn infer_event_family(
}
if contains_any(normalized.as_str(), &["close_position", "position_close"])
|| normalized.contains("close_position_if_empty")
|| normalized == "close_protocol_position"
{
return Some("position_close".to_string());
}
if contains_any(normalized.as_str(), &["fee", "collect", "claim_fee"])
&& !normalized.contains("reward")
&& !normalized.contains("config")
{
return Some("fee".to_string());
}
@@ -323,6 +329,9 @@ fn infer_event_family(
) {
return Some("admin_config".to_string());
}
if normalized == "create_support_mint_associated" {
return Some("account_create".to_string());
}
if normalized.contains("mint") {
return Some("mint".to_string());
}
@@ -335,21 +344,24 @@ fn infer_event_family(
if contains_any(normalized.as_str(), &["create_ata", "init_account", "open_orders_create"]) {
return Some("account_create".to_string());
}
if contains_any(normalized.as_str(), &["close_account", "close_open_orders"])
|| normalized.starts_with("close_")
{
return Some("account_close".to_string());
}
if normalized.contains("wrap_sol") {
return Some("wrap_sol".to_string());
}
if normalized.contains("unwrap_sol") {
return Some("unwrap_sol".to_string());
}
if normalized.contains("place_order") || normalized.contains("post_order") {
if normalized.contains("place_order")
|| normalized.contains("post_order")
|| normalized == "open_limit_order"
|| normalized == "increase_limit_order"
{
return Some("order_place".to_string());
}
if normalized.contains("cancel_order") || normalized.contains("cancel_all") {
if normalized.contains("cancel_order")
|| normalized.contains("cancel_all")
|| normalized == "close_limit_order"
|| normalized == "decrease_limit_order"
{
return Some("order_cancel".to_string());
}
if normalized.contains("fill") {
@@ -358,9 +370,14 @@ fn infer_event_family(
if normalized.contains("consume_events") {
return Some("consume_events".to_string());
}
if normalized.contains("settle_funds") {
if normalized.contains("settle_funds") || normalized == "settle_limit_order" {
return Some("settle_funds".to_string());
}
if contains_any(normalized.as_str(), &["close_account", "close_open_orders"])
|| normalized.starts_with("close_")
{
return Some("account_close".to_string());
}
if normalized.contains("vault") && normalized.contains("deposit") {
return Some("vault_deposit".to_string());
}
@@ -399,7 +416,7 @@ fn contains_any(value: &str, needles: &[&str]) -> bool {
return false;
}
fn known_local_event_kind(
pub(crate) fn known_local_event_kind(
decoder_code: &str,
entry_name: &str,
) -> std::option::Option<std::string::String> {
@@ -444,19 +461,98 @@ fn known_local_event_kind(
return Some("raydium_cpmm.update_pool_status".to_string());
},
("raydium_cpmm", "withdraw") => return Some("raydium_cpmm.withdraw".to_string()),
("raydium_clmm", "swap") => return Some("raydium_clmm.swap".to_string()),
("raydium_clmm", "swap_v2") => return Some("raydium_clmm.swap_v2".to_string()),
("raydium_clmm", "increase_liquidity_v2") => {
return Some("raydium_clmm.increase_liquidity_v2".to_string());
("raydium_clmm", "close_limit_order") => {
return Some("raydium_clmm.close_limit_order".to_string());
},
("raydium_clmm", "open_limit_order") => {
return Some("raydium_clmm.open_limit_order".to_string());
},
("raydium_clmm", "increase_limit_order") => {
return Some("raydium_clmm.increase_limit_order".to_string());
},
("raydium_clmm", "decrease_limit_order") => {
return Some("raydium_clmm.decrease_limit_order".to_string());
},
("raydium_clmm", "close_position") => {
return Some("raydium_clmm.close_position".to_string());
},
("raydium_clmm", "close_protocol_position") => {
return Some("raydium_clmm.close_protocol_position".to_string());
},
("raydium_clmm", "collect_fund_fee") => {
return Some("raydium_clmm.collect_fund_fee".to_string());
},
("raydium_clmm", "collect_protocol_fee") => {
return Some("raydium_clmm.collect_protocol_fee".to_string());
},
("raydium_clmm", "collect_remaining_rewards") => {
return Some("raydium_clmm.collect_remaining_rewards".to_string());
},
("raydium_clmm", "create_amm_config") => {
return Some("raydium_clmm.create_amm_config".to_string());
},
("raydium_clmm", "create_customizable_pool") => {
return Some("raydium_clmm.create_customizable_pool".to_string());
},
("raydium_clmm", "create_dynamic_fee_config") => {
return Some("raydium_clmm.create_dynamic_fee_config".to_string());
},
("raydium_clmm", "create_operation_account") => {
return Some("raydium_clmm.create_operation_account".to_string());
},
("raydium_clmm", "create_pool") => return Some("raydium_clmm.create_pool".to_string()),
("raydium_clmm", "create_support_mint_associated") => {
return Some("raydium_clmm.create_support_mint_associated".to_string());
},
("raydium_clmm", "decrease_liquidity") => {
return Some("raydium_clmm.decrease_liquidity".to_string());
},
("raydium_clmm", "decrease_liquidity_v2") => {
return Some("raydium_clmm.decrease_liquidity_v2".to_string());
},
("raydium_clmm", "increase_liquidity") => {
return Some("raydium_clmm.increase_liquidity".to_string());
},
("raydium_clmm", "increase_liquidity_v2") => {
return Some("raydium_clmm.increase_liquidity_v2".to_string());
},
("raydium_clmm", "initialize_reward") => {
return Some("raydium_clmm.initialize_reward".to_string());
},
("raydium_clmm", "open_position") => {
return Some("raydium_clmm.open_position".to_string());
},
("raydium_clmm", "open_position_v2") => {
return Some("raydium_clmm.open_position_v2".to_string());
},
("raydium_clmm", "open_position_with_token22_nft") => {
return Some("raydium_clmm.open_position_with_token22_nft".to_string());
},
("raydium_clmm", "close_position") => {
return Some("raydium_clmm.close_position".to_string());
("raydium_clmm", "set_reward_params") => {
return Some("raydium_clmm.set_reward_params".to_string());
},
("raydium_clmm", "settle_limit_order") => {
return Some("raydium_clmm.settle_limit_order".to_string());
},
("raydium_clmm", "swap") => return Some("raydium_clmm.swap".to_string()),
("raydium_clmm", "swap_router_base_in") => {
return Some("raydium_clmm.swap_router_base_in".to_string());
},
("raydium_clmm", "swap_v2") => return Some("raydium_clmm.swap_v2".to_string()),
("raydium_clmm", "transfer_reward_owner") => {
return Some("raydium_clmm.transfer_reward_owner".to_string());
},
("raydium_clmm", "update_amm_config") => {
return Some("raydium_clmm.update_amm_config".to_string());
},
("raydium_clmm", "update_operation_account") => {
return Some("raydium_clmm.update_operation_account".to_string());
},
("raydium_clmm", "update_pool_status") => {
return Some("raydium_clmm.update_pool_status".to_string());
},
("raydium_clmm", "update_reward_infos") => {
return Some("raydium_clmm.update_reward_infos".to_string());
},
_ => return None,
}
@@ -511,6 +607,77 @@ mod tests {
);
}
#[test]
fn event_family_inference_covers_raydium_clmm_idl_entries() {
assert_eq!(
super::infer_event_family("create_customizable_pool", crate::ENTRY_KIND_INSTRUCTION),
Some("pool_create".to_string())
);
assert_eq!(
super::infer_event_family("create_dynamic_fee_config", crate::ENTRY_KIND_INSTRUCTION),
Some("admin_config".to_string())
);
assert_eq!(
super::infer_event_family(
"create_support_mint_associated",
crate::ENTRY_KIND_INSTRUCTION,
),
Some("account_create".to_string())
);
assert_eq!(
super::infer_event_family("close_limit_order", crate::ENTRY_KIND_INSTRUCTION),
Some("order_cancel".to_string())
);
assert_eq!(
super::infer_event_family("open_limit_order", crate::ENTRY_KIND_INSTRUCTION),
Some("order_place".to_string())
);
assert_eq!(
super::infer_event_family("increase_limit_order", crate::ENTRY_KIND_INSTRUCTION),
Some("order_place".to_string())
);
assert_eq!(
super::infer_event_family("decrease_limit_order", crate::ENTRY_KIND_INSTRUCTION),
Some("order_cancel".to_string())
);
assert_eq!(
super::infer_event_family("close_protocol_position", crate::ENTRY_KIND_INSTRUCTION),
Some("position_close".to_string())
);
assert_eq!(
super::infer_event_family("settle_limit_order", crate::ENTRY_KIND_INSTRUCTION),
Some("settle_funds".to_string())
);
assert_eq!(
super::infer_expected_db_target(Some("position_open"), crate::ENTRY_KIND_INSTRUCTION),
Some(crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS.to_string())
);
assert_eq!(
super::infer_expected_db_target(Some("position_close"), crate::ENTRY_KIND_INSTRUCTION),
Some(crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS.to_string())
);
assert_eq!(
super::known_local_event_kind("raydium_clmm", "create_pool"),
Some("raydium_clmm.create_pool".to_string())
);
assert_eq!(
super::known_local_event_kind("raydium_clmm", "collect_protocol_fee"),
Some("raydium_clmm.collect_protocol_fee".to_string())
);
assert_eq!(
super::known_local_event_kind("raydium_clmm", "open_limit_order"),
Some("raydium_clmm.open_limit_order".to_string())
);
assert_eq!(
super::known_local_event_kind("raydium_clmm", "increase_limit_order"),
Some("raydium_clmm.increase_limit_order".to_string())
);
assert_eq!(
super::known_local_event_kind("raydium_clmm", "decrease_limit_order"),
Some("raydium_clmm.decrease_limit_order".to_string())
);
}
#[tokio::test]
async fn sync_upstream_registry_persists_raydium_cpmm_coverage_rows() {
let database = make_database().await;

View File

@@ -60,6 +60,19 @@ impl InstructionObservationIndexService {
return self.upsert_source_rows(rows).await;
}
/// Refreshes observations for the same transaction window used by local replay.
pub async fn refresh_replay_window(
&self,
limit: std::option::Option<i64>,
) -> Result<crate::InstructionObservationIndexRefreshResult, crate::Error> {
let rows_result = self.list_replay_window_source_rows(limit).await;
let rows = match rows_result {
Ok(rows) => rows,
Err(error) => return Err(error),
};
return self.upsert_source_rows(rows).await;
}
/// Refreshes observations for recently persisted instructions.
pub async fn refresh_recent(
&self,
@@ -145,6 +158,73 @@ ORDER BY ins.instruction_index ASC, ins.inner_instruction_index ASC, ins.id ASC
}
}
async fn list_replay_window_source_rows(
&self,
limit: std::option::Option<i64>,
) -> Result<std::vec::Vec<InstructionObservationSourceRow>, crate::Error> {
let effective_limit = match limit {
Some(limit) => {
if limit <= 0 {
10_000
} else {
limit
}
},
None => 10_000,
};
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, InstructionObservationSourceRow>(
r#"
WITH replay_transactions AS (
SELECT id
FROM k_sol_chain_transactions
ORDER BY id ASC
LIMIT ?
)
SELECT
tx.id AS transaction_id,
tx.signature AS signature,
tx.slot AS slot,
tx.block_time_unix AS block_time,
tx.err_json AS err_json,
ins.id AS instruction_id,
ins.parent_instruction_id AS parent_instruction_id,
ins.instruction_index AS instruction_index,
ins.inner_instruction_index AS inner_instruction_index,
ins.program_id AS program_id,
ins.accounts_json AS accounts_json,
ins.data_json AS data_json,
de.pool_account AS pool_account,
de.event_kind AS decoded_event_kind,
de.id AS decoded_event_id
FROM k_sol_chain_instructions ins
JOIN replay_transactions replay_tx
ON replay_tx.id = ins.transaction_id
JOIN k_sol_chain_transactions tx
ON tx.id = ins.transaction_id
LEFT JOIN k_sol_dex_decoded_events de
ON de.transaction_id = tx.id
AND de.instruction_id = ins.id
ORDER BY tx.id ASC, ins.instruction_index ASC, ins.inner_instruction_index ASC, ins.id ASC
"#,
)
.bind(effective_limit)
.fetch_all(pool)
.await;
match query_result {
Ok(rows) => return Ok(rows),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list instruction observation source rows for replay window: {}",
error
)));
},
}
},
}
}
async fn list_recent_source_rows(
&self,
limit: u32,
@@ -280,6 +360,45 @@ fn resolve_instruction_name(
};
return Some(name.to_string());
}
if program_id == crate::RAYDIUM_CLMM_PROGRAM_ID || decoder_code == Some("raydium_clmm") {
let name = match discriminator_hex {
"4c7c800fd55725fa" => "raydium_clmm.close_limit_order",
"9d20dab7471d1293" => "raydium_clmm.open_limit_order",
"b19059ecfaba7d63" => "raydium_clmm.increase_limit_order",
"759d3c674231a300" => "raydium_clmm.decrease_limit_order",
"7b86510031446262" => "raydium_clmm.close_position",
"c975989055556cb2" => "raydium_clmm.close_protocol_position",
"a78a4e95dfc2067e" => "raydium_clmm.collect_fund_fee",
"8888fcddc2427e59" => "raydium_clmm.collect_protocol_fee",
"12eda6c52210d590" => "raydium_clmm.collect_remaining_rewards",
"8934edd4d7756c68" => "raydium_clmm.create_amm_config",
"2b44d4a7592fa401" => "raydium_clmm.create_customizable_pool",
"bd0eb5785576e33e" => "raydium_clmm.create_dynamic_fee_config",
"3f5794216d230868" => "raydium_clmm.create_operation_account",
"e992d18ecf6840bc" => "raydium_clmm.create_pool",
"11fb415c88f20ea9" => "raydium_clmm.create_support_mint_associated",
"a026d06f685b2c01" => "raydium_clmm.decrease_liquidity",
"3a7fbc3e4f52c460" => "raydium_clmm.decrease_liquidity_v2",
"2e9cf3760dcdfbb2" => "raydium_clmm.increase_liquidity",
"851d59df45eeb00a" => "raydium_clmm.increase_liquidity_v2",
"5f87c0c4f281e644" => "raydium_clmm.initialize_reward",
"87802f4d0f98f031" => "raydium_clmm.open_position",
"4db84ad67056f1c7" => "raydium_clmm.open_position_v2",
"4dffae527d1dc92e" => "raydium_clmm.open_position_with_token22_nft",
"7034a74b20c9d389" => "raydium_clmm.set_reward_params",
"cd4e74215c691a60" => "raydium_clmm.settle_limit_order",
"f8c69e91e17587c8" => "raydium_clmm.swap",
"457d73daf5baf2c4" => "raydium_clmm.swap_router_base_in",
"2b04ed0b1ac91e62" => "raydium_clmm.swap_v2",
"07160c53f22b3079" => "raydium_clmm.transfer_reward_owner",
"313cae889a1c74c8" => "raydium_clmm.update_amm_config",
"7f467728bce33d07" => "raydium_clmm.update_operation_account",
"82576c062ee0757b" => "raydium_clmm.update_pool_status",
"a3ace0340b9a6adf" => "raydium_clmm.update_reward_infos",
_ => return None,
};
return Some(name.to_string());
}
return None;
}

View File

@@ -585,6 +585,9 @@ pub use db::ObservedTokenStatus;
pub use db::OnchainObservationDto;
/// Persisted on-chain observation row.
pub use db::OnchainObservationEntity;
/// Application-facing normalized orderbook or limit-order event DTO.
pub use db::OrderbookEventDto;
pub use db::OrderbookEventEntity;
/// Application-facing pair-analytic-signal DTO.
pub use db::PairAnalyticSignalDto;
/// Persisted pair-analytic-signal row.
@@ -737,10 +740,13 @@ pub use db::query_dex_decode_replay_ledger_get_by_transaction;
pub use db::query_dex_decode_replay_ledger_upsert;
/// Deletes one decoded DEX event row by its natural key.
pub use db::query_dex_decoded_events_delete_by_key;
/// Deletes upstream registry instruction-match rows already covered by specialized local decoders.
pub use db::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches;
/// Deletes Meteora DLMM Anchor self-CPI swap audit rows already covered by decoded swaps.
pub use db::query_dex_decoded_events_delete_meteora_dlmm_anchor_swap_instruction_audits;
/// Deletes decoded DEX instruction audit rows related to one decoded instruction.
pub use db::query_dex_decoded_events_delete_related_instruction_audit;
pub use db::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits;
/// Reads one decoded DEX event by its natural key.
pub use db::query_dex_decoded_events_get_by_key;
/// Returns the latest Pump.fun create payload associated with a token mint.
@@ -864,6 +870,8 @@ pub use db::query_observed_tokens_upsert;
pub use db::query_onchain_observations_insert;
/// Lists recent on-chain observations ordered from newest to oldest.
pub use db::query_onchain_observations_list_recent;
/// Inserts or updates one normalized orderbook event row.
pub use db::query_orderbook_events_upsert;
/// Returns one pair-analytic-signal row identified by its key, if it exists.
pub use db::query_pair_analytic_signals_get_by_key;
/// Lists all pair-analytic signals for one pair ordered by key.
@@ -1138,12 +1146,18 @@ pub use dex::RaydiumAmmV4Decoder;
pub use dex::RaydiumAmmV4Initialize2PoolDecoded;
/// Decoded Raydium AMM v4 swap event.
pub use dex::RaydiumAmmV4SwapDecoded;
/// Decoded Raydium CLMM collect_protocol_fee instruction.
pub use dex::RaydiumClmmCollectProtocolFeeDecoded;
/// Decoded Raydium CLMM create_pool instruction.
pub use dex::RaydiumClmmCreatePoolDecoded;
/// Decoded Raydium CLMM event.
pub use dex::RaydiumClmmDecodedEvent;
/// Decoded Raydium CLMM instruction event with projected instruction id.
pub use dex::RaydiumClmmDecodedInstructionEvent;
/// Raydium CLMM transaction decoder.
pub use dex::RaydiumClmmDecoder;
/// Decoded Raydium CLMM Anchor Program data event payload.
pub use dex::RaydiumClmmProgramDataEventDecoded;
/// Decoded Raydium CLMM legacy swap event.
pub use dex::RaydiumClmmSwapLegacyDecoded;
/// Decoded Raydium CLMM swap_v2 instruction.
@@ -1162,6 +1176,8 @@ pub use dex::RaydiumCpmmSwapMode;
pub use dex::classify_raydium_cpmm_instruction_data;
/// Decodes a Raydium CLMM instruction.
pub use dex::decode_raydium_clmm_instruction;
/// Decodes one Raydium CLMM Anchor Program data event.
pub use dex::decode_raydium_clmm_program_data_event;
/// Decodes one Raydium CPMM instruction from projected instruction fields.
pub use dex::decode_raydium_cpmm_instruction;
/// Decodes Raydium CPMM Anchor events emitted in `Program data:` logs.
@@ -1217,6 +1233,8 @@ pub use dex_event_classification::is_dex_liquidity_event_kind;
pub use dex_event_classification::is_dex_liquidity_remove_event_kind;
/// Returns true for migration DEX events.
pub use dex_event_classification::is_dex_migration_event_kind;
/// Returns true for orderbook or limit-order events that must not become candles.
pub use dex_event_classification::is_dex_orderbook_event_kind;
/// Returns true for pair creation DEX events.
pub use dex_event_classification::is_dex_pair_creation_event_kind;
/// Returns true for pool creation DEX events.
@@ -1408,6 +1426,10 @@ pub use solana_pubsub_ws::parse_solana_ws_typed_notification_from_event;
pub use token_backfill::PoolBackfillResult;
/// One signature-backfill result summary.
pub use token_backfill::SignatureBackfillResult;
/// One item produced by a batch signature backfill.
pub use token_backfill::SignatureBatchBackfillItemResult;
/// Batch signature-backfill result summary.
pub use token_backfill::SignatureBatchBackfillResult;
/// One token-backfill result summary.
pub use token_backfill::TokenBackfillResult;
/// Historical token backfill service.

View File

@@ -13,6 +13,10 @@ fn default_skip_certified_dex_decode() -> bool {
return true;
}
fn default_defer_instruction_observation_index_refresh() -> bool {
return true;
}
/// Configuration for a local pipeline replay pass.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -31,6 +35,9 @@ pub struct LocalPipelineReplayConfig {
/// Whether DEX decoding must run even when the replay ledger certifies a safe prior pass.
#[serde(default)]
pub force_decode_replay: bool,
/// Whether instruction observation indexing is deferred and refreshed once after replay.
#[serde(default = "default_defer_instruction_observation_index_refresh")]
pub defer_instruction_observation_index_refresh: bool,
}
impl Default for LocalPipelineReplayConfig {
@@ -42,6 +49,7 @@ impl Default for LocalPipelineReplayConfig {
reset_market_materialization_before_replay: true,
skip_certified_dex_decode: true,
force_decode_replay: false,
defer_instruction_observation_index_refresh: true,
};
}
}
@@ -90,6 +98,8 @@ pub struct LocalPipelineReplayResult {
pub reward_event_count: usize,
/// Total pool administration event materialization results returned by replayed non-trade calls.
pub pool_admin_event_count: usize,
/// Total orderbook event materialization results returned by replayed non-trade calls.
pub orderbook_event_count: usize,
/// Total candle upsert results returned by replayed candle calls.
///
/// This is a replay write/result counter, not the number of distinct rows
@@ -111,6 +121,10 @@ pub struct LocalPipelineReplayResult {
pub pair_symbol_updated_count: usize,
/// Number of derived market materialization rows deleted before replay.
pub reset_market_materialization_deleted_count: u64,
/// Total instruction source rows scanned by the observation index refresh.
pub instruction_observation_scanned_count: usize,
/// Total instruction-observation rows upserted by the observation index refresh.
pub instruction_observation_upserted_count: usize,
/// Number of errors outside per-signature replay.
pub global_error_count: usize,
}
@@ -352,6 +366,7 @@ impl LocalPipelineReplayService {
result.fee_event_count += non_trade_result.fee_event_count;
result.reward_event_count += non_trade_result.reward_event_count;
result.pool_admin_event_count += non_trade_result.pool_admin_event_count;
result.orderbook_event_count += non_trade_result.orderbook_event_count;
},
Err(error) => {
result.non_trade_materialization_error_count += 1;
@@ -426,25 +441,55 @@ impl LocalPipelineReplayService {
);
},
}
if !config.defer_instruction_observation_index_refresh {
let instruction_index_result =
instruction_observation_index.refresh_signature(signature.as_str()).await;
match instruction_index_result {
Ok(index_result) => {
result.instruction_observation_scanned_count +=
index_result.scanned_instruction_count;
result.instruction_observation_upserted_count +=
index_result.upserted_observation_count;
tracing::debug!(
signature = %signature,
upserted_observation_count = index_result.upserted_observation_count,
"instruction observation index refreshed during local replay"
);
},
Err(error) => {
tracing::warn!(
signature = %signature,
error = %error,
"instruction observation index refresh failed during local replay"
);
},
}
}
result.replayed_transaction_count += 1;
}
if config.defer_instruction_observation_index_refresh {
let instruction_index_result =
instruction_observation_index.refresh_signature(signature.as_str()).await;
instruction_observation_index.refresh_replay_window(config.limit).await;
match instruction_index_result {
Ok(index_result) => {
result.instruction_observation_scanned_count +=
index_result.scanned_instruction_count;
result.instruction_observation_upserted_count +=
index_result.upserted_observation_count;
tracing::debug!(
signature = %signature,
scanned_instruction_count = index_result.scanned_instruction_count,
upserted_observation_count = index_result.upserted_observation_count,
"instruction observation index refreshed during local replay"
"instruction observation index refreshed after local replay"
);
},
Err(error) => {
result.global_error_count += 1;
tracing::warn!(
signature = %signature,
error = %error,
"instruction observation index refresh failed during local replay"
"instruction observation index refresh failed after local replay"
);
},
}
result.replayed_transaction_count += 1;
}
if config.refresh_missing_token_metadata {
let metadata_service = match &self.http_pool {
@@ -476,6 +521,52 @@ impl LocalPipelineReplayService {
}
async fn refresh_event_coverage_best_effort(&self) {
let cleanup_result =
crate::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits(
self.database.as_ref(),
None,
)
.await;
match cleanup_result {
Ok(deleted_count) => {
if deleted_count > 0 {
tracing::info!(
deleted_count = deleted_count,
"replaced Raydium CLMM instruction audits cleaned before dex event coverage refresh"
);
}
},
Err(error) => {
tracing::warn!(
error = %error,
"Raydium CLMM replaced instruction-audit cleanup failed before dex event coverage refresh"
);
},
}
let upstream_cleanup_result =
crate::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches(
self.database.as_ref(),
None,
)
.await;
match upstream_cleanup_result {
Ok(deleted_count) => {
if deleted_count > 0 {
tracing::info!(
deleted_count = deleted_count,
"locally covered upstream instruction matches cleaned before dex event coverage refresh"
);
}
},
Err(error) => {
tracing::warn!(
error = %error,
"locally covered upstream instruction-match cleanup failed before dex event coverage refresh"
);
},
}
let coverage_service = crate::DexEventCoverageService::new(self.database.clone());
let refresh_result = coverage_service.refresh_local_counts(None).await;
match refresh_result {
@@ -494,6 +585,46 @@ impl LocalPipelineReplayService {
);
},
}
let post_refresh_upstream_cleanup_result =
crate::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches(
self.database.as_ref(),
None,
)
.await;
match post_refresh_upstream_cleanup_result {
Ok(deleted_count) => {
if deleted_count > 0 {
tracing::info!(
deleted_count = deleted_count,
"locally covered upstream instruction matches cleaned after dex event coverage refresh"
);
let second_refresh_result = coverage_service.refresh_local_counts(None).await;
match second_refresh_result {
Ok(second_refresh_result) => {
tracing::debug!(
upserted_entry_count = second_refresh_result.upserted_entry_count,
refreshed_entry_count = second_refresh_result.refreshed_entry_count,
summary_count = second_refresh_result.summaries.len(),
"dex event coverage refreshed after upstream instruction-match cleanup"
);
},
Err(error) => {
tracing::warn!(
error = %error,
"dex event coverage refresh failed after upstream instruction-match cleanup"
);
},
}
}
},
Err(error) => {
tracing::warn!(
error = %error,
"locally covered upstream instruction-match cleanup failed after dex event coverage refresh"
);
},
}
}
async fn get_certified_dex_decode_skip_ledger(

View File

@@ -19,6 +19,8 @@ pub struct NonTradeEventMaterializationResult {
pub reward_event_count: usize,
/// Number of pool administration events inserted or refreshed.
pub pool_admin_event_count: usize,
/// Number of orderbook or limit-order events inserted or refreshed.
pub orderbook_event_count: usize,
}
/// Materializes useful non-trade decoded DEX events.
@@ -61,7 +63,7 @@ impl NonTradeEventMaterializationService {
)));
},
};
if transaction.err_json.is_some() {
if transaction_has_effective_error(&transaction) {
tracing::debug!(
signature = %transaction.signature,
"skipping non-trade materialization for failed transaction"
@@ -189,6 +191,24 @@ impl NonTradeEventMaterializationService {
Err(error) => return Err(error),
}
}
if crate::is_dex_orderbook_event_kind(decoded_event.event_kind.as_str()) {
let materialized = self
.materialize_orderbook_event(
&transaction,
transaction_id,
decoded_event,
&payload,
)
.await;
match materialized {
Ok(was_materialized) => {
if was_materialized {
result.orderbook_event_count += 1;
}
},
Err(error) => return Err(error),
}
}
}
for decoded_event in &decoded_events {
if !decoded_event.event_kind.ends_with(".lp_change_event") {
@@ -673,6 +693,86 @@ WHERE decoded_event_id = ?
}
}
async fn materialize_orderbook_event(
&self,
transaction: &crate::ChainTransactionDto,
transaction_id: i64,
decoded_event: &crate::DexDecodedEventDto,
payload: &serde_json::Value,
) -> Result<bool, crate::Error> {
let decoded_event_id = match decoded_event.id {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(false),
};
let context = self.resolve_decoded_event_context(decoded_event).await;
let context = match context {
Ok(context) => context,
Err(error) => return Err(error),
};
let order_action = normalize_orderbook_action(decoded_event.event_kind.as_str());
let actor_wallet = extract_first_string(
payload,
&["actorWallet", "actor_wallet", "owner", "authority", "payer", "user"],
);
let order_account = match extract_first_string(
payload,
&["orderAccount", "order_account", "limitOrder", "limit_order", "order"],
) {
Some(order_account) => Some(order_account),
None => fallback_order_account(decoded_event.event_kind.as_str(), payload),
};
let amount_raw = extract_first_amount_string(
payload,
&[
"amountRaw",
"amount_raw",
"amount",
"decreasedAmountRaw",
"decreased_amount_raw",
"decreasedAmount",
"increasedAmountRaw",
"increased_amount_raw",
"increasedAmount",
],
);
let amount_min_raw = extract_first_amount_string(
payload,
&["amountMinRaw", "amount_min_raw", "amountMin", "amount_min"],
);
let tick_index = extract_first_i64(payload, &["tickIndex", "tick_index"]);
let zero_for_one = extract_first_bool(payload, &["zeroForOne", "zero_for_one"]);
let dto = crate::OrderbookEventDto::new(
transaction_id,
Some(decoded_event_id),
context.dex_id,
context.pool_id,
context.pair_id,
transaction.signature.clone(),
transaction.slot,
decoded_event.protocol_name.clone(),
decoded_event.program_id.clone(),
decoded_event.event_kind.clone(),
order_action,
decoded_event.pool_account.clone(),
decoded_event.market_account.clone(),
actor_wallet,
order_account,
decoded_event.token_a_mint.clone(),
decoded_event.token_b_mint.clone(),
amount_raw,
amount_min_raw,
tick_index,
zero_for_one,
decoded_event.payload_json.clone(),
);
let upsert_result =
crate::query_orderbook_events_upsert(self.database.as_ref(), &dto).await;
match upsert_result {
Ok(_) => return Ok(true),
Err(error) => return Err(error),
}
}
async fn ensure_liquidity_context_from_decoded_event(
&self,
decoded_event: &crate::DexDecodedEventDto,
@@ -789,6 +889,162 @@ WHERE decoded_event_id = ?
}
}
fn normalize_orderbook_action(event_kind: &str) -> std::string::String {
if event_kind.contains(".open_limit_order") {
return "open_limit_order".to_string();
}
if event_kind.contains(".increase_limit_order") {
return "increase_limit_order".to_string();
}
if event_kind.contains(".decrease_limit_order") {
return "decrease_limit_order".to_string();
}
if event_kind.contains(".close_limit_order") {
return "close_limit_order".to_string();
}
if event_kind.contains(".settle_limit_order") {
return "settle_limit_order".to_string();
}
if event_kind.contains("order_place") {
return "order_place".to_string();
}
if event_kind.contains("order_cancel") {
return "order_cancel".to_string();
}
if event_kind.contains("settle_funds") {
return "settle_funds".to_string();
}
return event_kind.to_string();
}
fn fallback_order_account(
event_kind: &str,
payload: &serde_json::Value,
) -> std::option::Option<std::string::String> {
if event_kind.contains(".close_limit_order") {
return extract_account_at(payload, 2);
}
if event_kind.contains(".open_limit_order")
|| event_kind.contains(".increase_limit_order")
|| event_kind.contains(".decrease_limit_order")
{
return extract_account_at(payload, 3);
}
return None;
}
fn extract_account_at(
value: &serde_json::Value,
index: usize,
) -> std::option::Option<std::string::String> {
if let Some(object) = value.as_object() {
let accounts = object.get("accounts");
if let Some(accounts) = accounts {
if let Some(array) = accounts.as_array() {
let candidate = array.get(index);
if let Some(candidate) = candidate {
if let Some(text) = candidate.as_str() {
let trimmed = text.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
}
}
for nested_value in object.values() {
let nested = extract_account_at(nested_value, index);
if nested.is_some() {
return nested;
}
}
}
return None;
}
fn extract_first_i64(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<i64> {
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
let candidate_value = object.get(*candidate_key);
if let Some(candidate_value) = candidate_value {
if let Some(number) = candidate_value.as_i64() {
return Some(number);
}
if let Some(number) = candidate_value.as_u64() {
let converted = i64::try_from(number);
if let Ok(converted) = converted {
return Some(converted);
}
}
if let Some(text) = candidate_value.as_str() {
let parsed = text.parse::<i64>();
if let Ok(parsed) = parsed {
return Some(parsed);
}
}
}
}
for nested_value in object.values() {
let nested = extract_first_i64(nested_value, candidate_keys);
if nested.is_some() {
return nested;
}
}
}
return None;
}
fn extract_first_bool(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<bool> {
if let Some(object) = value.as_object() {
for candidate_key in candidate_keys {
let candidate_value = object.get(*candidate_key);
if let Some(candidate_value) = candidate_value {
if let Some(flag) = candidate_value.as_bool() {
return Some(flag);
}
if let Some(number) = candidate_value.as_i64() {
return Some(number != 0);
}
if let Some(text) = candidate_value.as_str() {
if text == "true" || text == "1" {
return Some(true);
}
if text == "false" || text == "0" {
return Some(false);
}
}
}
}
for nested_value in object.values() {
let nested = extract_first_bool(nested_value, candidate_keys);
if nested.is_some() {
return nested;
}
}
}
return None;
}
fn transaction_has_effective_error(transaction: &crate::ChainTransactionDto) -> bool {
let err_json = match transaction.err_json.as_ref() {
Some(err_json) => err_json.trim(),
None => return false,
};
if err_json.is_empty() {
return false;
}
if err_json == "null" {
return false;
}
return true;
}
fn extract_first_u64(
value: &serde_json::Value,
candidate_keys: &[&str],
@@ -902,6 +1158,28 @@ fn extract_first_number_as_string(
#[cfg(test)]
mod tests {
#[test]
fn blank_or_null_err_json_is_not_effective_failure() {
let mut transaction = crate::ChainTransactionDto::new(
"sig-non-trade-effective-error".to_string(),
Some(1),
None,
None,
None,
None,
None,
"{}".to_string(),
);
assert!(!super::transaction_has_effective_error(&transaction));
transaction.err_json = Some("".to_string());
assert!(!super::transaction_has_effective_error(&transaction));
transaction.err_json = Some("null".to_string());
assert!(!super::transaction_has_effective_error(&transaction));
transaction.err_json = Some("{\"InstructionError\":[0,\"Custom\"]}".to_string());
assert!(super::transaction_has_effective_error(&transaction));
}
#[test]
fn extracts_nested_liquidity_amounts() {
let payload = serde_json::json!({

View File

@@ -1279,6 +1279,53 @@ fn decode_raydium_clmm_candidate(
),
});
},
crate::RaydiumClmmDecodedEvent::CreatePool(event) => {
return Some(crate::OnchainDexPairCandidateDto {
signature: signature.to_string(),
slot,
block_time,
failed,
program_id: program_id.to_string(),
dex_code,
candidate_kind: "create_pool".to_string(),
confidence: "high".to_string(),
instruction_index: instruction.instruction_index,
inner_instruction_index: instruction.inner_instruction_index,
instruction_name: Some("raydium_clmm.create_pool".to_string()),
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
instruction_discriminator_hex: instruction_discriminator_hex(
instruction.data.as_deref(),
),
pool_address: Some(event.pool_state.clone()),
token_a_mint: Some(event.token_mint_0),
token_b_mint: Some(event.token_mint_1),
verified_pool_address: Some(event.pool_state.clone()),
observed_token_mints: std::vec::Vec::new(),
token_balance_deltas: std::vec::Vec::new(),
candidate_pool_accounts: std::vec::Vec::new(),
candidate_token_vault_accounts: std::vec::Vec::new(),
candidate_program_accounts: std::vec::Vec::new(),
account_samples: sample_strings(instruction.accounts.as_slice(), 12),
log_samples: sample_logs(logs, 8),
backfill_hint: build_backfill_hint(
"pool",
Some(event.pool_state.as_str()),
signature,
),
});
},
crate::RaydiumClmmDecodedEvent::CollectProtocolFee(_)
| crate::RaydiumClmmDecodedEvent::CollectPersonalFeeEvent(_)
| crate::RaydiumClmmDecodedEvent::CollectProtocolFeeEvent(_)
| crate::RaydiumClmmDecodedEvent::ConfigChangeEvent(_)
| crate::RaydiumClmmDecodedEvent::CreatePersonalPositionEvent(_)
| crate::RaydiumClmmDecodedEvent::DecreaseLiquidityEvent(_)
| crate::RaydiumClmmDecodedEvent::IncreaseLiquidityEvent(_)
| crate::RaydiumClmmDecodedEvent::LiquidityCalculateEvent(_)
| crate::RaydiumClmmDecodedEvent::LiquidityChangeEvent(_)
| crate::RaydiumClmmDecodedEvent::PoolCreatedEvent(_)
| crate::RaydiumClmmDecodedEvent::SwapEvent(_)
| crate::RaydiumClmmDecodedEvent::UpdateRewardInfosEvent(_) => return None,
}
}
return None;

View File

@@ -131,6 +131,68 @@ pub struct SignatureBackfillResult {
pub pair_candle_count: usize,
}
/// One item produced by a batch signature backfill.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SignatureBatchBackfillItemResult {
/// Input transaction signature.
pub signature: std::string::String,
/// Whether the signature was replayed successfully.
pub success: bool,
/// Error text when this signature failed before a replay result could be produced.
pub error: std::option::Option<std::string::String>,
/// Per-signature replay result when available.
pub result: std::option::Option<crate::SignatureBackfillResult>,
}
/// Batch signature-backfill result summary.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SignatureBatchBackfillResult {
/// Number of raw signatures submitted by the UI.
pub input_signature_count: usize,
/// Number of unique non-empty signatures processed.
pub unique_signature_count: usize,
/// Number of successfully replayed signatures.
pub success_count: usize,
/// Number of signatures that failed before a replay result could be produced.
pub failure_count: usize,
/// Whether processing stopped after the first hard failure.
pub aborted: bool,
/// Number of transactions resolved through HTTP during this run.
pub resolved_transaction_count: usize,
/// Number of signatures whose `getTransaction` lookup returned `null`.
pub missing_transaction_count: usize,
/// Number of signatures whose `getTransaction` lookup failed after retries.
pub transaction_fetch_error_count: usize,
/// Last transaction fetch error observed during this run, if any.
pub last_transaction_fetch_error: std::option::Option<std::string::String>,
/// Total number of decoded DEX events replayed during this run.
pub decoded_event_count: usize,
/// Total number of DEX detection results produced during this run.
pub detection_count: usize,
/// Total number of launch-attribution results produced during this run.
pub launch_attribution_count: usize,
/// Total number of pool-origin results produced during this run.
pub pool_origin_count: usize,
/// Total number of wallet-participation observations produced during this run.
pub wallet_participation_count: usize,
/// Total number of trade-aggregation results produced during this run.
pub trade_event_count: usize,
/// Total number of liquidity event materialization results produced during this run.
pub liquidity_event_count: usize,
/// Total number of pool lifecycle event materialization results produced during this run.
pub pool_lifecycle_event_count: usize,
/// Total number of fee event materialization results produced during this run.
pub fee_event_count: usize,
/// Total number of reward event materialization results produced during this run.
pub reward_event_count: usize,
/// Total number of pool administration event materialization results produced during this run.
pub pool_admin_event_count: usize,
/// Total number of pair-candle aggregation results produced during this run.
pub pair_candle_count: usize,
/// Detailed per-signature results.
pub items: std::vec::Vec<crate::SignatureBatchBackfillItemResult>,
}
/// Historical token backfill service.
///
/// This service reuses the existing transaction projection and downstream
@@ -878,6 +940,178 @@ impl TokenBackfillService {
return Ok(result);
}
/// Replays a batch of known transaction signatures through the existing pipeline.
///
/// Unlike [`Self::backfill_signature`], this method refreshes token metadata and
/// event coverage only once after the whole batch has been processed. This keeps
/// manual discovery backfills responsive when many signatures were collected from
/// an external explorer.
pub async fn backfill_signatures(
&self,
signatures: &[std::string::String],
continue_on_error: bool,
) -> Result<crate::SignatureBatchBackfillResult, crate::Error> {
let mut result = crate::SignatureBatchBackfillResult {
input_signature_count: signatures.len(),
unique_signature_count: 0,
success_count: 0,
failure_count: 0,
aborted: false,
resolved_transaction_count: 0,
missing_transaction_count: 0,
transaction_fetch_error_count: 0,
last_transaction_fetch_error: None,
decoded_event_count: 0,
detection_count: 0,
launch_attribution_count: 0,
pool_origin_count: 0,
wallet_participation_count: 0,
trade_event_count: 0,
liquidity_event_count: 0,
pool_lifecycle_event_count: 0,
fee_event_count: 0,
reward_event_count: 0,
pool_admin_event_count: 0,
pair_candle_count: 0,
items: std::vec::Vec::new(),
};
let mut seen = std::collections::BTreeSet::<std::string::String>::new();
for signature in signatures {
let trimmed_signature = signature.trim().to_string();
if trimmed_signature.is_empty() {
continue;
}
if seen.contains(trimmed_signature.as_str()) {
continue;
}
seen.insert(trimmed_signature.clone());
result.unique_signature_count += 1;
let replay_result = self.replay_signature(trimmed_signature.clone()).await;
let replay = match replay_result {
Ok(replay) => replay,
Err(error) => {
result.failure_count += 1;
result.items.push(crate::SignatureBatchBackfillItemResult {
signature: trimmed_signature.clone(),
success: false,
error: Some(error.to_string()),
result: None,
});
if !continue_on_error {
result.aborted = true;
break;
}
continue;
},
};
let signature_result = crate::SignatureBackfillResult {
signature: trimmed_signature.clone(),
resolved_transaction_count: replay.resolved_transaction_count,
missing_transaction_count: replay.missing_transaction_count,
transaction_fetch_error_count: replay.transaction_fetch_error_count,
last_transaction_fetch_error: replay.last_transaction_fetch_error.clone(),
decoded_event_count: replay.decoded_event_count,
detection_count: replay.detection_count,
launch_attribution_count: replay.launch_attribution_count,
pool_origin_count: replay.pool_origin_count,
wallet_participation_count: replay.wallet_participation_count,
trade_event_count: replay.trade_event_count,
liquidity_event_count: replay.liquidity_event_count,
pool_lifecycle_event_count: replay.pool_lifecycle_event_count,
fee_event_count: replay.fee_event_count,
reward_event_count: replay.reward_event_count,
pool_admin_event_count: replay.pool_admin_event_count,
pair_candle_count: replay.pair_candle_count,
};
result.success_count += 1;
result.resolved_transaction_count += signature_result.resolved_transaction_count;
result.missing_transaction_count += signature_result.missing_transaction_count;
result.transaction_fetch_error_count += signature_result.transaction_fetch_error_count;
if signature_result.last_transaction_fetch_error.is_some() {
result.last_transaction_fetch_error =
signature_result.last_transaction_fetch_error.clone();
}
result.decoded_event_count += signature_result.decoded_event_count;
result.detection_count += signature_result.detection_count;
result.launch_attribution_count += signature_result.launch_attribution_count;
result.pool_origin_count += signature_result.pool_origin_count;
result.wallet_participation_count += signature_result.wallet_participation_count;
result.trade_event_count += signature_result.trade_event_count;
result.liquidity_event_count += signature_result.liquidity_event_count;
result.pool_lifecycle_event_count += signature_result.pool_lifecycle_event_count;
result.fee_event_count += signature_result.fee_event_count;
result.reward_event_count += signature_result.reward_event_count;
result.pool_admin_event_count += signature_result.pool_admin_event_count;
result.pair_candle_count += signature_result.pair_candle_count;
result.items.push(crate::SignatureBatchBackfillItemResult {
signature: trimmed_signature,
success: true,
error: None,
result: Some(signature_result),
});
}
if result.unique_signature_count == 0 {
return Err(crate::Error::Config(
"signature batch must contain at least one non-empty signature".to_string(),
));
}
self.backfill_missing_token_metadata_best_effort(100).await;
self.refresh_event_coverage_best_effort().await;
let summary_payload = serde_json::json!({
"inputSignatureCount": result.input_signature_count,
"uniqueSignatureCount": result.unique_signature_count,
"successCount": result.success_count,
"failureCount": result.failure_count,
"aborted": result.aborted,
"resolvedTransactionCount": result.resolved_transaction_count,
"missingTransactionCount": result.missing_transaction_count,
"transactionFetchErrorCount": result.transaction_fetch_error_count,
"lastTransactionFetchError": result.last_transaction_fetch_error,
"decodedEventCount": result.decoded_event_count,
"detectionCount": result.detection_count,
"launchAttributionCount": result.launch_attribution_count,
"poolOriginCount": result.pool_origin_count,
"walletParticipationCount": result.wallet_participation_count,
"tradeEventCount": result.trade_event_count,
"liquidityEventCount": result.liquidity_event_count,
"poolLifecycleEventCount": result.pool_lifecycle_event_count,
"feeEventCount": result.fee_event_count,
"rewardEventCount": result.reward_event_count,
"poolAdminEventCount": result.pool_admin_event_count,
"pairCandleCount": result.pair_candle_count
});
let observation_result = self
.persistence
.record_observation(&crate::DetectionObservationInput::new(
"signature_batch.backfill.completed".to_string(),
crate::ObservationSourceKind::HttpRpc,
Some(format!("backfill:{}", self.http_role)),
format!("{} signatures", result.unique_signature_count),
None,
summary_payload.clone(),
))
.await;
let observation_id = match observation_result {
Ok(observation_id) => observation_id,
Err(error) => return Err(error),
};
let signal_result = self
.persistence
.record_signal(&crate::DetectionSignalInput::new(
"signal.signature_batch.backfill.completed".to_string(),
crate::AnalysisSignalSeverity::Low,
format!("{} signatures", result.unique_signature_count),
Some(observation_id),
None,
summary_payload,
))
.await;
if let Err(error) = signal_result {
return Err(error);
}
return Ok(result);
}
async fn fetch_transaction_value_with_retry(
&self,
signature: &str,
@@ -943,6 +1177,52 @@ impl TokenBackfillService {
}
async fn refresh_event_coverage_best_effort(&self) {
let cleanup_result =
crate::query_dex_decoded_events_delete_replaced_raydium_clmm_instruction_audits(
self.database.as_ref(),
None,
)
.await;
match cleanup_result {
Ok(deleted_count) => {
if deleted_count > 0 {
tracing::info!(
deleted_count = deleted_count,
"replaced Raydium CLMM instruction audits cleaned before dex event coverage refresh"
);
}
},
Err(error) => {
tracing::warn!(
error = %error,
"Raydium CLMM replaced instruction-audit cleanup failed before dex event coverage refresh"
);
},
}
let upstream_cleanup_result =
crate::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches(
self.database.as_ref(),
None,
)
.await;
match upstream_cleanup_result {
Ok(deleted_count) => {
if deleted_count > 0 {
tracing::info!(
deleted_count = deleted_count,
"locally covered upstream instruction matches cleaned before dex event coverage refresh"
);
}
},
Err(error) => {
tracing::warn!(
error = %error,
"locally covered upstream instruction-match cleanup failed before dex event coverage refresh"
);
},
}
let coverage_service = crate::DexEventCoverageService::new(self.database.clone());
let refresh_result = coverage_service.refresh_local_counts(None).await;
match refresh_result {
@@ -960,6 +1240,45 @@ impl TokenBackfillService {
);
},
}
let post_refresh_upstream_cleanup_result =
crate::query_dex_decoded_events_delete_locally_covered_upstream_instruction_matches(
self.database.as_ref(),
None,
)
.await;
match post_refresh_upstream_cleanup_result {
Ok(deleted_count) => {
if deleted_count > 0 {
tracing::info!(
deleted_count = deleted_count,
"locally covered upstream instruction matches cleaned after dex event coverage refresh"
);
let second_refresh_result = coverage_service.refresh_local_counts(None).await;
match second_refresh_result {
Ok(second_refresh_result) => {
tracing::debug!(
upserted_entry_count = second_refresh_result.upserted_entry_count,
summary_count = second_refresh_result.summaries.len(),
"dex event coverage refreshed after upstream instruction-match cleanup"
);
},
Err(error) => {
tracing::warn!(
error = %error,
"dex event coverage refresh failed after upstream instruction-match cleanup"
);
},
}
}
},
Err(error) => {
tracing::warn!(
error = %error,
"locally covered upstream instruction-match cleanup failed after dex event coverage refresh"
);
},
}
}
}

View File

@@ -12,6 +12,37 @@ const UPSTREAM_GIT_PROGRAM_NOTES: &str = "program id extracted from upstream Git
const UPSTREAM_GIT_DISCRIMINATOR_NOTES: &str = "entry name and discriminator extracted from upstream Git decoder source or from the discriminator convention used by that upstream decoder; not corpus-verified; no trade/candle/materialization proof";
const UPSTREAM_GIT_ALIAS_PROGRAM_NOTES: &str = "upstream Git decoder name kept as a discovery alias; program id and discriminator rows are represented by the canonical decoder entry to avoid duplicate registry keys";
const RAYDIUM_IDL_SOURCE_REPO: &str = "raydium-io/raydium-idl";
const RAYDIUM_IDL_DISCRIMINATOR_NOTES: &str = "entry name and discriminator extracted from Raydium official IDL snapshot; not corpus-verified; no trade/candle/materialization proof";
const fn raydium_idl_discriminator_entry(
decoder_code: &'static str,
program_id: std::option::Option<&'static str>,
program_family: &'static str,
surface_kind: &'static str,
entry_kind: &'static str,
entry_name: &'static str,
discriminator_hex: &'static str,
discriminator_len: u16,
source_path: &'static str,
) -> crate::UpstreamRegistryEntry {
return crate::UpstreamRegistryEntry {
source_repo: Some(RAYDIUM_IDL_SOURCE_REPO),
source_path: Some(source_path),
decoder_code,
program_id,
program_family,
surface_kind,
entry_kind,
entry_name,
discriminator_hex: Some(discriminator_hex),
discriminator_len: Some(discriminator_len),
proof_status: crate::PROOF_STATUS_UPSTREAM_GIT_UNVERIFIED,
notes: RAYDIUM_IDL_DISCRIMINATOR_NOTES,
};
}
const fn upstream_git_program_entry(
decoder_code: &'static str,
program_id: std::option::Option<&'static str>,
@@ -11775,6 +11806,61 @@ pub(crate) const UPSTREAM_REGISTRY_ENTRIES: &[crate::UpstreamRegistryEntry] = &[
8,
"decoders/raydium-clmm-decoder/src/instructions/close_position.rs",
),
raydium_idl_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
"raydium",
"clmm",
crate::ENTRY_KIND_INSTRUCTION,
"close_limit_order",
"4c7c800fd55725fa",
8,
"raydium_clmm/raydium_clmm.json",
),
raydium_idl_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
"raydium",
"clmm",
crate::ENTRY_KIND_INSTRUCTION,
"open_limit_order",
"9d20dab7471d1293",
8,
"raydium_clmm/raydium_clmm.json",
),
raydium_idl_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
"raydium",
"clmm",
crate::ENTRY_KIND_INSTRUCTION,
"increase_limit_order",
"b19059ecfaba7d63",
8,
"raydium_clmm/raydium_clmm.json",
),
raydium_idl_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
"raydium",
"clmm",
crate::ENTRY_KIND_INSTRUCTION,
"decrease_limit_order",
"759d3c674231a300",
8,
"raydium_clmm/raydium_clmm.json",
),
raydium_idl_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
"raydium",
"clmm",
crate::ENTRY_KIND_INSTRUCTION,
"close_protocol_position",
"c975989055556cb2",
8,
"raydium_clmm/raydium_clmm.json",
),
upstream_git_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
@@ -11852,6 +11938,28 @@ pub(crate) const UPSTREAM_REGISTRY_ENTRIES: &[crate::UpstreamRegistryEntry] = &[
8,
"decoders/raydium-clmm-decoder/src/instructions/create_amm_config.rs",
),
raydium_idl_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
"raydium",
"clmm",
crate::ENTRY_KIND_INSTRUCTION,
"create_customizable_pool",
"2b44d4a7592fa401",
8,
"raydium_clmm/raydium_clmm.json",
),
raydium_idl_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
"raydium",
"clmm",
crate::ENTRY_KIND_INSTRUCTION,
"create_dynamic_fee_config",
"bd0eb5785576e33e",
8,
"raydium_clmm/raydium_clmm.json",
),
upstream_git_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
@@ -11885,6 +11993,17 @@ pub(crate) const UPSTREAM_REGISTRY_ENTRIES: &[crate::UpstreamRegistryEntry] = &[
8,
"decoders/raydium-clmm-decoder/src/instructions/create_pool.rs",
),
raydium_idl_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
"raydium",
"clmm",
crate::ENTRY_KIND_INSTRUCTION,
"create_support_mint_associated",
"11fb415c88f20ea9",
8,
"raydium_clmm/raydium_clmm.json",
),
upstream_git_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
@@ -12039,6 +12158,17 @@ pub(crate) const UPSTREAM_REGISTRY_ENTRIES: &[crate::UpstreamRegistryEntry] = &[
8,
"decoders/raydium-clmm-decoder/src/instructions/set_reward_params.rs",
),
raydium_idl_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),
"raydium",
"clmm",
crate::ENTRY_KIND_INSTRUCTION,
"settle_limit_order",
"cd4e74215c691a60",
8,
"raydium_clmm/raydium_clmm.json",
),
upstream_git_discriminator_entry(
"raydium_clmm",
Some(crate::RAYDIUM_CLMM_PROGRAM_ID),