diff --git a/.sqlx/query-21193630991ce15c085a35fe0457794340f8ff29933f69a80edb321ce4010306.json b/.sqlx/query-32fb4070f630d7d577d972a59c44a13c3924c0e6d1cf502cdb368140b5a94b39.json similarity index 98% rename from .sqlx/query-21193630991ce15c085a35fe0457794340f8ff29933f69a80edb321ce4010306.json rename to .sqlx/query-32fb4070f630d7d577d972a59c44a13c3924c0e6d1cf502cdb368140b5a94b39.json index 4a83990..129bf6e 100644 --- a/.sqlx/query-21193630991ce15c085a35fe0457794340f8ff29933f69a80edb321ce4010306.json +++ b/.sqlx/query-32fb4070f630d7d577d972a59c44a13c3924c0e6d1cf502cdb368140b5a94b39.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "WITH\n canonical_blocks AS (\n SELECT\n *\n FROM\n blocks\n WHERE\n chain_status='canonical'\n ),\n max_canonical_height AS (\n SELECT\n max(HEIGHT) AS max_height\n FROM\n canonical_blocks\n ),\n pending_blocks AS (\n SELECT\n b.*\n FROM\n blocks AS b,\n max_canonical_height AS m\n WHERE\n b.height>m.max_height\n AND b.chain_status='pending'\n ),\n blocks AS (\n SELECT\n *\n FROM\n canonical_blocks\n UNION ALL\n SELECT\n *\n FROM\n pending_blocks\n ),\n user_command_info AS (\n SELECT DISTINCT\n ON (\n buc.block_id,\n buc.user_command_id,\n buc.sequence_no\n ) u.id,\n u.command_type AS \"command_type: UserCommandType\",\n u.fee_payer_id,\n u.source_id,\n u.receiver_id,\n u.nonce,\n u.amount,\n u.fee,\n u.valid_until,\n u.memo,\n u.hash,\n buc.block_id,\n buc.sequence_no,\n buc.status AS \"status: TransactionStatus\",\n buc.failure_reason,\n b.state_hash,\n b.chain_status AS \"chain_status: ChainStatus\",\n b.height\n FROM\n user_commands AS u\n INNER JOIN blocks_user_commands AS buc ON u.id=buc.user_command_id\n INNER JOIN public_keys AS pk ON u.fee_payer_id=pk.id\n OR (\n buc.status='applied'\n AND (\n u.source_id=pk.id\n OR u.receiver_id=pk.id\n )\n )\n INNER JOIN blocks AS b ON buc.block_id=b.id\n WHERE\n (\n $1<=b.height\n OR $1 IS NULL\n )\n AND (\n $2=u.hash\n OR $2 IS NULL\n )\n AND (\n $3=pk.value\n AND $4=''\n OR (\n $3 IS NULL\n AND $4 IS NULL\n )\n )\n AND (\n $5=buc.status\n OR $5 IS NULL\n )\n AND (\n $6=buc.status\n OR $6 IS NULL\n )\n AND (\n $7=pk.value\n OR $7 IS NULL\n )\n ),\n id_count AS (\n SELECT\n count(*) AS total_count\n FROM\n user_command_info\n )\nSELECT\n u.*,\n id_count.total_count,\n pk_payer.value AS fee_payer,\n pk_source.value AS source,\n pk_receiver.value AS receiver,\n ac.creation_fee AS \"creation_fee?\"\nFROM\n id_count,\n (\n SELECT\n *\n FROM\n user_command_info\n ORDER BY\n block_id,\n id,\n sequence_no\n LIMIT\n $8\n OFFSET\n $9\n ) AS u\n INNER JOIN public_keys AS pk_payer ON u.fee_payer_id=pk_payer.id\n INNER JOIN public_keys AS pk_source ON u.source_id=pk_source.id\n INNER JOIN public_keys AS pk_receiver ON u.receiver_id=pk_receiver.id\n /* Account creation fees are attributed to the first successful command in the\n block that mentions the account with the following LEFT JOINs */\n LEFT JOIN account_identifiers AS ai_receiver ON u.receiver_id=ai_receiver.public_key_id\n LEFT JOIN accounts_created AS ac ON u.block_id=ac.block_id\n AND ai_receiver.id=ac.account_identifier_id\n AND u.\"status: TransactionStatus\"='applied'\n AND u.sequence_no=(\n SELECT\n least(\n (\n SELECT\n min(bic2.sequence_no)\n FROM\n blocks_internal_commands AS bic2\n INNER JOIN internal_commands AS ic2 ON bic2.internal_command_id=ic2.id\n WHERE\n u.receiver_id=ic2.receiver_id\n AND bic2.block_id=u.block_id\n AND bic2.status='applied'\n ),\n (\n SELECT\n min(buc2.sequence_no)\n FROM\n blocks_user_commands AS buc2\n INNER JOIN user_commands AS uc2 ON buc2.user_command_id=uc2.id\n WHERE\n u.receiver_id=uc2.receiver_id\n AND buc2.block_id=u.block_id\n AND buc2.status='applied'\n )\n )\n )\nORDER BY\n u.block_id,\n u.id,\n u.sequence_no\n", + "query": "WITH\n canonical_blocks AS (\n SELECT\n *\n FROM\n blocks\n WHERE\n chain_status='canonical'\n ),\n max_canonical_height AS (\n SELECT\n max(HEIGHT) AS max_height\n FROM\n canonical_blocks\n ),\n pending_blocks AS (\n SELECT\n b.*\n FROM\n blocks AS b,\n max_canonical_height AS m\n WHERE\n b.height>m.max_height\n AND b.chain_status='pending'\n ),\n blocks AS (\n SELECT\n *\n FROM\n canonical_blocks\n UNION ALL\n SELECT\n *\n FROM\n pending_blocks\n ),\n user_command_info AS (\n SELECT DISTINCT\n ON (\n buc.block_id,\n buc.user_command_id,\n buc.sequence_no\n ) u.id,\n u.command_type AS \"command_type: UserCommandType\",\n u.fee_payer_id,\n u.source_id,\n u.receiver_id,\n u.nonce,\n u.amount,\n u.fee,\n u.valid_until,\n u.memo,\n u.hash,\n buc.block_id,\n buc.sequence_no,\n buc.status AS \"status: TransactionStatus\",\n buc.failure_reason,\n b.state_hash,\n b.chain_status AS \"chain_status: ChainStatus\",\n b.height\n FROM\n user_commands AS u\n INNER JOIN blocks_user_commands AS buc ON u.id=buc.user_command_id\n INNER JOIN public_keys AS pk ON u.fee_payer_id=pk.id\n OR (\n buc.status='applied'\n AND (\n u.source_id=pk.id\n OR u.receiver_id=pk.id\n )\n )\n INNER JOIN blocks AS b ON buc.block_id=b.id\n WHERE\n (\n $1>=b.height\n OR $1 IS NULL\n )\n AND (\n $2=u.hash\n OR $2 IS NULL\n )\n AND (\n $3=pk.value\n AND $4=''\n OR (\n $3 IS NULL\n AND $4 IS NULL\n )\n )\n AND (\n $5=buc.status\n OR $5 IS NULL\n )\n AND (\n $6=buc.status\n OR $6 IS NULL\n )\n AND (\n $7=pk.value\n OR $7 IS NULL\n )\n ),\n id_count AS (\n SELECT\n count(*) AS total_count\n FROM\n user_command_info\n )\nSELECT\n u.*,\n id_count.total_count,\n pk_payer.value AS fee_payer,\n pk_source.value AS source,\n pk_receiver.value AS receiver,\n ac.creation_fee AS \"creation_fee?\"\nFROM\n id_count,\n (\n SELECT\n *\n FROM\n user_command_info\n ORDER BY\n block_id,\n id,\n sequence_no\n LIMIT\n $8\n OFFSET\n $9\n ) AS u\n INNER JOIN public_keys AS pk_payer ON u.fee_payer_id=pk_payer.id\n INNER JOIN public_keys AS pk_source ON u.source_id=pk_source.id\n INNER JOIN public_keys AS pk_receiver ON u.receiver_id=pk_receiver.id\n /* Account creation fees are attributed to the first successful command in the\n block that mentions the account with the following LEFT JOINs */\n LEFT JOIN account_identifiers AS ai_receiver ON u.receiver_id=ai_receiver.public_key_id\n LEFT JOIN accounts_created AS ac ON u.block_id=ac.block_id\n AND ai_receiver.id=ac.account_identifier_id\n AND u.\"status: TransactionStatus\"='applied'\n AND u.sequence_no=(\n SELECT\n least(\n (\n SELECT\n min(bic2.sequence_no)\n FROM\n blocks_internal_commands AS bic2\n INNER JOIN internal_commands AS ic2 ON bic2.internal_command_id=ic2.id\n WHERE\n u.receiver_id=ic2.receiver_id\n AND bic2.block_id=u.block_id\n AND bic2.status='applied'\n ),\n (\n SELECT\n min(buc2.sequence_no)\n FROM\n blocks_user_commands AS buc2\n INNER JOIN user_commands AS uc2 ON buc2.user_command_id=uc2.id\n WHERE\n u.receiver_id=uc2.receiver_id\n AND buc2.block_id=u.block_id\n AND buc2.status='applied'\n )\n )\n )\nORDER BY\n u.block_id,\n u.id,\n u.sequence_no\n", "describe": { "columns": [ { @@ -209,5 +209,5 @@ false ] }, - "hash": "21193630991ce15c085a35fe0457794340f8ff29933f69a80edb321ce4010306" + "hash": "32fb4070f630d7d577d972a59c44a13c3924c0e6d1cf502cdb368140b5a94b39" } diff --git a/.sqlx/query-d93db1f20d1bba42aa81b0262b61265537ece2ae5d5278afa3e9027b2066aeb4.json b/.sqlx/query-d93db1f20d1bba42aa81b0262b61265537ece2ae5d5278afa3e9027b2066aeb4.json new file mode 100644 index 0000000..d9052f7 --- /dev/null +++ b/.sqlx/query-d93db1f20d1bba42aa81b0262b61265537ece2ae5d5278afa3e9027b2066aeb4.json @@ -0,0 +1,155 @@ +{ + "db_name": "PostgreSQL", + "query": "WITH\n canonical_blocks AS (\n SELECT\n *\n FROM\n blocks\n WHERE\n chain_status='canonical'\n ),\n max_canonical_height AS (\n SELECT\n max(HEIGHT) AS max_height\n FROM\n canonical_blocks\n ),\n pending_blocks AS (\n SELECT\n b.*\n FROM\n blocks AS b,\n max_canonical_height AS m\n WHERE\n b.height>m.max_height\n AND b.chain_status='pending'\n ),\n blocks AS (\n SELECT\n *\n FROM\n canonical_blocks\n UNION ALL\n SELECT\n *\n FROM\n pending_blocks\n ),\n coinbase_receiver_info AS (\n SELECT\n bic.block_id,\n bic.internal_command_id,\n bic.sequence_no,\n bic.secondary_sequence_no,\n coinbase_receiver_pk.value AS coinbase_receiver\n FROM\n blocks_internal_commands AS bic\n INNER JOIN internal_commands AS ic ON bic.internal_command_id=ic.id\n INNER JOIN blocks_internal_commands AS bic_coinbase_receiver ON bic.block_id=bic_coinbase_receiver.block_id\n AND (\n bic.internal_command_id<>bic_coinbase_receiver.internal_command_id\n OR bic.sequence_no<>bic_coinbase_receiver.sequence_no\n OR bic.secondary_sequence_no<>bic_coinbase_receiver.secondary_sequence_no\n )\n INNER JOIN internal_commands AS ic_coinbase_receiver ON ic.command_type='fee_transfer_via_coinbase'\n AND ic_coinbase_receiver.command_type='coinbase'\n AND bic_coinbase_receiver.internal_command_id=ic_coinbase_receiver.id\n INNER JOIN public_keys AS coinbase_receiver_pk ON ic_coinbase_receiver.receiver_id=coinbase_receiver_pk.id\n ),\n internal_commands_info AS (\n SELECT DISTINCT\n ON (\n bic.block_id,\n bic.internal_command_id,\n bic.sequence_no,\n bic.secondary_sequence_no\n ) i.id,\n i.command_type AS \"command_type: InternalCommandType\",\n i.receiver_id,\n i.fee,\n i.hash,\n pk.value AS receiver,\n cri.coinbase_receiver AS \"coinbase_receiver?\",\n bic.sequence_no,\n bic.secondary_sequence_no,\n bic.block_id,\n bic.status AS \"status: TransactionStatus\",\n b.state_hash,\n b.height\n FROM\n internal_commands AS i\n INNER JOIN blocks_internal_commands AS bic ON i.id=bic.internal_command_id\n INNER JOIN public_keys AS pk ON i.receiver_id=pk.id\n INNER JOIN blocks AS b ON bic.block_id=b.id\n LEFT JOIN coinbase_receiver_info AS cri ON bic.block_id=cri.block_id\n AND bic.internal_command_id=cri.internal_command_id\n AND bic.sequence_no=cri.sequence_no\n AND bic.secondary_sequence_no=cri.secondary_sequence_no\n WHERE\n (\n $1>=b.height\n OR $1 IS NULL\n )\n AND (\n $2=i.hash\n OR $2 IS NULL\n )\n AND (\n (\n $3=pk.value\n OR $3=cri.coinbase_receiver\n )\n AND $4=''\n OR (\n $3 IS NULL\n AND $4 IS NULL\n )\n )\n AND (\n $5=bic.status\n OR $5 IS NULL\n )\n AND (\n $6=bic.status\n OR $6 IS NULL\n )\n AND (\n (\n $7=pk.value\n OR $7=cri.coinbase_receiver\n )\n OR $7 IS NULL\n )\n ),\n id_count AS (\n SELECT\n count(*) AS total_count\n FROM\n internal_commands_info\n )\nSELECT\n i.*,\n id_count.total_count,\n ac.creation_fee AS \"creation_fee?\"\nFROM\n id_count,\n (\n SELECT\n *\n FROM\n internal_commands_info\n ORDER BY\n block_id,\n id,\n sequence_no,\n secondary_sequence_no\n LIMIT\n $8\n OFFSET\n $9\n ) AS i\n LEFT JOIN account_identifiers AS ai ON i.receiver_id=ai.public_key_id\n LEFT JOIN accounts_created AS ac ON ai.id=ac.account_identifier_id\n AND i.block_id=ac.block_id\n AND i.sequence_no=(\n SELECT\n least(\n (\n SELECT\n min(bic2.sequence_no)\n FROM\n blocks_internal_commands AS bic2\n INNER JOIN internal_commands AS ic2 ON bic2.internal_command_id=ic2.id\n WHERE\n i.receiver_id=ic2.receiver_id\n AND bic2.block_id=i.block_id\n AND bic2.status='applied'\n ),\n (\n SELECT\n min(buc2.sequence_no)\n FROM\n blocks_user_commands AS buc2\n INNER JOIN user_commands AS uc2 ON buc2.user_command_id=uc2.id\n WHERE\n i.receiver_id=uc2.receiver_id\n AND buc2.block_id=i.block_id\n AND buc2.status='applied'\n )\n )\n )\nORDER BY\n i.block_id,\n i.id,\n i.sequence_no,\n i.secondary_sequence_no\n", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "command_type: InternalCommandType", + "type_info": { + "Custom": { + "name": "internal_command_type", + "kind": { + "Enum": [ + "fee_transfer_via_coinbase", + "fee_transfer", + "coinbase" + ] + } + } + } + }, + { + "ordinal": 2, + "name": "receiver_id", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "fee", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "hash", + "type_info": "Text" + }, + { + "ordinal": 5, + "name": "receiver", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "coinbase_receiver?", + "type_info": "Text" + }, + { + "ordinal": 7, + "name": "sequence_no", + "type_info": "Int4" + }, + { + "ordinal": 8, + "name": "secondary_sequence_no", + "type_info": "Int4" + }, + { + "ordinal": 9, + "name": "block_id", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "status: TransactionStatus", + "type_info": { + "Custom": { + "name": "transaction_status", + "kind": { + "Enum": [ + "applied", + "failed" + ] + } + } + } + }, + { + "ordinal": 11, + "name": "state_hash", + "type_info": "Text" + }, + { + "ordinal": 12, + "name": "height", + "type_info": "Int8" + }, + { + "ordinal": 13, + "name": "total_count", + "type_info": "Int8" + }, + { + "ordinal": 14, + "name": "creation_fee?", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text", + "Text", + "Text", + { + "Custom": { + "name": "transaction_status", + "kind": { + "Enum": [ + "applied", + "failed" + ] + } + } + }, + { + "Custom": { + "name": "transaction_status", + "kind": { + "Enum": [ + "applied", + "failed" + ] + } + } + }, + "Text", + "Int8", + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + null, + null, + null, + false + ] + }, + "hash": "d93db1f20d1bba42aa81b0262b61265537ece2ae5d5278afa3e9027b2066aeb4" +} diff --git a/sql/indexer_internal_commands.sql b/sql/indexer_internal_commands.sql index 1c2e31e..6356e06 100644 --- a/sql/indexer_internal_commands.sql +++ b/sql/indexer_internal_commands.sql @@ -63,15 +63,16 @@ WITH bic.sequence_no, bic.secondary_sequence_no ) i.id, - i.command_type, + i.command_type AS "command_type: InternalCommandType", i.receiver_id, i.fee, i.hash, pk.value AS receiver, - cri.coinbase_receiver, + cri.coinbase_receiver AS "coinbase_receiver?", bic.sequence_no, bic.secondary_sequence_no, bic.block_id, + bic.status AS "status: TransactionStatus", b.state_hash, b.height FROM @@ -85,7 +86,7 @@ WITH AND bic.secondary_sequence_no=cri.secondary_sequence_no WHERE ( - $1<=b.height + $1>=b.height OR $1 IS NULL ) AND ( @@ -128,7 +129,7 @@ WITH SELECT i.*, id_count.total_count, - ac.creation_fee + ac.creation_fee AS "creation_fee?" FROM id_count, ( @@ -142,9 +143,9 @@ FROM sequence_no, secondary_sequence_no LIMIT - 5 + $8 OFFSET - 0 + $9 ) AS i LEFT JOIN account_identifiers AS ai ON i.receiver_id=ai.public_key_id LEFT JOIN accounts_created AS ac ON ai.id=ac.account_identifier_id diff --git a/sql/indexer_user_commands.sql b/sql/indexer_user_commands.sql index 08d4132..e5bbfd4 100644 --- a/sql/indexer_user_commands.sql +++ b/sql/indexer_user_commands.sql @@ -72,7 +72,7 @@ WITH INNER JOIN blocks AS b ON buc.block_id=b.id WHERE ( - $1<=b.height + $1>=b.height OR $1 IS NULL ) AND ( diff --git a/sql/indexer_zkapp_commands.sql b/sql/indexer_zkapp_commands.sql index 72075cb..1bcabcc 100644 --- a/sql/indexer_zkapp_commands.sql +++ b/sql/indexer_zkapp_commands.sql @@ -85,7 +85,7 @@ WITH INNER JOIN tokens AS token_update_body ON ai_update_body.token_id=token_update_body.id WHERE ( - $1<=b.height + $1>=b.height OR $1 IS NULL ) AND ( diff --git a/src/api/search_transactions.rs b/src/api/search_transactions.rs index 26a4ba9..b33222c 100644 --- a/src/api/search_transactions.rs +++ b/src/api/search_transactions.rs @@ -2,12 +2,13 @@ use coinbase_mesh::models::{ AccountIdentifier, BlockIdentifier, BlockTransaction, SearchTransactionsRequest, SearchTransactionsResponse, Transaction, TransactionIdentifier, }; +use convert_case::{Case, Casing}; use serde_json::json; use sqlx::FromRow; use crate::{ - operation, util::DEFAULT_TOKEN_ID, ChainStatus, MinaMesh, MinaMeshError, OperationType, TransactionStatus, - UserCommandType, + operation, util::DEFAULT_TOKEN_ID, ChainStatus, InternalCommandType, MinaMesh, MinaMeshError, OperationType, + TransactionStatus, UserCommandType, }; impl MinaMesh { @@ -15,62 +16,66 @@ impl MinaMesh { &self, req: SearchTransactionsRequest, ) -> Result { - let user_commands = self.fetch_user_commands(&req).await?; - let user_commands_len = user_commands.len(); - let next_offset = req.offset.unwrap_or(0) + user_commands_len as i64; + let original_offset = req.offset.unwrap_or(0); + let mut offset = original_offset; + let mut limit = req.limit.unwrap_or(100); + let mut transactions = Vec::new(); + let mut txs_len = 0; + let mut total_count = 0; - // Extract the total count from the first user command, or default to 0 + // User Commands + let user_commands = self.fetch_user_commands(&req, offset, limit).await?; + let user_commands_len = user_commands.len() as i64; let user_commands_total_count = user_commands.first().and_then(|uc| uc.total_count).unwrap_or(0); + transactions.extend(user_commands.into_iter().map(|ic| ic.into())); + total_count += user_commands_total_count; + txs_len += user_commands_len; - // Map user commands into block transactions - let user_commands_bt = user_commands.into_iter().map(|uc| uc.into_block_transaction()).collect(); + // Internal Commands + if limit > total_count { + // if we are below the limit, fetch internal commands + (offset, limit) = adjust_limit_and_offset(limit, offset, txs_len); + let internal_commands = self.fetch_internal_commands(&req, offset, limit).await?; + let internal_commands_len = internal_commands.len() as i64; + let internal_commands_total_count = internal_commands.first().and_then(|ic| ic.total_count).unwrap_or(0); + transactions.extend(internal_commands.into_iter().map(|uc| uc.into())); + txs_len += internal_commands_len; + total_count += internal_commands_total_count; + } else { + // otherwise only fetch the first internal command to get the total count + let internal_commands = self.fetch_internal_commands(&req, 0, 1).await?; + let internal_commands_total_count = internal_commands.first().and_then(|ic| ic.total_count).unwrap_or(0); + total_count += internal_commands_total_count; + } + let next_offset = original_offset + txs_len; let response = SearchTransactionsResponse { - transactions: user_commands_bt, - total_count: user_commands_total_count, - next_offset: match next_offset { - offset if offset < user_commands_total_count => Some(offset), - _ => None, - }, + transactions, + total_count, + next_offset: if next_offset < total_count { Some(next_offset) } else { None }, }; Ok(response) } - pub async fn fetch_user_commands(&self, req: &SearchTransactionsRequest) -> Result, MinaMeshError> { - let max_block = req.max_block; - let txn_hash = req.transaction_identifier.as_ref().map(|t| &t.hash); - let account_identifier = req.account_identifier.as_ref().map(|a| &a.address); - let token_id = req.account_identifier.as_ref().and_then(|a| a.metadata.as_ref().map(|meta| meta.to_string())); - let status = match req.status.as_deref() { - Some("applied") => Some(TransactionStatus::Applied), - Some("failed") => Some(TransactionStatus::Failed), - Some(other) => { - return Err(MinaMeshError::Exception( - format!("Invalid transaction status: '{other}'. Valid are 'applied' and 'failed'").to_string(), - )) - } - None => None, - }; - let success_status = match req.success { - Some(true) => Some(TransactionStatus::Applied), - Some(false) => Some(TransactionStatus::Failed), - None => None, - }; - let address = req.address.as_ref(); - let limit = req.limit.unwrap_or(100); - let offset = req.offset.unwrap_or(0); + pub async fn fetch_user_commands( + &self, + req: &SearchTransactionsRequest, + offset: i64, + limit: i64, + ) -> Result, MinaMeshError> { + let query_params = SearchTransactionsQueryParams::try_from(req.clone())?; let user_commands = sqlx::query_file_as!( UserCommand, "sql/indexer_user_commands.sql", - max_block, - txn_hash, - account_identifier, - token_id, - status as Option, - success_status as Option, - address, + query_params.max_block, + query_params.transaction_hash, + query_params.account_identifier, + query_params.token_id, + query_params.status as Option, + query_params.success_status as Option, + query_params.address, limit, offset, ) @@ -80,12 +85,31 @@ impl MinaMesh { Ok(user_commands) } - #[allow(dead_code)] - async fn fetch_internal_commands( + pub async fn fetch_internal_commands( &self, - _req: &SearchTransactionsRequest, - ) -> Result, MinaMeshError> { - unimplemented!() + req: &SearchTransactionsRequest, + offset: i64, + limit: i64, + ) -> Result, MinaMeshError> { + let query_params = SearchTransactionsQueryParams::try_from(req.clone())?; + + let internal_commands = sqlx::query_file_as!( + InternalCommand, + "sql/indexer_internal_commands.sql", + query_params.max_block, + query_params.transaction_hash, + query_params.account_identifier, + query_params.token_id, + query_params.status as Option, + query_params.success_status as Option, + query_params.address, + limit, + offset + ) + .fetch_all(&self.pg_pool) + .await?; + + Ok(internal_commands) } #[allow(dead_code)] @@ -97,6 +121,129 @@ impl MinaMesh { } } +#[derive(Debug, FromRow)] +pub struct InternalCommand { + pub id: Option, + pub command_type: InternalCommandType, + pub receiver_id: Option, + pub fee: Option, + pub hash: String, + pub receiver: String, + pub coinbase_receiver: Option, + pub sequence_no: i32, + pub secondary_sequence_no: i32, + pub block_id: i32, + pub status: TransactionStatus, + pub state_hash: Option, + pub height: Option, + pub total_count: Option, + pub creation_fee: Option, +} + +impl From for BlockTransaction { + fn from(internal_command: InternalCommand) -> Self { + // Derive transaction_identifier by combining command_type, sequence numbers, + // and the hash + let transaction_identifier = format!( + "{}:{}:{}:{}", + internal_command.command_type.to_string().to_case(Case::Snake), + internal_command.sequence_no, + internal_command.secondary_sequence_no, + internal_command.hash + ); + let fee = internal_command.fee.unwrap_or("0".to_string()); + let status = &internal_command.status; + + let mut operations = Vec::new(); + let mut operation_index = 0; + + // Receiver Account Identifier + let receiver_account_id = &AccountIdentifier { + address: internal_command.receiver.clone(), + metadata: Some(json!({ "token_id": DEFAULT_TOKEN_ID })), + sub_account: None, + }; + + // Handle Account Creation Fee if applicable + if let Some(creation_fee) = &internal_command.creation_fee { + operations.push(operation( + operation_index, + Some(creation_fee), + receiver_account_id, + OperationType::AccountCreationFeeViaFeeReceiver, + Some(status), + None, + None, + )); + operation_index += 1; + } + + match internal_command.command_type { + InternalCommandType::Coinbase => { + operations.push(operation( + operation_index, + Some(&fee), + receiver_account_id, + OperationType::CoinbaseInc, + Some(status), + None, + None, + )); + } + + InternalCommandType::FeeTransfer => { + operations.push(operation( + operation_index, + Some(&fee), + receiver_account_id, + OperationType::FeeReceiverInc, + Some(status), + None, + None, + )); + } + + InternalCommandType::FeeTransferViaCoinbase => { + if let Some(coinbase_receiver) = &internal_command.coinbase_receiver { + operations.push(operation( + operation_index, + Some(&fee), + receiver_account_id, + OperationType::FeeReceiverInc, + Some(status), + None, + None, + )); + operation_index += 1; + + operations.push(operation( + operation_index, + Some(&fee), + &AccountIdentifier::new(coinbase_receiver.to_string()), + OperationType::FeePayerDec, + Some(status), + Some(vec![operation_index - 1]), + None, + )); + } + } + } + + let block_identifier = BlockIdentifier::new( + internal_command.height.unwrap_or_default(), + internal_command.state_hash.unwrap_or_default(), + ); + let transaction = Transaction { + transaction_identifier: Box::new(TransactionIdentifier::new(transaction_identifier)), + operations, + related_transactions: None, + metadata: None, + }; + + BlockTransaction::new(block_identifier, transaction) + } +} + #[derive(Debug, FromRow)] pub struct UserCommand { pub id: Option, @@ -135,22 +282,24 @@ impl UserCommand { Err(_) => None, } } +} - pub fn into_block_transaction(self) -> BlockTransaction { - let decoded_memo = self.decoded_memo().unwrap_or_default(); - let amt = self.amount.clone().unwrap_or_else(|| "0".to_string()); +impl From for BlockTransaction { + fn from(user_command: UserCommand) -> Self { + let decoded_memo = user_command.decoded_memo().unwrap_or_default(); + let amt = user_command.amount.clone().unwrap_or_else(|| "0".to_string()); let receiver_account_id = &AccountIdentifier { - address: self.receiver.clone(), + address: user_command.receiver.clone(), metadata: Some(json!({ "token_id": DEFAULT_TOKEN_ID })), sub_account: None, }; let source_account_id = &AccountIdentifier { - address: self.source, + address: user_command.source, metadata: Some(json!({ "token_id": DEFAULT_TOKEN_ID })), sub_account: None, }; let fee_payer_account_id = &AccountIdentifier { - address: self.fee_payer, + address: user_command.fee_payer, metadata: Some(json!({ "token_id": DEFAULT_TOKEN_ID })), sub_account: None, }; @@ -161,10 +310,10 @@ impl UserCommand { // Operation 1: Fee Payment operations.push(operation( operation_index, - Some(&format!("-{}", self.fee.unwrap_or_else(|| "0".to_string()))), + Some(&format!("-{}", user_command.fee.unwrap_or_else(|| "0".to_string()))), fee_payer_account_id, OperationType::FeePayment, - Some(&self.status), + Some(&user_command.status), None, None, )); @@ -172,13 +321,13 @@ impl UserCommand { operation_index += 1; // Operation 2: Account Creation Fee (if applicable) - if let Some(creation_fee) = &self.creation_fee { + if let Some(creation_fee) = &user_command.creation_fee { operations.push(operation( operation_index, Some(&format!("-{}", creation_fee)), receiver_account_id, OperationType::AccountCreationFeeViaPayment, - Some(&self.status), + Some(&user_command.status), None, None, )); @@ -187,7 +336,7 @@ impl UserCommand { } // Decide on the type of operation based on command type - match self.command_type { + match user_command.command_type { // Operation 3: Payment Source Decrement UserCommandType::Payment => { operations.push(operation( @@ -195,7 +344,7 @@ impl UserCommand { Some(&format!("-{}", amt)), source_account_id, OperationType::PaymentSourceDec, - Some(&self.status), + Some(&user_command.status), None, None, )); @@ -208,7 +357,7 @@ impl UserCommand { Some(&amt), receiver_account_id, OperationType::PaymentReceiverInc, - Some(&self.status), + Some(&user_command.status), Some(vec![operation_index - 1]), None, )); @@ -221,16 +370,17 @@ impl UserCommand { None, source_account_id, OperationType::DelegateChange, - Some(&self.status), + Some(&user_command.status), None, - Some(json!({ "delegate_change_target": self.receiver })), + Some(json!({ "delegate_change_target": user_command.receiver })), )); } } - let block_identifier = BlockIdentifier::new(self.height.unwrap_or_default(), self.state_hash.unwrap_or_default()); + let block_identifier = + BlockIdentifier::new(user_command.height.unwrap_or_default(), user_command.state_hash.unwrap_or_default()); let transaction = Transaction { - transaction_identifier: Box::new(TransactionIdentifier::new(self.hash)), + transaction_identifier: Box::new(TransactionIdentifier::new(user_command.hash)), operations, related_transactions: None, metadata: match decoded_memo.as_str() { @@ -241,3 +391,68 @@ impl UserCommand { BlockTransaction::new(block_identifier, transaction) } } + +pub struct SearchTransactionsQueryParams { + pub max_block: Option, + pub transaction_hash: Option, + pub account_identifier: Option, + pub token_id: Option, + pub status: Option, + pub success_status: Option, + pub address: Option, +} + +impl TryFrom for SearchTransactionsQueryParams { + type Error = MinaMeshError; + + fn try_from(req: SearchTransactionsRequest) -> Result { + let max_block = req.max_block; + let transaction_hash = req.transaction_identifier.map(|t| t.hash); + let token_id = req.account_identifier.as_ref().and_then(|a| a.metadata.as_ref().map(|meta| meta.to_string())); + let account_identifier = req.account_identifier.map(|a| a.address); + + let status = match req.status.as_deref() { + Some("applied") => Some(TransactionStatus::Applied), + Some("failed") => Some(TransactionStatus::Failed), + Some(other) => { + return Err(MinaMeshError::Exception(format!( + "Invalid transaction status: '{}'. Valid statuses are 'applied' and 'failed'", + other + ))); + } + None => None, + }; + + let success_status = match req.success { + Some(true) => Some(TransactionStatus::Applied), + Some(false) => Some(TransactionStatus::Failed), + None => None, + }; + + let address = req.address; + + Ok(SearchTransactionsQueryParams { + max_block, + transaction_hash, + account_identifier, + token_id, + status, + success_status, + address, + }) + } +} + +fn adjust_limit_and_offset(mut limit: i64, mut offset: i64, txs_len: i64) -> (i64, i64) { + if offset >= txs_len { + offset -= txs_len; + } else { + offset = 0; + } + if limit >= txs_len { + limit -= txs_len; + } else { + limit = 0; + } + (offset, limit) +} diff --git a/src/types.rs b/src/types.rs index 264df6b..1322802 100644 --- a/src/types.rs +++ b/src/types.rs @@ -17,7 +17,7 @@ pub enum UserCommandType { Delegation, } -#[derive(Type, Debug, PartialEq, Eq, Serialize)] +#[derive(Type, Debug, PartialEq, Eq, Serialize, Display)] #[sqlx(type_name = "internal_command_type", rename_all = "snake_case")] pub enum InternalCommandType { FeeTransferViaCoinbase, diff --git a/tests/search_transactions.rs b/tests/search_transactions.rs index 2fb0c85..8f3091a 100644 --- a/tests/search_transactions.rs +++ b/tests/search_transactions.rs @@ -1,7 +1,7 @@ use anyhow::Result; use insta::assert_debug_snapshot; use mina_mesh::{ - models::{NetworkIdentifier, SearchTransactionsRequest}, + models::{NetworkIdentifier, SearchTransactionsRequest, TransactionIdentifier}, MinaMeshConfig, }; @@ -25,3 +25,44 @@ async fn search_transactions_specified() -> Result<()> { assert_debug_snapshot!(response); Ok(()) } + +#[tokio::test] +async fn search_transactions_failed() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + + let request = SearchTransactionsRequest { + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "mainnet".to_string())), + max_block: Some(44), + status: Some("failed".to_string()), + limit: Some(5), + ..Default::default() + }; + + let response = mina_mesh.search_transactions(request).await; + + assert!(response.is_ok()); + assert_debug_snapshot!(response); + Ok(()) +} + +#[tokio::test] +async fn search_transactions_internal_command() -> Result<()> { + let mina_mesh = MinaMeshConfig::from_env().to_mina_mesh().await?; + + let request = SearchTransactionsRequest { + network_identifier: Box::new(NetworkIdentifier::new("mina".to_string(), "mainnet".to_string())), + max_block: Some(44), + transaction_identifier: Some(Box::new(TransactionIdentifier::new( + // cspell:disable-next-line + "CkpZZWqdA87JmPxHA5NmFEQ3qh7pUmqXi9GBWzf4pADtPEHQAeH7M".to_string(), + ))), + limit: Some(5), + ..Default::default() + }; + + let response = mina_mesh.search_transactions(request).await; + + assert!(response.is_ok()); + assert_debug_snapshot!(response); + Ok(()) +} diff --git a/tests/snapshots/search_transactions__search_transactions_failed.snap b/tests/snapshots/search_transactions__search_transactions_failed.snap new file mode 100644 index 0000000..65cb0f5 --- /dev/null +++ b/tests/snapshots/search_transactions__search_transactions_failed.snap @@ -0,0 +1,649 @@ +--- +source: tests/search_transactions.rs +expression: response +--- +Ok( + SearchTransactionsResponse { + transactions: [ + BlockTransaction { + block_identifier: BlockIdentifier { + index: 3, + hash: "3NKd5So3VNqGZtRZiWsti4yaEe1fX79yz5TbfG6jBZqgMnCQQp3R", + }, + transaction: Transaction { + transaction_identifier: TransactionIdentifier { + hash: "CkpZirFuoLVVab6x2ry4j8Ld5gMmQdak7VHW6f5C7VJYE34WAEWqa", + }, + operations: [ + Operation { + operation_identifier: OperationIdentifier { + index: 0, + network_index: None, + }, + related_operations: None, + type: "fee_payment", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qre3erTHfzQckNuibViWQGyyKwZseztqrjPZBv6SQF384Rg6ESAy", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "-10000000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + Operation { + operation_identifier: OperationIdentifier { + index: 1, + network_index: None, + }, + related_operations: None, + type: "payment_source_dec", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qre3erTHfzQckNuibViWQGyyKwZseztqrjPZBv6SQF384Rg6ESAy", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "-1000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + Operation { + operation_identifier: OperationIdentifier { + index: 2, + network_index: None, + }, + related_operations: Some( + [ + OperationIdentifier { + index: 1, + network_index: None, + }, + ], + ), + type: "payment_receiver_inc", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qjYanmV7y9njVeH5UHkz3GYBm7xKir1rAnoY4KsEYUGLMiU45FSM", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "1000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + ], + related_transactions: None, + metadata: None, + }, + }, + BlockTransaction { + block_identifier: BlockIdentifier { + index: 3, + hash: "3NKd5So3VNqGZtRZiWsti4yaEe1fX79yz5TbfG6jBZqgMnCQQp3R", + }, + transaction: Transaction { + transaction_identifier: TransactionIdentifier { + hash: "CkpZB4WE3wDRJ4CqCXqS4dqF8hoRQDVK8banePKUgTR6kvhTfyjRp", + }, + operations: [ + Operation { + operation_identifier: OperationIdentifier { + index: 0, + network_index: None, + }, + related_operations: None, + type: "fee_payment", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qre3erTHfzQckNuibViWQGyyKwZseztqrjPZBv6SQF384Rg6ESAy", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "-50000000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + Operation { + operation_identifier: OperationIdentifier { + index: 1, + network_index: None, + }, + related_operations: None, + type: "payment_source_dec", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qre3erTHfzQckNuibViWQGyyKwZseztqrjPZBv6SQF384Rg6ESAy", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "-10000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + Operation { + operation_identifier: OperationIdentifier { + index: 2, + network_index: None, + }, + related_operations: Some( + [ + OperationIdentifier { + index: 1, + network_index: None, + }, + ], + ), + type: "payment_receiver_inc", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qjYanmV7y9njVeH5UHkz3GYBm7xKir1rAnoY4KsEYUGLMiU45FSM", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "10000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + ], + related_transactions: None, + metadata: None, + }, + }, + BlockTransaction { + block_identifier: BlockIdentifier { + index: 3, + hash: "3NKd5So3VNqGZtRZiWsti4yaEe1fX79yz5TbfG6jBZqgMnCQQp3R", + }, + transaction: Transaction { + transaction_identifier: TransactionIdentifier { + hash: "CkpYeG32dVJUjs6iq3oroXWitXar1eBtV3GVFyH5agw7HPp9bG4yQ", + }, + operations: [ + Operation { + operation_identifier: OperationIdentifier { + index: 0, + network_index: None, + }, + related_operations: None, + type: "fee_payment", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qre3erTHfzQckNuibViWQGyyKwZseztqrjPZBv6SQF384Rg6ESAy", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "-10000000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + Operation { + operation_identifier: OperationIdentifier { + index: 1, + network_index: None, + }, + related_operations: None, + type: "payment_source_dec", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qre3erTHfzQckNuibViWQGyyKwZseztqrjPZBv6SQF384Rg6ESAy", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "-1000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + Operation { + operation_identifier: OperationIdentifier { + index: 2, + network_index: None, + }, + related_operations: Some( + [ + OperationIdentifier { + index: 1, + network_index: None, + }, + ], + ), + type: "payment_receiver_inc", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qjYanmV7y9njVeH5UHkz3GYBm7xKir1rAnoY4KsEYUGLMiU45FSM", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "1000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + ], + related_transactions: None, + metadata: None, + }, + }, + BlockTransaction { + block_identifier: BlockIdentifier { + index: 3, + hash: "3NKd5So3VNqGZtRZiWsti4yaEe1fX79yz5TbfG6jBZqgMnCQQp3R", + }, + transaction: Transaction { + transaction_identifier: TransactionIdentifier { + hash: "CkpZ1u12zrTuEttp7QktfEy7wosHrPV6r3DJkq4sA9f1yKgEqmj5k", + }, + operations: [ + Operation { + operation_identifier: OperationIdentifier { + index: 0, + network_index: None, + }, + related_operations: None, + type: "fee_payment", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qre3erTHfzQckNuibViWQGyyKwZseztqrjPZBv6SQF384Rg6ESAy", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "-50000000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + Operation { + operation_identifier: OperationIdentifier { + index: 1, + network_index: None, + }, + related_operations: None, + type: "payment_source_dec", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qre3erTHfzQckNuibViWQGyyKwZseztqrjPZBv6SQF384Rg6ESAy", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "-10000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + Operation { + operation_identifier: OperationIdentifier { + index: 2, + network_index: None, + }, + related_operations: Some( + [ + OperationIdentifier { + index: 1, + network_index: None, + }, + ], + ), + type: "payment_receiver_inc", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qjYanmV7y9njVeH5UHkz3GYBm7xKir1rAnoY4KsEYUGLMiU45FSM", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "10000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + ], + related_transactions: None, + metadata: None, + }, + }, + BlockTransaction { + block_identifier: BlockIdentifier { + index: 4, + hash: "3NL9qBsNibXPm5Nh8cSg5CCqrbzX5VUVY9gJzAbg7EVCF3hfhazG", + }, + transaction: Transaction { + transaction_identifier: TransactionIdentifier { + hash: "CkpaDbDiRtzF6AUVrny7VoJKTu1wStBHDEsG9W27UFeoeDwMP8VAc", + }, + operations: [ + Operation { + operation_identifier: OperationIdentifier { + index: 0, + network_index: None, + }, + related_operations: None, + type: "fee_payment", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qre3erTHfzQckNuibViWQGyyKwZseztqrjPZBv6SQF384Rg6ESAy", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "-10000000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + Operation { + operation_identifier: OperationIdentifier { + index: 1, + network_index: None, + }, + related_operations: None, + type: "payment_source_dec", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qre3erTHfzQckNuibViWQGyyKwZseztqrjPZBv6SQF384Rg6ESAy", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "-1000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + Operation { + operation_identifier: OperationIdentifier { + index: 2, + network_index: None, + }, + related_operations: Some( + [ + OperationIdentifier { + index: 1, + network_index: None, + }, + ], + ), + type: "payment_receiver_inc", + status: Some( + "Failed", + ), + account: Some( + AccountIdentifier { + address: "B62qjYanmV7y9njVeH5UHkz3GYBm7xKir1rAnoY4KsEYUGLMiU45FSM", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "1000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + ], + related_transactions: None, + metadata: None, + }, + }, + ], + total_count: 12, + next_offset: Some( + 5, + ), + }, +) diff --git a/tests/snapshots/search_transactions__search_transactions_internal_command.snap b/tests/snapshots/search_transactions__search_transactions_internal_command.snap new file mode 100644 index 0000000..5ca859a --- /dev/null +++ b/tests/snapshots/search_transactions__search_transactions_internal_command.snap @@ -0,0 +1,62 @@ +--- +source: tests/search_transactions.rs +expression: response +--- +Ok( + SearchTransactionsResponse { + transactions: [ + BlockTransaction { + block_identifier: BlockIdentifier { + index: 42, + hash: "3NLuesuqs8NoehpUnNouzr6ArcmQcnjQhJaGsPfHMpT7BQZD6erj", + }, + transaction: Transaction { + transaction_identifier: TransactionIdentifier { + hash: "coinbase:1:0:CkpZZWqdA87JmPxHA5NmFEQ3qh7pUmqXi9GBWzf4pADtPEHQAeH7M", + }, + operations: [ + Operation { + operation_identifier: OperationIdentifier { + index: 0, + network_index: None, + }, + related_operations: None, + type: "coinbase_inc", + status: Some( + "Success", + ), + account: Some( + AccountIdentifier { + address: "B62qrwBEB3tjGvjZNNr4h8N2iHGvcgRa6bb3V7Qs7Z15EZVWnyJBpXR", + sub_account: None, + metadata: Some( + Object { + "token_id": String("wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf"), + }, + ), + }, + ), + amount: Some( + Amount { + value: "720000000000", + currency: Currency { + symbol: "MINA", + decimals: 9, + metadata: None, + }, + metadata: None, + }, + ), + coin_change: None, + metadata: None, + }, + ], + related_transactions: None, + metadata: None, + }, + }, + ], + total_count: 1, + next_offset: None, + }, +)