diff --git a/Cargo.lock b/Cargo.lock index 165f7e23..931088a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,7 +507,7 @@ dependencies = [ [[package]] name = "casper-binary-port" version = "1.0.0" -source = "git+https://github.com/casper-network/casper-node?branch=feat-2.0#de6313b423c38e95e7551f232103070ce8e87632" +source = "git+https://github.com/casper-network/casper-node?branch=feat-2.0#b2b2fba996218845ff99467f8d97a95d3e5a621c" dependencies = [ "bincode", "casper-types", @@ -703,7 +703,7 @@ dependencies = [ [[package]] name = "casper-types" version = "5.0.0" -source = "git+https://github.com/casper-network/casper-node?branch=feat-2.0#de6313b423c38e95e7551f232103070ce8e87632" +source = "git+https://github.com/casper-network/casper-node?branch=feat-2.0#b2b2fba996218845ff99467f8d97a95d3e5a621c" dependencies = [ "base16", "base64 0.13.1", diff --git a/Cargo.toml b/Cargo.toml index 7c2a0bc3..1a1e8d81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ anyhow = "1" async-stream = "0.3.4" async-trait = "0.1.77" casper-types = { git = "https://github.com/casper-network/casper-node", branch = "feat-2.0" } +casper-binary-port = { git = "https://github.com/casper-network/casper-node", branch = "feat-2.0" } casper-event-sidecar = { path = "./event_sidecar", version = "1.0.0" } casper-event-types = { path = "./types", version = "1.0.0" } casper-rpc-sidecar = { path = "./rpc_sidecar", version = "1.0.0" } diff --git a/resources/test/rpc_schema.json b/resources/test/rpc_schema.json index 55aa9ee0..b3ac2b60 100644 --- a/resources/test/rpc_schema.json +++ b/resources/test/rpc_schema.json @@ -1157,7 +1157,7 @@ "type": "string" }, "balance": { - "description": "The balance represented in motes.", + "description": "The available balance in motes (total balance - sum of all active holds).", "$ref": "#/components/schemas/U512" } } @@ -1190,6 +1190,111 @@ } ] }, + { + "name": "query_balance_details", + "summary": "query for full balance information using a purse identifier and a state identifier", + "params": [ + { + "name": "purse_identifier", + "schema": { + "description": "The identifier to obtain the purse corresponding to balance query.", + "$ref": "#/components/schemas/PurseIdentifier" + }, + "required": true + }, + { + "name": "state_identifier", + "schema": { + "description": "The identifier for the state used for the query, if none is passed, the latest block will be used.", + "anyOf": [ + { + "$ref": "#/components/schemas/BalanceStateIdentifier" + }, + { + "type": "null" + } + ] + }, + "required": false + } + ], + "result": { + "name": "query_balance_details_result", + "schema": { + "description": "Result for \"query_balance\" RPC response.", + "type": "object", + "required": [ + "api_version", + "available_balance", + "holds", + "total_balance", + "total_balance_proof" + ], + "properties": { + "api_version": { + "description": "The RPC API version.", + "type": "string" + }, + "total_balance": { + "description": "The purses total balance, not considering holds.", + "$ref": "#/components/schemas/U512" + }, + "available_balance": { + "description": "The available balance in motes (total balance - sum of all active holds).", + "$ref": "#/components/schemas/U512" + }, + "total_balance_proof": { + "description": "A proof that the given value is present in the Merkle trie.", + "type": "string" + }, + "holds": { + "description": "Holds active at the requested point in time.", + "type": "array", + "items": { + "$ref": "#/components/schemas/BalanceHoldWithProof" + } + } + } + } + }, + "examples": [ + { + "name": "query_balance_details_example", + "params": [ + { + "name": "state_identifier", + "value": { + "block": { + "Hash": "0707070707070707070707070707070707070707070707070707070707070707" + } + } + }, + { + "name": "purse_identifier", + "value": { + "main_purse_under_account_hash": "account-hash-0909090909090909090909090909090909090909090909090909090909090909" + } + } + ], + "result": { + "name": "query_balance_details_example_result", + "value": { + "api_version": "2.0.0", + "total_balance": "123456", + "available_balance": "123456", + "total_balance_proof": "01000000006ef2e0949ac76e55812421f755abe129b6244fe7168b77f47a72536147614625016ef2e0949ac76e55812421f755abe129b6244fe7168b77f47a72536147614625000000003529cde5c621f857f75f3810611eb4af3f998caaa9d4a3413cf799f99c67db0307010000006ef2e0949ac76e55812421f755abe129b6244fe7168b77f47a7253614761462501010102000000006e06000000000074769d28aac597a36a03a932d4b43e4f10bf0403ee5c41dd035102553f5773631200b9e173e8f05361b681513c14e25e3138639eb03232581db7557c9e8dbbc83ce94500226a9a7fe4f2b7b88d5103a4fc7400f02bf89c860c9ccdd56951a2afe9be0e0267006d820fb5676eb2960e15722f7725f3f8f41030078f8b2e44bf0dc03f71b176d6e800dc5ae9805068c5be6da1a90b2528ee85db0609cc0fb4bd60bbd559f497a98b67f500e1e3e846592f4918234647fca39830b7e1e6ad6f5b7a99b39af823d82ba1873d000003000000010186ff500f287e9b53f823ae1582b1fa429dfede28015125fd233a31ca04d5012002015cc42669a55467a1fdf49750772bfc1aed59b9b085558eb81510e9b015a7c83b0301e3cf4a34b1db6bfa58808b686cb8fe21ebe0c1bcbcee522649d2b135fe510fe3", + "holds": [ + { + "time": 0, + "amount": "123456", + "proof": "01000000006ef2e0949ac76e55812421f755abe129b6244fe7168b77f47a72536147614625016ef2e0949ac76e55812421f755abe129b6244fe7168b77f47a72536147614625000000003529cde5c621f857f75f3810611eb4af3f998caaa9d4a3413cf799f99c67db0307010000006ef2e0949ac76e55812421f755abe129b6244fe7168b77f47a7253614761462501010102000000006e06000000000074769d28aac597a36a03a932d4b43e4f10bf0403ee5c41dd035102553f5773631200b9e173e8f05361b681513c14e25e3138639eb03232581db7557c9e8dbbc83ce94500226a9a7fe4f2b7b88d5103a4fc7400f02bf89c860c9ccdd56951a2afe9be0e0267006d820fb5676eb2960e15722f7725f3f8f41030078f8b2e44bf0dc03f71b176d6e800dc5ae9805068c5be6da1a90b2528ee85db0609cc0fb4bd60bbd559f497a98b67f500e1e3e846592f4918234647fca39830b7e1e6ad6f5b7a99b39af823d82ba1873d000003000000010186ff500f287e9b53f823ae1582b1fa429dfede28015125fd233a31ca04d5012002015cc42669a55467a1fdf49750772bfc1aed59b9b085558eb81510e9b015a7c83b0301e3cf4a34b1db6bfa58808b686cb8fe21ebe0c1bcbcee522649d2b135fe510fe3" + } + ] + } + } + } + ] + }, { "name": "info_get_peers", "summary": "returns a list of peers connected to the node", @@ -1927,7 +2032,7 @@ "type": "string" }, "balance_value": { - "description": "The balance value.", + "description": "The available balance in motes (total balance - sum of all active holds). The active holds are determined by the current timestamp and not the state root hash. If you need to account for holds at a specific time, you should use the `query_balance_details` RPC.", "$ref": "#/components/schemas/U512" }, "merkle_proof": { @@ -7085,6 +7190,19 @@ }, "additionalProperties": false }, + { + "description": "The main purse of the account identified by this entity address.", + "type": "object", + "required": [ + "main_purse_under_entity_addr" + ], + "properties": { + "main_purse_under_entity_addr": { + "$ref": "#/components/schemas/EntityAddr" + } + }, + "additionalProperties": false + }, { "description": "The purse identified by this URef.", "type": "object", @@ -7100,6 +7218,90 @@ } ] }, + "BalanceStateIdentifier": { + "description": "Identifier of a balance.", + "oneOf": [ + { + "description": "The balance at a specific block.", + "type": "object", + "required": [ + "block" + ], + "properties": { + "block": { + "$ref": "#/components/schemas/BlockIdentifier" + } + }, + "additionalProperties": false + }, + { + "description": "The balance at a specific state root.", + "type": "object", + "required": [ + "state_root" + ], + "properties": { + "state_root": { + "type": "object", + "required": [ + "state_root_hash", + "timestamp" + ], + "properties": { + "state_root_hash": { + "description": "The state root hash.", + "allOf": [ + { + "$ref": "#/components/schemas/Digest" + } + ] + }, + "timestamp": { + "description": "Timestamp for holds lookup.", + "allOf": [ + { + "$ref": "#/components/schemas/Timestamp" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "BalanceHoldWithProof": { + "type": "object", + "required": [ + "amount", + "proof", + "time" + ], + "properties": { + "time": { + "description": "The block time at which the hold was created.", + "allOf": [ + { + "$ref": "#/components/schemas/BlockTime" + } + ] + }, + "amount": { + "description": "The amount in the hold.", + "allOf": [ + { + "$ref": "#/components/schemas/U512" + } + ] + }, + "proof": { + "description": "A proof that the given value is present in the Merkle trie.", + "type": "string" + } + } + }, "Peers": { "description": "Map of peer IDs to network addresses.", "type": "array", diff --git a/rpc_sidecar/Cargo.toml b/rpc_sidecar/Cargo.toml index ffd1e3ad..01cbeb60 100644 --- a/rpc_sidecar/Cargo.toml +++ b/rpc_sidecar/Cargo.toml @@ -19,7 +19,7 @@ bincode = "1" bytes = "1.5.0" casper-json-rpc = { version = "1.0.0", path = "../json_rpc" } casper-types = { workspace = true, features = ["datasize", "json-schema", "std"] } -casper-binary-port = { git = "https://github.com/casper-network/casper-node.git", branch = "feat-2.0" } +casper-binary-port.workspace = true datasize = { workspace = true, features = ["detailed", "fake_clock-types"] } futures = { workspace = true } http = "0.2.1" @@ -45,7 +45,7 @@ warp = { version = "0.3.6", features = ["compression"] } [dev-dependencies] assert-json-diff = "2" casper-types = { workspace = true, features = ["datasize", "json-schema", "std", "testing"] } -casper-binary-port = { git = "https://github.com/casper-network/casper-node.git", branch = "feat-2.0", features = ["testing"] } +casper-binary-port = { workspace = true, features = ["testing"] } pretty_assertions = "0.7.2" regex = "1" tempfile = "3" diff --git a/rpc_sidecar/src/http_server.rs b/rpc_sidecar/src/http_server.rs index 4d369de0..a2d2af21 100644 --- a/rpc_sidecar/src/http_server.rs +++ b/rpc_sidecar/src/http_server.rs @@ -7,7 +7,7 @@ use casper_json_rpc::{CorsOrigin, RequestHandlersBuilder}; use crate::{ rpcs::{ info::{GetPeers, GetStatus, GetTransaction}, - state::GetAddressableEntity, + state::{GetAddressableEntity, QueryBalanceDetails}, }, NodeClient, }; @@ -62,7 +62,8 @@ pub async fn run( ListRpcs::register_as_handler(node.clone(), &mut handlers); GetDictionaryItem::register_as_handler(node.clone(), &mut handlers); GetChainspec::register_as_handler(node.clone(), &mut handlers); - QueryBalance::register_as_handler(node, &mut handlers); + QueryBalance::register_as_handler(node.clone(), &mut handlers); + QueryBalanceDetails::register_as_handler(node, &mut handlers); let handlers = handlers.build(); match cors_origin.as_str() { diff --git a/rpc_sidecar/src/node_client.rs b/rpc_sidecar/src/node_client.rs index 1279055b..4c2d0722 100644 --- a/rpc_sidecar/src/node_client.rs +++ b/rpc_sidecar/src/node_client.rs @@ -12,16 +12,17 @@ use std::{ }; use casper_binary_port::{ - BinaryRequest, BinaryRequestHeader, BinaryResponse, BinaryResponseAndRequest, + BalanceResponse, BinaryRequest, BinaryRequestHeader, BinaryResponse, BinaryResponseAndRequest, ConsensusValidatorChanges, DictionaryItemIdentifier, DictionaryQueryResult, ErrorCode, GetRequest, GetTrieFullResult, GlobalStateQueryResult, GlobalStateRequest, InformationRequest, - NodeStatus, PayloadEntity, RecordId, SpeculativeExecutionResult, TransactionWithExecutionInfo, + NodeStatus, PayloadEntity, PurseIdentifier, RecordId, SpeculativeExecutionResult, + TransactionWithExecutionInfo, }; use casper_types::{ bytesrepr::{self, FromBytes, ToBytes}, AvailableBlockRange, BlockHash, BlockHeader, BlockIdentifier, ChainspecRawBytes, Digest, GlobalStateIdentifier, Key, KeyTag, Peers, ProtocolVersion, SignedBlock, StoredValue, - Transaction, TransactionHash, Transfer, + Timestamp, Transaction, TransactionHash, Transfer, }; use juliet::{ io::IoCoreBuilder, @@ -75,7 +76,7 @@ pub trait NodeClient: Send + Sync { path, }; let resp = self - .send_request(BinaryRequest::Get(GetRequest::State(req))) + .send_request(BinaryRequest::Get(GetRequest::State(Box::new(req)))) .await?; parse_response::(&resp.into()) } @@ -90,15 +91,47 @@ pub trait NodeClient: Send + Sync { key_tag, }; let resp = self - .send_request(BinaryRequest::Get(GetRequest::State(get))) + .send_request(BinaryRequest::Get(GetRequest::State(Box::new(get)))) .await?; parse_response::>(&resp.into())?.ok_or(Error::EmptyEnvelope) } + async fn get_balance_by_state_root( + &self, + state_identifier: Option, + purse_identifier: PurseIdentifier, + timestamp: Timestamp, + ) -> Result { + let get = GlobalStateRequest::BalanceByStateRoot { + state_identifier, + purse_identifier, + timestamp, + }; + let resp = self + .send_request(BinaryRequest::Get(GetRequest::State(Box::new(get)))) + .await?; + parse_response::(&resp.into())?.ok_or(Error::EmptyEnvelope) + } + + async fn get_balance_by_block( + &self, + block_identifier: Option, + purse_identifier: PurseIdentifier, + ) -> Result { + let get = GlobalStateRequest::BalanceByBlock { + block_identifier, + purse_identifier, + }; + let resp = self + .send_request(BinaryRequest::Get(GetRequest::State(Box::new(get)))) + .await?; + parse_response::(&resp.into())?.ok_or(Error::EmptyEnvelope) + } + async fn read_trie_bytes(&self, trie_key: Digest) -> Result>, Error> { let req = GlobalStateRequest::Trie { trie_key }; let resp = self - .send_request(BinaryRequest::Get(GetRequest::State(req))) + .send_request(BinaryRequest::Get(GetRequest::State(Box::new(req)))) .await?; let res = parse_response::(&resp.into())?.ok_or(Error::EmptyEnvelope)?; Ok(res.into_inner().map(>::from)) @@ -114,7 +147,7 @@ pub trait NodeClient: Send + Sync { identifier, }; let resp = self - .send_request(BinaryRequest::Get(GetRequest::State(get))) + .send_request(BinaryRequest::Get(GetRequest::State(Box::new(get)))) .await?; parse_response::(&resp.into()) } diff --git a/rpc_sidecar/src/rpcs/chain.rs b/rpc_sidecar/src/rpcs/chain.rs index 43aaa1f9..38290a26 100644 --- a/rpc_sidecar/src/rpcs/chain.rs +++ b/rpc_sidecar/src/rpcs/chain.rs @@ -760,16 +760,26 @@ mod tests { &[], )) } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::EraSummary, - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - GlobalStateQueryResult::new(StoredValue::EraInfo(EraInfo::new()), vec![]), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )), + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::Item { + base_key: Key::EraSummary, + .. + } + ) => + { + Ok(BinaryResponseAndRequest::new( + BinaryResponse::from_value( + GlobalStateQueryResult::new( + StoredValue::EraInfo(EraInfo::new()), + vec![], + ), + SUPPORTED_PROTOCOL_VERSION, + ), + &[], + )) + } req => unimplemented!("unexpected request: {:?}", req), } } diff --git a/rpc_sidecar/src/rpcs/common.rs b/rpc_sidecar/src/rpcs/common.rs index 126bebb4..9a247de5 100644 --- a/rpc_sidecar/src/rpcs/common.rs +++ b/rpc_sidecar/src/rpcs/common.rs @@ -7,13 +7,11 @@ use crate::rpcs::error::Error; use casper_types::{ account::AccountHash, bytesrepr::ToBytes, global_state::TrieMerkleProof, Account, AddressableEntity, AvailableBlockRange, BlockHeader, BlockIdentifier, EntityAddr, - GlobalStateIdentifier, Key, SignedBlock, StoredValue, URef, U512, + GlobalStateIdentifier, Key, SignedBlock, StoredValue, }; use crate::NodeClient; -use super::state::PurseIdentifier; - pub(super) static MERKLE_PROOF: Lazy = Lazy::new(|| { String::from( "01000000006ef2e0949ac76e55812421f755abe129b6244fe7168b77f47a72536147614625016ef2e0949ac76e\ @@ -178,51 +176,6 @@ pub async fn resolve_entity_addr( })) } -pub async fn get_main_purse( - node_client: &dyn NodeClient, - identifier: PurseIdentifier, - state_identifier: Option, -) -> Result { - let account_hash = match identifier { - PurseIdentifier::MainPurseUnderPublicKey(account_public_key) => { - account_public_key.to_account_hash() - } - PurseIdentifier::MainPurseUnderAccountHash(account_hash) => account_hash, - PurseIdentifier::PurseUref(purse_uref) => return Ok(purse_uref), - }; - match resolve_account_hash(node_client, account_hash, state_identifier) - .await? - .ok_or(Error::MainPurseNotFound)? - .value - { - EntityOrAccount::AddressableEntity(entity) => Ok(entity.main_purse()), - EntityOrAccount::LegacyAccount(account) => Ok(account.main_purse()), - } -} - -pub async fn get_balance( - node_client: &dyn NodeClient, - uref: URef, - state_identifier: Option, -) -> Result, Error> { - let key = Key::Balance(uref.addr()); - let (value, merkle_proof) = node_client - .query_global_state(state_identifier, key, vec![]) - .await - .map_err(|err| Error::NodeRequest("balance by uref", err))? - .ok_or(Error::GlobalStateEntryNotFound)? - .into_inner(); - let value = value - .into_cl_value() - .ok_or(Error::InvalidPurseBalance)? - .into_t() - .map_err(|_| Error::InvalidPurseBalance)?; - Ok(SuccessfulQueryResult { - value, - merkle_proof, - }) -} - pub fn encode_proof(proof: &Vec>) -> Result { Ok(base16::encode_lower( &proof.to_bytes().map_err(Error::BytesreprFailure)?, diff --git a/rpc_sidecar/src/rpcs/docs.rs b/rpc_sidecar/src/rpcs/docs.rs index b3b89875..56bbf842 100644 --- a/rpc_sidecar/src/rpcs/docs.rs +++ b/rpc_sidecar/src/rpcs/docs.rs @@ -20,7 +20,7 @@ use super::{ info::{GetChainspec, GetDeploy, GetPeers, GetStatus, GetTransaction, GetValidatorChanges}, state::{ GetAccountInfo, GetAddressableEntity, GetAuctionInfo, GetBalance, GetDictionaryItem, - GetItem, QueryBalance, QueryGlobalState, + GetItem, QueryBalance, QueryBalanceDetails, QueryGlobalState, }, ApiVersion, NodeClient, RpcError, RpcWithOptionalParams, RpcWithParams, RpcWithoutParams, CURRENT_API_VERSION, @@ -84,6 +84,9 @@ pub(crate) static OPEN_RPC_SCHEMA: Lazy = Lazy::new(|| { schema.push_with_params::( "query for a balance using a purse identifier and a state identifier", ); + schema.push_with_params::( + "query for full balance information using a purse identifier and a state identifier", + ); schema.push_without_params::("returns a list of peers connected to the node"); schema.push_without_params::("returns the current status of the node"); schema diff --git a/rpc_sidecar/src/rpcs/state.rs b/rpc_sidecar/src/rpcs/state.rs index ea5f8ee4..8295d95b 100644 --- a/rpc_sidecar/src/rpcs/state.rs +++ b/rpc_sidecar/src/rpcs/state.rs @@ -14,6 +14,7 @@ use super::{ CURRENT_API_VERSION, }; use casper_binary_port::DictionaryItemIdentifier; +use casper_binary_port::PurseIdentifier as PortPurseIdentifier; #[cfg(test)] use casper_types::testing::TestRng; use casper_types::{ @@ -28,8 +29,8 @@ use casper_types::{ AUCTION, }, AddressableEntity, AddressableEntityHash, AuctionState, BlockHash, BlockHeader, BlockHeaderV2, - BlockIdentifier, BlockV2, CLValue, Digest, EntityAddr, GlobalStateIdentifier, Key, KeyTag, - PublicKey, SecretKey, StoredValue, URef, U512, + BlockIdentifier, BlockTime, BlockV2, CLValue, Digest, EntityAddr, GlobalStateIdentifier, Key, + KeyTag, PublicKey, SecretKey, StoredValue, Timestamp, URef, U512, }; #[cfg(test)] use rand::Rng; @@ -137,6 +138,25 @@ static QUERY_BALANCE_RESULT: Lazy = Lazy::new(|| QueryBalanc api_version: DOCS_EXAMPLE_API_VERSION, balance: U512::from(123_456), }); +static QUERY_BALANCE_DETAILS_PARAMS: Lazy = + Lazy::new(|| QueryBalanceDetailsParams { + state_identifier: Some(BalanceStateIdentifier::Block(BlockIdentifier::Hash( + *BlockHash::example(), + ))), + purse_identifier: PurseIdentifier::MainPurseUnderAccountHash(AccountHash::new([9u8; 32])), + }); +static QUERY_BALANCE_DETAILS_RESULT: Lazy = + Lazy::new(|| QueryBalanceDetailsResult { + api_version: DOCS_EXAMPLE_API_VERSION, + total_balance: U512::from(123_456), + available_balance: U512::from(123_456), + total_balance_proof: MERKLE_PROOF.clone(), + holds: vec![BalanceHoldWithProof { + time: BlockTime::new(0), + amount: U512::from(123_456), + proof: MERKLE_PROOF.clone(), + }], + }); /// Params for "state_get_item" RPC request. #[derive(Serialize, Deserialize, Debug, JsonSchema)] @@ -228,7 +248,10 @@ pub struct GetBalanceResult { /// The RPC API version. #[schemars(with = "String")] pub api_version: ApiVersion, - /// The balance value. + /// The available balance in motes (total balance - sum of all active holds). + /// The active holds are determined by the current timestamp and not the + /// state root hash. If you need to account for holds at a specific time, + /// you should use the `query_balance_details` RPC. pub balance_value: U512, /// The Merkle proof. pub merkle_proof: String, @@ -255,12 +278,20 @@ impl RpcWithParams for GetBalance { ) -> Result { let purse_uref = URef::from_formatted_str(¶ms.purse_uref).map_err(Error::InvalidPurseURef)?; - let state_identifier = GlobalStateIdentifier::StateRootHash(params.state_root_hash); - let result = common::get_balance(&*node_client, purse_uref, Some(state_identifier)).await?; + + let state_id = GlobalStateIdentifier::StateRootHash(params.state_root_hash); + let purse_id = PortPurseIdentifier::Purse(purse_uref); + // we cannot query the balance at a specific timestamp, so we use the current one + let timestamp = Timestamp::now(); + let balance = node_client + .get_balance_by_state_root(Some(state_id), purse_id, timestamp) + .await + .map_err(|err| Error::NodeRequest("balance", err))?; + Ok(Self::ResponseResult { api_version: CURRENT_API_VERSION, - balance_value: result.value, - merkle_proof: common::encode_proof(&result.merkle_proof)?, + balance_value: balance.available_balance, + merkle_proof: common::encode_proof(&vec![*balance.total_balance_proof])?, }) } } @@ -844,10 +875,25 @@ pub enum PurseIdentifier { MainPurseUnderPublicKey(PublicKey), /// The main purse of the account identified by this account hash. MainPurseUnderAccountHash(AccountHash), + /// The main purse of the account identified by this entity address. + MainPurseUnderEntityAddr(EntityAddr), /// The purse identified by this URef. PurseUref(URef), } +impl PurseIdentifier { + pub fn into_port_purse_identifier(self) -> PortPurseIdentifier { + match self { + Self::MainPurseUnderPublicKey(public_key) => PortPurseIdentifier::PublicKey(public_key), + Self::MainPurseUnderAccountHash(account_hash) => { + PortPurseIdentifier::Account(account_hash) + } + Self::MainPurseUnderEntityAddr(entity_addr) => PortPurseIdentifier::Entity(entity_addr), + Self::PurseUref(uref) => PortPurseIdentifier::Purse(uref), + } + } +} + /// Params for "query_balance" RPC request. #[derive(Serialize, Deserialize, Debug, JsonSchema)] pub struct QueryBalanceParams { @@ -870,7 +916,7 @@ pub struct QueryBalanceResult { /// The RPC API version. #[schemars(with = "String")] pub api_version: ApiVersion, - /// The balance represented in motes. + /// The available balance in motes (total balance - sum of all active holds). pub balance: U512, } @@ -893,17 +939,156 @@ impl RpcWithParams for QueryBalance { node_client: Arc, params: Self::RequestParams, ) -> Result { - let purse = common::get_main_purse( - &*node_client, - params.purse_identifier, - params.state_identifier, - ) - .await?; - let balance = common::get_balance(&*node_client, purse, params.state_identifier).await?; + let purse_id = params.purse_identifier.into_port_purse_identifier(); + let balance = match params.state_identifier { + Some(GlobalStateIdentifier::BlockHash(hash)) => node_client + .get_balance_by_block(Some(BlockIdentifier::Hash(hash)), purse_id) + .await + .map_err(|err| Error::NodeRequest("balance by block hash", err))?, + Some(GlobalStateIdentifier::BlockHeight(height)) => node_client + .get_balance_by_block(Some(BlockIdentifier::Height(height)), purse_id) + .await + .map_err(|err| Error::NodeRequest("balance by block height", err))?, + Some(GlobalStateIdentifier::StateRootHash(digest)) => { + // we cannot query the balance at a specific timestamp, so we use the current one + let timestamp = Timestamp::now(); + let state_id = GlobalStateIdentifier::StateRootHash(digest); + node_client + .get_balance_by_state_root(Some(state_id), purse_id, timestamp) + .await + .map_err(|err| Error::NodeRequest("balance by state root", err))? + } + None => node_client + .get_balance_by_block(None, purse_id) + .await + .map_err(|err| Error::NodeRequest("balance by latest block", err))?, + }; + Ok(Self::ResponseResult { + api_version: CURRENT_API_VERSION, + balance: balance.available_balance, + }) + } +} + +/// Identifier of a balance. +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[serde(deny_unknown_fields, rename_all = "snake_case")] +pub enum BalanceStateIdentifier { + /// The balance at a specific block. + Block(BlockIdentifier), + /// The balance at a specific state root. + StateRoot { + /// The state root hash. + state_root_hash: Digest, + /// Timestamp for holds lookup. + timestamp: Timestamp, + }, +} + +/// Params for "query_balance" RPC request. +#[derive(Serialize, Deserialize, Debug, JsonSchema)] +pub struct QueryBalanceDetailsParams { + /// The identifier for the state used for the query, if none is passed, + /// the latest block will be used. + pub state_identifier: Option, + /// The identifier to obtain the purse corresponding to balance query. + pub purse_identifier: PurseIdentifier, +} + +impl DocExample for QueryBalanceDetailsParams { + fn doc_example() -> &'static Self { + &QUERY_BALANCE_DETAILS_PARAMS + } +} + +/// Result for "query_balance" RPC response. +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, JsonSchema)] +pub struct QueryBalanceDetailsResult { + /// The RPC API version. + #[schemars(with = "String")] + pub api_version: ApiVersion, + /// The purses total balance, not considering holds. + pub total_balance: U512, + /// The available balance in motes (total balance - sum of all active holds). + pub available_balance: U512, + /// A proof that the given value is present in the Merkle trie. + pub total_balance_proof: String, + /// Holds active at the requested point in time. + pub holds: Vec, +} + +impl DocExample for QueryBalanceDetailsResult { + fn doc_example() -> &'static Self { + &QUERY_BALANCE_DETAILS_RESULT + } +} + +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, JsonSchema)] +pub struct BalanceHoldWithProof { + /// The block time at which the hold was created. + pub time: BlockTime, + /// The amount in the hold. + pub amount: U512, + /// A proof that the given value is present in the Merkle trie. + pub proof: String, +} + +/// "query_balance_details" RPC. +pub struct QueryBalanceDetails {} + +#[async_trait] +impl RpcWithParams for QueryBalanceDetails { + const METHOD: &'static str = "query_balance_details"; + type RequestParams = QueryBalanceDetailsParams; + type ResponseResult = QueryBalanceDetailsResult; + + async fn do_handle_request( + node_client: Arc, + params: Self::RequestParams, + ) -> Result { + let purse_id = params.purse_identifier.into_port_purse_identifier(); + let balance = match params.state_identifier { + Some(BalanceStateIdentifier::Block(block_identifier)) => node_client + .get_balance_by_block(Some(block_identifier), purse_id) + .await + .map_err(|err| Error::NodeRequest("balance by block", err))?, + Some(BalanceStateIdentifier::StateRoot { + state_root_hash, + timestamp, + }) => node_client + .get_balance_by_state_root( + Some(GlobalStateIdentifier::StateRootHash(state_root_hash)), + purse_id, + timestamp, + ) + .await + .map_err(|err| Error::NodeRequest("balance by state root", err))?, + None => node_client + .get_balance_by_block(None, purse_id) + .await + .map_err(|err| Error::NodeRequest("balance by latest block", err))?, + }; + + let holds = balance + .balance_holds + .into_iter() + .flat_map(|(time, holds)| { + holds.into_iter().map(move |(_, (amount, proof))| { + Ok(BalanceHoldWithProof { + time, + amount, + proof: common::encode_proof(&vec![proof])?, + }) + }) + }) + .collect::, Error>>()?; Ok(Self::ResponseResult { api_version: CURRENT_API_VERSION, - balance: balance.value, + total_balance: balance.total_balance, + available_balance: balance.available_balance, + total_balance_proof: common::encode_proof(&vec![*balance.total_balance_proof])?, + holds, }) } } @@ -992,13 +1177,12 @@ mod tests { use crate::{rpcs::ErrorCode, ClientError, SUPPORTED_PROTOCOL_VERSION}; use casper_binary_port::{ - BinaryRequest, BinaryResponse, BinaryResponseAndRequest, DictionaryQueryResult, GetRequest, - GlobalStateQueryResult, GlobalStateRequest, InformationRequestTag, + BalanceResponse, BinaryRequest, BinaryResponse, BinaryResponseAndRequest, + DictionaryQueryResult, GetRequest, GlobalStateQueryResult, GlobalStateRequest, + InformationRequestTag, }; use casper_types::{ - addressable_entity::{ - ActionThresholds, AssociatedKeys, EntityKindTag, MessageTopics, NamedKeys, - }, + addressable_entity::{MessageTopics, NamedKeys}, global_state::{TrieMerkleProof, TrieMerkleProofStep}, system::auction::{Bid, BidKind, ValidatorBid}, testing::TestRng, @@ -1046,14 +1230,21 @@ mod tests { #[tokio::test] async fn should_read_balance() { let rng = &mut TestRng::new(); - let balance_value: U512 = rng.gen(); - let result = GlobalStateQueryResult::new( - StoredValue::CLValue(CLValue::from_t(balance_value).unwrap()), - vec![], - ); + let available_balance = rng.gen(); + let total_balance = rng.gen(); + let balance = BalanceResponse { + total_balance, + available_balance, + total_balance_proof: Box::new(TrieMerkleProof::new( + Key::Account(rng.gen()), + StoredValue::CLValue(CLValue::from_t(rng.gen::()).unwrap()), + VecDeque::from_iter([TrieMerkleProofStep::random(rng)]), + )), + balance_holds: BTreeMap::new(), + }; let resp = GetBalance::do_handle_request( - Arc::new(ValidGlobalStateResultMock(result.clone())), + Arc::new(ValidBalanceMock(balance.clone())), GetBalanceParams { state_root_hash: rng.gen(), purse_uref: URef::new(rng.gen(), AccessRights::empty()).to_formatted_string(), @@ -1066,8 +1257,9 @@ mod tests { resp, GetBalanceResult { api_version: CURRENT_API_VERSION, - balance_value, - merkle_proof: String::from("00000000"), + balance_value: available_balance, + merkle_proof: common::encode_proof(&vec![*balance.total_balance_proof]) + .expect("should encode proof"), } ); } @@ -1101,10 +1293,15 @@ mod tests { &[], )) } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::AllItems { - key_tag: KeyTag::Bid, - .. - })) => { + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::AllItems { + key_tag: KeyTag::Bid, + .. + } + ) => + { let bids = self .legacy_bids .iter() @@ -1116,10 +1313,15 @@ mod tests { &[], )) } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::AllItems { - key_tag: KeyTag::BidAddr, - .. - })) => { + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::AllItems { + key_tag: KeyTag::BidAddr, + .. + } + ) => + { let bids = self .bids .iter() @@ -1131,10 +1333,15 @@ mod tests { &[], )) } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::SystemEntityRegistry, - .. - })) => { + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::Item { + base_key: Key::SystemEntityRegistry, + .. + } + ) => + { let system_contracts = iter::once((AUCTION.to_string(), self.contract_hash)) .collect::>(); @@ -1147,10 +1354,15 @@ mod tests { &[], )) } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::AddressableEntity(_), - .. - })) => { + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::Item { + base_key: Key::AddressableEntity(_), + .. + } + ) => + { let result = GlobalStateQueryResult::new( StoredValue::CLValue(CLValue::from_t(self.snapshot.clone()).unwrap()), vec![], @@ -1226,35 +1438,49 @@ mod tests { &[], )) } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::Account(_), - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - GlobalStateQueryResult::new( - StoredValue::CLValue( - CLValue::from_t(Key::contract_entity_key(self.entity_hash)) - .unwrap(), + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::Item { + base_key: Key::Account(_), + .. + } + ) => + { + Ok(BinaryResponseAndRequest::new( + BinaryResponse::from_value( + GlobalStateQueryResult::new( + StoredValue::CLValue( + CLValue::from_t(Key::contract_entity_key(self.entity_hash)) + .unwrap(), + ), + vec![], ), - vec![], + SUPPORTED_PROTOCOL_VERSION, ), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )), - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::AddressableEntity(_), - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - GlobalStateQueryResult::new( - StoredValue::AddressableEntity(self.entity.clone()), - vec![], + &[], + )) + } + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::Item { + base_key: Key::AddressableEntity(_), + .. + } + ) => + { + Ok(BinaryResponseAndRequest::new( + BinaryResponse::from_value( + GlobalStateQueryResult::new( + StoredValue::AddressableEntity(self.entity.clone()), + vec![], + ), + SUPPORTED_PROTOCOL_VERSION, ), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )), + &[], + )) + } req => unimplemented!("unexpected request: {:?}", req), } } @@ -1363,13 +1589,20 @@ mod tests { &[], )) } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::AddressableEntity(_), - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::new_empty(SUPPORTED_PROTOCOL_VERSION), - &[], - )), + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::Item { + base_key: Key::AddressableEntity(_), + .. + } + ) => + { + Ok(BinaryResponseAndRequest::new( + BinaryResponse::new_empty(SUPPORTED_PROTOCOL_VERSION), + &[], + )) + } req => unimplemented!("unexpected request: {:?}", req), } } @@ -1458,22 +1691,29 @@ mod tests { &[], )) } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::Account(_), - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - GlobalStateQueryResult::new( - StoredValue::CLValue( - CLValue::from_t(Key::contract_entity_key(self.entity_hash)) - .unwrap(), + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::Item { + base_key: Key::Account(_), + .. + } + ) => + { + Ok(BinaryResponseAndRequest::new( + BinaryResponse::from_value( + GlobalStateQueryResult::new( + StoredValue::CLValue( + CLValue::from_t(Key::contract_entity_key(self.entity_hash)) + .unwrap(), + ), + vec![], ), - vec![], + SUPPORTED_PROTOCOL_VERSION, ), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )), + &[], + )) + } req => unimplemented!("unexpected request: {:?}", req), } } @@ -1525,13 +1765,20 @@ mod tests { &[], )) } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::Account(_), - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::new_empty(SUPPORTED_PROTOCOL_VERSION), - &[], - )), + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::Item { + base_key: Key::Account(_), + .. + } + ) => + { + Ok(BinaryResponseAndRequest::new( + BinaryResponse::new_empty(SUPPORTED_PROTOCOL_VERSION), + &[], + )) + } req => unimplemented!("unexpected request: {:?}", req), } } @@ -1626,20 +1873,25 @@ mod tests { } #[tokio::test] - async fn should_read_query_balance_by_uref_result() { + async fn should_read_query_balance_result() { let rng = &mut TestRng::new(); - let block = Block::V2(TestBlockBuilder::new().build(rng)); - let balance = rng.gen::(); - let stored_value = StoredValue::CLValue(CLValue::from_t(balance).unwrap()); - let expected = GlobalStateQueryResult::new(stored_value.clone(), vec![]); + let available_balance = rng.gen(); + let total_balance = rng.gen(); + let balance = BalanceResponse { + total_balance, + available_balance, + total_balance_proof: Box::new(TrieMerkleProof::new( + Key::Account(rng.gen()), + StoredValue::CLValue(CLValue::from_t(rng.gen::()).unwrap()), + VecDeque::from_iter([TrieMerkleProofStep::random(rng)]), + )), + balance_holds: BTreeMap::new(), + }; let resp = QueryBalance::do_handle_request( - Arc::new(ValidGlobalStateResultWithBlockMock { - block: block.clone(), - result: expected.clone(), - }), + Arc::new(ValidBalanceMock(balance.clone())), QueryBalanceParams { - state_identifier: None, + state_identifier: Some(GlobalStateIdentifier::random(rng)), purse_identifier: PurseIdentifier::PurseUref(URef::new( rng.gen(), AccessRights::empty(), @@ -1653,209 +1905,35 @@ mod tests { resp, QueryBalanceResult { api_version: CURRENT_API_VERSION, - balance + balance: available_balance, } ); } #[tokio::test] - async fn should_read_query_balance_by_account_result() { - use casper_types::account::{ActionThresholds, AssociatedKeys}; - - struct ClientMock { - block: Block, - account: Account, - balance: U512, - } - - #[async_trait] - impl NodeClient for ClientMock { - async fn send_request( - &self, - req: BinaryRequest, - ) -> Result { - match req { - BinaryRequest::Get(GetRequest::Information { info_type_tag, .. }) - if InformationRequestTag::try_from(info_type_tag) - == Ok(InformationRequestTag::BlockHeader) => - { - Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - self.block.clone_header(), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )) - } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::Account(_), - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - GlobalStateQueryResult::new( - StoredValue::Account(self.account.clone()), - vec![], - ), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )), - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::Balance(_), - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - GlobalStateQueryResult::new( - StoredValue::CLValue(CLValue::from_t(self.balance).unwrap()), - vec![], - ), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )), - req => unimplemented!("unexpected request: {:?}", req), - } - } - } - - let rng = &mut TestRng::new(); - let block = Block::V2(TestBlockBuilder::new().build(rng)); - let account = Account::new( - rng.gen(), - NamedKeys::default(), - rng.gen(), - AssociatedKeys::default(), - ActionThresholds::default(), - ); - - let balance = rng.gen::(); - - let resp = QueryBalance::do_handle_request( - Arc::new(ClientMock { - block: block.clone(), - account: account.clone(), - balance, - }), - QueryBalanceParams { - state_identifier: None, - purse_identifier: PurseIdentifier::MainPurseUnderAccountHash( - account.account_hash(), - ), - }, - ) - .await - .expect("should handle request"); - - assert_eq!( - resp, - QueryBalanceResult { - api_version: CURRENT_API_VERSION, - balance - } - ); - } - - #[tokio::test] - async fn should_read_query_balance_by_addressable_entity_result() { - struct ClientMock { - block: Block, - entity_hash: AddressableEntityHash, - entity: AddressableEntity, - balance: U512, - } - - #[async_trait] - impl NodeClient for ClientMock { - async fn send_request( - &self, - req: BinaryRequest, - ) -> Result { - match req { - BinaryRequest::Get(GetRequest::Information { info_type_tag, .. }) - if InformationRequestTag::try_from(info_type_tag) - == Ok(InformationRequestTag::BlockHeader) => - { - Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - self.block.clone_header(), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )) - } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::Account(_), - .. - })) => { - let key = - Key::addressable_entity_key(EntityKindTag::Account, self.entity_hash); - let value = CLValue::from_t(key).unwrap(); - Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - GlobalStateQueryResult::new(StoredValue::CLValue(value), vec![]), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )) - } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::AddressableEntity(_), - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - GlobalStateQueryResult::new( - StoredValue::AddressableEntity(self.entity.clone()), - vec![], - ), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )), - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::Balance(_), - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - GlobalStateQueryResult::new( - StoredValue::CLValue(CLValue::from_t(self.balance).unwrap()), - vec![], - ), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )), - req => unimplemented!("unexpected request: {:?}", req), - } - } - } - + async fn should_read_query_balance_details_result() { let rng = &mut TestRng::new(); - let block = Block::V2(TestBlockBuilder::new().build(rng)); - let entity = AddressableEntity::new( - PackageHash::new(rng.gen()), - ByteCodeHash::new(rng.gen()), - EntryPoints::default(), - ProtocolVersion::V1_0_0, - rng.gen(), - AssociatedKeys::default(), - ActionThresholds::default(), - MessageTopics::default(), - EntityKind::default(), - ); - - let balance: U512 = rng.gen(); - let entity_hash: AddressableEntityHash = rng.gen(); + let available_balance = rng.gen(); + let total_balance = rng.gen(); + let balance = BalanceResponse { + total_balance, + available_balance, + total_balance_proof: Box::new(TrieMerkleProof::new( + Key::Account(rng.gen()), + StoredValue::CLValue(CLValue::from_t(rng.gen::()).unwrap()), + VecDeque::from_iter([TrieMerkleProofStep::random(rng)]), + )), + balance_holds: BTreeMap::new(), + }; - let resp = QueryBalance::do_handle_request( - Arc::new(ClientMock { - block: block.clone(), - entity_hash, - entity: entity.clone(), - balance, - }), - QueryBalanceParams { - state_identifier: None, - purse_identifier: PurseIdentifier::MainPurseUnderAccountHash(rng.gen()), + let resp = QueryBalanceDetails::do_handle_request( + Arc::new(ValidBalanceMock(balance.clone())), + QueryBalanceDetailsParams { + state_identifier: Some(BalanceStateIdentifier::Block(BlockIdentifier::random(rng))), + purse_identifier: PurseIdentifier::PurseUref(URef::new( + rng.gen(), + AccessRights::empty(), + )), }, ) .await @@ -1863,9 +1941,13 @@ mod tests { assert_eq!( resp, - QueryBalanceResult { + QueryBalanceDetailsResult { api_version: CURRENT_API_VERSION, - balance + total_balance, + available_balance, + total_balance_proof: common::encode_proof(&vec![*balance.total_balance_proof]) + .expect("should encode proof"), + holds: vec![], } ); } @@ -1882,15 +1964,17 @@ mod tests { req: BinaryRequest, ) -> Result { match req { - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::DictionaryItem { - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - DictionaryQueryResult::new(self.dict_key, self.query_result.clone()), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )), + BinaryRequest::Get(GetRequest::State(req)) + if matches!(&*req, GlobalStateRequest::DictionaryItem { .. }) => + { + Ok(BinaryResponseAndRequest::new( + BinaryResponse::from_value( + DictionaryQueryResult::new(self.dict_key, self.query_result.clone()), + SUPPORTED_PROTOCOL_VERSION, + ), + &[], + )) + } req => unimplemented!("unexpected request: {:?}", req), } } @@ -1905,7 +1989,9 @@ mod tests { req: BinaryRequest, ) -> Result { match req { - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { .. })) => { + BinaryRequest::Get(GetRequest::State(req)) + if matches!(&*req, GlobalStateRequest::Item { .. }) => + { Ok(BinaryResponseAndRequest::new( BinaryResponse::from_value(self.0.clone(), SUPPORTED_PROTOCOL_VERSION), &[], @@ -1940,7 +2026,9 @@ mod tests { &[], )) } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { .. })) => { + BinaryRequest::Get(GetRequest::State(req)) + if matches!(&*req, GlobalStateRequest::Item { .. }) => + { Ok(BinaryResponseAndRequest::new( BinaryResponse::from_value(self.result.clone(), SUPPORTED_PROTOCOL_VERSION), &[], @@ -1975,19 +2063,52 @@ mod tests { &[], )) } - BinaryRequest::Get(GetRequest::State(GlobalStateRequest::Item { - base_key: Key::Account(_), - .. - })) => Ok(BinaryResponseAndRequest::new( - BinaryResponse::from_value( - GlobalStateQueryResult::new( - StoredValue::Account(self.account.clone()), - vec![], + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::Item { + base_key: Key::Account(_), + .. + } + ) => + { + Ok(BinaryResponseAndRequest::new( + BinaryResponse::from_value( + GlobalStateQueryResult::new( + StoredValue::Account(self.account.clone()), + vec![], + ), + SUPPORTED_PROTOCOL_VERSION, ), - SUPPORTED_PROTOCOL_VERSION, - ), - &[], - )), + &[], + )) + } + req => unimplemented!("unexpected request: {:?}", req), + } + } + } + + struct ValidBalanceMock(BalanceResponse); + + #[async_trait] + impl NodeClient for ValidBalanceMock { + async fn send_request( + &self, + req: BinaryRequest, + ) -> Result { + match req { + BinaryRequest::Get(GetRequest::State(req)) + if matches!( + &*req, + GlobalStateRequest::BalanceByBlock { .. } + | GlobalStateRequest::BalanceByStateRoot { .. } + ) => + { + Ok(BinaryResponseAndRequest::new( + BinaryResponse::from_value(self.0.clone(), SUPPORTED_PROTOCOL_VERSION), + &[], + )) + } req => unimplemented!("unexpected request: {:?}", req), } }