From 914cbfb9dc8361d7230f385d03c4065060115440 Mon Sep 17 00:00:00 2001 From: Oleksandr Pravdyvyi Date: Thu, 21 Aug 2025 15:58:31 +0300 Subject: [PATCH] fix: complete cli intefaces --- common/src/rpc_primitives/requests.rs | 12 +++++ common/src/sequencer_client/json.rs | 2 +- common/src/sequencer_client/mod.rs | 59 +++++++++++++++------- key_protocol/src/key_protocol_core/mod.rs | 6 +++ sequencer_rpc/src/process.rs | 43 ++++++++++++++-- wallet/Cargo.toml | 1 + wallet/src/chain_storage/mod.rs | 5 +- wallet/src/config.rs | 10 +++- wallet/src/lib.rs | 60 +++++++++++++++++++++-- wallet/src/poller.rs | 55 +++++++++++++++++++++ 10 files changed, 223 insertions(+), 30 deletions(-) create mode 100644 wallet/src/poller.rs diff --git a/common/src/rpc_primitives/requests.rs b/common/src/rpc_primitives/requests.rs index 2e67f16..674c077 100644 --- a/common/src/rpc_primitives/requests.rs +++ b/common/src/rpc_primitives/requests.rs @@ -43,6 +43,11 @@ pub struct GetTransactionByHashRequest { pub hash: String, } +#[derive(Serialize, Deserialize, Debug)] +pub struct GetAccountsNoncesRequest { + pub addresses: Vec, +} + parse_request!(HelloRequest); parse_request!(RegisterAccountRequest); parse_request!(SendTxRequest); @@ -52,6 +57,7 @@ parse_request!(GetLastBlockRequest); parse_request!(GetInitialTestnetAccountsRequest); parse_request!(GetAccountBalanceRequest); parse_request!(GetTransactionByHashRequest); +parse_request!(GetAccountsNoncesRequest); #[derive(Serialize, Deserialize, Debug)] pub struct HelloResponse { @@ -66,6 +72,7 @@ pub struct RegisterAccountResponse { #[derive(Serialize, Deserialize, Debug)] pub struct SendTxResponse { pub status: String, + pub tx_hash: String, } #[derive(Serialize, Deserialize, Debug)] @@ -88,6 +95,11 @@ pub struct GetAccountBalanceResponse { pub balance: u128, } +#[derive(Serialize, Deserialize, Debug)] +pub struct GetAccountsNoncesResponse { + pub nonces: Vec, +} + #[derive(Serialize, Deserialize, Debug)] pub struct GetTransactionByHashResponse { pub transaction: Option, diff --git a/common/src/sequencer_client/json.rs b/common/src/sequencer_client/json.rs index 71b1719..59895bf 100644 --- a/common/src/sequencer_client/json.rs +++ b/common/src/sequencer_client/json.rs @@ -12,7 +12,7 @@ pub struct SendTxRequest { #[derive(Serialize, Deserialize, Debug)] pub struct SendTxResponse { pub status: String, - pub additional_data: Option, + pub tx_hash: String, } //General diff --git a/common/src/sequencer_client/mod.rs b/common/src/sequencer_client/mod.rs index 196713f..ad66d82 100644 --- a/common/src/sequencer_client/mod.rs +++ b/common/src/sequencer_client/mod.rs @@ -7,7 +7,10 @@ use json::{SendTxRequest, SendTxResponse, SequencerRpcRequest, SequencerRpcRespo use reqwest::Client; use serde_json::Value; -use crate::rpc_primitives::requests::{GetTransactionByHashRequest, GetTransactionByHashResponse}; +use crate::rpc_primitives::requests::{ + GetAccountsNoncesRequest, GetAccountsNoncesResponse, GetTransactionByHashRequest, + GetTransactionByHashResponse, +}; use crate::sequencer_client::json::AccountInitialData; use crate::{SequencerClientError, SequencerRpcError}; @@ -87,6 +90,42 @@ impl SequencerClient { Ok(resp_deser) } + ///Get accounts nonces for `addresses`. `addresses` must be a list of valid hex-strings for 32 bytes. + pub async fn get_accounts_nonces( + &self, + addresses: Vec, + ) -> Result { + let block_req = GetAccountsNoncesRequest { addresses }; + + let req = serde_json::to_value(block_req)?; + + let resp = self + .call_method_with_payload("get_accounts_nonces", req) + .await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + + ///Get transaction details for `hash`. + pub async fn get_transaction_by_hash( + &self, + hash: String, + ) -> Result { + let block_req = GetTransactionByHashRequest { hash }; + + let req = serde_json::to_value(block_req)?; + + let resp = self + .call_method_with_payload("get_transaction_by_hash", req) + .await?; + + let resp_deser = serde_json::from_value(resp)?; + + Ok(resp_deser) + } + ///Send transaction to sequencer pub async fn send_tx( &self, @@ -138,22 +177,4 @@ impl SequencerClient { Ok(resp_deser) } - - ///Get tx data for `tx_hash` from sequencer - pub async fn get_transaction_by_hash( - &self, - tx_hash: String, - ) -> Result { - let block_req = GetTransactionByHashRequest { hash: tx_hash }; - - let req = serde_json::to_value(block_req)?; - - let resp = self - .call_method_with_payload("get_transaction_by_hash", req) - .await?; - - let resp_deser = serde_json::from_value(resp)?; - - Ok(resp_deser) - } } diff --git a/key_protocol/src/key_protocol_core/mod.rs b/key_protocol/src/key_protocol_core/mod.rs index 5362095..facc5a2 100644 --- a/key_protocol/src/key_protocol_core/mod.rs +++ b/key_protocol/src/key_protocol_core/mod.rs @@ -116,6 +116,12 @@ impl NSSAUserData { .and_modify(|(_, acc)| acc.balance = new_balance); } + pub fn increment_account_nonce(&mut self, address: nssa::Address) { + self.accounts + .entry(address) + .and_modify(|(_, acc)| acc.nonce += 1); + } + //ToDo: Part of a private keys update // pub fn make_tag(&self) -> Tag { // self.address.value()[0] diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs index 2d5a29a..83f1e1c 100644 --- a/sequencer_rpc/src/process.rs +++ b/sequencer_rpc/src/process.rs @@ -12,7 +12,8 @@ use common::{ message::{Message, Request}, parser::RpcRequest, requests::{ - GetAccountBalanceRequest, GetAccountBalanceResponse, GetInitialTestnetAccountsRequest, + GetAccountBalanceRequest, GetAccountBalanceResponse, GetAccountsNoncesRequest, + GetAccountsNoncesResponse, GetInitialTestnetAccountsRequest, GetTransactionByHashRequest, GetTransactionByHashResponse, }, }, @@ -33,10 +34,11 @@ 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 GET_TRANSACTION_BY_HASH: &str = "get_transaction_by_hash"; +pub const GET_ACCOUNTS_NONCES: &str = "get_accounts_nonces"; pub const HELLO_FROM_SEQUENCER: &str = "HELLO_FROM_SEQUENCER"; -pub const SUCCESS: &str = "Success"; +pub const TRANSACTION_SUBMITTED: &str = "Transaction submitted"; pub const GET_INITIAL_TESTNET_ACCOUNTS: &str = "get_initial_testnet_accounts"; @@ -72,6 +74,7 @@ impl JsonHandler { let send_tx_req = SendTxRequest::parse(Some(request.params))?; let tx = nssa::PublicTransaction::from_bytes(&send_tx_req.transaction) .map_err(|e| RpcError::serialization_error(&e.to_string()))?; + let tx_hash = hex::encode(tx.hash()); { let mut state = self.sequencer_state.lock().await; @@ -80,7 +83,8 @@ impl JsonHandler { } let helperstruct = SendTxResponse { - status: SUCCESS.to_string(), + status: TRANSACTION_SUBMITTED.to_string(), + tx_hash, }; respond(helperstruct) @@ -171,6 +175,38 @@ impl JsonHandler { respond(helperstruct) } + /// Returns the nonces of the accounts at the given addresses. + /// Each address must be a valid hex string of the correct length. + async fn process_get_accounts_nonces(&self, request: Request) -> Result { + let get_account_nonces_req = GetAccountsNoncesRequest::parse(Some(request.params))?; + let mut addresses = vec![]; + for address_raw in get_account_nonces_req.addresses { + let address_bytes = hex::decode(address_raw) + .map_err(|_| RpcError::invalid_params("invalid hex".to_string()))?; + + let address = nssa::Address::new( + address_bytes + .try_into() + .map_err(|_| RpcError::invalid_params("invalid length".to_string()))?, + ); + + addresses.push(address); + } + + let nonces = { + let state = self.sequencer_state.lock().await; + + addresses + .into_iter() + .map(|addr| state.store.state.get_account_by_address(&addr).nonce) + .collect() + }; + + let helperstruct = GetAccountsNoncesResponse { nonces }; + + respond(helperstruct) + } + /// Returns the transaction corresponding to the given hash, if it exists in the blockchain. /// The hash must be a valid hex string of the correct length. async fn process_get_transaction_by_hash(&self, request: Request) -> Result { @@ -205,6 +241,7 @@ impl JsonHandler { GET_LAST_BLOCK => self.process_get_last_block(request).await, GET_INITIAL_TESTNET_ACCOUNTS => self.get_initial_testnet_accounts(request).await, GET_ACCOUNT_BALANCE => self.process_get_account_balance(request).await, + GET_ACCOUNTS_NONCES => self.process_get_accounts_nonces(request).await, GET_TRANSACTION_BY_HASH => self.process_get_transaction_by_hash(request).await, _ => Err(RpcErr(RpcError::method_not_found(request.method))), } diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 4a67791..c3ddd3b 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -23,6 +23,7 @@ hex.workspace = true actix-rt.workspace = true clap.workspace = true nssa-core = { path = "../nssa/core" } +base64.workspace = true [dependencies.key_protocol] path = "../key_protocol" diff --git a/wallet/src/chain_storage/mod.rs b/wallet/src/chain_storage/mod.rs index daa51bf..16195f5 100644 --- a/wallet/src/chain_storage/mod.rs +++ b/wallet/src/chain_storage/mod.rs @@ -78,7 +78,10 @@ mod tests { home, override_rust_log: None, sequencer_addr: "http://127.0.0.1".to_string(), - seq_poll_timeout_secs: 1, + seq_poll_timeout_millis: 12000, + seq_poll_max_blocks: 5, + seq_poll_max_retries: 10, + seq_poll_retry_delay_millis: 500, initial_accounts: create_initial_accounts(), } } diff --git a/wallet/src/config.rs b/wallet/src/config.rs index 21bd912..e6737f9 100644 --- a/wallet/src/config.rs +++ b/wallet/src/config.rs @@ -34,8 +34,14 @@ pub struct WalletConfig { pub override_rust_log: Option, ///Sequencer URL pub sequencer_addr: String, - ///Sequencer polling duration for new blocks in seconds - pub seq_poll_timeout_secs: u64, + ///Sequencer polling duration for new blocks in milliseconds + pub seq_poll_timeout_millis: u64, + ///Sequencer polling max number of blocks + pub seq_poll_max_blocks: usize, + ///Sequencer polling max number error retries + pub seq_poll_max_retries: u64, + ///Sequencer polling error retry delay in milliseconds + pub seq_poll_retry_delay_millis: u64, ///Initial accounts for wallet pub initial_accounts: Vec, } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index c6d0704..bc34e20 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -1,5 +1,6 @@ use std::{fs::File, io::Write, path::PathBuf, sync::Arc}; +use base64::Engine; use common::{ sequencer_client::{json::SendTxResponse, SequencerClient}, ExecutionFailureKind, @@ -13,9 +14,12 @@ use nssa::Address; use clap::{Parser, Subcommand}; -use crate::helperfunctions::{ - fetch_config, fetch_persistent_accounts, get_home, produce_account_addr_from_hex, - produce_data_for_storage, +use crate::{ + helperfunctions::{ + fetch_config, fetch_persistent_accounts, get_home, produce_account_addr_from_hex, + produce_data_for_storage, + }, + poller::TxPoller, }; pub const HOME_DIR_ENV_VAR: &str = "NSSA_WALLET_HOME_DIR"; @@ -24,15 +28,24 @@ pub const BLOCK_GEN_DELAY_SECS: u64 = 20; pub mod chain_storage; pub mod config; pub mod helperfunctions; +pub mod poller; pub struct WalletCore { pub storage: WalletChainStore, + pub poller: TxPoller, pub sequencer_client: Arc, } impl WalletCore { pub async fn start_from_config_update_chain(config: WalletConfig) -> Result { let client = Arc::new(SequencerClient::new(config.sequencer_addr.clone())?); + let tx_poller = TxPoller { + polling_delay_millis: config.seq_poll_timeout_millis, + polling_max_blocks_to_query: config.seq_poll_max_blocks, + polling_max_error_attempts: config.seq_poll_max_retries, + polling_error_delay_millis: config.seq_poll_retry_delay_millis, + client: client.clone(), + }; let mut storage = WalletChainStore::new(config)?; @@ -48,6 +61,7 @@ impl WalletCore { Ok(Self { storage, + poller: tx_poller, sequencer_client: client.clone(), }) } @@ -110,6 +124,38 @@ impl WalletCore { Err(ExecutionFailureKind::AmountMismatchError) } } + + pub async fn poll_public_native_token_transfer( + &self, + hash: String, + ) -> Result { + let transaction_encoded = self.poller.poll_tx(hash).await?; + let tx_base64_decode = + base64::engine::general_purpose::STANDARD.decode(transaction_encoded)?; + let pub_tx = nssa::PublicTransaction::from_bytes(&tx_base64_decode)?; + + Ok(pub_tx) + } + + pub fn execute_native_token_transfer( + &mut self, + from: Address, + to: Address, + balance_to_move: u128, + ) { + self.storage.user_data.increment_account_nonce(from); + self.storage.user_data.increment_account_nonce(to); + + let from_bal = self.storage.user_data.get_account_balance(&from); + let to_bal = self.storage.user_data.get_account_balance(&to); + + self.storage + .user_data + .update_account_balance(from, from_bal - balance_to_move); + self.storage + .user_data + .update_account_balance(to, to_bal + balance_to_move); + } } ///Represents CLI command for a wallet @@ -169,7 +215,13 @@ pub async fn execute_subcommand(command: Command) -> Result<()> { info!("Results of tx send is {res:#?}"); - //ToDo: Insert transaction polling logic here + let transfer_tx = wallet_core + .poll_public_native_token_transfer(res.tx_hash) + .await?; + + info!("Transaction data is {transfer_tx:#?}"); + + wallet_core.execute_native_token_transfer(from, to, amount); } Command::RegisterAccount {} => { let addr = wallet_core.create_new_account(); diff --git a/wallet/src/poller.rs b/wallet/src/poller.rs new file mode 100644 index 0000000..ae0a793 --- /dev/null +++ b/wallet/src/poller.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use anyhow::Result; +use common::sequencer_client::SequencerClient; +use log::{info, warn}; + +#[derive(Clone)] +pub struct TxPoller { + pub polling_max_blocks_to_query: usize, + pub polling_max_error_attempts: u64, + pub polling_error_delay_millis: u64, + pub polling_delay_millis: u64, + pub client: Arc, +} + +impl TxPoller { + pub async fn poll_tx(&self, tx_hash: String) -> Result { + let max_blocks_to_query = self.polling_max_blocks_to_query; + + info!("Starting poll for transaction {tx_hash:#?}"); + for poll_id in 1..max_blocks_to_query { + info!("Poll {poll_id}"); + + let mut try_error_counter = 0; + + let tx_obj = loop { + let tx_obj = self + .client + .get_transaction_by_hash(tx_hash.clone()) + .await + .inspect_err(|err| { + warn!("Failed to get transaction by hash {tx_hash:#?} with error: {err:#?}") + }); + + if let Ok(tx_obj) = tx_obj { + break tx_obj; + } else { + try_error_counter += 1; + } + + if try_error_counter > self.polling_max_error_attempts { + anyhow::bail!("Number of retries exceeded"); + } + }; + + if tx_obj.transaction.is_some() { + return Ok(tx_obj.transaction.unwrap()); + } + + tokio::time::sleep(std::time::Duration::from_millis(self.polling_delay_millis)).await; + } + + anyhow::bail!("Transaction not found in preconfigured amount of blocks"); + } +}