From 5d31cfa18e3c647da10a2fd1968fb980ad1c62b2 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Tue, 10 Oct 2023 18:19:10 +0200 Subject: [PATCH 01/11] Keep track of token supply in the wallet cache --- wallet/src/account/output_cache/mod.rs | 304 +++++++++++++++++++++++-- wallet/src/wallet/mod.rs | 12 + 2 files changed, 293 insertions(+), 23 deletions(-) diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index 597bacb03b..0425978c34 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -17,11 +17,14 @@ use std::collections::{btree_map::Entry, BTreeMap, BTreeSet}; use common::{ chain::{ - tokens::{is_token_or_nft_issuance, make_token_id, TokenId}, + output_value::OutputValue, + tokens::{ + is_token_or_nft_issuance, make_token_id, TokenId, TokenIssuance, TokenTotalSupply, + }, AccountNonce, AccountOp, DelegationId, Destination, OutPointSourceId, PoolId, Transaction, TxInput, TxOutput, UtxoOutPoint, }, - primitives::{id::WithId, Id}, + primitives::{id::WithId, Amount, Id}, }; use pos_accounting::make_delegation_id; use utils::ensure; @@ -74,6 +77,101 @@ impl PoolData { } } +pub enum TokenTotalSupplyState { + Fixed(Amount), // fixed to a certain amount + Lockable(Amount), // not known in advance but can be locked once at some point in time + Locked(Amount), // Locked + Unlimited(Amount), // limited only by the Amount data type +} + +impl From for TokenTotalSupplyState { + fn from(value: TokenTotalSupply) -> Self { + match value { + TokenTotalSupply::Fixed(amount) => TokenTotalSupplyState::Fixed(amount), + TokenTotalSupply::Lockable => TokenTotalSupplyState::Lockable(Amount::ZERO), + TokenTotalSupply::Unlimited => TokenTotalSupplyState::Unlimited(Amount::ZERO), + } + } +} + +impl TokenTotalSupplyState { + fn str_state(&self) -> &'static str { + match self { + Self::Unlimited(_) => "Unlimited", + Self::Locked(_) => "Locked", + Self::Lockable(_) => "Lockable", + Self::Fixed(_) => "Fixed", + } + } + + fn mint(&self, amount: Amount) -> WalletResult { + match self { + TokenTotalSupplyState::Lockable(current) => Ok(TokenTotalSupplyState::Lockable( + (*current + amount).ok_or(WalletError::OutputAmountOverflow)?, + )), + TokenTotalSupplyState::Unlimited(current) => Ok(TokenTotalSupplyState::Unlimited( + (*current + amount).ok_or(WalletError::OutputAmountOverflow)?, + )), + TokenTotalSupplyState::Fixed(_) | TokenTotalSupplyState::Locked(_) => { + Err(WalletError::CannotChangeTokenSupply(self.str_state())) + } + } + } + + fn redeem(&self, amount: Amount) -> WalletResult { + match self { + TokenTotalSupplyState::Lockable(current) => Ok(TokenTotalSupplyState::Lockable( + (*current - amount).ok_or(WalletError::OutputAmountOverflow)?, + )), + TokenTotalSupplyState::Unlimited(current) => Ok(TokenTotalSupplyState::Unlimited( + (*current - amount).ok_or(WalletError::OutputAmountOverflow)?, + )), + TokenTotalSupplyState::Fixed(_) | TokenTotalSupplyState::Locked(_) => { + Err(WalletError::CannotChangeTokenSupply(self.str_state())) + } + } + } + + fn lock(&self) -> WalletResult { + match self { + TokenTotalSupplyState::Lockable(current) => Ok(TokenTotalSupplyState::Locked(*current)), + TokenTotalSupplyState::Unlimited(_) + | TokenTotalSupplyState::Fixed(_) + | TokenTotalSupplyState::Locked(_) => { + Err(WalletError::CannotLockTokenSupply(self.str_state())) + } + } + } + + fn unlock(&self) -> WalletResult { + match self { + TokenTotalSupplyState::Locked(current) => Ok(TokenTotalSupplyState::Lockable(*current)), + TokenTotalSupplyState::Unlimited(_) + | TokenTotalSupplyState::Fixed(_) + | TokenTotalSupplyState::Lockable(_) => { + Err(WalletError::InconsistentUnLockTokenSupply(self.str_state())) + } + } + } +} + +pub struct TokenIssuanceData { + total_supply: TokenTotalSupplyState, + pub last_nonce: Option, + /// last parent transaction if the parent is unconfirmed + pub last_parent: Option, +} + +impl TokenIssuanceData { + fn new(data: TokenTotalSupply) -> Self { + Self { + total_supply: data.into(), + last_nonce: None, + last_parent: None, + } + } +} + /// A helper structure for the UTXO search. /// /// All transactions and blocks from the DB are cached here. If a transaction @@ -91,6 +189,7 @@ pub struct OutputCache { unconfirmed_descendants: BTreeMap>, pools: BTreeMap, delegations: BTreeMap, + token_issuance: BTreeMap, } impl OutputCache { @@ -101,6 +200,7 @@ impl OutputCache { unconfirmed_descendants: BTreeMap::new(), pools: BTreeMap::new(), delegations: BTreeMap::new(), + token_issuance: BTreeMap::new(), } } @@ -236,10 +336,17 @@ impl OutputCache { | TxOutput::Burn(_) | TxOutput::Transfer(_, _) | TxOutput::LockThenTransfer(_, _, _) => {} - | TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) => { - // TODO: add support for tokens v1 - // See https://github.com/mintlayer/mintlayer-core/issues/1237 + | TxOutput::IssueFungibleToken(issuance) => { + let input0_outpoint = tx.inputs(); + let token_id = make_token_id(input0_outpoint).ok_or(WalletError::NoUtxos)?; + match issuance.as_ref() { + TokenIssuance::V1(data) => { + self.token_issuance + .insert(token_id, TokenIssuanceData::new(data.total_supply)); + } + } } + TxOutput::IssueNft(_, _, _) => {} }; } Ok(()) @@ -281,12 +388,42 @@ impl OutputCache { )?; } } - AccountOp::MintTokens(_, _) - | AccountOp::UnmintTokens(_) - | AccountOp::LockTokenSupply(_) => { - // TODO: add support for tokens v1 - // See https://github.com/mintlayer/mintlayer-core/issues/1237 - unimplemented!() + AccountOp::MintTokens(token_id, amount) => { + if let Some(data) = self.token_issuance.get_mut(token_id) { + Self::update_token_issuance_state( + &mut self.unconfirmed_descendants, + data, + token_id, + outpoint, + tx_id, + )?; + data.total_supply.mint(*amount)?; + } + } + AccountOp::UnmintTokens(token_id) => { + if let Some(data) = self.token_issuance.get_mut(token_id) { + Self::update_token_issuance_state( + &mut self.unconfirmed_descendants, + data, + token_id, + outpoint, + tx_id, + )?; + let amount = sum_burned_token_amount(tx.outputs(), token_id)?; + data.total_supply.redeem(amount)?; + } + } + | AccountOp::LockTokenSupply(token_id) => { + if let Some(data) = self.token_issuance.get_mut(token_id) { + Self::update_token_issuance_state( + &mut self.unconfirmed_descendants, + data, + token_id, + outpoint, + tx_id, + )?; + data.total_supply.lock()?; + } } } } @@ -327,6 +464,37 @@ impl OutputCache { Ok(()) } + /// Update delegation state with new tx input + fn update_token_issuance_state( + unconfirmed_descendants: &mut BTreeMap>, + data: &mut TokenIssuanceData, + delegation_id: &TokenId, + outpoint: &common::chain::AccountOutPoint, + tx_id: &OutPointSourceId, + ) -> Result<(), WalletError> { + let next_nonce = data + .last_nonce + .map_or(Some(AccountNonce::new(0)), |nonce| nonce.increment()) + .ok_or(WalletError::TokenIssuanceNonceOverflow(*delegation_id))?; + + ensure!( + outpoint.nonce() == next_nonce, + WalletError::InconsistentTokenIssuanceDuplicateNonce(*delegation_id, outpoint.nonce()) + ); + + data.last_nonce = Some(outpoint.nonce()); + // update unconfirmed descendants + if let Some(descendants) = data + .last_parent + .as_ref() + .and_then(|parent_tx_id| unconfirmed_descendants.get_mut(parent_tx_id)) + { + descendants.insert(tx_id.clone()); + } + data.last_parent = Some(tx_id.clone()); + Ok(()) + } + pub fn remove_tx(&mut self, tx_id: &OutPointSourceId) -> WalletResult<()> { let tx_opt = self.txs.remove(tx_id); if let Some(tx) = tx_opt { @@ -344,12 +512,31 @@ impl OutputCache { find_parent(&self.unconfirmed_descendants, tx_id.clone()); } } - AccountOp::MintTokens(_, _) - | AccountOp::UnmintTokens(_) - | AccountOp::LockTokenSupply(_) => { - // TODO: add support for tokens v1 - // See https://github.com/mintlayer/mintlayer-core/issues/1237 - unimplemented!() + AccountOp::MintTokens(token_id, amount) => { + if let Some(data) = self.token_issuance.get_mut(token_id) { + data.last_nonce = outpoint.nonce().decrement(); + data.last_parent = + find_parent(&self.unconfirmed_descendants, tx_id.clone()); + data.total_supply.redeem(*amount)?; + } + } + + AccountOp::UnmintTokens(token_id) => { + if let Some(data) = self.token_issuance.get_mut(token_id) { + data.last_nonce = outpoint.nonce().decrement(); + data.last_parent = + find_parent(&self.unconfirmed_descendants, tx_id.clone()); + let amount = sum_burned_token_amount(tx.outputs(), token_id)?; + data.total_supply.mint(amount)?; + } + } + AccountOp::LockTokenSupply(token_id) => { + if let Some(data) = self.token_issuance.get_mut(token_id) { + data.last_nonce = outpoint.nonce().decrement(); + data.last_parent = + find_parent(&self.unconfirmed_descendants, tx_id.clone()); + data.total_supply.unlock()?; + } } }, } @@ -398,6 +585,11 @@ impl OutputCache { WalletError::LockedUtxo(utxo.clone()) ); + ensure!( + !is_v0_token_output(output), + WalletError::TokenV0Utxo(utxo.clone()) + ); + let token_id = match tx { WalletTx::Tx(tx_data) => make_token_id(tx_data.get_transaction().inputs()), WalletTx::Block(_) => None, @@ -445,6 +637,7 @@ impl OutputCache { tx_block_info, outpoint, ) + && !is_v0_token_output(output) }) .map(move |(output, outpoint)| { let token_id = match tx { @@ -516,12 +709,45 @@ impl OutputCache { ); } } - AccountOp::MintTokens(_, _) - | AccountOp::UnmintTokens(_) - | AccountOp::LockTokenSupply(_) => { - // TODO: add support for tokens v1 - // See https://github.com/mintlayer/mintlayer-core/issues/1237 - unimplemented!() + AccountOp::MintTokens(token_id, amount) => { + if let Some(data) = + self.token_issuance.get_mut(token_id) + { + data.last_nonce = outpoint.nonce().decrement(); + data.last_parent = find_parent( + &self.unconfirmed_descendants, + tx_id.into(), + ); + data.total_supply.redeem(*amount)?; + } + } + | AccountOp::UnmintTokens(token_id) => { + if let Some(data) = + self.token_issuance.get_mut(token_id) + { + data.last_nonce = outpoint.nonce().decrement(); + data.last_parent = find_parent( + &self.unconfirmed_descendants, + tx_id.into(), + ); + let amount = sum_burned_token_amount( + tx.get_transaction().outputs(), + token_id, + )?; + data.total_supply.mint(amount)?; + } + } + | AccountOp::LockTokenSupply(token_id) => { + if let Some(data) = + self.token_issuance.get_mut(token_id) + { + data.last_nonce = outpoint.nonce().decrement(); + data.last_parent = find_parent( + &self.unconfirmed_descendants, + tx_id.into(), + ); + data.total_supply.unlock()?; + } } }, } @@ -539,6 +765,38 @@ impl OutputCache { } } +fn sum_burned_token_amount( + outputs: &[TxOutput], + token_id: &TokenId, +) -> Result { + let amount = outputs + .iter() + .filter_map(|output| match output { + TxOutput::Burn(OutputValue::TokenV1(tid, amount)) if tid == token_id => Some(*amount), + _ => None, + }) + .sum::>() + .ok_or(WalletError::OutputAmountOverflow); + amount +} + +/// Check if the TxOutput is a v0 token +fn is_v0_token_output(output: &TxOutput) -> bool { + match output { + TxOutput::LockThenTransfer(out, _, _) | TxOutput::Transfer(out, _) => match out { + OutputValue::TokenV0(_) => true, + OutputValue::Coin(_) | OutputValue::TokenV1(_, _) => false, + }, + TxOutput::Burn(_) + | TxOutput::CreateStakePool(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::IssueNft(_, _, _) + | TxOutput::IssueFungibleToken(_) + | TxOutput::ProduceBlockFromStake(_, _) => false, + } +} + /// Checks the output against the current block height and compares it with the locked_state parameter. /// If they match, the function return true, if they don't, it returns false. /// For example, if we would like to check that an output is locked, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 56da84b244..60e8ab6de4 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -103,6 +103,10 @@ pub enum WalletError { InconsistentProduceBlockFromStake(PoolId), #[error("Delegation nonce overflow for id: {0}")] DelegationNonceOverflow(DelegationId), + #[error("Token issuance nonce overflow for id: {0}")] + TokenIssuanceNonceOverflow(TokenId), + #[error("Token with id: {0} with duplicate AccountNonce: {1}")] + InconsistentTokenIssuanceDuplicateNonce(TokenId, AccountNonce), #[error("Empty inputs in token issuance transaction")] MissingTokenId, #[error("Unknown token with Id {0}")] @@ -135,6 +139,14 @@ pub enum WalletError { ConsumedUtxo(UtxoOutPoint), #[error("Selected UTXO is still locked")] LockedUtxo(UtxoOutPoint), + #[error("Selected UTXO is a token v0 and cannot be used")] + TokenV0Utxo(UtxoOutPoint), + #[error("Cannot change Token supply in state: {0}")] + CannotChangeTokenSupply(&'static str), + #[error("Cannot lock Token supply in state: {0}")] + CannotLockTokenSupply(&'static str), + #[error("Cannot revert lock Token supply in state: {0}")] + InconsistentUnLockTokenSupply(&'static str), } /// Result type used for the wallet From 679d0ee5877aa97a2ae0497138ca8307e6acac4f Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Thu, 12 Oct 2023 21:52:24 +0200 Subject: [PATCH 02/11] Add change token supply commands to wallet CLI --- wallet/src/account/mod.rs | 274 ++++++++++++++---- wallet/src/account/output_cache/mod.rs | 68 +++-- wallet/src/account/tests.rs | 4 +- wallet/src/account/utxo_selector/mod.rs | 6 + .../src/account/utxo_selector/output_group.rs | 6 +- wallet/src/lib.rs | 2 +- wallet/src/send_request/mod.rs | 118 +++++++- wallet/src/wallet/mod.rs | 89 +++++- wallet/src/wallet/tests.rs | 68 ++++- wallet/types/src/utxo_types.rs | 2 + .../src/commands/helper_types.rs | 93 +++++- wallet/wallet-cli-lib/src/commands/mod.rs | 142 +++++---- .../src/synced_controller.rs | 63 ++++ 13 files changed, 754 insertions(+), 181 deletions(-) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index f3576c1c86..d80f175dcd 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -32,8 +32,9 @@ use wallet_types::with_locked::WithLocked; use crate::account::utxo_selector::{select_coins, OutputGroup}; use crate::key_chain::{make_path_to_vrf_key, AccountKeyChain, KeyChainError}; use crate::send_request::{ - make_address_output, make_address_output_from_delegation, make_address_output_token, - make_decomission_stake_pool_output, make_stake_output, IssueNftArguments, + get_tx_output_destination, make_address_output, make_address_output_from_delegation, + make_address_output_token, make_decomission_stake_pool_output, make_lock_token_outputs, + make_mint_token_outputs, make_redeem_token_outputs, make_stake_output, IssueNftArguments, StakePoolDataArguments, }; use crate::wallet_events::{WalletEvents, WalletEventsNoOp}; @@ -57,8 +58,9 @@ use crypto::key::PublicKey; use crypto::vrf::{VRFPrivateKey, VRFPublicKey}; use itertools::Itertools; use std::cmp::Reverse; +use std::collections::btree_map::Entry; use std::collections::BTreeMap; -use std::ops::Add; +use std::ops::{Add, Sub}; use std::sync::Arc; use wallet_storage::{ StoreTxRw, WalletStorageReadLocked, WalletStorageReadUnlocked, WalletStorageWriteLocked, @@ -72,10 +74,15 @@ use wallet_types::{ }; pub use self::output_cache::DelegationData; -use self::output_cache::OutputCache; +use self::output_cache::{OutputCache, TokenIssuanceData}; use self::transaction_list::{get_transaction_list, TransactionList}; use self::utxo_selector::{CoinSelectionAlgo, PayFee}; +pub struct CurrentFeeRate { + pub current_fee_rate: FeeRate, + pub consolidate_fee_rate: FeeRate, +} + pub struct Account { chain_config: Arc, key_chain: AccountKeyChain, @@ -176,10 +183,15 @@ impl Account { timestamp: median_time, }; + let mut preselected_inputs = group_preselected_inputs(&request, current_fee_rate)?; + let (utxos, selection_algo) = if input_utxos.is_empty() { ( self.get_utxos( - UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoType::Transfer + | UtxoType::LockThenTransfer + | UtxoType::IssueNft + | UtxoType::MintTokens, median_time, UtxoState::Confirmed | UtxoState::InMempool | UtxoState::Inactive, WithLocked::Unlocked, @@ -209,6 +221,8 @@ impl Account { .iter() .map(|(currency, output_amount)| -> WalletResult<_> { let utxos = utxos_by_currency.remove(currency).unwrap_or(vec![]); + let (preselected_amount, preselected_fee) = + preselected_inputs.remove(currency).unwrap_or((Amount::ZERO, Amount::ZERO)); let cost_of_change = match currency { Currency::Coin => coin_change_fee, @@ -216,7 +230,7 @@ impl Account { }; let selection_result = select_coins( utxos, - *output_amount, + output_amount.sub(preselected_amount).unwrap_or(Amount::ZERO), PayFee::DoNotPayFeeWithThisCurrency, cost_of_change, selection_algo, @@ -224,7 +238,12 @@ impl Account { total_fees_not_paid = (total_fees_not_paid + selection_result.get_total_fees()) .ok_or(WalletError::OutputAmountOverflow)?; + total_fees_not_paid = (total_fees_not_paid + preselected_fee) + .ok_or(WalletError::OutputAmountOverflow)?; + let preselected_change = + (preselected_amount - *output_amount).unwrap_or(Amount::ZERO); + let selection_result = selection_result.add_change(preselected_change)?; let change_amount = selection_result.get_change(); if change_amount > Amount::ZERO { total_fees_not_paid = (total_fees_not_paid + cost_of_change) @@ -236,7 +255,12 @@ impl Account { .try_collect()?; let utxos = utxos_by_currency.remove(&pay_fee_with_currency).unwrap_or(vec![]); + let (preselected_amount, preselected_fee) = preselected_inputs + .remove(&pay_fee_with_currency) + .unwrap_or((Amount::ZERO, Amount::ZERO)); + total_fees_not_paid = + (total_fees_not_paid + preselected_fee).ok_or(WalletError::OutputAmountOverflow)?; let mut amount_to_be_paid_in_currency_with_fees = (amount_to_be_paid_in_currency_with_fees + total_fees_not_paid) .ok_or(WalletError::OutputAmountOverflow)?; @@ -248,12 +272,15 @@ impl Account { let selection_result = select_coins( utxos, - amount_to_be_paid_in_currency_with_fees, + (amount_to_be_paid_in_currency_with_fees - preselected_amount).unwrap_or(Amount::ZERO), PayFee::PayFeeWithThisCurrency, cost_of_change, selection_algo, )?; + let selection_result = selection_result.add_change( + (preselected_amount - amount_to_be_paid_in_currency_with_fees).unwrap_or(Amount::ZERO), + )?; let change_amount = selection_result.get_change(); if change_amount > Amount::ZERO { amount_to_be_paid_in_currency_with_fees = (amount_to_be_paid_in_currency_with_fees @@ -304,7 +331,7 @@ impl Account { let selected_inputs = selected_inputs.into_iter().flat_map(|x| x.1.into_output_pairs()); - Ok(request.with_inputs(selected_inputs)) + request.with_inputs(selected_inputs) } fn utxo_output_groups_by_currency( @@ -319,7 +346,7 @@ impl Account { let tx_input: TxInput = outpoint.into(); let input_size = serialization::Encode::encoded_size(&tx_input); - let destination = Self::get_tx_output_destination(&txo).ok_or_else(|| { + let destination = get_tx_output_destination(&txo).ok_or_else(|| { WalletError::UnsupportedTransactionOutput(Box::new(txo.clone())) })?; @@ -376,16 +403,15 @@ impl Account { request: SendRequest, inputs: Vec, median_time: BlockTimestamp, - current_fee_rate: FeeRate, - consolidate_fee_rate: FeeRate, + fee_rate: CurrentFeeRate, ) -> WalletResult { let request = self.select_inputs_for_send_request( request, inputs, db_tx, median_time, - current_fee_rate, - consolidate_fee_rate, + fee_rate.current_fee_rate, + fee_rate.consolidate_fee_rate, )?; // TODO: Randomize inputs and outputs @@ -543,14 +569,20 @@ impl Account { .ok_or(WalletError::DelegationNotFound(*delegation_id)) } + pub fn find_token(&self, token_id: &TokenId) -> WalletResult<&TokenIssuanceData> { + self.output_cache + .token_data(token_id) + .filter(|data| self.is_mine_or_watched_destination(&data.reissuance_controller)) + .ok_or(WalletError::UnknownTokenId(*token_id)) + } + pub fn create_stake_pool_tx( &mut self, db_tx: &mut impl WalletStorageWriteUnlocked, stake_pool_arguments: StakePoolDataArguments, decomission_key: Option, median_time: BlockTimestamp, - current_fee_rate: FeeRate, - consolidate_fee_rate: FeeRate, + fee_rate: CurrentFeeRate, ) -> WalletResult { // TODO: Use other accounts here let staker = self.key_chain.issue_key(db_tx, KeyPurpose::ReceiveFunds)?; @@ -576,8 +608,8 @@ impl Account { vec![], db_tx, median_time, - current_fee_rate, - consolidate_fee_rate, + fee_rate.current_fee_rate, + fee_rate.consolidate_fee_rate, )?; let new_pool_id = match request @@ -618,10 +650,9 @@ impl Account { db_tx: &mut impl WalletStorageWriteUnlocked, nft_issue_arguments: IssueNftArguments, median_time: BlockTimestamp, - current_fee_rate: FeeRate, - consolidate_fee_rate: FeeRate, + fee_rate: CurrentFeeRate, ) -> WalletResult { - // the first UTXO is needed in advance to calculate pool_id, so just make a dummy one + // the first UTXO is needed in advance to issue a new nft, so just make a dummy one // and then replace it with when we can calculate the pool_id let dummy_token_id = TokenId::new(H256::zero()); let dummy_issuance_output = TxOutput::IssueNft( @@ -643,8 +674,8 @@ impl Account { vec![], db_tx, median_time, - current_fee_rate, - consolidate_fee_rate, + fee_rate.current_fee_rate, + fee_rate.consolidate_fee_rate, )?; let new_token_id = make_token_id(request.inputs()).ok_or(WalletError::NoUtxos)?; @@ -666,13 +697,111 @@ impl Account { (*token_id == dummy_token_id).then_some(token_id) } }) - .expect("find output with dummy_pool_id"); + .expect("find output with dummy_token_id"); *old_token_id = new_token_id; let tx = self.sign_transaction_from_req(request, db_tx)?; Ok(tx) } + pub fn mint_tokens( + &mut self, + db_tx: &mut impl WalletStorageWriteUnlocked, + token_id: TokenId, + address: Address, + amount: Amount, + median_time: BlockTimestamp, + fee_rate: CurrentFeeRate, + ) -> WalletResult { + let outputs = + make_mint_token_outputs(token_id, amount, address, self.chain_config.as_ref())?; + + self.change_token_supply_transaction( + token_id, + amount, + outputs, + db_tx, + median_time, + fee_rate, + ) + } + + fn change_token_supply_transaction( + &mut self, + token_id: TokenId, + amount: Amount, + outputs: Vec, + db_tx: &mut impl WalletStorageWriteUnlocked, + median_time: BlockTimestamp, + fee_rate: CurrentFeeRate, + ) -> Result { + let token_data = self.find_token(&token_id)?; + let nonce = token_data + .last_nonce + .map_or(Some(AccountNonce::new(0)), |nonce| nonce.increment()) + .ok_or(WalletError::TokenIssuanceNonceOverflow(token_id))?; + //FIXME: pass different input in + let tx_input = TxInput::Account(AccountOutPoint::new( + nonce, + AccountOp::MintTokens(token_id, amount), + )); + + let request = SendRequest::new() + .with_outputs(outputs) + .with_inputs_and_destinations([(tx_input, token_data.reissuance_controller.clone())]); + + let request = self.select_inputs_for_send_request( + request, + vec![], + db_tx, + median_time, + fee_rate.current_fee_rate, + fee_rate.consolidate_fee_rate, + )?; + + let tx = self.sign_transaction_from_req(request, db_tx)?; + Ok(tx) + } + + pub fn redeem_tokens( + &mut self, + db_tx: &mut impl WalletStorageWriteUnlocked, + token_id: TokenId, + amount: Amount, + median_time: BlockTimestamp, + fee_rate: CurrentFeeRate, + ) -> WalletResult { + let outputs = make_redeem_token_outputs(token_id, amount, self.chain_config.as_ref())?; + + self.change_token_supply_transaction( + token_id, + amount, + outputs, + db_tx, + median_time, + fee_rate, + ) + } + + pub fn lock_tokens( + &mut self, + db_tx: &mut impl WalletStorageWriteUnlocked, + token_id: TokenId, + median_time: BlockTimestamp, + fee_rate: CurrentFeeRate, + ) -> WalletResult { + let outputs = make_lock_token_outputs(self.chain_config.as_ref())?; + + self.change_token_supply_transaction( + token_id, + Amount::ZERO, + outputs, + db_tx, + median_time, + fee_rate, + ) + } + pub fn get_pos_gen_block_data( &self, db_tx: &impl WalletStorageReadUnlocked, @@ -704,7 +833,7 @@ impl Account { .ok_or(WalletError::UnknownPoolId(pool_id))?; let kernel_input: TxInput = kernel_input_outpoint.into(); - let stake_destination = Self::get_tx_output_destination(kernel_input_utxo) + let stake_destination = get_tx_output_destination(kernel_input_utxo) .expect("must succeed for CreateStakePool and ProduceBlockFromStake outputs"); let stake_private_key = self .key_chain @@ -730,18 +859,11 @@ impl Account { request: SendRequest, db_tx: &impl WalletStorageReadUnlocked, ) -> WalletResult { - let (tx, utxos) = request.into_transaction_and_utxos()?; - let destinations = utxos - .iter() - .map(|utxo| { - Self::get_tx_output_destination(utxo).ok_or_else(|| { - WalletError::UnsupportedTransactionOutput(Box::new(utxo.clone())) - }) - }) - .collect::, WalletError>>()?; - let input_utxos = utxos.iter().map(Some).collect::>(); + let (tx, input_utxos, destinations) = request.into_transaction_and_utxos()?; + let destinations = destinations.iter().collect_vec(); + let input_utxos = input_utxos.iter().map(Option::as_ref).collect_vec(); - self.sign_transaction(tx, destinations.as_slice(), &input_utxos, db_tx) + self.sign_transaction(tx, destinations.as_slice(), input_utxos.as_slice(), db_tx) } // TODO: Use a different type to support partially signed transactions @@ -830,26 +952,10 @@ impl Account { self.key_chain.get_addresses_usage_state() } - fn get_tx_output_destination(txo: &TxOutput) -> Option<&Destination> { - // TODO: Reuse code from TxVerifier - match txo { - TxOutput::Transfer(_, d) - | TxOutput::LockThenTransfer(_, d, _) - | TxOutput::CreateDelegationId(d, _) - | TxOutput::ProduceBlockFromStake(d, _) => Some(d), - TxOutput::CreateStakePool(_, data) => Some(data.staker()), - TxOutput::IssueNft(_, _, d) => Some(d), - TxOutput::IssueFungibleToken(_) - | TxOutput::Burn(_) - | TxOutput::DelegateStaking(_, _) => None, - } - } - /// Return true if this transaction output is can be spent by this account or if it is being /// watched. fn is_mine_or_watched(&self, txo: &TxOutput) -> bool { - Self::get_tx_output_destination(txo) - .map_or(false, |d| self.is_mine_or_watched_destination(d)) + get_tx_output_destination(txo).map_or(false, |d| self.is_mine_or_watched_destination(d)) } /// Return true if this destination can be spent by this account or if it is being watched. @@ -880,7 +986,7 @@ impl Account { db_tx: &mut impl WalletStorageWriteLocked, output: &TxOutput, ) -> WalletResult { - if let Some(d) = Self::get_tx_output_destination(output) { + if let Some(d) = get_tx_output_destination(output) { match d { Destination::Address(pkh) => { let found = self.key_chain.mark_public_key_hash_as_used(db_tx, pkh)?; @@ -1003,13 +1109,9 @@ impl Account { AccountOp::SpendDelegationBalance(delegation_id, _) => { self.find_delegation(delegation_id).is_ok() } - AccountOp::MintTokens(_, _) - | AccountOp::UnmintTokens(_) - | AccountOp::LockTokenSupply(_) => { - // TODO: add support for tokens v1 - // See https://github.com/mintlayer/mintlayer-core/issues/1237 - unimplemented!() - } + AccountOp::MintTokens(token_id, _) + | AccountOp::UnmintTokens(token_id) + | AccountOp::LockTokenSupply(token_id) => self.find_token(token_id).is_ok(), }, }); let relevant_outputs = self.mark_outputs_as_seen(db_tx, tx.outputs())?; @@ -1262,6 +1364,52 @@ impl Account { } } +fn group_preselected_inputs( + request: &SendRequest, + current_fee_rate: FeeRate, +) -> Result, WalletError> { + let mut preselected_inputs = BTreeMap::new(); + for (input, destination) in request.inputs().iter().zip(request.destinations()) { + let input_size = serialization::Encode::encoded_size(&input); + let inp_sig_size = input_signature_size(destination)?; + + let fee = current_fee_rate + .compute_fee(input_size + inp_sig_size) + .map_err(|_| UtxoSelectorError::AmountArithmeticError)?; + + let mut update_preselected_inputs = + |currency: Currency, amount: Amount, fee: Amount| -> WalletResult<()> { + match preselected_inputs.entry(currency) { + Entry::Vacant(entry) => { + entry.insert((amount, fee)); + } + Entry::Occupied(mut entry) => { + let (existing_amount, existing_fee) = entry.get_mut(); + *existing_amount = + (*existing_amount + amount).ok_or(WalletError::OutputAmountOverflow)?; + *existing_fee = + (*existing_fee + fee).ok_or(WalletError::OutputAmountOverflow)?; + } + } + Ok(()) + }; + + match input { + TxInput::Utxo(_) => {} + TxInput::Account(acc) => match acc.account() { + AccountOp::MintTokens(token_id, amount) => { + update_preselected_inputs(Currency::Token(*token_id), *amount, *fee)?; + } + AccountOp::LockTokenSupply(_) | AccountOp::UnmintTokens(_) => {} + AccountOp::SpendDelegationBalance(_, amount) => { + update_preselected_inputs(Currency::Coin, *amount, *fee)?; + } + }, + } + } + Ok(preselected_inputs) +} + #[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)] pub enum Currency { Coin, @@ -1342,12 +1490,14 @@ fn group_utxos_for_input( let output_value = match get_tx_output(&output) { TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) => v.clone(), TxOutput::CreateStakePool(_, stake) => OutputValue::Coin(stake.value()), + TxOutput::IssueNft(token_id, _, _) => { + OutputValue::TokenV1(*token_id, Amount::from_atoms(1)) + } TxOutput::ProduceBlockFromStake(_, _) | TxOutput::Burn(_) | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) - | TxOutput::IssueFungibleToken(_) - | TxOutput::IssueNft(_, _, _) => { + | TxOutput::IssueFungibleToken(_) => { return Err(WalletError::UnsupportedTransactionOutput(Box::new( get_tx_output(&output).clone(), ))) diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index 0425978c34..aa7cf9c09a 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -156,16 +156,18 @@ impl TokenTotalSupplyState { } pub struct TokenIssuanceData { - total_supply: TokenTotalSupplyState, + pub total_supply: TokenTotalSupplyState, + pub reissuance_controller: Destination, pub last_nonce: Option, /// last parent transaction if the parent is unconfirmed pub last_parent: Option, } impl TokenIssuanceData { - fn new(data: TokenTotalSupply) -> Self { + fn new(data: TokenTotalSupply, reissuance_controller: Destination) -> Self { Self { total_supply: data.into(), + reissuance_controller, last_nonce: None, last_parent: None, } @@ -267,6 +269,10 @@ impl OutputCache { self.delegations.get(delegation_id) } + pub fn token_data(&self, token_id: &TokenId) -> Option<&TokenIssuanceData> { + self.token_issuance.get(token_id) + } + pub fn add_tx(&mut self, tx_id: OutPointSourceId, tx: WalletTx) -> WalletResult<()> { let already_present = self.txs.contains_key(&tx_id); let is_unconfirmed = match tx.state() { @@ -282,15 +288,19 @@ impl OutputCache { self.update_inputs(&tx, is_unconfirmed, &tx_id, already_present)?; - if let Some(block_info) = get_block_info(&tx) { - self.update_outputs(&tx, block_info)?; - } + self.update_outputs(&tx, get_block_info(&tx), already_present)?; + self.txs.insert(tx_id, tx); Ok(()) } /// Update the pool states for a newly confirmed transaction - fn update_outputs(&mut self, tx: &WalletTx, block_info: BlockInfo) -> Result<(), WalletError> { + fn update_outputs( + &mut self, + tx: &WalletTx, + block_info: Option, + already_present: bool, + ) -> Result<(), WalletError> { for (idx, output) in tx.outputs().iter().enumerate() { match output { TxOutput::ProduceBlockFromStake(_, pool_id) => { @@ -301,26 +311,34 @@ impl OutputCache { } } TxOutput::CreateStakePool(pool_id, data) => { - self.pools - .entry(*pool_id) - .and_modify(|entry| { - entry.utxo_outpoint = UtxoOutPoint::new(tx.id(), idx as u32) - }) - .or_insert_with(|| { - PoolData::new( - UtxoOutPoint::new(tx.id(), idx as u32), - block_info, - data.decommission_key().clone(), - ) - }); + if let Some(block_info) = block_info { + self.pools + .entry(*pool_id) + .and_modify(|entry| { + entry.utxo_outpoint = UtxoOutPoint::new(tx.id(), idx as u32) + }) + .or_insert_with(|| { + PoolData::new( + UtxoOutPoint::new(tx.id(), idx as u32), + block_info, + data.decommission_key().clone(), + ) + }); + } } TxOutput::DelegateStaking(_, delegation_id) => { + if block_info.is_none() { + continue; + } if let Some(delegation_data) = self.delegations.get_mut(delegation_id) { delegation_data.not_staked_yet = false; } // Else it is not ours } TxOutput::CreateDelegationId(destination, pool_id) => { + if block_info.is_none() { + continue; + } let input0_outpoint = tx .inputs() .get(0) @@ -336,13 +354,21 @@ impl OutputCache { | TxOutput::Burn(_) | TxOutput::Transfer(_, _) | TxOutput::LockThenTransfer(_, _, _) => {} - | TxOutput::IssueFungibleToken(issuance) => { + TxOutput::IssueFungibleToken(issuance) => { + if already_present { + continue; + } let input0_outpoint = tx.inputs(); let token_id = make_token_id(input0_outpoint).ok_or(WalletError::NoUtxos)?; match issuance.as_ref() { TokenIssuance::V1(data) => { - self.token_issuance - .insert(token_id, TokenIssuanceData::new(data.total_supply)); + self.token_issuance.insert( + token_id, + TokenIssuanceData::new( + data.total_supply, + data.reissuance_controller.clone(), + ), + ); } } } diff --git a/wallet/src/account/tests.rs b/wallet/src/account/tests.rs index 19495c4fef..72dc6f735f 100644 --- a/wallet/src/account/tests.rs +++ b/wallet/src/account/tests.rs @@ -212,14 +212,14 @@ fn sign_transaction(#[case] seed: Seed) { let tx = Transaction::new(0, inputs, outputs).unwrap(); - let req = SendRequest::from_transaction(tx, utxos.clone()); + let req = SendRequest::from_transaction(tx, utxos.clone()).unwrap(); let sig_tx = account.sign_transaction_from_req(req, &db_tx).unwrap(); let utxos_ref = utxos.iter().map(Some).collect::>(); for i in 0..sig_tx.inputs().len() { - let destination = Account::get_tx_output_destination(utxos_ref[i].unwrap()).unwrap(); + let destination = get_tx_output_destination(utxos_ref[i].unwrap()).unwrap(); verify_signature(&config, destination, &sig_tx, &utxos_ref, i).unwrap(); } } diff --git a/wallet/src/account/utxo_selector/mod.rs b/wallet/src/account/utxo_selector/mod.rs index c0a05e80ab..96bbb391a4 100644 --- a/wallet/src/account/utxo_selector/mod.rs +++ b/wallet/src/account/utxo_selector/mod.rs @@ -27,6 +27,7 @@ use utils::ensure; const TOTAL_TRIES: u32 = 100_000; +#[derive(Debug)] pub struct SelectionResult { outputs: Vec<(TxInput, TxOutput)>, effective_value: Amount, @@ -58,6 +59,11 @@ impl SelectionResult { self.change } + pub fn add_change(mut self, change: Amount) -> Result { + self.change = (self.change + change).ok_or(UtxoSelectorError::AmountArithmeticError)?; + Ok(self) + } + pub fn into_output_pairs(self) -> Vec<(TxInput, TxOutput)> { self.outputs } diff --git a/wallet/src/account/utxo_selector/output_group.rs b/wallet/src/account/utxo_selector/output_group.rs index b64b9abc50..056663b507 100644 --- a/wallet/src/account/utxo_selector/output_group.rs +++ b/wallet/src/account/utxo_selector/output_group.rs @@ -56,13 +56,15 @@ impl OutputGroup { ) -> Result { let output_value = match &output.1 { TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) => v.clone(), + TxOutput::IssueNft(token_id, _, _) => { + OutputValue::TokenV1(*token_id, Amount::from_atoms(1)) + } TxOutput::CreateStakePool(_, _) | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::Burn(_) | TxOutput::CreateDelegationId(_, _) | TxOutput::DelegateStaking(_, _) - | TxOutput::IssueFungibleToken(_) - | TxOutput::IssueNft(_, _, _) => { + | TxOutput::IssueFungibleToken(_) => { return Err(UtxoSelectorError::UnsupportedTransactionOutput(Box::new( output.1.clone(), ))) diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index debbf3d44e..49c5bb839e 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -21,7 +21,7 @@ pub mod wallet; pub mod wallet_events; pub use crate::account::Account; -pub use crate::send_request::SendRequest; +pub use crate::send_request::{get_tx_output_destination, SendRequest}; pub use crate::wallet::{Wallet, WalletError, WalletResult}; pub type DefaultWallet = Wallet; diff --git a/wallet/src/send_request/mod.rs b/wallet/src/send_request/mod.rs index 1d0caf864a..53b73ccb31 100644 --- a/wallet/src/send_request/mod.rs +++ b/wallet/src/send_request/mod.rs @@ -26,7 +26,7 @@ use common::primitives::{Amount, BlockHeight}; use crypto::key::PublicKey; use crypto::vrf::VRFPublicKey; -use crate::WalletResult; +use crate::{WalletError, WalletResult}; /// The `SendRequest` struct provides the necessary information to the wallet /// on the precise method of sending funds to a designated destination. @@ -35,7 +35,10 @@ pub struct SendRequest { flags: u128, /// The UTXOs for each input, this can be empty - utxos: Vec, + utxos: Vec>, + + /// destination for each input + destinations: Vec, inputs: Vec, @@ -80,6 +83,44 @@ pub fn make_issue_token_outputs( Ok(vec![issuance_output, token_issuance_fee]) } +pub fn make_mint_token_outputs( + token_id: TokenId, + amount: Amount, + address: Address, + chain_config: &ChainConfig, +) -> WalletResult> { + let destination = address.decode_object(chain_config)?; + let mint_output = TxOutput::Transfer(OutputValue::TokenV1(token_id, amount), destination); + + let token_change_supply_fee = TxOutput::Burn(OutputValue::Coin( + chain_config.token_min_supply_change_fee(), + )); + + Ok(vec![mint_output, token_change_supply_fee]) +} + +pub fn make_redeem_token_outputs( + token_id: TokenId, + amount: Amount, + chain_config: &ChainConfig, +) -> WalletResult> { + let burn_tokens = TxOutput::Burn(OutputValue::TokenV1(token_id, amount)); + + let token_change_supply_fee = TxOutput::Burn(OutputValue::Coin( + chain_config.token_min_supply_change_fee(), + )); + + Ok(vec![burn_tokens, token_change_supply_fee]) +} + +pub fn make_lock_token_outputs(chain_config: &ChainConfig) -> WalletResult> { + let token_change_supply_fee = TxOutput::Burn(OutputValue::Coin( + chain_config.token_min_supply_change_fee(), + )); + + Ok(vec![token_change_supply_fee]) +} + pub fn make_create_delegation_output( chain_config: &ChainConfig, address: Address, @@ -156,43 +197,78 @@ pub struct IssueNftArguments { pub destination: Destination, } +type TxAndInputs = (Transaction, Vec>, Vec); + impl SendRequest { pub fn new() -> Self { Self { flags: 0, utxos: Vec::new(), + destinations: Vec::new(), inputs: Vec::new(), outputs: Vec::new(), } } - pub fn from_transaction(transaction: Transaction, utxos: Vec) -> Self { - Self { + pub fn from_transaction(transaction: Transaction, utxos: Vec) -> WalletResult { + let destinations = utxos + .iter() + .map(|utxo| { + get_tx_output_destination(utxo).cloned().ok_or_else(|| { + WalletError::UnsupportedTransactionOutput(Box::new(utxo.clone())) + }) + }) + .collect::>>()?; + + Ok(Self { flags: transaction.flags(), - utxos, + utxos: utxos.into_iter().map(Some).collect(), + destinations, inputs: transaction.inputs().to_vec(), outputs: transaction.outputs().to_vec(), - } + }) } pub fn inputs(&self) -> &[TxInput] { &self.inputs } + pub fn destinations(&self) -> &[Destination] { + &self.destinations + } + pub fn outputs(&self) -> &[TxOutput] { &self.outputs } - pub fn utxos(&self) -> &[TxOutput] { - &self.utxos + pub fn with_inputs_and_destinations( + mut self, + utxos: impl IntoIterator, + ) -> Self { + for (outpoint, destination) in utxos { + self.inputs.push(outpoint); + self.destinations.push(destination); + self.utxos.push(None); + } + + self } - pub fn with_inputs(mut self, utxos: impl IntoIterator) -> Self { + pub fn with_inputs( + mut self, + utxos: impl IntoIterator, + ) -> WalletResult { for (outpoint, txo) in utxos { self.inputs.push(outpoint); - self.utxos.push(txo); + self.destinations.push( + get_tx_output_destination(&txo).cloned().ok_or_else(|| { + WalletError::UnsupportedTransactionOutput(Box::new(txo.clone())) + })?, + ); + self.utxos.push(Some(txo)); } - self + + Ok(self) } pub fn with_outputs(mut self, outputs: impl IntoIterator) -> Self { @@ -204,10 +280,22 @@ impl SendRequest { &mut self.outputs } - pub fn into_transaction_and_utxos( - self, - ) -> Result<(Transaction, Vec), TransactionCreationError> { + pub fn into_transaction_and_utxos(self) -> Result { let tx = Transaction::new(self.flags, self.inputs, self.outputs)?; - Ok((tx, self.utxos)) + Ok((tx, self.utxos, self.destinations)) + } +} + +pub fn get_tx_output_destination(txo: &TxOutput) -> Option<&Destination> { + match txo { + TxOutput::Transfer(_, d) + | TxOutput::LockThenTransfer(_, d, _) + | TxOutput::CreateDelegationId(d, _) + | TxOutput::IssueNft(_, _, d) + | TxOutput::ProduceBlockFromStake(d, _) => Some(d), + TxOutput::CreateStakePool(_, data) => Some(data.staker()), + TxOutput::IssueFungibleToken(_) | TxOutput::Burn(_) | TxOutput::DelegateStaking(_, _) => { + None + } } } diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 60e8ab6de4..d8bfdb6b53 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -18,7 +18,7 @@ use std::path::Path; use std::sync::Arc; use crate::account::transaction_list::TransactionList; -use crate::account::{Currency, DelegationData, UtxoSelectorError}; +use crate::account::{Currency, CurrentFeeRate, DelegationData, UtxoSelectorError}; use crate::key_chain::{KeyChainError, MasterKeyChain}; use crate::send_request::{make_issue_token_outputs, IssueNftArguments, StakePoolDataArguments}; use crate::wallet_events::{WalletEvents, WalletEventsNoOp}; @@ -768,8 +768,10 @@ impl Wallet { request, inputs, latest_median_time, - current_fee_rate, - consolidate_fee_rate, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, ) }) } @@ -795,6 +797,75 @@ impl Wallet { }) } + pub fn mint_tokens( + &mut self, + account_index: U31, + token_id: TokenId, + amount: Amount, + destination: Address, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> WalletResult { + let latest_median_time = self.latest_median_time; + self.for_account_rw_unlocked(account_index, |account, db_tx| { + account.mint_tokens( + db_tx, + token_id, + destination, + amount, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }) + } + + pub fn redeem_tokens( + &mut self, + account_index: U31, + token_id: TokenId, + amount: Amount, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> WalletResult { + let latest_median_time = self.latest_median_time; + self.for_account_rw_unlocked(account_index, |account, db_tx| { + account.redeem_tokens( + db_tx, + token_id, + amount, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }) + } + + pub fn lock_tokens( + &mut self, + account_index: U31, + token_id: TokenId, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> WalletResult { + let latest_median_time = self.latest_median_time; + self.for_account_rw_unlocked(account_index, |account, db_tx| { + account.lock_tokens( + db_tx, + token_id, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }) + } + pub fn create_delegation( &mut self, account_index: U31, @@ -861,8 +932,10 @@ impl Wallet { destination, }, latest_median_time, - current_fee_rate, - consolidate_fee_rate, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, ) })?; @@ -886,8 +959,10 @@ impl Wallet { stake_pool_arguments, decommission_key, latest_median_time, - current_fee_rate, - consolidate_fee_rate, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, ) }) } diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index c564deedbd..0563955959 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -2015,9 +2015,6 @@ fn create_spend_from_delegations(#[case] seed: Seed) { assert_eq!(deleg_data.last_nonce, Some(AccountNonce::new(0))); } -// TODO: add support for tokens v1 -// See https://github.com/mintlayer/mintlayer-core/issues/1237 -#[ignore] #[rstest] #[trace] #[case(Seed::from_entropy())] @@ -2041,9 +2038,16 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { assert_eq!(coin_balance, Amount::ZERO); // Generate a new block which sends reward to the wallet - let block1_amount = (Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)) - + chain_config.token_min_issuance_fee()) - .unwrap(); + let mut block1_amount = + (Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)) + + chain_config.token_min_issuance_fee()) + .unwrap(); + + let issue_token = rng.gen::(); + if issue_token { + block1_amount = (block1_amount + chain_config.token_min_supply_change_fee()).unwrap(); + } + let address = get_address( &chain_config, MNEMONIC, @@ -2088,8 +2092,8 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { let amount_fraction = (block1_amount.into_atoms() - NETWORK_FEE) / 10; let mut token_amount_to_issue = Amount::from_atoms(rng.gen_range(1..amount_fraction)); - let (issued_token_id, token_issuance_transaction) = if rng.gen::() { - wallet + let (issued_token_id, token_issuance_transaction) = if issue_token { + let (issued_token_id, token_issuance_transaction) = wallet .issue_new_token( DEFAULT_ACCOUNT_INDEX, TokenIssuance::V1(TokenIssuanceV1 { @@ -2102,10 +2106,30 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { FeeRate::new(Amount::ZERO), FeeRate::new(Amount::ZERO), ) - .unwrap() + .unwrap(); + + wallet + .add_unconfirmed_tx(token_issuance_transaction.clone(), &WalletEventsNoOp) + .unwrap(); + + let mint_transaction = wallet + .mint_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_issue, + address2, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + ( + issued_token_id, + vec![token_issuance_transaction, mint_transaction], + ) } else { token_amount_to_issue = Amount::from_atoms(1); - wallet + let (issued_token_id, token_issuance_transaction) = wallet .issue_new_nft( DEFAULT_ACCOUNT_INDEX, address2, @@ -2122,11 +2146,12 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { FeeRate::new(Amount::ZERO), FeeRate::new(Amount::ZERO), ) - .unwrap() + .unwrap(); + (issued_token_id, vec![token_issuance_transaction]) }; let block2 = Block::new( - vec![token_issuance_transaction], + token_issuance_transaction, block1_id.into(), block1_timestamp, ConsensusData::None, @@ -2144,14 +2169,22 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { let currency_balances = wallet .get_balance( DEFAULT_ACCOUNT_INDEX, - UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoType::Transfer + | UtxoType::LockThenTransfer + | UtxoType::MintTokens + | UtxoType::IssueNft, UtxoState::Confirmed.into(), WithLocked::Unlocked, ) .unwrap(); + let mut expected_amount = + ((block1_amount * 2).unwrap() - chain_config.token_min_issuance_fee()).unwrap(); + if issue_token { + expected_amount = (expected_amount - chain_config.token_min_supply_change_fee()).unwrap(); + } assert_eq!( currency_balances.get(&Currency::Coin).copied().unwrap_or(Amount::ZERO), - ((block1_amount * 2).unwrap() - chain_config.token_min_issuance_fee()).unwrap() + expected_amount, ); let token_balances = currency_balances .into_iter() @@ -2213,9 +2246,14 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { WithLocked::Unlocked, ) .unwrap(); + let mut expected_amount = + ((block1_amount * 3).unwrap() - chain_config.token_min_issuance_fee()).unwrap(); + if issue_token { + expected_amount = (expected_amount - chain_config.token_min_supply_change_fee()).unwrap(); + } assert_eq!( currency_balances.get(&Currency::Coin).copied().unwrap_or(Amount::ZERO), - ((block1_amount * 3).unwrap() - chain_config.token_min_issuance_fee()).unwrap() + expected_amount, ); let token_balances = currency_balances .into_iter() diff --git a/wallet/types/src/utxo_types.rs b/wallet/types/src/utxo_types.rs index 3dc225f026..7c93f28159 100644 --- a/wallet/types/src/utxo_types.rs +++ b/wallet/types/src/utxo_types.rs @@ -31,6 +31,8 @@ pub enum UtxoType { ProduceBlockFromStake = 1 << 4, CreateDelegationId = 1 << 5, DelegateStaking = 1 << 6, + MintTokens = 1 << 7, + IssueNft = 1 << 8, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/wallet/wallet-cli-lib/src/commands/helper_types.rs b/wallet/wallet-cli-lib/src/commands/helper_types.rs index 9bf5d80f38..0c9ace96bd 100644 --- a/wallet/wallet-cli-lib/src/commands/helper_types.rs +++ b/wallet/wallet-cli-lib/src/commands/helper_types.rs @@ -21,10 +21,11 @@ use wallet_controller::{UtxoState, UtxoStates, UtxoType, UtxoTypes}; use common::{ address::Address, chain::{ - block::timestamp::BlockTimestamp, ChainConfig, DelegationId, OutPointSourceId, PoolId, - UtxoOutPoint, + block::timestamp::BlockTimestamp, + tokens::{TokenId, TokenTotalSupply}, + ChainConfig, DelegationId, Destination, OutPointSourceId, PoolId, UtxoOutPoint, }, - primitives::{Amount, BlockHeight, Id, H256}, + primitives::{per_thousand::PerThousand, Amount, BlockHeight, Id, H256}, }; use wallet_types::{seed_phrase::StoreSeedPhrase, with_locked::WithLocked}; @@ -202,6 +203,92 @@ pub fn parse_utxo_outpoint(mut input: String) -> Result Result { + match input { + "unlimited" => Ok(TokenTotalSupply::Unlimited), + "lockable" => Ok(TokenTotalSupply::Lockable), + _ => parse_fixed_token_supply(input, token_number_of_decimals), + } +} + +/// Try to parse a fixed total token supply in the format of "fixed(Amount)" +fn parse_fixed_token_supply( + input: &str, + token_number_of_decimals: u8, +) -> Result { + if let Some(inner) = input.strip_prefix("fixed(").and_then(|str| str.strip_suffix(')')) { + Ok(TokenTotalSupply::Fixed(parse_token_amount( + token_number_of_decimals, + inner, + )?)) + } else { + Err(WalletCliError::InvalidInput(format!( + "Failed to parse token supply from {input}" + ))) + } +} + +pub fn to_per_thousand( + value_str: &str, + variable_name: &str, +) -> Result { + PerThousand::from_decimal_str(value_str).ok_or(WalletCliError::InvalidInput(format!( + "Failed to parse {variable_name} the decimal that must be in the range [0.001,1.000] or [0.1%,100%]", + ))) +} + +pub fn parse_address( + chain_config: &ChainConfig, + address: &str, +) -> Result, WalletCliError> { + Address::from_str(chain_config, address) + .map_err(|e| WalletCliError::InvalidInput(format!("Invalid address '{address}': {e}"))) +} + +pub fn parse_pool_id(chain_config: &ChainConfig, pool_id: &str) -> Result { + Address::::from_str(chain_config, pool_id) + .and_then(|address| address.decode_object(chain_config)) + .map_err(|e| WalletCliError::InvalidInput(format!("Invalid pool ID '{pool_id}': {e}"))) +} + +pub fn parse_token_id( + chain_config: &ChainConfig, + token_id: &str, +) -> Result { + Address::::from_str(chain_config, token_id) + .and_then(|address| address.decode_object(chain_config)) + .map_err(|e| WalletCliError::InvalidInput(format!("Invalid token ID '{token_id}': {e}"))) +} + +pub fn parse_coin_amount( + chain_config: &ChainConfig, + value: &str, +) -> Result { + Amount::from_fixedpoint_str(value, chain_config.coin_decimals()) + .ok_or_else(|| WalletCliError::InvalidInput(value.to_owned())) +} + +pub fn parse_token_amount( + token_number_of_decimals: u8, + value: &str, +) -> Result { + Amount::from_fixedpoint_str(value, token_number_of_decimals) + .ok_or_else(|| WalletCliError::InvalidInput(value.to_owned())) +} + +pub fn print_coin_amount(chain_config: &ChainConfig, value: Amount) -> String { + value.into_fixedpoint_str(chain_config.coin_decimals()) +} + +pub fn print_token_amount(token_number_of_decimals: u8, value: Amount) -> String { + value.into_fixedpoint_str(token_number_of_decimals) +} + #[cfg(test)] mod tests { use rstest::rstest; diff --git a/wallet/wallet-cli-lib/src/commands/mod.rs b/wallet/wallet-cli-lib/src/commands/mod.rs index 7e0ae6883e..ccc5882a2e 100644 --- a/wallet/wallet-cli-lib/src/commands/mod.rs +++ b/wallet/wallet-cli-lib/src/commands/mod.rs @@ -22,10 +22,10 @@ use clap::Parser; use common::{ address::Address, chain::{ - tokens::{Metadata, TokenCreator, TokenId}, - Block, ChainConfig, Destination, PoolId, SignedTransaction, Transaction, UtxoOutPoint, + tokens::{Metadata, TokenCreator}, + Block, ChainConfig, SignedTransaction, Transaction, UtxoOutPoint, }, - primitives::{per_thousand::PerThousand, Amount, BlockHeight, Id, H256}, + primitives::{Amount, BlockHeight, Id, H256}, }; use crypto::key::{hdkd::u31::U31, PublicKey}; use mempool::tx_accumulator::PackingStrategy; @@ -40,11 +40,16 @@ use wallet_controller::{ ControllerError, NodeInterface, NodeRpcClient, PeerId, DEFAULT_ACCOUNT_INDEX, }; -use crate::{errors::WalletCliError, CliController}; +use crate::{ + commands::helper_types::{parse_address, parse_token_supply}, + errors::WalletCliError, + CliController, +}; use self::helper_types::{ - format_delegation_info, format_pool_info, parse_utxo_outpoint, CliStoreSeedPhrase, - CliUtxoState, CliUtxoTypes, CliWithLocked, + format_delegation_info, format_pool_info, parse_coin_amount, parse_pool_id, parse_token_amount, + parse_token_id, parse_utxo_outpoint, print_coin_amount, print_token_amount, to_per_thousand, + CliStoreSeedPhrase, CliUtxoState, CliUtxoTypes, CliWithLocked, }; #[derive(Debug, Parser)] @@ -186,6 +191,7 @@ pub enum WalletCommand { number_of_decimals: u8, metadata_uri: String, destination_address: String, + token_supply: String, }, /// Issue a new token @@ -201,6 +207,24 @@ pub enum WalletCommand { additional_metadata_uri: Option, }, + /// Mint new tokens and increase the total supply + MintTokens { + token_id: String, + address: String, + amount: String, + }, + + /// Redeem existing tokens and reduce the total supply + RedeemTokens { + token_id: String, + amount: String, + }, + + /// Lock the total supply for the tokens + LockTokens { + token_id: String, + }, + /// Rescan Rescan, @@ -363,50 +387,6 @@ pub enum ConsoleCommand { Exit, } -fn to_per_thousand(value_str: &str, variable_name: &str) -> Result { - PerThousand::from_decimal_str(value_str).ok_or(WalletCliError::InvalidInput(format!( - "Failed to parse {variable_name} the decimal that must be in the range [0.001,1.000] or [0.1%,100%]", - ))) -} - -fn parse_address( - chain_config: &ChainConfig, - address: &str, -) -> Result, WalletCliError> { - Address::from_str(chain_config, address) - .map_err(|e| WalletCliError::InvalidInput(format!("Invalid address '{address}': {e}"))) -} - -fn parse_pool_id(chain_config: &ChainConfig, pool_id: &str) -> Result { - Address::::from_str(chain_config, pool_id) - .and_then(|address| address.decode_object(chain_config)) - .map_err(|e| WalletCliError::InvalidInput(format!("Invalid pool ID '{pool_id}': {e}"))) -} - -fn parse_token_id(chain_config: &ChainConfig, token_id: &str) -> Result { - Address::::from_str(chain_config, token_id) - .and_then(|address| address.decode_object(chain_config)) - .map_err(|e| WalletCliError::InvalidInput(format!("Invalid token ID '{token_id}': {e}"))) -} - -fn parse_coin_amount(chain_config: &ChainConfig, value: &str) -> Result { - Amount::from_fixedpoint_str(value, chain_config.coin_decimals()) - .ok_or_else(|| WalletCliError::InvalidInput(value.to_owned())) -} - -fn parse_token_amount(token_number_of_decimals: u8, value: &str) -> Result { - Amount::from_fixedpoint_str(value, token_number_of_decimals) - .ok_or_else(|| WalletCliError::InvalidInput(value.to_owned())) -} - -fn print_coin_amount(chain_config: &ChainConfig, value: Amount) -> String { - value.into_fixedpoint_str(chain_config.coin_decimals()) -} - -fn print_token_amount(token_number_of_decimals: u8, value: Amount) -> String { - value.into_fixedpoint_str(token_number_of_decimals) -} - struct CliWalletState { selected_account: U31, } @@ -797,6 +777,7 @@ impl CommandHandler { number_of_decimals, metadata_uri, destination_address, + token_supply, } => { ensure!( number_of_decimals <= chain_config.token_max_dec_count(), @@ -806,6 +787,7 @@ impl CommandHandler { ); let destination_address = parse_address(chain_config, &destination_address)?; + let token_supply = parse_token_supply(&token_supply, number_of_decimals)?; let token_id = self .get_synced_controller() @@ -815,9 +797,7 @@ impl CommandHandler { token_ticker.into_bytes(), number_of_decimals, metadata_uri.into_bytes(), - // TODO: add support for tokens v1 - // See https://github.com/mintlayer/mintlayer-core/issues/1237 - common::chain::tokens::TokenTotalSupply::Unlimited, + token_supply, ) .await .map_err(WalletCliError::Controller)?; @@ -867,6 +847,62 @@ impl CommandHandler { ))) } + WalletCommand::MintTokens { + token_id, + address, + amount, + } => { + let token_id = parse_token_id(chain_config, token_id.as_str())?; + let address = parse_address(chain_config, &address)?; + let amount = { + let token_number_of_decimals = self + .controller()? + .get_token_number_of_decimals(token_id) + .await + .map_err(WalletCliError::Controller)?; + parse_token_amount(token_number_of_decimals, &amount)? + }; + + self.get_synced_controller() + .await? + .mint_tokens(token_id, amount, address) + .await + .map_err(WalletCliError::Controller)?; + Ok(Self::tx_submitted_command()) + } + + WalletCommand::RedeemTokens { token_id, amount } => { + let token_id = parse_token_id(chain_config, token_id.as_str())?; + let amount = { + let token_number_of_decimals = self + .controller()? + .get_token_number_of_decimals(token_id) + .await + .map_err(WalletCliError::Controller)?; + parse_token_amount(token_number_of_decimals, &amount)? + }; + + self.get_synced_controller() + .await? + .redeem_tokens(token_id, amount) + .await + .map_err(WalletCliError::Controller)?; + + Ok(Self::tx_submitted_command()) + } + + WalletCommand::LockTokens { token_id } => { + let token_id = parse_token_id(chain_config, token_id.as_str())?; + + self.get_synced_controller() + .await? + .lock_tokens(token_id) + .await + .map_err(WalletCliError::Controller)?; + + Ok(Self::tx_submitted_command()) + } + WalletCommand::Rescan => { let controller = self.controller()?; controller.reset_wallet_to_genesis().map_err(WalletCliError::Controller)?; diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index ac883983d7..d9f7161a42 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -159,6 +159,69 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { Ok(token_id) } + pub async fn mint_tokens( + &mut self, + token_id: TokenId, + amount: Amount, + address: Address, + ) -> Result<(), ControllerError> { + let (current_fee_rate, consolidate_fee_rate) = + self.get_current_and_consolidation_fee_rate().await?; + + let tx = self + .wallet + .mint_tokens( + self.account_index, + token_id, + amount, + address, + current_fee_rate, + consolidate_fee_rate, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await + } + + pub async fn redeem_tokens( + &mut self, + token_id: TokenId, + amount: Amount, + ) -> Result<(), ControllerError> { + let (current_fee_rate, consolidate_fee_rate) = + self.get_current_and_consolidation_fee_rate().await?; + + let tx = self + .wallet + .redeem_tokens( + self.account_index, + token_id, + amount, + current_fee_rate, + consolidate_fee_rate, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await + } + + pub async fn lock_tokens(&mut self, token_id: TokenId) -> Result<(), ControllerError> { + let (current_fee_rate, consolidate_fee_rate) = + self.get_current_and_consolidation_fee_rate().await?; + + let tx = self + .wallet + .lock_tokens( + self.account_index, + token_id, + current_fee_rate, + consolidate_fee_rate, + ) + .map_err(ControllerError::WalletError)?; + + self.broadcast_to_mempool(tx).await + } + pub async fn send_to_address( &mut self, address: Address, From 2b000d4a750f501727aebc8ab63a5039b726d9c6 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Fri, 13 Oct 2023 00:50:59 +0200 Subject: [PATCH 03/11] Fix and add tests for change token supply in the wallet --- .../test_framework/wallet_cli_controller.py | 14 +- test/functional/test_runner.py | 1 + test/functional/wallet_tokens.py | 42 +-- .../functional/wallet_tokens_change_supply.py | 193 ++++++++++++ wallet/src/account/mod.rs | 90 +++--- wallet/src/account/output_cache/mod.rs | 100 ++++-- wallet/src/account/utxo_selector/mod.rs | 3 + .../src/account/utxo_selector/output_group.rs | 2 +- wallet/src/wallet/mod.rs | 12 +- wallet/src/wallet/tests.rs | 297 ++++++++++++++++++ wallet/wallet-controller/src/read.rs | 5 +- 11 files changed, 676 insertions(+), 83 deletions(-) create mode 100644 test/functional/wallet_tokens_change_supply.py diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index e87a975b36..a694c23d12 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -178,13 +178,23 @@ async def issue_new_token(self, amount_to_issue: str, number_of_decimals: int, metadata_uri: str, - destination_address: str) -> Tuple[Optional[str], Optional[str]]: - output = await self._write_command(f'issuenewtoken "{token_ticker}" "{amount_to_issue}" "{number_of_decimals}" "{metadata_uri}" {destination_address}\n') + destination_address: str, + token_supply: str = 'unlimited') -> Tuple[Optional[str], Optional[str]]: + output = await self._write_command(f'issuenewtoken "{token_ticker}" "{amount_to_issue}" "{number_of_decimals}" "{metadata_uri}" {destination_address} {token_supply}\n') if output.startswith("A new token has been issued with ID"): return output[output.find(':')+2:], None return None, output + async def mint_tokens(self, token_id: str, address: str, amount: int) -> str: + return await self._write_command(f"minttokens {token_id} {address} {amount}\n") + + async def redeem_tokens(self, token_id: str, amount: int) -> str: + return await self._write_command(f"redeemtokens {token_id} {amount}\n") + + async def lock_tokens(self, token_id: str) -> str: + return await self._write_command(f"locktokens {token_id}\n") + async def issue_new_nft(self, destination_address: str, media_hash: str, diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 7d3e176b70..f5e1a7110b 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -132,6 +132,7 @@ class UnicodeOnWindowsError(ValueError): 'wallet_recover_accounts.py', 'wallet_get_address_usage.py', 'wallet_tokens.py', + 'wallet_tokens_change_supply.py', 'wallet_nfts.py', 'wallet_delegations.py', 'wallet_high_fee.py', diff --git a/test/functional/wallet_tokens.py b/test/functional/wallet_tokens.py index 9ad7f6edec..414a12c8dd 100644 --- a/test/functional/wallet_tokens.py +++ b/test/functional/wallet_tokens.py @@ -31,7 +31,7 @@ from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) from test_framework.util import assert_in, assert_equal from test_framework.mintlayer import mintlayer_hash, block_input_data_obj -from test_framework.wallet_cli_controller import WalletCliController +from test_framework.wallet_cli_controller import DEFAULT_ACCOUNT_INDEX, WalletCliController import asyncio import sys @@ -90,7 +90,7 @@ async def async_test(self): # Submit a valid transaction output = { - 'Transfer': [ { 'Coin': 101 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ], + 'Transfer': [ { 'Coin': 201 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ], } encoded_tx, tx_id = make_tx([reward_input(tip_id)], [output], 0) @@ -107,7 +107,7 @@ async def async_test(self): assert_equal(await wallet.get_best_block_height(), '1') assert_equal(await wallet.get_best_block(), block_id) - assert_in("Coins amount: 101", await wallet.get_balance()) + assert_in("Coins amount: 201", await wallet.get_balance()) address = await wallet.new_address() @@ -144,31 +144,33 @@ async def async_test(self): self.generate_block() assert_in("Success", await wallet.sync()) - # TODO: add support for tokens v1 - # Hint with tokens v1 they have to be minted before any could be sent - # See https://github.com/mintlayer/mintlayer-core/issues/1237 - #assert_in(f"{token_id} amount: 10000", await wallet.get_balance()) + assert_in("The transaction was submitted successfully", await wallet.mint_tokens(token_id, address, 10000)) + + self.generate_block() + assert_in("Success", await wallet.sync()) + + assert_in(f"{token_id} amount: 10000", await wallet.get_balance()) ## create a new account and send some tokens to it - #await wallet.create_new_account() - #await wallet.select_account(1) - #address = await wallet.new_address() + await wallet.create_new_account() + await wallet.select_account(1) + address = await wallet.new_address() - #await wallet.select_account(0) - #output = await wallet.send_tokens_to_address(token_id, address, 10.01) - #assert_in("The transaction was submitted successfully", output) + await wallet.select_account(DEFAULT_ACCOUNT_INDEX) + output = await wallet.send_tokens_to_address(token_id, address, 10.01) + assert_in("The transaction was submitted successfully", output) - #self.generate_block() - #assert_in("Success", await wallet.sync()) + self.generate_block() + assert_in("Success", await wallet.sync()) ## check the new balance - #assert_in(f"{token_id} amount: 9989.99", await wallet.get_balance()) + assert_in(f"{token_id} amount: 9989.99", await wallet.get_balance()) ## try to issue a new token, should fail with not enough coins - #token_id, err = await wallet.issue_new_token("XXX", "10000", 2, "http://uri", address) - #assert token_id is None - #assert err is not None - #assert_in("Not enough funds", err) + token_id, err = await wallet.issue_new_token("XXX", "10000", 2, "http://uri", address) + assert token_id is None + assert err is not None + assert_in("Not enough funds", err) if __name__ == '__main__': WalletTokens().main() diff --git a/test/functional/wallet_tokens_change_supply.py b/test/functional/wallet_tokens_change_supply.py new file mode 100644 index 0000000000..67751a56a3 --- /dev/null +++ b/test/functional/wallet_tokens_change_supply.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023 RBB S.r.l +# Copyright (c) 2017-2021 The Bitcoin Core developers +# 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. +"""Wallet tokens change supply test + +Check that: +* We can create a new wallet, +* get an address +* send coins to the wallet's address +* sync the wallet with the node +* check balance +* issue new token +* mint new tokens +* redeem existing tokens +* lock the tokens supply +""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.mintlayer import (make_tx, reward_input, tx_input, ATOMS_PER_COIN) +from test_framework.util import assert_in, assert_equal +from test_framework.mintlayer import mintlayer_hash, block_input_data_obj +from test_framework.wallet_cli_controller import DEFAULT_ACCOUNT_INDEX, WalletCliController + +import asyncio +import sys + +class WalletTokens(BitcoinTestFramework): + + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 1 + self.extra_args = [[ + "--blockprod-min-peers-to-produce-blocks=0", + ]] + + def setup_network(self): + self.setup_nodes() + self.sync_all(self.nodes[0:1]) + + def generate_block(self): + node = self.nodes[0] + + block_input_data = { "PoW": { "reward_destination": "AnyoneCanSpend" } } + block_input_data = block_input_data_obj.encode(block_input_data).to_hex()[2:] + + # create a new block, taking transactions from mempool + block = node.blockprod_generate_block(block_input_data, None) + node.chainstate_submit_block(block) + block_id = node.chainstate_best_block_id() + + # Wait for mempool to sync + self.wait_until(lambda: node.mempool_local_best_block_id() == block_id, timeout = 5) + + return block_id + + def run_test(self): + if 'win32' in sys.platform: + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + asyncio.run(self.async_test()) + + async def async_test(self): + node = self.nodes[0] + + # new wallet + async with WalletCliController(node, self.config, self.log) as wallet: + await wallet.create_wallet() + + # check it is on genesis + assert_equal('0', await wallet.get_best_block_height()) + + # new address + pub_key_bytes = await wallet.new_public_key() + assert_equal(len(pub_key_bytes), 33) + + # Get chain tip + tip_id = node.chainstate_best_block_id() + self.log.debug(f'Tip: {tip_id}') + + # Submit a valid transaction + output = { + 'Transfer': [ { 'Coin': 1001 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ], + } + encoded_tx, tx_id = make_tx([reward_input(tip_id)], [output], 0) + + node.mempool_submit_transaction(encoded_tx) + assert node.mempool_contains_tx(tx_id) + + block_id = self.generate_block() # Block 1 + assert not node.mempool_contains_tx(tx_id) + + # sync the wallet + assert_in("Success", await wallet.sync()) + + # check wallet best block if it is synced + assert_equal(await wallet.get_best_block_height(), '1') + assert_equal(await wallet.get_best_block(), block_id) + + assert_in("Coins amount: 1001", await wallet.get_balance()) + + address = await wallet.new_address() + + # invalid ticker + # > max len + token_id, err = await wallet.issue_new_token("asdddd", "10000", 2, "http://uri", address) + assert token_id is None + assert err is not None + assert_in("Invalid ticker length", err) + # non alphanumeric + token_id, err = await wallet.issue_new_token("asd#", "10000", 2, "http://uri", address) + assert token_id is None + assert err is not None + assert_in("Invalid character in token ticker", err) + + # invalid url + token_id, err = await wallet.issue_new_token("XXX", "10000", 2, "123 123", address) + assert token_id is None + assert err is not None + assert_in("Incorrect metadata URI", err) + + # invalid num decimals + token_id, err = await wallet.issue_new_token("XXX", "10000", 99, "http://uri", address) + assert token_id is None + assert err is not None + assert_in("Too many decimals", err) + + # issue a valid token + token_id, err = await wallet.issue_new_token("XXX", "10000", 2, "http://uri", address, 'lockable') + assert token_id is not None + assert err is None + self.log.info(f"new token id: {token_id}") + + self.generate_block() + assert_in("Success", await wallet.sync()) + + tokens_to_mint = 10000 + total_tokens_supply = tokens_to_mint + assert_in("The transaction was submitted successfully", await wallet.mint_tokens(token_id, address, tokens_to_mint)) + + self.generate_block() + assert_in("Success", await wallet.sync()) + + assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance()) + + # cannot unmint more than minted + assert_in(f"Trying to redeem Amount {{ val: {tokens_to_mint+1}00 }} but the current supply is Amount {{ val: {tokens_to_mint}00 }}", await wallet.redeem_tokens(token_id, tokens_to_mint + 1)) + + tokens_to_redeem = 1000 + total_tokens_supply = total_tokens_supply - tokens_to_redeem + assert_in("The transaction was submitted successfully", await wallet.redeem_tokens(token_id, tokens_to_redeem)) + + self.generate_block() + assert_in("Success", await wallet.sync()) + assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance()) + + # mint some more tokens + tokens_to_mint = 1000 + total_tokens_supply = total_tokens_supply + tokens_to_mint + assert_in("The transaction was submitted successfully", await wallet.mint_tokens(token_id, address, tokens_to_mint)) + + self.generate_block() + assert_in("Success", await wallet.sync()) + assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance()) + + # lock token suply + assert_in("The transaction was submitted successfully", await wallet.lock_tokens(token_id)) + self.generate_block() + assert_in("Success", await wallet.sync()) + assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance()) + + # cannot mint any more tokens as it is locked + assert_in("Cannot change a Locked Token supply", await wallet.mint_tokens(token_id, address, tokens_to_mint)) + assert_in("Cannot change a Locked Token supply", await wallet.redeem_tokens(token_id, tokens_to_mint)) + assert_in("Cannot lock Token supply in state: Locked", await wallet.lock_tokens(token_id)) + + +if __name__ == '__main__': + WalletTokens().main() + + + diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index d80f175dcd..abc6e1872f 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -261,6 +261,11 @@ impl Account { total_fees_not_paid = (total_fees_not_paid + preselected_fee).ok_or(WalletError::OutputAmountOverflow)?; + total_fees_not_paid = preselected_inputs + .values() + .try_fold(total_fees_not_paid, |total, (_amount, fee)| total + *fee) + .ok_or(WalletError::OutputAmountOverflow)?; + let mut amount_to_be_paid_in_currency_with_fees = (amount_to_be_paid_in_currency_with_fees + total_fees_not_paid) .ok_or(WalletError::OutputAmountOverflow)?; @@ -716,6 +721,8 @@ impl Account { let outputs = make_mint_token_outputs(token_id, amount, address, self.chain_config.as_ref())?; + self.find_token(&token_id)?.total_supply.check_can_mint(amount)?; + self.change_token_supply_transaction( token_id, amount, @@ -726,43 +733,6 @@ impl Account { ) } - fn change_token_supply_transaction( - &mut self, - token_id: TokenId, - amount: Amount, - outputs: Vec, - db_tx: &mut impl WalletStorageWriteUnlocked, - median_time: BlockTimestamp, - fee_rate: CurrentFeeRate, - ) -> Result { - let token_data = self.find_token(&token_id)?; - let nonce = token_data - .last_nonce - .map_or(Some(AccountNonce::new(0)), |nonce| nonce.increment()) - .ok_or(WalletError::TokenIssuanceNonceOverflow(token_id))?; - //FIXME: pass different input in - let tx_input = TxInput::Account(AccountOutPoint::new( - nonce, - AccountOp::MintTokens(token_id, amount), - )); - - let request = SendRequest::new() - .with_outputs(outputs) - .with_inputs_and_destinations([(tx_input, token_data.reissuance_controller.clone())]); - - let request = self.select_inputs_for_send_request( - request, - vec![], - db_tx, - median_time, - fee_rate.current_fee_rate, - fee_rate.consolidate_fee_rate, - )?; - - let tx = self.sign_transaction_from_req(request, db_tx)?; - Ok(tx) - } - pub fn redeem_tokens( &mut self, db_tx: &mut impl WalletStorageWriteUnlocked, @@ -773,6 +743,8 @@ impl Account { ) -> WalletResult { let outputs = make_redeem_token_outputs(token_id, amount, self.chain_config.as_ref())?; + self.find_token(&token_id)?.total_supply.check_can_redeem(amount)?; + self.change_token_supply_transaction( token_id, amount, @@ -792,6 +764,8 @@ impl Account { ) -> WalletResult { let outputs = make_lock_token_outputs(self.chain_config.as_ref())?; + self.find_token(&token_id)?.total_supply.check_can_lock()?; + self.change_token_supply_transaction( token_id, Amount::ZERO, @@ -802,6 +776,44 @@ impl Account { ) } + fn change_token_supply_transaction( + &mut self, + token_id: TokenId, + amount: Amount, + outputs: Vec, + db_tx: &mut impl WalletStorageWriteUnlocked, + median_time: BlockTimestamp, + fee_rate: CurrentFeeRate, + ) -> Result { + let token_data = self.find_token(&token_id)?; + + let nonce = token_data + .last_nonce + .map_or(Some(AccountNonce::new(0)), |nonce| nonce.increment()) + .ok_or(WalletError::TokenIssuanceNonceOverflow(token_id))?; + //FIXME: pass different input in + let tx_input = TxInput::Account(AccountOutPoint::new( + nonce, + AccountOp::MintTokens(token_id, amount), + )); + + let request = SendRequest::new() + .with_outputs(outputs) + .with_inputs_and_destinations([(tx_input, token_data.reissuance_controller.clone())]); + + let request = self.select_inputs_for_send_request( + request, + vec![], + db_tx, + median_time, + fee_rate.current_fee_rate, + fee_rate.consolidate_fee_rate, + )?; + + let tx = self.sign_transaction_from_req(request, db_tx)?; + Ok(tx) + } + pub fn get_pos_gen_block_data( &self, db_tx: &impl WalletStorageReadUnlocked, @@ -1364,6 +1376,10 @@ impl Account { } } +/// There are some preselected inputs like the Token account inputs with a nonce +/// that need to be included in the request +/// Here we group them up by currency and sum the total amount and fee they bring to the +/// transaction fn group_preselected_inputs( request: &SendRequest, current_fee_rate: FeeRate, diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index aa7cf9c09a..6c5605b7c9 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -13,7 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::collections::{btree_map::Entry, BTreeMap, BTreeSet}; +use std::{ + collections::{btree_map::Entry, BTreeMap, BTreeSet}, + ops::Add, +}; use common::{ chain::{ @@ -78,16 +81,16 @@ impl PoolData { } pub enum TokenTotalSupplyState { - Fixed(Amount), // fixed to a certain amount - Lockable(Amount), // not known in advance but can be locked once at some point in time - Locked(Amount), // Locked - Unlimited(Amount), // limited only by the Amount data type + Fixed(Amount, Amount), // fixed to a certain amount + Lockable(Amount), // not known in advance but can be locked once at some point in time + Locked(Amount), // Locked + Unlimited(Amount), // limited only by the Amount data type } impl From for TokenTotalSupplyState { fn from(value: TokenTotalSupply) -> Self { match value { - TokenTotalSupply::Fixed(amount) => TokenTotalSupplyState::Fixed(amount), + TokenTotalSupply::Fixed(amount) => TokenTotalSupplyState::Fixed(amount, Amount::ZERO), TokenTotalSupply::Lockable => TokenTotalSupplyState::Lockable(Amount::ZERO), TokenTotalSupply::Unlimited => TokenTotalSupplyState::Unlimited(Amount::ZERO), } @@ -95,12 +98,60 @@ impl From for TokenTotalSupplyState { } impl TokenTotalSupplyState { - fn str_state(&self) -> &'static str { + pub fn str_state(&self) -> &'static str { match self { Self::Unlimited(_) => "Unlimited", Self::Locked(_) => "Locked", Self::Lockable(_) => "Lockable", - Self::Fixed(_) => "Fixed", + Self::Fixed(_, _) => "Fixed", + } + } + + pub fn check_can_mint(&self, amount: Amount) -> WalletResult<()> { + match self { + Self::Unlimited(_) | Self::Lockable(_) => Ok(()), + Self::Fixed(max, current) => { + let changed = current.add(amount).ok_or(WalletError::OutputAmountOverflow)?; + ensure!( + changed <= *max, + WalletError::CannotMintFixedTokenSupply(*max, *current, amount) + ); + Ok(()) + } + Self::Locked(_) => Err(WalletError::CannotChangeLockedTokenSupply), + } + } + + pub fn check_can_redeem(&self, amount: Amount) -> WalletResult<()> { + match self { + Self::Unlimited(current) | Self::Lockable(current) | Self::Fixed(_, current) => { + ensure!( + *current >= amount, + WalletError::CannotRedeemTokenSupply(amount, *current) + ); + Ok(()) + } + Self::Locked(_) => Err(WalletError::CannotChangeLockedTokenSupply), + } + } + + pub fn check_can_lock(&self) -> WalletResult<()> { + match self { + TokenTotalSupplyState::Lockable(_) => Ok(()), + TokenTotalSupplyState::Unlimited(_) + | TokenTotalSupplyState::Fixed(_, _) + | TokenTotalSupplyState::Locked(_) => { + Err(WalletError::CannotLockTokenSupply(self.str_state())) + } + } + } + + pub fn current_supply(&self) -> Amount { + match self { + Self::Unlimited(amount) + | Self::Fixed(_, amount) + | Self::Locked(amount) + | Self::Lockable(amount) => *amount, } } @@ -112,23 +163,34 @@ impl TokenTotalSupplyState { TokenTotalSupplyState::Unlimited(current) => Ok(TokenTotalSupplyState::Unlimited( (*current + amount).ok_or(WalletError::OutputAmountOverflow)?, )), - TokenTotalSupplyState::Fixed(_) | TokenTotalSupplyState::Locked(_) => { - Err(WalletError::CannotChangeTokenSupply(self.str_state())) + TokenTotalSupplyState::Fixed(max, current) => { + let changed = (*current + amount).ok_or(WalletError::OutputAmountOverflow)?; + ensure!( + changed <= *max, + WalletError::CannotMintFixedTokenSupply(*max, *current, amount) + ); + Ok(TokenTotalSupplyState::Fixed(*max, changed)) } + TokenTotalSupplyState::Locked(_) => Err(WalletError::CannotChangeLockedTokenSupply), } } fn redeem(&self, amount: Amount) -> WalletResult { match self { TokenTotalSupplyState::Lockable(current) => Ok(TokenTotalSupplyState::Lockable( - (*current - amount).ok_or(WalletError::OutputAmountOverflow)?, + (*current - amount) + .ok_or(WalletError::CannotRedeemTokenSupply(amount, *current))?, )), TokenTotalSupplyState::Unlimited(current) => Ok(TokenTotalSupplyState::Unlimited( - (*current - amount).ok_or(WalletError::OutputAmountOverflow)?, + (*current - amount) + .ok_or(WalletError::CannotRedeemTokenSupply(amount, *current))?, )), - TokenTotalSupplyState::Fixed(_) | TokenTotalSupplyState::Locked(_) => { - Err(WalletError::CannotChangeTokenSupply(self.str_state())) - } + TokenTotalSupplyState::Fixed(max, current) => Ok(TokenTotalSupplyState::Fixed( + *max, + (*current - amount) + .ok_or(WalletError::CannotRedeemTokenSupply(amount, *current))?, + )), + TokenTotalSupplyState::Locked(_) => Err(WalletError::CannotChangeLockedTokenSupply), } } @@ -136,7 +198,7 @@ impl TokenTotalSupplyState { match self { TokenTotalSupplyState::Lockable(current) => Ok(TokenTotalSupplyState::Locked(*current)), TokenTotalSupplyState::Unlimited(_) - | TokenTotalSupplyState::Fixed(_) + | TokenTotalSupplyState::Fixed(_, _) | TokenTotalSupplyState::Locked(_) => { Err(WalletError::CannotLockTokenSupply(self.str_state())) } @@ -147,9 +209,9 @@ impl TokenTotalSupplyState { match self { TokenTotalSupplyState::Locked(current) => Ok(TokenTotalSupplyState::Lockable(*current)), TokenTotalSupplyState::Unlimited(_) - | TokenTotalSupplyState::Fixed(_) + | TokenTotalSupplyState::Fixed(_, _) | TokenTotalSupplyState::Lockable(_) => { - Err(WalletError::InconsistentUnLockTokenSupply(self.str_state())) + Err(WalletError::InconsistentUnlockTokenSupply(self.str_state())) } } } @@ -490,7 +552,7 @@ impl OutputCache { Ok(()) } - /// Update delegation state with new tx input + /// Update token issuance state with new tx input fn update_token_issuance_state( unconfirmed_descendants: &mut BTreeMap>, data: &mut TokenIssuanceData, diff --git a/wallet/src/account/utxo_selector/mod.rs b/wallet/src/account/utxo_selector/mod.rs index 96bbb391a4..0df354d892 100644 --- a/wallet/src/account/utxo_selector/mod.rs +++ b/wallet/src/account/utxo_selector/mod.rs @@ -669,6 +669,9 @@ pub fn select_coins( cost_of_change: Amount, coin_selection_algo: CoinSelectionAlgo, ) -> Result { + if selection_target == Amount::ZERO && pay_fees == PayFee::DoNotPayFeeWithThisCurrency { + return Ok(SelectionResult::new(selection_target)); + } ensure!(!utxo_pool.is_empty(), UtxoSelectorError::NoUtxos); let total_available_value = utxo_pool diff --git a/wallet/src/account/utxo_selector/output_group.rs b/wallet/src/account/utxo_selector/output_group.rs index 056663b507..3c3c279a9e 100644 --- a/wallet/src/account/utxo_selector/output_group.rs +++ b/wallet/src/account/utxo_selector/output_group.rs @@ -41,7 +41,7 @@ pub struct OutputGroup { /// Should we pay fee with this currency or not in the case we pay the total fees with another /// currency. Here Currency refers to either a coin or a token_id. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum PayFee { PayFeeWithThisCurrency, DoNotPayFeeWithThisCurrency, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index d8bfdb6b53..b763fe66f8 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -141,12 +141,18 @@ pub enum WalletError { LockedUtxo(UtxoOutPoint), #[error("Selected UTXO is a token v0 and cannot be used")] TokenV0Utxo(UtxoOutPoint), - #[error("Cannot change Token supply in state: {0}")] - CannotChangeTokenSupply(&'static str), + #[error("Cannot change a Locked Token supply")] + CannotChangeLockedTokenSupply, #[error("Cannot lock Token supply in state: {0}")] CannotLockTokenSupply(&'static str), #[error("Cannot revert lock Token supply in state: {0}")] - InconsistentUnLockTokenSupply(&'static str), + InconsistentUnlockTokenSupply(&'static str), + #[error( + "Cannot mint Token over the fixed supply {0:?}, current supply {1:?} trying to mint {2:?}" + )] + CannotMintFixedTokenSupply(Amount, Amount, Amount), + #[error("Trying to redeem {0:?} but the current supply is {1:?}")] + CannotRedeemTokenSupply(Amount, Amount), } /// Result type used for the wallet diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 0563955959..606e2b2a02 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -2313,6 +2313,303 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { } } +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn change_and_lock_token_supply(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let chain_config = Arc::new(create_mainnet()); + + let mut wallet = create_wallet(chain_config.clone()); + + let coin_balance = wallet + .get_balance( + DEFAULT_ACCOUNT_INDEX, + UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoState::Confirmed.into(), + WithLocked::Unlocked, + ) + .unwrap() + .get(&Currency::Coin) + .copied() + .unwrap_or(Amount::ZERO); + assert_eq!(coin_balance, Amount::ZERO); + + // Generate a new block which sends reward to the wallet + let block1_amount = (Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)) + + chain_config.token_min_issuance_fee()) + .unwrap(); + + let address = get_address( + &chain_config, + MNEMONIC, + DEFAULT_ACCOUNT_INDEX, + KeyPurpose::ReceiveFunds, + 0.try_into().unwrap(), + ); + let block1 = Block::new( + vec![], + chain_config.genesis_block_id(), + chain_config.genesis_block().timestamp(), + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block1_amount, + ) + .unwrap()]), + ) + .unwrap(); + + let block1_id = block1.get_id(); + let block1_timestamp = block1.timestamp(); + + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]); + + let coin_balance = wallet + .get_balance( + DEFAULT_ACCOUNT_INDEX, + UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoState::Confirmed.into(), + WithLocked::Unlocked, + ) + .unwrap() + .get(&Currency::Coin) + .copied() + .unwrap_or(Amount::ZERO); + assert_eq!(coin_balance, block1_amount); + + let address2 = wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; + let (issued_token_id, token_issuance_transaction) = wallet + .issue_new_token( + DEFAULT_ACCOUNT_INDEX, + TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: "XXXX".as_bytes().to_vec(), + number_of_decimals: rng.gen_range(1..18), + metadata_uri: "http://uri".as_bytes().to_vec(), + total_supply: common::chain::tokens::TokenTotalSupply::Lockable, + reissuance_controller: address2.decode_object(&chain_config).unwrap(), + }), + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + let block2_amount = chain_config.token_min_supply_change_fee(); + + let block2 = Block::new( + vec![token_issuance_transaction], + block1_id.into(), + block1_timestamp, + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block2_amount, + ) + .unwrap()]), + ) + .unwrap(); + + scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2]); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!( + token_issuance_data.reissuance_controller, + address2.decode_object(&chain_config).unwrap() + ); + + assert_eq!(token_issuance_data.last_nonce, None); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + Amount::ZERO, + ); + + let token_amount_to_mint = Amount::from_atoms(rng.gen_range(2..100)); + let mint_transaction = wallet + .mint_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_mint, + address2.clone(), + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + let block3 = Block::new( + vec![mint_transaction], + block1_id.into(), + block1_timestamp, + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block2_amount, + ) + .unwrap()]), + ) + .unwrap(); + + scan_wallet(&mut wallet, BlockHeight::new(2), vec![block3]); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!(token_issuance_data.last_nonce, Some(AccountNonce::new(0))); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + token_amount_to_mint, + ); + + // Try to redeem more than the current total supply + let token_amount_to_redeem = (token_amount_to_mint + Amount::from_atoms(1)).unwrap(); + let err = wallet + .redeem_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_redeem, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap_err(); + assert_eq!( + err, + WalletError::CannotRedeemTokenSupply(token_amount_to_redeem, token_amount_to_mint) + ); + + let token_amount_to_redeem = + Amount::from_atoms(rng.gen_range(1..token_amount_to_mint.into_atoms())); + let redeem_transaction = wallet + .redeem_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_redeem, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + let block4 = Block::new( + vec![redeem_transaction], + block1_id.into(), + block1_timestamp, + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block2_amount, + ) + .unwrap()]), + ) + .unwrap(); + + scan_wallet(&mut wallet, BlockHeight::new(3), vec![block4]); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!(token_issuance_data.last_nonce, Some(AccountNonce::new(1))); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + (token_amount_to_mint - token_amount_to_redeem).unwrap(), + ); + assert!(token_issuance_data.total_supply.check_can_lock().is_ok()); + + let lock_transaction = wallet + .lock_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + let block5 = Block::new( + vec![lock_transaction], + block1_id.into(), + block1_timestamp, + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block2_amount, + ) + .unwrap()]), + ) + .unwrap(); + + scan_wallet(&mut wallet, BlockHeight::new(4), vec![block5]); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!(token_issuance_data.last_nonce, Some(AccountNonce::new(2))); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + (token_amount_to_mint - token_amount_to_redeem).unwrap(), + ); + + assert_eq!( + token_issuance_data.total_supply.check_can_lock(), + Err(WalletError::CannotLockTokenSupply("Locked")) + ); + + let err = wallet + .mint_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_mint, + address2.clone(), + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap_err(); + assert_eq!(err, WalletError::CannotChangeLockedTokenSupply); + let err = wallet + .redeem_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_redeem, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap_err(); + assert_eq!(err, WalletError::CannotChangeLockedTokenSupply); + + let err = wallet + .lock_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap_err(); + assert_eq!(err, WalletError::CannotLockTokenSupply("Locked")); +} + #[rstest] #[trace] #[case(Seed::from_entropy())] diff --git a/wallet/wallet-controller/src/read.rs b/wallet/wallet-controller/src/read.rs index 07b3d19a77..b96018d549 100644 --- a/wallet/wallet-controller/src/read.rs +++ b/wallet/wallet-controller/src/read.rs @@ -72,7 +72,10 @@ impl<'a, T: NodeInterface> ReadOnlyController<'a, T> { self.wallet .get_balance( self.account_index, - UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoType::Transfer + | UtxoType::LockThenTransfer + | UtxoType::MintTokens + | UtxoType::IssueNft, utxo_states, with_locked, ) From 1cc904a4f220fdcb47f9fa1ddc7f0649aa0f5ac9 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Fri, 13 Oct 2023 18:58:55 +0200 Subject: [PATCH 04/11] Fix tests --- wallet/src/account/mod.rs | 78 ++++++++++++++++---------- wallet/src/account/output_cache/mod.rs | 20 ++++--- wallet/src/wallet/tests.rs | 5 +- wallet/types/src/utxo_types.rs | 6 +- wallet/wallet-controller/src/read.rs | 5 +- 5 files changed, 65 insertions(+), 49 deletions(-) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index abc6e1872f..19edade104 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -188,10 +188,7 @@ impl Account { let (utxos, selection_algo) = if input_utxos.is_empty() { ( self.get_utxos( - UtxoType::Transfer - | UtxoType::LockThenTransfer - | UtxoType::IssueNft - | UtxoType::MintTokens, + UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::TokensOp, median_time, UtxoState::Confirmed | UtxoState::InMempool | UtxoState::Inactive, WithLocked::Unlocked, @@ -721,11 +718,22 @@ impl Account { let outputs = make_mint_token_outputs(token_id, amount, address, self.chain_config.as_ref())?; - self.find_token(&token_id)?.total_supply.check_can_mint(amount)?; + let token_data = self.find_token(&token_id)?; + token_data.total_supply.check_can_mint(amount)?; + + let nonce = token_data + .last_nonce + .map_or(Some(AccountNonce::new(0)), |nonce| nonce.increment()) + .ok_or(WalletError::TokenIssuanceNonceOverflow(token_id))?; + let tx_input = TxInput::Account(AccountOutPoint::new( + nonce, + AccountOp::MintTokens(token_id, amount), + )); + let reissuance_controller = token_data.reissuance_controller.clone(); self.change_token_supply_transaction( - token_id, - amount, + reissuance_controller, + tx_input, outputs, db_tx, median_time, @@ -743,11 +751,22 @@ impl Account { ) -> WalletResult { let outputs = make_redeem_token_outputs(token_id, amount, self.chain_config.as_ref())?; - self.find_token(&token_id)?.total_supply.check_can_redeem(amount)?; + let token_data = self.find_token(&token_id)?; + token_data.total_supply.check_can_redeem(amount)?; + + let nonce = token_data + .last_nonce + .map_or(Some(AccountNonce::new(0)), |nonce| nonce.increment()) + .ok_or(WalletError::TokenIssuanceNonceOverflow(token_id))?; + let tx_input = TxInput::Account(AccountOutPoint::new( + nonce, + AccountOp::UnmintTokens(token_id), + )); + let reissuance_controller = token_data.reissuance_controller.clone(); self.change_token_supply_transaction( - token_id, - amount, + reissuance_controller, + tx_input, outputs, db_tx, median_time, @@ -764,11 +783,22 @@ impl Account { ) -> WalletResult { let outputs = make_lock_token_outputs(self.chain_config.as_ref())?; - self.find_token(&token_id)?.total_supply.check_can_lock()?; + let token_data = self.find_token(&token_id)?; + token_data.total_supply.check_can_lock()?; + + let nonce = token_data + .last_nonce + .map_or(Some(AccountNonce::new(0)), |nonce| nonce.increment()) + .ok_or(WalletError::TokenIssuanceNonceOverflow(token_id))?; + let tx_input = TxInput::Account(AccountOutPoint::new( + nonce, + AccountOp::LockTokenSupply(token_id), + )); + let reissuance_controller = token_data.reissuance_controller.clone(); self.change_token_supply_transaction( - token_id, - Amount::ZERO, + reissuance_controller, + tx_input, outputs, db_tx, median_time, @@ -778,28 +808,16 @@ impl Account { fn change_token_supply_transaction( &mut self, - token_id: TokenId, - amount: Amount, + reissuance_controller: Destination, + tx_input: TxInput, outputs: Vec, db_tx: &mut impl WalletStorageWriteUnlocked, median_time: BlockTimestamp, fee_rate: CurrentFeeRate, ) -> Result { - let token_data = self.find_token(&token_id)?; - - let nonce = token_data - .last_nonce - .map_or(Some(AccountNonce::new(0)), |nonce| nonce.increment()) - .ok_or(WalletError::TokenIssuanceNonceOverflow(token_id))?; - //FIXME: pass different input in - let tx_input = TxInput::Account(AccountOutPoint::new( - nonce, - AccountOp::MintTokens(token_id, amount), - )); - let request = SendRequest::new() .with_outputs(outputs) - .with_inputs_and_destinations([(tx_input, token_data.reissuance_controller.clone())]); + .with_inputs_and_destinations([(tx_input, reissuance_controller)]); let request = self.select_inputs_for_send_request( request, @@ -1416,7 +1434,9 @@ fn group_preselected_inputs( AccountOp::MintTokens(token_id, amount) => { update_preselected_inputs(Currency::Token(*token_id), *amount, *fee)?; } - AccountOp::LockTokenSupply(_) | AccountOp::UnmintTokens(_) => {} + AccountOp::LockTokenSupply(token_id) | AccountOp::UnmintTokens(token_id) => { + update_preselected_inputs(Currency::Token(*token_id), Amount::ZERO, *fee)?; + } AccountOp::SpendDelegationBalance(_, amount) => { update_preselected_inputs(Currency::Coin, *amount, *fee)?; } diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index 6c5605b7c9..0030234ec1 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -485,7 +485,7 @@ impl OutputCache { outpoint, tx_id, )?; - data.total_supply.mint(*amount)?; + data.total_supply = data.total_supply.mint(*amount)?; } } AccountOp::UnmintTokens(token_id) => { @@ -498,7 +498,7 @@ impl OutputCache { tx_id, )?; let amount = sum_burned_token_amount(tx.outputs(), token_id)?; - data.total_supply.redeem(amount)?; + data.total_supply = data.total_supply.redeem(amount)?; } } | AccountOp::LockTokenSupply(token_id) => { @@ -510,7 +510,7 @@ impl OutputCache { outpoint, tx_id, )?; - data.total_supply.lock()?; + data.total_supply = data.total_supply.lock()?; } } } @@ -605,7 +605,7 @@ impl OutputCache { data.last_nonce = outpoint.nonce().decrement(); data.last_parent = find_parent(&self.unconfirmed_descendants, tx_id.clone()); - data.total_supply.redeem(*amount)?; + data.total_supply = data.total_supply.redeem(*amount)?; } } @@ -615,7 +615,7 @@ impl OutputCache { data.last_parent = find_parent(&self.unconfirmed_descendants, tx_id.clone()); let amount = sum_burned_token_amount(tx.outputs(), token_id)?; - data.total_supply.mint(amount)?; + data.total_supply = data.total_supply.mint(amount)?; } } AccountOp::LockTokenSupply(token_id) => { @@ -623,7 +623,7 @@ impl OutputCache { data.last_nonce = outpoint.nonce().decrement(); data.last_parent = find_parent(&self.unconfirmed_descendants, tx_id.clone()); - data.total_supply.unlock()?; + data.total_supply = data.total_supply.unlock()?; } } }, @@ -806,7 +806,8 @@ impl OutputCache { &self.unconfirmed_descendants, tx_id.into(), ); - data.total_supply.redeem(*amount)?; + data.total_supply = + data.total_supply.redeem(*amount)?; } } | AccountOp::UnmintTokens(token_id) => { @@ -822,7 +823,8 @@ impl OutputCache { tx.get_transaction().outputs(), token_id, )?; - data.total_supply.mint(amount)?; + data.total_supply = + data.total_supply.mint(amount)?; } } | AccountOp::LockTokenSupply(token_id) => { @@ -834,7 +836,7 @@ impl OutputCache { &self.unconfirmed_descendants, tx_id.into(), ); - data.total_supply.unlock()?; + data.total_supply = data.total_supply.unlock()?; } } }, diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 606e2b2a02..82cdbf0ddf 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -2169,10 +2169,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { let currency_balances = wallet .get_balance( DEFAULT_ACCOUNT_INDEX, - UtxoType::Transfer - | UtxoType::LockThenTransfer - | UtxoType::MintTokens - | UtxoType::IssueNft, + UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::TokensOp, UtxoState::Confirmed.into(), WithLocked::Unlocked, ) diff --git a/wallet/types/src/utxo_types.rs b/wallet/types/src/utxo_types.rs index 7c93f28159..f2b319f379 100644 --- a/wallet/types/src/utxo_types.rs +++ b/wallet/types/src/utxo_types.rs @@ -31,8 +31,7 @@ pub enum UtxoType { ProduceBlockFromStake = 1 << 4, CreateDelegationId = 1 << 5, DelegateStaking = 1 << 6, - MintTokens = 1 << 7, - IssueNft = 1 << 8, + TokensOp = 1 << 7, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -54,7 +53,8 @@ pub fn get_utxo_type(output: &TxOutput) -> Option { TxOutput::ProduceBlockFromStake(_, _) => Some(UtxoType::ProduceBlockFromStake), TxOutput::CreateDelegationId(_, _) => Some(UtxoType::CreateDelegationId), TxOutput::DelegateStaking(_, _) => Some(UtxoType::DelegateStaking), - TxOutput::IssueFungibleToken(_) | TxOutput::IssueNft(_, _, _) => None, + TxOutput::IssueFungibleToken(_) => None, + TxOutput::IssueNft(_, _, _) => Some(UtxoType::TokensOp), } } pub fn get_utxo_state(output: &TxState) -> UtxoState { diff --git a/wallet/wallet-controller/src/read.rs b/wallet/wallet-controller/src/read.rs index b96018d549..b2b8e9dc5b 100644 --- a/wallet/wallet-controller/src/read.rs +++ b/wallet/wallet-controller/src/read.rs @@ -72,10 +72,7 @@ impl<'a, T: NodeInterface> ReadOnlyController<'a, T> { self.wallet .get_balance( self.account_index, - UtxoType::Transfer - | UtxoType::LockThenTransfer - | UtxoType::MintTokens - | UtxoType::IssueNft, + UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::TokensOp, utxo_states, with_locked, ) From 7bcb07caa511f833c764dcced327ee37a83d0476 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Fri, 13 Oct 2023 19:14:00 +0200 Subject: [PATCH 05/11] Rename redeem to unmint --- .../test_framework/wallet_cli_controller.py | 4 +-- .../functional/wallet_tokens_change_supply.py | 12 ++++---- wallet/src/account/mod.rs | 8 +++--- wallet/src/account/output_cache/mod.rs | 18 ++++++------ wallet/src/send_request/mod.rs | 2 +- wallet/src/wallet/mod.rs | 8 +++--- wallet/src/wallet/tests.rs | 28 +++++++++---------- wallet/wallet-cli-lib/src/commands/mod.rs | 8 +++--- .../src/synced_controller.rs | 4 +-- 9 files changed, 46 insertions(+), 46 deletions(-) diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index a694c23d12..f079f63d77 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -189,8 +189,8 @@ async def issue_new_token(self, async def mint_tokens(self, token_id: str, address: str, amount: int) -> str: return await self._write_command(f"minttokens {token_id} {address} {amount}\n") - async def redeem_tokens(self, token_id: str, amount: int) -> str: - return await self._write_command(f"redeemtokens {token_id} {amount}\n") + async def unmint_tokens(self, token_id: str, amount: int) -> str: + return await self._write_command(f"unminttokens {token_id} {amount}\n") async def lock_tokens(self, token_id: str) -> str: return await self._write_command(f"locktokens {token_id}\n") diff --git a/test/functional/wallet_tokens_change_supply.py b/test/functional/wallet_tokens_change_supply.py index 67751a56a3..63c8c5692d 100644 --- a/test/functional/wallet_tokens_change_supply.py +++ b/test/functional/wallet_tokens_change_supply.py @@ -24,7 +24,7 @@ * check balance * issue new token * mint new tokens -* redeem existing tokens +* unmint existing tokens * lock the tokens supply """ @@ -155,11 +155,11 @@ async def async_test(self): assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance()) # cannot unmint more than minted - assert_in(f"Trying to redeem Amount {{ val: {tokens_to_mint+1}00 }} but the current supply is Amount {{ val: {tokens_to_mint}00 }}", await wallet.redeem_tokens(token_id, tokens_to_mint + 1)) + assert_in(f"Trying to unmint Amount {{ val: {tokens_to_mint+1}00 }} but the current supply is Amount {{ val: {tokens_to_mint}00 }}", await wallet.unmint_tokens(token_id, tokens_to_mint + 1)) - tokens_to_redeem = 1000 - total_tokens_supply = total_tokens_supply - tokens_to_redeem - assert_in("The transaction was submitted successfully", await wallet.redeem_tokens(token_id, tokens_to_redeem)) + tokens_to_unmint = 1000 + total_tokens_supply = total_tokens_supply - tokens_to_unmint + assert_in("The transaction was submitted successfully", await wallet.unmint_tokens(token_id, tokens_to_unmint)) self.generate_block() assert_in("Success", await wallet.sync()) @@ -182,7 +182,7 @@ async def async_test(self): # cannot mint any more tokens as it is locked assert_in("Cannot change a Locked Token supply", await wallet.mint_tokens(token_id, address, tokens_to_mint)) - assert_in("Cannot change a Locked Token supply", await wallet.redeem_tokens(token_id, tokens_to_mint)) + assert_in("Cannot change a Locked Token supply", await wallet.unmint_tokens(token_id, tokens_to_mint)) assert_in("Cannot lock Token supply in state: Locked", await wallet.lock_tokens(token_id)) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 19edade104..6929dc02b0 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -34,7 +34,7 @@ use crate::key_chain::{make_path_to_vrf_key, AccountKeyChain, KeyChainError}; use crate::send_request::{ get_tx_output_destination, make_address_output, make_address_output_from_delegation, make_address_output_token, make_decomission_stake_pool_output, make_lock_token_outputs, - make_mint_token_outputs, make_redeem_token_outputs, make_stake_output, IssueNftArguments, + make_mint_token_outputs, make_stake_output, make_unmint_token_outputs, IssueNftArguments, StakePoolDataArguments, }; use crate::wallet_events::{WalletEvents, WalletEventsNoOp}; @@ -741,7 +741,7 @@ impl Account { ) } - pub fn redeem_tokens( + pub fn unmint_tokens( &mut self, db_tx: &mut impl WalletStorageWriteUnlocked, token_id: TokenId, @@ -749,10 +749,10 @@ impl Account { median_time: BlockTimestamp, fee_rate: CurrentFeeRate, ) -> WalletResult { - let outputs = make_redeem_token_outputs(token_id, amount, self.chain_config.as_ref())?; + let outputs = make_unmint_token_outputs(token_id, amount, self.chain_config.as_ref())?; let token_data = self.find_token(&token_id)?; - token_data.total_supply.check_can_redeem(amount)?; + token_data.total_supply.check_can_unmint(amount)?; let nonce = token_data .last_nonce diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index 0030234ec1..9d740a371b 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -122,12 +122,12 @@ impl TokenTotalSupplyState { } } - pub fn check_can_redeem(&self, amount: Amount) -> WalletResult<()> { + pub fn check_can_unmint(&self, amount: Amount) -> WalletResult<()> { match self { Self::Unlimited(current) | Self::Lockable(current) | Self::Fixed(_, current) => { ensure!( *current >= amount, - WalletError::CannotRedeemTokenSupply(amount, *current) + WalletError::CannotUnmintTokenSupply(amount, *current) ); Ok(()) } @@ -175,20 +175,20 @@ impl TokenTotalSupplyState { } } - fn redeem(&self, amount: Amount) -> WalletResult { + fn unmint(&self, amount: Amount) -> WalletResult { match self { TokenTotalSupplyState::Lockable(current) => Ok(TokenTotalSupplyState::Lockable( (*current - amount) - .ok_or(WalletError::CannotRedeemTokenSupply(amount, *current))?, + .ok_or(WalletError::CannotUnmintTokenSupply(amount, *current))?, )), TokenTotalSupplyState::Unlimited(current) => Ok(TokenTotalSupplyState::Unlimited( (*current - amount) - .ok_or(WalletError::CannotRedeemTokenSupply(amount, *current))?, + .ok_or(WalletError::CannotUnmintTokenSupply(amount, *current))?, )), TokenTotalSupplyState::Fixed(max, current) => Ok(TokenTotalSupplyState::Fixed( *max, (*current - amount) - .ok_or(WalletError::CannotRedeemTokenSupply(amount, *current))?, + .ok_or(WalletError::CannotUnmintTokenSupply(amount, *current))?, )), TokenTotalSupplyState::Locked(_) => Err(WalletError::CannotChangeLockedTokenSupply), } @@ -498,7 +498,7 @@ impl OutputCache { tx_id, )?; let amount = sum_burned_token_amount(tx.outputs(), token_id)?; - data.total_supply = data.total_supply.redeem(amount)?; + data.total_supply = data.total_supply.unmint(amount)?; } } | AccountOp::LockTokenSupply(token_id) => { @@ -605,7 +605,7 @@ impl OutputCache { data.last_nonce = outpoint.nonce().decrement(); data.last_parent = find_parent(&self.unconfirmed_descendants, tx_id.clone()); - data.total_supply = data.total_supply.redeem(*amount)?; + data.total_supply = data.total_supply.unmint(*amount)?; } } @@ -807,7 +807,7 @@ impl OutputCache { tx_id.into(), ); data.total_supply = - data.total_supply.redeem(*amount)?; + data.total_supply.unmint(*amount)?; } } | AccountOp::UnmintTokens(token_id) => { diff --git a/wallet/src/send_request/mod.rs b/wallet/src/send_request/mod.rs index 53b73ccb31..94baa1f3e1 100644 --- a/wallet/src/send_request/mod.rs +++ b/wallet/src/send_request/mod.rs @@ -99,7 +99,7 @@ pub fn make_mint_token_outputs( Ok(vec![mint_output, token_change_supply_fee]) } -pub fn make_redeem_token_outputs( +pub fn make_unmint_token_outputs( token_id: TokenId, amount: Amount, chain_config: &ChainConfig, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index b763fe66f8..6508db5592 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -151,8 +151,8 @@ pub enum WalletError { "Cannot mint Token over the fixed supply {0:?}, current supply {1:?} trying to mint {2:?}" )] CannotMintFixedTokenSupply(Amount, Amount, Amount), - #[error("Trying to redeem {0:?} but the current supply is {1:?}")] - CannotRedeemTokenSupply(Amount, Amount), + #[error("Trying to unmint {0:?} but the current supply is {1:?}")] + CannotUnmintTokenSupply(Amount, Amount), } /// Result type used for the wallet @@ -828,7 +828,7 @@ impl Wallet { }) } - pub fn redeem_tokens( + pub fn unmint_tokens( &mut self, account_index: U31, token_id: TokenId, @@ -838,7 +838,7 @@ impl Wallet { ) -> WalletResult { let latest_median_time = self.latest_median_time; self.for_account_rw_unlocked(account_index, |account, db_tx| { - account.redeem_tokens( + account.unmint_tokens( db_tx, token_id, amount, diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 82cdbf0ddf..6206e206d9 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -2471,36 +2471,36 @@ fn change_and_lock_token_supply(#[case] seed: Seed) { token_amount_to_mint, ); - // Try to redeem more than the current total supply - let token_amount_to_redeem = (token_amount_to_mint + Amount::from_atoms(1)).unwrap(); + // Try to unmint more than the current total supply + let token_amount_to_unmint = (token_amount_to_mint + Amount::from_atoms(1)).unwrap(); let err = wallet - .redeem_tokens( + .unmint_tokens( DEFAULT_ACCOUNT_INDEX, issued_token_id, - token_amount_to_redeem, + token_amount_to_unmint, FeeRate::new(Amount::ZERO), FeeRate::new(Amount::ZERO), ) .unwrap_err(); assert_eq!( err, - WalletError::CannotRedeemTokenSupply(token_amount_to_redeem, token_amount_to_mint) + WalletError::CannotUnmintTokenSupply(token_amount_to_unmint, token_amount_to_mint) ); - let token_amount_to_redeem = + let token_amount_to_unmint = Amount::from_atoms(rng.gen_range(1..token_amount_to_mint.into_atoms())); - let redeem_transaction = wallet - .redeem_tokens( + let unmint_transaction = wallet + .unmint_tokens( DEFAULT_ACCOUNT_INDEX, issued_token_id, - token_amount_to_redeem, + token_amount_to_unmint, FeeRate::new(Amount::ZERO), FeeRate::new(Amount::ZERO), ) .unwrap(); let block4 = Block::new( - vec![redeem_transaction], + vec![unmint_transaction], block1_id.into(), block1_timestamp, ConsensusData::None, @@ -2526,7 +2526,7 @@ fn change_and_lock_token_supply(#[case] seed: Seed) { assert_eq!( token_issuance_data.total_supply.current_supply(), - (token_amount_to_mint - token_amount_to_redeem).unwrap(), + (token_amount_to_mint - token_amount_to_unmint).unwrap(), ); assert!(token_issuance_data.total_supply.check_can_lock().is_ok()); @@ -2566,7 +2566,7 @@ fn change_and_lock_token_supply(#[case] seed: Seed) { assert_eq!( token_issuance_data.total_supply.current_supply(), - (token_amount_to_mint - token_amount_to_redeem).unwrap(), + (token_amount_to_mint - token_amount_to_unmint).unwrap(), ); assert_eq!( @@ -2586,10 +2586,10 @@ fn change_and_lock_token_supply(#[case] seed: Seed) { .unwrap_err(); assert_eq!(err, WalletError::CannotChangeLockedTokenSupply); let err = wallet - .redeem_tokens( + .unmint_tokens( DEFAULT_ACCOUNT_INDEX, issued_token_id, - token_amount_to_redeem, + token_amount_to_unmint, FeeRate::new(Amount::ZERO), FeeRate::new(Amount::ZERO), ) diff --git a/wallet/wallet-cli-lib/src/commands/mod.rs b/wallet/wallet-cli-lib/src/commands/mod.rs index ccc5882a2e..510ec20338 100644 --- a/wallet/wallet-cli-lib/src/commands/mod.rs +++ b/wallet/wallet-cli-lib/src/commands/mod.rs @@ -214,8 +214,8 @@ pub enum WalletCommand { amount: String, }, - /// Redeem existing tokens and reduce the total supply - RedeemTokens { + /// Unmint existing tokens and reduce the total supply + UnmintTokens { token_id: String, amount: String, }, @@ -871,7 +871,7 @@ impl CommandHandler { Ok(Self::tx_submitted_command()) } - WalletCommand::RedeemTokens { token_id, amount } => { + WalletCommand::UnmintTokens { token_id, amount } => { let token_id = parse_token_id(chain_config, token_id.as_str())?; let amount = { let token_number_of_decimals = self @@ -884,7 +884,7 @@ impl CommandHandler { self.get_synced_controller() .await? - .redeem_tokens(token_id, amount) + .unmint_tokens(token_id, amount) .await .map_err(WalletCliError::Controller)?; diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index d9f7161a42..2f8d9e3253 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -183,7 +183,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.broadcast_to_mempool(tx).await } - pub async fn redeem_tokens( + pub async fn unmint_tokens( &mut self, token_id: TokenId, amount: Amount, @@ -193,7 +193,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { let tx = self .wallet - .redeem_tokens( + .unmint_tokens( self.account_index, token_id, amount, From 8d93248581f1484673ce31d1ec453be9c1bf1e4a Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Mon, 16 Oct 2023 19:49:21 +0200 Subject: [PATCH 06/11] Add more tests for tokens in the wallet --- wallet/src/account/output_cache/mod.rs | 2 +- wallet/src/wallet/tests.rs | 672 ++++++++++++++++++++++++- wallet/types/src/wallet_tx.rs | 10 + 3 files changed, 673 insertions(+), 11 deletions(-) diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index 9d740a371b..a8cb8d7786 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -336,7 +336,7 @@ impl OutputCache { } pub fn add_tx(&mut self, tx_id: OutPointSourceId, tx: WalletTx) -> WalletResult<()> { - let already_present = self.txs.contains_key(&tx_id); + let already_present = self.txs.get(&tx_id).map_or(false, |tx| !tx.state().is_abandoned()); let is_unconfirmed = match tx.state() { TxState::Inactive(_) | TxState::InMempool(_) diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 6206e206d9..1e284459f3 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -31,7 +31,7 @@ use common::{ output_value::OutputValue, signature::inputsig::InputWitness, timelock::OutputTimeLock, - tokens::{TokenData, TokenIssuanceV1, TokenTransfer}, + tokens::{TokenData, TokenIssuanceV0, TokenIssuanceV1}, Destination, Genesis, OutPointSourceId, TxInput, }, primitives::{per_thousand::PerThousand, Idable, H256}, @@ -2200,10 +2200,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { let some_other_address = PublicKeyHash::from_low_u64_be(1); let new_output = TxOutput::Transfer( - OutputValue::TokenV0(Box::new(TokenData::TokenTransfer(TokenTransfer { - token_id: *token_id, - amount: tokens_to_transfer, - }))), + OutputValue::TokenV1(*token_id, tokens_to_transfer), Destination::Address(some_other_address), ); @@ -2275,10 +2272,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { let some_other_address = PublicKeyHash::from_low_u64_be(1); let new_output = TxOutput::Transfer( - OutputValue::TokenV0(Box::new(TokenData::TokenTransfer(TokenTransfer { - token_id: *token_id, - amount: not_enough_tokens_to_transfer, - }))), + OutputValue::TokenV1(*token_id, not_enough_tokens_to_transfer), Destination::Address(some_other_address), ); @@ -2313,7 +2307,665 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn change_and_lock_token_supply(#[case] seed: Seed) { +fn check_tokens_v0_are_ignored(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let chain_config = Arc::new(create_mainnet()); + + let mut wallet = create_wallet(chain_config.clone()); + + let coin_balance = wallet + .get_balance( + DEFAULT_ACCOUNT_INDEX, + UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoState::Confirmed.into(), + WithLocked::Unlocked, + ) + .unwrap() + .get(&Currency::Coin) + .copied() + .unwrap_or(Amount::ZERO); + assert_eq!(coin_balance, Amount::ZERO); + + // Generate a new block which sends reward to the wallet + let block1_amount = (Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)) + + chain_config.token_min_issuance_fee()) + .unwrap(); + + let address = get_address( + &chain_config, + MNEMONIC, + DEFAULT_ACCOUNT_INDEX, + KeyPurpose::ReceiveFunds, + 0.try_into().unwrap(), + ); + let block1 = Block::new( + vec![], + chain_config.genesis_block_id(), + chain_config.genesis_block().timestamp(), + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block1_amount, + ) + .unwrap()]), + ) + .unwrap(); + + let block1_id = block1.get_id(); + let block1_timestamp = block1.timestamp(); + + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]); + + let coin_balance = wallet + .get_balance( + DEFAULT_ACCOUNT_INDEX, + UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoState::Confirmed.into(), + WithLocked::Unlocked, + ) + .unwrap() + .get(&Currency::Coin) + .copied() + .unwrap_or(Amount::ZERO); + assert_eq!(coin_balance, block1_amount); + + let address2 = wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; + let token_issuance_transaction = wallet + .create_transaction_to_addresses( + DEFAULT_ACCOUNT_INDEX, + [TxOutput::Transfer( + OutputValue::TokenV0(Box::new(TokenData::TokenIssuance(Box::new( + TokenIssuanceV0 { + token_ticker: "XXXX".as_bytes().to_vec(), + number_of_decimals: rng.gen_range(1..18), + metadata_uri: "http://uri".as_bytes().to_vec(), + amount_to_issue: Amount::from_atoms(rng.gen_range(1..10000)), + }, + )))), + address2.decode_object(&chain_config).unwrap(), + )], + [], + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + let block2_amount = chain_config.token_min_supply_change_fee(); + + let block2 = Block::new( + vec![token_issuance_transaction], + block1_id.into(), + block1_timestamp, + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block2_amount, + ) + .unwrap()]), + ) + .unwrap(); + + scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2]); + + let currency_balances = wallet + .get_balance( + DEFAULT_ACCOUNT_INDEX, + UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoState::Confirmed.into(), + WithLocked::Unlocked, + ) + .unwrap(); + + let token_balances = currency_balances + .into_iter() + .filter_map(|(c, amount)| match c { + Currency::Coin => None, + Currency::Token(token_id) => Some((token_id, amount)), + }) + .collect_vec(); + // the token should be ignored + assert_eq!(token_balances.len(), 0); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn change_token_supply_fixed(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let chain_config = Arc::new(create_mainnet()); + + let mut wallet = create_wallet(chain_config.clone()); + + let coin_balance = wallet + .get_balance( + DEFAULT_ACCOUNT_INDEX, + UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoState::Confirmed.into(), + WithLocked::Unlocked, + ) + .unwrap() + .get(&Currency::Coin) + .copied() + .unwrap_or(Amount::ZERO); + assert_eq!(coin_balance, Amount::ZERO); + + // Generate a new block which sends reward to the wallet + let block1_amount = (Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)) + + chain_config.token_min_issuance_fee()) + .unwrap(); + + let address = get_address( + &chain_config, + MNEMONIC, + DEFAULT_ACCOUNT_INDEX, + KeyPurpose::ReceiveFunds, + 0.try_into().unwrap(), + ); + let block1 = Block::new( + vec![], + chain_config.genesis_block_id(), + chain_config.genesis_block().timestamp(), + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block1_amount, + ) + .unwrap()]), + ) + .unwrap(); + + let block1_id = block1.get_id(); + let block1_timestamp = block1.timestamp(); + + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]); + + let coin_balance = wallet + .get_balance( + DEFAULT_ACCOUNT_INDEX, + UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoState::Confirmed.into(), + WithLocked::Unlocked, + ) + .unwrap() + .get(&Currency::Coin) + .copied() + .unwrap_or(Amount::ZERO); + assert_eq!(coin_balance, block1_amount); + + let fixed_max_amount = Amount::from_atoms(rng.gen_range(1..100000)); + let address2 = wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; + let (issued_token_id, token_issuance_transaction) = wallet + .issue_new_token( + DEFAULT_ACCOUNT_INDEX, + TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: "XXXX".as_bytes().to_vec(), + number_of_decimals: rng.gen_range(1..18), + metadata_uri: "http://uri".as_bytes().to_vec(), + total_supply: common::chain::tokens::TokenTotalSupply::Fixed(fixed_max_amount), + reissuance_controller: address2.decode_object(&chain_config).unwrap(), + }), + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + let block2_amount = chain_config.token_min_supply_change_fee(); + + let block2 = Block::new( + vec![token_issuance_transaction], + block1_id.into(), + block1_timestamp, + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block2_amount, + ) + .unwrap()]), + ) + .unwrap(); + + scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2]); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!( + token_issuance_data.reissuance_controller, + address2.decode_object(&chain_config).unwrap() + ); + + assert_eq!(token_issuance_data.last_nonce, None); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + Amount::ZERO, + ); + + let token_amount_to_mint = Amount::from_atoms(rng.gen_range(1..fixed_max_amount.into_atoms())); + let mint_transaction = wallet + .mint_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_mint, + address2.clone(), + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + wallet.add_unconfirmed_tx(mint_transaction.clone(), &WalletEventsNoOp).unwrap(); + + // Try to mint more then the fixed maximum + let leftover = (fixed_max_amount - token_amount_to_mint).unwrap(); + let token_amount_to_mint_more_than_maximum = + (leftover + Amount::from_atoms(rng.gen_range(1..1000))).unwrap(); + let err = wallet + .mint_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_mint_more_than_maximum, + address2.clone(), + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap_err(); + + assert_eq!( + err, + WalletError::CannotMintFixedTokenSupply( + fixed_max_amount, + token_amount_to_mint, + token_amount_to_mint_more_than_maximum + ) + ); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!(token_issuance_data.last_nonce, Some(AccountNonce::new(0))); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + token_amount_to_mint, + ); + + // test abandoning a transaction + wallet + .abandon_transaction( + DEFAULT_ACCOUNT_INDEX, + mint_transaction.transaction().get_id(), + ) + .unwrap(); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!(token_issuance_data.last_nonce, None); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + Amount::ZERO, + ); + + let block3 = Block::new( + vec![mint_transaction], + block1_id.into(), + block1_timestamp, + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block2_amount, + ) + .unwrap()]), + ) + .unwrap(); + + scan_wallet(&mut wallet, BlockHeight::new(2), vec![block3]); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!(token_issuance_data.last_nonce, Some(AccountNonce::new(0))); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + token_amount_to_mint, + ); + + // Try to unmint more than the current total supply + let token_amount_to_unmint = (token_amount_to_mint + Amount::from_atoms(1)).unwrap(); + let err = wallet + .unmint_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_unmint, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap_err(); + assert_eq!( + err, + WalletError::CannotUnmintTokenSupply(token_amount_to_unmint, token_amount_to_mint) + ); + + let token_amount_to_unmint = + Amount::from_atoms(rng.gen_range(1..token_amount_to_mint.into_atoms())); + let unmint_transaction = wallet + .unmint_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_unmint, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + let block4 = Block::new( + vec![unmint_transaction], + block1_id.into(), + block1_timestamp, + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block2_amount, + ) + .unwrap()]), + ) + .unwrap(); + + scan_wallet(&mut wallet, BlockHeight::new(3), vec![block4]); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!(token_issuance_data.last_nonce, Some(AccountNonce::new(1))); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + (token_amount_to_mint - token_amount_to_unmint).unwrap(), + ); + assert_eq!( + token_issuance_data.total_supply.check_can_lock().unwrap_err(), + WalletError::CannotLockTokenSupply("Fixed") + ); + + let err = wallet + .lock_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap_err(); + assert_eq!(err, WalletError::CannotLockTokenSupply("Fixed")); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn change_token_supply_unlimited(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let chain_config = Arc::new(create_mainnet()); + + let mut wallet = create_wallet(chain_config.clone()); + + let coin_balance = wallet + .get_balance( + DEFAULT_ACCOUNT_INDEX, + UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoState::Confirmed.into(), + WithLocked::Unlocked, + ) + .unwrap() + .get(&Currency::Coin) + .copied() + .unwrap_or(Amount::ZERO); + assert_eq!(coin_balance, Amount::ZERO); + + // Generate a new block which sends reward to the wallet + let block1_amount = (Amount::from_atoms(rng.gen_range(NETWORK_FEE + 100..NETWORK_FEE + 10000)) + + chain_config.token_min_issuance_fee()) + .unwrap(); + + let address = get_address( + &chain_config, + MNEMONIC, + DEFAULT_ACCOUNT_INDEX, + KeyPurpose::ReceiveFunds, + 0.try_into().unwrap(), + ); + let block1 = Block::new( + vec![], + chain_config.genesis_block_id(), + chain_config.genesis_block().timestamp(), + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block1_amount, + ) + .unwrap()]), + ) + .unwrap(); + + let block1_id = block1.get_id(); + let block1_timestamp = block1.timestamp(); + + scan_wallet(&mut wallet, BlockHeight::new(0), vec![block1]); + + let coin_balance = wallet + .get_balance( + DEFAULT_ACCOUNT_INDEX, + UtxoType::Transfer | UtxoType::LockThenTransfer, + UtxoState::Confirmed.into(), + WithLocked::Unlocked, + ) + .unwrap() + .get(&Currency::Coin) + .copied() + .unwrap_or(Amount::ZERO); + assert_eq!(coin_balance, block1_amount); + + let address2 = wallet.get_new_address(DEFAULT_ACCOUNT_INDEX).unwrap().1; + let (issued_token_id, token_issuance_transaction) = wallet + .issue_new_token( + DEFAULT_ACCOUNT_INDEX, + TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: "XXXX".as_bytes().to_vec(), + number_of_decimals: rng.gen_range(1..18), + metadata_uri: "http://uri".as_bytes().to_vec(), + total_supply: common::chain::tokens::TokenTotalSupply::Unlimited, + reissuance_controller: address2.decode_object(&chain_config).unwrap(), + }), + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + let block2_amount = chain_config.token_min_supply_change_fee(); + + let block2 = Block::new( + vec![token_issuance_transaction], + block1_id.into(), + block1_timestamp, + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block2_amount, + ) + .unwrap()]), + ) + .unwrap(); + + scan_wallet(&mut wallet, BlockHeight::new(1), vec![block2]); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!( + token_issuance_data.reissuance_controller, + address2.decode_object(&chain_config).unwrap() + ); + + assert_eq!(token_issuance_data.last_nonce, None); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + Amount::ZERO, + ); + + let token_amount_to_mint = Amount::from_atoms(rng.gen_range(1..10000)); + let mint_transaction = wallet + .mint_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_mint, + address2.clone(), + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + wallet.add_unconfirmed_tx(mint_transaction.clone(), &WalletEventsNoOp).unwrap(); + + let block3 = Block::new( + vec![mint_transaction], + block1_id.into(), + block1_timestamp, + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block2_amount, + ) + .unwrap()]), + ) + .unwrap(); + + scan_wallet(&mut wallet, BlockHeight::new(2), vec![block3]); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!(token_issuance_data.last_nonce, Some(AccountNonce::new(0))); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + token_amount_to_mint, + ); + + // Try to unmint more than the current total supply + let token_amount_to_unmint = (token_amount_to_mint + Amount::from_atoms(1)).unwrap(); + let err = wallet + .unmint_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_unmint, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap_err(); + assert_eq!( + err, + WalletError::CannotUnmintTokenSupply(token_amount_to_unmint, token_amount_to_mint) + ); + + let token_amount_to_unmint = + Amount::from_atoms(rng.gen_range(1..token_amount_to_mint.into_atoms())); + let unmint_transaction = wallet + .unmint_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + token_amount_to_unmint, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap(); + + let block4 = Block::new( + vec![unmint_transaction], + block1_id.into(), + block1_timestamp, + ConsensusData::None, + BlockReward::new(vec![make_address_output( + chain_config.as_ref(), + address.clone(), + block2_amount, + ) + .unwrap()]), + ) + .unwrap(); + + scan_wallet(&mut wallet, BlockHeight::new(3), vec![block4]); + + let token_issuance_data = wallet + .accounts + .get(&DEFAULT_ACCOUNT_INDEX) + .unwrap() + .find_token(&issued_token_id) + .unwrap(); + + assert_eq!(token_issuance_data.last_nonce, Some(AccountNonce::new(1))); + + assert_eq!( + token_issuance_data.total_supply.current_supply(), + (token_amount_to_mint - token_amount_to_unmint).unwrap(), + ); + assert_eq!( + token_issuance_data.total_supply.check_can_lock().unwrap_err(), + WalletError::CannotLockTokenSupply("Unlimited") + ); + + let err = wallet + .lock_tokens( + DEFAULT_ACCOUNT_INDEX, + issued_token_id, + FeeRate::new(Amount::ZERO), + FeeRate::new(Amount::ZERO), + ) + .unwrap_err(); + assert_eq!(err, WalletError::CannotLockTokenSupply("Unlimited")); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let chain_config = Arc::new(create_mainnet()); diff --git a/wallet/types/src/wallet_tx.rs b/wallet/types/src/wallet_tx.rs index 95e64f6be0..8e7f602c31 100644 --- a/wallet/types/src/wallet_tx.rs +++ b/wallet/types/src/wallet_tx.rs @@ -73,6 +73,16 @@ impl TxState { TxState::Abandoned => "Abandoned", } } + + pub fn is_abandoned(&self) -> bool { + match self { + TxState::Abandoned => true, + TxState::Confirmed(_, _, _) + | TxState::Conflicted(_) + | TxState::InMempool(_) + | TxState::Inactive(_) => false, + } + } } impl Display for TxState { From 429ebf0486a7cbd0a3d1d9934ef3a7842a5f3a36 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Wed, 18 Oct 2023 18:14:38 +0200 Subject: [PATCH 07/11] Rename lock tokens and add randomness to functional test lock_tokens -> lock_token_supply specify a randomseed argument for the functional tests randomize token supply change test --- .../test_framework/wallet_cli_controller.py | 4 +- test/functional/test_runner.py | 5 +- .../functional/wallet_tokens_change_supply.py | 52 ++++++++++--------- wallet/src/account/mod.rs | 2 +- wallet/src/wallet/mod.rs | 4 +- wallet/src/wallet/tests.rs | 8 +-- .../src/commands/helper_types.rs | 2 +- wallet/wallet-cli-lib/src/commands/mod.rs | 2 +- .../src/synced_controller.rs | 4 +- 9 files changed, 45 insertions(+), 38 deletions(-) diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index f079f63d77..91e2301526 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -258,5 +258,5 @@ async def stop_staking(self) -> str: async def get_addresses_usage(self) -> str: return await self._write_command("showreceiveaddresses\n") - async def get_balance(self, with_locked: str = 'unlocked') -> str: - return await self._write_command(f"getbalance {with_locked}\n") + async def get_balance(self, with_locked: str = 'unlocked', utxo_states: List[str] = ['confirmed']) -> str: + return await self._write_command(f"getbalance {with_locked} {' '.join(utxo_states)}\n") diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index f5e1a7110b..04fd54f64a 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -30,6 +30,7 @@ import datetime import locale import os +import random import time import shutil import signal @@ -444,6 +445,7 @@ def __init__(self, *, num_tests_parallel, tests_dir, tmpdir, test_list, flags, u self.num_running = 0 self.jobs = [] self.use_term_control = use_term_control + self.randomseed = random.randrange(sys.maxsize) def get_next(self): while self.num_running < self.num_jobs and self.test_list: @@ -452,6 +454,7 @@ def get_next(self): test = self.test_list.pop(0) portseed = len(self.test_list) portseed_arg = ["--portseed={}".format(portseed)] + randomseed_arg = [f"--randomseed={self.randomseed}"] log_stdout = tempfile.SpooledTemporaryFile(max_size=2**16) log_stderr = tempfile.SpooledTemporaryFile(max_size=2**16) test_argv = test.split() @@ -459,7 +462,7 @@ def get_next(self): tmpdir_arg = ["--tmpdir={}".format(testdir)] self.jobs.append((test, time.time(), - subprocess.Popen([sys.executable, self.tests_dir + test_argv[0]] + test_argv[1:] + self.flags + portseed_arg + tmpdir_arg, + subprocess.Popen([sys.executable, self.tests_dir + test_argv[0]] + test_argv[1:] + self.flags + portseed_arg + tmpdir_arg + randomseed_arg, universal_newlines=True, stdout=log_stdout, stderr=log_stderr), diff --git a/test/functional/wallet_tokens_change_supply.py b/test/functional/wallet_tokens_change_supply.py index 63c8c5692d..4e508a04bc 100644 --- a/test/functional/wallet_tokens_change_supply.py +++ b/test/functional/wallet_tokens_change_supply.py @@ -36,6 +36,7 @@ import asyncio import sys +import random class WalletTokens(BitcoinTestFramework): @@ -145,36 +146,39 @@ async def async_test(self): self.generate_block() assert_in("Success", await wallet.sync()) - tokens_to_mint = 10000 + tokens_to_mint = random.randrange(2, 10000) total_tokens_supply = tokens_to_mint assert_in("The transaction was submitted successfully", await wallet.mint_tokens(token_id, address, tokens_to_mint)) self.generate_block() assert_in("Success", await wallet.sync()) - assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance()) - - # cannot unmint more than minted - assert_in(f"Trying to unmint Amount {{ val: {tokens_to_mint+1}00 }} but the current supply is Amount {{ val: {tokens_to_mint}00 }}", await wallet.unmint_tokens(token_id, tokens_to_mint + 1)) - - tokens_to_unmint = 1000 - total_tokens_supply = total_tokens_supply - tokens_to_unmint - assert_in("The transaction was submitted successfully", await wallet.unmint_tokens(token_id, tokens_to_unmint)) - - self.generate_block() - assert_in("Success", await wallet.sync()) - assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance()) - - # mint some more tokens - tokens_to_mint = 1000 - total_tokens_supply = total_tokens_supply + tokens_to_mint - assert_in("The transaction was submitted successfully", await wallet.mint_tokens(token_id, address, tokens_to_mint)) - - self.generate_block() - assert_in("Success", await wallet.sync()) - assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance()) - - # lock token suply + # randomize minting and unminting + for _ in range(10): + if random.choice([True, False]): + # mint some more tokens + tokens_to_mint = random.randrange(1, 10000) + total_tokens_supply = total_tokens_supply + tokens_to_mint + assert_in("The transaction was submitted successfully", await wallet.mint_tokens(token_id, address, tokens_to_mint)) + else: + # unmint some tokens + tokens_to_unmint = random.randrange(1, 20000) + if tokens_to_unmint <= total_tokens_supply: + total_tokens_supply = total_tokens_supply - tokens_to_unmint + assert_in("The transaction was submitted successfully", await wallet.unmint_tokens(token_id, tokens_to_unmint)) + else: + assert_in(f"Trying to unmint Amount {{ val: {tokens_to_unmint}00 }} but the current supply is Amount {{ val: {total_tokens_supply}00 }}", await wallet.unmint_tokens(token_id, tokens_to_unmint)) + continue + + # either generate a new block or leave the transaction as in-memory state + if random.choice([True, False]): + self.generate_block() + assert_in("Success", await wallet.sync()) + + # check total supply is correct + assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance(utxo_states=['confirmed', 'inactive'])) + + # lock token supply assert_in("The transaction was submitted successfully", await wallet.lock_tokens(token_id)) self.generate_block() assert_in("Success", await wallet.sync()) diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 6929dc02b0..48048a80ba 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -774,7 +774,7 @@ impl Account { ) } - pub fn lock_tokens( + pub fn lock_token_supply( &mut self, db_tx: &mut impl WalletStorageWriteUnlocked, token_id: TokenId, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 6508db5592..d46bf9cfb1 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -851,7 +851,7 @@ impl Wallet { }) } - pub fn lock_tokens( + pub fn lock_token_supply( &mut self, account_index: U31, token_id: TokenId, @@ -860,7 +860,7 @@ impl Wallet { ) -> WalletResult { let latest_median_time = self.latest_median_time; self.for_account_rw_unlocked(account_index, |account, db_tx| { - account.lock_tokens( + account.lock_token_supply( db_tx, token_id, latest_median_time, diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 1e284459f3..c9c4acba2b 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -2716,7 +2716,7 @@ fn change_token_supply_fixed(#[case] seed: Seed) { ); let err = wallet - .lock_tokens( + .lock_token_supply( DEFAULT_ACCOUNT_INDEX, issued_token_id, FeeRate::new(Amount::ZERO), @@ -2952,7 +2952,7 @@ fn change_token_supply_unlimited(#[case] seed: Seed) { ); let err = wallet - .lock_tokens( + .lock_token_supply( DEFAULT_ACCOUNT_INDEX, issued_token_id, FeeRate::new(Amount::ZERO), @@ -3183,7 +3183,7 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { assert!(token_issuance_data.total_supply.check_can_lock().is_ok()); let lock_transaction = wallet - .lock_tokens( + .lock_token_supply( DEFAULT_ACCOUNT_INDEX, issued_token_id, FeeRate::new(Amount::ZERO), @@ -3249,7 +3249,7 @@ fn change_and_lock_token_supply_lockable(#[case] seed: Seed) { assert_eq!(err, WalletError::CannotChangeLockedTokenSupply); let err = wallet - .lock_tokens( + .lock_token_supply( DEFAULT_ACCOUNT_INDEX, issued_token_id, FeeRate::new(Amount::ZERO), diff --git a/wallet/wallet-cli-lib/src/commands/helper_types.rs b/wallet/wallet-cli-lib/src/commands/helper_types.rs index 0c9ace96bd..c147c362df 100644 --- a/wallet/wallet-cli-lib/src/commands/helper_types.rs +++ b/wallet/wallet-cli-lib/src/commands/helper_types.rs @@ -238,7 +238,7 @@ pub fn to_per_thousand( variable_name: &str, ) -> Result { PerThousand::from_decimal_str(value_str).ok_or(WalletCliError::InvalidInput(format!( - "Failed to parse {variable_name} the decimal that must be in the range [0.001,1.000] or [0.1%,100%]", + "Failed to parse {variable_name}. The decimal must be in the range [0.001,1.000] or [0.1%,100%]", ))) } diff --git a/wallet/wallet-cli-lib/src/commands/mod.rs b/wallet/wallet-cli-lib/src/commands/mod.rs index 510ec20338..59e80ef42e 100644 --- a/wallet/wallet-cli-lib/src/commands/mod.rs +++ b/wallet/wallet-cli-lib/src/commands/mod.rs @@ -896,7 +896,7 @@ impl CommandHandler { self.get_synced_controller() .await? - .lock_tokens(token_id) + .lock_token_supply(token_id) .await .map_err(WalletCliError::Controller)?; diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index 2f8d9e3253..e53085905c 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -205,13 +205,13 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { self.broadcast_to_mempool(tx).await } - pub async fn lock_tokens(&mut self, token_id: TokenId) -> Result<(), ControllerError> { + pub async fn lock_token_supply(&mut self, token_id: TokenId) -> Result<(), ControllerError> { let (current_fee_rate, consolidate_fee_rate) = self.get_current_and_consolidation_fee_rate().await?; let tx = self .wallet - .lock_tokens( + .lock_token_supply( self.account_index, token_id, current_fee_rate, From d8a8e50dd0b936c43d15fe77ad010121e13d841f Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Wed, 18 Oct 2023 20:33:26 +0200 Subject: [PATCH 08/11] Remove unused command argument - remove unused amount_to_issue argument for the new issue totkens command - rename TokenTotalSupplyState to TokenCurrentSupplyState --- .../test_framework/wallet_cli_controller.py | 7 +- test/functional/wallet_tokens.py | 12 +-- .../functional/wallet_tokens_change_supply.py | 18 ++--- wallet/src/account/mod.rs | 2 +- wallet/src/account/output_cache/mod.rs | 78 +++++++++++-------- wallet/src/wallet/tests.rs | 2 +- wallet/types/src/utxo_types.rs | 4 +- wallet/wallet-cli-lib/src/commands/mod.rs | 8 +- wallet/wallet-controller/src/read.rs | 2 +- 9 files changed, 71 insertions(+), 62 deletions(-) diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index 91e2301526..182266be0c 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -175,12 +175,11 @@ async def send_tokens_to_address(self, token_id: str, address: str, amount: floa async def issue_new_token(self, token_ticker: str, - amount_to_issue: str, number_of_decimals: int, metadata_uri: str, destination_address: str, token_supply: str = 'unlimited') -> Tuple[Optional[str], Optional[str]]: - output = await self._write_command(f'issuenewtoken "{token_ticker}" "{amount_to_issue}" "{number_of_decimals}" "{metadata_uri}" {destination_address} {token_supply}\n') + output = await self._write_command(f'issuenewtoken "{token_ticker}" "{number_of_decimals}" "{metadata_uri}" {destination_address} {token_supply}\n') if output.startswith("A new token has been issued with ID"): return output[output.find(':')+2:], None @@ -192,8 +191,8 @@ async def mint_tokens(self, token_id: str, address: str, amount: int) -> str: async def unmint_tokens(self, token_id: str, amount: int) -> str: return await self._write_command(f"unminttokens {token_id} {amount}\n") - async def lock_tokens(self, token_id: str) -> str: - return await self._write_command(f"locktokens {token_id}\n") + async def lock_token_suply(self, token_id: str) -> str: + return await self._write_command(f"locktokensuply {token_id}\n") async def issue_new_nft(self, destination_address: str, diff --git a/test/functional/wallet_tokens.py b/test/functional/wallet_tokens.py index 414a12c8dd..e93a55e94c 100644 --- a/test/functional/wallet_tokens.py +++ b/test/functional/wallet_tokens.py @@ -113,30 +113,30 @@ async def async_test(self): # invalid ticker # > max len - token_id, err = await wallet.issue_new_token("asdddd", "10000", 2, "http://uri", address) + token_id, err = await wallet.issue_new_token("asdddd", 2, "http://uri", address) assert token_id is None assert err is not None assert_in("Invalid ticker length", err) # non alphanumeric - token_id, err = await wallet.issue_new_token("asd#", "10000", 2, "http://uri", address) + token_id, err = await wallet.issue_new_token("asd#", 2, "http://uri", address) assert token_id is None assert err is not None assert_in("Invalid character in token ticker", err) # invalid url - token_id, err = await wallet.issue_new_token("XXX", "10000", 2, "123 123", address) + token_id, err = await wallet.issue_new_token("XXX", 2, "123 123", address) assert token_id is None assert err is not None assert_in("Incorrect metadata URI", err) # invalid num decimals - token_id, err = await wallet.issue_new_token("XXX", "10000", 99, "http://uri", address) + token_id, err = await wallet.issue_new_token("XXX", 99, "http://uri", address) assert token_id is None assert err is not None assert_in("Too many decimals", err) # issue a valid token - token_id, err = await wallet.issue_new_token("XXX", "10000", 2, "http://uri", address) + token_id, err = await wallet.issue_new_token("XXX", 2, "http://uri", address) assert token_id is not None assert err is None self.log.info(f"new token id: {token_id}") @@ -167,7 +167,7 @@ async def async_test(self): assert_in(f"{token_id} amount: 9989.99", await wallet.get_balance()) ## try to issue a new token, should fail with not enough coins - token_id, err = await wallet.issue_new_token("XXX", "10000", 2, "http://uri", address) + token_id, err = await wallet.issue_new_token("XXX", 2, "http://uri", address) assert token_id is None assert err is not None assert_in("Not enough funds", err) diff --git a/test/functional/wallet_tokens_change_supply.py b/test/functional/wallet_tokens_change_supply.py index 4e508a04bc..939de8c1a6 100644 --- a/test/functional/wallet_tokens_change_supply.py +++ b/test/functional/wallet_tokens_change_supply.py @@ -92,7 +92,7 @@ async def async_test(self): # Submit a valid transaction output = { - 'Transfer': [ { 'Coin': 1001 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ], + 'Transfer': [ { 'Coin': 2001 * ATOMS_PER_COIN }, { 'PublicKey': {'key': {'Secp256k1Schnorr' : {'pubkey_data': pub_key_bytes}}} } ], } encoded_tx, tx_id = make_tx([reward_input(tip_id)], [output], 0) @@ -109,36 +109,36 @@ async def async_test(self): assert_equal(await wallet.get_best_block_height(), '1') assert_equal(await wallet.get_best_block(), block_id) - assert_in("Coins amount: 1001", await wallet.get_balance()) + assert_in("Coins amount: 2001", await wallet.get_balance()) address = await wallet.new_address() # invalid ticker # > max len - token_id, err = await wallet.issue_new_token("asdddd", "10000", 2, "http://uri", address) + token_id, err = await wallet.issue_new_token("asdddd", 2, "http://uri", address) assert token_id is None assert err is not None assert_in("Invalid ticker length", err) # non alphanumeric - token_id, err = await wallet.issue_new_token("asd#", "10000", 2, "http://uri", address) + token_id, err = await wallet.issue_new_token("asd#", 2, "http://uri", address) assert token_id is None assert err is not None assert_in("Invalid character in token ticker", err) # invalid url - token_id, err = await wallet.issue_new_token("XXX", "10000", 2, "123 123", address) + token_id, err = await wallet.issue_new_token("XXX", 2, "123 123", address) assert token_id is None assert err is not None assert_in("Incorrect metadata URI", err) # invalid num decimals - token_id, err = await wallet.issue_new_token("XXX", "10000", 99, "http://uri", address) + token_id, err = await wallet.issue_new_token("XXX", 99, "http://uri", address) assert token_id is None assert err is not None assert_in("Too many decimals", err) # issue a valid token - token_id, err = await wallet.issue_new_token("XXX", "10000", 2, "http://uri", address, 'lockable') + token_id, err = await wallet.issue_new_token("XXX", 2, "http://uri", address, 'lockable') assert token_id is not None assert err is None self.log.info(f"new token id: {token_id}") @@ -179,7 +179,7 @@ async def async_test(self): assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance(utxo_states=['confirmed', 'inactive'])) # lock token supply - assert_in("The transaction was submitted successfully", await wallet.lock_tokens(token_id)) + assert_in("The transaction was submitted successfully", await wallet.lock_token_suply(token_id)) self.generate_block() assert_in("Success", await wallet.sync()) assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance()) @@ -187,7 +187,7 @@ async def async_test(self): # cannot mint any more tokens as it is locked assert_in("Cannot change a Locked Token supply", await wallet.mint_tokens(token_id, address, tokens_to_mint)) assert_in("Cannot change a Locked Token supply", await wallet.unmint_tokens(token_id, tokens_to_mint)) - assert_in("Cannot lock Token supply in state: Locked", await wallet.lock_tokens(token_id)) + assert_in("Cannot lock Token supply in state: Locked", await wallet.lock_token_suply(token_id)) if __name__ == '__main__': diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index 48048a80ba..bec227a678 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -188,7 +188,7 @@ impl Account { let (utxos, selection_algo) = if input_utxos.is_empty() { ( self.get_utxos( - UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::TokensOp, + UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::IssueNft, median_time, UtxoState::Confirmed | UtxoState::InMempool | UtxoState::Inactive, WithLocked::Unlocked, diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index a8cb8d7786..d596207ac6 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -80,24 +80,24 @@ impl PoolData { } } -pub enum TokenTotalSupplyState { +pub enum TokenCurrentSupplyState { Fixed(Amount, Amount), // fixed to a certain amount Lockable(Amount), // not known in advance but can be locked once at some point in time Locked(Amount), // Locked Unlimited(Amount), // limited only by the Amount data type } -impl From for TokenTotalSupplyState { +impl From for TokenCurrentSupplyState { fn from(value: TokenTotalSupply) -> Self { match value { - TokenTotalSupply::Fixed(amount) => TokenTotalSupplyState::Fixed(amount, Amount::ZERO), - TokenTotalSupply::Lockable => TokenTotalSupplyState::Lockable(Amount::ZERO), - TokenTotalSupply::Unlimited => TokenTotalSupplyState::Unlimited(Amount::ZERO), + TokenTotalSupply::Fixed(amount) => TokenCurrentSupplyState::Fixed(amount, Amount::ZERO), + TokenTotalSupply::Lockable => TokenCurrentSupplyState::Lockable(Amount::ZERO), + TokenTotalSupply::Unlimited => TokenCurrentSupplyState::Unlimited(Amount::ZERO), } } } -impl TokenTotalSupplyState { +impl TokenCurrentSupplyState { pub fn str_state(&self) -> &'static str { match self { Self::Unlimited(_) => "Unlimited", @@ -137,10 +137,10 @@ impl TokenTotalSupplyState { pub fn check_can_lock(&self) -> WalletResult<()> { match self { - TokenTotalSupplyState::Lockable(_) => Ok(()), - TokenTotalSupplyState::Unlimited(_) - | TokenTotalSupplyState::Fixed(_, _) - | TokenTotalSupplyState::Locked(_) => { + TokenCurrentSupplyState::Lockable(_) => Ok(()), + TokenCurrentSupplyState::Unlimited(_) + | TokenCurrentSupplyState::Fixed(_, _) + | TokenCurrentSupplyState::Locked(_) => { Err(WalletError::CannotLockTokenSupply(self.str_state())) } } @@ -155,62 +155,66 @@ impl TokenTotalSupplyState { } } - fn mint(&self, amount: Amount) -> WalletResult { + fn mint(&self, amount: Amount) -> WalletResult { match self { - TokenTotalSupplyState::Lockable(current) => Ok(TokenTotalSupplyState::Lockable( + TokenCurrentSupplyState::Lockable(current) => Ok(TokenCurrentSupplyState::Lockable( (*current + amount).ok_or(WalletError::OutputAmountOverflow)?, )), - TokenTotalSupplyState::Unlimited(current) => Ok(TokenTotalSupplyState::Unlimited( + TokenCurrentSupplyState::Unlimited(current) => Ok(TokenCurrentSupplyState::Unlimited( (*current + amount).ok_or(WalletError::OutputAmountOverflow)?, )), - TokenTotalSupplyState::Fixed(max, current) => { + TokenCurrentSupplyState::Fixed(max, current) => { let changed = (*current + amount).ok_or(WalletError::OutputAmountOverflow)?; ensure!( changed <= *max, WalletError::CannotMintFixedTokenSupply(*max, *current, amount) ); - Ok(TokenTotalSupplyState::Fixed(*max, changed)) + Ok(TokenCurrentSupplyState::Fixed(*max, changed)) } - TokenTotalSupplyState::Locked(_) => Err(WalletError::CannotChangeLockedTokenSupply), + TokenCurrentSupplyState::Locked(_) => Err(WalletError::CannotChangeLockedTokenSupply), } } - fn unmint(&self, amount: Amount) -> WalletResult { + fn unmint(&self, amount: Amount) -> WalletResult { match self { - TokenTotalSupplyState::Lockable(current) => Ok(TokenTotalSupplyState::Lockable( + TokenCurrentSupplyState::Lockable(current) => Ok(TokenCurrentSupplyState::Lockable( (*current - amount) .ok_or(WalletError::CannotUnmintTokenSupply(amount, *current))?, )), - TokenTotalSupplyState::Unlimited(current) => Ok(TokenTotalSupplyState::Unlimited( + TokenCurrentSupplyState::Unlimited(current) => Ok(TokenCurrentSupplyState::Unlimited( (*current - amount) .ok_or(WalletError::CannotUnmintTokenSupply(amount, *current))?, )), - TokenTotalSupplyState::Fixed(max, current) => Ok(TokenTotalSupplyState::Fixed( + TokenCurrentSupplyState::Fixed(max, current) => Ok(TokenCurrentSupplyState::Fixed( *max, (*current - amount) .ok_or(WalletError::CannotUnmintTokenSupply(amount, *current))?, )), - TokenTotalSupplyState::Locked(_) => Err(WalletError::CannotChangeLockedTokenSupply), + TokenCurrentSupplyState::Locked(_) => Err(WalletError::CannotChangeLockedTokenSupply), } } - fn lock(&self) -> WalletResult { + fn lock(&self) -> WalletResult { match self { - TokenTotalSupplyState::Lockable(current) => Ok(TokenTotalSupplyState::Locked(*current)), - TokenTotalSupplyState::Unlimited(_) - | TokenTotalSupplyState::Fixed(_, _) - | TokenTotalSupplyState::Locked(_) => { + TokenCurrentSupplyState::Lockable(current) => { + Ok(TokenCurrentSupplyState::Locked(*current)) + } + TokenCurrentSupplyState::Unlimited(_) + | TokenCurrentSupplyState::Fixed(_, _) + | TokenCurrentSupplyState::Locked(_) => { Err(WalletError::CannotLockTokenSupply(self.str_state())) } } } - fn unlock(&self) -> WalletResult { + fn unlock(&self) -> WalletResult { match self { - TokenTotalSupplyState::Locked(current) => Ok(TokenTotalSupplyState::Lockable(*current)), - TokenTotalSupplyState::Unlimited(_) - | TokenTotalSupplyState::Fixed(_, _) - | TokenTotalSupplyState::Lockable(_) => { + TokenCurrentSupplyState::Locked(current) => { + Ok(TokenCurrentSupplyState::Lockable(*current)) + } + TokenCurrentSupplyState::Unlimited(_) + | TokenCurrentSupplyState::Fixed(_, _) + | TokenCurrentSupplyState::Lockable(_) => { Err(WalletError::InconsistentUnlockTokenSupply(self.str_state())) } } @@ -218,7 +222,7 @@ impl TokenTotalSupplyState { } pub struct TokenIssuanceData { - pub total_supply: TokenTotalSupplyState, + pub total_supply: TokenCurrentSupplyState, pub reissuance_controller: Destination, pub last_nonce: Option, /// last parent transaction if the parent is unconfirmed @@ -863,7 +867,15 @@ fn sum_burned_token_amount( .iter() .filter_map(|output| match output { TxOutput::Burn(OutputValue::TokenV1(tid, amount)) if tid == token_id => Some(*amount), - _ => None, + TxOutput::Transfer(_, _) + | TxOutput::Burn(_) + | TxOutput::IssueFungibleToken(_) + | TxOutput::IssueNft(_, _, _) + | TxOutput::CreateStakePool(_, _) + | TxOutput::LockThenTransfer(_, _, _) + | TxOutput::DelegateStaking(_, _) + | TxOutput::CreateDelegationId(_, _) + | TxOutput::ProduceBlockFromStake(_, _) => None, }) .sum::>() .ok_or(WalletError::OutputAmountOverflow); diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index c9c4acba2b..9a42e251e7 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -2169,7 +2169,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { let currency_balances = wallet .get_balance( DEFAULT_ACCOUNT_INDEX, - UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::TokensOp, + UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::IssueNft, UtxoState::Confirmed.into(), WithLocked::Unlocked, ) diff --git a/wallet/types/src/utxo_types.rs b/wallet/types/src/utxo_types.rs index f2b319f379..a2dab7b9e2 100644 --- a/wallet/types/src/utxo_types.rs +++ b/wallet/types/src/utxo_types.rs @@ -31,7 +31,7 @@ pub enum UtxoType { ProduceBlockFromStake = 1 << 4, CreateDelegationId = 1 << 5, DelegateStaking = 1 << 6, - TokensOp = 1 << 7, + IssueNft = 1 << 7, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -53,8 +53,8 @@ pub fn get_utxo_type(output: &TxOutput) -> Option { TxOutput::ProduceBlockFromStake(_, _) => Some(UtxoType::ProduceBlockFromStake), TxOutput::CreateDelegationId(_, _) => Some(UtxoType::CreateDelegationId), TxOutput::DelegateStaking(_, _) => Some(UtxoType::DelegateStaking), + TxOutput::IssueNft(_, _, _) => Some(UtxoType::IssueNft), TxOutput::IssueFungibleToken(_) => None, - TxOutput::IssueNft(_, _, _) => Some(UtxoType::TokensOp), } } pub fn get_utxo_state(output: &TxState) -> UtxoState { diff --git a/wallet/wallet-cli-lib/src/commands/mod.rs b/wallet/wallet-cli-lib/src/commands/mod.rs index 59e80ef42e..03a61fca8a 100644 --- a/wallet/wallet-cli-lib/src/commands/mod.rs +++ b/wallet/wallet-cli-lib/src/commands/mod.rs @@ -187,7 +187,6 @@ pub enum WalletCommand { /// Issue a new token IssueNewToken { token_ticker: String, - amount_to_issue: String, number_of_decimals: u8, metadata_uri: String, destination_address: String, @@ -220,8 +219,8 @@ pub enum WalletCommand { amount: String, }, - /// Lock the total supply for the tokens - LockTokens { + /// Lock the circulating supply for the token + LockTokenSuply { token_id: String, }, @@ -773,7 +772,6 @@ impl CommandHandler { WalletCommand::IssueNewToken { token_ticker, - amount_to_issue: _, number_of_decimals, metadata_uri, destination_address, @@ -891,7 +889,7 @@ impl CommandHandler { Ok(Self::tx_submitted_command()) } - WalletCommand::LockTokens { token_id } => { + WalletCommand::LockTokenSuply { token_id } => { let token_id = parse_token_id(chain_config, token_id.as_str())?; self.get_synced_controller() diff --git a/wallet/wallet-controller/src/read.rs b/wallet/wallet-controller/src/read.rs index b2b8e9dc5b..f1a6ab57a6 100644 --- a/wallet/wallet-controller/src/read.rs +++ b/wallet/wallet-controller/src/read.rs @@ -72,7 +72,7 @@ impl<'a, T: NodeInterface> ReadOnlyController<'a, T> { self.wallet .get_balance( self.account_index, - UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::TokensOp, + UtxoType::Transfer | UtxoType::LockThenTransfer | UtxoType::IssueNft, utxo_states, with_locked, ) From 0866470233f1420d973fd6230752a4215a11952c Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Thu, 19 Oct 2023 14:24:15 +0200 Subject: [PATCH 09/11] Fix query token data for NFT issuance v1 --- chainstate/src/detail/query.rs | 78 +++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/chainstate/src/detail/query.rs b/chainstate/src/detail/query.rs index 61decb8e3d..3b37142eba 100644 --- a/chainstate/src/detail/query.rs +++ b/chainstate/src/detail/query.rs @@ -19,8 +19,8 @@ use common::{ chain::{ block::{signed_block_header::SignedBlockHeader, BlockReward}, tokens::{ - RPCFungibleTokenInfo, RPCNonFungibleTokenInfo, RPCTokenInfo, RPCTokenTotalSupply, - TokenAuxiliaryData, TokenData, TokenId, + NftIssuance, RPCFungibleTokenInfo, RPCNonFungibleTokenInfo, RPCTokenInfo, + RPCTokenTotalSupply, TokenAuxiliaryData, TokenData, TokenId, }, Block, GenBlock, OutPointSourceId, SignedTransaction, Transaction, TxMainChainIndex, TxOutput, @@ -298,39 +298,29 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat .issuance_tx() .outputs() .iter() - // Filter tokens - .filter_map(|output| match output { + // find tokens + .find_map(|output| match output { TxOutput::Transfer(v, _) | TxOutput::LockThenTransfer(v, _, _) - | TxOutput::Burn(v) => v.token_data(), + | TxOutput::Burn(v) => v.token_data().and_then(|token_data| { + to_rpc_token_info(token_data, token_id, &token_aux_data) + }), TxOutput::CreateStakePool(_, _) | TxOutput::ProduceBlockFromStake(_, _) | TxOutput::CreateDelegationId(_, _) - | TxOutput::DelegateStaking(_, _) - | TxOutput::IssueFungibleToken(_) - | TxOutput::IssueNft(_, _, _) => None, - }) - // Find issuance data and return RPCTokenInfo - .find_map(|token_data| match token_data { - TokenData::TokenIssuance(issuance) => { - Some(RPCTokenInfo::new_fungible(RPCFungibleTokenInfo::new( - token_id, - issuance.token_ticker.clone(), - issuance.number_of_decimals, - issuance.metadata_uri.clone(), - issuance.amount_to_issue, - RPCTokenTotalSupply::Fixed(issuance.amount_to_issue), - ))) - } - TokenData::NftIssuance(nft) => { - Some(RPCTokenInfo::new_nonfungible(RPCNonFungibleTokenInfo::new( - token_id, - token_aux_data.issuance_tx().get_id(), - token_aux_data.issuance_block_id(), - &nft.metadata, - ))) - } - TokenData::TokenTransfer(_) => None, + | TxOutput::DelegateStaking(_, _) => None, + TxOutput::IssueNft(_, issuance, _) => match issuance.as_ref() { + NftIssuance::V0(nft) => { + Some(RPCTokenInfo::new_nonfungible(RPCNonFungibleTokenInfo::new( + token_id, + token_aux_data.issuance_tx().get_id(), + token_aux_data.issuance_block_id(), + &nft.metadata, + ))) + } + }, + // Should be handled by the token data branch + TxOutput::IssueFungibleToken(_) => None, })) } } @@ -371,3 +361,31 @@ impl<'a, S: BlockchainStorageRead, V: TransactionVerificationStrategy> Chainstat self.chainstate_ref.get_circulating_supply(id).map_err(PropertyQueryError::from) } } + +fn to_rpc_token_info( + token_data: &TokenData, + token_id: TokenId, + token_aux_data: &TokenAuxiliaryData, +) -> Option { + match token_data { + TokenData::TokenIssuance(issuance) => { + Some(RPCTokenInfo::new_fungible(RPCFungibleTokenInfo::new( + token_id, + issuance.token_ticker.clone(), + issuance.number_of_decimals, + issuance.metadata_uri.clone(), + issuance.amount_to_issue, + RPCTokenTotalSupply::Fixed(issuance.amount_to_issue), + ))) + } + TokenData::NftIssuance(nft) => { + Some(RPCTokenInfo::new_nonfungible(RPCNonFungibleTokenInfo::new( + token_id, + token_aux_data.issuance_tx().get_id(), + token_aux_data.issuance_block_id(), + &nft.metadata, + ))) + } + TokenData::TokenTransfer(_) => None, + } +} From b530f83038d7bbfc32b09d98698b1018c8456663 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Thu, 19 Oct 2023 14:24:58 +0200 Subject: [PATCH 10/11] Uncomment NFT wallet functional test --- .../test_framework/wallet_cli_controller.py | 2 +- test/functional/wallet_nfts.py | 40 +++++++++---------- .../functional/wallet_tokens_change_supply.py | 2 +- wallet/src/wallet/tests.rs | 2 +- wallet/wallet-cli-lib/src/commands/mod.rs | 4 +- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index 182266be0c..98b3a9e567 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -192,7 +192,7 @@ async def unmint_tokens(self, token_id: str, amount: int) -> str: return await self._write_command(f"unminttokens {token_id} {amount}\n") async def lock_token_suply(self, token_id: str) -> str: - return await self._write_command(f"locktokensuply {token_id}\n") + return await self._write_command(f"locktokensupply {token_id}\n") async def issue_new_nft(self, destination_address: str, diff --git a/test/functional/wallet_nfts.py b/test/functional/wallet_nfts.py index d17cc3be46..5952e18c40 100644 --- a/test/functional/wallet_nfts.py +++ b/test/functional/wallet_nfts.py @@ -119,27 +119,25 @@ async def async_test(self): assert_in("Success", await wallet.sync()) - # TODO: add support for tokens v1 - # See https://github.com/mintlayer/mintlayer-core/issues/1237 - #self.log.info(await wallet.get_balance()) - #assert_in(f"{nft_id} amount: 1", await wallet.get_balance()) - - ## create a new account and send some tokens to it - #await wallet.create_new_account() - #await wallet.select_account(1) - #address = await wallet.new_address() - - #await wallet.select_account(0) - #assert_in(f"{nft_id} amount: 1", await wallet.get_balance()) - #output = await wallet.send_tokens_to_address(nft_id, address, 1) - #self.log.info(output) - #assert_in("The transaction was submitted successfully", output) - - #self.generate_block() - #assert_in("Success", await wallet.sync()) - - ## check the new balance nft is not present - #assert nft_id not in await wallet.get_balance() + self.log.info(await wallet.get_balance()) + assert_in(f"{nft_id} amount: 1", await wallet.get_balance()) + + # create a new account and send some tokens to it + await wallet.create_new_account() + await wallet.select_account(1) + address = await wallet.new_address() + + await wallet.select_account(0) + assert_in(f"{nft_id} amount: 1", await wallet.get_balance()) + output = await wallet.send_tokens_to_address(nft_id, address, 1) + self.log.info(output) + assert_in("The transaction was submitted successfully", output) + + self.generate_block() + assert_in("Success", await wallet.sync()) + + # check the new balance nft is not present + assert nft_id not in await wallet.get_balance() if __name__ == '__main__': WalletNfts().main() diff --git a/test/functional/wallet_tokens_change_supply.py b/test/functional/wallet_tokens_change_supply.py index 939de8c1a6..1df2e63a2d 100644 --- a/test/functional/wallet_tokens_change_supply.py +++ b/test/functional/wallet_tokens_change_supply.py @@ -58,7 +58,7 @@ def generate_block(self): block_input_data = block_input_data_obj.encode(block_input_data).to_hex()[2:] # create a new block, taking transactions from mempool - block = node.blockprod_generate_block(block_input_data, None) + block = node.blockprod_generate_block(block_input_data, [], [], "FillSpaceFromMempool") node.chainstate_submit_block(block) block_id = node.chainstate_best_block_id() diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 9a42e251e7..985d4ca216 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -2309,7 +2309,7 @@ fn issue_and_transfer_tokens(#[case] seed: Seed) { #[case(Seed::from_entropy())] fn check_tokens_v0_are_ignored(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); - let chain_config = Arc::new(create_mainnet()); + let chain_config = Arc::new(create_regtest()); let mut wallet = create_wallet(chain_config.clone()); diff --git a/wallet/wallet-cli-lib/src/commands/mod.rs b/wallet/wallet-cli-lib/src/commands/mod.rs index 03a61fca8a..7f19921c6e 100644 --- a/wallet/wallet-cli-lib/src/commands/mod.rs +++ b/wallet/wallet-cli-lib/src/commands/mod.rs @@ -220,7 +220,7 @@ pub enum WalletCommand { }, /// Lock the circulating supply for the token - LockTokenSuply { + LockTokenSupply { token_id: String, }, @@ -889,7 +889,7 @@ impl CommandHandler { Ok(Self::tx_submitted_command()) } - WalletCommand::LockTokenSuply { token_id } => { + WalletCommand::LockTokenSupply { token_id } => { let token_id = parse_token_id(chain_config, token_id.as_str())?; self.get_synced_controller() From 8d0d1b32898aeb146129183e640e6bf512a553d9 Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Tue, 24 Oct 2023 13:54:42 +0200 Subject: [PATCH 11/11] Fix typos --- test/functional/test_framework/wallet_cli_controller.py | 2 +- test/functional/wallet_delegations.py | 4 ++-- test/functional/wallet_tokens_change_supply.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index 98b3a9e567..ed0e2f1d02 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -191,7 +191,7 @@ async def mint_tokens(self, token_id: str, address: str, amount: int) -> str: async def unmint_tokens(self, token_id: str, amount: int) -> str: return await self._write_command(f"unminttokens {token_id} {amount}\n") - async def lock_token_suply(self, token_id: str) -> str: + async def lock_token_supply(self, token_id: str) -> str: return await self._write_command(f"locktokensupply {token_id}\n") async def issue_new_nft(self, diff --git a/test/functional/wallet_delegations.py b/test/functional/wallet_delegations.py index f633e4a73c..10eab5ac64 100644 --- a/test/functional/wallet_delegations.py +++ b/test/functional/wallet_delegations.py @@ -389,7 +389,7 @@ async def async_test(self): self.wait_until(lambda: node.chainstate_best_block_id() != tip_id, timeout = 5) assert_in("Success", await wallet.sync()) - # check that we still don't have any delagations for this account + # check that we still don't have any delegations for this account delegations = await wallet.list_delegation_ids() assert_equal(len(delegations), 0) @@ -399,7 +399,7 @@ async def async_test(self): self.wait_until(lambda: node.chainstate_best_block_id() != tip_id, timeout = 5) assert_in("Success", await wallet.sync()) - # check that we still don't have any delagations for this account + # check that we still don't have any delegations for this account delegations = await wallet.list_delegation_ids() assert_equal(len(delegations), 0) diff --git a/test/functional/wallet_tokens_change_supply.py b/test/functional/wallet_tokens_change_supply.py index 1df2e63a2d..0b8b1d5cd4 100644 --- a/test/functional/wallet_tokens_change_supply.py +++ b/test/functional/wallet_tokens_change_supply.py @@ -179,7 +179,7 @@ async def async_test(self): assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance(utxo_states=['confirmed', 'inactive'])) # lock token supply - assert_in("The transaction was submitted successfully", await wallet.lock_token_suply(token_id)) + assert_in("The transaction was submitted successfully", await wallet.lock_token_supply(token_id)) self.generate_block() assert_in("Success", await wallet.sync()) assert_in(f"{token_id} amount: {total_tokens_supply}", await wallet.get_balance()) @@ -187,7 +187,7 @@ async def async_test(self): # cannot mint any more tokens as it is locked assert_in("Cannot change a Locked Token supply", await wallet.mint_tokens(token_id, address, tokens_to_mint)) assert_in("Cannot change a Locked Token supply", await wallet.unmint_tokens(token_id, tokens_to_mint)) - assert_in("Cannot lock Token supply in state: Locked", await wallet.lock_token_suply(token_id)) + assert_in("Cannot lock Token supply in state: Locked", await wallet.lock_token_supply(token_id)) if __name__ == '__main__':