From f3aaacb6ca5b6aac33f31647fd92c5dedaa0bec2 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Tue, 22 Jul 2025 08:40:04 -0300 Subject: [PATCH] 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); + } +}