diff --git a/CHANGELOG.md b/CHANGELOG.md index 7301d2316..3ae8ed84b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/ ## [Unreleased] +### Added + - Node RPC: new method added - `chainstate_tokens_info`. + ### Changed - Wallet RPC: `wallet_info`: the structure of the returned field `extra_info` was changed. diff --git a/Cargo.lock b/Cargo.lock index 5622b8120..c2f550c62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1358,6 +1358,7 @@ dependencies = [ "logging", "orders-accounting", "pos-accounting", + "rand 0.8.5", "randomness", "rstest", "serialization", diff --git a/chainstate/src/detail/chainstateref/mod.rs b/chainstate/src/detail/chainstateref/mod.rs index 392521cb9..3c37cadf5 100644 --- a/chainstate/src/detail/chainstateref/mod.rs +++ b/chainstate/src/detail/chainstateref/mod.rs @@ -361,6 +361,8 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat self.last_common_ancestor(block_index, &best_block_index) } + // Note: this function will return Some for NFTs only, because the data is only written + // for NFTs. This is decided by `token_issuance_cache::has_tokens_issuance_to_cache`. #[log_error] pub fn get_token_aux_data( &self, @@ -369,8 +371,9 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat self.db_tx.get_token_aux_data(token_id).map_err(PropertyQueryError::from) } + // Note: same as get_token_aux_data, this only works for NFTs, for the same reason. #[log_error] - pub fn get_token_id( + pub fn get_token_id_from_issuance_tx( &self, tx_id: &Id, ) -> Result, PropertyQueryError> { diff --git a/chainstate/src/detail/error_classification.rs b/chainstate/src/detail/error_classification.rs index 7952d85bb..9449a9149 100644 --- a/chainstate/src/detail/error_classification.rs +++ b/chainstate/src/detail/error_classification.rs @@ -449,9 +449,8 @@ impl BlockProcessingErrorClassification for PropertyQueryError { | PropertyQueryError::GenesisHeaderRequested | PropertyQueryError::InvalidStartingBlockHeightForMainchainBlocks(_) | PropertyQueryError::InvalidBlockHeightRange { .. } - | PropertyQueryError::UnsupportedTokenV0InOrder(_) => { - BlockProcessingErrorClass::General - } + | PropertyQueryError::UnsupportedTokenV0InOrder(_) + | PropertyQueryError::TokenInfoMissing(_) => BlockProcessingErrorClass::General, // Note: these errors are strange - sometimes they don't look like General, judging // by the code that uses them. But other times some of them seem to just wrap storage // errors. diff --git a/chainstate/src/detail/query.rs b/chainstate/src/detail/query.rs index 87b2d6074..24118b3f7 100644 --- a/chainstate/src/detail/query.rs +++ b/chainstate/src/detail/query.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::num::NonZeroUsize; +use std::{collections::BTreeSet, num::NonZeroUsize}; use chainstate_storage::BlockchainStorageRead; use chainstate_types::{BlockIndex, GenBlockIndex, Locator, PropertyQueryError}; @@ -367,6 +367,19 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat } } + pub fn get_tokens_info_for_rpc( + &self, + token_ids: &BTreeSet, + ) -> Result, PropertyQueryError> { + token_ids + .iter() + .map(|id| -> Result<_, PropertyQueryError> { + self.get_token_info_for_rpc(*id)? + .ok_or(PropertyQueryError::TokenInfoMissing(*id)) + }) + .collect::>() + } + pub fn get_token_aux_data( &self, token_id: &TokenId, @@ -378,7 +391,7 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat &self, tx_id: &Id, ) -> Result, PropertyQueryError> { - self.chainstate_ref.get_token_id(tx_id) + self.chainstate_ref.get_token_id_from_issuance_tx(tx_id) } pub fn get_mainchain_blocks_list(&self) -> Result>, PropertyQueryError> { diff --git a/chainstate/src/interface/chainstate_interface.rs b/chainstate/src/interface/chainstate_interface.rs index d2ca4869e..d5c2323ff 100644 --- a/chainstate/src/interface/chainstate_interface.rs +++ b/chainstate/src/interface/chainstate_interface.rs @@ -13,7 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, num::NonZeroUsize, sync::Arc}; +use std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZeroUsize, + sync::Arc, +}; use crate::{ detail::BlockSource, ChainInfo, ChainstateConfig, ChainstateError, ChainstateEvent, @@ -206,14 +210,24 @@ pub trait ChainstateInterface: Send + Sync { &self, token_id: TokenId, ) -> Result, ChainstateError>; + + /// Return infos for the specified token ids. + fn get_tokens_info_for_rpc( + &self, + token_ids: &BTreeSet, + ) -> Result, ChainstateError>; + + /// Return token's auxiliary data; available for NFTs only. fn get_token_aux_data( &self, token_id: TokenId, ) -> Result, ChainstateError>; + /// Obtain token id given the id of the issuing tx; available for NFTs only. fn get_token_id_from_issuance_tx( &self, tx_id: &Id, ) -> Result, ChainstateError>; + /// Obtain token data given its id; available for fungible tokens only. fn get_token_data( &self, id: &TokenId, diff --git a/chainstate/src/interface/chainstate_interface_impl.rs b/chainstate/src/interface/chainstate_interface_impl.rs index 411cdd800..68fb9ed2f 100644 --- a/chainstate/src/interface/chainstate_interface_impl.rs +++ b/chainstate/src/interface/chainstate_interface_impl.rs @@ -13,7 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, num::NonZeroUsize, sync::Arc}; +use std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZeroUsize, + sync::Arc, +}; use crate::{ detail::{ @@ -523,6 +527,18 @@ where .map_err(ChainstateError::FailedToReadProperty) } + #[tracing::instrument(skip(self))] + fn get_tokens_info_for_rpc( + &self, + token_ids: &BTreeSet, + ) -> Result, ChainstateError> { + self.chainstate + .query() + .map_err(ChainstateError::from)? + .get_tokens_info_for_rpc(token_ids) + .map_err(ChainstateError::FailedToReadProperty) + } + #[tracing::instrument(skip_all, fields(token_id = %token_id))] fn get_token_aux_data( &self, diff --git a/chainstate/src/interface/chainstate_interface_impl_delegation.rs b/chainstate/src/interface/chainstate_interface_impl_delegation.rs index 7f12286ba..c146fc9a8 100644 --- a/chainstate/src/interface/chainstate_interface_impl_delegation.rs +++ b/chainstate/src/interface/chainstate_interface_impl_delegation.rs @@ -14,7 +14,7 @@ // limitations under the License. use std::{ - collections::BTreeMap, + collections::{BTreeMap, BTreeSet}, num::NonZeroUsize, ops::{Deref, DerefMut}, sync::Arc, @@ -276,6 +276,13 @@ where self.deref().get_token_info_for_rpc(token_id) } + fn get_tokens_info_for_rpc( + &self, + token_ids: &BTreeSet, + ) -> Result, ChainstateError> { + self.deref().get_tokens_info_for_rpc(token_ids) + } + fn get_token_aux_data( &self, token_id: TokenId, diff --git a/chainstate/src/rpc/mod.rs b/chainstate/src/rpc/mod.rs index c693c1316..4bc3628db 100644 --- a/chainstate/src/rpc/mod.rs +++ b/chainstate/src/rpc/mod.rs @@ -172,10 +172,14 @@ trait ChainstateRpc { delegation_address: String, ) -> RpcResult>; - /// Get token information, given a token id, in address form. + /// Get token information, given a token id in address form. #[method(name = "token_info")] async fn token_info(&self, token_id: String) -> RpcResult>; + /// Get tokens information, given multiple token ids in address form. + #[method(name = "tokens_info")] + async fn tokens_info(&self, token_ids: Vec) -> RpcResult>; + /// Get order information, given an order id, in address form. #[method(name = "order_info")] async fn order_info(&self, order_id: String) -> RpcResult>; @@ -243,19 +247,22 @@ impl ChainstateRpcServer for super::ChainstateHandle { if let Some((block, block_index)) = both { let token_ids = collect_token_v1_ids_from_output_values_holder(&block); - let mut token_decimals = BTreeMap::new(); - - // TODO replace this loop with a single ChainstateInterface function call obtaining - // all infos at once (when the function is implemented). - for token_id in token_ids { - let token_info: RPCTokenInfo = rpc::handle_result( - self.call(move |this| get_existing_token_info_for_rpc(this, token_id)).await, - )?; - token_decimals.insert( - token_id, - TokenDecimals(token_info.token_number_of_decimals()), - ); - } + let token_decimals: BTreeMap = rpc::handle_result( + self.call(move |this| -> Result<_, ChainstateError> { + let infos = this.get_tokens_info_for_rpc(&token_ids)?; + let decimals = infos + .iter() + .map(|info| { + ( + info.token_id(), + TokenDecimals(info.token_number_of_decimals()), + ) + }) + .collect::>(); + Ok(decimals) + }) + .await, + )?; let rpc_block: RpcBlock = rpc::handle_result(RpcBlock::new( &chain_config, @@ -438,6 +445,27 @@ impl ChainstateRpcServer for super::ChainstateHandle { ) } + async fn tokens_info(&self, token_ids: Vec) -> RpcResult> { + rpc::handle_result( + self.call(move |this| -> Result<_, DynamizedError> { + let chain_config = this.get_chain_config(); + + let token_ids = token_ids + .into_iter() + .map(|token_id| -> Result<_, DynamizedError> { + Ok( + dynamize_err(Address::::from_string(chain_config, token_id))? + .into_object(), + ) + }) + .collect::>()?; + + dynamize_err(this.get_tokens_info_for_rpc(&token_ids)) + }) + .await, + ) + } + async fn order_info(&self, order_id: String) -> RpcResult> { rpc::handle_result( self.call(move |this| { @@ -488,33 +516,15 @@ impl ChainstateRpcServer for super::ChainstateHandle { } } -fn dynamize_err( - o: Result, -) -> Result> +type DynamizedError = Box; + +fn dynamize_err(o: Result) -> Result where - Box: From, + DynamizedError: From, { o.map_err(Into::into) } -fn get_existing_token_info_for_rpc( - chainstate: &(impl ChainstateInterface + ?Sized), - token_id: TokenId, -) -> Result { - chainstate - .get_token_info_for_rpc(token_id)? - .ok_or(LocalRpcError::MissingTokenInfo(token_id)) -} - -#[derive(thiserror::Error, Debug, PartialEq, Eq)] -enum LocalRpcError { - #[error("Token info missing for token {0:x}")] - MissingTokenInfo(TokenId), - - #[error(transparent)] - ChainstateError(#[from] ChainstateError), -} - #[cfg(test)] mod test { use super::*; diff --git a/chainstate/test-framework/src/helpers.rs b/chainstate/test-framework/src/helpers.rs index 8062f4c3f..b5975e832 100644 --- a/chainstate/test-framework/src/helpers.rs +++ b/chainstate/test-framework/src/helpers.rs @@ -64,7 +64,7 @@ pub fn issue_and_mint_random_token_from_best_block( TokenIssuance::V1(issuance) }; - let (token_id, _, utxo_with_change) = + let (token_id, _, _, utxo_with_change) = issue_token_from_block(rng, tf, best_block_id, utxo_to_pay_fee, issuance); let best_block_id = tf.best_block_id(); @@ -91,7 +91,12 @@ pub fn issue_token_from_block( parent_block_id: Id, utxo_to_pay_fee: UtxoOutPoint, issuance: TokenIssuance, -) -> (TokenId, Id, UtxoOutPoint) { +) -> ( + TokenId, + /*issuance_block_id*/ Id, + /*issuance_tx*/ Transaction, + /*change_outpoint*/ UtxoOutPoint, +) { let token_issuance_fee = tf.chainstate.get_chain_config().fungible_token_issuance_fee(); let fee_utxo_coins = @@ -118,13 +123,64 @@ pub fn issue_token_from_block( let tx_id = tx.transaction().get_id(); let block = tf .make_block_builder() - .add_transaction(tx) + .add_transaction(tx.clone()) .with_parent(parent_block_id) .build(rng); let block_id = block.get_id(); tf.process_block(block, BlockSource::Local).unwrap(); - (token_id, block_id, UtxoOutPoint::new(tx_id.into(), 0)) + ( + token_id, + block_id, + tx.take_transaction(), + UtxoOutPoint::new(tx_id.into(), 0), + ) +} + +pub fn make_token_issuance( + rng: &mut impl Rng, + supply: TokenTotalSupply, + freezable: IsTokenFreezable, +) -> TokenIssuance { + TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: random_ascii_alphanumeric_string(rng, 1..5).as_bytes().to_vec(), + number_of_decimals: rng.gen_range(1..18), + metadata_uri: random_ascii_alphanumeric_string(rng, 1..1024).as_bytes().to_vec(), + total_supply: supply, + authority: Destination::AnyoneCanSpend, + is_freezable: freezable, + }) +} + +pub fn issue_token_from_genesis( + rng: &mut (impl Rng + CryptoRng), + tf: &mut TestFramework, + supply: TokenTotalSupply, + freezable: IsTokenFreezable, +) -> ( + TokenId, + /*issuance_block_id*/ Id, + /*issuance_tx*/ Transaction, + TokenIssuance, + /*change_outpoint*/ UtxoOutPoint, +) { + let utxo_input_outpoint = UtxoOutPoint::new(tf.best_block_id().into(), 0); + let issuance = make_token_issuance(rng, supply, freezable); + let (token_id, issuance_block_id, issuance_tx, change_outpoint) = issue_token_from_block( + rng, + tf, + tf.genesis().get_id().into(), + utxo_input_outpoint, + issuance.clone(), + ); + + ( + token_id, + issuance_block_id, + issuance_tx, + issuance, + change_outpoint, + ) } pub fn mint_tokens_in_block( diff --git a/chainstate/test-suite/Cargo.toml b/chainstate/test-suite/Cargo.toml index 827443074..0ffa6fe4c 100644 --- a/chainstate/test-suite/Cargo.toml +++ b/chainstate/test-suite/Cargo.toml @@ -37,6 +37,7 @@ tokio = { workspace = true, features = ["rt", "time"] } criterion.workspace = true expect-test.workspace = true +rand.workspace = true rstest.workspace = true [[bench]] diff --git a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs index cb66e1c56..1a7a146be 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{borrow::Cow, collections::BTreeMap}; +use std::borrow::Cow; use rstest::rstest; @@ -23,7 +23,9 @@ use chainstate::{ }; use chainstate_storage::{BlockchainStorageRead, Transactional}; use chainstate_test_framework::{ - helpers::{issue_token_from_block, mint_tokens_in_block}, + helpers::{ + issue_token_from_block, issue_token_from_genesis, make_token_issuance, mint_tokens_in_block, + }, TestFramework, TransactionBuilder, }; use common::{ @@ -37,8 +39,8 @@ use common::{ }, timelock::OutputTimeLock, tokens::{ - IsTokenFreezable, IsTokenUnfreezable, TokenId, TokenIssuance, TokenIssuanceV1, - TokenTotalSupply, + IsTokenFreezable, IsTokenFrozen, IsTokenUnfreezable, TokenId, TokenIssuance, + TokenIssuanceV1, TokenTotalSupply, }, AccountCommand, AccountNonce, AccountType, Block, ChainstateUpgradeBuilder, Destination, GenBlock, OrderData, OutPointSourceId, SignedTransaction, Transaction, TxInput, TxOutput, @@ -49,49 +51,19 @@ use common::{ use crypto::key::{KeyKind, PrivateKey}; use randomness::{CryptoRng, Rng}; use test_utils::{ - gen_text_with_non_ascii, + assert_matches_return_val, gen_text_with_non_ascii, random::{make_seedable_rng, Seed}, random_ascii_alphanumeric_string, split_value, }; -use tokens_accounting::TokensAccountingStorageRead; use tx_verifier::{ error::{InputCheckError, ScriptError, TimelockError}, transaction_verifier::error::TokenIssuanceError, CheckTransactionError, }; -fn make_issuance( - rng: &mut impl Rng, - supply: TokenTotalSupply, - freezable: IsTokenFreezable, -) -> TokenIssuance { - TokenIssuance::V1(TokenIssuanceV1 { - token_ticker: random_ascii_alphanumeric_string(rng, 1..5).as_bytes().to_vec(), - number_of_decimals: rng.gen_range(1..18), - metadata_uri: random_ascii_alphanumeric_string(rng, 1..1024).as_bytes().to_vec(), - total_supply: supply, - authority: Destination::AnyoneCanSpend, - is_freezable: freezable, - }) -} - -// Returns created token id and outpoint with change -fn issue_token_from_genesis( - rng: &mut (impl Rng + CryptoRng), - tf: &mut TestFramework, - supply: TokenTotalSupply, - freezable: IsTokenFreezable, -) -> (TokenId, Id, UtxoOutPoint) { - let utxo_input_outpoint = UtxoOutPoint::new(tf.best_block_id().into(), 0); - let issuance = make_issuance(rng, supply, freezable); - issue_token_from_block( - rng, - tf, - tf.genesis().get_id().into(), - utxo_input_outpoint, - issuance, - ) -} +use crate::tests::helpers::token_checks::{ + assert_token_missing, check_fungible_token, ExpectedFungibleTokenData, +}; fn unmint_tokens_in_block( rng: &mut (impl Rng + CryptoRng), @@ -282,7 +254,7 @@ fn token_issue_test(#[case] seed: Seed) { CheckBlockError::CheckTransactionFailed( CheckBlockTransactionsError::CheckTransactionError( CheckTransactionError::TokensError(TokensError::IssueError( - TokenIssuanceError::IssueErrorTickerHasNoneAlphaNumericChar, + TokenIssuanceError::IssueErrorTickerHasNonAlphaNumericChar, tx_id, )) ) @@ -377,7 +349,8 @@ fn token_issue_test(#[case] seed: Seed) { ); // Valid case - let issuance = make_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let issuance = + make_token_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); let tx = TransactionBuilder::new() .add_input( TxInput::from_utxo(genesis_source_id, 0), @@ -391,22 +364,28 @@ fn token_issue_test(#[case] seed: Seed) { tx.inputs(), ) .unwrap(); - tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); - - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), - &token_id, - ) - .unwrap(); - let expected_token_data = tokens_accounting::TokenData::FungibleToken(issuance.into()); - assert_eq!(actual_token_data, Some(expected_token_data)); + let issuance_block_id = *tf + .make_block_builder() + .add_transaction(tx.clone()) + .build_and_process(&mut rng) + .unwrap() + .unwrap() + .block_id(); - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!(actual_supply, None); + &ExpectedFungibleTokenData { + issuance, + issuance_tx: tx.take_transaction(), + issuance_block_id, + circulating_supply: None, + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); }); } @@ -493,7 +472,8 @@ fn token_issue_not_enough_fee(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - let issuance = make_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let issuance = + make_token_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); let tx = TransactionBuilder::new() .add_input( TxInput::from_utxo(tx_with_fee_id.into(), 0), @@ -540,7 +520,8 @@ fn token_issuance_output_cannot_be_spent(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); - let issuance = make_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let issuance = + make_token_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); let tx = TransactionBuilder::new() .add_input( TxInput::from_utxo(tf.genesis().get_id().into(), 0), @@ -597,12 +578,13 @@ fn mint_unmint_fixed_supply(#[case] seed: Seed) { tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); let total_supply = Amount::from_atoms(rng.gen_range(2..100_000_000)); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Fixed(total_supply), - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Fixed(total_supply), + IsTokenFreezable::No, + ); let amount_to_mint = Amount::from_atoms(rng.gen_range(1..total_supply.into_atoms())); let amount_to_mint_over_limit = (total_supply + Amount::from_atoms(1)).unwrap(); @@ -675,12 +657,20 @@ fn mint_unmint_fixed_supply(#[case] seed: Seed) { nonce = nonce.increment().unwrap(); // Check result - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!(actual_supply, Some(amount_to_mint)); + &ExpectedFungibleTokenData { + issuance: issuance.clone(), + issuance_tx: issuance_tx.clone(), + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); // Unmint more than minted let tx = TransactionBuilder::new() @@ -741,14 +731,19 @@ fn mint_unmint_fixed_supply(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!( - actual_supply, - Some((amount_to_mint - amount_to_unmint).unwrap()) + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some((amount_to_mint - amount_to_unmint).unwrap()), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, ); }); } @@ -764,7 +759,7 @@ fn mint_twice_in_same_tx(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Unlimited, @@ -825,12 +820,13 @@ fn try_unmint_twice_in_same_tx(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Unlimited, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Unlimited, + IsTokenFreezable::No, + ); let amount_to_mint = Amount::from_atoms(rng.gen_range(1..100_000)); let best_block_id = tf.best_block_id(); @@ -844,12 +840,20 @@ fn try_unmint_twice_in_same_tx(#[case] seed: Seed) { true, ); - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!(actual_supply, Some(amount_to_mint)); + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); // Unmint tokens twice let unmint_tx = TransactionBuilder::new() @@ -901,21 +905,25 @@ fn unmint_two_tokens_in_same_tx(#[case] seed: Seed) { let mut rng2 = make_seedable_rng(rng.gen::()); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id_1, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Unlimited, - IsTokenFreezable::No, - ); + let (token1_id, issuance1_block_id, issuance1_tx, issuance1, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Unlimited, + IsTokenFreezable::No, + ); + let issuance2 = + make_token_issuance(&mut rng2, TokenTotalSupply::Unlimited, IsTokenFreezable::No); let best_block_id = tf.best_block_id(); - let (token_id_2, _, utxo_with_change) = issue_token_from_block( - &mut rng, - &mut tf, - best_block_id, - utxo_with_change, - make_issuance(&mut rng2, TokenTotalSupply::Unlimited, IsTokenFreezable::No), - ); + let (token2_id, issuance2_block_id, issuance2_tx, utxo_with_change) = + issue_token_from_block( + &mut rng, + &mut tf, + best_block_id, + utxo_with_change, + issuance2.clone(), + ); let amount_to_mint = Amount::from_atoms(rng.gen_range(1..100_000)); let best_block_id = tf.best_block_id(); @@ -924,7 +932,7 @@ fn unmint_two_tokens_in_same_tx(#[case] seed: Seed) { &mut tf, best_block_id, utxo_with_change, - token_id_1, + token1_id, amount_to_mint, true, ); @@ -935,26 +943,38 @@ fn unmint_two_tokens_in_same_tx(#[case] seed: Seed) { &mut tf, best_block_id, UtxoOutPoint::new(mint_tx_1_id.into(), 1), - token_id_2, + token2_id, amount_to_mint, true, ); - assert_eq!( - TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), - &token_id_1 - ) - .unwrap(), - Some(amount_to_mint) + check_fungible_token( + &tf, + &mut rng, + &token1_id, + &ExpectedFungibleTokenData { + issuance: issuance1, + issuance_tx: issuance1_tx, + issuance_block_id: issuance1_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + false, ); - assert_eq!( - TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), - &token_id_2 - ) - .unwrap(), - Some(amount_to_mint) + check_fungible_token( + &tf, + &mut rng, + &token2_id, + &ExpectedFungibleTokenData { + issuance: issuance2, + issuance_tx: issuance2_tx, + issuance_block_id: issuance2_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + false, ); // Unmint both tokens tokens same tx @@ -962,14 +982,14 @@ fn unmint_two_tokens_in_same_tx(#[case] seed: Seed) { .add_input( TxInput::from_command( AccountNonce::new(1), - AccountCommand::UnmintTokens(token_id_1), + AccountCommand::UnmintTokens(token1_id), ), InputWitness::NoSignature(None), ) .add_input( TxInput::from_command( AccountNonce::new(1), - AccountCommand::UnmintTokens(token_id_2), + AccountCommand::UnmintTokens(token2_id), ), InputWitness::NoSignature(None), ) @@ -986,11 +1006,11 @@ fn unmint_two_tokens_in_same_tx(#[case] seed: Seed) { InputWitness::NoSignature(None), ) .add_output(TxOutput::Burn(OutputValue::TokenV1( - token_id_1, + token1_id, amount_to_mint, ))) .add_output(TxOutput::Burn(OutputValue::TokenV1( - token_id_2, + token2_id, amount_to_mint, ))) .build(); @@ -1026,12 +1046,13 @@ fn mint_unmint_fixed_supply_repeatedly(#[case] seed: Seed) { let mut tf = TestFramework::builder(&mut rng).build(); let total_supply = Amount::from_atoms(rng.gen_range(2..100_000_000)); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Fixed(total_supply), - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Fixed(total_supply), + IsTokenFreezable::No, + ); // Mint all the tokens up to the total supply let best_block_id = tf.best_block_id(); @@ -1211,14 +1232,19 @@ fn mint_unmint_fixed_supply_repeatedly(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - assert_eq!( - tf.storage - .transaction_ro() - .unwrap() - .get_circulating_supply(&token_id) - .unwrap() - .unwrap(), - total_supply + check_fungible_token( + &tf, + &mut rng, + &token_id, + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some(total_supply), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, ); }); } @@ -1236,12 +1262,13 @@ fn mint_unlimited_supply(#[case] seed: Seed) { tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); let amount_to_mint = Amount::from_atoms(rng.gen_range(1..=i128::MAX as u128)); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Unlimited, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Unlimited, + IsTokenFreezable::No, + ); // Mint more than i128::MAX let mint_tx = TransactionBuilder::new() @@ -1305,12 +1332,20 @@ fn mint_unlimited_supply(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!(actual_supply, Some(amount_to_mint)); + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); }); } @@ -1329,12 +1364,13 @@ fn mint_unlimited_supply_max(#[case] seed: Seed) { tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); let max_amount_to_mint = Amount::from_atoms(i128::MAX as u128); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Unlimited, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Unlimited, + IsTokenFreezable::No, + ); // Mint tokens i128::MAX let mint_tx = TransactionBuilder::new() @@ -1364,12 +1400,20 @@ fn mint_unlimited_supply_max(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!(actual_supply, Some(max_amount_to_mint)); + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some(max_amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); { // Try mint one more over i128::MAX @@ -1461,7 +1505,7 @@ fn mint_from_wrong_account(#[case] seed: Seed) { let amount_to_mint = Amount::from_atoms(rng.gen_range(2..SignedAmount::MAX.into_atoms() as u128)); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Lockable, @@ -1532,7 +1576,7 @@ fn try_to_print_money_on_mint(#[case] seed: Seed) { let amount_to_mint = Amount::from_atoms(rng.gen_range(2..SignedAmount::MAX.into_atoms() as u128)); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Unlimited, @@ -1615,12 +1659,13 @@ fn burn_from_total_supply_account(#[case] seed: Seed) { let amount_to_mint = Amount::from_atoms(rng.gen_range(2..1_000_000)); let amount_to_unmint = Amount::from_atoms(rng.gen_range(1..amount_to_mint.into_atoms())); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Lockable, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::No, + ); let best_block_id = tf.best_block_id(); let (_, mint_tx_id) = mint_tokens_in_block( @@ -1663,14 +1708,19 @@ fn burn_from_total_supply_account(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - let circulating_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!( - circulating_supply, - Some((amount_to_mint + amount_to_unmint).unwrap()) + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some((amount_to_mint + amount_to_unmint).unwrap()), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, ); }); } @@ -1690,12 +1740,13 @@ fn burn_from_lock_supply_account(#[case] seed: Seed) { Amount::from_atoms(rng.gen_range(2..SignedAmount::MAX.into_atoms() as u128)); let amount_to_unmint = Amount::from_atoms(rng.gen_range(1..amount_to_mint.into_atoms())); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Lockable, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::No, + ); let best_block_id = tf.best_block_id(); let (_, mint_tx_id) = mint_tokens_in_block( @@ -1736,21 +1787,20 @@ fn burn_from_lock_supply_account(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - let circulating_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), - &token_id, - ) - .unwrap(); - assert_eq!(circulating_supply, Some(amount_to_mint)); - - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - match actual_token_data.unwrap() { - tokens_accounting::TokenData::FungibleToken(data) => assert!(data.is_locked()), - }; + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: true, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); }); } @@ -1768,12 +1818,13 @@ fn burn_zero_tokens_on_unmint(#[case] seed: Seed) { let amount_to_mint = Amount::from_atoms(rng.gen_range(2..SignedAmount::MAX.into_atoms() as u128)); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Unlimited, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Unlimited, + IsTokenFreezable::No, + ); let best_block_id = tf.best_block_id(); let (_, mint_tx_id) = mint_tokens_in_block( @@ -1815,12 +1866,20 @@ fn burn_zero_tokens_on_unmint(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!(actual_supply, Some(amount_to_mint)); + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); }); } @@ -1839,12 +1898,13 @@ fn burn_less_than_input_on_unmint(#[case] seed: Seed) { Amount::from_atoms(rng.gen_range(2..SignedAmount::MAX.into_atoms() as u128)); let amount_to_burn = Amount::from_atoms(rng.gen_range(1..amount_to_mint.into_atoms())); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Unlimited, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Unlimited, + IsTokenFreezable::No, + ); let best_block_id = tf.best_block_id(); let (_, mint_tx_id) = mint_tokens_in_block( @@ -1889,14 +1949,19 @@ fn burn_less_than_input_on_unmint(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!( - actual_supply, - Some((amount_to_mint - amount_to_burn).unwrap()) + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some((amount_to_mint - amount_to_burn).unwrap()), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, ); }); } @@ -1916,7 +1981,7 @@ fn burn_less_by_providing_smaller_input_utxo(#[case] seed: Seed) { Amount::from_atoms(rng.gen_range(2..SignedAmount::MAX.into_atoms() as u128)); let amount_to_unmint = Amount::from_atoms(rng.gen_range(1..amount_to_mint.into_atoms())); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Unlimited, @@ -2016,12 +2081,13 @@ fn unmint_using_multiple_burn_utxos(#[case] seed: Seed) { let amount_to_mint = Amount::from_atoms(rng.gen_range(2..100_000)); let amount_to_unmint = Amount::from_atoms(rng.gen_range(1..amount_to_mint.into_atoms())); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Unlimited, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Unlimited, + IsTokenFreezable::No, + ); let best_block_id = tf.best_block_id(); let (_, mint_tx_id) = mint_tokens_in_block( @@ -2095,14 +2161,19 @@ fn unmint_using_multiple_burn_utxos(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!( - actual_supply, - Some((amount_to_mint - amount_to_unmint).unwrap()) + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some((amount_to_mint - amount_to_unmint).unwrap()), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, ); }); } @@ -2122,12 +2193,13 @@ fn check_lockable_supply(#[case] seed: Seed) { let token_supply_change_fee = tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Lockable, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::No, + ); let amount_to_mint = Amount::from_atoms(rng.gen_range(2..100_000_000)); let amount_to_unmint = Amount::from_atoms(rng.gen_range(1..amount_to_mint.into_atoms())); @@ -2159,21 +2231,20 @@ fn check_lockable_supply(#[case] seed: Seed) { nonce = nonce.increment().unwrap(); // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), - &token_id, - ) - .unwrap(); - match actual_token_data.unwrap() { - tokens_accounting::TokenData::FungibleToken(data) => assert!(!data.is_locked()), - }; - - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!(actual_supply, Some(amount_to_mint)); + &ExpectedFungibleTokenData { + issuance: issuance.clone(), + issuance_tx: issuance_tx.clone(), + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); // Lock the supply let lock_tx = TransactionBuilder::new() @@ -2198,21 +2269,20 @@ fn check_lockable_supply(#[case] seed: Seed) { nonce = nonce.increment().unwrap(); // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), - &token_id, - ) - .unwrap(); - match actual_token_data.unwrap() { - tokens_accounting::TokenData::FungibleToken(data) => assert!(data.is_locked()), - }; - - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!(actual_supply, Some(amount_to_mint)); + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: true, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); // Try to mint some tokens let tx = TransactionBuilder::new() @@ -2292,7 +2362,7 @@ fn try_lock_not_lockable_supply(#[case] seed: Seed, #[case] supply: TokenTotalSu let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id, _, utxo_with_change) = + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis(&mut rng, &mut tf, supply, IsTokenFreezable::No); let result = tf @@ -2336,12 +2406,13 @@ fn try_lock_twice(#[case] seed: Seed) { let token_supply_change_fee = tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Lockable, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::No, + ); // Lock the supply let lock_tx = TransactionBuilder::new() @@ -2363,14 +2434,20 @@ fn try_lock_twice(#[case] seed: Seed) { nonce = nonce.increment().unwrap(); // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - match actual_token_data.unwrap() { - tokens_accounting::TokenData::FungibleToken(data) => assert!(data.is_locked()), - }; + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: None, + is_locked: true, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); // Try lock again let result = tf @@ -2411,7 +2488,7 @@ fn try_lock_twice_in_same_tx(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Lockable, @@ -2465,7 +2542,7 @@ fn lock_two_tokens_in_same_tx(#[case] seed: Seed) { let mut rng2 = make_seedable_rng(rng.gen::()); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id_1, _, utxo_with_change) = issue_token_from_genesis( + let (token_id_1, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Lockable, @@ -2473,12 +2550,12 @@ fn lock_two_tokens_in_same_tx(#[case] seed: Seed) { ); let best_block_id = tf.best_block_id(); - let (token_id_2, _, utxo_with_change) = issue_token_from_block( + let (token_id_2, _, _, utxo_with_change) = issue_token_from_block( &mut rng, &mut tf, best_block_id, utxo_with_change, - make_issuance(&mut rng2, TokenTotalSupply::Lockable, IsTokenFreezable::No), + make_token_issuance(&mut rng2, TokenTotalSupply::Lockable, IsTokenFreezable::No), ); // Lock both tokens tokens same tx @@ -2530,7 +2607,7 @@ fn mint_fee(#[case] seed: Seed) { tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); let some_amount = Amount::from_atoms(rng.gen_range(100..100_000)); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Unlimited, @@ -2625,7 +2702,7 @@ fn unmint_fee(#[case] seed: Seed) { tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); let some_amount = Amount::from_atoms(rng.gen_range(100..100_000)); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Unlimited, @@ -2725,7 +2802,7 @@ fn lock_supply_fee(#[case] seed: Seed) { let token_supply_change_fee = tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Lockable, @@ -2813,12 +2890,13 @@ fn spend_mint_tokens_output(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Lockable, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::No, + ); let amount_to_mint = Amount::from_atoms(rng.gen_range(2..100_000_000)); let amount_to_overspend = (amount_to_mint + Amount::from_atoms(1)).unwrap(); @@ -2848,12 +2926,20 @@ fn spend_mint_tokens_output(#[case] seed: Seed) { .unwrap(); // Check result - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!(actual_supply, Some(amount_to_mint)); + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); // Try to overspend minted amount let tx = TransactionBuilder::new() @@ -2919,7 +3005,8 @@ fn issue_and_mint_same_tx(#[case] seed: Seed) { ) .unwrap(); - let issuance = make_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let issuance = + make_token_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); let tx = TransactionBuilder::new() .add_input( TxInput::from_utxo(genesis_source_id, 0), @@ -2977,8 +3064,9 @@ fn issue_and_mint_same_block(#[case] seed: Seed) { ) .unwrap(); - let issuance = make_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); - let tx_issuance = TransactionBuilder::new() + let issuance = + make_token_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let issuance_tx = TransactionBuilder::new() .with_witnesses( tx_issuance_inputs.iter().map(|_| InputWitness::NoSignature(None)).collect(), ) @@ -2989,11 +3077,11 @@ fn issue_and_mint_same_block(#[case] seed: Seed) { Destination::AnyoneCanSpend, )) .build(); - let tx_issuance_id = tx_issuance.transaction().get_id(); + let issuance_tx_id = issuance_tx.transaction().get_id(); - let tx_minting = TransactionBuilder::new() + let minting_tx = TransactionBuilder::new() .add_input( - TxInput::from_utxo(tx_issuance_id.into(), 1), + TxInput::from_utxo(issuance_tx_id.into(), 1), InputWitness::NoSignature(None), ) .add_input( @@ -3009,25 +3097,28 @@ fn issue_and_mint_same_block(#[case] seed: Seed) { )) .build(); - tf.make_block_builder() - .with_transactions(vec![tx_issuance, tx_minting]) + let issuance_block_id = *tf + .make_block_builder() + .with_transactions(vec![issuance_tx.clone(), minting_tx]) .build_and_process(&mut rng) - .unwrap(); - - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), - &token_id, - ) - .unwrap(); - let expected_token_data = tokens_accounting::TokenData::FungibleToken(issuance.into()); - assert_eq!(actual_token_data, Some(expected_token_data)); + .unwrap() + .unwrap() + .block_id(); - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!(actual_supply, Some(amount_to_mint)); + &ExpectedFungibleTokenData { + issuance, + issuance_tx: issuance_tx.take_transaction(), + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); }); } @@ -3045,7 +3136,7 @@ fn mint_unmint_same_tx(#[case] seed: Seed) { let amount_to_mint = Amount::from_atoms(rng.gen_range(1000..100_000)); let amount_to_unmint = Amount::from_atoms(rng.gen_range(1..amount_to_mint.into_atoms())); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Unlimited, @@ -3093,7 +3184,7 @@ fn mint_unmint_same_tx(#[case] seed: Seed) { let tx_id = tx.transaction().get_id(); let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); - // Check the storage + // Check result assert_eq!( result.unwrap_err(), ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( @@ -3122,14 +3213,15 @@ fn reorg_test_simple(#[case] seed: Seed) { // Create block `a` with token issuance let token_issuance = - make_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); - let (token_id, block_a_id, block_a_change_utxo) = issue_token_from_block( - &mut rng, - &mut tf, - genesis_block_id, - UtxoOutPoint::new(genesis_block_id.into(), 0), - token_issuance.clone(), - ); + make_token_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let (token_id, block_a_id, block_a_issuance_tx, block_a_change_utxo) = + issue_token_from_block( + &mut rng, + &mut tf, + genesis_block_id, + UtxoOutPoint::new(genesis_block_id.into(), 0), + token_issuance.clone(), + ); assert_eq!(tf.best_block_id(), block_a_id); // Create block `b` with token minting @@ -3145,30 +3237,30 @@ fn reorg_test_simple(#[case] seed: Seed) { ); assert_eq!(tf.best_block_id(), block_b_id); - // Check the storage - let actual_data = - tf.storage.transaction_ro().unwrap().read_tokens_accounting_data().unwrap(); - let expected_data = tokens_accounting::TokensAccountingData { - token_data: BTreeMap::from_iter([( - token_id, - tokens_accounting::TokenData::FungibleToken(token_issuance.into()), - )]), - circulating_supply: BTreeMap::from_iter([(token_id, amount_to_mint)]), - }; - assert_eq!(actual_data, expected_data); + let issuance_tx_id = block_a_issuance_tx.get_id(); + + // Check result + check_fungible_token( + &tf, + &mut rng, + &token_id, + &ExpectedFungibleTokenData { + issuance: token_issuance, + issuance_tx: block_a_issuance_tx, + issuance_block_id: block_a_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); // Add blocks from genesis to trigger the reorg let block_e_id = tf.create_chain(&genesis_block_id, 3, &mut rng).unwrap(); assert_eq!(tf.best_block_id(), block_e_id); - // Check the storage - let actual_data = - tf.storage.transaction_ro().unwrap().read_tokens_accounting_data().unwrap(); - let expected_data = tokens_accounting::TokensAccountingData { - token_data: BTreeMap::new(), - circulating_supply: BTreeMap::new(), - }; - assert_eq!(actual_data, expected_data); + // Check result + assert_token_missing(&tf, &token_id, &issuance_tx_id, true); }); } @@ -3217,13 +3309,14 @@ fn reorg_test_2_tokens(#[case] seed: Seed) { let block_a_id = tf.best_block_id(); // Create block `b` with token1 issuance - let (token_id_1, block_b_id, block_b_change_utxo) = issue_token_from_block( - &mut rng, - &mut tf, - block_a_id, - UtxoOutPoint::new(tx_a_id.into(), 0), - make_issuance(&mut rng2, TokenTotalSupply::Unlimited, IsTokenFreezable::No), - ); + let (token_id_1, block_b_id, block_b_issuance_tx, block_b_change_utxo) = + issue_token_from_block( + &mut rng, + &mut tf, + block_a_id, + UtxoOutPoint::new(tx_a_id.into(), 0), + make_token_issuance(&mut rng2, TokenTotalSupply::Unlimited, IsTokenFreezable::No), + ); assert_eq!(tf.best_block_id(), block_b_id); // Create block `c` with token1 minting @@ -3240,14 +3333,15 @@ fn reorg_test_2_tokens(#[case] seed: Seed) { // Create block `d` with another token issuance let issuance_token_2 = - make_issuance(&mut rng, TokenTotalSupply::Lockable, IsTokenFreezable::No); - let (token_id_2, block_d_id, block_d_change_utxo) = issue_token_from_block( - &mut rng, - &mut tf, - block_a_id, - UtxoOutPoint::new(tx_a_id.into(), 1), - issuance_token_2.clone(), - ); + make_token_issuance(&mut rng, TokenTotalSupply::Lockable, IsTokenFreezable::No); + let (token_id_2, block_d_id, block_d_issuance_tx, block_d_change_utxo) = + issue_token_from_block( + &mut rng, + &mut tf, + block_a_id, + UtxoOutPoint::new(tx_a_id.into(), 1), + issuance_token_2.clone(), + ); // No reorg assert_eq!(tf.best_block_id(), block_c_id); @@ -3270,17 +3364,22 @@ fn reorg_test_2_tokens(#[case] seed: Seed) { tf.process_block(block_f, BlockSource::Local).unwrap(); assert_eq!(tf.best_block_id(), block_f_id); - // Check the storage - let actual_data = - tf.storage.transaction_ro().unwrap().read_tokens_accounting_data().unwrap(); - let expected_data = tokens_accounting::TokensAccountingData { - token_data: BTreeMap::from_iter([( - token_id_2, - tokens_accounting::TokenData::FungibleToken(issuance_token_2.into()), - )]), - circulating_supply: BTreeMap::from_iter([(token_id_2, amount_to_mint_2)]), - }; - assert_eq!(actual_data, expected_data); + // Check result + assert_token_missing(&tf, &token_id_1, &block_b_issuance_tx.get_id(), false); + check_fungible_token( + &tf, + &mut rng, + &token_id_2, + &ExpectedFungibleTokenData { + issuance: issuance_token_2, + issuance_tx: block_d_issuance_tx, + issuance_block_id: block_d_id, + circulating_supply: Some(amount_to_mint_2), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); }); } @@ -3309,7 +3408,7 @@ fn check_signature_on_mint(#[case] seed: Seed) { is_freezable: IsTokenFreezable::No, }); - let (token_id, _, utxo_with_change) = issue_token_from_block( + let (token_id, _, _, utxo_with_change) = issue_token_from_block( &mut rng, &mut tf, genesis_block_id.into(), @@ -3446,7 +3545,7 @@ fn check_signature_on_unmint(#[case] seed: Seed) { is_freezable: IsTokenFreezable::No, }); - let (token_id, _, utxo_with_change) = issue_token_from_block( + let (token_id, _, _, utxo_with_change) = issue_token_from_block( &mut rng, &mut tf, genesis_block_id.into(), @@ -3656,7 +3755,7 @@ fn check_signature_on_lock_supply(#[case] seed: Seed) { is_freezable: IsTokenFreezable::No, }); - let (token_id, _, utxo_with_change) = issue_token_from_block( + let (token_id, _, _, utxo_with_change) = issue_token_from_block( &mut rng, &mut tf, genesis_block_id.into(), @@ -3777,12 +3876,13 @@ fn mint_with_timelock(#[case] seed: Seed) { let amount_to_mint = Amount::from_atoms(rng.gen_range(2..SignedAmount::MAX.into_atoms() as u128)); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Lockable, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::No, + ); // Mint with locked output let mint_tx = TransactionBuilder::new() @@ -3813,12 +3913,20 @@ fn mint_with_timelock(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - let actual_supply = TokensAccountingStorageRead::get_circulating_supply( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - assert_eq!(actual_supply, Some(amount_to_mint)); + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); // Try spend tokens at once let token_mint_outpoint = UtxoOutPoint::new(mint_tx_id.into(), 0); @@ -3927,7 +4035,7 @@ fn only_ascii_alphanumeric_after_v1(#[case] seed: Seed) { CheckBlockError::CheckTransactionFailed( CheckBlockTransactionsError::CheckTransactionError( CheckTransactionError::TokensError(TokensError::IssueError( - TokenIssuanceError::IssueErrorTickerHasNoneAlphaNumericChar, + TokenIssuanceError::IssueErrorTickerHasNonAlphaNumericChar, tx_id, )) ) @@ -3974,7 +4082,7 @@ fn token_issue_mint_and_data_deposit_not_enough_fee(#[case] seed: Seed) { let data_deposit_fee = tf.chainstate.get_chain_config().data_deposit_fee(BlockHeight::zero()); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Lockable, @@ -4011,7 +4119,8 @@ fn token_issue_mint_and_data_deposit_not_enough_fee(#[case] seed: Seed) { let amount_to_mint = Amount::from_atoms(rng.gen_range(2..100_000_000)); - let issuance = make_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let issuance = + make_token_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); let tx = TransactionBuilder::new() .add_input( TxInput::from_utxo(tx_with_fee_id.into(), 0), @@ -4078,12 +4187,13 @@ fn check_freezable_supply(#[case] seed: Seed) { let token_supply_change_fee = tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Lockable, - IsTokenFreezable::Yes, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::Yes, + ); // Mint some tokens let amount_to_mint = Amount::from_atoms(rng.gen_range(1..100_000_000)); @@ -4099,14 +4209,20 @@ fn check_freezable_supply(#[case] seed: Seed) { ); // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - match actual_token_data.unwrap() { - tokens_accounting::TokenData::FungibleToken(data) => assert!(!data.is_frozen()), - }; + &ExpectedFungibleTokenData { + issuance: issuance.clone(), + issuance_tx: issuance_tx.clone(), + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::Yes), + }, + true, + ); // Freeze the token let freeze_tx = TransactionBuilder::new() @@ -4133,14 +4249,20 @@ fn check_freezable_supply(#[case] seed: Seed) { .unwrap(); // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - match actual_token_data.unwrap() { - tokens_accounting::TokenData::FungibleToken(data) => assert!(data.is_frozen()), - }; + &ExpectedFungibleTokenData { + issuance: issuance.clone(), + issuance_tx: issuance_tx.clone(), + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::Yes(IsTokenUnfreezable::Yes), + }, + true, + ); // Try to mint some tokens @@ -4410,14 +4532,20 @@ fn check_freezable_supply(#[case] seed: Seed) { .unwrap(); // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - match actual_token_data.unwrap() { - tokens_accounting::TokenData::FungibleToken(data) => assert!(!data.is_frozen()), - }; + &ExpectedFungibleTokenData { + issuance: issuance.clone(), + issuance_tx: issuance_tx.clone(), + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::Yes), + }, + true, + ); // Now all operations are available again. Try mint/transfer tf.make_block_builder() @@ -4458,12 +4586,13 @@ fn check_freeze_unfreeze_takes_effect_after_submit(#[case] seed: Seed) { let token_supply_change_fee = tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Lockable, - IsTokenFreezable::Yes, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::Yes, + ); // Mint some tokens let amount_to_mint = Amount::from_atoms(rng.gen_range(1..100_000_000)); @@ -4511,14 +4640,20 @@ fn check_freeze_unfreeze_takes_effect_after_submit(#[case] seed: Seed) { .unwrap(); // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - match actual_token_data.unwrap() { - tokens_accounting::TokenData::FungibleToken(data) => assert!(data.is_frozen()), - }; + &ExpectedFungibleTokenData { + issuance: issuance.clone(), + issuance_tx: issuance_tx.clone(), + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::Yes(IsTokenUnfreezable::Yes), + }, + true, + ); // Try unfreeze the token and transfer at the same tx let result = tf @@ -4584,14 +4719,20 @@ fn check_freeze_unfreeze_takes_effect_after_submit(#[case] seed: Seed) { .unwrap(); // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - match actual_token_data.unwrap() { - tokens_accounting::TokenData::FungibleToken(data) => assert!(!data.is_frozen()), - }; + &ExpectedFungibleTokenData { + issuance, + issuance_tx, + issuance_block_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::Yes), + }, + true, + ); // Now all operations are available again. Try mint/transfer tf.make_block_builder() @@ -4630,7 +4771,7 @@ fn token_freeze_fee(#[case] seed: Seed) { let ok_fee = tf.chainstate.get_chain_config().token_freeze_fee(BlockHeight::zero()); let not_ok_fee = (ok_fee - Amount::from_atoms(1)).unwrap(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Unlimited, @@ -4717,7 +4858,7 @@ fn token_unfreeze_fee(#[case] seed: Seed) { let ok_fee = tf.chainstate.get_chain_config().token_freeze_fee(BlockHeight::zero()); let not_ok_fee = (ok_fee - Amount::from_atoms(1)).unwrap(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Unlimited, @@ -4849,7 +4990,7 @@ fn check_signature_on_freeze_unfreeze(#[case] seed: Seed) { is_freezable: IsTokenFreezable::Yes, }); - let (token_id, _, utxo_with_change) = issue_token_from_block( + let (token_id, _, _, utxo_with_change) = issue_token_from_block( &mut rng, &mut tf, genesis_block_id.into(), @@ -5048,7 +5189,7 @@ fn check_signature_on_change_authority(#[case] seed: Seed) { is_freezable: IsTokenFreezable::No, }); - let (token_id, _, utxo_with_change) = issue_token_from_block( + let (token_id, _, _, utxo_with_change) = issue_token_from_block( &mut rng, &mut tf, genesis_block_id.into(), @@ -5252,23 +5393,30 @@ fn check_change_authority(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Lockable, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::No, + ); - let original_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + let issuance_v1 = + assert_matches_return_val!(issuance.clone(), TokenIssuance::V1(issuance), issuance); + assert_eq!(issuance_v1.authority, Destination::AnyoneCanSpend); + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - let tokens_accounting::TokenData::FungibleToken(original_token_data) = - original_token_data.unwrap(); - assert_eq!( - original_token_data.authority(), - &Destination::AnyoneCanSpend + &ExpectedFungibleTokenData { + issuance, + issuance_tx: issuance_tx.clone(), + issuance_block_id, + circulating_supply: None, + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, ); let (_, some_pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); @@ -5293,14 +5441,27 @@ fn check_change_authority(#[case] seed: Seed) { .unwrap(); // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - let tokens_accounting::TokenData::FungibleToken(actual_token_data) = - actual_token_data.unwrap(); - assert_eq!(actual_token_data.authority(), &new_authority); + &ExpectedFungibleTokenData { + issuance: TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: issuance_v1.token_ticker, + number_of_decimals: issuance_v1.number_of_decimals, + metadata_uri: issuance_v1.metadata_uri, + total_supply: issuance_v1.total_supply, + authority: new_authority, + is_freezable: issuance_v1.is_freezable, + }), + issuance_tx, + issuance_block_id, + circulating_supply: None, + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); }); } @@ -5312,23 +5473,30 @@ fn check_change_authority_twice(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Lockable, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::No, + ); - let original_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + let issuance_v1 = + assert_matches_return_val!(issuance.clone(), TokenIssuance::V1(issuance), issuance); + assert_eq!(issuance_v1.authority, Destination::AnyoneCanSpend); + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - let tokens_accounting::TokenData::FungibleToken(original_token_data) = - original_token_data.unwrap(); - assert_eq!( - original_token_data.authority(), - &Destination::AnyoneCanSpend + &ExpectedFungibleTokenData { + issuance, + issuance_tx: issuance_tx.clone(), + issuance_block_id, + circulating_supply: None, + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, ); let (_, pk_1) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); @@ -5384,12 +5552,13 @@ fn check_change_authority_for_frozen_token(#[case] seed: Seed) { let change_authority_fee = tf.chain_config().token_change_authority_fee(BlockHeight::zero()); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Unlimited, - IsTokenFreezable::Yes, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Unlimited, + IsTokenFreezable::Yes, + ); // Freeze the token let freeze_token_tx = TransactionBuilder::new() @@ -5489,15 +5658,31 @@ fn check_change_authority_for_frozen_token(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); + let issuance_v1 = + assert_matches_return_val!(issuance.clone(), TokenIssuance::V1(issuance), issuance); + // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - let tokens_accounting::TokenData::FungibleToken(actual_token_data) = - actual_token_data.unwrap(); - assert_eq!(actual_token_data.authority(), &new_authority); + &ExpectedFungibleTokenData { + issuance: TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: issuance_v1.token_ticker, + number_of_decimals: issuance_v1.number_of_decimals, + metadata_uri: issuance_v1.metadata_uri, + total_supply: issuance_v1.total_supply, + authority: new_authority, + is_freezable: issuance_v1.is_freezable, + }), + issuance_tx, + issuance_block_id, + circulating_supply: None, + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::Yes), + }, + true, + ); }); } @@ -5512,7 +5697,7 @@ fn change_authority_fee(#[case] seed: Seed) { let token_change_authority_fee = tf.chainstate.get_chain_config().token_change_authority_fee(BlockHeight::zero()); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Lockable, @@ -5620,7 +5805,7 @@ fn reorg_tokens_tx_with_simple_tx(#[case] seed: Seed) { .build(); let transfer_tx_id = transfer_tx.transaction().get_id(); - let issuance = make_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let issuance = make_token_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); let issue_token_tx = TransactionBuilder::new() .add_input( TxInput::from_utxo(transfer_tx_id.into(), 0), @@ -5679,7 +5864,7 @@ fn issue_same_token_alternative_pos_chain(#[case] seed: Seed) { tf.chainstate.get_chain_config().token_supply_change_fee(BlockHeight::zero()); //issue a token - let issuance = make_issuance( + let issuance = make_token_issuance( &mut rng, TokenTotalSupply::Fixed(Amount::from_atoms(100)), IsTokenFreezable::No, @@ -5812,12 +5997,13 @@ fn check_change_metadata_uri(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Lockable, - IsTokenFreezable::No, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::No, + ); // too large metadata let max_len = tf.chain_config().token_max_uri_len(); @@ -5885,15 +6071,31 @@ fn check_change_metadata_uri(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); + let issuance_v1 = + assert_matches_return_val!(issuance.clone(), TokenIssuance::V1(issuance), issuance); + // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - let tokens_accounting::TokenData::FungibleToken(actual_token_data) = - actual_token_data.unwrap(); - assert_eq!(actual_token_data.metadata_uri(), &new_metadata_uri); + &ExpectedFungibleTokenData { + issuance: TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: issuance_v1.token_ticker, + number_of_decimals: issuance_v1.number_of_decimals, + metadata_uri: new_metadata_uri, + total_supply: issuance_v1.total_supply, + authority: issuance_v1.authority, + is_freezable: issuance_v1.is_freezable, + }), + issuance_tx, + issuance_block_id, + circulating_supply: None, + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); }); } @@ -5908,12 +6110,13 @@ fn check_change_metadata_for_frozen_token(#[case] seed: Seed) { let unfreeze_fee = tf.chain_config().token_freeze_fee(BlockHeight::zero()); let change_metadata_fee = tf.chain_config().token_change_metadata_uri_fee(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( - &mut rng, - &mut tf, - TokenTotalSupply::Unlimited, - IsTokenFreezable::Yes, - ); + let (token_id, issuance_block_id, issuance_tx, issuance, utxo_with_change) = + issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Unlimited, + IsTokenFreezable::Yes, + ); // Freeze the token let freeze_token_tx = TransactionBuilder::new() @@ -6019,15 +6222,31 @@ fn check_change_metadata_for_frozen_token(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); + let issuance_v1 = + assert_matches_return_val!(issuance.clone(), TokenIssuance::V1(issuance), issuance); + // Check result - let actual_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - let tokens_accounting::TokenData::FungibleToken(actual_token_data) = - actual_token_data.unwrap(); - assert_eq!(actual_token_data.metadata_uri(), &new_metadata_uri); + &ExpectedFungibleTokenData { + issuance: TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: issuance_v1.token_ticker, + number_of_decimals: issuance_v1.number_of_decimals, + metadata_uri: new_metadata_uri, + total_supply: issuance_v1.total_supply, + authority: issuance_v1.authority, + is_freezable: issuance_v1.is_freezable, + }), + issuance_tx, + issuance_block_id, + circulating_supply: None, + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::Yes), + }, + true, + ); }); } @@ -6042,14 +6261,15 @@ fn reorg_metadata_uri_change(#[case] seed: Seed) { // Create block `a` with token issuance let token_issuance = - make_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); - let (token_id, block_a_id, block_a_change_utxo) = issue_token_from_block( - &mut rng, - &mut tf, - genesis_block_id, - UtxoOutPoint::new(genesis_block_id.into(), 0), - token_issuance.clone(), - ); + make_token_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let (token_id, block_a_id, block_a_issuance_tx, block_a_change_utxo) = + issue_token_from_block( + &mut rng, + &mut tf, + genesis_block_id, + UtxoOutPoint::new(genesis_block_id.into(), 0), + token_issuance.clone(), + ); assert_eq!(tf.best_block_id(), block_a_id); // Create block `b` with token minting @@ -6065,18 +6285,38 @@ fn reorg_metadata_uri_change(#[case] seed: Seed) { ); assert_eq!(tf.best_block_id(), block_b_id); - let original_token_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + let issuance_v1 = assert_matches_return_val!( + token_issuance.clone(), + TokenIssuance::V1(issuance), + issuance + ); + + check_fungible_token( + &tf, + &mut rng, &token_id, - ) - .unwrap(); - let tokens_accounting::TokenData::FungibleToken(original_token_data) = - original_token_data.unwrap(); - let original_metadata_uri = original_token_data.metadata_uri().to_owned(); + &ExpectedFungibleTokenData { + issuance: token_issuance.clone(), + issuance_tx: block_a_issuance_tx.clone(), + issuance_block_id: block_a_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); // Create block `c` which changes token metadata uri - let new_metadata_uri = - random_ascii_alphanumeric_string(&mut rng, 1..1024).as_bytes().to_vec(); + let new_metadata_uri = (|| { + for _ in 0..100 { + let new_uri = + random_ascii_alphanumeric_string(&mut rng, 1..1024).as_bytes().to_vec(); + if new_uri != issuance_v1.metadata_uri { + return new_uri; + } + } + panic!("Cannot create distinct metadata uri"); + })(); tf.make_block_builder() .add_transaction( TransactionBuilder::new() @@ -6099,37 +6339,48 @@ fn reorg_metadata_uri_change(#[case] seed: Seed) { .build_and_process(&mut rng) .unwrap(); - // Check the storage - let tokens_accounting::TokenData::FungibleToken(actual_new_token_data) = tf - .storage - .transaction_ro() - .unwrap() - .read_tokens_accounting_data() - .unwrap() - .token_data - .get(&token_id) - .cloned() - .unwrap(); - let actual_new_metadata_uri = actual_new_token_data.metadata_uri(); - assert_eq!(actual_new_metadata_uri, new_metadata_uri); + // Check result + check_fungible_token( + &tf, + &mut rng, + &token_id, + &ExpectedFungibleTokenData { + issuance: TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: issuance_v1.token_ticker, + number_of_decimals: issuance_v1.number_of_decimals, + metadata_uri: new_metadata_uri, + total_supply: issuance_v1.total_supply, + authority: issuance_v1.authority, + is_freezable: issuance_v1.is_freezable, + }), + issuance_tx: block_a_issuance_tx.clone(), + issuance_block_id: block_a_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); // Add blocks from genesis to trigger the reorg let block_e_id = tf.create_chain((&block_b_id).into(), 2, &mut rng).unwrap(); assert_eq!(tf.best_block_id(), block_e_id); - // Check the storage - let tokens_accounting::TokenData::FungibleToken(actual_token_data) = tf - .storage - .transaction_ro() - .unwrap() - .read_tokens_accounting_data() - .unwrap() - .token_data - .get(&token_id) - .cloned() - .unwrap(); - let actual_metadata_uri = actual_token_data.metadata_uri(); - assert_eq!(actual_metadata_uri, original_metadata_uri); + // Check result + check_fungible_token( + &tf, + &mut rng, + &token_id, + &ExpectedFungibleTokenData { + issuance: token_issuance, + issuance_tx: block_a_issuance_tx, + issuance_block_id: block_a_id, + circulating_supply: Some(amount_to_mint), + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + true, + ); }); } @@ -6169,7 +6420,7 @@ fn test_change_metadata_uri_activation(#[case] seed: Seed) { ) .build(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Lockable, @@ -6262,7 +6513,7 @@ fn only_authority_can_change_metadata_uri(#[case] seed: Seed) { is_freezable: IsTokenFreezable::No, }); - let (token_id, _, utxo_with_change) = issue_token_from_block( + let (token_id, _, _, utxo_with_change) = issue_token_from_block( &mut rng, &mut tf, genesis_block_id.into(), @@ -6414,7 +6665,7 @@ fn token_id_generation_v1_activation(#[case] seed: Seed) { .build(); // Create a token just so that there is an account to use in inputs - let (token_id_0, _, utxo_with_change) = issue_token_from_genesis( + let (token_id_0, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Lockable, @@ -6422,7 +6673,8 @@ fn token_id_generation_v1_activation(#[case] seed: Seed) { ); // Create a token from account input - let issuance = make_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let issuance = + make_token_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); let token_issuance_fee = tf.chainstate.get_chain_config().fungible_token_issuance_fee(); let token_change_authority_fee = tf.chainstate.get_chain_config().token_change_authority_fee(BlockHeight::zero()); @@ -6453,17 +6705,28 @@ fn token_id_generation_v1_activation(#[case] seed: Seed) { ) .unwrap(); - tf.make_block_builder() - .add_transaction(tx1) + let tx1_block_id = *tf + .make_block_builder() + .add_transaction(tx1.clone()) .build_and_process(&mut rng) - .unwrap(); + .unwrap() + .unwrap() + .block_id(); - let token_1_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token1_id, - ) - .unwrap(); - assert!(token_1_data.is_some()); + &ExpectedFungibleTokenData { + issuance: issuance.clone(), + issuance_tx: tx1.take_transaction(), + issuance_block_id: tx1_block_id, + circulating_supply: None, + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + false, + ); assert_eq!(token1_id, TokenId::from_tx_input(&tx1_first_input)); assert_ne!(token1_id, TokenId::from_tx_input(&tx1_first_utxo_input)); @@ -6512,24 +6775,31 @@ fn token_id_generation_v1_activation(#[case] seed: Seed) { ); assert_ne!(token2_id, token2_id_before_fork); - tf.make_block_builder() - .add_transaction(tx2) + let tx2_block_id = *tf + .make_block_builder() + .add_transaction(tx2.clone()) .build_and_process(&mut rng) - .unwrap(); + .unwrap() + .unwrap() + .block_id(); - let token_2_data = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), + check_fungible_token( + &tf, + &mut rng, &token2_id, - ) - .unwrap(); - assert!(token_2_data.is_some()); + &ExpectedFungibleTokenData { + issuance, + issuance_tx: tx2.take_transaction(), + issuance_block_id: tx2_block_id, + circulating_supply: None, + is_locked: false, + is_frozen: IsTokenFrozen::No(IsTokenFreezable::No), + }, + false, + ); - let token_2_data_for_invalid_token_id = TokensAccountingStorageRead::get_token_data( - &tf.storage.transaction_ro().unwrap(), - &token2_id_before_fork, - ) - .unwrap(); - assert!(token_2_data_for_invalid_token_id.is_none()); + let bogus_tx_id = Id::::random_using(&mut rng); + assert_token_missing(&tf, &token2_id_before_fork, &bogus_tx_id, false); }); } @@ -6543,7 +6813,7 @@ fn zero_amount_transfer(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Unlimited, @@ -6575,7 +6845,7 @@ fn zero_amount_transfer_of_frozen_token(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let mut tf = TestFramework::builder(&mut rng).build(); - let (token_id, _, utxo_with_change) = issue_token_from_genesis( + let (token_id, _, _, _, utxo_with_change) = issue_token_from_genesis( &mut rng, &mut tf, TokenTotalSupply::Unlimited, diff --git a/chainstate/test-suite/src/tests/helpers/mod.rs b/chainstate/test-suite/src/tests/helpers/mod.rs index 5d2de2cc9..47be1efb7 100644 --- a/chainstate/test-suite/src/tests/helpers/mod.rs +++ b/chainstate/test-suite/src/tests/helpers/mod.rs @@ -29,6 +29,7 @@ pub mod block_creation_helpers; pub mod block_status_helpers; pub mod in_memory_storage_wrapper; pub mod pos; +pub mod token_checks; /// Adds a block with the locked output and returns input corresponding to this output. pub fn add_block_with_locked_output( diff --git a/chainstate/test-suite/src/tests/helpers/token_checks.rs b/chainstate/test-suite/src/tests/helpers/token_checks.rs new file mode 100644 index 000000000..469a6f313 --- /dev/null +++ b/chainstate/test-suite/src/tests/helpers/token_checks.rs @@ -0,0 +1,434 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::BTreeSet; + +use chainstate::{ChainstateError, PropertyQueryError}; +use chainstate_storage::{BlockchainStorageRead as _, Transactional}; +use chainstate_test_framework::TestFramework; +use common::{ + chain::{ + make_token_id, + tokens::{ + IsTokenFrozen, Metadata, NftIssuance, NftIssuanceV0, RPCFungibleTokenInfo, + RPCNonFungibleTokenInfo, RPCTokenInfo, TokenAuxiliaryData, TokenId, TokenIssuance, + TokenIssuanceV1, + }, + Block, Transaction, TxOutput, + }, + primitives::{Amount, Id, Idable}, +}; +use randomness::{CryptoRng, Rng}; +use test_utils::assert_matches_return_val; +use tokens_accounting::{FungibleTokenData, TokensAccountingStorageRead}; + +pub struct ExpectedFungibleTokenData { + pub issuance: TokenIssuance, + pub issuance_tx: Transaction, + pub issuance_block_id: Id, + pub circulating_supply: Option, + pub is_locked: bool, + pub is_frozen: IsTokenFrozen, +} + +pub fn make_expected_rpc_token_info_from_token_issuance( + token_id: TokenId, + issuance: &TokenIssuanceV1, + circulating_supply: Amount, + is_locked: bool, + is_frozen: IsTokenFrozen, +) -> RPCTokenInfo { + RPCTokenInfo::FungibleToken(RPCFungibleTokenInfo { + token_id, + token_ticker: issuance.token_ticker.clone().into(), + number_of_decimals: issuance.number_of_decimals, + metadata_uri: issuance.metadata_uri.clone().into(), + circulating_supply, + total_supply: issuance.total_supply.into(), + is_locked, + frozen: is_frozen.into(), + authority: issuance.authority.clone(), + }) +} + +pub fn check_fungible_token( + tf: &TestFramework, + rng: &mut (impl Rng + CryptoRng), + token_id: &TokenId, + expected_data: &ExpectedFungibleTokenData, + no_other_tokens_present: bool, +) { + let issuance_tx_id = expected_data.issuance_tx.get_id(); + let block = tf.block(expected_data.issuance_block_id); + let block_index = tf.block_index(&expected_data.issuance_block_id); + // Ensure the block actually has the transaction + block + .transactions() + .iter() + .find(|tx| tx.transaction().get_id() == issuance_tx_id) + .unwrap(); + let expected_token_id = make_token_id( + tf.chain_config(), + block_index.block_height(), + expected_data.issuance_tx.inputs(), + ) + .unwrap(); + assert_eq!(token_id, &expected_token_id); + + let (expected_info_for_rpc, expected_token_data) = match &expected_data.issuance { + TokenIssuance::V1(issuance) => { + let expected_info_for_rpc = make_expected_rpc_token_info_from_token_issuance( + *token_id, + issuance, + expected_data.circulating_supply.unwrap_or(Amount::ZERO), + expected_data.is_locked, + expected_data.is_frozen, + ); + + let expected_token_data = + tokens_accounting::TokenData::FungibleToken(FungibleTokenData::new_unchecked( + issuance.token_ticker.clone(), + issuance.number_of_decimals, + issuance.metadata_uri.clone(), + issuance.total_supply, + expected_data.is_locked, + expected_data.is_frozen, + issuance.authority.clone(), + )); + + (expected_info_for_rpc, expected_token_data) + } + }; + let expected_infos_for_rpc = vec![expected_info_for_rpc.clone()]; + + let random_token_id = TokenId::random_using(rng); + + // Check ChainstateInterface::get_token_info_for_rpc + let actual_info_for_rpc = tf.chainstate.get_token_info_for_rpc(*token_id).unwrap().unwrap(); + assert_eq!(actual_info_for_rpc, expected_info_for_rpc); + assert_eq!( + tf.chainstate.get_token_info_for_rpc(random_token_id).unwrap(), + None + ); + + // Check ChainstateInterface::get_tokens_info_for_rpc + let actual_infos_for_rpc = tf + .chainstate + .get_tokens_info_for_rpc(&BTreeSet::from_iter([*token_id])) + .unwrap(); + assert_eq!(actual_infos_for_rpc, expected_infos_for_rpc); + assert_eq!( + tf.chainstate.get_tokens_info_for_rpc(&BTreeSet::new()).unwrap(), + vec![] + ); + assert_eq!( + tf.chainstate + .get_tokens_info_for_rpc(&BTreeSet::from_iter([*token_id, random_token_id])) + .unwrap_err(), + ChainstateError::FailedToReadProperty(PropertyQueryError::TokenInfoMissing( + random_token_id + )) + ); + + // Check ChainstateInterface::get_token_aux_data; currently it's only stored for NFTs, + // so the result should be None. + assert_eq!(tf.chainstate.get_token_aux_data(*token_id).unwrap(), None); + + // Check ChainstateInterface::get_token_id_from_issuance_tx; this only works for NFTs too. + assert_eq!( + tf.chainstate.get_token_id_from_issuance_tx(&issuance_tx_id).unwrap(), + None + ); + + // Check ChainstateInterface::get_token_data + let actual_token_data = tf.chainstate.get_token_data(token_id).unwrap().unwrap(); + assert_eq!(actual_token_data, expected_token_data); + assert_eq!( + tf.chainstate.get_token_data(&random_token_id).unwrap(), + None + ); + + // Check ChainstateInterface::get_token_circulating_supply + let actual_circulating_supply = tf.chainstate.get_token_circulating_supply(token_id).unwrap(); + assert_eq!(actual_circulating_supply, expected_data.circulating_supply); + assert_eq!( + tf.chainstate.get_token_circulating_supply(&random_token_id).unwrap(), + None + ); + + // Check the storage directly + { + let storage_tx = tf.storage.transaction_ro().unwrap(); + + let actual_token_data = storage_tx.get_token_data(token_id).unwrap().unwrap(); + assert_eq!(actual_token_data, expected_token_data); + assert_eq!(storage_tx.get_token_data(&random_token_id).unwrap(), None); + + let actual_circulating_supply = storage_tx.get_circulating_supply(token_id).unwrap(); + assert_eq!(actual_circulating_supply, expected_data.circulating_supply); + assert_eq!( + storage_tx.get_circulating_supply(&random_token_id).unwrap(), + None + ); + + let tokens_acc_data = storage_tx.read_tokens_accounting_data().unwrap(); + let token_data_from_acc_data = tokens_acc_data.token_data.get(token_id).unwrap(); + assert_eq!(token_data_from_acc_data, &expected_token_data); + let circulating_supply_from_acc_data = tokens_acc_data.circulating_supply.get(token_id); + assert_eq!( + circulating_supply_from_acc_data, + expected_data.circulating_supply.as_ref() + ); + + if no_other_tokens_present { + assert_eq!(tokens_acc_data.token_data.len(), 1); + assert_eq!( + tokens_acc_data.circulating_supply.len(), + if expected_data.circulating_supply.is_some() { + 1 + } else { + 0 + } + ); + } + + // These correspond to ChainstateInterface::get_token_aux_data and + // ChainstateInterface::get_token_id_from_issuance_tx respectively. + assert_eq!(storage_tx.get_token_aux_data(token_id).unwrap(), None); + assert_eq!(storage_tx.get_token_id(&issuance_tx_id).unwrap(), None); + } +} + +pub struct ExpectedNftData { + pub metadata: Metadata, + pub issuance_tx: Transaction, + pub issuance_tx_output_index: u32, + pub issuance_block_id: Id, +} + +pub fn make_expected_rpc_token_info_from_nft_metadata( + token_id: TokenId, + issuance_tx_id: Id, + issuance_block_id: Id, + metadata: &Metadata, +) -> RPCTokenInfo { + RPCTokenInfo::NonFungibleToken(Box::new(RPCNonFungibleTokenInfo { + token_id, + creation_tx_id: issuance_tx_id, + creation_block_id: issuance_block_id, + metadata: metadata.into(), + })) +} + +pub fn check_nft( + tf: &TestFramework, + rng: &mut (impl Rng + CryptoRng), + token_id: &TokenId, + expected_data: &ExpectedNftData, +) { + let issuance_tx_id = expected_data.issuance_tx.get_id(); + let block = tf.block(expected_data.issuance_block_id); + let block_index = tf.block_index(&expected_data.issuance_block_id); + // Ensure the block actually has the transaction + block + .transactions() + .iter() + .find(|tx| tx.transaction().get_id() == issuance_tx_id) + .unwrap(); + let expected_token_id = make_token_id( + tf.chain_config(), + block_index.block_height(), + expected_data.issuance_tx.inputs(), + ) + .unwrap(); + assert_eq!(token_id, &expected_token_id); + + let (token_id_in_txo, issuance_in_txo, _dest_in_txo) = assert_matches_return_val!( + &expected_data.issuance_tx.outputs()[expected_data.issuance_tx_output_index as usize], + TxOutput::IssueNft(id, issuance, dest), + (id, issuance, dest) + ); + let issuance_v0_in_txo = assert_matches_return_val!( + issuance_in_txo.as_ref(), + NftIssuance::V0(issuance), + issuance + ); + assert_eq!(token_id_in_txo, token_id); + assert_eq!( + issuance_v0_in_txo, + &NftIssuanceV0 { + metadata: expected_data.metadata.clone() + } + ); + + let expected_info_for_rpc = make_expected_rpc_token_info_from_nft_metadata( + *token_id, + issuance_tx_id, + expected_data.issuance_block_id, + &expected_data.metadata, + ); + let expected_infos_for_rpc = vec![expected_info_for_rpc.clone()]; + let expected_aux_data = TokenAuxiliaryData::new( + expected_data.issuance_tx.clone(), + expected_data.issuance_block_id, + ); + + let random_token_id = TokenId::random_using(rng); + let random_tx_id = Id::::random_using(rng); + + // Check ChainstateInterface::get_token_info_for_rpc + let actual_info_for_rpc = tf.chainstate.get_token_info_for_rpc(*token_id).unwrap().unwrap(); + assert_eq!(actual_info_for_rpc, expected_info_for_rpc); + assert_eq!( + tf.chainstate.get_token_info_for_rpc(random_token_id).unwrap(), + None + ); + + // Check ChainstateInterface::get_tokens_info_for_rpc + let actual_infos_for_rpc = tf + .chainstate + .get_tokens_info_for_rpc(&BTreeSet::from_iter([*token_id])) + .unwrap(); + assert_eq!(actual_infos_for_rpc, expected_infos_for_rpc); + assert_eq!( + tf.chainstate.get_tokens_info_for_rpc(&BTreeSet::new()).unwrap(), + vec![] + ); + assert_eq!( + tf.chainstate + .get_tokens_info_for_rpc(&BTreeSet::from_iter([*token_id, random_token_id])) + .unwrap_err(), + ChainstateError::FailedToReadProperty(PropertyQueryError::TokenInfoMissing( + random_token_id + )) + ); + + // Check ChainstateInterface::get_token_aux_data + let actual_aux_data = tf.chainstate.get_token_aux_data(*token_id).unwrap().unwrap(); + assert_eq!(actual_aux_data, expected_aux_data); + assert_eq!( + tf.chainstate.get_token_aux_data(random_token_id).unwrap(), + None + ); + + // Check ChainstateInterface::get_token_id_from_issuance_tx + let token_id_from_issuance_tx = tf + .chainstate + .get_token_id_from_issuance_tx(&expected_data.issuance_tx.get_id()) + .unwrap() + .unwrap(); + assert_eq!(token_id_from_issuance_tx, *token_id); + assert_eq!( + tf.chainstate.get_token_id_from_issuance_tx(&random_tx_id).unwrap(), + None + ); + + // Check ChainstateInterface::get_token_data - this is only available for fungible tokens + // currently, so the result should be None. + assert_eq!(tf.chainstate.get_token_data(token_id).unwrap(), None); + + // Check ChainstateInterface::get_token_circulating_supply - this is only available for + // fungible tokens. + assert_eq!( + tf.chainstate.get_token_circulating_supply(token_id).unwrap(), + None + ); + + // Check the storage directly + { + let storage_tx = tf.storage.transaction_ro().unwrap(); + + assert_eq!(storage_tx.get_token_data(token_id).unwrap(), None); + assert_eq!(storage_tx.get_circulating_supply(token_id).unwrap(), None); + + let tokens_acc_data = storage_tx.read_tokens_accounting_data().unwrap(); + assert_eq!(tokens_acc_data.token_data.get(token_id), None); + assert_eq!(tokens_acc_data.circulating_supply.get(token_id), None); + + assert_eq!( + storage_tx.get_token_aux_data(token_id).unwrap().unwrap(), + expected_aux_data + ); + assert_eq!( + storage_tx.get_token_aux_data(&random_token_id).unwrap(), + None + ); + + assert_eq!( + storage_tx.get_token_id(&issuance_tx_id).unwrap().unwrap(), + *token_id + ); + assert_eq!(storage_tx.get_token_id(&random_tx_id).unwrap(), None); + } +} + +pub fn assert_token_missing( + tf: &TestFramework, + token_id: &TokenId, + issuance_tx_id: &Id, + no_other_tokens_present: bool, +) { + // Check ChainstateInterface::get_token_info_for_rpc + assert_eq!( + tf.chainstate.get_token_info_for_rpc(*token_id).unwrap(), + None + ); + + // Check ChainstateInterface::get_tokens_info_for_rpc + assert_eq!( + tf.chainstate + .get_tokens_info_for_rpc(&BTreeSet::from_iter([*token_id])) + .unwrap_err(), + ChainstateError::FailedToReadProperty(PropertyQueryError::TokenInfoMissing(*token_id)) + ); + + // Check ChainstateInterface::get_token_aux_data + assert_eq!(tf.chainstate.get_token_aux_data(*token_id).unwrap(), None); + + // Check ChainstateInterface::get_token_id_from_issuance_tx; this only works for NFTs too. + assert_eq!( + tf.chainstate.get_token_id_from_issuance_tx(issuance_tx_id).unwrap(), + None + ); + + // Check ChainstateInterface::get_token_data + assert_eq!(tf.chainstate.get_token_data(token_id).unwrap(), None); + + // Check ChainstateInterface::get_token_circulating_supply + assert_eq!( + tf.chainstate.get_token_circulating_supply(token_id).unwrap(), + None + ); + + // Check the storage directly + { + let storage_tx = tf.storage.transaction_ro().unwrap(); + + assert_eq!(storage_tx.get_token_data(token_id).unwrap(), None); + assert_eq!(storage_tx.get_circulating_supply(token_id).unwrap(), None); + + let tokens_acc_data = storage_tx.read_tokens_accounting_data().unwrap(); + assert_eq!(tokens_acc_data.token_data.get(token_id), None); + assert_eq!(tokens_acc_data.circulating_supply.get(token_id), None); + + if no_other_tokens_present { + assert_eq!(tokens_acc_data.token_data.len(), 0); + assert_eq!(tokens_acc_data.circulating_supply.len(), 0); + } + + assert_eq!(storage_tx.get_token_aux_data(token_id).unwrap(), None); + assert_eq!(storage_tx.get_token_id(issuance_tx_id).unwrap(), None); + } +} diff --git a/chainstate/test-suite/src/tests/mod.rs b/chainstate/test-suite/src/tests/mod.rs index 9d5948017..5866e85c1 100644 --- a/chainstate/test-suite/src/tests/mod.rs +++ b/chainstate/test-suite/src/tests/mod.rs @@ -61,6 +61,7 @@ mod reorgs_tests; mod signature_tests; mod stake_pool_tests; mod syncing_tests; +mod tokens_misc_tests; mod tx_fee; mod tx_verification_simulation; mod tx_verifier_among_threads; diff --git a/chainstate/test-suite/src/tests/nft_issuance.rs b/chainstate/test-suite/src/tests/nft_issuance.rs index 80b726b07..eb6f435dd 100644 --- a/chainstate/test-suite/src/tests/nft_issuance.rs +++ b/chainstate/test-suite/src/tests/nft_issuance.rs @@ -20,7 +20,6 @@ use chainstate::{ ConnectTransactionError, TokensError, }; use chainstate_test_framework::{TestFramework, TransactionBuilder}; -use common::primitives::{BlockHeight, Idable}; use common::{ chain::{ output_value::OutputValue, @@ -29,7 +28,7 @@ use common::{ Block, ChainstateUpgradeBuilder, Destination, GenBlock, OutPointSourceId, TokenIssuanceVersion, TxInput, TxOutput, UtxoOutPoint, }, - primitives::Id, + primitives::{BlockHeight, Id, Idable}, }; use randomness::{CryptoRng, Rng}; use serialization::extras::non_empty_vec::DataOrNoVec; @@ -41,6 +40,8 @@ use test_utils::{ }; use tx_verifier::{error::TokenIssuanceError, CheckTransactionError}; +use crate::tests::helpers::token_checks::{check_nft, ExpectedNftData}; + #[rstest] #[trace] #[case(Seed::from_entropy())] @@ -456,7 +457,7 @@ fn nft_invalid_ticker(#[case] seed: Seed) { BlockError::CheckBlockFailed(CheckBlockError::CheckTransactionFailed( CheckBlockTransactionsError::CheckTransactionError( CheckTransactionError::TokensError(TokensError::IssueError( - TokenIssuanceError::IssueErrorTickerHasNoneAlphaNumericChar, + TokenIssuanceError::IssueErrorTickerHasNonAlphaNumericChar, _, )) ) @@ -786,43 +787,39 @@ fn nft_icon_uri_empty(#[case] seed: Seed) { media_uri: DataOrNoVec::from(None), media_hash: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0], }; - let block_index = tf + let issuance_tx = TransactionBuilder::new() + .add_input(tx_first_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::IssueNft( + token_id, + Box::new( + NftIssuanceV0 { + metadata: metadata.clone(), + } + .into(), + ), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) + .build(); + let issuance_block_id = *tf .make_block_builder() - .add_transaction( - TransactionBuilder::new() - .add_input(tx_first_input, InputWitness::NoSignature(None)) - .add_output(TxOutput::IssueNft( - token_id, - Box::new( - NftIssuanceV0 { - metadata: metadata.clone(), - } - .into(), - ), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) - .build(), - ) + .add_transaction(issuance_tx.clone()) .build_and_process(&mut rng) .unwrap() - .unwrap(); - - assert_eq!( - block_index.block_height(), - common::primitives::BlockHeight::from(1) - ); - - let block = tf.block(*block_index.block_id()); - let outputs = - tf.outputs_from_genblock(block.get_id().into()).values().next().unwrap().clone(); - - match &outputs[0] { - TxOutput::IssueNft(_, nft, _) => match nft.as_ref() { - NftIssuance::V0(nft) => assert_eq!(nft.metadata, metadata), + .unwrap() + .block_id(); + + check_nft( + &tf, + &mut rng, + &token_id, + &ExpectedNftData { + metadata, + issuance_tx: issuance_tx.take_transaction(), + issuance_tx_output_index: 0, + issuance_block_id, }, - _ => panic!("unexpected output"), - }; + ); }) } @@ -1014,43 +1011,39 @@ fn nft_metadata_uri_empty(#[case] seed: Seed) { media_uri: DataOrNoVec::from(None), media_hash: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0], }; - let block_index = tf + let issuance_tx = TransactionBuilder::new() + .add_input(tx_first_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::IssueNft( + token_id, + Box::new( + NftIssuanceV0 { + metadata: metadata.clone(), + } + .into(), + ), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) + .build(); + let issuance_block_id = *tf .make_block_builder() - .add_transaction( - TransactionBuilder::new() - .add_input(tx_first_input, InputWitness::NoSignature(None)) - .add_output(TxOutput::IssueNft( - token_id, - Box::new( - NftIssuanceV0 { - metadata: metadata.clone(), - } - .into(), - ), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) - .build(), - ) + .add_transaction(issuance_tx.clone()) .build_and_process(&mut rng) .unwrap() - .unwrap(); - - assert_eq!( - block_index.block_height(), - common::primitives::BlockHeight::from(1) - ); - - let block = tf.block(*block_index.block_id()); - let outputs = - tf.outputs_from_genblock(block.get_id().into()).values().next().unwrap().clone(); - - match &outputs[0] { - TxOutput::IssueNft(_, nft, _) => match nft.as_ref() { - NftIssuance::V0(nft) => assert_eq!(nft.metadata, metadata), + .unwrap() + .block_id(); + + check_nft( + &tf, + &mut rng, + &token_id, + &ExpectedNftData { + metadata, + issuance_tx: issuance_tx.take_transaction(), + issuance_tx_output_index: 0, + issuance_block_id, }, - _ => panic!("unexpected output"), - }; + ); }) } @@ -1244,43 +1237,39 @@ fn nft_media_uri_empty(#[case] seed: Seed) { media_hash: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0], }; - let block_index = tf + let issuance_tx = TransactionBuilder::new() + .add_input(tx_first_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::IssueNft( + token_id, + Box::new( + NftIssuanceV0 { + metadata: metadata.clone(), + } + .into(), + ), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) + .build(); + let issuance_block_id = *tf .make_block_builder() - .add_transaction( - TransactionBuilder::new() - .add_input(tx_first_input, InputWitness::NoSignature(None)) - .add_output(TxOutput::IssueNft( - token_id, - Box::new( - NftIssuanceV0 { - metadata: metadata.clone(), - } - .into(), - ), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) - .build(), - ) + .add_transaction(issuance_tx.clone()) .build_and_process(&mut rng) .unwrap() - .unwrap(); - - assert_eq!( - block_index.block_height(), - common::primitives::BlockHeight::from(1) - ); - - let block = tf.block(*block_index.block_id()); - let outputs = - tf.outputs_from_genblock(block.get_id().into()).values().next().unwrap().clone(); - - match &outputs[0] { - TxOutput::IssueNft(_, nft, _) => match nft.as_ref() { - NftIssuance::V0(nft) => assert_eq!(nft.metadata, metadata), + .unwrap() + .block_id(); + + check_nft( + &tf, + &mut rng, + &token_id, + &ExpectedNftData { + metadata, + issuance_tx: issuance_tx.take_transaction(), + issuance_tx_output_index: 0, + issuance_block_id, }, - _ => panic!("unexpected output"), - }; + ); }) } @@ -1559,43 +1548,39 @@ fn nft_valid_case(#[case] seed: Seed) { media_hash: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 0], }; - let block_index = tf + let issuance_tx = TransactionBuilder::new() + .add_input(tx_first_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::IssueNft( + token_id, + Box::new( + NftIssuanceV0 { + metadata: metadata.clone(), + } + .into(), + ), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) + .build(); + let issuance_block_id = *tf .make_block_builder() - .add_transaction( - TransactionBuilder::new() - .add_input(tx_first_input, InputWitness::NoSignature(None)) - .add_output(TxOutput::IssueNft( - token_id, - Box::new( - NftIssuanceV0 { - metadata: metadata.clone(), - } - .into(), - ), - Destination::AnyoneCanSpend, - )) - .add_output(TxOutput::Burn(OutputValue::Coin(token_min_issuance_fee))) - .build(), - ) + .add_transaction(issuance_tx.clone()) .build_and_process(&mut rng) .unwrap() - .unwrap(); - - assert_eq!( - block_index.block_height(), - common::primitives::BlockHeight::from(1) - ); - - let block = tf.block(*block_index.block_id()); - let outputs = - tf.outputs_from_genblock(block.get_id().into()).values().next().unwrap().clone(); - - match &outputs[0] { - TxOutput::IssueNft(_, nft, _) => match nft.as_ref() { - NftIssuance::V0(nft) => assert_eq!(nft.metadata, metadata), + .unwrap() + .block_id(); + + check_nft( + &tf, + &mut rng, + &token_id, + &ExpectedNftData { + metadata, + issuance_tx: issuance_tx.take_transaction(), + issuance_tx_output_index: 0, + issuance_block_id, }, - _ => panic!("unexpected output"), - }; + ); }) } @@ -1818,7 +1803,7 @@ fn only_ascii_alphanumeric_after_v1(#[case] seed: Seed) { CheckBlockError::CheckTransactionFailed( CheckBlockTransactionsError::CheckTransactionError( CheckTransactionError::TokensError(TokensError::IssueError( - TokenIssuanceError::IssueErrorTickerHasNoneAlphaNumericChar, + TokenIssuanceError::IssueErrorTickerHasNonAlphaNumericChar, tx_id, )) ) diff --git a/chainstate/test-suite/src/tests/nft_reorgs.rs b/chainstate/test-suite/src/tests/nft_reorgs.rs index c3b34cb7c..074bafb72 100644 --- a/chainstate/test-suite/src/tests/nft_reorgs.rs +++ b/chainstate/test-suite/src/tests/nft_reorgs.rs @@ -17,10 +17,8 @@ use chainstate::{BlockError, BlockSource, ChainstateError, ConnectTransactionErr use chainstate_test_framework::{TestFramework, TransactionBuilder}; use common::{ chain::{ - output_value::OutputValue, - signature::inputsig::InputWitness, - tokens::{NftIssuance, TokenId}, - Destination, OutPointSourceId, TxInput, TxOutput, UtxoOutPoint, + output_value::OutputValue, signature::inputsig::InputWitness, tokens::TokenId, Destination, + OutPointSourceId, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, BlockHeight, Idable}, }; @@ -30,6 +28,8 @@ use test_utils::{ token_utils::random_nft_issuance, }; +use crate::tests::helpers::token_checks::{assert_token_missing, check_nft, ExpectedNftData}; + #[rstest] #[trace] #[case(Seed::from_entropy())] @@ -328,56 +328,42 @@ fn nft_reorgs_and_cleanup_data(#[case] seed: Seed) { let mut tf = TestFramework::builder(&mut rng).build(); // Issue a new NFT - let issuance_value = random_nft_issuance(tf.chain_config().as_ref(), &mut rng); + let issuance = random_nft_issuance(tf.chain_config().as_ref(), &mut rng); let genesis_id = tf.genesis().get_id(); let genesis_outpoint_id = OutPointSourceId::BlockReward(tf.genesis().get_id().into()); let issuance_tx_first_input = TxInput::from_utxo(genesis_outpoint_id, 0); let token_id = TokenId::from_tx_input(&issuance_tx_first_input); + let issuance_tx = TransactionBuilder::new() + .add_input(issuance_tx_first_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::IssueNft( + token_id, + Box::new(issuance.clone().into()), + Destination::AnyoneCanSpend, + )) + .build(); + let issuance_tx_id = issuance_tx.transaction().get_id(); let block_index = tf .make_block_builder() .with_parent(genesis_id.into()) - .add_transaction( - TransactionBuilder::new() - .add_input(issuance_tx_first_input, InputWitness::NoSignature(None)) - .add_output(TxOutput::IssueNft( - token_id, - Box::new(issuance_value.clone().into()), - Destination::AnyoneCanSpend, - )) - .build(), - ) + .add_transaction(issuance_tx.clone()) .build_and_process(&mut rng) .unwrap() .unwrap(); - let issuance_block = tf.block(*block_index.block_id()); - - // Check NFT available in storage - let token_aux_data = tf.chainstate.get_token_aux_data(token_id).unwrap().unwrap(); - // Check id - assert_eq!(issuance_block.get_id(), token_aux_data.issuance_block_id()); - let issuance_tx = &issuance_block.transactions()[0]; - assert_eq!( - issuance_tx.transaction().get_id(), - token_aux_data.issuance_tx().get_id() - ); - - // Check issuance storage in the chain and in the storage - - match &issuance_tx.outputs()[0] { - TxOutput::IssueNft(_, nft, _) => match nft.as_ref() { - NftIssuance::V0(nft) => assert_eq!(*nft, issuance_value), - }, - _ => panic!("unexpected output"), - }; - match &token_aux_data.issuance_tx().outputs()[0] { - TxOutput::IssueNft(_, nft, _) => match nft.as_ref() { - NftIssuance::V0(nft) => assert_eq!(*nft, issuance_value), + // Check NFT + check_nft( + &tf, + &mut rng, + &token_id, + &ExpectedNftData { + metadata: issuance.metadata.clone(), + issuance_tx: issuance_tx.take_transaction(), + issuance_tx_output_index: 0, + issuance_block_id: *block_index.block_id(), }, - _ => panic!("unexpected output"), - }; + ); // Cause reorg tf.create_chain(&tf.genesis().get_id().into(), 5, &mut rng).unwrap(); @@ -393,14 +379,6 @@ fn nft_reorgs_and_cleanup_data(#[case] seed: Seed) { ); // Check that tokens not in storage - assert!(tf - .chainstate - .get_token_id_from_issuance_tx(&issuance_tx.transaction().get_id()) - .unwrap() - .is_none()); - - assert!(tf.chainstate.get_token_info_for_rpc(token_id).unwrap().is_none()); - - assert!(tf.chainstate.get_token_aux_data(token_id).unwrap().is_none()); + assert_token_missing(&tf, &token_id, &issuance_tx_id, true); }) } diff --git a/chainstate/test-suite/src/tests/tokens_misc_tests.rs b/chainstate/test-suite/src/tests/tokens_misc_tests.rs new file mode 100644 index 000000000..1cc6e587d --- /dev/null +++ b/chainstate/test-suite/src/tests/tokens_misc_tests.rs @@ -0,0 +1,232 @@ +// Copyright (c) 2021-2025 RBB S.r.l +// opensource@mintlayer.org +// SPDX-License-Identifier: MIT +// Licensed under the MIT License; +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://github.com/mintlayer/mintlayer-core/blob/master/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::{BTreeMap, BTreeSet}; + +use itertools::Itertools as _; +use rand::seq::IteratorRandom as _; +use rstest::rstest; + +use chainstate::{ChainstateError, PropertyQueryError}; +use chainstate_test_framework::{ + get_output_value, + helpers::{issue_token_from_block, issue_token_from_genesis, make_token_issuance}, + TestFramework, TransactionBuilder, +}; +use common::{ + chain::{ + output_value::OutputValue, + signature::inputsig::InputWitness, + tokens::{IsTokenFreezable, IsTokenFrozen, TokenId, TokenIssuance, TokenTotalSupply}, + Destination, TxInput, TxOutput, UtxoOutPoint, + }, + primitives::{Amount, BlockHeight, Idable}, +}; +use test_utils::{ + assert_matches_return_val, + random::{make_seedable_rng, Seed}, + token_utils::random_nft_issuance, +}; + +use crate::tests::helpers::token_checks::{ + make_expected_rpc_token_info_from_nft_metadata, + make_expected_rpc_token_info_from_token_issuance, +}; + +// Test get_tokens_info_for_rpc when multiple tokens are available (2 fungible ones, 2 NFTs). +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn get_tokens_info_for_rpc_test(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token1_id, _, _, issuance1, utxo_with_change) = issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Unlimited, + IsTokenFreezable::No, + ); + + let issuance1_v1 = + assert_matches_return_val!(issuance1, TokenIssuance::V1(issuance), issuance); + + let token1_expected_info_for_rpc = make_expected_rpc_token_info_from_token_issuance( + token1_id, + &issuance1_v1, + Amount::ZERO, + false, + IsTokenFrozen::No(IsTokenFreezable::No), + ); + + let issuance2 = + make_token_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let best_block_id = tf.best_block_id(); + let (token2_id, _, _, utxo_with_change) = issue_token_from_block( + &mut rng, + &mut tf, + best_block_id, + utxo_with_change, + issuance2.clone(), + ); + + let issuance2_v1 = + assert_matches_return_val!(issuance2, TokenIssuance::V1(issuance), issuance); + + let token2_expected_info_for_rpc = make_expected_rpc_token_info_from_token_issuance( + token2_id, + &issuance2_v1, + Amount::ZERO, + false, + IsTokenFrozen::No(IsTokenFreezable::No), + ); + + let nft_issuance_fee = + tf.chainstate.get_chain_config().nft_issuance_fee(BlockHeight::zero()); + let change_amount = + get_output_value(tf.chainstate.utxo(&utxo_with_change).unwrap().unwrap().output()) + .unwrap() + .coin_amount() + .unwrap(); + + let nft_tx1_first_input = TxInput::Utxo(utxo_with_change); + let nft1_id = TokenId::from_tx_input(&nft_tx1_first_input); + let nft1_issuance = random_nft_issuance(tf.chain_config().as_ref(), &mut rng); + let next_change_amount = (change_amount - nft_issuance_fee).unwrap(); + + let ntf1_issuance_tx = TransactionBuilder::new() + .add_input(nft_tx1_first_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::IssueNft( + nft1_id, + Box::new(nft1_issuance.clone().into()), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(next_change_amount), + Destination::AnyoneCanSpend, + )) + .build(); + let nft1_issuance_tx_id = ntf1_issuance_tx.transaction().get_id(); + let utxo_with_change = UtxoOutPoint::new(ntf1_issuance_tx.transaction().get_id().into(), 1); + let change_amount = next_change_amount; + + let nft1_issuance_block_id = *tf + .make_block_builder() + .add_transaction(ntf1_issuance_tx) + .build_and_process(&mut rng) + .unwrap() + .unwrap() + .block_id(); + + let nft1_expected_info_for_rpc = make_expected_rpc_token_info_from_nft_metadata( + nft1_id, + nft1_issuance_tx_id, + nft1_issuance_block_id, + &nft1_issuance.metadata, + ); + + let nft_tx2_first_input = TxInput::Utxo(utxo_with_change); + let nft2_id = TokenId::from_tx_input(&nft_tx2_first_input); + let nft2_issuance = random_nft_issuance(tf.chain_config().as_ref(), &mut rng); + let next_change_amount = (change_amount - nft_issuance_fee).unwrap(); + + let ntf2_issuance_tx = TransactionBuilder::new() + .add_input(nft_tx2_first_input, InputWitness::NoSignature(None)) + .add_output(TxOutput::IssueNft( + nft2_id, + Box::new(nft2_issuance.clone().into()), + Destination::AnyoneCanSpend, + )) + .add_output(TxOutput::Transfer( + OutputValue::Coin(next_change_amount), + Destination::AnyoneCanSpend, + )) + .build(); + let nft2_issuance_tx_id = ntf2_issuance_tx.transaction().get_id(); + + let nft2_issuance_block_id = *tf + .make_block_builder() + .add_transaction(ntf2_issuance_tx) + .build_and_process(&mut rng) + .unwrap() + .unwrap() + .block_id(); + + let nft2_expected_info_for_rpc = make_expected_rpc_token_info_from_nft_metadata( + nft2_id, + nft2_issuance_tx_id, + nft2_issuance_block_id, + &nft2_issuance.metadata, + ); + + let random_token_id = TokenId::random_using(&mut rng); + + let all_expected_infos = BTreeMap::from_iter([ + (token1_id, token1_expected_info_for_rpc), + (token2_id, token2_expected_info_for_rpc), + (nft1_id, nft1_expected_info_for_rpc), + (nft2_id, nft2_expected_info_for_rpc), + ]); + + // Check obtaining the info for each token individually + for (token_id, expected_info_for_rpc) in &all_expected_infos { + let actual_infos_for_rpc = tf + .chainstate + .get_tokens_info_for_rpc(&BTreeSet::from_iter([*token_id])) + .unwrap(); + assert_eq!( + &actual_infos_for_rpc[..], + std::slice::from_ref(expected_info_for_rpc) + ); + + // Also check that adding random_token_id to the set results in an error. + assert_eq!( + tf.chainstate + .get_tokens_info_for_rpc(&BTreeSet::from_iter([*token_id, random_token_id])) + .unwrap_err(), + ChainstateError::FailedToReadProperty(PropertyQueryError::TokenInfoMissing( + random_token_id + )) + ) + } + + // Check obtaining the info for 2, 3 and all the tokens simultaneously + for test_set in [ + all_expected_infos.iter().choose_multiple(&mut rng, 2), + all_expected_infos.iter().choose_multiple(&mut rng, 3), + all_expected_infos.iter().collect_vec(), + ] { + // Collect the test set into a BTreeMap, so that the expected infos are sorted + // by token id. This is how get_tokens_info_for_rpc returns them. + let tokens_map = test_set.into_iter().collect::>(); + let token_ids = tokens_map.keys().copied().copied().collect::>(); + let expected_infos_for_rpc = tokens_map.values().copied().cloned().collect_vec(); + + let actual_infos_for_rpc = tf.chainstate.get_tokens_info_for_rpc(&token_ids).unwrap(); + assert_eq!(&actual_infos_for_rpc[..], &expected_infos_for_rpc); + + // Also check that adding random_token_id to the set results in an error. + let mut token_ids = token_ids; + token_ids.insert(random_token_id); + assert_eq!( + tf.chainstate.get_tokens_info_for_rpc(&token_ids).unwrap_err(), + ChainstateError::FailedToReadProperty(PropertyQueryError::TokenInfoMissing( + random_token_id + )) + ) + } + }) +} diff --git a/chainstate/tx-verifier/src/transaction_verifier/error.rs b/chainstate/tx-verifier/src/transaction_verifier/error.rs index 575e583f7..8cb3b897b 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/error.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/error.rs @@ -176,7 +176,7 @@ pub enum TokenIssuanceError { #[error("Invalid description length")] IssueErrorInvalidDescriptionLength, #[error("Invalid character in token ticker")] - IssueErrorTickerHasNoneAlphaNumericChar, + IssueErrorTickerHasNonAlphaNumericChar, #[error("Invalid character in token name")] IssueErrorNameHasNoneAlphaNumericChar, #[error("Invalid character in token description")] diff --git a/chainstate/tx-verifier/src/transaction_verifier/tokens_check/check_utils.rs b/chainstate/tx-verifier/src/transaction_verifier/tokens_check/check_utils.rs index 9688edbd9..97fc4b781 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/tokens_check/check_utils.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/tokens_check/check_utils.rs @@ -58,7 +58,7 @@ pub fn check_token_ticker( // Check if ticker has alphanumeric chars ensure!( check_is_text_ascii_alphanumeric(ticker), - TokenIssuanceError::IssueErrorTickerHasNoneAlphaNumericChar + TokenIssuanceError::IssueErrorTickerHasNonAlphaNumericChar ); Ok(()) diff --git a/chainstate/types/src/error.rs b/chainstate/types/src/error.rs index cd7f6355d..a68b96021 100644 --- a/chainstate/types/src/error.rs +++ b/chainstate/types/src/error.rs @@ -16,7 +16,7 @@ use thiserror::Error; use common::{ - chain::{Block, GenBlock, OrderId, PoolId}, + chain::{tokens::TokenId, Block, GenBlock, OrderId, PoolId}, primitives::{BlockHeight, Id}, }; @@ -60,6 +60,8 @@ pub enum PropertyQueryError { start: BlockHeight, end: BlockHeight, }, + #[error("Token info missing for token {0:x}")] + TokenInfoMissing(TokenId), } #[derive(Error, Debug, PartialEq, Eq, Clone)] diff --git a/common/src/chain/tokens/rpc.rs b/common/src/chain/tokens/rpc.rs index 4fd41fcf1..3d24804c1 100644 --- a/common/src/chain/tokens/rpc.rs +++ b/common/src/chain/tokens/rpc.rs @@ -22,7 +22,7 @@ use rpc_description::HasValueHint; use rpc_types::{RpcHexString, RpcString}; use serialization::Encode; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, HasValueHint)] #[serde(tag = "type", content = "content")] pub enum RPCTokenInfo { FungibleToken(RPCFungibleTokenInfo), @@ -149,7 +149,7 @@ impl RPCFungibleTokenInfo { } } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, HasValueHint)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, HasValueHint)] pub struct RPCNonFungibleTokenInfo { pub token_id: TokenId, pub creation_tx_id: Id, diff --git a/mocks/src/chainstate.rs b/mocks/src/chainstate.rs index cc02020f0..243ca26b9 100644 --- a/mocks/src/chainstate.rs +++ b/mocks/src/chainstate.rs @@ -13,7 +13,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, num::NonZeroUsize, sync::Arc}; +use std::{ + collections::{BTreeMap, BTreeSet}, + num::NonZeroUsize, + sync::Arc, +}; use chainstate::{ BlockSource, ChainInfo, ChainstateConfig, ChainstateError, ChainstateEvent, Locator, @@ -139,6 +143,7 @@ mockall::mock! { ) -> Result, ChainstateError>; fn get_epoch_data(&self, epoch_index: u64) -> Result, ChainstateError>; fn get_token_info_for_rpc(&self, token_id: TokenId) -> Result, ChainstateError>; + fn get_tokens_info_for_rpc(&self, token_ids: &BTreeSet) -> Result, ChainstateError>; fn get_token_aux_data( &self, token_id: TokenId, diff --git a/node-daemon/docs/RPC.md b/node-daemon/docs/RPC.md index 4b70a995e..d3292de64 100644 --- a/node-daemon/docs/RPC.md +++ b/node-daemon/docs/RPC.md @@ -389,7 +389,7 @@ EITHER OF ### Method `chainstate_token_info` -Get token information, given a token id, in address form. +Get token information, given a token id in address form. Parameters: @@ -497,6 +497,115 @@ EITHER OF 3) null ``` +### Method `chainstate_tokens_info` + +Get tokens information, given multiple token ids in address form. + + +Parameters: +``` +{ "token_ids": [ string, .. ] } +``` + +Returns: +``` +[ EITHER OF + 1) { + "type": "FungibleToken", + "content": { + "token_id": hex string, + "token_ticker": { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + }, + "number_of_decimals": number, + "metadata_uri": { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + }, + "circulating_supply": { "atoms": number string }, + "total_supply": EITHER OF + 1) { + "type": "Fixed", + "content": { "amount": { "atoms": number string } }, + } + 2) { "type": "Lockable" } + 3) { "type": "Unlimited" }, + "is_locked": bool, + "frozen": EITHER OF + 1) { + "type": "NotFrozen", + "content": { "freezable": bool }, + } + 2) { + "type": "Frozen", + "content": { "unfreezable": bool }, + }, + "authority": bech32 string, + }, + } + 2) { + "type": "NonFungibleToken", + "content": { + "token_id": hex string, + "creation_tx_id": hex string, + "creation_block_id": hex string, + "metadata": { + "creator": EITHER OF + 1) hex string + 2) null, + "name": { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + }, + "description": { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + }, + "ticker": { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + }, + "icon_uri": EITHER OF + 1) { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + } + 2) null, + "additional_metadata_uri": EITHER OF + 1) { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + } + 2) null, + "media_uri": EITHER OF + 1) { + "text": EITHER OF + 1) string + 2) null, + "hex": hex string, + } + 2) null, + "media_hash": hex string, + }, + }, + }, .. ] +``` + ### Method `chainstate_order_info` Get order information, given an order id, in address form. diff --git a/wallet/wallet-controller/src/sync/tests/mod.rs b/wallet/wallet-controller/src/sync/tests/mod.rs index 2119443cd..d1ebbd1fb 100644 --- a/wallet/wallet-controller/src/sync/tests/mod.rs +++ b/wallet/wallet-controller/src/sync/tests/mod.rs @@ -14,6 +14,7 @@ // limitations under the License. use std::{ + collections::BTreeSet, num::NonZeroUsize, sync::{Arc, Mutex}, time::Duration, @@ -297,6 +298,13 @@ impl NodeInterface for MockNode { unreachable!() } + async fn get_tokens_info( + &self, + _token_ids: BTreeSet, + ) -> Result, Self::Error> { + unreachable!() + } + async fn get_order_info( &self, _order_id: OrderId, diff --git a/wallet/wallet-node-client/src/handles_client/mod.rs b/wallet/wallet-node-client/src/handles_client/mod.rs index 8ae8ee7e6..a9d9d98a1 100644 --- a/wallet/wallet-node-client/src/handles_client/mod.rs +++ b/wallet/wallet-node-client/src/handles_client/mod.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{num::NonZeroUsize, time::Duration}; +use std::{collections::BTreeSet, num::NonZeroUsize, time::Duration}; use blockprod::{BlockProductionError, BlockProductionHandle, TimestampSearchData}; use chainstate::{BlockSource, ChainInfo, ChainstateError, ChainstateHandle}; @@ -228,6 +228,17 @@ impl NodeInterface for WalletHandlesClient { Ok(result) } + async fn get_tokens_info( + &self, + token_ids: BTreeSet, + ) -> Result, Self::Error> { + let result = self + .chainstate + .call(move |this| this.get_tokens_info_for_rpc(&token_ids)) + .await??; + Ok(result) + } + async fn get_order_info(&self, order_id: OrderId) -> Result, Self::Error> { let result = self .chainstate diff --git a/wallet/wallet-node-client/src/mock.rs b/wallet/wallet-node-client/src/mock.rs index 677fb53d3..9b2b3cd81 100644 --- a/wallet/wallet-node-client/src/mock.rs +++ b/wallet/wallet-node-client/src/mock.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{num::NonZeroUsize, sync::Arc, time::Duration}; +use std::{collections::BTreeSet, num::NonZeroUsize, sync::Arc, time::Duration}; use chainstate::ChainInfo; use common::{ @@ -140,6 +140,13 @@ impl NodeInterface for ClonableMockNodeInterface { self.lock().await.get_token_info(token_id).await } + async fn get_tokens_info( + &self, + token_ids: BTreeSet, + ) -> Result, Self::Error> { + self.lock().await.get_tokens_info(token_ids).await + } + async fn get_order_info(&self, order_id: OrderId) -> Result, Self::Error> { self.lock().await.get_order_info(order_id).await } diff --git a/wallet/wallet-node-client/src/node_traits.rs b/wallet/wallet-node-client/src/node_traits.rs index ae976af4f..8375b3f2c 100644 --- a/wallet/wallet-node-client/src/node_traits.rs +++ b/wallet/wallet-node-client/src/node_traits.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{num::NonZeroUsize, time::Duration}; +use std::{collections::BTreeSet, num::NonZeroUsize, time::Duration}; use chainstate::ChainInfo; use common::{ @@ -77,6 +77,10 @@ pub trait NodeInterface { delegation_id: DelegationId, ) -> Result, Self::Error>; async fn get_token_info(&self, token_id: TokenId) -> Result, Self::Error>; + async fn get_tokens_info( + &self, + token_ids: BTreeSet, + ) -> Result, Self::Error>; async fn get_order_info(&self, order_id: OrderId) -> Result, Self::Error>; async fn blockprod_e2e_public_key(&self) -> Result; async fn generate_block( diff --git a/wallet/wallet-node-client/src/rpc_client/client_impl.rs b/wallet/wallet-node-client/src/rpc_client/client_impl.rs index 133fa292b..a3545a4a3 100644 --- a/wallet/wallet-node-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-node-client/src/rpc_client/client_impl.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{num::NonZeroUsize, time::Duration}; +use std::{collections::BTreeSet, num::NonZeroUsize, time::Duration}; use blockprod::{rpc::BlockProductionRpcClient, TimestampSearchData}; use chainstate::{rpc::ChainstateRpcClient, ChainInfo}; @@ -173,6 +173,21 @@ impl NodeInterface for NodeRpcClient { .map_err(NodeRpcError::ResponseError) } + async fn get_tokens_info( + &self, + token_ids: BTreeSet, + ) -> Result, Self::Error> { + let token_ids = token_ids + .into_iter() + .map(|token_id| { + Ok::<_, Self::Error>(Address::new(&self.chain_config, token_id)?.into_string()) + }) + .collect::>()?; + ChainstateRpcClient::tokens_info(&self.http_client, token_ids) + .await + .map_err(NodeRpcError::ResponseError) + } + async fn get_order_info(&self, order_id: OrderId) -> Result, Self::Error> { let order_id = Address::new(&self.chain_config, order_id)?.into_string(); ChainstateRpcClient::order_info(&self.http_client, order_id) diff --git a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs index 09c744818..f2d9ba9c2 100644 --- a/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs +++ b/wallet/wallet-node-client/src/rpc_client/cold_wallet_client.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{num::NonZeroUsize, time::Duration}; +use std::{collections::BTreeSet, num::NonZeroUsize, time::Duration}; use blockprod::TimestampSearchData; use chainstate::ChainInfo; @@ -141,6 +141,13 @@ impl NodeInterface for ColdWalletClient { Err(ColdWalletRpcError::NotAvailable) } + async fn get_tokens_info( + &self, + _token_ids: BTreeSet, + ) -> Result, Self::Error> { + Err(ColdWalletRpcError::NotAvailable) + } + async fn get_order_info( &self, _order_id: OrderId, diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index 9d7682b9d..4a3224e10 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -41,9 +41,7 @@ use common::{ signature::inputsig::arbitrary_message::{ produce_message_challenge, ArbitraryMessageSignature, }, - tokens::{ - IsTokenFreezable, IsTokenUnfreezable, Metadata, RPCTokenInfo, TokenId, TokenTotalSupply, - }, + tokens::{IsTokenFreezable, IsTokenUnfreezable, Metadata, TokenId, TokenTotalSupply}, Block, ChainConfig, DelegationId, Destination, GenBlock, OrderId, PoolId, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint, }, @@ -760,36 +758,22 @@ where .await? } - pub async fn get_token_infos( - &self, - token_ids: BTreeSet, - ) -> WRpcResult, N> { - let mut result = BTreeMap::new(); - - // TODO: consider introducing a separate node RPC call that would fetch all token infos at once. - for token_id in token_ids { - let token_info = self - .node - .get_token_info(token_id) - .await - .map_err(RpcError::RpcError)? - .ok_or(RpcError::MissingTokenInfo(token_id))?; - result.insert(token_id, token_info); - } - - Ok(result) - } - pub async fn get_tokens_decimals( &self, token_ids: BTreeSet, ) -> WRpcResult, N> { - Ok(self - .get_token_infos(token_ids) - .await? + let infos = self.node.get_tokens_info(token_ids).await.map_err(RpcError::RpcError)?; + let desimals = infos .iter() - .map(|(id, info)| (*id, TokenDecimals(info.token_number_of_decimals()))) - .collect()) + .map(|info| { + ( + info.token_id(), + TokenDecimals(info.token_number_of_decimals()), + ) + }) + .collect(); + + Ok(desimals) } pub async fn get_transaction( diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index 478f99b70..9e5a9a541 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -172,9 +172,6 @@ pub enum RpcError { #[error("Wallet recovery requires mnemonic to be specified")] WalletRecoveryWithoutMnemonic, - - #[error("Token info missing for token {0:x}")] - MissingTokenInfo(TokenId), } impl From> for rpc::Error {