This commit is contained in:
2026-06-01 19:05:46 +02:00
parent abb810d544
commit 27e25d5bf4
59 changed files with 5727 additions and 1706 deletions

View File

@@ -288,7 +288,7 @@ pub const RAYDIUM_STABLE_SWAP_AMM_PROGRAM_ID: &str = "5quBtoiQqxF9Jv6KYKctB59NT3
pub const BONKSWAP_PROGRAM_ID: &str = "BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p";
/// Boop program id extracted from upstream Git decoder source.
pub const BOOP_PROGRAM_ID: &str = "boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4";
pub const BOOP_FUN_PROGRAM_ID: &str = "boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4";
/// DFlow Aggregator v4 program id extracted from upstream Git decoder source.
pub const DFLOW_AGGREGATOR_V4_PROGRAM_ID: &str = "DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH";

View File

@@ -27,6 +27,7 @@ pub use dtos::DexDto;
pub use dtos::DexEventCoverageEntryDto;
pub use dtos::DexEventCoverageSummaryDto;
pub use dtos::FeeEventDto;
pub use dtos::InstructionObservationDto;
pub use dtos::KnownHttpEndpointDto;
pub use dtos::KnownWsEndpointDto;
pub use dtos::LaunchAttributionDto;
@@ -96,6 +97,7 @@ pub use entities::DexEntity;
pub use entities::DexEventCoverageEntryEntity;
pub use entities::DexEventCoverageSummaryEntity;
pub use entities::FeeEventEntity;
pub use entities::InstructionObservationEntity;
pub use entities::KnownHttpEndpointEntity;
pub use entities::KnownWsEndpointEntity;
pub use entities::LaunchAttributionEntity;
@@ -168,6 +170,8 @@ pub use queries::query_dexs_upsert;
pub use queries::query_fee_events_get_by_decoded_event_id;
pub use queries::query_fee_events_list_recent;
pub use queries::query_fee_events_upsert;
pub use queries::query_instruction_observations_list_by_filter;
pub use queries::query_instruction_observations_upsert;
pub use queries::query_known_http_endpoints_get;
pub use queries::query_known_http_endpoints_list;
pub use queries::query_known_http_endpoints_upsert;

View File

@@ -15,6 +15,7 @@ mod dex_event_coverage_entry;
mod fee_event;
mod known_http_endpoint;
mod known_ws_endpoint;
mod instruction_observation;
mod launch_attribution;
mod launch_surface;
mod launch_surface_key;
@@ -84,6 +85,7 @@ pub use dex_event_coverage_entry::DexEventCoverageSummaryDto;
pub use fee_event::FeeEventDto;
pub use known_http_endpoint::KnownHttpEndpointDto;
pub use known_ws_endpoint::KnownWsEndpointDto;
pub use instruction_observation::InstructionObservationDto;
pub use launch_attribution::LaunchAttributionDto;
pub use launch_surface::LaunchSurfaceDto;
pub use launch_surface_key::LaunchSurfaceKeyDto;

View File

@@ -0,0 +1,155 @@
// file: kb_lib/src/db/dtos/instruction_observation.rs
//! Instruction observation DTOs.
/// Persisted technical observation for one Solana instruction.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct InstructionObservationDto {
/// Optional numeric primary key.
pub id: std::option::Option<i64>,
/// Stable observation key.
pub observation_key: std::string::String,
/// Parent transaction id.
pub transaction_id: i64,
/// Parent transaction signature.
pub signature: std::string::String,
/// Optional Solana slot.
pub slot: std::option::Option<i64>,
/// Optional block time.
pub block_time: std::option::Option<i64>,
/// Whether the parent transaction failed.
pub failed: bool,
/// Instruction row id.
pub instruction_id: i64,
/// Optional parent instruction id.
pub parent_instruction_id: std::option::Option<i64>,
/// Outer instruction index.
pub instruction_index: i64,
/// Optional inner instruction index.
pub inner_instruction_index: std::option::Option<i64>,
/// Instruction program id.
pub program_id: std::string::String,
/// Local decoder code when resolved.
pub decoder_code: std::option::Option<std::string::String>,
/// First eight instruction-data bytes as lower-hex.
pub discriminator_hex: std::option::Option<std::string::String>,
/// Known local instruction name when resolved.
pub instruction_name: std::option::Option<std::string::String>,
/// Serialized accounts JSON.
pub accounts_json: std::string::String,
/// Optional serialized data JSON.
pub data_json: std::option::Option<std::string::String>,
/// Optional decoded pool account from local decoded events.
pub pool_account: std::option::Option<std::string::String>,
/// Optional decoded event kind attached to this instruction.
pub decoded_event_kind: std::option::Option<std::string::String>,
/// Optional decoded event id attached to this instruction.
pub decoded_event_id: std::option::Option<i64>,
/// First observation timestamp.
pub observed_at: chrono::DateTime<chrono::Utc>,
/// Last refresh timestamp.
pub updated_at: chrono::DateTime<chrono::Utc>,
}
impl InstructionObservationDto {
/// Creates a new instruction observation DTO.
#[allow(clippy::too_many_arguments)]
pub fn new(
observation_key: std::string::String,
transaction_id: i64,
signature: std::string::String,
slot: std::option::Option<i64>,
block_time: std::option::Option<i64>,
failed: bool,
instruction_id: i64,
parent_instruction_id: std::option::Option<i64>,
instruction_index: i64,
inner_instruction_index: std::option::Option<i64>,
program_id: std::string::String,
decoder_code: std::option::Option<std::string::String>,
discriminator_hex: std::option::Option<std::string::String>,
instruction_name: std::option::Option<std::string::String>,
accounts_json: std::string::String,
data_json: std::option::Option<std::string::String>,
pool_account: std::option::Option<std::string::String>,
decoded_event_kind: std::option::Option<std::string::String>,
decoded_event_id: std::option::Option<i64>,
) -> Self {
let now = chrono::Utc::now();
return Self {
id: None,
observation_key,
transaction_id,
signature,
slot,
block_time,
failed,
instruction_id,
parent_instruction_id,
instruction_index,
inner_instruction_index,
program_id,
decoder_code,
discriminator_hex,
instruction_name,
accounts_json,
data_json,
pool_account,
decoded_event_kind,
decoded_event_id,
observed_at: now,
updated_at: now,
};
}
}
impl TryFrom<crate::InstructionObservationEntity> for InstructionObservationDto {
type Error = crate::Error;
fn try_from(entity: crate::InstructionObservationEntity) -> Result<Self, Self::Error> {
let observed_at_result = chrono::DateTime::parse_from_rfc3339(entity.observed_at.as_str());
let observed_at = match observed_at_result {
Ok(observed_at) => observed_at.with_timezone(&chrono::Utc),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot parse instruction observation observed_at '{}': {}",
entity.observed_at, error
)));
},
};
let updated_at_result = chrono::DateTime::parse_from_rfc3339(entity.updated_at.as_str());
let updated_at = match updated_at_result {
Ok(updated_at) => updated_at.with_timezone(&chrono::Utc),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot parse instruction observation updated_at '{}': {}",
entity.updated_at, error
)));
},
};
return Ok(Self {
id: Some(entity.id),
observation_key: entity.observation_key,
transaction_id: entity.transaction_id,
signature: entity.signature,
slot: entity.slot,
block_time: entity.block_time,
failed: entity.failed != 0,
instruction_id: entity.instruction_id,
parent_instruction_id: entity.parent_instruction_id,
instruction_index: entity.instruction_index,
inner_instruction_index: entity.inner_instruction_index,
program_id: entity.program_id,
decoder_code: entity.decoder_code,
discriminator_hex: entity.discriminator_hex,
instruction_name: entity.instruction_name,
accounts_json: entity.accounts_json,
data_json: entity.data_json,
pool_account: entity.pool_account,
decoded_event_kind: entity.decoded_event_kind,
decoded_event_id: entity.decoded_event_id,
observed_at,
updated_at,
});
}
}

View File

@@ -17,6 +17,7 @@ mod dex_event_coverage_entry;
mod fee_event;
mod known_http_endpoint;
mod known_ws_endpoint;
mod instruction_observation;
mod launch_attribution;
mod launch_surface;
mod launch_surface_key;
@@ -62,6 +63,7 @@ pub use dex_event_coverage_entry::DexEventCoverageSummaryEntity;
pub use fee_event::FeeEventEntity;
pub use known_http_endpoint::KnownHttpEndpointEntity;
pub use known_ws_endpoint::KnownWsEndpointEntity;
pub use instruction_observation::InstructionObservationEntity;
pub use launch_attribution::LaunchAttributionEntity;
pub use launch_surface::LaunchSurfaceEntity;
pub use launch_surface_key::LaunchSurfaceKeyEntity;

View File

@@ -0,0 +1,52 @@
// file: kb_lib/src/db/entities/instruction_observation.rs
//! Instruction observation entity.
/// Persisted technical observation for one Solana instruction.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
pub struct InstructionObservationEntity {
/// Internal row id.
pub id: i64,
/// Stable observation key.
pub observation_key: std::string::String,
/// Parent transaction id.
pub transaction_id: i64,
/// Parent transaction signature.
pub signature: std::string::String,
/// Optional Solana slot.
pub slot: std::option::Option<i64>,
/// Optional block time.
pub block_time: std::option::Option<i64>,
/// Transaction failed flag.
pub failed: i64,
/// Instruction row id.
pub instruction_id: i64,
/// Optional parent instruction id.
pub parent_instruction_id: std::option::Option<i64>,
/// Outer instruction index.
pub instruction_index: i64,
/// Optional inner instruction index.
pub inner_instruction_index: std::option::Option<i64>,
/// Instruction program id.
pub program_id: std::string::String,
/// Local decoder code when resolved.
pub decoder_code: std::option::Option<std::string::String>,
/// First eight instruction-data bytes as lower-hex.
pub discriminator_hex: std::option::Option<std::string::String>,
/// Known local instruction name when resolved.
pub instruction_name: std::option::Option<std::string::String>,
/// Accounts JSON.
pub accounts_json: std::string::String,
/// Optional data JSON.
pub data_json: std::option::Option<std::string::String>,
/// Optional pool account.
pub pool_account: std::option::Option<std::string::String>,
/// Optional decoded event kind.
pub decoded_event_kind: std::option::Option<std::string::String>,
/// Optional decoded event id.
pub decoded_event_id: std::option::Option<i64>,
/// First observation timestamp.
pub observed_at: std::string::String,
/// Last refresh timestamp.
pub updated_at: std::string::String,
}

View File

@@ -15,6 +15,7 @@ mod dex_event_coverage_entry;
mod fee_event;
mod known_http_endpoint;
mod known_ws_endpoint;
mod instruction_observation;
mod launch_attribution;
mod launch_surface;
mod launch_surface_key;
@@ -92,6 +93,8 @@ 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;

View File

@@ -442,7 +442,7 @@ mod tests {
0,
None,
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
Some("raydium-amm-v4".to_string()),
Some("raydium_amm_v4".to_string()),
Some(1),
r#"["Account0","Pool111","Lp111","TokenA111","TokenB111"]"#.to_string(),
None,
@@ -529,7 +529,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()),
Some("meteora_dlmm".to_string()),
Some(1),
r#"["ParentAccount","Pool111"]"#.to_string(),
None,
@@ -548,7 +548,7 @@ mod tests {
0,
Some(0),
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()),
Some("meteora_dlmm".to_string()),
Some(2),
r#"["ChildAccount","Pool111"]"#.to_string(),
None,

View File

@@ -735,7 +735,7 @@ mod tests {
let database = make_database().await;
let upstream_service = crate::UpstreamRegistryService::new();
let request = crate::UpstreamRegistrySearchRequestDto {
decoder_code: Some("raydium-cpmm".to_string()),
decoder_code: Some("raydium_cpmm".to_string()),
program_id: None,
program_family: None,
surface_kind: None,
@@ -759,7 +759,7 @@ mod tests {
.expect("coverage upsert must succeed");
assert!(id > 0);
let rows =
crate::query_dex_event_coverage_entries_list_by_decoder(&database, "raydium-cpmm")
crate::query_dex_event_coverage_entries_list_by_decoder(&database, "raydium_cpmm")
.await
.expect("coverage list must succeed");
assert_eq!(rows.len(), 1);
@@ -768,7 +768,7 @@ mod tests {
.await
.expect("coverage summary must succeed");
assert_eq!(summaries.len(), 1);
assert_eq!(summaries[0].decoder_code, "raydium-cpmm");
assert_eq!(summaries[0].decoder_code, "raydium_cpmm");
assert_eq!(summaries[0].listed_entry_count, 1);
assert_eq!(summaries[0].decoded_entry_count, 1);
assert_eq!(summaries[0].observed_entry_count, 1);

View File

@@ -0,0 +1,173 @@
// file: kb_lib/src/db/queries/instruction_observation.rs
//! Queries for `k_sol_instruction_observations`.
/// Upserts one instruction observation row.
pub async fn query_instruction_observations_upsert(
database: &crate::Database,
dto: &crate::InstructionObservationDto,
) -> Result<i64, crate::Error> {
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query(
r#"
INSERT INTO k_sol_instruction_observations (
observation_key,
transaction_id,
signature,
slot,
block_time,
failed,
instruction_id,
parent_instruction_id,
instruction_index,
inner_instruction_index,
program_id,
decoder_code,
discriminator_hex,
instruction_name,
accounts_json,
data_json,
pool_account,
decoded_event_kind,
decoded_event_id,
observed_at,
updated_at
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(observation_key) DO UPDATE SET
transaction_id = excluded.transaction_id,
signature = excluded.signature,
slot = excluded.slot,
block_time = excluded.block_time,
failed = excluded.failed,
instruction_id = excluded.instruction_id,
parent_instruction_id = excluded.parent_instruction_id,
instruction_index = excluded.instruction_index,
inner_instruction_index = excluded.inner_instruction_index,
program_id = excluded.program_id,
decoder_code = excluded.decoder_code,
discriminator_hex = excluded.discriminator_hex,
instruction_name = excluded.instruction_name,
accounts_json = excluded.accounts_json,
data_json = excluded.data_json,
pool_account = excluded.pool_account,
decoded_event_kind = excluded.decoded_event_kind,
decoded_event_id = excluded.decoded_event_id,
updated_at = excluded.updated_at
"#,
)
.bind(dto.observation_key.clone())
.bind(dto.transaction_id)
.bind(dto.signature.clone())
.bind(dto.slot)
.bind(dto.block_time)
.bind(if dto.failed { 1_i64 } else { 0_i64 })
.bind(dto.instruction_id)
.bind(dto.parent_instruction_id)
.bind(dto.instruction_index)
.bind(dto.inner_instruction_index)
.bind(dto.program_id.clone())
.bind(dto.decoder_code.clone())
.bind(dto.discriminator_hex.clone())
.bind(dto.instruction_name.clone())
.bind(dto.accounts_json.clone())
.bind(dto.data_json.clone())
.bind(dto.pool_account.clone())
.bind(dto.decoded_event_kind.clone())
.bind(dto.decoded_event_id)
.bind(dto.observed_at.to_rfc3339())
.bind(dto.updated_at.to_rfc3339())
.execute(pool)
.await;
let query_result = match query_result {
Ok(query_result) => query_result,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot upsert k_sol_instruction_observations on sqlite: {}",
error
)));
},
};
return Ok(query_result.last_insert_rowid());
},
}
}
/// Lists instruction observations by optional decoder/discriminator/instruction filters.
pub async fn query_instruction_observations_list_by_filter(
database: &crate::Database,
decoder_code: std::option::Option<&str>,
discriminator_hex: std::option::Option<&str>,
instruction_name: std::option::Option<&str>,
limit: u32,
) -> Result<std::vec::Vec<crate::InstructionObservationDto>, crate::Error> {
if limit == 0 {
return Ok(std::vec::Vec::new());
}
match database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, crate::InstructionObservationEntity>(
r#"
SELECT
id,
observation_key,
transaction_id,
signature,
slot,
block_time,
failed,
instruction_id,
parent_instruction_id,
instruction_index,
inner_instruction_index,
program_id,
decoder_code,
discriminator_hex,
instruction_name,
accounts_json,
data_json,
pool_account,
decoded_event_kind,
decoded_event_id,
observed_at,
updated_at
FROM k_sol_instruction_observations
WHERE (? IS NULL OR decoder_code = ?)
AND (? IS NULL OR discriminator_hex = ?)
AND (? IS NULL OR instruction_name = ?)
ORDER BY slot DESC, transaction_id DESC, instruction_id ASC
LIMIT ?
"#,
)
.bind(decoder_code.map(str::to_string))
.bind(decoder_code.map(str::to_string))
.bind(discriminator_hex.map(str::to_string))
.bind(discriminator_hex.map(str::to_string))
.bind(instruction_name.map(str::to_string))
.bind(instruction_name.map(str::to_string))
.bind(i64::from(limit))
.fetch_all(pool)
.await;
let entities = match query_result {
Ok(entities) => entities,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list k_sol_instruction_observations on sqlite: {}",
error
)));
},
};
let mut dtos = std::vec::Vec::new();
for entity in entities {
let dto_result = crate::InstructionObservationDto::try_from(entity);
let dto = match dto_result {
Ok(dto) => dto,
Err(error) => return Err(error),
};
dtos.push(dto);
}
return Ok(dtos);
},
}
}

