// file: kb_lib/src/db/queries/chain_instruction.rs //! Queries for `kb_chain_instructions`. /// Inserts one normalized chain instruction row. pub async fn insert_chain_instruction( database: &crate::KbDatabase, dto: &crate::KbChainInstructionDto, ) -> Result { let instruction_index_result = i64::from(dto.instruction_index); let inner_instruction_index = match dto.inner_instruction_index { Some(inner_instruction_index) => Some(i64::from(inner_instruction_index)), None => None, }; let stack_height = match dto.stack_height { Some(stack_height) => Some(i64::from(stack_height)), None => None, }; match database.connection() { crate::KbDatabaseConnection::Sqlite(pool) => { let query_result = sqlx::query( r#" INSERT INTO kb_chain_instructions ( transaction_id, parent_instruction_id, instruction_index, inner_instruction_index, program_id, program_name, stack_height, accounts_json, data_json, parsed_type, parsed_json, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "#, ) .bind(dto.transaction_id) .bind(dto.parent_instruction_id) .bind(instruction_index_result) .bind(inner_instruction_index) .bind(dto.program_id.clone()) .bind(dto.program_name.clone()) .bind(stack_height) .bind(dto.accounts_json.clone()) .bind(dto.data_json.clone()) .bind(dto.parsed_type.clone()) .bind(dto.parsed_json.clone()) .bind(dto.created_at.to_rfc3339()) .execute(pool) .await; let insert_result = match query_result { Ok(insert_result) => insert_result, Err(error) => { return Err(crate::KbError::Db(format!( "cannot insert kb_chain_instructions on sqlite: {}", error ))); }, }; return Ok(insert_result.last_insert_rowid()); }, } } /// Reads one chain instruction by its internal id. pub async fn get_chain_instruction_by_id( database: &crate::KbDatabase, instruction_id: i64, ) -> Result, crate::KbError> { match database.connection() { crate::KbDatabaseConnection::Sqlite(pool) => { let query_result = sqlx::query_as::( r#" SELECT id, transaction_id, parent_instruction_id, instruction_index, inner_instruction_index, program_id, program_name, stack_height, accounts_json, data_json, parsed_type, parsed_json, created_at FROM kb_chain_instructions WHERE id = ? LIMIT 1 "#, ) .bind(instruction_id) .fetch_optional(pool) .await; let entity_option = match query_result { Ok(entity_option) => entity_option, Err(error) => { return Err(crate::KbError::Db(format!( "cannot fetch kb_chain_instructions id '{}' on sqlite: {}", instruction_id, error ))); }, }; match entity_option { Some(entity) => { let dto_result = crate::KbChainInstructionDto::try_from(entity); match dto_result { Ok(dto) => return Ok(Some(dto)), Err(error) => return Err(error), } }, None => return Ok(None), } }, } } /// Lists instructions for one transaction ordered from outer to inner. pub async fn list_chain_instructions_by_transaction_id( database: &crate::KbDatabase, transaction_id: i64, ) -> Result, crate::KbError> { match database.connection() { crate::KbDatabaseConnection::Sqlite(pool) => { let query_result = sqlx::query_as::( r#" SELECT id, transaction_id, parent_instruction_id, instruction_index, inner_instruction_index, program_id, program_name, stack_height, accounts_json, data_json, parsed_type, parsed_json, created_at FROM kb_chain_instructions WHERE transaction_id = ? ORDER BY instruction_index ASC, inner_instruction_index ASC, id ASC "#, ) .bind(transaction_id) .fetch_all(pool) .await; let entities = match query_result { Ok(entities) => entities, Err(error) => { return Err(crate::KbError::Db(format!( "cannot list kb_chain_instructions for transaction_id '{}' on sqlite: {}", transaction_id, error ))); }, }; let mut dtos = std::vec::Vec::new(); for entity in entities { let dto_result = crate::KbChainInstructionDto::try_from(entity); let dto = match dto_result { Ok(dto) => dto, Err(error) => return Err(error), }; dtos.push(dto); } return Ok(dtos); }, } } /// Deletes all instructions for one transaction id. pub async fn delete_chain_instructions_by_transaction_id( database: &crate::KbDatabase, transaction_id: i64, ) -> Result<(), crate::KbError> { match database.connection() { crate::KbDatabaseConnection::Sqlite(pool) => { let query_result = sqlx::query( r#" DELETE FROM kb_chain_instructions WHERE transaction_id = ? "#, ) .bind(transaction_id) .execute(pool) .await; if let Err(error) = query_result { return Err(crate::KbError::Db(format!( "cannot delete kb_chain_instructions for transaction_id '{}' on sqlite: {}", transaction_id, error ))); } return Ok(()); }, } } #[cfg(test)] mod tests { async fn make_database() -> crate::KbDatabase { let tempdir = tempfile::tempdir().expect("tempdir must succeed"); let database_path = tempdir.path().join("chain_instruction.sqlite3"); let config = crate::KbDatabaseConfig { enabled: true, backend: crate::KbDatabaseBackend::Sqlite, sqlite: crate::KbSqliteDatabaseConfig { path: database_path.to_string_lossy().to_string(), create_if_missing: true, busy_timeout_ms: 5000, max_connections: 1, auto_initialize_schema: true, use_wal: true, }, }; return crate::KbDatabase::connect_and_initialize(&config) .await .expect("database init must succeed"); } async fn make_transaction(database: &crate::KbDatabase) -> i64 { let dto = crate::KbChainTransactionDto::new( "sig-chain-instruction-1".to_string(), None, None, Some("mainnet_public_http".to_string()), Some("legacy".to_string()), None, None, r#"{"transaction":{"message":{"instructions":[]}}}"#.to_string(), ); return crate::upsert_chain_transaction(database, &dto) .await .expect("chain transaction upsert must succeed"); } #[tokio::test] async fn chain_instruction_insert_list_delete_works() { let database = make_database().await; let transaction_id = make_transaction(&database).await; let outer_dto = crate::KbChainInstructionDto::new( transaction_id, None, 0, None, Some(crate::SPL_TOKEN_PROGRAM_ID.to_string()), Some("spl-token".to_string()), Some(1), r#"["AccountA","AccountB"]"#.to_string(), Some(r#""raw-data-outer""#.to_string()), Some("transfer".to_string()), Some(r#"{"type":"transfer","info":{"amount":"10"}}"#.to_string()), ); let outer_instruction_id = crate::insert_chain_instruction(&database, &outer_dto) .await .expect("outer instruction insert must succeed"); assert!(outer_instruction_id > 0); let inner_dto = crate::KbChainInstructionDto::new( transaction_id, Some(outer_instruction_id), 0, Some(0), Some(crate::SPL_TOKEN_PROGRAM_ID.to_string()), Some("spl-token".to_string()), Some(2), r#"["InnerA","InnerB"]"#.to_string(), Some(r#""raw-data-inner""#.to_string()), Some("mintTo".to_string()), Some(r#"{"type":"mintTo","info":{"amount":"5"}}"#.to_string()), ); let inner_instruction_id = crate::insert_chain_instruction(&database, &inner_dto) .await .expect("inner instruction insert must succeed"); assert!(inner_instruction_id > outer_instruction_id); let listed = crate::list_chain_instructions_by_transaction_id(&database, transaction_id) .await .expect("chain instruction list must succeed"); assert_eq!(listed.len(), 2); assert_eq!(listed[0].parent_instruction_id, None); assert_eq!(listed[0].instruction_index, 0); assert_eq!(listed[0].inner_instruction_index, None); assert_eq!(listed[0].parsed_type, Some("transfer".to_string())); assert_eq!(listed[1].parent_instruction_id, Some(outer_instruction_id)); assert_eq!(listed[1].instruction_index, 0); assert_eq!(listed[1].inner_instruction_index, Some(0)); assert_eq!(listed[1].parsed_type, Some("mintTo".to_string())); crate::delete_chain_instructions_by_transaction_id(&database, transaction_id) .await .expect("chain instruction delete must succeed"); let listed_after_delete = crate::list_chain_instructions_by_transaction_id(&database, transaction_id) .await .expect("chain instruction list after delete must succeed"); assert_eq!(listed_after_delete.len(), 0); } }