From 902404f660e963e6788ca8fbea23799f2cbdf66a Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Mon, 9 Dec 2024 13:16:57 +0100 Subject: [PATCH 1/7] generate_operations_zkapp_command --- src/api/search_transactions.rs | 69 ++++----------------------- src/sql_to_mesh.rs | 71 +++++----------------------- src/transaction_operations.rs | 86 +++++++++++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 119 deletions(-) diff --git a/src/api/search_transactions.rs b/src/api/search_transactions.rs index 62aad2c..6e95d18 100644 --- a/src/api/search_transactions.rs +++ b/src/api/search_transactions.rs @@ -1,16 +1,13 @@ -use std::collections::BTreeMap; - use coinbase_mesh::models::{ - AccountIdentifier, BlockIdentifier, BlockTransaction, Operation, SearchTransactionsRequest, - SearchTransactionsResponse, Transaction, TransactionIdentifier, + BlockIdentifier, BlockTransaction, SearchTransactionsRequest, SearchTransactionsResponse, Transaction, + TransactionIdentifier, }; -use serde_json::json; use crate::{ generate_internal_command_transaction_identifier, generate_operations_internal_command, - generate_operations_user_command, generate_transaction_metadata, operation, util::DEFAULT_TOKEN_ID, ChainStatus, - InternalCommand, InternalCommandType, MinaMesh, MinaMeshError, OperationType, TransactionStatus, UserCommand, - UserCommandType, ZkAppCommand, + generate_operations_user_command, generate_operations_zkapp_command, generate_transaction_metadata, ChainStatus, + InternalCommand, InternalCommandType, MinaMesh, MinaMeshError, TransactionStatus, UserCommand, UserCommandType, + ZkAppCommand, }; impl MinaMesh { @@ -220,63 +217,17 @@ impl MinaMesh { } pub fn zkapp_commands_to_block_transactions(commands: Vec) -> Vec { - let mut block_map: BTreeMap<(i64, String), BTreeMap>> = BTreeMap::new(); - - for command in commands { - // Group by block identifier (block index and block hash) - let block_key = (command.height.unwrap_or(0), command.state_hash.clone().unwrap_or_default()); - let tx_hash = command.hash; - - // Initialize or update the operation list for this transaction - let operations = block_map.entry(block_key).or_default().entry(tx_hash.clone()).or_default(); - - // Add fee operation (zkapp_fee_payer_dec) - if operations.is_empty() { - operations.push(operation( - 0, - Some(&format!("-{}", command.fee)), - &AccountIdentifier { - address: command.fee_payer.clone(), - metadata: Some(json!({ "token_id": DEFAULT_TOKEN_ID })), - sub_account: None, - }, - OperationType::ZkappFeePayerDec, - Some(&TransactionStatus::Applied), - None, - None, - None, - )); - } - - // Add zkapp balance update operation - operations.push(operation( - 0, - Some(&command.balance_change), - &AccountIdentifier { - address: command.pk_update_body.clone(), - metadata: Some(json!({ "token_id": command.token })), - sub_account: None, - }, - OperationType::ZkappBalanceUpdate, - Some(&command.status), - None, - None, - command.token.as_ref(), - )); - } + let block_map = generate_operations_zkapp_command(commands); let mut result = Vec::new(); for ((block_index, block_hash), tx_map) in block_map { - for (tx_hash, mut operations) in tx_map { - // Ensure the operations are correctly indexed - for (i, operation) in operations.iter_mut().enumerate() { - operation.operation_identifier.index = i as i64; - } - + let block_index = block_index.unwrap_or(0); + let block_hash = block_hash.unwrap_or_default(); + for (tx_hash, operations) in tx_map { let transaction = BlockTransaction { block_identifier: Box::new(BlockIdentifier { index: block_index, hash: block_hash.clone() }), transaction: Box::new(Transaction { - transaction_identifier: Box::new(TransactionIdentifier { hash: tx_hash.clone() }), + transaction_identifier: Box::new(TransactionIdentifier { hash: tx_hash }), operations, metadata: None, related_transactions: None, diff --git a/src/sql_to_mesh.rs b/src/sql_to_mesh.rs index d970390..a7a7f65 100644 --- a/src/sql_to_mesh.rs +++ b/src/sql_to_mesh.rs @@ -1,68 +1,21 @@ -use std::collections::BTreeMap; +use coinbase_mesh::models::{Transaction, TransactionIdentifier}; -use coinbase_mesh::models::{AccountIdentifier, Operation, Transaction, TransactionIdentifier}; -use serde_json::json; - -use crate::{operation, util::DEFAULT_TOKEN_ID, OperationType, TransactionStatus, ZkAppCommand}; +use crate::{generate_operations_zkapp_command, ZkAppCommand}; pub fn zkapp_commands_to_transactions(commands: Vec) -> Vec { - let mut tx_map = BTreeMap::>::new(); - - for command in commands { - let tx_hash = command.hash.clone(); - - // Initialize or update the operation list for this transaction - let operations = tx_map.entry(tx_hash.clone()).or_default(); - - // Add fee operation (zkapp_fee_payer_dec) - if operations.is_empty() { - operations.push(operation( - 0, - Some(&format!("-{}", command.fee)), - &AccountIdentifier { - address: command.fee_payer.clone(), - metadata: Some(json!({ "token_id": DEFAULT_TOKEN_ID })), - sub_account: None, - }, - OperationType::ZkappFeePayerDec, - Some(&TransactionStatus::Applied), - None, - None, - None, - )); - } - - // Add zkapp balance update operation - operations.push(operation( - 0, - Some(&command.balance_change), - &AccountIdentifier { - address: command.pk_update_body.clone(), - metadata: Some(json!({ "token_id": DEFAULT_TOKEN_ID })), - sub_account: None, - }, - OperationType::ZkappBalanceUpdate, - Some(&command.status), - None, - None, - None, - )); - } + let block_map = generate_operations_zkapp_command(commands); let mut result = Vec::new(); - for (tx_hash, mut operations) in tx_map { - // Ensure the operations are correctly indexed - for (i, operation) in operations.iter_mut().enumerate() { - operation.operation_identifier.index = i as i64; + for (_, tx_map) in block_map { + for (tx_hash, operations) in tx_map { + let transaction = Transaction { + transaction_identifier: Box::new(TransactionIdentifier { hash: tx_hash }), + operations, + metadata: None, + related_transactions: None, + }; + result.push(transaction); } - - let transaction = Transaction { - transaction_identifier: Box::new(TransactionIdentifier { hash: tx_hash.clone() }), - operations, - metadata: None, - related_transactions: None, - }; - result.push(transaction); } result diff --git a/src/transaction_operations.rs b/src/transaction_operations.rs index 5a5d7cc..41d44ec 100644 --- a/src/transaction_operations.rs +++ b/src/transaction_operations.rs @@ -1,10 +1,12 @@ +use std::collections::BTreeMap; + use coinbase_mesh::models::{AccountIdentifier, Amount, Currency, Operation, OperationIdentifier}; use convert_case::{Case, Casing}; use serde_json::{json, Map, Value}; use crate::{ util::DEFAULT_TOKEN_ID, InternalCommandOperationsData, InternalCommandType, OperationStatus, OperationType, - TransactionStatus, UserCommandOperationsData, UserCommandType, + TransactionStatus, UserCommandOperationsData, UserCommandType, ZkAppCommand, }; /// Creates a `Currency` based on the token provided. @@ -276,3 +278,85 @@ pub fn generate_operations_internal_command(da operations } + +/// Groups zkApp commands into operations mapped by block and transaction. +/// +/// This function processes a vector of `ZkAppCommand` objects, generating +/// operations for each command and organizing them into a nested `BTreeMap` +/// structure: +/// - Outer key: `(Option, Option)` representing block height and +/// state hash. +/// - Inner key: `String` representing the transaction hash. +/// - Value: `Vec` containing operations for each transaction. +/// +/// ### Operations Generated +/// - `ZkappFeePayerDec`: Deducts the fee from the fee payer account. +/// - `ZkappBalanceUpdate`: Updates the balance for the zkApp account. +/// +/// Operations are indexed sequentially (starting at 0) within each transaction. +/// +/// ### Parameters +/// - `commands`: A vector of `ZkAppCommand` objects. +/// +/// ### Returns +/// - A `BTreeMap` mapping blocks and transactions to their operations. +/// +/// This function supports constructing higher-level transaction structures like +/// `BlockTransaction`, `Transaction`. +pub fn generate_operations_zkapp_command( + commands: Vec, +) -> BTreeMap<(Option, Option), BTreeMap>> { + let mut block_map: BTreeMap<(Option, Option), BTreeMap>> = BTreeMap::new(); + + for command in commands { + let block_key = (command.height, command.state_hash.clone()); + let tx_hash = command.hash.clone(); + + let operations = block_map.entry(block_key).or_default().entry(tx_hash.clone()).or_default(); + + // Add fee operation (zkapp_fee_payer_dec) + if operations.is_empty() { + operations.push(operation( + 0, + Some(&format!("-{}", command.fee)), + &AccountIdentifier { + address: command.fee_payer.clone(), + metadata: Some(json!({ "token_id": DEFAULT_TOKEN_ID })), + sub_account: None, + }, + OperationType::ZkappFeePayerDec, + Some(&TransactionStatus::Applied), + None, + None, + None, + )); + } + + // Add zkapp balance update operation + operations.push(operation( + 0, + Some(&command.balance_change), + &AccountIdentifier { + address: command.pk_update_body.clone(), + metadata: Some(json!({ "token_id": command.token })), + sub_account: None, + }, + OperationType::ZkappBalanceUpdate, + Some(&command.status), + None, + None, + command.token.as_ref(), + )); + } + + // Reindex operations within each transaction + for (_, tx_map) in block_map.iter_mut() { + for (_, operations) in tx_map.iter_mut() { + for (i, operation) in operations.iter_mut().enumerate() { + operation.operation_identifier.index = i as i64; + } + } + } + + block_map +} From 341c5483b929b7ee333b5de7945ed62c89a78d4b Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Mon, 9 Dec 2024 15:29:59 +0100 Subject: [PATCH 2/7] move zkapp_commands_to_transactions to block --- src/api/block.rs | 54 +++++++++++++++------------------------------- src/lib.rs | 1 - src/sql_to_mesh.rs | 22 ------------------- 3 files changed, 17 insertions(+), 60 deletions(-) delete mode 100644 src/sql_to_mesh.rs diff --git a/src/api/block.rs b/src/api/block.rs index 7dd2a17..004541a 100644 --- a/src/api/block.rs +++ b/src/api/block.rs @@ -8,7 +8,7 @@ use sqlx::FromRow; use crate::{ generate_internal_command_transaction_identifier, generate_operations_internal_command, - generate_operations_user_command, generate_transaction_metadata, sql_to_mesh::zkapp_commands_to_transactions, + generate_operations_user_command, generate_operations_zkapp_command, generate_transaction_metadata, util::DEFAULT_TOKEN_ID, ChainStatus, InternalCommandMetadata, InternalCommandType, MinaMesh, MinaMeshError, TransactionStatus, UserCommandMetadata, UserCommandType, ZkAppCommand, }; @@ -160,41 +160,21 @@ pub struct BlockMetadata { winner: String, } -#[derive(Debug, PartialEq, Eq, FromRow, Serialize)] -pub struct ZkappCommandMetadata { - id: i64, - memo: Option, - hash: String, - fee_payer: String, - fee: String, - valid_until: Option, - nonce: i64, - sequence_no: i64, - status: TransactionStatus, - failure_reasons: Option>, - balance_change: String, - account: String, - token: String, -} +pub fn zkapp_commands_to_transactions(commands: Vec) -> Vec { + let block_map = generate_operations_zkapp_command(commands); -#[derive(Debug, PartialEq, Eq, FromRow, Serialize)] -pub struct ZkappAccountUpdateMetadata { - account_identifier_id: i32, - update_id: i32, - balance_change: String, - increment_nonce: bool, - events_id: i32, - actions_id: i32, - call_data_id: i32, - call_depth: i32, - zkapp_network_precondition_id: i32, - zkapp_account_precondition_id: i32, - zkapp_valid_while_precondition_id: Option, - use_full_commitment: bool, - implicit_account_creation_fee: bool, - may_use_token: String, - authorization_kind: String, - verification_key_hash_id: Option, - account: String, - token: String, + let mut result = Vec::new(); + for (_, tx_map) in block_map { + for (tx_hash, operations) in tx_map { + let transaction = Transaction { + transaction_identifier: Box::new(TransactionIdentifier { hash: tx_hash }), + operations, + metadata: None, + related_transactions: None, + }; + result.push(transaction); + } + } + + result } diff --git a/src/lib.rs b/src/lib.rs index 2f4b46d..407143c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,6 @@ mod create_router; mod error; mod graphql; mod playground; -mod sql_to_mesh; pub mod test; mod transaction_operations; mod types; diff --git a/src/sql_to_mesh.rs b/src/sql_to_mesh.rs deleted file mode 100644 index a7a7f65..0000000 --- a/src/sql_to_mesh.rs +++ /dev/null @@ -1,22 +0,0 @@ -use coinbase_mesh::models::{Transaction, TransactionIdentifier}; - -use crate::{generate_operations_zkapp_command, ZkAppCommand}; - -pub fn zkapp_commands_to_transactions(commands: Vec) -> Vec { - let block_map = generate_operations_zkapp_command(commands); - - let mut result = Vec::new(); - for (_, tx_map) in block_map { - for (tx_hash, operations) in tx_map { - let transaction = Transaction { - transaction_identifier: Box::new(TransactionIdentifier { hash: tx_hash }), - operations, - metadata: None, - related_transactions: None, - }; - result.push(transaction); - } - } - - result -} From 8e893dff39f24549f55be958216a434d34ef4fe8 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 10 Dec 2024 14:28:12 +0100 Subject: [PATCH 3/7] test_util: sort_transactions --- src/test.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/test.rs b/src/test.rs index ff7d438..b20a42f 100644 --- a/src/test.rs +++ b/src/test.rs @@ -106,6 +106,7 @@ fn normalize_body(raw: &str) -> Result { let mut json_unsorted: Value = serde_json::from_str(raw)?; sort_json_value(&mut json_unsorted); remove_empty_tx_fields(&mut json_unsorted); + sort_transactions(&mut json_unsorted); Ok(serde_json::to_string_pretty(&json_unsorted)?) } @@ -132,10 +133,6 @@ fn sort_json_value(value: &mut Value) { } } -// Remove empty "related_transactions" | "other_transactions" arrays from the -// JSON This is necessary because Rosetta OCaml includes empty arrays in the -// response but mina-mesh does not -// Workaround for https://github.com/MinaFoundation/MinaMesh/issues/48 fn remove_empty_tx_fields(value: &mut Value) { match value { Value::Object(map) => { @@ -160,6 +157,22 @@ fn remove_empty_tx_fields(value: &mut Value) { } } +fn sort_transactions(value: &mut Value) { + if let Some(block) = value.get_mut("block") { + if let Some(transactions) = block.get_mut("transactions") { + if let Value::Array(tx_array) = transactions { + tx_array.sort_by(|a, b| { + let hash_a = + a.get("transaction_identifier").and_then(|ti| ti.get("hash")).and_then(|h| h.as_str()).unwrap_or(""); + let hash_b = + b.get("transaction_identifier").and_then(|ti| ti.get("hash")).and_then(|h| h.as_str()).unwrap_or(""); + hash_a.cmp(hash_b) + }); + } + } + } +} + pub const DEVNET_BLOCKCHAIN_ID: &str = "mina"; pub const DEVNET_NETWORK_ID: &str = "testnet"; From 5b4679ed9f8c56590d8ac64ec4fb29651a8b93bf Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 10 Dec 2024 14:46:24 +0100 Subject: [PATCH 4/7] update zkapp query and balance_change operation push --- ...fa9675d842510ca206e8e32066423eacb79e.json} | 10 +++--- sql/queries/zkapp_commands.sql | 6 ++-- src/api/block.rs | 2 +- src/transaction_operations.rs | 32 ++++++++++--------- src/types.rs | 12 +++---- 5 files changed, 32 insertions(+), 30 deletions(-) rename .sqlx/{query-52d18e4008bd0006fafee2f64d97c2ff75a1d9b210b99eed6f14e8a17d05530b.json => query-b060c16dd0800f47632051a04106fa9675d842510ca206e8e32066423eacb79e.json} (67%) diff --git a/.sqlx/query-52d18e4008bd0006fafee2f64d97c2ff75a1d9b210b99eed6f14e8a17d05530b.json b/.sqlx/query-b060c16dd0800f47632051a04106fa9675d842510ca206e8e32066423eacb79e.json similarity index 67% rename from .sqlx/query-52d18e4008bd0006fafee2f64d97c2ff75a1d9b210b99eed6f14e8a17d05530b.json rename to .sqlx/query-b060c16dd0800f47632051a04106fa9675d842510ca206e8e32066423eacb79e.json index 0dc5cfd..b6c203d 100644 --- a/.sqlx/query-52d18e4008bd0006fafee2f64d97c2ff75a1d9b210b99eed6f14e8a17d05530b.json +++ b/.sqlx/query-b060c16dd0800f47632051a04106fa9675d842510ca206e8e32066423eacb79e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n zc.id,\n zc.memo,\n zc.hash,\n pk_fee_payer.value AS fee_payer,\n zfpb.fee,\n zfpb.valid_until,\n zfpb.nonce,\n bzc.sequence_no,\n bzc.status AS \"status: TransactionStatus\",\n b.state_hash,\n b.height,\n bzc.block_id,\n cast(0 AS BIGINT) AS total_count,\n ARRAY(\n SELECT\n unnest(zauf.failures)\n FROM\n zkapp_account_update_failures AS zauf\n WHERE\n zauf.id=ANY (bzc.failure_reasons_ids)\n ) AS failure_reasons,\n zaub.balance_change,\n pk_update_body.value AS pk_update_body,\n token_update_body.value AS token\nFROM\n blocks_zkapp_commands AS bzc\n INNER JOIN zkapp_commands AS zc ON bzc.zkapp_command_id=zc.id\n INNER JOIN zkapp_fee_payer_body AS zfpb ON zc.zkapp_fee_payer_body_id=zfpb.id\n INNER JOIN public_keys AS pk_fee_payer ON zfpb.public_key_id=pk_fee_payer.id\n INNER JOIN blocks AS b ON bzc.block_id=b.id\n LEFT JOIN zkapp_account_update AS zau ON zau.id=ANY (zc.zkapp_account_updates_ids)\n LEFT JOIN zkapp_account_update_body AS zaub ON zau.body_id=zaub.id\n LEFT JOIN account_identifiers AS ai_update_body ON zaub.account_identifier_id=ai_update_body.id\n LEFT JOIN public_keys AS pk_update_body ON ai_update_body.public_key_id=pk_update_body.id\n LEFT JOIN tokens AS token_update_body ON ai_update_body.token_id=token_update_body.id\nWHERE\n bzc.block_id=$1\n AND (\n token_update_body.value=$2\n OR token_update_body.id IS NULL\n )\nORDER BY\n zc.id,\n bzc.sequence_no\n", + "query": "SELECT\n zc.id,\n zc.memo,\n zc.hash,\n pk_fee_payer.value AS fee_payer,\n zfpb.fee,\n zfpb.valid_until,\n zfpb.nonce,\n bzc.sequence_no,\n bzc.status AS \"status: TransactionStatus\",\n b.state_hash,\n b.height,\n bzc.block_id,\n cast(0 AS BIGINT) AS total_count,\n ARRAY(\n SELECT\n unnest(zauf.failures)\n FROM\n zkapp_account_update_failures AS zauf\n WHERE\n zauf.id=ANY (bzc.failure_reasons_ids)\n ) AS failure_reasons,\n zaub.balance_change AS \"balance_change?\",\n pk_update_body.value AS \"pk_update_body?\",\n token_update_body.value AS \"token?\"\nFROM\n blocks_zkapp_commands AS bzc\n INNER JOIN zkapp_commands AS zc ON bzc.zkapp_command_id=zc.id\n INNER JOIN zkapp_fee_payer_body AS zfpb ON zc.zkapp_fee_payer_body_id=zfpb.id\n INNER JOIN public_keys AS pk_fee_payer ON zfpb.public_key_id=pk_fee_payer.id\n INNER JOIN blocks AS b ON bzc.block_id=b.id\n LEFT JOIN zkapp_account_update AS zau ON zau.id=ANY (zc.zkapp_account_updates_ids)\n LEFT JOIN zkapp_account_update_body AS zaub ON zau.body_id=zaub.id\n LEFT JOIN account_identifiers AS ai_update_body ON zaub.account_identifier_id=ai_update_body.id\n LEFT JOIN public_keys AS pk_update_body ON ai_update_body.public_key_id=pk_update_body.id\n LEFT JOIN tokens AS token_update_body ON ai_update_body.token_id=token_update_body.id\nWHERE\n bzc.block_id=$1\n AND (\n token_update_body.value=$2\n OR token_update_body.id IS NULL\n )\nORDER BY\n zc.id,\n bzc.sequence_no\n", "describe": { "columns": [ { @@ -85,17 +85,17 @@ }, { "ordinal": 14, - "name": "balance_change", + "name": "balance_change?", "type_info": "Text" }, { "ordinal": 15, - "name": "pk_update_body", + "name": "pk_update_body?", "type_info": "Text" }, { "ordinal": 16, - "name": "token", + "name": "token?", "type_info": "Text" } ], @@ -125,5 +125,5 @@ false ] }, - "hash": "52d18e4008bd0006fafee2f64d97c2ff75a1d9b210b99eed6f14e8a17d05530b" + "hash": "b060c16dd0800f47632051a04106fa9675d842510ca206e8e32066423eacb79e" } diff --git a/sql/queries/zkapp_commands.sql b/sql/queries/zkapp_commands.sql index dc8d359..4fe8458 100644 --- a/sql/queries/zkapp_commands.sql +++ b/sql/queries/zkapp_commands.sql @@ -20,9 +20,9 @@ SELECT WHERE zauf.id=ANY (bzc.failure_reasons_ids) ) AS failure_reasons, - zaub.balance_change, - pk_update_body.value AS pk_update_body, - token_update_body.value AS token + zaub.balance_change AS "balance_change?", + pk_update_body.value AS "pk_update_body?", + token_update_body.value AS "token?" FROM blocks_zkapp_commands AS bzc INNER JOIN zkapp_commands AS zc ON bzc.zkapp_command_id=zc.id diff --git a/src/api/block.rs b/src/api/block.rs index 004541a..e370386 100644 --- a/src/api/block.rs +++ b/src/api/block.rs @@ -149,7 +149,7 @@ pub struct BlockMetadata { state_hash: String, sub_window_densities: Vec, timestamp: String, - total_currency: String, + total_currency: Option, parent_hash: String, parent_id: Option, proposed_protocol_version_id: Option, diff --git a/src/transaction_operations.rs b/src/transaction_operations.rs index 41d44ec..4cf68f0 100644 --- a/src/transaction_operations.rs +++ b/src/transaction_operations.rs @@ -332,21 +332,23 @@ pub fn generate_operations_zkapp_command( )); } - // Add zkapp balance update operation - operations.push(operation( - 0, - Some(&command.balance_change), - &AccountIdentifier { - address: command.pk_update_body.clone(), - metadata: Some(json!({ "token_id": command.token })), - sub_account: None, - }, - OperationType::ZkappBalanceUpdate, - Some(&command.status), - None, - None, - command.token.as_ref(), - )); + if let Some(balance_change) = &command.balance_change { + // Add zkapp balance update operation + operations.push(operation( + 0, + Some(balance_change), + &AccountIdentifier { + address: command.pk_update_body.unwrap_or_default().clone(), + metadata: Some(json!({ "token_id": command.token })), + sub_account: None, + }, + OperationType::ZkappBalanceUpdate, + Some(&command.status), + None, + None, + command.token.as_ref(), + )); + } } // Reindex operations within each transaction diff --git a/src/types.rs b/src/types.rs index 05a9549..a304966 100644 --- a/src/types.rs +++ b/src/types.rs @@ -95,13 +95,13 @@ pub struct ZkAppCommand { pub memo: Option, pub hash: String, pub fee_payer: String, - pub pk_update_body: String, + pub pk_update_body: Option, pub fee: String, pub valid_until: Option, pub nonce: Option, pub sequence_no: i32, pub status: TransactionStatus, - pub balance_change: String, + pub balance_change: Option, pub state_hash: Option, pub failure_reasons: Option>, pub token: Option, @@ -144,7 +144,7 @@ pub struct UserCommandMetadata { pub command_type: UserCommandType, pub nonce: i64, pub amount: Option, - pub fee: String, + pub fee: Option, pub valid_until: Option, pub memo: Option, pub hash: String, @@ -247,7 +247,7 @@ impl UserCommandOperationsData for UserCommandMetadata { } fn fee(&self) -> &str { - &self.fee + &self.fee.as_deref().unwrap_or("0") } fn status(&self) -> &TransactionStatus { @@ -288,7 +288,7 @@ pub struct InternalCommand { pub struct InternalCommandMetadata { pub command_type: InternalCommandType, pub receiver: String, - pub fee: String, + pub fee: Option, pub hash: String, pub creation_fee: Option, pub sequence_no: i32, @@ -342,7 +342,7 @@ impl InternalCommandOperationsData for InternalCommandMetadata { } fn fee(&self) -> String { - self.fee.clone() + self.fee.clone().unwrap_or("0".to_string()) } fn creation_fee(&self) -> Option<&String> { From aa8f23a1a888beb3b6690f76c338f2291bda0bab Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Tue, 10 Dec 2024 17:19:15 +0100 Subject: [PATCH 5/7] adjust BlockMissing error --- src/api/account_balance.rs | 2 +- src/api/block.rs | 2 +- src/error.rs | 31 +++++++++++++++---- tests/error.rs | 8 ++++- .../network_options__network_options.snap | 8 ++++- 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/api/account_balance.rs b/src/api/account_balance.rs index 4a369f9..5c35531 100644 --- a/src/api/account_balance.rs +++ b/src/api/account_balance.rs @@ -33,7 +33,7 @@ impl MinaMesh { let block = sqlx::query_file!("sql/queries/maybe_block.sql", index) .fetch_optional(&self.pg_pool) .await? - .ok_or(MinaMeshError::BlockMissing(index.unwrap().to_string()))?; + .ok_or(MinaMeshError::BlockMissing(index, None))?; // has canonical height / do we really need to do a different query? let maybe_account_balance_info = sqlx::query_file!( "sql/queries/maybe_account_balance_info.sql", diff --git a/src/api/block.rs b/src/api/block.rs index e370386..db0f866 100644 --- a/src/api/block.rs +++ b/src/api/block.rs @@ -20,7 +20,7 @@ impl MinaMesh { let partial_block_identifier = *request.block_identifier; let metadata = match self.block_metadata(&partial_block_identifier).await? { Some(metadata) => metadata, - None => return Err(MinaMeshError::BlockMissing(serde_json::to_string(&partial_block_identifier)?)), + None => return Err(MinaMeshError::BlockMissing(partial_block_identifier.index, partial_block_identifier.hash)), }; let parent_block_metadata = match &metadata.parent_id { Some(parent_id) => { diff --git a/src/error.rs b/src/error.rs index 3f86bf2..3c47b39 100644 --- a/src/error.rs +++ b/src/error.rs @@ -39,7 +39,7 @@ pub enum MinaMeshError { TransactionNotFound(String), #[error("Block not found")] - BlockMissing(String), + BlockMissing(Option, Option), #[error("Malformed public key")] MalformedPublicKey, @@ -118,7 +118,7 @@ impl MinaMeshError { MinaMeshError::AccountNotFound("Account ID".to_string()), MinaMeshError::InvariantViolation, MinaMeshError::TransactionNotFound("Transaction ID".to_string()), - MinaMeshError::BlockMissing("Block ID".to_string()), + MinaMeshError::BlockMissing(Some(-1), Some("test_hash".to_string())), MinaMeshError::MalformedPublicKey, MinaMeshError::OperationsNotValid(vec![]), MinaMeshError::UnsupportedOperationForConstruction, @@ -150,7 +150,7 @@ impl MinaMeshError { MinaMeshError::AccountNotFound(_) => 6, MinaMeshError::InvariantViolation => 7, MinaMeshError::TransactionNotFound(_) => 8, - MinaMeshError::BlockMissing(_) => 9, + MinaMeshError::BlockMissing(_, _) => 9, MinaMeshError::MalformedPublicKey => 10, MinaMeshError::OperationsNotValid(_) => 11, MinaMeshError::UnsupportedOperationForConstruction => 12, @@ -179,7 +179,7 @@ impl MinaMeshError { | MinaMeshError::TransactionSubmitNoSender | MinaMeshError::AccountNotFound(_) | MinaMeshError::TransactionNotFound(_) - | MinaMeshError::BlockMissing(_) + | MinaMeshError::BlockMissing(_, _) | MinaMeshError::ChainInfoMissing ) } @@ -218,6 +218,25 @@ impl MinaMeshError { ), "transaction": tx, }), + MinaMeshError::BlockMissing(index, hash) => { + let block_identifier = match (index, hash) { + (Some(idx), Some(hsh)) => format!("index={}, hash={}", idx, hsh), + (Some(idx), None) => format!("index={}", idx), + (None, Some(hsh)) => format!("hash={}", hsh), + (None, None) => "no identifying information (index or hash)".to_string(), + }; + + let error_message = + format!("We couldn't find the block in the archive node, specified by {}.", block_identifier); + json!({ + "error": error_message, + "block": { + "index": index, + "hash": hash, + }, + }) + } + _ => json!(""), } } @@ -244,7 +263,7 @@ impl MinaMeshError { MinaMeshError::AccountNotFound(_) => "The specified account could not be found.".to_string(), MinaMeshError::InvariantViolation => "An internal invariant was violated.".to_string(), MinaMeshError::TransactionNotFound(_) => "The specified transaction could not be found.".to_string(), - MinaMeshError::BlockMissing(_) => "The specified block could not be found.".to_string(), + MinaMeshError::BlockMissing(_, _) => "The specified block could not be found.".to_string(), MinaMeshError::MalformedPublicKey => "The provided public key is malformed.".to_string(), MinaMeshError::OperationsNotValid(_) => { "We could not convert those operations to a valid transaction.".to_string() @@ -290,7 +309,7 @@ impl IntoResponse for MinaMeshError { MinaMeshError::AccountNotFound(_) => StatusCode::NOT_FOUND, MinaMeshError::InvariantViolation => StatusCode::INTERNAL_SERVER_ERROR, MinaMeshError::TransactionNotFound(_) => StatusCode::NOT_FOUND, - MinaMeshError::BlockMissing(_) => StatusCode::NOT_FOUND, + MinaMeshError::BlockMissing(_, _) => StatusCode::NOT_FOUND, MinaMeshError::MalformedPublicKey => StatusCode::BAD_REQUEST, MinaMeshError::OperationsNotValid(_) => StatusCode::BAD_REQUEST, MinaMeshError::UnsupportedOperationForConstruction => StatusCode::BAD_REQUEST, diff --git a/tests/error.rs b/tests/error.rs index 676fc4b..1b867f6 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -64,7 +64,13 @@ async fn test_error_properties() { true, StatusCode::NOT_FOUND, ), - (BlockMissing("Block ID".to_string()), 9, "The specified block could not be found.", true, StatusCode::NOT_FOUND), + ( + BlockMissing(Some(1), Some("test_hash".to_string())), + 9, + "The specified block could not be found.", + true, + StatusCode::NOT_FOUND, + ), (MalformedPublicKey, 10, "The provided public key is malformed.", false, StatusCode::BAD_REQUEST), ( OperationsNotValid(vec![]), diff --git a/tests/snapshots/network_options__network_options.snap b/tests/snapshots/network_options__network_options.snap index 9a9e835..c16d638 100644 --- a/tests/snapshots/network_options__network_options.snap +++ b/tests/snapshots/network_options__network_options.snap @@ -140,7 +140,13 @@ Allow { ), retriable: true, details: Some( - String(""), + Object { + "block": Object { + "hash": String("test_hash"), + "index": Number(-1), + }, + "error": String("We couldn't find the block in the archive node, specified by index=-1, hash=test_hash."), + }, ), }, Error { From aa9b143db50d5a3c018b510c2f1467e5022025d7 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Wed, 11 Dec 2024 10:55:24 +0100 Subject: [PATCH 6/7] more block testing --- src/test.rs | 4 ++++ tests/compare_to_ocaml.rs | 6 ++++++ tests/fixtures/block.rs | 33 +++++++++++++++++++++++++++++---- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/test.rs b/src/test.rs index b20a42f..9ea4e18 100644 --- a/src/test.rs +++ b/src/test.rs @@ -133,6 +133,10 @@ fn sort_json_value(value: &mut Value) { } } +// Remove empty "related_transactions" | "other_transactions" arrays from the +// JSON This is necessary because Rosetta OCaml includes empty arrays in the +// response but mina-mesh does not +// Workaround for https://github.com/MinaFoundation/MinaMesh/issues/48 fn remove_empty_tx_fields(value: &mut Value) { match value { Value::Object(map) => { diff --git a/tests/compare_to_ocaml.rs b/tests/compare_to_ocaml.rs index 9c9bcf4..a8062bb 100644 --- a/tests/compare_to_ocaml.rs +++ b/tests/compare_to_ocaml.rs @@ -86,3 +86,9 @@ async fn block() -> Result<()> { let (subpath, reqs) = fixtures::block(); assert_responses_eq(subpath, &reqs).await } + +#[tokio::test] +async fn block_not_found() -> Result<()> { + let (subpath, reqs) = fixtures::block_not_found(); + assert_responses_contain(subpath, &reqs, "\"message\": \"Block not found").await +} diff --git a/tests/fixtures/block.rs b/tests/fixtures/block.rs index 6fdb090..19d2576 100644 --- a/tests/fixtures/block.rs +++ b/tests/fixtures/block.rs @@ -6,14 +6,39 @@ use mina_mesh::{ use super::CompareGroup; pub fn block<'a>() -> CompareGroup<'a> { - ("/block", vec![ + let blocks_by_index: Vec> = (373700 ..= 373800) + .map(|index| { + Box::new(BlockRequest { + network_identifier: Box::new(network_id()), + block_identifier: Box::new(PartialBlockIdentifier { index: Some(index), hash: None }), + }) as Box + }) + .collect(); + + let blocks_by_hash: Vec> = vec![ Box::new(BlockRequest { network_identifier: Box::new(network_id()), - block_identifier: Box::new(PartialBlockIdentifier { index: Some(373797), hash: None }), + block_identifier: Box::new(PartialBlockIdentifier { + index: None, + hash: Some("3NLRpBDtzPySnPXGzKjFn4jsnPchoRk6N88NVGV3bexvdwJaptg1".to_string()), + }), }), Box::new(BlockRequest { network_identifier: Box::new(network_id()), - block_identifier: Box::new(PartialBlockIdentifier { index: Some(52676), hash: None }), + block_identifier: Box::new(PartialBlockIdentifier { + index: Some(73706), + hash: Some("3NK8kTsPoTErvXN5PqtZVpztz6ZE8hmCATRAs8wTcdaevUsdALf3".to_string()), + }), }), - ]) + ]; + + let blocks = blocks_by_index.into_iter().chain(blocks_by_hash.into_iter()).collect(); + ("/block", blocks) +} + +pub fn block_not_found<'a>() -> CompareGroup<'a> { + ("/block", vec![Box::new(BlockRequest { + network_identifier: Box::new(network_id()), + block_identifier: Box::new(PartialBlockIdentifier { index: None, hash: Some("not_founc".to_string()) }), + })]) } From 7aa14280912bb5443bebe421e4ae8f642aff4603 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Wed, 11 Dec 2024 15:11:27 +0100 Subject: [PATCH 7/7] satisfy ci checks --- src/test.rs | 18 ++++++++---------- src/transaction_operations.rs | 22 +++++++++++++--------- src/types.rs | 2 +- tests/fixtures/block.rs | 6 ++++-- 4 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/test.rs b/src/test.rs index 9ea4e18..1c8752e 100644 --- a/src/test.rs +++ b/src/test.rs @@ -163,16 +163,14 @@ fn remove_empty_tx_fields(value: &mut Value) { fn sort_transactions(value: &mut Value) { if let Some(block) = value.get_mut("block") { - if let Some(transactions) = block.get_mut("transactions") { - if let Value::Array(tx_array) = transactions { - tx_array.sort_by(|a, b| { - let hash_a = - a.get("transaction_identifier").and_then(|ti| ti.get("hash")).and_then(|h| h.as_str()).unwrap_or(""); - let hash_b = - b.get("transaction_identifier").and_then(|ti| ti.get("hash")).and_then(|h| h.as_str()).unwrap_or(""); - hash_a.cmp(hash_b) - }); - } + if let Some(Value::Array(tx_array)) = block.get_mut("transactions") { + tx_array.sort_by(|a, b| { + let hash_a = + a.get("transaction_identifier").and_then(|ti| ti.get("hash")).and_then(|h| h.as_str()).unwrap_or(""); + let hash_b = + b.get("transaction_identifier").and_then(|ti| ti.get("hash")).and_then(|h| h.as_str()).unwrap_or(""); + hash_a.cmp(hash_b) + }); } } } diff --git a/src/transaction_operations.rs b/src/transaction_operations.rs index 4cf68f0..7428514 100644 --- a/src/transaction_operations.rs +++ b/src/transaction_operations.rs @@ -279,15 +279,21 @@ pub fn generate_operations_internal_command(da operations } +type BlockKey = (Option, Option); // Represents the block identifier +type TransactionOperations = BTreeMap>; // Maps transaction hashes to their operations +type BlockMap = BTreeMap; // Maps block keys to transaction operations + /// Groups zkApp commands into operations mapped by block and transaction. /// /// This function processes a vector of `ZkAppCommand` objects, generating -/// operations for each command and organizing them into a nested `BTreeMap` +/// operations for each command and organizing them into +/// `BlockMap = BTreeMap` a nested `BTreeMap` /// structure: -/// - Outer key: `(Option, Option)` representing block height and +/// - `BlockKey`: `(Option, Option)` representing block height and /// state hash. -/// - Inner key: `String` representing the transaction hash. -/// - Value: `Vec` containing operations for each transaction. +/// - `TransactionOperations = BTreeMap>` mapping +/// - Inner key: `String` representing the transaction hash. +/// - Value: `Vec` containing operations for each transaction. /// /// ### Operations Generated /// - `ZkappFeePayerDec`: Deducts the fee from the fee payer account. @@ -303,10 +309,8 @@ pub fn generate_operations_internal_command(da /// /// This function supports constructing higher-level transaction structures like /// `BlockTransaction`, `Transaction`. -pub fn generate_operations_zkapp_command( - commands: Vec, -) -> BTreeMap<(Option, Option), BTreeMap>> { - let mut block_map: BTreeMap<(Option, Option), BTreeMap>> = BTreeMap::new(); +pub fn generate_operations_zkapp_command(commands: Vec) -> BlockMap { + let mut block_map: BlockMap = BTreeMap::new(); for command in commands { let block_key = (command.height, command.state_hash.clone()); @@ -351,7 +355,7 @@ pub fn generate_operations_zkapp_command( } } - // Reindex operations within each transaction + // Re-index operations within each transaction for (_, tx_map) in block_map.iter_mut() { for (_, operations) in tx_map.iter_mut() { for (i, operation) in operations.iter_mut().enumerate() { diff --git a/src/types.rs b/src/types.rs index a304966..3259c56 100644 --- a/src/types.rs +++ b/src/types.rs @@ -247,7 +247,7 @@ impl UserCommandOperationsData for UserCommandMetadata { } fn fee(&self) -> &str { - &self.fee.as_deref().unwrap_or("0") + self.fee.as_deref().unwrap_or("0") } fn status(&self) -> &TransactionStatus { diff --git a/tests/fixtures/block.rs b/tests/fixtures/block.rs index 19d2576..1926fef 100644 --- a/tests/fixtures/block.rs +++ b/tests/fixtures/block.rs @@ -20,6 +20,7 @@ pub fn block<'a>() -> CompareGroup<'a> { network_identifier: Box::new(network_id()), block_identifier: Box::new(PartialBlockIdentifier { index: None, + // cspell:disable-next-line hash: Some("3NLRpBDtzPySnPXGzKjFn4jsnPchoRk6N88NVGV3bexvdwJaptg1".to_string()), }), }), @@ -27,18 +28,19 @@ pub fn block<'a>() -> CompareGroup<'a> { network_identifier: Box::new(network_id()), block_identifier: Box::new(PartialBlockIdentifier { index: Some(73706), + // cspell:disable-next-line hash: Some("3NK8kTsPoTErvXN5PqtZVpztz6ZE8hmCATRAs8wTcdaevUsdALf3".to_string()), }), }), ]; - let blocks = blocks_by_index.into_iter().chain(blocks_by_hash.into_iter()).collect(); + let blocks = blocks_by_index.into_iter().chain(blocks_by_hash).collect(); ("/block", blocks) } pub fn block_not_found<'a>() -> CompareGroup<'a> { ("/block", vec![Box::new(BlockRequest { network_identifier: Box::new(network_id()), - block_identifier: Box::new(PartialBlockIdentifier { index: None, hash: Some("not_founc".to_string()) }), + block_identifier: Box::new(PartialBlockIdentifier { index: None, hash: Some("not_found".to_string()) }), })]) }