View File

@@ -230,6 +230,26 @@ pub(crate) async fn ensure_schema(database: &crate::Database) -> Result<(), crat
if let Err(error) = result {
return Err(error);
}
let result = create_tbl_instruction_observations(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_uix_instruction_observations_key(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_instruction_observations_decoder_discriminator(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_instruction_observations_signature(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_idx_instruction_observations_instruction_name(pool).await;
if let Err(error) = result {
return Err(error);
}
let result = create_tbl_dex_decode_replay_ledger(pool).await;
if let Err(error) = result {
return Err(error);
@@ -1423,6 +1443,104 @@ ON k_sol_chain_instructions (program_id)
.await;
}
/// Creates `k_sol_instruction_observations`.
async fn create_tbl_instruction_observations(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_tbl_instruction_observations",
r#"
CREATE TABLE IF NOT EXISTS k_sol_instruction_observations (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
observation_key TEXT NOT NULL,
transaction_id INTEGER NOT NULL,
signature TEXT NOT NULL,
slot INTEGER NULL,
block_time INTEGER NULL,
failed INTEGER NOT NULL,
instruction_id INTEGER NOT NULL,
parent_instruction_id INTEGER NULL,
instruction_index INTEGER NOT NULL,
inner_instruction_index INTEGER NULL,
program_id TEXT NOT NULL,
decoder_code TEXT NULL,
discriminator_hex TEXT NULL,
instruction_name TEXT NULL,
accounts_json TEXT NOT NULL,
data_json TEXT NULL,
pool_account TEXT NULL,
decoded_event_kind TEXT NULL,
decoded_event_id INTEGER NULL,
observed_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY(transaction_id) REFERENCES k_sol_chain_transactions(id),
FOREIGN KEY(instruction_id) REFERENCES k_sol_chain_instructions(id),
FOREIGN KEY(decoded_event_id) REFERENCES k_sol_dex_decoded_events(id)
)
"#,
)
.await;
}
/// Creates unique index on instruction observation key.
async fn create_uix_instruction_observations_key(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_uix_instruction_observations_key",
r#"
CREATE UNIQUE INDEX IF NOT EXISTS uix_instruction_observations_key
ON k_sol_instruction_observations (observation_key)
"#,
)
.await;
}
/// Creates lookup index on decoder/discriminator.
async fn create_idx_instruction_observations_decoder_discriminator(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_instruction_observations_decoder_discriminator",
r#"
CREATE INDEX IF NOT EXISTS idx_instruction_observations_decoder_discriminator
ON k_sol_instruction_observations (decoder_code, discriminator_hex)
"#,
)
.await;
}
/// Creates lookup index on signature.
async fn create_idx_instruction_observations_signature(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_instruction_observations_signature",
r#"
CREATE INDEX IF NOT EXISTS idx_instruction_observations_signature
ON k_sol_instruction_observations (signature)
"#,
)
.await;
}
/// Creates lookup index on instruction name.
async fn create_idx_instruction_observations_instruction_name(
pool: &sqlx::SqlitePool,
) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(
pool,
"create_idx_instruction_observations_instruction_name",
r#"
CREATE INDEX IF NOT EXISTS idx_instruction_observations_instruction_name
ON k_sol_instruction_observations (instruction_name)
"#,
)
.await;
}
/// Creates `k_sol_dex_decoded_events`.
async fn create_tbl_dex_decoded_events(pool: &sqlx::SqlitePool) -> Result<(), crate::Error> {
return execute_sqlite_schema_statement(

View File

@@ -77,6 +77,10 @@ pub use raydium_clmm::RaydiumClmmSwapLegacyDecoded;
pub use raydium_clmm::RaydiumClmmSwapV2Decoded;
pub use raydium_clmm::decode_raydium_clmm_instruction;
pub use raydium_cpmm::RaydiumCpmmDecodedEvent;
pub use raydium_cpmm::RaydiumCpmmLpChangeEventDecoded;
pub use raydium_cpmm::RaydiumCpmmSwapDecoded;
pub use raydium_cpmm::RaydiumCpmmSwapEventDecoded;
pub use raydium_cpmm::RaydiumCpmmSwapMode;
pub use raydium_cpmm::classify_raydium_cpmm_instruction_data;
pub use raydium_cpmm::decode_raydium_cpmm_instruction;
pub use raydium_cpmm::decode_raydium_cpmm_program_data_event;

View File

@@ -3009,7 +3009,7 @@ fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSid
mod tests {
fn make_create_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-damm-v1-create-1".to_string(),
"sig-meteora_damm_v1-create-1".to_string(),
Some(890001),
Some(1779500001),
Some("helius_primary_http".to_string()),
@@ -3042,7 +3042,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
Some("meteora-damm-v1".to_string()),
Some("meteora_damm_v1".to_string()),
Some(1),
serde_json::json!([
"DammV1Pool111",
@@ -3074,7 +3074,7 @@ mod tests {
fn make_swap_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-damm-v1-swap-1".to_string(),
"sig-meteora_damm_v1-swap-1".to_string(),
Some(890002),
Some(1779500002),
Some("helius_primary_http".to_string()),
@@ -3107,7 +3107,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
Some("meteora-damm-v1".to_string()),
Some("meteora_damm_v1".to_string()),
Some(1),
serde_json::json!(["DammV1SwapPool111", "DammV1SwapTokenA111", crate::WSOL_MINT_ID])
.to_string(),
@@ -3141,7 +3141,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()),
Some("meteora-damm-v1".to_string()),
Some("meteora_damm_v1".to_string()),
Some(1),
accounts.to_string(),
Some(format!("\"{}\"", bs58::encode(data).into_string())),

View File

@@ -758,7 +758,7 @@ fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
mod tests {
fn make_create_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-damm-v2-create-1".to_string(),
"sig-meteora_damm_v2-create-1".to_string(),
Some(889001),
Some(1779400001),
Some("helius_primary_http".to_string()),
@@ -791,7 +791,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
Some("meteora-damm-v2".to_string()),
Some("meteora_damm_v2".to_string()),
Some(1),
serde_json::json!([
"DammV2Pool111",
@@ -823,7 +823,7 @@ mod tests {
fn make_swap_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-damm-v2-swap-1".to_string(),
"sig-meteora_damm_v2-swap-1".to_string(),
Some(889002),
Some(1779400002),
Some("helius_primary_http".to_string()),
@@ -856,7 +856,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
Some("meteora-damm-v2".to_string()),
Some("meteora_damm_v2".to_string()),
Some(1),
serde_json::json!(["DammV2SwapPool111", "DammV2SwapTokenA111", crate::WSOL_MINT_ID])
.to_string(),

View File

@@ -727,7 +727,7 @@ fn is_trade_amount_or_price_key(normalized_key: &str) -> bool {
mod tests {
fn make_create_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-dbc-create-1".to_string(),
"sig-meteora_dbc-create-1".to_string(),
Some(888001),
Some(1779300001),
Some("helius_primary_http".to_string()),
@@ -760,7 +760,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
Some("meteora-dbc".to_string()),
Some("meteora_dbc".to_string()),
Some(1),
serde_json::json!([
"DbcPool111",
@@ -791,7 +791,7 @@ mod tests {
fn make_swap_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-dbc-swap-1".to_string(),
"sig-meteora_dbc-swap-1".to_string(),
Some(888002),
Some(1779300002),
Some("helius_primary_http".to_string()),
@@ -824,7 +824,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DBC_PROGRAM_ID.to_string()),
Some("meteora-dbc".to_string()),
Some("meteora_dbc".to_string()),
Some(1),
serde_json::json!(["DbcPoolSwap111", "DbcSwapTokenA111", crate::WSOL_MINT_ID])
.to_string(),

View File

@@ -2646,7 +2646,7 @@ fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
mod tests {
fn make_create_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-dlmm-create-1".to_string(),
"sig-meteora_dlmm-create-1".to_string(),
Some(888101),
Some(1779400001),
Some("helius_primary_http".to_string()),
@@ -2679,7 +2679,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()),
Some("meteora_dlmm".to_string()),
Some(1),
serde_json::json!([
"DlmmPair111",
@@ -2708,7 +2708,7 @@ mod tests {
fn make_swap_transaction() -> crate::ChainTransactionDto {
let mut dto = crate::ChainTransactionDto::new(
"sig-meteora-dlmm-swap-1".to_string(),
"sig-meteora_dlmm-swap-1".to_string(),
Some(888102),
Some(1779400002),
Some("helius_primary_http".to_string()),
@@ -2741,7 +2741,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()),
Some("meteora_dlmm".to_string()),
Some(1),
serde_json::json!(["DlmmPairSwap111", "DlmmSwapTokenX111", crate::WSOL_MINT_ID])
.to_string(),
@@ -2906,7 +2906,7 @@ mod tests {
fn meteora_dlmm_inner_swap2_instruction_is_not_skipped() {
let decoder = crate::MeteoraDlmmDecoder::new();
let mut transaction = crate::ChainTransactionDto::new(
"sig-meteora-dlmm-inner-swap2".to_string(),
"sig-meteora_dlmm-inner-swap2".to_string(),
Some(888103),
Some(1779400003),
Some("helius_primary_http".to_string()),
@@ -2933,7 +2933,7 @@ mod tests {
3,
Some(14),
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()),
Some("meteora_dlmm".to_string()),
Some(2),
serde_json::json!([
"LbPair111",
@@ -3030,7 +3030,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()),
Some("meteora_dlmm".to_string()),
Some(1),
serde_json::json!([
"Position111",
@@ -3083,7 +3083,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()),
Some("meteora_dlmm".to_string()),
Some(1),
serde_json::json!([
"DlmmPairFee111",
@@ -3132,7 +3132,7 @@ mod tests {
0,
None,
Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()),
Some("meteora-dlmm".to_string()),
Some("meteora_dlmm".to_string()),
Some(1),
serde_json::json!([
"Position111",

View File

@@ -102,7 +102,7 @@ impl OpenBookV2Decoder {
Some(registry_match) => registry_match,
None => continue,
};
if registry_match.decoder_code.as_str() != "openbook-v2" {
if registry_match.decoder_code.as_str() != "openbook_v2" {
continue;
}
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());

View File

@@ -532,7 +532,7 @@ mod tests {
0,
None,
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
Some("orca-whirlpools".to_string()),
Some("orca_whirlpools".to_string()),
Some(1),
serde_json::json!([
"OrcaPool111",
@@ -599,7 +599,7 @@ mod tests {
0,
None,
Some(crate::ORCA_WHIRLPOOLS_PROGRAM_ID.to_string()),
Some("orca-whirlpools".to_string()),
Some("orca_whirlpools".to_string()),
Some(1),
serde_json::json!(["OrcaSwapPool111", "OrcaSwapTokenA111", crate::WSOL_MINT_ID])
.to_string(),

View File

@@ -101,7 +101,7 @@ impl PhoenixV1Decoder {
Some(registry_match) => registry_match,
None => continue,
};
if registry_match.decoder_code.as_str() != "phoenix-v1" {
if registry_match.decoder_code.as_str() != "phoenix_v1" {
continue;
}
let accounts = parse_instruction_accounts_vec(instruction.accounts_json.as_str());

View File

@@ -1080,7 +1080,7 @@ mod tests {
0,
None,
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
Some("raydium-amm-v4".to_string()),
Some("raydium_amm_v4".to_string()),
Some(1),
serde_json::json!([
"Account0",
@@ -1215,7 +1215,7 @@ mod tests {
4,
Some(0),
Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()),
Some("raydium-amm-v4".to_string()),
Some("raydium_amm_v4".to_string()),
Some(2),
serde_json::json!([
crate::SPL_TOKEN_PROGRAM_ID,

File diff suppressed because it is too large Load Diff

View File

@@ -1012,11 +1012,11 @@ impl DexDecodeService {
"raydium_cpmm",
crate::RAYDIUM_CPMM_PROGRAM_ID.to_string(),
event_kind.as_str(),
Some(decoded_event.pool_account().to_string()),
None,
Some(decoded_event.base_mint().to_string()),
Some(decoded_event.quote_mint().to_string()),
decoded_event.pool_account().map(|value| return value.to_string()),
None,
decoded_event.base_mint().map(|value| return value.to_string()),
decoded_event.quote_mint().map(|value| return value.to_string()),
decoded_event.lp_mint().map(|value| return value.to_string()),
payload_value,
)
.await;
@@ -1174,6 +1174,7 @@ impl DexDecodeService {
instructions: &[crate::ChainInstructionDto],
) -> Result<std::vec::Vec<crate::DexDecodedEventDto>, crate::Error> {
let mut persisted = std::vec::Vec::new();
let mut program_data_events = collect_raydium_cpmm_program_data_events(transaction);
for instruction in instructions {
let program_id = match instruction.program_id.as_ref() {
Some(program_id) => program_id,
@@ -1186,6 +1187,8 @@ impl DexDecodeService {
Some(data_json) => data_json,
None => continue,
};
let instruction_kind =
crate::classify_raydium_cpmm_instruction_data(data_json.as_str());
let decoded_events = crate::decode_raydium_cpmm_instruction(
instruction.accounts_json.as_str(),
data_json.as_str(),
@@ -1199,6 +1202,18 @@ impl DexDecodeService {
};
persisted.push(persisted_event);
}
let program_data_persist_result = persist_matching_raydium_cpmm_program_data_event(
self,
transaction,
instruction,
instruction_kind,
&mut program_data_events,
&mut persisted,
)
.await;
if let Err(error) = program_data_persist_result {
return Err(error);
}
}
return Ok(persisted);
}
@@ -1808,6 +1823,11 @@ struct RaydiumMappedNonTradeInstructionSpec {
enum RaydiumMappedNonTradeAmountLayout {
None,
ClmmLiquidityV2,
CpmmAmmConfig,
CpmmDeposit,
CpmmFeePair,
CpmmInitialize,
CpmmPoolStatus,
CpmmWithdraw,
}
@@ -1894,26 +1914,81 @@ fn raydium_mapped_non_trade_instruction_spec(
}
}
if protocol_name == "raydium_cpmm" {
if discriminator_hex == "1416567bc61cdb84" && account_count >= 14 {
if discriminator_hex == "9c5420764587467b" && account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_creator_fee",
event_kind: "raydium_cpmm.collect_creator_fee",
pool_account_index: Some(3),
instruction_name: "close_permission_pda",
event_kind: "raydium_cpmm.close_permission_pda",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "b712469c946da122" && account_count >= 14 {
if discriminator_hex == "1416567bc61cdb84" && account_count >= 13 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "withdraw",
event_kind: "raydium_cpmm.withdraw",
pool_account_index: Some(3),
instruction_name: "collect_creator_fee",
event_kind: "raydium_cpmm.collect_creator_fee",
pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "a78a4e95dfc2067e" && account_count >= 12 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_fund_fee",
event_kind: "raydium_cpmm.collect_fund_fee",
pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair,
});
}
if discriminator_hex == "8888fcddc2427e59" && account_count >= 12 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "collect_protocol_fee",
event_kind: "raydium_cpmm.collect_protocol_fee",
pool_account_index: Some(2),
token_a_mint_index: Some(6),
token_b_mint_index: Some(7),
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmFeePair,
});
}
if discriminator_hex == "8934edd4d7756c68" && account_count >= 3 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_amm_config",
event_kind: "raydium_cpmm.create_amm_config",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmWithdraw,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig,
});
}
if discriminator_hex == "878802d889a9b5ca" && account_count >= 4 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "create_permission_pda",
event_kind: "raydium_cpmm.create_permission_pda",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
});
}
if discriminator_hex == "f223c68952e1f2b6" && account_count >= 13 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "deposit",
event_kind: "raydium_cpmm.deposit",
pool_account_index: Some(2),
token_a_mint_index: Some(10),
token_b_mint_index: Some(11),
lp_mint_index: Some(12),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmDeposit,
});
}
if discriminator_hex == "afaf6d1f0d989bed" && account_count >= 20 {
@@ -1923,8 +1998,52 @@ fn raydium_mapped_non_trade_instruction_spec(
pool_account_index: Some(3),
token_a_mint_index: Some(4),
token_b_mint_index: Some(5),
lp_mint_index: Some(13),
amount_layout: RaydiumMappedNonTradeAmountLayout::None,
lp_mint_index: Some(6),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize,
});
}
if discriminator_hex == "3f37fe4131b25979" && account_count >= 21 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "initialize_with_permission",
event_kind: "raydium_cpmm.initialize_with_permission",
pool_account_index: Some(4),
token_a_mint_index: Some(5),
token_b_mint_index: Some(6),
lp_mint_index: Some(7),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmInitialize,
});
}
if discriminator_hex == "313cae889a1c74c8" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_amm_config",
event_kind: "raydium_cpmm.update_amm_config",
pool_account_index: None,
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig,
});
}
if discriminator_hex == "82576c062ee0757b" && account_count >= 2 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "update_pool_status",
event_kind: "raydium_cpmm.update_pool_status",
pool_account_index: Some(1),
token_a_mint_index: None,
token_b_mint_index: None,
lp_mint_index: None,
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus,
});
}
if discriminator_hex == "b712469c946da122" && account_count >= 14 {
return Some(RaydiumMappedNonTradeInstructionSpec {
instruction_name: "withdraw",
event_kind: "raydium_cpmm.withdraw",
pool_account_index: Some(2),
token_a_mint_index: Some(10),
token_b_mint_index: Some(11),
lp_mint_index: Some(12),
amount_layout: RaydiumMappedNonTradeAmountLayout::CpmmWithdraw,
});
}
}
@@ -1979,6 +2098,15 @@ fn enrich_raydium_mapped_non_trade_payload(
"instructionName".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
);
object.insert(
"upstreamInstructionName".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
);
object.insert("localSpecializedDecoder".to_string(), serde_json::Value::Bool(true));
object.insert(
"adminAction".to_string(),
serde_json::Value::String(mapped_spec.instruction_name.to_string()),
);
object.insert("decodedFromAudit".to_string(), serde_json::Value::Bool(true));
object.insert(
"auditReason".to_string(),
@@ -2032,6 +2160,94 @@ fn insert_raydium_mapped_amounts(
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmAmmConfig => {
if let Some(param) = read_u8_from_bytes(data, 8) {
object.insert(
"configParam".to_string(),
serde_json::Value::Number(serde_json::Number::from(param as u64)),
);
}
if let Some(value) = read_u64_le_from_bytes(data, 9) {
object.insert(
"configValue".to_string(),
serde_json::Value::String(value.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmDeposit => {
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
object.insert(
"lpAmountRaw".to_string(),
serde_json::Value::String(lp_amount.to_string()),
);
object.insert(
"liquidity".to_string(),
serde_json::Value::String(lp_amount.to_string()),
);
}
if let Some(amount_0) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 24) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmFeePair => {
if let Some(amount_0) = read_u64_le_from_bytes(data, 8) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
object.insert(
"amount0RequestedRaw".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
object.insert(
"amount1RequestedRaw".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmInitialize => {
if let Some(amount_0) = read_u64_le_from_bytes(data, 8) {
object.insert(
"tokenAAmount".to_string(),
serde_json::Value::String(amount_0.to_string()),
);
}
if let Some(amount_1) = read_u64_le_from_bytes(data, 16) {
object.insert(
"tokenBAmount".to_string(),
serde_json::Value::String(amount_1.to_string()),
);
}
if let Some(open_time) = read_u64_le_from_bytes(data, 24) {
object.insert(
"openTime".to_string(),
serde_json::Value::String(open_time.to_string()),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmPoolStatus => {
if let Some(status) = read_u8_from_bytes(data, 8) {
object.insert(
"poolStatus".to_string(),
serde_json::Value::Number(serde_json::Number::from(status as u64)),
);
}
},
RaydiumMappedNonTradeAmountLayout::CpmmWithdraw => {
if let Some(lp_amount) = read_u64_le_from_bytes(data, 8) {
object.insert(
@@ -2073,6 +2289,13 @@ fn instruction_data_bytes_from_base58(
}
}
fn read_u8_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u8> {
if data.len() < offset + 1 {
return None;
}
return Some(data[offset]);
}
fn read_u64_le_from_bytes(data: &[u8], offset: usize) -> std::option::Option<u64> {
if data.len() < offset + 8 {
return None;
@@ -2453,6 +2676,150 @@ fn append_persisted_events(
}
}
#[derive(Clone, Debug)]
struct RaydiumCpmmProgramDataEventCandidate {
decoded_event: crate::RaydiumCpmmDecodedEvent,
consumed: bool,
}
fn collect_raydium_cpmm_program_data_events(
transaction: &crate::ChainTransactionDto,
) -> std::vec::Vec<RaydiumCpmmProgramDataEventCandidate> {
let logs = extract_transaction_log_messages(transaction.transaction_json.as_str());
let mut events = std::vec::Vec::new();
let mut cpmm_stack_depth = 0_u32;
for log_message in logs {
if is_program_invoke_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) {
cpmm_stack_depth += 1;
continue;
}
if is_program_success_or_failed_log(log_message.as_str(), crate::RAYDIUM_CPMM_PROGRAM_ID) {
cpmm_stack_depth = cpmm_stack_depth.saturating_sub(1);
continue;
}
if cpmm_stack_depth == 0 {
continue;
}
let data_base64 = match log_message.strip_prefix("Program data: ") {
Some(data_base64) => data_base64.trim(),
None => continue,
};
if data_base64.is_empty() {
continue;
}
let decoded_event = crate::decode_raydium_cpmm_program_data_event(data_base64);
if let Some(decoded_event) = decoded_event {
events.push(RaydiumCpmmProgramDataEventCandidate { decoded_event, consumed: false });
}
}
return events;
}
async fn persist_matching_raydium_cpmm_program_data_event(
service: &DexDecodeService,
transaction: &crate::ChainTransactionDto,
instruction: &crate::ChainInstructionDto,
instruction_kind: std::option::Option<&str>,
program_data_events: &mut [RaydiumCpmmProgramDataEventCandidate],
persisted: &mut std::vec::Vec<crate::DexDecodedEventDto>,
) -> Result<(), crate::Error> {
let expected_event_kind = match instruction_kind {
Some("swap_base_input") => Some("swap_event"),
Some("swap_base_output") => Some("swap_event"),
Some("deposit") => Some("lp_change_event"),
Some("withdraw") => Some("lp_change_event"),
_ => None,
};
let expected_event_kind = match expected_event_kind {
Some(expected_event_kind) => expected_event_kind,
None => return Ok(()),
};
let mut index = 0_usize;
while index < program_data_events.len() {
if program_data_events[index].consumed {
index += 1;
continue;
}
let event_matches = match (&program_data_events[index].decoded_event, expected_event_kind) {
(crate::RaydiumCpmmDecodedEvent::SwapEvent(_), "swap_event") => true,
(crate::RaydiumCpmmDecodedEvent::LpChangeEvent(_), "lp_change_event") => true,
_ => false,
};
if !event_matches {
index += 1;
continue;
}
program_data_events[index].consumed = true;
let persist_result = service
.persist_raydium_cpmm_event(
transaction,
instruction,
&program_data_events[index].decoded_event,
)
.await;
let persisted_event = match persist_result {
Ok(persisted_event) => persisted_event,
Err(error) => return Err(error),
};
persisted.push(persisted_event);
return Ok(());
}
return Ok(());
}
fn extract_transaction_log_messages(transaction_json: &str) -> std::vec::Vec<std::string::String> {
let value_result = serde_json::from_str::<serde_json::Value>(transaction_json);
let value = match value_result {
Ok(value) => value,
Err(_) => return std::vec::Vec::new(),
};
let meta = match value.get("meta") {
Some(meta) => meta,
None => return std::vec::Vec::new(),
};
let logs = match meta.get("logMessages") {
Some(logs) => logs,
None => return std::vec::Vec::new(),
};
let logs = match logs.as_array() {
Some(logs) => logs,
None => return std::vec::Vec::new(),
};
let mut output = std::vec::Vec::new();
for log in logs {
if let Some(log) = log.as_str() {
output.push(log.to_string());
}
}
return output;
}
fn is_program_invoke_log(log_message: &str, program_id: &str) -> bool {
if !log_message.starts_with("Program ") {
return false;
}
if !log_message.contains(" invoke [") {
return false;
}
return log_message.contains(program_id);
}
fn is_program_success_or_failed_log(log_message: &str, program_id: &str) -> bool {
if !log_message.starts_with("Program ") {
return false;
}
if !log_message.contains(program_id) {
return false;
}
if log_message.ends_with(" success") {
return true;
}
if log_message.contains(" failed: ") {
return true;
}
return false;
}
fn decoded_instruction_ids_from_persisted_events(
persisted: &[crate::DexDecodedEventDto],
) -> std::collections::HashSet<i64> {
@@ -2603,7 +2970,7 @@ mod tests {
"instructions": [
{
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
"program": "raydium-amm-v4",
"program": "raydium_amm_v4",
"stackHeight": 1,
"accounts": [
"Account0",
@@ -2887,7 +3254,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DBC_PROGRAM_ID,
"program": "meteora-dbc",
"program": "meteora_dbc",
"stackHeight": 1,
"accounts": [
"DbcPoolDecode111",
@@ -2962,7 +3329,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
"program": "meteora-damm-v2",
"program": "meteora_damm_v2",
"stackHeight": 1,
"accounts": [
"DammV2DecodePool111",
@@ -3039,7 +3406,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
"program": "meteora-damm-v1",
"program": "meteora_damm_v1",
"stackHeight": 1,
"accounts": [
"DammV1DecodePool111",
@@ -3116,7 +3483,7 @@ mod tests {
"instructions": [
{
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
"program": "orca-whirlpools",
"program": "orca_whirlpools",
"stackHeight": 1,
"accounts": [
"OrcaDecodePool111",
@@ -3488,36 +3855,32 @@ mod tests {
#[test]
fn maps_observed_raydium_cpmm_non_swap_discriminators() {
let collect_creator_fee = super::raydium_mapped_non_trade_instruction_spec(
"raydium_cpmm",
Some("1416567bc61cdb84"),
14,
);
let collect_creator_fee = match collect_creator_fee {
Some(collect_creator_fee) => collect_creator_fee,
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"),
14,
);
let withdraw = match withdraw {
Some(withdraw) => withdraw,
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"),
20,
);
let initialize = match initialize {
Some(initialize) => initialize,
None => panic!("initialize discriminator must be mapped"),
};
assert_eq!(initialize.event_kind, "raydium_cpmm.initialize");
let expected = [
("9c5420764587467b", 4_usize, "raydium_cpmm.close_permission_pda"),
("1416567bc61cdb84", 13_usize, "raydium_cpmm.collect_creator_fee"),
("a78a4e95dfc2067e", 12_usize, "raydium_cpmm.collect_fund_fee"),
("8888fcddc2427e59", 12_usize, "raydium_cpmm.collect_protocol_fee"),
("8934edd4d7756c68", 3_usize, "raydium_cpmm.create_amm_config"),
("878802d889a9b5ca", 4_usize, "raydium_cpmm.create_permission_pda"),
("f223c68952e1f2b6", 13_usize, "raydium_cpmm.deposit"),
("afaf6d1f0d989bed", 20_usize, "raydium_cpmm.initialize"),
("3f37fe4131b25979", 21_usize, "raydium_cpmm.initialize_with_permission"),
("313cae889a1c74c8", 2_usize, "raydium_cpmm.update_amm_config"),
("82576c062ee0757b", 2_usize, "raydium_cpmm.update_pool_status"),
("b712469c946da122", 14_usize, "raydium_cpmm.withdraw"),
];
for (discriminator, account_count, event_kind) in expected {
let mapped = super::raydium_mapped_non_trade_instruction_spec(
"raydium_cpmm",
Some(discriminator),
account_count,
);
let mapped = match mapped {
Some(mapped) => mapped,
None => panic!("raydium cpmm discriminator must be mapped: {}", discriminator),
};
assert_eq!(mapped.event_kind, event_kind);
}
}
#[test]
@@ -3573,7 +3936,7 @@ mod tests {
let registry_match = crate::UpstreamRegistryEntryDto {
source_repo: Some("sevenlabs-hq/carbon".to_string()),
source_path: Some("decoders/example.rs".to_string()),
decoder_code: "meteora-damm-v2".to_string(),
decoder_code: "meteora_damm_v2".to_string(),
program_id: Some(crate::METEORA_DAMM_V2_PROGRAM_ID.to_string()),
program_family: "meteora".to_string(),
surface_kind: "amm".to_string(),

View File

@@ -1036,7 +1036,7 @@ mod tests {
"instructions": [
{
"programId": crate::RAYDIUM_AMM_V4_PROGRAM_ID,
"program": "raydium-amm-v4",
"program": "raydium_amm_v4",
"stackHeight": 1,
"accounts": [
"Account0",
@@ -1462,7 +1462,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DBC_PROGRAM_ID,
"program": "meteora-dbc",
"program": "meteora_dbc",
"stackHeight": 1,
"accounts": [
"DbcDetectPool111",
@@ -1581,7 +1581,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
"program": "meteora-damm-v2",
"program": "meteora_damm_v2",
"stackHeight": 1,
"accounts": [
"DammV2DetectPool111",
@@ -1701,7 +1701,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DAMM_V1_PROGRAM_ID,
"program": "meteora-damm-v1",
"program": "meteora_damm_v1",
"stackHeight": 1,
"accounts": [
"DammV1DetectPool111",
@@ -1821,7 +1821,7 @@ mod tests {
"instructions": [
{
"programId": crate::ORCA_WHIRLPOOLS_PROGRAM_ID,
"program": "orca-whirlpools",
"program": "orca_whirlpools",
"stackHeight": 1,
"accounts": [
"OrcaDetectPool111",

View File

@@ -320,6 +320,9 @@ pub fn is_dex_liquidity_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".deposit") {
return true;
}
if event_kind.contains(".lp_change_event") {
return true;
}
if event_kind.contains(".withdraw") {
return true;
}
@@ -518,6 +521,9 @@ pub fn is_dex_migration_event_kind(event_kind: &str) -> bool {
/// Returns true for pool creation or initialization events.
pub fn is_dex_pool_creation_event_kind(event_kind: &str) -> bool {
if event_kind.contains("amm_config") {
return false;
}
if event_kind.contains(".initialize_position") {
return false;
}
@@ -552,6 +558,9 @@ pub fn is_dex_pair_creation_event_kind(event_kind: &str) -> bool {
/// Returns true for admin, configuration or permission changes.
pub fn is_dex_admin_event_kind(event_kind: &str) -> bool {
if event_kind.contains(".initialize_with_permission") {
return false;
}
if event_kind.contains(".lock_liquidity") {
return true;
}
@@ -1152,6 +1161,15 @@ mod tests {
);
}
#[test]
fn classifies_initialize_with_permission_as_lifecycle_only() {
let event_kind = "raydium_cpmm.initialize_with_permission";
assert!(super::is_dex_pool_lifecycle_event_kind(event_kind));
assert!(!super::is_dex_admin_event_kind(event_kind));
assert_eq!(super::classify_dex_event_category_code(event_kind), "pool_lifecycle");
assert_eq!(super::classify_dex_event_lifecycle_kind_code(event_kind), "pool_creation");
}
#[test]
fn classifies_audit_suffix_events_as_informational() {
assert!(super::is_dex_informational_event_kind("openbook_v2.settle_funds_audit"));

View File

@@ -38,15 +38,10 @@ impl DexEventCoverageService {
};
}
/// Synchronizes static upstream registry entries into SQLite coverage rows.
///
/// The resulting rows are still discovery/audit metadata. A row can become
/// observed or materialized only through local corpus replay and explicit
/// count refreshes.
pub async fn sync_upstream_registry(
async fn upsert_upstream_registry_rows(
&self,
decoder_code: std::option::Option<std::string::String>,
) -> Result<crate::DexEventCoverageSyncResult, crate::Error> {
) -> Result<(usize, usize), crate::Error> {
let request = crate::UpstreamRegistrySearchRequestDto {
decoder_code: decoder_code.clone(),
program_id: None,
@@ -70,6 +65,30 @@ impl DexEventCoverageService {
Err(error) => return Err(error),
}
}
return Ok((search_result.entries.len(), upserted_entry_count));
}
async fn ensure_upstream_registry_rows_if_needed(
&self,
decoder_code: std::option::Option<std::string::String>,
) -> Result<(usize, usize), crate::Error> {
return self.upsert_upstream_registry_rows(decoder_code).await;
}
/// Synchronizes static upstream registry entries into SQLite coverage rows.
///
/// The resulting rows are still discovery/audit metadata. A row can become
/// observed or materialized only through local corpus replay and explicit
/// count refreshes.
pub async fn sync_upstream_registry(
&self,
decoder_code: std::option::Option<std::string::String>,
) -> Result<crate::DexEventCoverageSyncResult, crate::Error> {
let sync_counts = self.upsert_upstream_registry_rows(decoder_code.clone()).await;
let (upstream_entry_count, upserted_entry_count) = match sync_counts {
Ok(sync_counts) => sync_counts,
Err(error) => return Err(error),
};
let refreshed_entry_count = match &decoder_code {
Some(decoder_code) => {
let refresh_result =
@@ -103,7 +122,7 @@ impl DexEventCoverageService {
};
return Ok(crate::DexEventCoverageSyncResult {
decoder_code,
upstream_entry_count: search_result.entries.len(),
upstream_entry_count,
upserted_entry_count,
refreshed_entry_count,
summaries,
@@ -115,6 +134,11 @@ impl DexEventCoverageService {
&self,
decoder_code: std::option::Option<std::string::String>,
) -> Result<crate::DexEventCoverageSyncResult, crate::Error> {
let sync_counts = self.ensure_upstream_registry_rows_if_needed(decoder_code.clone()).await;
let (upstream_entry_count, upserted_entry_count) = match sync_counts {
Ok(sync_counts) => sync_counts,
Err(error) => return Err(error),
};
let refreshed_entry_count = match &decoder_code {
Some(decoder_code) => {
let refresh_result =
@@ -148,8 +172,8 @@ impl DexEventCoverageService {
};
return Ok(crate::DexEventCoverageSyncResult {
decoder_code,
upstream_entry_count: 0,
upserted_entry_count: 0,
upstream_entry_count,
upserted_entry_count,
refreshed_entry_count,
summaries,
});
@@ -160,8 +184,12 @@ fn build_coverage_entry_from_upstream(
entry: &crate::UpstreamRegistryEntryDto,
) -> crate::DexEventCoverageEntryDto {
let event_family = infer_event_family(entry.entry_name.as_str(), entry.entry_kind.as_str());
let expected_db_target =
infer_expected_db_target(event_family.as_deref(), entry.entry_kind.as_str());
let expected_db_target = infer_expected_db_target_for_entry(
entry.decoder_code.as_str(),
entry.entry_name.as_str(),
event_family.as_deref(),
entry.entry_kind.as_str(),
);
let local_event_kind =
known_local_event_kind(entry.decoder_code.as_str(), entry.entry_name.as_str());
let mut coverage_entry = crate::DexEventCoverageEntryDto::from_upstream_registry_entry(
@@ -177,6 +205,18 @@ fn build_coverage_entry_from_upstream(
return coverage_entry;
}
fn infer_expected_db_target_for_entry(
decoder_code: &str,
entry_name: &str,
event_family: std::option::Option<&str>,
entry_kind: &str,
) -> std::option::Option<std::string::String> {
if decoder_code == "raydium_cpmm" && entry_name == "swap_event" {
return Some(crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string());
}
return infer_expected_db_target(event_family, entry_kind);
}
fn infer_expected_db_target(
event_family: std::option::Option<&str>,
entry_kind: &str,
@@ -195,6 +235,7 @@ fn infer_expected_db_target(
let target = match family {
"swap" => crate::DexEventCoverageEntryDto::DB_TARGET_TRADE_EVENTS,
"pool_create" => crate::DexEventCoverageEntryDto::DB_TARGET_POOL_LIFECYCLE_EVENTS,
"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,
@@ -235,6 +276,9 @@ fn infer_event_family(
return None;
}
let normalized = entry_name.to_ascii_lowercase();
if normalized == "lp_change_event" {
return Some("liquidity".to_string());
}
if contains_any(normalized.as_str(), &["swap", "buy", "sell", "trade"]) {
return Some("swap".to_string());
}
@@ -360,29 +404,58 @@ fn known_local_event_kind(
entry_name: &str,
) -> std::option::Option<std::string::String> {
match (decoder_code, entry_name) {
("raydium-cpmm", "swap_base_input") => {
("raydium_cpmm", "swap_base_input") => {
return Some("raydium_cpmm.swap_base_input".to_string());
},
("raydium-cpmm", "swap_base_output") => {
("raydium_cpmm", "swap_base_output") => {
return Some("raydium_cpmm.swap_base_output".to_string());
},
("raydium-cpmm", "collect_creator_fee") => {
("raydium_cpmm", "close_permission_pda") => {
return Some("raydium_cpmm.close_permission_pda".to_string());
},
("raydium_cpmm", "collect_creator_fee") => {
return Some("raydium_cpmm.collect_creator_fee".to_string());
},
("raydium-cpmm", "withdraw") => return Some("raydium_cpmm.withdraw".to_string()),
("raydium-cpmm", "initialize") => return Some("raydium_cpmm.initialize".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") => {
("raydium_cpmm", "collect_fund_fee") => {
return Some("raydium_cpmm.collect_fund_fee".to_string());
},
("raydium_cpmm", "collect_protocol_fee") => {
return Some("raydium_cpmm.collect_protocol_fee".to_string());
},
("raydium_cpmm", "create_amm_config") => {
return Some("raydium_cpmm.create_amm_config".to_string());
},
("raydium_cpmm", "create_permission_pda") => {
return Some("raydium_cpmm.create_permission_pda".to_string());
},
("raydium_cpmm", "deposit") => return Some("raydium_cpmm.deposit".to_string()),
("raydium_cpmm", "initialize") => return Some("raydium_cpmm.initialize".to_string()),
("raydium_cpmm", "initialize_with_permission") => {
return Some("raydium_cpmm.initialize_with_permission".to_string());
},
("raydium_cpmm", "lp_change_event") => {
return Some("raydium_cpmm.lp_change_event".to_string());
},
("raydium_cpmm", "swap_event") => return Some("raydium_cpmm.swap_event".to_string()),
("raydium_cpmm", "update_amm_config") => {
return Some("raydium_cpmm.update_amm_config".to_string());
},
("raydium_cpmm", "update_pool_status") => {
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", "decrease_liquidity_v2") => {
("raydium_clmm", "decrease_liquidity_v2") => {
return Some("raydium_clmm.decrease_liquidity_v2".to_string());
},
("raydium-clmm", "open_position_with_token22_nft") => {
("raydium_clmm", "open_position_with_token22_nft") => {
return Some("raydium_clmm.open_position_with_token22_nft".to_string());
},
("raydium-clmm", "close_position") => {
("raydium_clmm", "close_position") => {
return Some("raydium_clmm.close_position".to_string());
},
_ => return None,
@@ -442,7 +515,7 @@ mod tests {
async fn sync_upstream_registry_persists_raydium_cpmm_coverage_rows() {
let database = make_database().await;
let service = crate::DexEventCoverageService::new(database.clone());
let result = service.sync_upstream_registry(Some("raydium-cpmm".to_string())).await;
let result = service.sync_upstream_registry(Some("raydium_cpmm".to_string())).await;
let result = match result {
Ok(result) => result,
Err(error) => panic!("coverage sync must succeed: {}", error),
@@ -451,7 +524,7 @@ mod tests {
assert_eq!(result.upstream_entry_count, result.upserted_entry_count);
let rows_result = crate::query_dex_event_coverage_entries_list_by_decoder(
database.as_ref(),
"raydium-cpmm",
"raydium_cpmm",
)
.await;
let rows = match rows_result {
@@ -467,7 +540,46 @@ mod tests {
assert!(rows.iter().any(|row| return {
row.entry_name == "deposit"
&& row.event_family == Some("liquidity_add".to_string())
&& row.local_event_kind.is_none()
&& row.local_event_kind == Some("raydium_cpmm.deposit".to_string())
}));
assert!(rows.iter().any(|row| return {
row.entry_name == "lp_change_event"
&& row.event_family == Some("liquidity".to_string())
&& row.expected_db_target
== Some(crate::DexEventCoverageEntryDto::DB_TARGET_LIQUIDITY_EVENTS.to_string())
&& row.local_event_kind == Some("raydium_cpmm.lp_change_event".to_string())
}));
assert!(rows.iter().any(|row| return {
row.entry_name == "swap_event"
&& row.event_family == Some("swap".to_string())
&& row.expected_db_target
== Some(
crate::DexEventCoverageEntryDto::DB_TARGET_DECODED_EVENTS_ONLY.to_string(),
)
&& row.local_event_kind == Some("raydium_cpmm.swap_event".to_string())
}));
}
#[tokio::test]
async fn refresh_local_counts_auto_syncs_empty_coverage_table() {
let database = make_database().await;
let service = crate::DexEventCoverageService::new(database.clone());
let result = service.refresh_local_counts(Some("raydium_cpmm".to_string())).await;
let result = match result {
Ok(result) => result,
Err(error) => panic!("coverage refresh must succeed: {}", error),
};
assert!(result.upstream_entry_count > 0);
assert_eq!(result.upstream_entry_count, result.upserted_entry_count);
let rows_result = crate::query_dex_event_coverage_entries_list_by_decoder(
database.as_ref(),
"raydium_cpmm",
)
.await;
let rows = match rows_result {
Ok(rows) => rows,
Err(error) => panic!("coverage rows must load: {}", error),
};
assert!(!rows.is_empty());
}
}

View File

@@ -628,7 +628,7 @@ const DEX_SUPPORT_MATRIX_ENTRIES: &[DexSupportMatrixEntry] = &[
version: "unknown",
surface_type: "launch",
surface_role: "launch_surface",
program_id: Some(crate::BOOP_PROGRAM_ID),
program_id: Some(crate::BOOP_FUN_PROGRAM_ID),
router_program_id: None,
program_id_status: "to_verify",
observed: false,
@@ -2934,7 +2934,7 @@ mod tests {
("zora", crate::ZORA_PROGRAM_ID),
("raydium_liquidity_locking", crate::RAYDIUM_LIQUIDITY_LOCKING_PROGRAM_ID),
("okx_dex", crate::OKX_DEX_PROGRAM_ID),
("boop_fun", crate::BOOP_PROGRAM_ID),
("boop_fun", crate::BOOP_FUN_PROGRAM_ID),
("heaven", crate::HEAVEN_PROGRAM_ID),
("bonkswap", crate::BONKSWAP_PROGRAM_ID),
("metadao_launchpad_v0_7_0", crate::METADAO_LAUNCHPAD_V0_7_0_PROGRAM_ID),

View File

@@ -0,0 +1,351 @@
// file: kb_lib/src/instruction_observation_index.rs
//! Local technical index of observed Solana instructions.
//!
//! This index is not a business materialization table. It is an audit/search
//! aid used to find local corpus evidence by program, decoder, instruction
//! discriminator and instruction name.
#[derive(Debug, Clone, sqlx::FromRow)]
struct InstructionObservationSourceRow {
transaction_id: i64,
signature: std::string::String,
slot: std::option::Option<i64>,
block_time: std::option::Option<i64>,
err_json: std::option::Option<std::string::String>,
instruction_id: i64,
parent_instruction_id: std::option::Option<i64>,
instruction_index: i64,
inner_instruction_index: std::option::Option<i64>,
program_id: std::option::Option<std::string::String>,
accounts_json: std::string::String,
data_json: std::option::Option<std::string::String>,
pool_account: std::option::Option<std::string::String>,
decoded_event_kind: std::option::Option<std::string::String>,
decoded_event_id: std::option::Option<i64>,
}
/// Result of refreshing the instruction-observation index.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InstructionObservationIndexRefreshResult {
/// Number of source instruction rows scanned.
pub scanned_instruction_count: usize,
/// Number of observation rows upserted.
pub upserted_observation_count: usize,
}
/// Service that builds and refreshes `k_sol_instruction_observations`.
#[derive(Debug, Clone)]
pub struct InstructionObservationIndexService {
database: std::sync::Arc<crate::Database>,
}
impl InstructionObservationIndexService {
/// Creates a new instruction-observation index service.
pub fn new(database: std::sync::Arc<crate::Database>) -> Self {
return Self { database };
}
/// Refreshes observations for one transaction signature.
pub async fn refresh_signature(
&self,
signature: &str,
) -> Result<crate::InstructionObservationIndexRefreshResult, crate::Error> {
let rows_result = self.list_source_rows_by_signature(signature).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,
limit: u32,
) -> Result<crate::InstructionObservationIndexRefreshResult, crate::Error> {
let rows_result = self.list_recent_source_rows(limit).await;
let rows = match rows_result {
Ok(rows) => rows,
Err(error) => return Err(error),
};
return self.upsert_source_rows(rows).await;
}
async fn upsert_source_rows(
&self,
rows: std::vec::Vec<InstructionObservationSourceRow>,
) -> Result<crate::InstructionObservationIndexRefreshResult, crate::Error> {
let mut result = crate::InstructionObservationIndexRefreshResult::default();
for row in rows {
result.scanned_instruction_count += 1;
let dto_option = build_instruction_observation_dto(row);
let dto = match dto_option {
Some(dto) => dto,
None => continue,
};
let upsert_result =
crate::query_instruction_observations_upsert(self.database.as_ref(), &dto).await;
match upsert_result {
Ok(_) => result.upserted_observation_count += 1,
Err(error) => return Err(error),
}
}
return Ok(result);
}
async fn list_source_rows_by_signature(
&self,
signature: &str,
) -> Result<std::vec::Vec<InstructionObservationSourceRow>, crate::Error> {
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, InstructionObservationSourceRow>(
r#"
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 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
WHERE tx.signature = ?
ORDER BY ins.instruction_index ASC, ins.inner_instruction_index ASC, ins.id ASC
"#,
)
.bind(signature.to_string())
.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 signature '{}': {}",
signature, error
)));
},
}
},
}
}
async fn list_recent_source_rows(
&self,
limit: u32,
) -> Result<std::vec::Vec<InstructionObservationSourceRow>, crate::Error> {
if limit == 0 {
return Ok(std::vec::Vec::new());
}
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let query_result = sqlx::query_as::<sqlx::Sqlite, InstructionObservationSourceRow>(
r#"
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 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 ins.id DESC
LIMIT ?
"#,
)
.bind(i64::from(limit))
.fetch_all(pool)
.await;
match query_result {
Ok(rows) => return Ok(rows),
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot list recent instruction observation source rows: {}",
error
)));
},
}
},
}
}
}
fn build_instruction_observation_dto(
row: InstructionObservationSourceRow,
) -> std::option::Option<crate::InstructionObservationDto> {
let program_id = match row.program_id.clone() {
Some(program_id) => program_id,
None => return None,
};
let discriminator_hex = discriminator_hex_from_data_json(row.data_json.as_ref());
let decoder_code = resolve_decoder_code(program_id.as_str());
let instruction_name = resolve_instruction_name(
program_id.as_str(),
decoder_code.as_deref(),
discriminator_hex.as_deref(),
);
let observation_key = format!(
"{}|{}|{}|{}",
row.signature,
row.instruction_index,
option_i64_key(row.inner_instruction_index),
discriminator_hex.clone().unwrap_or_default()
);
return Some(crate::InstructionObservationDto::new(
observation_key,
row.transaction_id,
row.signature,
row.slot,
row.block_time,
row.err_json.is_some(),
row.instruction_id,
row.parent_instruction_id,
row.instruction_index,
row.inner_instruction_index,
program_id,
decoder_code,
discriminator_hex,
instruction_name,
row.accounts_json,
row.data_json,
row.pool_account,
row.decoded_event_kind,
row.decoded_event_id,
));
}
fn resolve_decoder_code(program_id: &str) -> std::option::Option<std::string::String> {
let entry = crate::dex_support_matrix_entry_by_program_id(program_id);
match entry {
Some(entry) => return Some(entry.code.to_string()),
None => return None,
}
}
fn resolve_instruction_name(
program_id: &str,
decoder_code: std::option::Option<&str>,
discriminator_hex: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
let discriminator_hex = match discriminator_hex {
Some(discriminator_hex) => discriminator_hex,
None => return None,
};
if program_id == crate::RAYDIUM_CPMM_PROGRAM_ID || decoder_code == Some("raydium_cpmm") {
let name = match discriminator_hex {
"9c5420764587467b" => "raydium_cpmm.close_permission_pda",
"1416567bc61cdb84" => "raydium_cpmm.collect_creator_fee",
"a78a4e95dfc2067e" => "raydium_cpmm.collect_fund_fee",
"8888fcddc2427e59" => "raydium_cpmm.collect_protocol_fee",
"8934edd4d7756c68" => "raydium_cpmm.create_amm_config",
"878802d889a9b5ca" => "raydium_cpmm.create_permission_pda",
"f223c68952e1f2b6" => "raydium_cpmm.deposit",
"afaf6d1f0d989bed" => "raydium_cpmm.initialize",
"3f37fe4131b25979" => "raydium_cpmm.initialize_with_permission",
"8fbe5adac41e33de" => "raydium_cpmm.swap_base_input",
"37d96256a34ab4ad" => "raydium_cpmm.swap_base_output",
"313cae889a1c74c8" => "raydium_cpmm.update_amm_config",
"82576c062ee0757b" => "raydium_cpmm.update_pool_status",
"b712469c946da122" => "raydium_cpmm.withdraw",
_ => return None,
};
return Some(name.to_string());
}
return None;
}
fn discriminator_hex_from_data_json(
data_json: std::option::Option<&std::string::String>,
) -> std::option::Option<std::string::String> {
let decoded = match decode_data_json_as_bytes(data_json) {
Some(decoded) => decoded,
None => return None,
};
if decoded.len() < 8 {
return None;
}
return Some(bytes_to_hex(&decoded[0..8]));
}
fn decode_data_json_as_bytes(
data_json: std::option::Option<&std::string::String>,
) -> std::option::Option<std::vec::Vec<u8>> {
let data_json = match data_json {
Some(data_json) => data_json,
None => return None,
};
let parsed_result = serde_json::from_str::<serde_json::Value>(data_json.as_str());
let parsed = match parsed_result {
Ok(parsed) => parsed,
Err(_) => return None,
};
match parsed {
serde_json::Value::String(base58_text) => {
let decoded_result = bs58::decode(base58_text.as_str()).into_vec();
match decoded_result {
Ok(decoded) => return Some(decoded),
Err(_) => return None,
}
},
serde_json::Value::Array(values) => {
let first = match values.first() {
Some(first) => first,
None => return None,
};
let base58_text = match first.as_str() {
Some(base58_text) => base58_text,
None => return None,
};
let decoded_result = bs58::decode(base58_text).into_vec();
match decoded_result {
Ok(decoded) => return Some(decoded),
Err(_) => return None,
}
},
_ => return None,
}
}
fn bytes_to_hex(bytes: &[u8]) -> std::string::String {
let mut text = std::string::String::new();
for byte in bytes {
text.push_str(format!("{:02x}", byte).as_str());
}
return text;
}
fn option_i64_key(value: std::option::Option<i64>) -> std::string::String {
match value {
Some(value) => return value.to_string(),
None => return "-".to_string(),
}
}

View File

@@ -736,7 +736,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DBC_PROGRAM_ID,
"program": "meteora-dbc",
"program": "meteora_dbc",
"stackHeight": 1,
"accounts": [
"DbcDetectPool111",
@@ -829,7 +829,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DAMM_V2_PROGRAM_ID,
"program": "meteora-damm-v2",
"program": "meteora_damm_v2",
"stackHeight": 1,
"accounts": [
"MoonitDammV2Pool111",

View File

@@ -51,6 +51,8 @@ mod error;
mod http_client;
/// HTTP endpoint pool and routing.
mod http_pool;
/// Technical index for observed chain instructions.
mod instruction_observation_index;
/// Generic JSON-RPC 2.0 WebSocket helpers.
mod json_rpc_ws;
/// Launch surface attribution service.
@@ -173,7 +175,7 @@ pub use constants::BONK_MINT_ID;
/// Bonkswap program id extracted from upstream Git decoder source.
pub use constants::BONKSWAP_PROGRAM_ID;
/// Boop program id extracted from upstream Git decoder source.
pub use constants::BOOP_PROGRAM_ID;
pub use constants::BOOP_FUN_PROGRAM_ID;
/// BPF Loader program identifier. ("BPFLoader1111111111111111111111111111111111").
/// @see solana_sdk::pubkey::Pubkey = solana_sdk_ids::bpf_loader_deprecated::ID
pub use constants::BPF_LOADER_DEPRECATED_PROGRAM_ID;
@@ -491,6 +493,10 @@ pub use db::DexEventCoverageSummaryEntity;
pub use db::FeeEventDto;
/// Persisted fee event row.
pub use db::FeeEventEntity;
/// Application-facing on-chain observation DTO.
pub use db::InstructionObservationDto;
/// Persisted technical observation for one Solana instruction.
pub use db::InstructionObservationEntity;
/// Application-facing known HTTP endpoint DTO.
pub use db::KnownHttpEndpointDto;
/// Application-facing known WebSocket endpoint DTO.
@@ -767,6 +773,9 @@ pub use db::query_fee_events_get_by_decoded_event_id;
pub use db::query_fee_events_list_recent;
/// Inserts or updates one normalized fee event row.
pub use db::query_fee_events_upsert;
/// Inserts one on-chain observation row and returns its numeric id.
pub use db::query_instruction_observations_list_by_filter;
pub use db::query_instruction_observations_upsert;
/// Reads one known HTTP endpoint by name.
pub use db::query_known_http_endpoints_get;
/// Lists all known HTTP endpoints.
@@ -1141,14 +1150,22 @@ pub use dex::RaydiumClmmSwapLegacyDecoded;
pub use dex::RaydiumClmmSwapV2Decoded;
/// Raydium CPMM decoded event.
pub use dex::RaydiumCpmmDecodedEvent;
/// Raydium CPMM Anchor CPI liquidity-change event.
pub use dex::RaydiumCpmmLpChangeEventDecoded;
/// Raydium CPMM decoded swap.
pub use dex::RaydiumCpmmSwapDecoded;
/// Raydium CPMM Anchor CPI swap event retained as audit evidence.
pub use dex::RaydiumCpmmSwapEventDecoded;
/// Raydium CPMM swap mode.
pub use dex::RaydiumCpmmSwapMode;
/// Decodes one Raydium CPMM instruction from projected instruction fields.
pub use dex::classify_raydium_cpmm_instruction_data;
/// Decodes a Raydium CLMM instruction.
pub use dex::decode_raydium_clmm_instruction;
/// 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.
pub use dex::decode_raydium_cpmm_program_data_event;
/// DEX decode service.
pub use dex_decode::DexDecodeService;
/// Business-level DEX detection service.
@@ -1263,6 +1280,10 @@ pub use http_client::parse_json_rpc_http_response_value;
pub use http_pool::HttpEndpointPool;
/// Snapshot of one pooled HTTP endpoint.
pub use http_pool::HttpPoolClientSnapshot;
/// Instruction-observation index refresh result.
pub use instruction_observation_index::InstructionObservationIndexRefreshResult;
/// Technical service that indexes observed Solana instructions.
pub use instruction_observation_index::InstructionObservationIndexService;
/// JSON-RPC 2.0 error object.
pub use json_rpc_ws::JsonRpcWsErrorObject;
/// JSON-RPC 2.0 error response.

View File

@@ -825,17 +825,14 @@ async fn query_validation_i64(
async fn load_event_coverage_summaries(
database: &crate::Database,
) -> Result<std::vec::Vec<crate::DexEventCoverageSummaryDto>, crate::Error> {
let refresh_result =
crate::query_dex_event_coverage_entries_refresh_local_counts(database).await;
if let Err(error) = refresh_result {
return Err(error);
}
let summaries_result =
crate::query_dex_event_coverage_entries_list_summary_by_decoder(database).await;
match summaries_result {
Ok(summaries) => return Ok(summaries),
let coverage_service =
crate::DexEventCoverageService::new(std::sync::Arc::new(database.clone()));
let refresh_result = coverage_service.refresh_local_counts(None).await;
let refresh_result = match refresh_result {
Ok(refresh_result) => refresh_result,
Err(error) => return Err(error),
}
};
return Ok(refresh_result.summaries);
}
#[derive(Debug, Clone)]

View File

@@ -183,6 +183,8 @@ impl LocalPipelineReplayService {
let pair_analytic_signal = crate::PairAnalyticSignalService::new(self.database.clone());
let transaction_classification =
crate::TransactionClassificationService::new(self.database.clone());
let instruction_observation_index =
crate::InstructionObservationIndexService::new(self.database.clone());
let mut result = LocalPipelineReplayResult {
selected_transaction_count: signatures.len(),
reset_market_materialization_deleted_count,
@@ -424,6 +426,24 @@ impl LocalPipelineReplayService {
);
},
}
let instruction_index_result =
instruction_observation_index.refresh_signature(signature.as_str()).await;
match instruction_index_result {
Ok(index_result) => {
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.refresh_missing_token_metadata {
@@ -451,9 +471,31 @@ impl LocalPipelineReplayService {
},
}
}
self.refresh_event_coverage_best_effort().await;
return Ok(result);
}
async fn refresh_event_coverage_best_effort(&self) {
let coverage_service = crate::DexEventCoverageService::new(self.database.clone());
let refresh_result = coverage_service.refresh_local_counts(None).await;
match refresh_result {
Ok(refresh_result) => {
tracing::debug!(
upserted_entry_count = refresh_result.upserted_entry_count,
refreshed_entry_count = refresh_result.refreshed_entry_count,
summary_count = refresh_result.summaries.len(),
"dex event coverage refreshed after local pipeline replay"
);
},
Err(error) => {
tracing::warn!(
error = %error,
"dex event coverage refresh failed after local pipeline replay"
);
},
}
}
async fn get_certified_dex_decode_skip_ledger(
&self,
config: &crate::LocalPipelineReplayConfig,
@@ -777,7 +819,12 @@ mod tests {
let ledger = super::build_success_dex_decode_replay_ledger(1, "sig", events.as_slice())
.expect("ledger must build");
assert_eq!(ledger.event_count, 2);
assert_eq!(ledger.status_reason.as_deref(), Some("decode completed and certified for skip: event_count=2, effective_event_count=0, instruction_audit_count=2, distinct_token_mint_count=2"));
assert_eq!(
ledger.status_reason.as_deref(),
Some(
"decode completed and certified for skip: event_count=2, effective_event_count=0, instruction_audit_count=2, distinct_token_mint_count=2"
)
);
assert!(!ledger.force_replay_required);
assert!(ledger.can_skip_decode());
}

View File

@@ -1824,7 +1824,7 @@ mod tests {
summary.event_coverage_upstream_git_local_corpus_observed_entry_count = 1;
summary.event_coverage_upstream_git_local_corpus_materialized_entry_count = 1;
summary.event_coverage_summaries.push(crate::DexEventCoverageSummaryDto {
decoder_code: "raydium-cpmm".to_string(),
decoder_code: "raydium_cpmm".to_string(),
listed_entry_count: 4,
decoded_entry_count: 3,
observed_entry_count: 2,

View File

@@ -102,7 +102,17 @@ impl NonTradeEventMaterializationService {
continue;
},
};
if crate::is_dex_liquidity_event_kind(decoded_event.event_kind.as_str()) {
if crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str()) {
let cleanup_result =
self.delete_stale_pool_admin_event_for_lifecycle(decoded_event).await;
match cleanup_result {
Ok(_) => {},
Err(error) => return Err(error),
}
}
if crate::is_dex_liquidity_event_kind(decoded_event.event_kind.as_str())
&& !decoded_event.event_kind.ends_with(".lp_change_event")
{
let materialized = self
.materialize_liquidity_event(
&transaction,
@@ -159,7 +169,9 @@ impl NonTradeEventMaterializationService {
Err(error) => return Err(error),
}
}
if crate::is_dex_admin_event_kind(decoded_event.event_kind.as_str()) {
if crate::is_dex_admin_event_kind(decoded_event.event_kind.as_str())
&& !crate::is_dex_pool_lifecycle_event_kind(decoded_event.event_kind.as_str())
{
let materialized = self
.materialize_pool_admin_event(
&transaction,
@@ -178,6 +190,36 @@ impl NonTradeEventMaterializationService {
}
}
}
for decoded_event in &decoded_events {
if !decoded_event.event_kind.ends_with(".lp_change_event") {
continue;
}
let payload_result =
serde_json::from_str::<serde_json::Value>(decoded_event.payload_json.as_str());
let payload = match payload_result {
Ok(payload) => payload,
Err(error) => {
tracing::warn!(
signature = %transaction.signature,
event_kind = %decoded_event.event_kind,
error = %error,
"skipping postponed lp_change_event materialization for invalid decoded payload"
);
continue;
},
};
let materialized = self
.materialize_liquidity_event(&transaction, transaction_id, decoded_event, &payload)
.await;
match materialized {
Ok(was_materialized) => {
if was_materialized {
result.liquidity_event_count += 1;
}
},
Err(error) => return Err(error),
}
}
return Ok(result);
}
@@ -274,6 +316,12 @@ impl NonTradeEventMaterializationService {
"fund_fee_amount",
"creatorFeeAmount",
"creator_fee_amount",
"amount0RequestedRaw",
"amount_0_requested_raw",
"amount1RequestedRaw",
"amount_1_requested_raw",
"tokenAAmount",
"tokenBAmount",
"amount",
],
);
@@ -370,6 +418,48 @@ impl NonTradeEventMaterializationService {
}
}
async fn delete_stale_pool_admin_event_for_lifecycle(
&self,
decoded_event: &crate::DexDecodedEventDto,
) -> Result<(), crate::Error> {
let decoded_event_id = match decoded_event.id {
Some(decoded_event_id) => decoded_event_id,
None => return Ok(()),
};
match self.database.connection() {
crate::DatabaseConnection::Sqlite(pool) => {
let delete_result = sqlx::query(
r#"
DELETE FROM k_sol_pool_admin_events
WHERE decoded_event_id = ?
"#,
)
.bind(decoded_event_id)
.execute(pool)
.await;
let delete_result = match delete_result {
Ok(delete_result) => delete_result,
Err(error) => {
return Err(crate::Error::Db(format!(
"cannot delete stale k_sol_pool_admin_events for lifecycle decoded_event_id '{}' on sqlite: {}",
decoded_event_id, error
)));
},
};
let deleted_count = delete_result.rows_affected();
if deleted_count > 0 {
tracing::debug!(
decoded_event_id = decoded_event_id,
event_kind = %decoded_event.event_kind,
deleted_count = deleted_count,
"removed stale pool admin materialization for lifecycle event"
);
}
return Ok(());
},
}
}
async fn materialize_pool_admin_event(
&self,
transaction: &crate::ChainTransactionDto,
@@ -437,6 +527,16 @@ impl NonTradeEventMaterializationService {
Ok(context) => context,
Err(error) => return Err(error),
};
let context = if context.pool_id.is_some() && context.pair.is_some() {
context
} else {
let ensured_context =
self.ensure_liquidity_context_from_decoded_event(decoded_event, context).await;
match ensured_context {
Ok(ensured_context) => ensured_context,
Err(error) => return Err(error),
}
};
let dex_id = match context.dex_id {
Some(dex_id) => dex_id,
None => return Ok(false),
@@ -458,6 +558,12 @@ impl NonTradeEventMaterializationService {
crate::LiquidityEventKind::PositionOpen
} else if crate::is_dex_position_close_event_kind(decoded_event.event_kind.as_str()) {
crate::LiquidityEventKind::PositionClose
} else if decoded_event.event_kind.ends_with(".lp_change_event") {
let change_type = extract_first_u64(payload, &["changeType", "change_type"]);
match change_type {
Some(1) => crate::LiquidityEventKind::Remove,
_ => crate::LiquidityEventKind::Add,
}
} else if crate::is_dex_liquidity_remove_event_kind(decoded_event.event_kind.as_str()) {
crate::LiquidityEventKind::Remove
} else {
@@ -487,6 +593,10 @@ impl NonTradeEventMaterializationService {
"amount_base",
"tokenAAmount",
"token_a_amount",
"token0AmountRaw",
"token_0_amount_raw",
"amount0RequestedRaw",
"amount_0_requested_raw",
"amountA",
"amount_a",
],
@@ -502,6 +612,10 @@ impl NonTradeEventMaterializationService {
"amount_quote",
"tokenBAmount",
"token_b_amount",
"token1AmountRaw",
"token_1_amount_raw",
"amount1RequestedRaw",
"amount_1_requested_raw",
"amountB",
"amount_b",
],
@@ -559,6 +673,54 @@ impl NonTradeEventMaterializationService {
}
}
async fn ensure_liquidity_context_from_decoded_event(
&self,
decoded_event: &crate::DexDecodedEventDto,
context: NonTradeDecodedEventContext,
) -> Result<NonTradeDecodedEventContext, crate::Error> {
let dex_id = match context.dex_id {
Some(dex_id) => dex_id,
None => return Ok(context),
};
if context.pool_id.is_some() && context.pair.is_some() {
return Ok(context);
}
if decoded_event.pool_account.is_none()
|| decoded_event.token_a_mint.is_none()
|| decoded_event.token_b_mint.is_none()
{
return Ok(context);
}
let materialization_input_result =
crate::dex_pool_materialization::DexPoolMaterializationInput::from_decoded_event(
decoded_event,
dex_id,
crate::PoolKind::Amm,
crate::PoolStatus::Active,
crate::dex_pool_materialization::DexPoolTokenOrder::AlreadyBaseQuote,
None,
None,
None,
);
let materialization_input = match materialization_input_result {
Ok(materialization_input) => materialization_input,
Err(_) => return Ok(context),
};
let materialization_result = crate::dex_pool_materialization::materialize_dex_pool(
self.database.as_ref(),
&materialization_input,
)
.await;
if let Err(error) = materialization_result {
return Err(error);
}
let refreshed_context = self.resolve_decoded_event_context(decoded_event).await;
match refreshed_context {
Ok(refreshed_context) => return Ok(refreshed_context),
Err(error) => return Err(error),
}
}
async fn resolve_decoded_event_context(
&self,
decoded_event: &crate::DexDecodedEventDto,
@@ -627,6 +789,29 @@ impl NonTradeEventMaterializationService {
}
}
fn extract_first_u64(
value: &serde_json::Value,
candidate_keys: &[&str],
) -> std::option::Option<u64> {
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_u64() {
return Some(number);
}
if let Some(text) = candidate_value.as_str() {
let parsed = text.parse::<u64>();
if let Ok(parsed) = parsed {
return Some(parsed);
}
}
}
}
}
return None;
}
fn extract_first_amount_string(
value: &serde_json::Value,
candidate_keys: &[&str],

View File

@@ -37,6 +37,12 @@ pub struct OnchainDexPairDiscoveryRequestDto {
pub scan_order: std::option::Option<std::string::String>,
/// Optional target event family used to score and filter candidate signatures.
pub target_event: std::option::Option<std::string::String>,
/// Optional instruction-name filter, for example `raydium_cpmm.withdraw` or `withdraw`.
#[serde(default)]
pub target_instruction_name: std::option::Option<std::string::String>,
/// Optional first-eight-byte discriminator filter as lower hex; accepts comma/space separated values.
#[serde(default)]
pub target_discriminator_hex: std::option::Option<std::string::String>,
/// Whether transactions containing swap-like logs should be skipped.
pub exclude_swaps: bool,
/// Whether failed transactions should be returned as candidates.
@@ -209,6 +215,8 @@ pub struct OnchainDexPairCandidateDto {
pub instruction_name: std::option::Option<std::string::String>,
/// Prefix of the raw base58 instruction data, useful for audit grouping.
pub instruction_data_prefix: std::option::Option<std::string::String>,
/// First eight instruction-data bytes as lower hex.
pub instruction_discriminator_hex: std::option::Option<std::string::String>,
/// Candidate pool address when it can be extracted safely or heuristically.
pub pool_address: std::option::Option<std::string::String>,
/// Candidate token A/base mint when it can be extracted.
@@ -382,6 +390,8 @@ impl OnchainDexPairDiscoveryService {
let logs = extract_log_messages(&transaction_value);
let target_keeps_mixed_swaps = target_event_keeps_mixed_swap_transactions(
normalized_request.target_event.as_deref(),
normalized_request.target_instruction_name.as_deref(),
normalized_request.target_discriminator_hex.as_deref(),
);
if normalized_request.exclude_swaps
&& logs_contain_swap(logs.as_slice())
@@ -396,6 +406,8 @@ impl OnchainDexPairDiscoveryService {
resolved.program_id.as_str(),
resolved.dex_code.clone(),
normalized_request.target_event.as_deref(),
normalized_request.target_instruction_name.as_deref(),
normalized_request.target_discriminator_hex.as_deref(),
);
result.scanned_instruction_count += extraction.scanned_instruction_count;
result.target_program_instruction_count += extraction.target_program_instruction_count;
@@ -683,6 +695,8 @@ fn normalize_request(
max_pages: clamp_u32(default_if_zero(request.max_pages, 1), 1, 25),
scan_order: Some(normalize_scan_order(request.scan_order.as_deref()).to_string()),
target_event: normalize_target_event(request.target_event),
target_instruction_name: normalize_instruction_name_filter(request.target_instruction_name),
target_discriminator_hex: normalize_discriminator_filter(request.target_discriminator_hex),
exclude_swaps: request.exclude_swaps,
include_failed: request.include_failed,
http_role,
@@ -773,6 +787,56 @@ fn normalize_target_event(
return Some(targets.join(","));
}
fn normalize_instruction_name_filter(
value: std::option::Option<std::string::String>,
) -> std::option::Option<std::string::String> {
let value = match normalize_optional_string(value) {
Some(value) => value,
None => return None,
};
let mut normalized = std::vec::Vec::new();
for token in value.split(|character: char| {
return character == ',' || character == ';' || character.is_whitespace();
}) {
let token = token.trim().to_ascii_lowercase();
if token.is_empty() {
continue;
}
push_unique_string(&mut normalized, token.replace('-', "_"));
}
if normalized.is_empty() {
return None;
}
return Some(normalized.join(","));
}
fn normalize_discriminator_filter(
value: std::option::Option<std::string::String>,
) -> std::option::Option<std::string::String> {
let value = match normalize_optional_string(value) {
Some(value) => value,
None => return None,
};
let mut normalized = std::vec::Vec::new();
for token in value.split(|character: char| {
return character == ',' || character == ';' || character.is_whitespace();
}) {
let mut token = token.trim().to_ascii_lowercase();
if token.starts_with("0x") {
token = token[2..].to_string();
}
if token.len() != 16 || !token.chars().all(|character| return character.is_ascii_hexdigit())
{
continue;
}
push_unique_string(&mut normalized, token);
}
if normalized.is_empty() {
return None;
}
return Some(normalized.join(","));
}
fn normalize_signature_source(
value: std::option::Option<std::string::String>,
) -> std::option::Option<std::string::String> {
@@ -974,6 +1038,8 @@ fn extract_candidates_from_transaction(
target_program_id: &str,
dex_code: std::option::Option<std::string::String>,
target_event: std::option::Option<&str>,
target_instruction_name: std::option::Option<&str>,
target_discriminator_hex: std::option::Option<&str>,
) -> OnchainCandidateExtraction {
let mut candidates = std::vec::Vec::new();
let mut rejected_candidate_summary = std::vec::Vec::new();
@@ -1036,6 +1102,19 @@ fn extract_candidates_from_transaction(
);
continue;
}
if !candidate_matches_instruction_filters(
&candidate,
target_instruction_name,
target_discriminator_hex,
) {
target_rejected_candidate_count += 1;
push_rejected_candidate_summary(
&mut rejected_candidate_summary,
&candidate,
"target_instruction_filter",
);
continue;
}
if candidates.iter().any(|existing| {
return candidate_identity_key(existing) == candidate_identity_key(&candidate);
}) {
@@ -1144,6 +1223,9 @@ fn decode_raydium_clmm_candidate(
inner_instruction_index: instruction.inner_instruction_index,
instruction_name: Some("raydium_clmm.swap".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.base_mint),
token_b_mint: Some(event.quote_mint),
@@ -1176,6 +1258,9 @@ fn decode_raydium_clmm_candidate(
inner_instruction_index: instruction.inner_instruction_index,
instruction_name: Some("raydium_clmm.swap_v2".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.base_mint),
token_b_mint: Some(event.quote_mint),
@@ -1257,9 +1342,31 @@ fn decode_raydium_cpmm_candidate(
event.quote_mint,
));
},
_ => return None,
}
}
return None;
let raw_data = match decode_onchain_instruction_data(instruction.data.as_deref()) {
Some(raw_data) => raw_data,
None => return None,
};
let instruction_kind = classify_demo3_raydium_cpmm_instruction(raw_data.as_slice());
if instruction_kind == Demo3RaydiumCpmmInstructionKind::Unknown
|| instruction_kind == Demo3RaydiumCpmmInstructionKind::SwapBaseInput
|| instruction_kind == Demo3RaydiumCpmmInstructionKind::SwapBaseOutput
{
return None;
}
return Some(build_raydium_cpmm_non_swap_candidate(
signature,
slot,
block_time,
failed,
program_id,
dex_code,
instruction,
logs,
instruction_kind,
));
}
fn build_raydium_cpmm_candidate(
@@ -1289,6 +1396,7 @@ fn build_raydium_cpmm_candidate(
inner_instruction_index: instruction.inner_instruction_index,
instruction_name: Some(instruction_name.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(pool_address.clone()),
token_a_mint: Some(token_a_mint),
token_b_mint: Some(token_b_mint),
@@ -1304,6 +1412,267 @@ fn build_raydium_cpmm_candidate(
};
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Demo3RaydiumCpmmInstructionKind {
Unknown,
ClosePermissionPda,
CollectCreatorFee,
CollectFundFee,
CollectProtocolFee,
CreateAmmConfig,
CreatePermissionPda,
Deposit,
Initialize,
InitializeWithPermission,
SwapBaseInput,
SwapBaseOutput,
UpdateAmmConfig,
UpdatePoolStatus,
Withdraw,
}
fn build_raydium_cpmm_non_swap_candidate(
signature: &str,
slot: std::option::Option<u64>,
block_time: std::option::Option<i64>,
failed: bool,
program_id: &str,
dex_code: std::option::Option<std::string::String>,
instruction: &OnchainInstructionCandidate,
logs: &[std::string::String],
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> crate::OnchainDexPairCandidateDto {
let pool_address =
demo3_raydium_cpmm_pool_account(instruction.accounts.as_slice(), instruction_kind);
let token_a_mint =
demo3_raydium_cpmm_token_a_mint(instruction.accounts.as_slice(), instruction_kind);
let token_b_mint =
demo3_raydium_cpmm_token_b_mint(instruction.accounts.as_slice(), instruction_kind);
let candidate_kind = demo3_raydium_cpmm_candidate_kind(instruction_kind).to_string();
let instruction_name = demo3_raydium_cpmm_instruction_name(instruction_kind).to_string();
let verified_pool_address = pool_address.clone();
let backfill_hint = demo3_raydium_cpmm_backfill_hint(
instruction_kind,
candidate_kind.as_str(),
pool_address.as_deref(),
signature,
);
return crate::OnchainDexPairCandidateDto {
signature: signature.to_string(),
slot,
block_time,
failed,
program_id: program_id.to_string(),
dex_code,
candidate_kind,
confidence: "high".to_string(),
instruction_index: instruction.instruction_index,
inner_instruction_index: instruction.inner_instruction_index,
instruction_name: Some(instruction_name),
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
instruction_discriminator_hex: instruction_discriminator_hex(instruction.data.as_deref()),
pool_address,
token_a_mint,
token_b_mint,
verified_pool_address,
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,
};
}
fn classify_demo3_raydium_cpmm_instruction(data: &[u8]) -> Demo3RaydiumCpmmInstructionKind {
if data.len() < 8 {
return Demo3RaydiumCpmmInstructionKind::Unknown;
}
let discriminator = &data[0..8];
if discriminator == [156, 84, 32, 118, 69, 135, 70, 123] {
return Demo3RaydiumCpmmInstructionKind::ClosePermissionPda;
}
if discriminator == [20, 22, 86, 123, 198, 28, 219, 132] {
return Demo3RaydiumCpmmInstructionKind::CollectCreatorFee;
}
if discriminator == [167, 138, 78, 149, 223, 194, 6, 126] {
return Demo3RaydiumCpmmInstructionKind::CollectFundFee;
}
if discriminator == [136, 136, 252, 221, 194, 66, 126, 89] {
return Demo3RaydiumCpmmInstructionKind::CollectProtocolFee;
}
if discriminator == [137, 52, 237, 212, 215, 117, 108, 104] {
return Demo3RaydiumCpmmInstructionKind::CreateAmmConfig;
}
if discriminator == [135, 136, 2, 216, 137, 169, 181, 202] {
return Demo3RaydiumCpmmInstructionKind::CreatePermissionPda;
}
if discriminator == [242, 35, 198, 137, 82, 225, 242, 182] {
return Demo3RaydiumCpmmInstructionKind::Deposit;
}
if discriminator == [175, 175, 109, 31, 13, 152, 155, 237] {
return Demo3RaydiumCpmmInstructionKind::Initialize;
}
if discriminator == [63, 55, 254, 65, 49, 178, 89, 121] {
return Demo3RaydiumCpmmInstructionKind::InitializeWithPermission;
}
if discriminator == [143, 190, 90, 218, 196, 30, 51, 222] {
return Demo3RaydiumCpmmInstructionKind::SwapBaseInput;
}
if discriminator == [55, 217, 98, 86, 163, 74, 180, 173] {
return Demo3RaydiumCpmmInstructionKind::SwapBaseOutput;
}
if discriminator == [49, 60, 174, 136, 154, 28, 116, 200] {
return Demo3RaydiumCpmmInstructionKind::UpdateAmmConfig;
}
if discriminator == [130, 87, 108, 6, 46, 224, 117, 123] {
return Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus;
}
if discriminator == [183, 18, 70, 156, 148, 109, 161, 34] {
return Demo3RaydiumCpmmInstructionKind::Withdraw;
}
return Demo3RaydiumCpmmInstructionKind::Unknown;
}
fn demo3_raydium_cpmm_candidate_kind(
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> &'static str {
match instruction_kind {
Demo3RaydiumCpmmInstructionKind::ClosePermissionPda => return "pool_admin",
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => return "claim_fee",
Demo3RaydiumCpmmInstructionKind::CollectFundFee => return "claim_fee",
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => return "claim_fee",
Demo3RaydiumCpmmInstructionKind::CreateAmmConfig => return "pool_admin",
Demo3RaydiumCpmmInstructionKind::CreatePermissionPda => return "pool_admin",
Demo3RaydiumCpmmInstructionKind::Deposit => return "add_liquidity",
Demo3RaydiumCpmmInstructionKind::Initialize => return "create_pool",
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => return "create_pool",
Demo3RaydiumCpmmInstructionKind::UpdateAmmConfig => return "pool_admin",
Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus => return "pool_admin",
Demo3RaydiumCpmmInstructionKind::Withdraw => return "remove_liquidity",
_ => return "unclassified_instruction",
}
}
fn demo3_raydium_cpmm_instruction_name(
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> &'static str {
match instruction_kind {
Demo3RaydiumCpmmInstructionKind::ClosePermissionPda => {
return "raydium_cpmm.close_permission_pda";
},
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => {
return "raydium_cpmm.collect_creator_fee";
},
Demo3RaydiumCpmmInstructionKind::CollectFundFee => return "raydium_cpmm.collect_fund_fee",
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => {
return "raydium_cpmm.collect_protocol_fee";
},
Demo3RaydiumCpmmInstructionKind::CreateAmmConfig => return "raydium_cpmm.create_amm_config",
Demo3RaydiumCpmmInstructionKind::CreatePermissionPda => {
return "raydium_cpmm.create_permission_pda";
},
Demo3RaydiumCpmmInstructionKind::Deposit => return "raydium_cpmm.deposit",
Demo3RaydiumCpmmInstructionKind::Initialize => return "raydium_cpmm.initialize",
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => {
return "raydium_cpmm.initialize_with_permission";
},
Demo3RaydiumCpmmInstructionKind::UpdateAmmConfig => return "raydium_cpmm.update_amm_config",
Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus => {
return "raydium_cpmm.update_pool_status";
},
Demo3RaydiumCpmmInstructionKind::Withdraw => return "raydium_cpmm.withdraw",
Demo3RaydiumCpmmInstructionKind::SwapBaseInput => return "raydium_cpmm.swap_base_input",
Demo3RaydiumCpmmInstructionKind::SwapBaseOutput => return "raydium_cpmm.swap_base_output",
Demo3RaydiumCpmmInstructionKind::Unknown => return "raydium_cpmm.unknown",
}
}
fn demo3_raydium_cpmm_pool_account(
accounts: &[std::string::String],
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> std::option::Option<std::string::String> {
let index = match instruction_kind {
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => Some(2usize),
Demo3RaydiumCpmmInstructionKind::CollectFundFee => Some(2usize),
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => Some(2usize),
Demo3RaydiumCpmmInstructionKind::Deposit => Some(2usize),
Demo3RaydiumCpmmInstructionKind::Initialize => Some(3usize),
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => Some(4usize),
Demo3RaydiumCpmmInstructionKind::UpdatePoolStatus => Some(1usize),
Demo3RaydiumCpmmInstructionKind::Withdraw => Some(2usize),
_ => None,
};
let index = match index {
Some(index) => index,
None => return None,
};
return demo3_account_at(accounts, index);
}
fn demo3_raydium_cpmm_token_a_mint(
accounts: &[std::string::String],
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> std::option::Option<std::string::String> {
let index = match instruction_kind {
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => Some(6usize),
Demo3RaydiumCpmmInstructionKind::CollectFundFee => Some(6usize),
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => Some(6usize),
Demo3RaydiumCpmmInstructionKind::Deposit => Some(10usize),
Demo3RaydiumCpmmInstructionKind::Initialize => Some(4usize),
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => Some(5usize),
Demo3RaydiumCpmmInstructionKind::Withdraw => Some(10usize),
_ => None,
};
let index = match index {
Some(index) => index,
None => return None,
};
return demo3_account_at(accounts, index);
}
fn demo3_raydium_cpmm_token_b_mint(
accounts: &[std::string::String],
instruction_kind: Demo3RaydiumCpmmInstructionKind,
) -> std::option::Option<std::string::String> {
let index = match instruction_kind {
Demo3RaydiumCpmmInstructionKind::CollectCreatorFee => Some(7usize),
Demo3RaydiumCpmmInstructionKind::CollectFundFee => Some(7usize),
Demo3RaydiumCpmmInstructionKind::CollectProtocolFee => Some(7usize),
Demo3RaydiumCpmmInstructionKind::Deposit => Some(11usize),
Demo3RaydiumCpmmInstructionKind::Initialize => Some(5usize),
Demo3RaydiumCpmmInstructionKind::InitializeWithPermission => Some(6usize),
Demo3RaydiumCpmmInstructionKind::Withdraw => Some(11usize),
_ => None,
};
let index = match index {
Some(index) => index,
None => return None,
};
return demo3_account_at(accounts, index);
}
fn demo3_raydium_cpmm_backfill_hint(
instruction_kind: Demo3RaydiumCpmmInstructionKind,
candidate_kind: &str,
pool_address: std::option::Option<&str>,
signature: &str,
) -> std::string::String {
let instruction_name = demo3_raydium_cpmm_instruction_name(instruction_kind);
if let Some(pool_address) = pool_address {
return format!(
"Raydium CPMM {} candidate '{}'; backfill pool in Demo Pipeline 2: {} ; signature: {}",
candidate_kind, instruction_name, pool_address, signature
);
}
return format!(
"Raydium CPMM {} candidate '{}'; inspect/backfill transaction signature: {}",
candidate_kind, instruction_name, signature
);
}
fn decode_meteora_damm_v1_candidate(
signature: &str,
slot: std::option::Option<u64>,
@@ -1345,6 +1714,7 @@ fn decode_meteora_damm_v1_candidate(
inner_instruction_index: instruction.inner_instruction_index,
instruction_name: Some(instruction_name),
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
instruction_discriminator_hex: instruction_discriminator_hex(instruction.data.as_deref()),
pool_address,
token_a_mint,
token_b_mint,
@@ -1616,6 +1986,7 @@ fn build_heuristic_candidate(
inner_instruction_index: instruction.inner_instruction_index,
instruction_name,
instruction_data_prefix: instruction_data_prefix(instruction.data.as_deref()),
instruction_discriminator_hex: instruction_discriminator_hex(instruction.data.as_deref()),
pool_address: pool_address.clone(),
token_a_mint,
token_b_mint,
@@ -2789,8 +3160,21 @@ fn target_event_prefers_instruction_local_classification(
return false;
}
fn target_event_keeps_mixed_swap_transactions(target_event: std::option::Option<&str>) -> bool {
return !split_target_event_filter(target_event).is_empty();
fn target_event_keeps_mixed_swap_transactions(
target_event: std::option::Option<&str>,
target_instruction_name: std::option::Option<&str>,
target_discriminator_hex: std::option::Option<&str>,
) -> bool {
if !split_target_event_filter(target_event).is_empty() {
return true;
}
if !split_comma_filter(target_instruction_name).is_empty() {
return true;
}
if !split_comma_filter(target_discriminator_hex).is_empty() {
return true;
}
return false;
}
fn instruction_data_prefix(
@@ -2813,6 +3197,23 @@ fn instruction_data_prefix(
return Some(prefix);
}
fn instruction_discriminator_hex(
data: std::option::Option<&str>,
) -> std::option::Option<std::string::String> {
let decoded = match decode_onchain_instruction_data(data) {
Some(decoded) => decoded,
None => return None,
};
if decoded.len() < 8 {
return None;
}
let mut text = std::string::String::new();
for byte in &decoded[0..8] {
text.push_str(format!("{:02x}", byte).as_str());
}
return Some(text);
}
fn text_matches_non_swap_target(lower: &str) -> bool {
return text_matches_pool_admin(lower)
|| text_matches_claim_reward(lower)
@@ -3016,6 +3417,77 @@ fn first_matching_target_event(
return None;
}
fn candidate_matches_instruction_filters(
candidate: &crate::OnchainDexPairCandidateDto,
target_instruction_name: std::option::Option<&str>,
target_discriminator_hex: std::option::Option<&str>,
) -> bool {
if !candidate_matches_instruction_name_filter(candidate, target_instruction_name) {
return false;
}
if !candidate_matches_discriminator_filter(candidate, target_discriminator_hex) {
return false;
}
return true;
}
fn candidate_matches_instruction_name_filter(
candidate: &crate::OnchainDexPairCandidateDto,
target_instruction_name: std::option::Option<&str>,
) -> bool {
let targets = split_comma_filter(target_instruction_name);
if targets.is_empty() {
return true;
}
let instruction_name = match &candidate.instruction_name {
Some(instruction_name) => instruction_name.to_ascii_lowercase(),
None => return false,
};
for target in targets {
if instruction_name == target || instruction_name.ends_with(format!(".{}", target).as_str())
{
return true;
}
}
return false;
}
fn candidate_matches_discriminator_filter(
candidate: &crate::OnchainDexPairCandidateDto,
target_discriminator_hex: std::option::Option<&str>,
) -> bool {
let targets = split_comma_filter(target_discriminator_hex);
if targets.is_empty() {
return true;
}
let discriminator_hex = match &candidate.instruction_discriminator_hex {
Some(discriminator_hex) => discriminator_hex.to_ascii_lowercase(),
None => return false,
};
for target in targets {
if discriminator_hex == target {
return true;
}
}
return false;
}
fn split_comma_filter(value: std::option::Option<&str>) -> std::vec::Vec<std::string::String> {
let value = match value {
Some(value) => value,
None => return std::vec::Vec::new(),
};
let mut output = std::vec::Vec::new();
for token in value.split(',') {
let token = token.trim();
if token.is_empty() {
continue;
}
push_unique_string(&mut output, token.to_string());
}
return output;
}
fn candidate_matches_target_event(
candidate: &crate::OnchainDexPairCandidateDto,
target_event: std::option::Option<&str>,
@@ -3027,10 +3499,10 @@ fn candidate_matches_single_target_event(
candidate: &crate::OnchainDexPairCandidateDto,
target_event: &str,
) -> bool {
if target_event == "unknown_non_swap"
|| target_event == "audit_non_swap_like"
|| target_event == "non_swap"
{
if target_event == "unknown_non_swap" {
return candidate_is_unknown_non_swap_candidate(candidate);
}
if target_event == "audit_non_swap_like" || target_event == "non_swap" {
return candidate_is_non_swap_audit_candidate(candidate);
}
if target_event == "unclassified_instruction" {
@@ -3180,6 +3652,24 @@ fn candidate_kind_is_explicit_surface(candidate_kind: &str) -> bool {
|| candidate_kind == "close_market";
}
fn candidate_is_unknown_non_swap_candidate(candidate: &crate::OnchainDexPairCandidateDto) -> bool {
if candidate.candidate_kind == "swap" || candidate_is_known_trade_like_surface(candidate) {
return false;
}
if candidate_kind_is_explicit_surface(candidate.candidate_kind.as_str()) {
return false;
}
if candidate.candidate_kind == "unclassified_instruction"
|| candidate.candidate_kind == "program_activity"
|| candidate.candidate_kind == "liquidity_or_position"
|| candidate.candidate_kind == "non_swap_activity"
|| candidate.candidate_kind == "upstream_git_instruction"
{
return true;
}
return candidate.instruction_data_prefix.is_some() && candidate.instruction_name.is_none();
}
fn candidate_is_non_swap_audit_candidate(candidate: &crate::OnchainDexPairCandidateDto) -> bool {
if candidate.candidate_kind == "swap" || candidate_is_known_trade_like_surface(candidate) {
return false;
@@ -3659,6 +4149,8 @@ mod tests {
max_pages: 1,
scan_order: None,
target_event: None,
target_instruction_name: None,
target_discriminator_hex: None,
exclude_swaps: false,
include_failed: true,
http_role: "history_backfill".to_string(),
@@ -3689,6 +4181,8 @@ mod tests {
max_pages: 1,
scan_order: None,
target_event: None,
target_instruction_name: None,
target_discriminator_hex: None,
exclude_swaps: false,
include_failed: true,
http_role: "history_backfill".to_string(),
@@ -3713,6 +4207,8 @@ mod tests {
max_pages: 1,
scan_order: None,
target_event: None,
target_instruction_name: None,
target_discriminator_hex: None,
exclude_swaps: false,
include_failed: true,
http_role: "history_backfill".to_string(),
@@ -3762,11 +4258,13 @@ mod tests {
}
});
let extraction = super::extract_candidates_from_transaction(
"sig-openbook-v2-raw",
"sig-openbook_v2-raw",
&transaction,
crate::OPENBOOK_V2_PROGRAM_ID,
Some("openbook_v2".to_string()),
Some("order_place"),
None,
None,
);
assert_eq!(extraction.extracted_candidate_count, 1);
assert_eq!(extraction.target_rejected_candidate_count, 0);
@@ -3833,6 +4331,7 @@ mod tests {
inner_instruction_index: Some(2),
instruction_name: None,
instruction_data_prefix: Some("EVM9wLnauu9H41Gf".to_string()),
instruction_discriminator_hex: None,
pool_address: None,
token_a_mint: None,
token_b_mint: None,
@@ -3987,6 +4486,8 @@ mod tests {
max_pages: 2,
scan_order: Some("oldest_first".to_string()),
target_event: Some("claim_fee,remove_liquidity".to_string()),
target_instruction_name: None,
target_discriminator_hex: None,
exclude_swaps: true,
include_failed: true,
http_role: "history_backfill".to_string(),
@@ -4036,6 +4537,7 @@ mod tests {
inner_instruction_index: None,
instruction_name: Some(instruction_name.to_string()),
instruction_data_prefix: Some("prefix".to_string()),
instruction_discriminator_hex: None,
pool_address: None,
token_a_mint: None,
token_b_mint: None,
@@ -4076,7 +4578,41 @@ mod tests {
fn target_filter_accepts_open_orders_close_candidates() {
let candidate = make_candidate("open_orders_close", "CloseOpenOrdersAccount");
assert!(super::candidate_matches_target_event(&candidate, Some("open_orders_close")));
assert!(super::candidate_matches_target_event(&candidate, Some("unknown_non_swap")));
assert!(!super::candidate_matches_target_event(&candidate, Some("unknown_non_swap")));
assert!(super::candidate_matches_target_event(&candidate, Some("audit_non_swap_like")));
assert!(!super::candidate_matches_target_event(&candidate, Some("close_market")));
}
#[test]
fn target_filter_rejects_known_surface_for_unknown_non_swap() {
let candidate = make_candidate("claim_fee", "CollectProtocolFee");
assert!(!super::candidate_matches_target_event(&candidate, Some("unknown_non_swap")));
assert!(super::candidate_matches_target_event(&candidate, Some("audit_non_swap_like")));
}
#[test]
fn target_filter_accepts_unclassified_instruction_for_unknown_non_swap() {
let candidate = make_candidate("unclassified_instruction", "UnknownInstruction");
assert!(super::candidate_matches_target_event(&candidate, Some("unknown_non_swap")));
}
#[test]
fn target_instruction_filters_match_name_and_discriminator() {
let mut candidate = make_candidate("remove_liquidity", "raydium_cpmm.withdraw");
candidate.instruction_discriminator_hex = Some("b712469c946da122".to_string());
assert!(super::candidate_matches_instruction_filters(
&candidate,
Some("withdraw"),
Some("b712469c946da122")
));
assert!(!super::candidate_matches_instruction_filters(
&candidate,
Some("deposit"),
Some("b712469c946da122")
));
assert!(!super::candidate_matches_instruction_filters(
&candidate,
Some("withdraw"),
Some("f223c68952e1f2b6")
));
}
}

View File

@@ -299,7 +299,7 @@ mod tests {
"instructions": [
{
"programId": crate::METEORA_DBC_PROGRAM_ID,
"program": "meteora-dbc",
"program": "meteora_dbc",
"stackHeight": 1,
"accounts": [
"DbcOriginPool111",

View File

@@ -288,6 +288,7 @@ impl TokenBackfillService {
}
}
self.backfill_missing_token_metadata_best_effort(100).await;
self.refresh_event_coverage_best_effort().await;
let summary_payload = serde_json::json!({
"tokenMint": result.token_mint,
"mintSignatureCount": result.mint_signature_count,
@@ -572,6 +573,26 @@ impl TokenBackfillService {
if let Err(error) = transaction_classification_result {
return Err(error);
}
let instruction_observation_index =
crate::InstructionObservationIndexService::new(self.database.clone());
let instruction_observation_result =
instruction_observation_index.refresh_signature(signature.as_str()).await;
match instruction_observation_result {
Ok(index_result) => {
tracing::debug!(
signature = %signature,
upserted_observation_count = index_result.upserted_observation_count,
"instruction observation index refreshed after signature replay"
);
},
Err(error) => {
tracing::warn!(
signature = %signature,
error = %error,
"instruction observation index refresh failed after signature replay"
);
},
}
return Ok(TokenBackfillSignatureResult {
resolved_transaction_count: 1,
missing_transaction_count: 0,
@@ -715,6 +736,7 @@ impl TokenBackfillService {
}
}
self.backfill_missing_token_metadata_best_effort(100).await;
self.refresh_event_coverage_best_effort().await;
let summary_payload = serde_json::json!({
"poolAddress": result.pool_address,
"poolSignatureCount": result.pool_signature_count,
@@ -785,6 +807,7 @@ impl TokenBackfillService {
Err(error) => return Err(error),
};
self.backfill_missing_token_metadata_best_effort(100).await;
self.refresh_event_coverage_best_effort().await;
let result = crate::SignatureBackfillResult {
signature: trimmed_signature.clone(),
resolved_transaction_count: replay.resolved_transaction_count,
@@ -918,6 +941,26 @@ impl TokenBackfillService {
},
}
}
async fn refresh_event_coverage_best_effort(&self) {
let coverage_service = crate::DexEventCoverageService::new(self.database.clone());
let refresh_result = coverage_service.refresh_local_counts(None).await;
match refresh_result {
Ok(refresh_result) => {
tracing::debug!(
upserted_entry_count = refresh_result.upserted_entry_count,
summary_count = refresh_result.summaries.len(),
"dex event coverage refreshed after historical replay"
);
},
Err(error) => {
tracing::warn!(
error = %error,
"dex event coverage refresh failed after historical replay"
);
},
}
}
}
fn token_backfill_should_retry_http_error(error: &crate::Error) -> bool {

View File

@@ -43,7 +43,7 @@ mod tests {
fn service_search_preserves_normalized_request() {
let service = crate::UpstreamRegistryService::new();
let request = crate::UpstreamRegistrySearchRequestDto {
decoder_code: Some("raydium-cpmm".to_string()),
decoder_code: Some("raydium_cpmm".to_string()),
program_id: None,
program_family: None,
surface_kind: None,

File diff suppressed because it is too large Load Diff

View File

@@ -280,7 +280,7 @@ mod tests {
for (entry_name, discriminator_hex) in expected {
let mut found = false;
for entry in all_entries.as_slice() {
if entry.decoder_code == "openbook-v2"
if entry.decoder_code == "openbook_v2"
&& entry.program_id.as_deref() == Some(crate::OPENBOOK_V2_PROGRAM_ID)
&& entry.entry_kind == crate::ENTRY_KIND_INSTRUCTION
&& entry.entry_name == entry_name
@@ -312,7 +312,7 @@ mod tests {
Some(matched) => matched,
None => panic!("OpenBook v2 place_take_order discriminator must match"),
};
assert_eq!(matched.decoder_code, "openbook-v2".to_string());
assert_eq!(matched.decoder_code, "openbook_v2".to_string());
assert_eq!(matched.entry_name, "place_take_order".to_string());
assert_eq!(matched.discriminator_hex, Some("032c47031ac7cb55".to_string()));
}
@@ -320,65 +320,65 @@ mod tests {
#[test]
fn registry_contains_priority_family_program_seeds() {
let expected_codes = [
"meteora-damm-v2",
"meteora-dbc",
"meteora-dlmm",
"meteora-vault",
"raydium-amm-v4",
"raydium-clmm",
"raydium-cpmm",
"raydium-launchpad",
"raydium-liquidity-locking",
"raydium-stable-swap",
"orca-whirlpool",
"meteora_damm_v2",
"meteora_dbc",
"meteora_dlmm",
"meteora_vault",
"raydium_amm_v4",
"raydium_clmm",
"raydium_cpmm",
"raydium_launchlab",
"raydium_liquidity_locking",
"raydium_stable_swap",
"orca_whirlpools",
"fluxbeam",
"lifinity-amm-v2",
"phoenix-v1",
"openbook-v2",
"stabble-stable-swap",
"stabble-weighted-swap",
"lifinity_v2",
"phoenix_v1",
"openbook_v2",
"stabble_stable_swap",
"stabble_weighted_swap",
"bonkswap",
"boop",
"boop_fun",
"moonshot",
"heaven",
"okx-dex",
"pancake-swap",
"okx_dex",
"pancake_swap",
"vertigo",
"virtuals",
"wavebreak",
"onchain-labs-dex-v1",
"onchain-labs-dex-v2",
"jupiter-swap",
"jupiter-dca",
"jupiter-limit-order",
"jupiter-limit-order-2",
"jupiter-perpetuals",
"jupiter-lend",
"kamino-lending",
"kamino-vault",
"kamino-farms",
"kamino-limit-order",
"drift-v2",
"marginfi-v2",
"dflow-aggregator-v4",
"onchain_labs_dex_v1",
"onchain_labs_dex_v2",
"jupiter_swap",
"jupiter_dca",
"jupiter_limit_order",
"jupiter_limit_order_2",
"jupiter_perpetuals",
"jupiter_lend",
"kamino_lending",
"kamino_vault",
"kamino_farms",
"kamino_limit_order",
"drift_v2",
"marginfi_v2",
"dflow_aggregator_v4",
"zeta",
"system-program",
"token-program",
"token-2022",
"associated-token-account",
"address-lookup-table",
"memo-program",
"stake-program",
"mpl-token-metadata",
"mpl-core",
"system_program",
"token_program",
"token_2022",
"associated_token_account",
"address_lookup_table",
"memo_program",
"stake_program",
"mpl_token_metadata",
"mpl_core",
"bubblegum",
"name-service",
"marinade-finance",
"solayer-restaking-program",
"name_service",
"marinade_finance",
"solayer_restaking_program",
"swig",
"sharky",
"circle-message-transmitter-v2",
"circle-token-messenger-v2",
"circle_message_transmitter_v2",
"circle_token_messenger_v2",
];
let all_entries = crate::upstream_registry_match::upstream_registry_all_entries();
for expected_code in expected_codes {
@@ -527,7 +527,7 @@ mod tests {
for expected_entry in expected_entries {
let mut found = false;
for entry in all_entries.as_slice() {
if entry.decoder_code == "meteora-damm-v2"
if entry.decoder_code == "meteora_damm_v2"
&& entry.entry_kind == crate::ENTRY_KIND_INSTRUCTION
&& entry.entry_name == expected_entry.0
&& entry.discriminator_hex.as_deref() == Some(expected_entry.1)
@@ -555,7 +555,7 @@ mod tests {
for expected_entry in expected_entries {
let mut found = false;
for entry in all_entries.as_slice() {
if entry.decoder_code == "meteora-damm-v2"
if entry.decoder_code == "meteora_damm_v2"
&& entry.entry_kind == crate::ENTRY_KIND_EVENT
&& entry.entry_name == expected_entry.0
&& entry.discriminator_hex.as_deref() == Some(expected_entry.1)
@@ -581,9 +581,9 @@ mod tests {
);
let matched = match matched {
Some(matched) => matched,
None => panic!("missing meteora-damm-v2 add_liquidity registry match"),
None => panic!("missing meteora_damm_v2 add_liquidity registry match"),
};
assert_eq!(matched.decoder_code, "meteora-damm-v2");
assert_eq!(matched.decoder_code, "meteora_damm_v2");
assert_eq!(matched.entry_name, "add_liquidity");
assert_eq!(matched.discriminator_hex.as_deref(), Some("b59d59438fb63448"));
assert_eq!(matched.proof_status, crate::PROOF_STATUS_UPSTREAM_GIT_UNVERIFIED);
@@ -599,9 +599,9 @@ mod tests {
);
let matched = match matched {
Some(matched) => matched,
None => panic!("missing phoenix-v1 swap registry match"),
None => panic!("missing phoenix_v1 swap registry match"),
};
assert_eq!(matched.decoder_code, "phoenix-v1");
assert_eq!(matched.decoder_code, "phoenix_v1");
assert_eq!(matched.entry_name, "swap");
assert_eq!(matched.discriminator_hex.as_deref(), Some("00"));
assert_eq!(matched.discriminator_len, Some(1));
@@ -621,7 +621,7 @@ mod tests {
let result = crate::upstream_registry_match::upstream_registry_search(&request);
assert!(result.entries.len() >= 2);
for entry in result.entries.as_slice() {
assert_eq!(entry.decoder_code, "raydium-cpmm");
assert_eq!(entry.decoder_code, "raydium_cpmm");
}
}