From f3aaacb6ca5b6aac33f31647fd92c5dedaa0bec2 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 22 Jul 2025 08:40:04 -0300 Subject: [PATCH 1/5] add get_balance rpc method --- Cargo.lock | 15 +++ Cargo.toml | 1 + common/src/rpc_primitives/requests.rs | 12 ++ sequencer_rpc/Cargo.toml | 1 + sequencer_rpc/src/lib.rs | 1 + sequencer_rpc/src/net_utils.rs | 2 +- sequencer_rpc/src/process.rs | 162 ++++++++++++++++++++++++++ 7 files changed, 193 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 10ab1e9..78c4c62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,11 +196,13 @@ checksum = "a27e8fe9ba4ae613c21f677c2cfaf0696c3744030c6f485b34634e502d6bb379" dependencies = [ "actix-codec", "actix-http", + "actix-macros", "actix-router", "actix-rt", "actix-server", "actix-service", "actix-utils", + "actix-web-codegen", "ahash 0.7.8", "bytes", "bytestring", @@ -225,6 +227,18 @@ dependencies = [ "url", ] +[[package]] +name = "actix-web-codegen" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "actix_derive" version = "0.6.2" @@ -4367,6 +4381,7 @@ dependencies = [ "serde", "serde_json", "storage", + "tempfile", "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 7af2e24..f8be323 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ version = "1.0.60" [workspace.dependencies.actix-web] default-features = false +features = ["macros"] version = "=4.1.0" [workspace.dependencies.clap] diff --git a/common/src/rpc_primitives/requests.rs b/common/src/rpc_primitives/requests.rs index 395badf..f26ea29 100644 --- a/common/src/rpc_primitives/requests.rs +++ b/common/src/rpc_primitives/requests.rs @@ -1,4 +1,5 @@ use crate::block::Block; +use crate::merkle_tree_public::TreeHashType; use crate::parse_request; use crate::transaction::Transaction; @@ -34,12 +35,18 @@ pub struct GetGenesisIdRequest {} #[derive(Serialize, Deserialize, Debug)] pub struct GetLastBlockRequest {} +#[derive(Serialize, Deserialize, Debug)] +pub struct GetAccountBalanceRequest { + pub address: String, +} + parse_request!(HelloRequest); parse_request!(RegisterAccountRequest); parse_request!(SendTxRequest); parse_request!(GetBlockDataRequest); parse_request!(GetGenesisIdRequest); parse_request!(GetLastBlockRequest); +parse_request!(GetAccountBalanceRequest); #[derive(Serialize, Deserialize, Debug)] pub struct HelloResponse { @@ -70,3 +77,8 @@ pub struct GetGenesisIdResponse { pub struct GetLastBlockResponse { pub last_block: u64, } + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetAccountBalanceResponse { + pub balance: u64, +} diff --git a/sequencer_rpc/Cargo.toml b/sequencer_rpc/Cargo.toml index d3b2e4f..5da9521 100644 --- a/sequencer_rpc/Cargo.toml +++ b/sequencer_rpc/Cargo.toml @@ -13,6 +13,7 @@ actix.workspace = true actix-cors.workspace = true futures.workspace = true hex.workspace = true +tempfile.workspace = true actix-web.workspace = true tokio.workspace = true diff --git a/sequencer_rpc/src/lib.rs b/sequencer_rpc/src/lib.rs index 1112dbd..4534cf6 100644 --- a/sequencer_rpc/src/lib.rs +++ b/sequencer_rpc/src/lib.rs @@ -42,3 +42,4 @@ pub fn rpc_error_responce_inverter(err: RpcError) -> RpcError { data: content, } } + diff --git a/sequencer_rpc/src/net_utils.rs b/sequencer_rpc/src/net_utils.rs index 351d09b..38e4e38 100644 --- a/sequencer_rpc/src/net_utils.rs +++ b/sequencer_rpc/src/net_utils.rs @@ -18,7 +18,7 @@ pub const SHUTDOWN_TIMEOUT_SECS: u64 = 10; pub const NETWORK: &str = "network"; -fn rpc_handler( +pub(crate) fn rpc_handler( message: web::Json, handler: web::Data, ) -> impl Future> { diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs index cab07bf..74a22ca 100644 --- a/sequencer_rpc/src/process.rs +++ b/sequencer_rpc/src/process.rs @@ -5,6 +5,7 @@ use common::rpc_primitives::{ errors::RpcError, message::{Message, Request}, parser::RpcRequest, + requests::{GetAccountBalanceRequest, GetAccountBalanceResponse}, }; use common::rpc_primitives::requests::{ @@ -21,6 +22,7 @@ pub const SEND_TX: &str = "send_tx"; pub const GET_BLOCK: &str = "get_block"; pub const GET_GENESIS: &str = "get_genesis"; pub const GET_LAST_BLOCK: &str = "get_last_block"; +pub const GET_ACCOUNT_BALANCE: &str = "get_account_balance"; pub const HELLO_FROM_SEQUENCER: &str = "HELLO_FROM_SEQUENCER"; @@ -134,6 +136,27 @@ impl JsonHandler { respond(helperstruct) } + /// Returns the balance of the account at the given address. + /// The address must be a valid hex string. If it's invalid or the account doesn't exist, + /// a balance of zero is returned. + async fn process_get_account_balance(&self, request: Request) -> Result { + let get_account_req = GetAccountBalanceRequest::parse(Some(request.params))?; + + let balance = { + let address = hex::decode(get_account_req.address).unwrap_or_default(); + let state = self.sequencer_state.lock().await; + state + .store + .acc_store + .get_account_balance(&address.try_into().unwrap_or_default()) + } + .unwrap_or(0); + + let helperstruct = GetAccountBalanceResponse { balance }; + + respond(helperstruct) + } + pub async fn process_request_internal(&self, request: Request) -> Result { match request.method.as_ref() { HELLO => self.process_temp_hello(request).await, @@ -142,7 +165,146 @@ impl JsonHandler { GET_BLOCK => self.process_get_block_data(request).await, GET_GENESIS => self.process_get_genesis(request).await, GET_LAST_BLOCK => self.process_get_last_block(request).await, + GET_ACCOUNT_BALANCE => self.process_get_account_balance(request).await, _ => Err(RpcErr(RpcError::method_not_found(request.method))), } } } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use crate::{rpc_handler, JsonHandler}; + use common::rpc_primitives::RpcPollingConfig; + use sequencer_core::{ + config::{AccountInitialData, SequencerConfig}, + SequencerCore, + }; + use serde_json::Value; + use tempfile::tempdir; + use tokio::sync::Mutex; + + fn sequencer_config_for_tests() -> SequencerConfig { + let tempdir = tempdir().unwrap(); + let home = tempdir.path().to_path_buf(); + let initial_accounts = vec![ + AccountInitialData { + addr: "cafe".repeat(16).to_string(), + balance: 100, + }, + AccountInitialData { + addr: "feca".repeat(16).to_string(), + balance: 200, + }, + ]; + + SequencerConfig { + home, + override_rust_log: Some("info".to_string()), + genesis_id: 1, + is_genesis_random: false, + max_num_tx_in_block: 10, + block_create_timeout_millis: 1000, + port: 8080, + initial_accounts, + } + } + + fn json_handler_for_tests() -> JsonHandler { + let config = sequencer_config_for_tests(); + let sequencer_core = Arc::new(Mutex::new(SequencerCore::start_from_config(config))); + + JsonHandler { + polling_config: RpcPollingConfig::default(), + sequencer_state: sequencer_core, + } + } + + async fn call_rpc_handler_with_json(handler: JsonHandler, request_json: Value) -> Value { + use actix_web::{test, web, App}; + + let app = test::init_service( + App::new() + .app_data(web::Data::new(handler)) + .route("/", web::post().to(rpc_handler)), + ) + .await; + + let req = test::TestRequest::post() + .uri("/") + .set_json(request_json) + .to_request(); + + let resp = test::call_service(&app, req).await; + let body = test::read_body(resp).await; + + serde_json::from_slice(&body).unwrap() + } + + #[actix_web::test] + async fn test_get_account_balance_for_non_existent_account() { + let json_handler = json_handler_for_tests(); + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_account_balance", + "params": { "address": "cofe".repeat(16) }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "id": 1, + "jsonrpc": "2.0", + "result": { + "balance": 0 + } + }); + + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn test_get_account_balance_for_invalid_address() { + let json_handler = json_handler_for_tests(); + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_account_balance", + "params": { "address": "not_a_valid_hex" }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "id": 1, + "jsonrpc": "2.0", + "result": { + "balance": 0 + } + }); + + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn test_get_account_balance_for_existing_account() { + let json_handler = json_handler_for_tests(); + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_account_balance", + "params": { "address": "cafe".repeat(16) }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "id": 1, + "jsonrpc": "2.0", + "result": { + "balance": 100 + } + }); + + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } +} From d1cd653ed59badb042c7054461fdee811a017678 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 22 Jul 2025 08:45:47 -0300 Subject: [PATCH 2/5] fmt --- common/src/rpc_primitives/requests.rs | 1 - node_core/src/chain_storage/mod.rs | 3 --- sequencer_rpc/src/lib.rs | 1 - 3 files changed, 5 deletions(-) diff --git a/common/src/rpc_primitives/requests.rs b/common/src/rpc_primitives/requests.rs index f26ea29..e17c786 100644 --- a/common/src/rpc_primitives/requests.rs +++ b/common/src/rpc_primitives/requests.rs @@ -1,5 +1,4 @@ use crate::block::Block; -use crate::merkle_tree_public::TreeHashType; use crate::parse_request; use crate::transaction::Transaction; diff --git a/node_core/src/chain_storage/mod.rs b/node_core/src/chain_storage/mod.rs index 81be855..3227512 100644 --- a/node_core/src/chain_storage/mod.rs +++ b/node_core/src/chain_storage/mod.rs @@ -300,8 +300,6 @@ mod tests { } fn create_dummy_transaction( - hash: TreeHashType, - // execution_input: Vec, nullifier_created_hashes: Vec<[u8; 32]>, utxo_commitments_spent_hashes: Vec<[u8; 32]>, utxo_commitments_created_hashes: Vec<[u8; 32]>, @@ -420,7 +418,6 @@ mod tests { .utxo_commitments_store .add_tx_multiple(vec![UTXOCommitment { hash: [3u8; 32] }]); store.pub_tx_store.add_tx(create_dummy_transaction( - [12; 32], vec![[9; 32]], vec![[7; 32]], vec![[8; 32]], diff --git a/sequencer_rpc/src/lib.rs b/sequencer_rpc/src/lib.rs index 4534cf6..1112dbd 100644 --- a/sequencer_rpc/src/lib.rs +++ b/sequencer_rpc/src/lib.rs @@ -42,4 +42,3 @@ pub fn rpc_error_responce_inverter(err: RpcError) -> RpcError { data: content, } } - From cab71a0e00241a850528ee4bc4e13c30ab8f03fe Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 22 Jul 2025 09:13:41 -0300 Subject: [PATCH 3/5] return parse error for invalid addresses --- sequencer_rpc/src/process.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs index 74a22ca..f8bc880 100644 --- a/sequencer_rpc/src/process.rs +++ b/sequencer_rpc/src/process.rs @@ -2,7 +2,7 @@ use actix_web::Error as HttpError; use serde_json::Value; use common::rpc_primitives::{ - errors::RpcError, + errors::{RpcError, RpcParseError}, message::{Message, Request}, parser::RpcRequest, requests::{GetAccountBalanceRequest, GetAccountBalanceResponse}, @@ -137,13 +137,13 @@ impl JsonHandler { } /// Returns the balance of the account at the given address. - /// The address must be a valid hex string. If it's invalid or the account doesn't exist, - /// a balance of zero is returned. + /// The address must be a valid hex string. If the account doesn't exist, a balance of zero is returned. async fn process_get_account_balance(&self, request: Request) -> Result { let get_account_req = GetAccountBalanceRequest::parse(Some(request.params))?; + let address = hex::decode(get_account_req.address) + .map_err(|_| RpcParseError("invalid address".to_string()))?; let balance = { - let address = hex::decode(get_account_req.address).unwrap_or_default(); let state = self.sequencer_state.lock().await; state .store @@ -248,7 +248,7 @@ mod tests { let request = serde_json::json!({ "jsonrpc": "2.0", "method": "get_account_balance", - "params": { "address": "cofe".repeat(16) }, + "params": { "address": "efac".repeat(16) }, "id": 1 }); let expected_response = serde_json::json!({ @@ -274,13 +274,21 @@ mod tests { "id": 1 }); let expected_response = serde_json::json!({ - "id": 1, "jsonrpc": "2.0", - "result": { - "balance": 0 + "id": 1, + "error": { + "code": -32700, + "message": "Parse error", + "name": "REQUEST_VALIDATION_ERROR", + "data": "invalid address", + "cause": { + "name": "PARSE_ERROR", + "info": { + "error_message": "invalid address" + } + } } }); - let response = call_rpc_handler_with_json(json_handler, request).await; assert_eq!(response, expected_response); From 1afb77e63ef6f2c18534a03affd98bcad5e657aa Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 22 Jul 2025 10:37:04 -0300 Subject: [PATCH 4/5] use correct rpc error --- sequencer_rpc/src/process.rs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs index f8bc880..9b921c5 100644 --- a/sequencer_rpc/src/process.rs +++ b/sequencer_rpc/src/process.rs @@ -2,7 +2,7 @@ use actix_web::Error as HttpError; use serde_json::Value; use common::rpc_primitives::{ - errors::{RpcError, RpcParseError}, + errors::RpcError, message::{Message, Request}, parser::RpcRequest, requests::{GetAccountBalanceRequest, GetAccountBalanceResponse}, @@ -141,7 +141,7 @@ impl JsonHandler { async fn process_get_account_balance(&self, request: Request) -> Result { let get_account_req = GetAccountBalanceRequest::parse(Some(request.params))?; let address = hex::decode(get_account_req.address) - .map_err(|_| RpcParseError("invalid address".to_string()))?; + .map_err(|_| RpcError::invalid_params("invalid address".to_string()))?; let balance = { let state = self.sequencer_state.lock().await; @@ -277,16 +277,9 @@ mod tests { "jsonrpc": "2.0", "id": 1, "error": { - "code": -32700, - "message": "Parse error", - "name": "REQUEST_VALIDATION_ERROR", - "data": "invalid address", - "cause": { - "name": "PARSE_ERROR", - "info": { - "error_message": "invalid address" - } - } + "code": -32602, + "message": "Invalid params", + "data": "invalid address" } }); let response = call_rpc_handler_with_json(json_handler, request).await; From bf139aa634c610d78a29266f1acee4dce89ac0af Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 22 Jul 2025 13:52:11 -0300 Subject: [PATCH 5/5] improve error messages --- sequencer_rpc/src/process.rs | 42 +++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs index 9b921c5..f141450 100644 --- a/sequencer_rpc/src/process.rs +++ b/sequencer_rpc/src/process.rs @@ -137,18 +137,17 @@ impl JsonHandler { } /// Returns the balance of the account at the given address. - /// The address must be a valid hex string. If the account doesn't exist, a balance of zero is returned. + /// The address must be a valid hex string of the correct length. async fn process_get_account_balance(&self, request: Request) -> Result { let get_account_req = GetAccountBalanceRequest::parse(Some(request.params))?; - let address = hex::decode(get_account_req.address) - .map_err(|_| RpcError::invalid_params("invalid address".to_string()))?; - + let address_bytes = hex::decode(get_account_req.address) + .map_err(|_| RpcError::invalid_params("invalid hex".to_string()))?; + let address = address_bytes + .try_into() + .map_err(|_| RpcError::invalid_params("invalid length".to_string()))?; let balance = { let state = self.sequencer_state.lock().await; - state - .store - .acc_store - .get_account_balance(&address.try_into().unwrap_or_default()) + state.store.acc_store.get_account_balance(&address) } .unwrap_or(0); @@ -265,7 +264,7 @@ mod tests { } #[actix_web::test] - async fn test_get_account_balance_for_invalid_address() { + async fn test_get_account_balance_for_invalid_hex() { let json_handler = json_handler_for_tests(); let request = serde_json::json!({ "jsonrpc": "2.0", @@ -279,7 +278,30 @@ mod tests { "error": { "code": -32602, "message": "Invalid params", - "data": "invalid address" + "data": "invalid hex" + } + }); + let response = call_rpc_handler_with_json(json_handler, request).await; + + assert_eq!(response, expected_response); + } + + #[actix_web::test] + async fn test_get_account_balance_for_invalid_length() { + let json_handler = json_handler_for_tests(); + let request = serde_json::json!({ + "jsonrpc": "2.0", + "method": "get_account_balance", + "params": { "address": "cafecafe" }, + "id": 1 + }); + let expected_response = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32602, + "message": "Invalid params", + "data": "invalid length" } }); let response = call_rpc_handler_with_json(json_handler, request).await;