0.7.48
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
155
kb_lib/src/db/dtos/instruction_observation.rs
Normal file
155
kb_lib/src/db/dtos/instruction_observation.rs
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
52
kb_lib/src/db/entities/instruction_observation.rs
Normal file
52
kb_lib/src/db/entities/instruction_observation.rs
Normal 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,
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
173
kb_lib/src/db/queries/instruction_observation.rs
Normal file
173
kb_lib/src/db/queries/instruction_observation.rs
Normal 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);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
351
kb_lib/src/instruction_observation_index.rs
Normal file
351
kb_lib/src/instruction_observation_index.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,7 +299,7 @@ mod tests {
|
||||
"instructions": [
|
||||
{
|
||||
"programId": crate::METEORA_DBC_PROGRAM_ID,
|
||||
"program": "meteora-dbc",
|
||||
"program": "meteora_dbc",
|
||||
"stackHeight": 1,
|
||||
"accounts": [
|
||||
"DbcOriginPool111",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user