0.7.28 - final
This commit is contained in:
@@ -27,6 +27,7 @@ mod pool;
|
||||
mod pool_listing;
|
||||
mod pool_origin;
|
||||
mod pool_token;
|
||||
mod program_instruction_diagnostic;
|
||||
mod protocol_candidate;
|
||||
mod protocol_candidate_summary;
|
||||
mod swap;
|
||||
@@ -37,6 +38,7 @@ mod trade_event;
|
||||
mod transaction_classification;
|
||||
mod wallet;
|
||||
mod wallet_holding;
|
||||
mod program_instruction_discriminator_summary;
|
||||
mod wallet_participation;
|
||||
|
||||
pub(crate) use local_pipeline_diagnostics::LocalDecodedEventDiagnosticSummaryRow;
|
||||
@@ -50,6 +52,7 @@ pub(crate) use local_pipeline_diagnostics::LocalPairDiagnosticSummaryRow;
|
||||
pub(crate) use local_pipeline_diagnostics::LocalPairGapDiagnosticSampleRow;
|
||||
pub(crate) use local_pipeline_diagnostics::LocalPipelineDiagnosticCountersRow;
|
||||
|
||||
pub use program_instruction_discriminator_summary::ProgramInstructionDiscriminatorSummaryDto;
|
||||
pub use analysis_signal::AnalysisSignalDto;
|
||||
pub use chain_instruction::ChainInstructionDto;
|
||||
pub use chain_slot::ChainSlotDto;
|
||||
@@ -85,6 +88,7 @@ pub use pool::PoolDto;
|
||||
pub use pool_listing::PoolListingDto;
|
||||
pub use pool_origin::PoolOriginDto;
|
||||
pub use pool_token::PoolTokenDto;
|
||||
pub use program_instruction_diagnostic::ProgramInstructionDiagnosticDto;
|
||||
pub use protocol_candidate::ProtocolCandidateDto;
|
||||
pub use protocol_candidate_summary::ProtocolCandidateSummaryDto;
|
||||
pub use swap::SwapDto;
|
||||
|
||||
298
kb_lib/src/db/dtos/program_instruction_diagnostic.rs
Normal file
298
kb_lib/src/db/dtos/program_instruction_diagnostic.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
// file: kb_lib/src/db/dtos/program_instruction_diagnostic.rs
|
||||
|
||||
//! Program instruction diagnostic DTO.
|
||||
|
||||
/// Diagnostic row for instructions of one Solana program.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProgramInstructionDiagnosticDto {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Transaction signature.
|
||||
pub signature: std::string::String,
|
||||
/// Optional Solana slot.
|
||||
pub slot: std::option::Option<u64>,
|
||||
/// Internal instruction id.
|
||||
pub instruction_id: i64,
|
||||
/// Optional parent instruction id.
|
||||
pub parent_instruction_id: std::option::Option<i64>,
|
||||
/// Outer instruction index.
|
||||
pub instruction_index: u32,
|
||||
/// Optional inner instruction index.
|
||||
pub inner_instruction_index: std::option::Option<u32>,
|
||||
/// Program id.
|
||||
pub program_id: std::option::Option<std::string::String>,
|
||||
/// Optional program name.
|
||||
pub program_name: std::option::Option<std::string::String>,
|
||||
/// Optional stack height.
|
||||
pub stack_height: std::option::Option<u32>,
|
||||
/// Number of accounts in `accounts_json`.
|
||||
pub accounts_count: u64,
|
||||
/// First account, when present.
|
||||
pub account_0: std::option::Option<std::string::String>,
|
||||
/// Second account, when present.
|
||||
pub account_1: std::option::Option<std::string::String>,
|
||||
/// Third account, when present.
|
||||
pub account_2: std::option::Option<std::string::String>,
|
||||
/// Fourth account, when present.
|
||||
pub account_3: std::option::Option<std::string::String>,
|
||||
/// Last account, when present.
|
||||
pub last_account: std::option::Option<std::string::String>,
|
||||
/// Optional parsed instruction type.
|
||||
pub parsed_type: std::option::Option<std::string::String>,
|
||||
/// True when `data_json` exists.
|
||||
pub has_data_json: bool,
|
||||
/// True when `parsed_json` exists.
|
||||
pub has_parsed_json: bool,
|
||||
/// Short data JSON preview.
|
||||
pub data_json_preview: std::option::Option<std::string::String>,
|
||||
/// Short parsed JSON preview.
|
||||
pub parsed_json_preview: std::option::Option<std::string::String>,
|
||||
/// JSON array of useful log hints.
|
||||
pub log_hints_json: std::string::String,
|
||||
}
|
||||
|
||||
impl TryFrom<crate::ProgramInstructionDiagnosticEntity> for ProgramInstructionDiagnosticDto {
|
||||
type Error = crate::Error;
|
||||
|
||||
fn try_from(entity: crate::ProgramInstructionDiagnosticEntity) -> Result<Self, Self::Error> {
|
||||
let slot = match entity.slot {
|
||||
Some(slot) => match u64::try_from(slot) {
|
||||
Ok(slot) => Some(slot),
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot convert program instruction diagnostic slot '{}' to u64: {}",
|
||||
slot, error
|
||||
)));
|
||||
},
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let instruction_index = match u32::try_from(entity.instruction_index) {
|
||||
Ok(instruction_index) => instruction_index,
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot convert program instruction diagnostic instruction_index '{}' to u32: {}",
|
||||
entity.instruction_index, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let inner_instruction_index = match entity.inner_instruction_index {
|
||||
Some(inner_instruction_index) => match u32::try_from(inner_instruction_index) {
|
||||
Ok(inner_instruction_index) => Some(inner_instruction_index),
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot convert program instruction diagnostic inner_instruction_index '{}' to u32: {}",
|
||||
inner_instruction_index, error
|
||||
)));
|
||||
},
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let stack_height = match entity.stack_height {
|
||||
Some(stack_height) => match u32::try_from(stack_height) {
|
||||
Ok(stack_height) => Some(stack_height),
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot convert program instruction diagnostic stack_height '{}' to u32: {}",
|
||||
stack_height, error
|
||||
)));
|
||||
},
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let accounts = parse_accounts_json(entity.accounts_json.as_str());
|
||||
let accounts_count = match u64::try_from(accounts.len()) {
|
||||
Ok(accounts_count) => accounts_count,
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot convert accounts count to u64: {}",
|
||||
error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let account_0 = account_at(&accounts, 0);
|
||||
let account_1 = account_at(&accounts, 1);
|
||||
let account_2 = account_at(&accounts, 2);
|
||||
let account_3 = account_at(&accounts, 3);
|
||||
let last_account = match accounts.last() {
|
||||
Some(last_account) => Some(last_account.clone()),
|
||||
None => None,
|
||||
};
|
||||
let log_hints =
|
||||
collect_log_hints(entity.meta_json.as_deref(), entity.transaction_json.as_str());
|
||||
let log_hints_json_result = serde_json::to_string(&log_hints);
|
||||
let log_hints_json = match log_hints_json_result {
|
||||
Ok(log_hints_json) => log_hints_json,
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot serialize program instruction log hints: {}",
|
||||
error
|
||||
)));
|
||||
},
|
||||
};
|
||||
return Ok(Self {
|
||||
transaction_id: entity.transaction_id,
|
||||
signature: entity.signature,
|
||||
slot,
|
||||
instruction_id: entity.instruction_id,
|
||||
parent_instruction_id: entity.parent_instruction_id,
|
||||
instruction_index,
|
||||
inner_instruction_index,
|
||||
program_id: entity.program_id,
|
||||
program_name: entity.program_name,
|
||||
stack_height,
|
||||
accounts_count,
|
||||
account_0,
|
||||
account_1,
|
||||
account_2,
|
||||
account_3,
|
||||
last_account,
|
||||
parsed_type: entity.parsed_type,
|
||||
has_data_json: entity.data_json.is_some(),
|
||||
has_parsed_json: entity.parsed_json.is_some(),
|
||||
data_json_preview: preview_text(entity.data_json.as_deref(), 600),
|
||||
parsed_json_preview: preview_text(entity.parsed_json.as_deref(), 1200),
|
||||
log_hints_json,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn account_at(
|
||||
accounts: &[std::string::String],
|
||||
index: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
match accounts.get(index) {
|
||||
Some(account) => return Some(account.clone()),
|
||||
None => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn preview_text(
|
||||
text: std::option::Option<&str>,
|
||||
max_len: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let text = match text {
|
||||
Some(text) => text,
|
||||
None => return None,
|
||||
};
|
||||
if text.len() <= max_len {
|
||||
return Some(text.to_string());
|
||||
}
|
||||
let mut preview = text.chars().take(max_len).collect::<std::string::String>();
|
||||
preview.push_str("...");
|
||||
return Some(preview);
|
||||
}
|
||||
|
||||
fn parse_accounts_json(accounts_json: &str) -> std::vec::Vec<std::string::String> {
|
||||
let parsed_result = serde_json::from_str::<serde_json::Value>(accounts_json);
|
||||
let parsed = match parsed_result {
|
||||
Ok(parsed) => parsed,
|
||||
Err(_) => return std::vec::Vec::new(),
|
||||
};
|
||||
let array = match parsed.as_array() {
|
||||
Some(array) => array,
|
||||
None => return std::vec::Vec::new(),
|
||||
};
|
||||
let mut accounts = std::vec::Vec::new();
|
||||
for item in array {
|
||||
if let Some(text) = item.as_str() {
|
||||
accounts.push(text.to_string());
|
||||
continue;
|
||||
}
|
||||
if let Some(pubkey) = item.get("pubkey").and_then(|value| value.as_str()) {
|
||||
accounts.push(pubkey.to_string());
|
||||
}
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
fn collect_log_hints(
|
||||
meta_json: std::option::Option<&str>,
|
||||
transaction_json: &str,
|
||||
) -> std::vec::Vec<std::string::String> {
|
||||
let mut hints = std::vec::Vec::new();
|
||||
if let Some(meta_json) = meta_json {
|
||||
collect_log_hints_from_json_text(meta_json, &mut hints);
|
||||
}
|
||||
collect_log_hints_from_json_text(transaction_json, &mut hints);
|
||||
hints.sort();
|
||||
hints.dedup();
|
||||
return hints;
|
||||
}
|
||||
|
||||
fn collect_log_hints_from_json_text(
|
||||
json_text: &str,
|
||||
hints: &mut std::vec::Vec<std::string::String>,
|
||||
) {
|
||||
let value_result = serde_json::from_str::<serde_json::Value>(json_text);
|
||||
let value = match value_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
collect_log_hints_from_value(&value, hints);
|
||||
}
|
||||
|
||||
fn collect_log_hints_from_value(
|
||||
value: &serde_json::Value,
|
||||
hints: &mut std::vec::Vec<std::string::String>,
|
||||
) {
|
||||
match value {
|
||||
serde_json::Value::String(text) => {
|
||||
let normalized = text.to_ascii_lowercase();
|
||||
if normalized.contains("instruction:")
|
||||
|| normalized.contains("meteora")
|
||||
|| normalized.contains("dlmm")
|
||||
|| normalized.contains("lb")
|
||||
|| normalized.contains("swap")
|
||||
|| normalized.contains("bin")
|
||||
{
|
||||
hints.push(text.clone());
|
||||
}
|
||||
},
|
||||
serde_json::Value::Array(values) => {
|
||||
for nested in values {
|
||||
collect_log_hints_from_value(nested, hints);
|
||||
}
|
||||
},
|
||||
serde_json::Value::Object(object) => {
|
||||
for nested in object.values() {
|
||||
collect_log_hints_from_value(nested, hints);
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn accounts_json_extracts_string_accounts() {
|
||||
let accounts = super::parse_accounts_json(
|
||||
serde_json::json!(["A111", "B222", "C333"]).to_string().as_str(),
|
||||
);
|
||||
assert_eq!(accounts.len(), 3);
|
||||
assert_eq!(accounts[0], "A111");
|
||||
assert_eq!(accounts[1], "B222");
|
||||
assert_eq!(accounts[2], "C333");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_hints_are_extracted_from_nested_json() {
|
||||
let mut hints = std::vec::Vec::new();
|
||||
super::collect_log_hints_from_json_text(
|
||||
serde_json::json!({
|
||||
"meta": {
|
||||
"logMessages": [
|
||||
"Program log: Instruction: Swap",
|
||||
"irrelevant"
|
||||
]
|
||||
}
|
||||
})
|
||||
.to_string()
|
||||
.as_str(),
|
||||
&mut hints,
|
||||
);
|
||||
assert_eq!(hints.len(), 1);
|
||||
assert_eq!(hints[0], "Program log: Instruction: Swap");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// file: kb_lib/src/db/dtos/program_instruction_discriminator_summary.rs
|
||||
|
||||
//! Program instruction discriminator summary DTO.
|
||||
|
||||
/// Aggregated instruction discriminator diagnostic row.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ProgramInstructionDiscriminatorSummaryDto {
|
||||
/// Program id.
|
||||
pub program_id: std::string::String,
|
||||
/// First eight decoded instruction-data bytes as hex.
|
||||
pub discriminator_hex: std::option::Option<std::string::String>,
|
||||
/// Known instruction name, when recognized by local diagnostic mapping.
|
||||
pub known_instruction_name: std::option::Option<std::string::String>,
|
||||
/// Number of accounts in the instruction.
|
||||
pub accounts_count: u64,
|
||||
/// Optional stack height.
|
||||
pub stack_height: std::option::Option<u32>,
|
||||
/// True when the grouped instructions are inner instructions.
|
||||
pub is_inner_instruction: bool,
|
||||
/// Number of instruction rows in this group.
|
||||
pub occurrence_count: u64,
|
||||
/// Number of distinct transactions in this group.
|
||||
pub transaction_count: u64,
|
||||
/// Number of instruction rows already linked to decoded events.
|
||||
pub decoded_event_count: u64,
|
||||
/// Number of instruction rows not linked to decoded events.
|
||||
pub undecoded_occurrence_count: u64,
|
||||
/// Latest observed slot.
|
||||
pub latest_slot: std::option::Option<u64>,
|
||||
/// Latest signature in this group.
|
||||
pub latest_signature: std::string::String,
|
||||
/// Latest instruction id in this group.
|
||||
pub latest_instruction_id: i64,
|
||||
/// Latest outer instruction index.
|
||||
pub latest_instruction_index: u32,
|
||||
/// Latest inner instruction index.
|
||||
pub latest_inner_instruction_index: std::option::Option<u32>,
|
||||
/// Latest parsed type.
|
||||
pub latest_parsed_type: std::option::Option<std::string::String>,
|
||||
/// Latest decoded event kind.
|
||||
pub latest_decoded_event_kind: std::option::Option<std::string::String>,
|
||||
/// Data JSON preview from the latest row.
|
||||
pub latest_data_json_preview: std::option::Option<std::string::String>,
|
||||
/// Accounts JSON preview from the latest row.
|
||||
pub latest_accounts_json_preview: std::option::Option<std::string::String>,
|
||||
}
|
||||
@@ -28,6 +28,8 @@ mod pool;
|
||||
mod pool_listing;
|
||||
mod pool_origin;
|
||||
mod pool_token;
|
||||
mod program_instruction_diagnostic;
|
||||
mod program_instruction_discriminator_row;
|
||||
mod protocol_candidate;
|
||||
mod protocol_candidate_summary;
|
||||
mod swap;
|
||||
@@ -64,6 +66,8 @@ pub use pool::PoolEntity;
|
||||
pub use pool_listing::PoolListingEntity;
|
||||
pub use pool_origin::PoolOriginEntity;
|
||||
pub use pool_token::PoolTokenEntity;
|
||||
pub use program_instruction_diagnostic::ProgramInstructionDiagnosticEntity;
|
||||
pub use program_instruction_discriminator_row::ProgramInstructionDiscriminatorRowEntity;
|
||||
pub use protocol_candidate::ProtocolCandidateEntity;
|
||||
pub use protocol_candidate_summary::ProtocolCandidateSummaryEntity;
|
||||
pub use swap::SwapEntity;
|
||||
|
||||
40
kb_lib/src/db/entities/program_instruction_diagnostic.rs
Normal file
40
kb_lib/src/db/entities/program_instruction_diagnostic.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
// file: kb_lib/src/db/entities/program_instruction_diagnostic.rs
|
||||
|
||||
//! Program instruction diagnostic entity.
|
||||
|
||||
/// Raw diagnostic row for instructions of one Solana program.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
|
||||
pub struct ProgramInstructionDiagnosticEntity {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Transaction signature.
|
||||
pub signature: std::string::String,
|
||||
/// Optional Solana slot.
|
||||
pub slot: std::option::Option<i64>,
|
||||
/// Internal instruction 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>,
|
||||
/// Optional program id.
|
||||
pub program_id: std::option::Option<std::string::String>,
|
||||
/// Optional program name.
|
||||
pub program_name: std::option::Option<std::string::String>,
|
||||
/// Optional stack height.
|
||||
pub stack_height: std::option::Option<i64>,
|
||||
/// Serialized accounts JSON.
|
||||
pub accounts_json: std::string::String,
|
||||
/// Optional serialized data JSON.
|
||||
pub data_json: std::option::Option<std::string::String>,
|
||||
/// Optional parsed instruction type.
|
||||
pub parsed_type: std::option::Option<std::string::String>,
|
||||
/// Optional serialized parsed JSON.
|
||||
pub parsed_json: std::option::Option<std::string::String>,
|
||||
/// Optional transaction meta JSON.
|
||||
pub meta_json: std::option::Option<std::string::String>,
|
||||
/// Full transaction JSON.
|
||||
pub transaction_json: std::string::String,
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
|
||||
// file: kb_lib/src/db/entities/program_instruction_discriminator_row.rs
|
||||
|
||||
//! Program instruction discriminator diagnostic row entity.
|
||||
|
||||
/// Raw row used to summarize instruction discriminators for one Solana program.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::FromRow)]
|
||||
pub struct ProgramInstructionDiscriminatorRowEntity {
|
||||
/// Parent transaction id.
|
||||
pub transaction_id: i64,
|
||||
/// Transaction signature.
|
||||
pub signature: std::string::String,
|
||||
/// Optional Solana slot.
|
||||
pub slot: std::option::Option<i64>,
|
||||
/// Internal instruction 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>,
|
||||
/// Optional program id.
|
||||
pub program_id: std::option::Option<std::string::String>,
|
||||
/// Optional stack height.
|
||||
pub stack_height: std::option::Option<i64>,
|
||||
/// Serialized accounts JSON.
|
||||
pub accounts_json: std::string::String,
|
||||
/// Optional serialized data JSON.
|
||||
pub data_json: std::option::Option<std::string::String>,
|
||||
/// Optional parsed instruction type.
|
||||
pub parsed_type: std::option::Option<std::string::String>,
|
||||
/// Optional decoded event id for this instruction.
|
||||
pub decoded_event_id: std::option::Option<i64>,
|
||||
/// Optional decoded event kind for this instruction.
|
||||
pub decoded_event_kind: std::option::Option<std::string::String>,
|
||||
}
|
||||
@@ -27,6 +27,7 @@ mod pool;
|
||||
mod pool_listing;
|
||||
mod pool_origin;
|
||||
mod pool_token;
|
||||
mod program_instruction_diagnostic;
|
||||
mod protocol_candidate;
|
||||
mod swap;
|
||||
mod token;
|
||||
@@ -120,6 +121,8 @@ pub use pool_origin::query_pool_origins_list;
|
||||
pub use pool_origin::query_pool_origins_upsert;
|
||||
pub use pool_token::query_pool_tokens_list_by_pool_id;
|
||||
pub use pool_token::query_pool_tokens_upsert;
|
||||
pub use program_instruction_diagnostic::query_program_instruction_diagnostics_list_by_program_id;
|
||||
pub use program_instruction_diagnostic::query_program_instruction_discriminator_summaries_list_by_program_id;
|
||||
pub use protocol_candidate::query_protocol_candidate_summaries_list_by_priority;
|
||||
pub use protocol_candidate::query_protocol_candidates_delete_by_transaction_id;
|
||||
pub use protocol_candidate::query_protocol_candidates_insert;
|
||||
|
||||
451
kb_lib/src/db/queries/program_instruction_diagnostic.rs
Normal file
451
kb_lib/src/db/queries/program_instruction_diagnostic.rs
Normal file
@@ -0,0 +1,451 @@
|
||||
// file: kb_lib/src/db/queries/program_instruction_diagnostic.rs
|
||||
|
||||
//! Queries for program instruction diagnostics.
|
||||
|
||||
/// Lists diagnostic instruction rows for one program id.
|
||||
pub async fn query_program_instruction_diagnostics_list_by_program_id(
|
||||
database: &crate::Database,
|
||||
program_id: &str,
|
||||
limit: u32,
|
||||
) -> Result<std::vec::Vec<crate::ProgramInstructionDiagnosticDto>, 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::ProgramInstructionDiagnosticEntity>(
|
||||
r#"
|
||||
SELECT
|
||||
tx.id AS transaction_id,
|
||||
tx.signature AS signature,
|
||||
tx.slot AS slot,
|
||||
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.program_name AS program_name,
|
||||
ins.stack_height AS stack_height,
|
||||
ins.accounts_json AS accounts_json,
|
||||
ins.data_json AS data_json,
|
||||
ins.parsed_type AS parsed_type,
|
||||
ins.parsed_json AS parsed_json,
|
||||
tx.meta_json AS meta_json,
|
||||
tx.transaction_json AS transaction_json
|
||||
FROM k_sol_chain_instructions ins
|
||||
JOIN k_sol_chain_transactions tx
|
||||
ON tx.id = ins.transaction_id
|
||||
WHERE ins.program_id = ?
|
||||
ORDER BY
|
||||
tx.slot DESC,
|
||||
tx.id DESC,
|
||||
ins.instruction_index ASC,
|
||||
ins.inner_instruction_index ASC,
|
||||
ins.id ASC
|
||||
LIMIT ?
|
||||
"#,
|
||||
)
|
||||
.bind(program_id.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 program instruction diagnostics for program_id '{}' on sqlite: {}",
|
||||
program_id, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let mut dtos = std::vec::Vec::new();
|
||||
for entity in entities {
|
||||
let dto_result = crate::ProgramInstructionDiagnosticDto::try_from(entity);
|
||||
let dto = match dto_result {
|
||||
Ok(dto) => dto,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
dtos.push(dto);
|
||||
}
|
||||
return Ok(dtos);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct ProgramInstructionDiscriminatorSummaryKey {
|
||||
program_id: std::string::String,
|
||||
discriminator_hex: std::option::Option<std::string::String>,
|
||||
accounts_count: u64,
|
||||
stack_height: std::option::Option<u32>,
|
||||
is_inner_instruction: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ProgramInstructionDiscriminatorSummaryAccumulator {
|
||||
key: ProgramInstructionDiscriminatorSummaryKey,
|
||||
known_instruction_name: std::option::Option<std::string::String>,
|
||||
occurrence_count: u64,
|
||||
decoded_event_count: u64,
|
||||
transaction_signatures: std::collections::BTreeSet<std::string::String>,
|
||||
latest_slot: std::option::Option<u64>,
|
||||
latest_signature: std::string::String,
|
||||
latest_instruction_id: i64,
|
||||
latest_instruction_index: u32,
|
||||
latest_inner_instruction_index: std::option::Option<u32>,
|
||||
latest_parsed_type: std::option::Option<std::string::String>,
|
||||
latest_decoded_event_kind: std::option::Option<std::string::String>,
|
||||
latest_data_json_preview: std::option::Option<std::string::String>,
|
||||
latest_accounts_json_preview: std::option::Option<std::string::String>,
|
||||
}
|
||||
|
||||
fn build_program_instruction_discriminator_summaries(
|
||||
rows: std::vec::Vec<crate::ProgramInstructionDiscriminatorRowEntity>,
|
||||
) -> Result<std::vec::Vec<crate::ProgramInstructionDiscriminatorSummaryDto>, crate::Error> {
|
||||
let mut grouped = std::collections::BTreeMap::<
|
||||
ProgramInstructionDiscriminatorSummaryKey,
|
||||
ProgramInstructionDiscriminatorSummaryAccumulator,
|
||||
>::new();
|
||||
for row in rows {
|
||||
let summary_row_result = build_summary_row_from_discriminator_entity(row);
|
||||
let summary_row = match summary_row_result {
|
||||
Ok(summary_row) => summary_row,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let existing = grouped.get_mut(&summary_row.key);
|
||||
match existing {
|
||||
Some(existing) => {
|
||||
existing.occurrence_count += 1;
|
||||
if summary_row.decoded_event_count > 0 {
|
||||
existing.decoded_event_count += 1;
|
||||
}
|
||||
existing.transaction_signatures.insert(summary_row.latest_signature.clone());
|
||||
if summary_row.latest_instruction_id > existing.latest_instruction_id {
|
||||
existing.latest_slot = summary_row.latest_slot;
|
||||
existing.latest_signature = summary_row.latest_signature;
|
||||
existing.latest_instruction_id = summary_row.latest_instruction_id;
|
||||
existing.latest_instruction_index = summary_row.latest_instruction_index;
|
||||
existing.latest_inner_instruction_index =
|
||||
summary_row.latest_inner_instruction_index;
|
||||
existing.latest_parsed_type = summary_row.latest_parsed_type;
|
||||
existing.latest_decoded_event_kind = summary_row.latest_decoded_event_kind;
|
||||
existing.latest_data_json_preview = summary_row.latest_data_json_preview;
|
||||
existing.latest_accounts_json_preview =
|
||||
summary_row.latest_accounts_json_preview;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
grouped.insert(summary_row.key.clone(), summary_row);
|
||||
},
|
||||
}
|
||||
}
|
||||
let mut summaries = std::vec::Vec::new();
|
||||
for (_, accumulator) in grouped {
|
||||
let transaction_count_result = u64::try_from(accumulator.transaction_signatures.len());
|
||||
let transaction_count = match transaction_count_result {
|
||||
Ok(transaction_count) => transaction_count,
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot convert discriminator summary transaction_count to u64: {}",
|
||||
error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let undecoded_occurrence_count =
|
||||
accumulator.occurrence_count.saturating_sub(accumulator.decoded_event_count);
|
||||
summaries.push(crate::ProgramInstructionDiscriminatorSummaryDto {
|
||||
program_id: accumulator.key.program_id,
|
||||
discriminator_hex: accumulator.key.discriminator_hex,
|
||||
known_instruction_name: accumulator.known_instruction_name,
|
||||
accounts_count: accumulator.key.accounts_count,
|
||||
stack_height: accumulator.key.stack_height,
|
||||
is_inner_instruction: accumulator.key.is_inner_instruction,
|
||||
occurrence_count: accumulator.occurrence_count,
|
||||
transaction_count,
|
||||
decoded_event_count: accumulator.decoded_event_count,
|
||||
undecoded_occurrence_count,
|
||||
latest_slot: accumulator.latest_slot,
|
||||
latest_signature: accumulator.latest_signature,
|
||||
latest_instruction_id: accumulator.latest_instruction_id,
|
||||
latest_instruction_index: accumulator.latest_instruction_index,
|
||||
latest_inner_instruction_index: accumulator.latest_inner_instruction_index,
|
||||
latest_parsed_type: accumulator.latest_parsed_type,
|
||||
latest_decoded_event_kind: accumulator.latest_decoded_event_kind,
|
||||
latest_data_json_preview: accumulator.latest_data_json_preview,
|
||||
latest_accounts_json_preview: accumulator.latest_accounts_json_preview,
|
||||
});
|
||||
}
|
||||
summaries.sort_by(|left, right| {
|
||||
right
|
||||
.undecoded_occurrence_count
|
||||
.cmp(&left.undecoded_occurrence_count)
|
||||
.then(right.transaction_count.cmp(&left.transaction_count))
|
||||
.then(right.occurrence_count.cmp(&left.occurrence_count))
|
||||
.then(right.latest_instruction_id.cmp(&left.latest_instruction_id))
|
||||
});
|
||||
return Ok(summaries);
|
||||
}
|
||||
|
||||
fn build_summary_row_from_discriminator_entity(
|
||||
row: crate::ProgramInstructionDiscriminatorRowEntity,
|
||||
) -> Result<ProgramInstructionDiscriminatorSummaryAccumulator, crate::Error> {
|
||||
let program_id = match row.program_id.clone() {
|
||||
Some(program_id) => program_id,
|
||||
None => "unknown".to_string(),
|
||||
};
|
||||
let accounts_count = accounts_count_from_json(row.accounts_json.as_str());
|
||||
let stack_height = match row.stack_height {
|
||||
Some(stack_height) => match u32::try_from(stack_height) {
|
||||
Ok(stack_height) => Some(stack_height),
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot convert discriminator summary stack_height '{}' to u32: {}",
|
||||
stack_height, error
|
||||
)));
|
||||
},
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let instruction_index = match u32::try_from(row.instruction_index) {
|
||||
Ok(instruction_index) => instruction_index,
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot convert discriminator summary instruction_index '{}' to u32: {}",
|
||||
row.instruction_index, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
let inner_instruction_index = match row.inner_instruction_index {
|
||||
Some(inner_instruction_index) => match u32::try_from(inner_instruction_index) {
|
||||
Ok(inner_instruction_index) => Some(inner_instruction_index),
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot convert discriminator summary inner_instruction_index '{}' to u32: {}",
|
||||
inner_instruction_index, error
|
||||
)));
|
||||
},
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let latest_slot = match row.slot {
|
||||
Some(slot) => match u64::try_from(slot) {
|
||||
Ok(slot) => Some(slot),
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot convert discriminator summary slot '{}' to u64: {}",
|
||||
slot, error
|
||||
)));
|
||||
},
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let decoded_data = decode_instruction_data_json(row.data_json.as_ref());
|
||||
let discriminator_hex = match decoded_data {
|
||||
Some(decoded_data) => first_8_bytes_hex(decoded_data.as_slice()),
|
||||
None => None,
|
||||
};
|
||||
let known_instruction_name = known_instruction_name_for_program_discriminator(
|
||||
program_id.as_str(),
|
||||
discriminator_hex.as_deref(),
|
||||
);
|
||||
let mut transaction_signatures = std::collections::BTreeSet::new();
|
||||
transaction_signatures.insert(row.signature.clone());
|
||||
let decoded_event_count = if row.decoded_event_id.is_some() { 1_u64 } else { 0_u64 };
|
||||
let key = ProgramInstructionDiscriminatorSummaryKey {
|
||||
program_id,
|
||||
discriminator_hex,
|
||||
accounts_count,
|
||||
stack_height,
|
||||
is_inner_instruction: row.parent_instruction_id.is_some(),
|
||||
};
|
||||
return Ok(ProgramInstructionDiscriminatorSummaryAccumulator {
|
||||
key,
|
||||
known_instruction_name,
|
||||
occurrence_count: 1,
|
||||
decoded_event_count,
|
||||
transaction_signatures,
|
||||
latest_slot,
|
||||
latest_signature: row.signature,
|
||||
latest_instruction_id: row.instruction_id,
|
||||
latest_instruction_index: instruction_index,
|
||||
latest_inner_instruction_index: inner_instruction_index,
|
||||
latest_parsed_type: row.parsed_type,
|
||||
latest_decoded_event_kind: row.decoded_event_kind,
|
||||
latest_data_json_preview: preview_text(row.data_json.as_deref(), 300),
|
||||
latest_accounts_json_preview: preview_text(Some(row.accounts_json.as_str()), 600),
|
||||
});
|
||||
}
|
||||
|
||||
fn accounts_count_from_json(accounts_json: &str) -> u64 {
|
||||
let parsed_result = serde_json::from_str::<serde_json::Value>(accounts_json);
|
||||
let parsed = match parsed_result {
|
||||
Ok(parsed) => parsed,
|
||||
Err(_) => return 0,
|
||||
};
|
||||
let array = match parsed.as_array() {
|
||||
Some(array) => array,
|
||||
None => return 0,
|
||||
};
|
||||
let count_result = u64::try_from(array.len());
|
||||
match count_result {
|
||||
Ok(count) => return count,
|
||||
Err(_) => return 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_instruction_data_json(
|
||||
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,
|
||||
};
|
||||
if let serde_json::Value::String(base58_text) = parsed {
|
||||
let decoded_result = decode_base58(base58_text.as_str());
|
||||
match decoded_result {
|
||||
Ok(decoded) => return Some(decoded),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
fn decode_base58(input: &str) -> Result<std::vec::Vec<u8>, crate::Error> {
|
||||
let decoded_result = bs58::decode(input).into_vec();
|
||||
match decoded_result {
|
||||
Ok(decoded) => return Ok(decoded),
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Json(format!(
|
||||
"cannot decode instruction data from base58: {}",
|
||||
error
|
||||
)));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option<std::string::String> {
|
||||
if bytes.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
return Some(format!(
|
||||
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
|
||||
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
|
||||
));
|
||||
}
|
||||
|
||||
fn known_instruction_name_for_program_discriminator(
|
||||
program_id: &str,
|
||||
discriminator_hex: std::option::Option<&str>,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
if program_id != crate::METEORA_DLMM_PROGRAM_ID {
|
||||
return None;
|
||||
}
|
||||
let discriminator_hex = match discriminator_hex {
|
||||
Some(discriminator_hex) => discriminator_hex,
|
||||
None => return None,
|
||||
};
|
||||
let name = match discriminator_hex {
|
||||
"2d9aedd2dd0fa65c" => "initialize_lb_pair",
|
||||
"493b2478ed536cc6" => "initialize_lb_pair2",
|
||||
"2e2729876fb7c840" => "initialize_customizable_permissionless_lb_pair",
|
||||
"f349817e3313f16b" => "initialize_customizable_permissionless_lb_pair2",
|
||||
"f8c69e91e17587c8" => "swap",
|
||||
"414b3f4ceb5b5b88" => "swap2",
|
||||
"fa49652126cf4bb8" => "swap_exact_out",
|
||||
"2bd7f784893cf351" => "swap_exact_out2",
|
||||
"38ade6d0ade49ccd" => "swap_with_price_impact",
|
||||
"235613b94ed44bd3" => "initialize_bin_array",
|
||||
"dbc0ea47bebf6650" => "initialize_position",
|
||||
"b59d59438fb63448" => "add_liquidity",
|
||||
"5055d14818ceb16c" => "remove_liquidity",
|
||||
"e445a52e51cb9a1d" => "anchor_self_cpi_log_event",
|
||||
"70bf65ab1c907fbb" => "claim_fee2",
|
||||
_ => return None,
|
||||
};
|
||||
return Some(name.to_string());
|
||||
}
|
||||
|
||||
fn preview_text(
|
||||
text: std::option::Option<&str>,
|
||||
max_len: usize,
|
||||
) -> std::option::Option<std::string::String> {
|
||||
let text = match text {
|
||||
Some(text) => text,
|
||||
None => return None,
|
||||
};
|
||||
if text.len() <= max_len {
|
||||
return Some(text.to_string());
|
||||
}
|
||||
let mut preview = text.chars().take(max_len).collect::<std::string::String>();
|
||||
preview.push_str("...");
|
||||
return Some(preview);
|
||||
}
|
||||
|
||||
/// Lists instruction discriminator summaries for one program id.
|
||||
pub async fn query_program_instruction_discriminator_summaries_list_by_program_id(
|
||||
database: &crate::Database,
|
||||
program_id: &str,
|
||||
limit: u32,
|
||||
) -> Result<std::vec::Vec<crate::ProgramInstructionDiscriminatorSummaryDto>, 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::ProgramInstructionDiscriminatorRowEntity>(
|
||||
r#"
|
||||
SELECT
|
||||
tx.id AS transaction_id,
|
||||
tx.signature AS signature,
|
||||
tx.slot AS slot,
|
||||
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.stack_height AS stack_height,
|
||||
ins.accounts_json AS accounts_json,
|
||||
ins.data_json AS data_json,
|
||||
ins.parsed_type AS parsed_type,
|
||||
de.id AS decoded_event_id,
|
||||
de.event_kind AS decoded_event_kind
|
||||
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 ins.program_id = ?
|
||||
ORDER BY
|
||||
tx.slot DESC,
|
||||
tx.id DESC,
|
||||
ins.instruction_index ASC,
|
||||
ins.inner_instruction_index ASC,
|
||||
ins.id ASC
|
||||
LIMIT ?
|
||||
"#,
|
||||
)
|
||||
.bind(program_id.to_string())
|
||||
.bind(i64::from(limit))
|
||||
.fetch_all(pool)
|
||||
.await;
|
||||
let rows = match query_result {
|
||||
Ok(rows) => rows,
|
||||
Err(error) => {
|
||||
return Err(crate::Error::Db(format!(
|
||||
"cannot list program instruction discriminator diagnostics for program_id '{}' on sqlite: {}",
|
||||
program_id, error
|
||||
)));
|
||||
},
|
||||
};
|
||||
return build_program_instruction_discriminator_summaries(rows);
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user