diff --git a/api-server/api-server-common/src/storage/storage_api/mod.rs b/api-server/api-server-common/src/storage/storage_api/mod.rs index 3884c55d2..1beef4252 100644 --- a/api-server/api-server-common/src/storage/storage_api/mod.rs +++ b/api-server/api-server-common/src/storage/storage_api/mod.rs @@ -355,6 +355,11 @@ impl FungibleTokenData { self.authority = authority; self } + + pub fn change_metadata_uri(mut self, metadata_uri: Vec) -> Self { + self.metadata_uri = metadata_uri; + self + } } #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode)] diff --git a/api-server/scanner-lib/src/blockchain_state/mod.rs b/api-server/scanner-lib/src/blockchain_state/mod.rs index 9e16a78e4..734189da5 100644 --- a/api-server/scanner-lib/src/blockchain_state/mod.rs +++ b/api-server/scanner-lib/src/blockchain_state/mod.rs @@ -617,6 +617,7 @@ async fn calculate_fees( | AccountCommand::UnmintTokens(token_id) | AccountCommand::UnfreezeToken(token_id) | AccountCommand::LockTokenSupply(token_id) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) | AccountCommand::ChangeTokenAuthority(token_id, _) => Some(*token_id), AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => None, }, @@ -1138,6 +1139,30 @@ async fn update_tables_from_transaction_inputs( ) .await; } + AccountCommand::ChangeTokenMetadataUri(token_id, metadata_uri) => { + let issuance = + db_tx.get_fungible_token_issuance(*token_id).await?.expect("must exist"); + + let issuance = issuance.change_metadata_uri(metadata_uri.clone()); + db_tx.set_fungible_token_issuance(*token_id, block_height, issuance).await?; + let amount = chain_config.token_change_metadata_uri_fee(); + increase_statistic_amount( + db_tx, + CoinOrTokenStatistic::Burned, + &amount, + CoinOrTokenId::Coin, + block_height, + ) + .await; + decrease_statistic_amount( + db_tx, + CoinOrTokenStatistic::CirculatingSupply, + &amount, + CoinOrTokenId::Coin, + block_height, + ) + .await; + } AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => { // TODO(orders) } diff --git a/api-server/scanner-lib/src/sync/tests/simulation.rs b/api-server/scanner-lib/src/sync/tests/simulation.rs index 2102a7743..44bb6ce0f 100644 --- a/api-server/scanner-lib/src/sync/tests/simulation.rs +++ b/api-server/scanner-lib/src/sync/tests/simulation.rs @@ -486,6 +486,11 @@ async fn simulation( chain_config.token_change_authority_fee(block_height); burn_coins(&mut statistics, token_change_authority_fee); } + AccountCommand::ChangeTokenMetadataUri(_token_id, _) => { + let token_change_metadata_fee = + chain_config.token_change_metadata_uri_fee(); + burn_coins(&mut statistics, token_change_metadata_fee); + } AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => { unimplemented!() // TODO(orders) } diff --git a/api-server/web-server/src/api/json_helpers.rs b/api-server/web-server/src/api/json_helpers.rs index 73a88d971..955d8872b 100644 --- a/api-server/web-server/src/api/json_helpers.rs +++ b/api-server/web-server/src/api/json_helpers.rs @@ -361,6 +361,14 @@ pub fn tx_input_to_json( "destination": Address::new(chain_config, dest.clone()).expect("no error").as_str(), }) } + AccountCommand::ChangeTokenMetadataUri(token_id, metadata_uri) => { + json!({ + "input_type": "AccountCommand", + "command": "ChangeTokenMetadataUri", + "token_id": Address::new(chain_config, *token_id).expect("addressable").to_string(), + "metadata_uri": metadata_uri, + }) + } }, } } diff --git a/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs b/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs index d8f17ddff..a153d9606 100644 --- a/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs +++ b/chainstate/constraints-value-accumulator/src/constraints_accumulator.rs @@ -290,6 +290,10 @@ impl ConstrainedValueAccumulator { CoinOrTokenId::Coin, chain_config.token_change_authority_fee(block_height), )), + AccountCommand::ChangeTokenMetadataUri(_, _) => Ok(( + CoinOrTokenId::Coin, + chain_config.token_change_metadata_uri_fee(), + )), AccountCommand::ConcludeOrder(id) => { let order_data = orders_accounting_delta .get_order_data(id) diff --git a/chainstate/constraints-value-accumulator/src/tests/constraints_tests.rs b/chainstate/constraints-value-accumulator/src/tests/constraints_tests.rs index e77c5cf92..ea7ffdd83 100644 --- a/chainstate/constraints-value-accumulator/src/tests/constraints_tests.rs +++ b/chainstate/constraints-value-accumulator/src/tests/constraints_tests.rs @@ -749,7 +749,7 @@ fn calculate_fee_for_token_issuance(#[case] seed: Seed) { 0, ))]; let input_utxos = vec![Some(TxOutput::Transfer( - OutputValue::Coin((token_issuance_fee * 2).unwrap()), + OutputValue::Coin(token_issuance_fee), Destination::AnyoneCanSpend, ))]; @@ -781,7 +781,7 @@ fn calculate_fee_for_token_issuance(#[case] seed: Seed) { .map_into_block_fees(&chain_config, block_height) .unwrap(); - assert_eq!(accumulated_fee, Fee(token_issuance_fee)); + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); } // Check that token supply change fee is not accounted in accumulated fee and is burned rather then goes to staker. @@ -833,7 +833,7 @@ fn calculate_token_supply_change_fee(#[case] seed: Seed) { let input_utxos = vec![ None, Some(TxOutput::Transfer( - OutputValue::Coin((supply_change_fee * 2).unwrap()), + OutputValue::Coin(supply_change_fee), Destination::AnyoneCanSpend, )), ]; @@ -862,7 +862,7 @@ fn calculate_token_supply_change_fee(#[case] seed: Seed) { .map_into_block_fees(&chain_config, block_height) .unwrap(); - assert_eq!(accumulated_fee, Fee(supply_change_fee)); + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); } // Unmint @@ -877,7 +877,7 @@ fn calculate_token_supply_change_fee(#[case] seed: Seed) { let input_utxos = vec![ None, Some(TxOutput::Transfer( - OutputValue::Coin((supply_change_fee * 2).unwrap()), + OutputValue::Coin(supply_change_fee), Destination::AnyoneCanSpend, )), ]; @@ -906,7 +906,7 @@ fn calculate_token_supply_change_fee(#[case] seed: Seed) { .map_into_block_fees(&chain_config, block_height) .unwrap(); - assert_eq!(accumulated_fee, Fee(supply_change_fee)); + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); } // Lock supply @@ -924,7 +924,7 @@ fn calculate_token_supply_change_fee(#[case] seed: Seed) { let input_utxos = vec![ None, Some(TxOutput::Transfer( - OutputValue::Coin((supply_change_fee * 2).unwrap()), + OutputValue::Coin(supply_change_fee), Destination::AnyoneCanSpend, )), ]; @@ -953,7 +953,7 @@ fn calculate_token_supply_change_fee(#[case] seed: Seed) { .map_into_block_fees(&chain_config, block_height) .unwrap(); - assert_eq!(accumulated_fee, Fee(supply_change_fee)); + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); } } @@ -1009,7 +1009,7 @@ fn calculate_token_fee_freeze(#[case] seed: Seed) { let input_utxos = vec![ None, Some(TxOutput::Transfer( - OutputValue::Coin((supply_change_fee * 2).unwrap()), + OutputValue::Coin(supply_change_fee), Destination::AnyoneCanSpend, )), ]; @@ -1038,7 +1038,7 @@ fn calculate_token_fee_freeze(#[case] seed: Seed) { .map_into_block_fees(&chain_config, block_height) .unwrap(); - assert_eq!(accumulated_fee, Fee(supply_change_fee)); + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); } // Unfreeze @@ -1056,7 +1056,7 @@ fn calculate_token_fee_freeze(#[case] seed: Seed) { let input_utxos = vec![ None, Some(TxOutput::Transfer( - OutputValue::Coin((supply_change_fee * 2).unwrap()), + OutputValue::Coin(supply_change_fee), Destination::AnyoneCanSpend, )), ]; @@ -1085,7 +1085,7 @@ fn calculate_token_fee_freeze(#[case] seed: Seed) { .map_into_block_fees(&chain_config, block_height) .unwrap(); - assert_eq!(accumulated_fee, Fee(supply_change_fee)); + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); } } @@ -1136,7 +1136,7 @@ fn calculate_token_fee_change_authority(#[case] seed: Seed) { let input_utxos = vec![ None, Some(TxOutput::Transfer( - OutputValue::Coin((supply_change_fee * 2).unwrap()), + OutputValue::Coin(supply_change_fee), Destination::AnyoneCanSpend, )), ]; @@ -1164,7 +1164,7 @@ fn calculate_token_fee_change_authority(#[case] seed: Seed) { .map_into_block_fees(&chain_config, block_height) .unwrap(); - assert_eq!(accumulated_fee, Fee(supply_change_fee)); + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); } // Check that data deposit fee is not accounted in accumulated fee and is burned rather then goes to staker. @@ -1194,7 +1194,7 @@ fn calculate_data_deposit_fee(#[case] seed: Seed) { 0, ))]; let input_utxos = vec![Some(TxOutput::Transfer( - OutputValue::Coin((data_deposit_fee * 2).unwrap()), + OutputValue::Coin(data_deposit_fee), Destination::AnyoneCanSpend, ))]; @@ -1220,7 +1220,7 @@ fn calculate_data_deposit_fee(#[case] seed: Seed) { .map_into_block_fees(&chain_config, block_height) .unwrap(); - assert_eq!(accumulated_fee, Fee(data_deposit_fee)); + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); } // Check that nft issuance fee is not accounted in accumulated fee and is burned rather then goes to staker. @@ -1250,7 +1250,7 @@ fn calculate_nft_issuance_fee(#[case] seed: Seed) { 0, ))]; let input_utxos = vec![Some(TxOutput::Transfer( - OutputValue::Coin((nft_issuance_fee * 2).unwrap()), + OutputValue::Coin(nft_issuance_fee), Destination::AnyoneCanSpend, ))]; @@ -1281,5 +1281,83 @@ fn calculate_nft_issuance_fee(#[case] seed: Seed) { .map_into_block_fees(&chain_config, block_height) .unwrap(); - assert_eq!(accumulated_fee, Fee(nft_issuance_fee)); + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); +} + +// Check that token metadata change fee is not accounted in accumulated fee and is burned rather then goes to staker. +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn calculate_token_fee_change_metadata_uri(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let chain_config = common::chain::config::Builder::new(ChainType::Mainnet) + .consensus_upgrades(NetUpgrades::regtest_with_pos()) + .build(); + let block_height = BlockHeight::new(1); + let change_metadata_uri_fee = chain_config.token_change_metadata_uri_fee(); + + let pos_store = InMemoryPoSAccounting::new(); + let pos_db = PoSAccountingDB::new(&pos_store); + + let orders_store = InMemoryOrdersAccounting::new(); + let orders_db = OrdersAccountingDB::new(&orders_store); + + let token_data = tokens_accounting::TokenData::FungibleToken( + TokenIssuance::V1(test_utils::nft_utils::random_token_issuance_v1( + &chain_config, + Destination::AnyoneCanSpend, + &mut rng, + )) + .into(), + ); + let token_id = TokenId::random_using(&mut rng); + let tokens_store = tokens_accounting::InMemoryTokensAccounting::from_values( + BTreeMap::from_iter([(token_id, token_data)]), + BTreeMap::new(), + ); + let tokens_db = tokens_accounting::TokensAccountingDB::new(&tokens_store); + + let inputs = vec![ + TxInput::AccountCommand( + AccountNonce::new(0), + AccountCommand::ChangeTokenMetadataUri(token_id, Vec::new()), + ), + TxInput::Utxo(UtxoOutPoint::new( + OutPointSourceId::BlockReward(Id::new(H256::random_using(&mut rng))), + 0, + )), + ]; + let input_utxos = vec![ + None, + Some(TxOutput::Transfer( + OutputValue::Coin(change_metadata_uri_fee), + Destination::AnyoneCanSpend, + )), + ]; + + let outputs = + vec![TxOutput::Transfer(OutputValue::Coin(Amount::ZERO), Destination::AnyoneCanSpend)]; + + let inputs_accumulator = ConstrainedValueAccumulator::from_inputs( + &chain_config, + block_height, + &orders_db, + &pos_db, + &tokens_db, + &inputs, + &input_utxos, + ) + .unwrap(); + + let outputs_accumulator = + ConstrainedValueAccumulator::from_outputs(&chain_config, block_height, &outputs).unwrap(); + + let accumulated_fee = inputs_accumulator + .satisfy_with(outputs_accumulator) + .unwrap() + .map_into_block_fees(&chain_config, block_height) + .unwrap(); + + assert_eq!(accumulated_fee, Fee(Amount::ZERO)); } diff --git a/chainstate/src/detail/ban_score.rs b/chainstate/src/detail/ban_score.rs index 78b2b9a97..f807bf3a4 100644 --- a/chainstate/src/detail/ban_score.rs +++ b/chainstate/src/detail/ban_score.rs @@ -335,6 +335,7 @@ impl BanScore for TokensError { TokensError::TokensInBlockReward => 100, TokensError::InvariantBrokenUndoIssuanceOnNonexistentToken(_) => 100, TokensError::InvariantBrokenRegisterIssuanceWithDuplicateId(_) => 100, + TokensError::TokenMetadataUriTooLarge(_) => 100, } } } @@ -364,6 +365,7 @@ impl BanScore for CheckTransactionError { CheckTransactionError::HtlcsAreNotActivated => 100, CheckTransactionError::OrdersAreNotActivated(_) => 100, CheckTransactionError::OrdersCurrenciesMustBeDifferent(_) => 100, + CheckTransactionError::ChangeTokenMetadataUriNotActivated => 100, } } } @@ -629,6 +631,8 @@ impl BanScore for tokens_accounting::Error { tokens_accounting::Error::CannotLockFrozenToken(_) => 100, tokens_accounting::Error::CannotChangeAuthorityForFrozenToken(_) => 100, tokens_accounting::Error::CannotUndoChangeAuthorityForFrozenToken(_) => 100, + tokens_accounting::Error::CannotChangeMetadataUriForFrozenToken(_) => 100, + tokens_accounting::Error::CannotUndoChangeMetadataUriForFrozenToken(_) => 100, tokens_accounting::Error::InvariantErrorNonZeroSupplyForNonExistingToken => 100, tokens_accounting::Error::ViewFail => 0, tokens_accounting::Error::StorageWrite => 0, diff --git a/chainstate/src/detail/error_classification.rs b/chainstate/src/detail/error_classification.rs index 8a49e117b..a51c04026 100644 --- a/chainstate/src/detail/error_classification.rs +++ b/chainstate/src/detail/error_classification.rs @@ -471,6 +471,7 @@ impl BlockProcessingErrorClassification for TokensError { | TokensError::TransferZeroTokens(_, _) | TokensError::TokenIdCantBeCalculated | TokensError::TokensInBlockReward + | TokensError::TokenMetadataUriTooLarge(_) | TokensError::InvariantBrokenUndoIssuanceOnNonexistentToken(_) | TokensError::InvariantBrokenRegisterIssuanceWithDuplicateId(_) => { BlockProcessingErrorClass::BadBlock @@ -742,6 +743,8 @@ impl BlockProcessingErrorClassification for tokens_accounting::Error { | Error::CannotUndoUnfreezeTokenThatIsFrozen(_) | Error::CannotChangeAuthorityForFrozenToken(_) | Error::CannotUndoChangeAuthorityForFrozenToken(_) + | Error::CannotChangeMetadataUriForFrozenToken(_) + | Error::CannotUndoChangeMetadataUriForFrozenToken(_) | Error::InvariantErrorNonZeroSupplyForNonExistingToken => { BlockProcessingErrorClass::BadBlock } @@ -797,6 +800,7 @@ impl BlockProcessingErrorClassification for CheckTransactionError { | CheckTransactionError::DeprecatedTokenOperationVersion(_, _) | CheckTransactionError::HtlcsAreNotActivated | CheckTransactionError::OrdersAreNotActivated(_) + | CheckTransactionError::ChangeTokenMetadataUriNotActivated | CheckTransactionError::OrdersCurrenciesMustBeDifferent(_) => { BlockProcessingErrorClass::BadBlock } diff --git a/chainstate/src/rpc/types/account.rs b/chainstate/src/rpc/types/account.rs index 021db073b..3af70a4bd 100644 --- a/chainstate/src/rpc/types/account.rs +++ b/chainstate/src/rpc/types/account.rs @@ -21,6 +21,7 @@ use common::{ }, primitives::amount::RpcAmountOut, }; +use rpc::types::RpcHexString; use super::output::RpcOutputValue; @@ -74,6 +75,10 @@ pub enum RpcAccountCommand { token_id: RpcAddress, new_authority: RpcAddress, }, + ChangeTokenMetadataUri { + token_id: RpcAddress, + new_metadata_uri: RpcHexString, + }, ConcludeOrder { order_id: RpcAddress, }, @@ -113,6 +118,12 @@ impl RpcAccountCommand { new_authority: RpcAddress::new(chain_config, destination.clone())?, } } + AccountCommand::ChangeTokenMetadataUri(id, metadata_uri) => { + RpcAccountCommand::ChangeTokenMetadataUri { + token_id: RpcAddress::new(chain_config, *id)?, + new_metadata_uri: RpcHexString::from_bytes(metadata_uri.clone()), + } + } AccountCommand::ConcludeOrder(id) => RpcAccountCommand::ConcludeOrder { order_id: RpcAddress::new(chain_config, *id)?, }, diff --git a/chainstate/test-framework/src/random_tx_maker.rs b/chainstate/test-framework/src/random_tx_maker.rs index e1ebc10be..86ffdc119 100644 --- a/chainstate/test-framework/src/random_tx_maker.rs +++ b/chainstate/test-framework/src/random_tx_maker.rs @@ -53,7 +53,7 @@ use pos_accounting::{ PoolData, }; use randomness::{seq::IteratorRandom, CryptoRng, Rng, SliceRandom}; -use test_utils::nft_utils::*; +use test_utils::{nft_utils::*, random_ascii_alphanumeric_string}; use tokens_accounting::{ InMemoryTokensAccounting, TokensAccountingCache, TokensAccountingDB, TokensAccountingDeltaData, TokensAccountingOperations, TokensAccountingView, @@ -651,6 +651,28 @@ impl<'a> RandomTxMaker<'a> { Destination::AnyoneCanSpend, ); + (vec![account_input, fee_input], vec![fee_change_output]) + } else if rng.gen_bool(0.1) { + // Change token metadata uri + let new_nonce = self.get_next_nonce(AccountType::Token(token_id)); + let max_len = self.chainstate.get_chain_config().token_max_uri_len(); + let new_metadata_uri = + random_ascii_alphanumeric_string(rng, 1..=max_len).as_bytes().to_vec(); + let account_input = TxInput::AccountCommand( + new_nonce, + AccountCommand::ChangeTokenMetadataUri(token_id, new_metadata_uri.clone()), + ); + + let _ = tokens_cache.change_metadata_uri(token_id, new_metadata_uri).unwrap(); + self.account_command_used = true; + self.fee_input = None; + + let required_fee = self.chainstate.get_chain_config().token_change_metadata_uri_fee(); + let fee_change_output = TxOutput::Transfer( + OutputValue::Coin((fee_available_amount - required_fee).unwrap()), + Destination::AnyoneCanSpend, + ); + (vec![account_input, fee_input], vec![fee_change_output]) } else if !token_data.is_locked() { if rng.gen_bool(0.9) { diff --git a/chainstate/test-framework/src/signature_destination_getter.rs b/chainstate/test-framework/src/signature_destination_getter.rs index 4b244b16f..7f62d6a7b 100644 --- a/chainstate/test-framework/src/signature_destination_getter.rs +++ b/chainstate/test-framework/src/signature_destination_getter.rs @@ -150,6 +150,7 @@ impl<'a> SignatureDestinationGetter<'a> { | AccountCommand::LockTokenSupply(token_id) | AccountCommand::FreezeToken(token_id, _) | AccountCommand::UnfreezeToken(token_id) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) | AccountCommand::ChangeTokenAuthority(token_id, _) => { let token_data = tokens_view .get_token_data(token_id) diff --git a/chainstate/test-suite/src/tests/chainstate_storage_tests.rs b/chainstate/test-suite/src/tests/chainstate_storage_tests.rs index d21aa8847..dade3a9c6 100644 --- a/chainstate/test-suite/src/tests/chainstate_storage_tests.rs +++ b/chainstate/test-suite/src/tests/chainstate_storage_tests.rs @@ -24,9 +24,9 @@ use common::{ chain::{ output_value::OutputValue, tokens::{make_token_id, NftIssuance, TokenAuxiliaryData, TokenIssuanceV0}, - ChainstateUpgrade, DataDepositFeeVersion, Destination, HtlcActivated, NetUpgrades, - OrdersActivated, OutPointSourceId, RewardDistributionVersion, TokenIssuanceVersion, - TokensFeeVersion, Transaction, TxInput, TxOutput, UtxoOutPoint, + ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, + HtlcActivated, NetUpgrades, OrdersActivated, OutPointSourceId, RewardDistributionVersion, + TokenIssuanceVersion, TokensFeeVersion, Transaction, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, Id, Idable}, }; @@ -120,6 +120,7 @@ fn store_fungible_token_v0(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -201,6 +202,7 @@ fn store_nft_v0(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -513,6 +515,7 @@ fn store_aux_data_from_issue_nft(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/fungible_tokens.rs b/chainstate/test-suite/src/tests/fungible_tokens.rs index 6bc0327d2..954daf0e0 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens.rs @@ -28,8 +28,9 @@ use common::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{make_token_id, TokenData, TokenId}, - ChainstateUpgrade, DataDepositFeeVersion, Destination, HtlcActivated, OrdersActivated, - OutPointSourceId, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, + HtlcActivated, OrdersActivated, OutPointSourceId, TokenIssuanceVersion, TokensFeeVersion, + TxInput, TxOutput, }, primitives::{Amount, Idable}, }; @@ -57,6 +58,7 @@ fn make_test_framework_with_v0(rng: &mut (impl Rng + CryptoRng)) -> TestFramewor RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -963,6 +965,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -1028,6 +1031,7 @@ fn no_v0_transfer_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -1039,6 +1043,7 @@ fn no_v0_transfer_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs index 8c3cfc04c..0df410c8f 100644 --- a/chainstate/test-suite/src/tests/fungible_tokens_v1.rs +++ b/chainstate/test-suite/src/tests/fungible_tokens_v1.rs @@ -5437,3 +5437,585 @@ fn issue_same_token_alternative_pos_chain(#[case] seed: Seed) { assert_eq!(Id::::from(alt_block_c_id), tf.best_block_id()); }); } + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn check_change_metadata_uri(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let (token_id, _, utxo_with_change) = issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::No, + ); + + // too large metadata + let max_len = tf.chain_config().token_max_uri_len(); + { + let too_large_metadata_uri = + random_ascii_alphanumeric_string(&mut rng, (max_len + 1)..(max_len * 100)) + .as_bytes() + .to_vec(); + let result = tf + .make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(0), + AccountCommand::ChangeTokenMetadataUri( + token_id, + too_large_metadata_uri, + ), + ), + InputWitness::NoSignature(None), + ) + .add_input( + utxo_with_change.clone().into(), + InputWitness::NoSignature(None), + ) + .build(), + ) + .build_and_process(&mut rng); + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::TokensError( + TokensError::TokenMetadataUriTooLarge(token_id) + ) + ) + ) + )) + ); + } + + let new_metadata_uri = + random_ascii_alphanumeric_string(&mut rng, 1..=max_len).as_bytes().to_vec(); + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(0), + AccountCommand::ChangeTokenMetadataUri( + token_id, + new_metadata_uri.clone(), + ), + ), + InputWitness::NoSignature(None), + ) + .add_input( + utxo_with_change.clone().into(), + InputWitness::NoSignature(None), + ) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + + // Check result + let actual_token_data = TokensAccountingStorageRead::get_token_data( + &tf.storage.transaction_ro().unwrap(), + &token_id, + ) + .unwrap(); + let tokens_accounting::TokenData::FungibleToken(actual_token_data) = + actual_token_data.unwrap(); + assert_eq!(actual_token_data.metadata_uri(), &new_metadata_uri); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn check_change_metadata_for_frozen_token(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + + let unfreeze_fee = tf.chain_config().token_freeze_fee(BlockHeight::zero()); + let change_metadata_fee = tf.chain_config().token_change_metadata_uri_fee(); + + let (token_id, _, utxo_with_change) = issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Unlimited, + IsTokenFreezable::Yes, + ); + + // Freeze the token + let freeze_token_tx = TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(0), + AccountCommand::FreezeToken(token_id, IsTokenUnfreezable::Yes), + ), + InputWitness::NoSignature(None), + ) + .add_input( + utxo_with_change.clone().into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin((change_metadata_fee + unfreeze_fee).unwrap()), + Destination::AnyoneCanSpend, + )) + .build(); + let freeze_token_tx_id = freeze_token_tx.transaction().get_id(); + tf.make_block_builder() + .add_transaction(freeze_token_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Try change metadata when the token is frozen + let new_metadata_uri = + random_ascii_alphanumeric_string(&mut rng, 1..1024).as_bytes().to_vec(); + + let result = tf + .make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(1), + AccountCommand::ChangeTokenMetadataUri( + token_id, + new_metadata_uri.clone(), + ), + ), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::from_utxo(freeze_token_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + .build(), + ) + .build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::TokensAccountingError( + tokens_accounting::Error::CannotChangeMetadataUriForFrozenToken(token_id) + ) + )) + ); + + // Unfreeze token + let unfreeze_token_tx = TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(1), + AccountCommand::UnfreezeToken(token_id), + ), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::from_utxo(freeze_token_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin(change_metadata_fee), + Destination::AnyoneCanSpend, + )) + .build(); + let unfreeze_token_tx_id = unfreeze_token_tx.transaction().get_id(); + tf.make_block_builder() + .add_transaction(unfreeze_token_tx) + .build_and_process(&mut rng) + .unwrap(); + + // Change metadata after unfreeze + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(2), + AccountCommand::ChangeTokenMetadataUri( + token_id, + new_metadata_uri.clone(), + ), + ), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::from_utxo(unfreeze_token_tx_id.into(), 0), + InputWitness::NoSignature(None), + ) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + + // Check result + let actual_token_data = TokensAccountingStorageRead::get_token_data( + &tf.storage.transaction_ro().unwrap(), + &token_id, + ) + .unwrap(); + let tokens_accounting::TokenData::FungibleToken(actual_token_data) = + actual_token_data.unwrap(); + assert_eq!(actual_token_data.metadata_uri(), &new_metadata_uri); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn reorg_metadata_uri_change(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + let genesis_block_id = tf.best_block_id(); + + // Create block `a` with token issuance + let token_issuance = + make_issuance(&mut rng, TokenTotalSupply::Unlimited, IsTokenFreezable::No); + let (token_id, block_a_id, block_a_change_utxo) = issue_token_from_block( + &mut rng, + &mut tf, + genesis_block_id, + UtxoOutPoint::new(genesis_block_id.into(), 0), + token_issuance.clone(), + ); + assert_eq!(tf.best_block_id(), block_a_id); + + // Create block `b` with token minting + let amount_to_mint = Amount::from_atoms(rng.gen_range(2..100_000_000)); + let (block_b_id, mint_tokens_tx_id) = mint_tokens_in_block( + &mut rng, + &mut tf, + block_a_id.into(), + block_a_change_utxo, + token_id, + amount_to_mint, + true, + ); + assert_eq!(tf.best_block_id(), block_b_id); + + let original_token_data = TokensAccountingStorageRead::get_token_data( + &tf.storage.transaction_ro().unwrap(), + &token_id, + ) + .unwrap(); + let tokens_accounting::TokenData::FungibleToken(original_token_data) = + original_token_data.unwrap(); + let original_metadata_uri = original_token_data.metadata_uri().to_owned(); + + // Create block `c` which changes token metadata uri + let new_metadata_uri = + random_ascii_alphanumeric_string(&mut rng, 1..1024).as_bytes().to_vec(); + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(1), + AccountCommand::ChangeTokenMetadataUri( + token_id, + new_metadata_uri.clone(), + ), + ), + InputWitness::NoSignature(None), + ) + .add_input( + TxInput::from_utxo(mint_tokens_tx_id.into(), 1), + InputWitness::NoSignature(None), + ) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + + // Check the storage + let tokens_accounting::TokenData::FungibleToken(actual_new_token_data) = tf + .storage + .transaction_ro() + .unwrap() + .read_tokens_accounting_data() + .unwrap() + .token_data + .get(&token_id) + .cloned() + .unwrap(); + let actual_new_metadata_uri = actual_new_token_data.metadata_uri(); + assert_eq!(actual_new_metadata_uri, new_metadata_uri); + + // Add blocks from genesis to trigger the reorg + let block_e_id = tf.create_chain((&block_b_id).into(), 2, &mut rng).unwrap(); + assert_eq!(tf.best_block_id(), block_e_id); + + // Check the storage + let tokens_accounting::TokenData::FungibleToken(actual_token_data) = tf + .storage + .transaction_ro() + .unwrap() + .read_tokens_accounting_data() + .unwrap() + .token_data + .get(&token_id) + .cloned() + .unwrap(); + let actual_metadata_uri = actual_token_data.metadata_uri(); + assert_eq!(actual_metadata_uri, original_metadata_uri); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn test_change_metadata_uri_activation(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = test_utils::random::make_seedable_rng(seed); + // activate feature at height 3 + let mut tf = TestFramework::builder(&mut rng) + .with_chain_config( + common::chain::config::Builder::test_chain() + .chainstate_upgrades( + common::chain::NetUpgrades::initialize(vec![ + ( + BlockHeight::zero(), + common::chain::ChainstateUpgrade::new( + common::chain::TokenIssuanceVersion::V1, + common::chain::RewardDistributionVersion::V1, + common::chain::TokensFeeVersion::V1, + common::chain::DataDepositFeeVersion::V1, + common::chain::ChangeTokenMetadataUriActivated::No, + common::chain::HtlcActivated::Yes, + common::chain::OrdersActivated::Yes, + ), + ), + ( + BlockHeight::new(3), + common::chain::ChainstateUpgrade::new( + common::chain::TokenIssuanceVersion::V1, + common::chain::RewardDistributionVersion::V1, + common::chain::TokensFeeVersion::V1, + common::chain::DataDepositFeeVersion::V1, + common::chain::ChangeTokenMetadataUriActivated::Yes, + common::chain::HtlcActivated::Yes, + common::chain::OrdersActivated::Yes, + ), + ), + ]) + .unwrap(), + ) + .genesis_unittest(Destination::AnyoneCanSpend) + .build(), + ) + .build(); + + let (token_id, _, utxo_with_change) = issue_token_from_genesis( + &mut rng, + &mut tf, + TokenTotalSupply::Lockable, + IsTokenFreezable::No, + ); + + let new_metadata_uri = + random_ascii_alphanumeric_string(&mut rng, 1..1024).as_bytes().to_vec(); + + // Try to change metadata before activation, check an error + let result = tf + .make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(0), + AccountCommand::ChangeTokenMetadataUri( + token_id, + new_metadata_uri.clone(), + ), + ), + InputWitness::NoSignature(None), + ) + .add_input( + utxo_with_change.clone().into(), + InputWitness::NoSignature(None), + ) + .build(), + ) + .build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::CheckBlockFailed( + chainstate::CheckBlockError::CheckTransactionFailed( + chainstate::CheckBlockTransactionsError::CheckTransactionError( + tx_verifier::CheckTransactionError::ChangeTokenMetadataUriNotActivated + ) + ) + )) + ); + + // produce an empty block + tf.make_block_builder().build_and_process(&mut rng).unwrap(); + + // now it should be possible to use htlc output + tf.make_block_builder() + .add_transaction( + TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(0), + AccountCommand::ChangeTokenMetadataUri( + token_id, + new_metadata_uri.clone(), + ), + ), + InputWitness::NoSignature(None), + ) + .add_input( + utxo_with_change.clone().into(), + InputWitness::NoSignature(None), + ) + .build(), + ) + .build_and_process(&mut rng) + .unwrap(); + }); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn only_authority_can_change_metadata_uri(#[case] seed: Seed) { + utils::concurrency::model(move || { + let mut rng = make_seedable_rng(seed); + let mut tf = TestFramework::builder(&mut rng).build(); + let genesis_block_id = tf.genesis().get_id(); + + let (original_sk, original_pk) = + PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + + let issuance = TokenIssuance::V1(TokenIssuanceV1 { + token_ticker: random_ascii_alphanumeric_string(&mut rng, 1..5).as_bytes().to_vec(), + number_of_decimals: rng.gen_range(1..18), + metadata_uri: random_ascii_alphanumeric_string(&mut rng, 1..1024).as_bytes().to_vec(), + total_supply: TokenTotalSupply::Lockable, + authority: Destination::PublicKey(original_pk.clone()), + is_freezable: IsTokenFreezable::No, + }); + + let (token_id, _, utxo_with_change) = issue_token_from_block( + &mut rng, + &mut tf, + genesis_block_id.into(), + UtxoOutPoint::new(genesis_block_id.into(), 0), + issuance, + ); + + let new_metadata_uri = + random_ascii_alphanumeric_string(&mut rng, 1..1024).as_bytes().to_vec(); + + // Try to change metadata without a signature + let tx_1_no_signatures = TransactionBuilder::new() + .add_input( + TxInput::from_command( + AccountNonce::new(0), + AccountCommand::ChangeTokenMetadataUri(token_id, new_metadata_uri), + ), + InputWitness::NoSignature(None), + ) + .add_input( + utxo_with_change.clone().into(), + InputWitness::NoSignature(None), + ) + .add_output(TxOutput::Transfer( + OutputValue::Coin( + tf.chain_config().token_change_authority_fee(BlockHeight::zero()), + ), + Destination::AnyoneCanSpend, + )) + .build(); + + let result = tf + .make_block_builder() + .add_transaction(tx_1_no_signatures.clone()) + .build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::InputCheck(InputCheckError::new( + 0, + ScriptError::Signature(DestinationSigError::SignatureNotFound) + )) + )) + ); + + let inputs_utxos = vec![ + None, + tf.chainstate.utxo(&utxo_with_change).unwrap().map(|utxo| utxo.output().clone()), + ]; + let inputs_utxos_refs = inputs_utxos.iter().map(|utxo| utxo.as_ref()).collect::>(); + + // Try to change metadata with wrong signature + let tx = { + let tx = tx_1_no_signatures.transaction().clone(); + + let (some_sk, some_pk) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let account_sig = StandardInputSignature::produce_uniparty_signature_for_input( + &some_sk, + Default::default(), + Destination::PublicKey(some_pk), + &tx, + &inputs_utxos_refs, + 0, + &mut rng, + ) + .unwrap(); + + SignedTransaction::new( + tx, + vec![InputWitness::Standard(account_sig), InputWitness::NoSignature(None)], + ) + .unwrap() + }; + + let result = tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng); + + assert_eq!( + result.unwrap_err(), + ChainstateError::ProcessBlockError(BlockError::StateUpdateFailed( + ConnectTransactionError::InputCheck(InputCheckError::new( + 0, + ScriptError::Signature(DestinationSigError::SignatureVerificationFailed) + )) + )) + ); + + // Change metadata with proper keys + let tx = { + let tx = tx_1_no_signatures.transaction().clone(); + + let account_sig = StandardInputSignature::produce_uniparty_signature_for_input( + &original_sk, + Default::default(), + Destination::PublicKey(original_pk.clone()), + &tx, + &inputs_utxos_refs, + 0, + &mut rng, + ) + .unwrap(); + + SignedTransaction::new( + tx, + vec![InputWitness::Standard(account_sig), InputWitness::NoSignature(None)], + ) + .unwrap() + }; + + tf.make_block_builder().add_transaction(tx).build_and_process(&mut rng).unwrap(); + }); +} diff --git a/chainstate/test-suite/src/tests/htlc.rs b/chainstate/test-suite/src/tests/htlc.rs index b9aa9f442..a011039b5 100644 --- a/chainstate/test-suite/src/tests/htlc.rs +++ b/chainstate/test-suite/src/tests/htlc.rs @@ -38,9 +38,10 @@ use common::{ signed_transaction::SignedTransaction, timelock::OutputTimeLock, tokens::{make_token_id, TokenData, TokenIssuance, TokenTransfer}, - AccountCommand, AccountNonce, ChainConfig, ChainstateUpgrade, DataDepositFeeVersion, - Destination, HtlcActivated, OrdersActivated, RewardDistributionVersion, - TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + AccountCommand, AccountNonce, ChainConfig, ChainstateUpgrade, + ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, HtlcActivated, + OrdersActivated, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + TxInput, TxOutput, }, primitives::{Amount, Idable}, }; @@ -588,6 +589,7 @@ fn fork_activation(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::No, OrdersActivated::No, ), @@ -599,6 +601,7 @@ fn fork_activation(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::No, ), @@ -689,6 +692,7 @@ fn spend_tokens(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -700,6 +704,7 @@ fn spend_tokens(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/nft_burn.rs b/chainstate/test-suite/src/tests/nft_burn.rs index 21fed775c..23ed642e9 100644 --- a/chainstate/test-suite/src/tests/nft_burn.rs +++ b/chainstate/test-suite/src/tests/nft_burn.rs @@ -17,8 +17,9 @@ use chainstate::{BlockError, ChainstateError, ConnectTransactionError}; use chainstate_test_framework::{TestFramework, TransactionBuilder}; use common::chain::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::make_token_id, - ChainstateUpgrade, DataDepositFeeVersion, Destination, HtlcActivated, OrdersActivated, - RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, + ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, + HtlcActivated, OrdersActivated, RewardDistributionVersion, TokenIssuanceVersion, + TokensFeeVersion, TxInput, TxOutput, }; use common::chain::{OutPointSourceId, UtxoOutPoint}; use common::primitives::{Amount, BlockHeight, CoinOrTokenId, Idable}; @@ -216,6 +217,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/nft_issuance.rs b/chainstate/test-suite/src/tests/nft_issuance.rs index 6410447a2..b3ee6af3c 100644 --- a/chainstate/test-suite/src/tests/nft_issuance.rs +++ b/chainstate/test-suite/src/tests/nft_issuance.rs @@ -22,9 +22,9 @@ use common::chain::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{is_rfc3986_valid_symbol, make_token_id, Metadata, NftIssuance, NftIssuanceV0}, - Block, ChainstateUpgrade, DataDepositFeeVersion, Destination, HtlcActivated, OrdersActivated, - OutPointSourceId, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, TxInput, - TxOutput, + Block, ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, + HtlcActivated, OrdersActivated, OutPointSourceId, RewardDistributionVersion, + TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, }; use common::primitives::{BlockHeight, Idable}; use randomness::{CryptoRng, Rng}; @@ -1653,6 +1653,7 @@ fn no_v0_issuance_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -1718,6 +1719,7 @@ fn only_ascii_alphanumeric_after_v1(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/nft_transfer.rs b/chainstate/test-suite/src/tests/nft_transfer.rs index 645897612..66f6eb112 100644 --- a/chainstate/test-suite/src/tests/nft_transfer.rs +++ b/chainstate/test-suite/src/tests/nft_transfer.rs @@ -21,9 +21,9 @@ use common::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{make_token_id, NftIssuance, TokenId}, - ChainstateUpgrade, DataDepositFeeVersion, Destination, HtlcActivated, NetUpgrades, - OrdersActivated, OutPointSourceId, RewardDistributionVersion, TokenIssuanceVersion, - TokensFeeVersion, TxInput, TxOutput, + ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, Destination, + HtlcActivated, NetUpgrades, OrdersActivated, OutPointSourceId, RewardDistributionVersion, + TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, }, primitives::{Amount, BlockHeight, CoinOrTokenId}, }; @@ -371,6 +371,7 @@ fn ensure_nft_cannot_be_printed_from_tokens_op(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/orders_tests.rs b/chainstate/test-suite/src/tests/orders_tests.rs index 72ba1c983..ab73f3fda 100644 --- a/chainstate/test-suite/src/tests/orders_tests.rs +++ b/chainstate/test-suite/src/tests/orders_tests.rs @@ -1506,6 +1506,7 @@ fn test_activation(#[case] seed: Seed) { common::chain::RewardDistributionVersion::V1, common::chain::TokensFeeVersion::V1, common::chain::DataDepositFeeVersion::V1, + common::chain::ChangeTokenMetadataUriActivated::Yes, common::chain::HtlcActivated::No, common::chain::OrdersActivated::No, ), @@ -1517,6 +1518,7 @@ fn test_activation(#[case] seed: Seed) { common::chain::RewardDistributionVersion::V1, common::chain::TokensFeeVersion::V1, common::chain::DataDepositFeeVersion::V1, + common::chain::ChangeTokenMetadataUriActivated::Yes, common::chain::HtlcActivated::No, common::chain::OrdersActivated::Yes, ), diff --git a/chainstate/test-suite/src/tests/tx_fee.rs b/chainstate/test-suite/src/tests/tx_fee.rs index 94537502e..5b42e2a3e 100644 --- a/chainstate/test-suite/src/tests/tx_fee.rs +++ b/chainstate/test-suite/src/tests/tx_fee.rs @@ -33,9 +33,9 @@ use common::{ make_token_id, IsTokenFreezable, TokenIssuance, TokenIssuanceV0, TokenIssuanceV1, TokenTotalSupply, }, - ChainConfig, ChainstateUpgrade, DataDepositFeeVersion, Destination, HtlcActivated, - NetUpgrades, OrdersActivated, TokenIssuanceVersion, TokensFeeVersion, TxInput, TxOutput, - UtxoOutPoint, + ChainConfig, ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, + Destination, HtlcActivated, NetUpgrades, OrdersActivated, TokenIssuanceVersion, + TokensFeeVersion, TxInput, TxOutput, UtxoOutPoint, }, primitives::{Amount, Fee, Idable}, }; @@ -578,6 +578,7 @@ fn issue_fungible_token_v0(#[case] seed: Seed) { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs b/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs index f7e6b9b31..15ebab22d 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/check_transaction.rs @@ -21,8 +21,8 @@ use common::{ output_value::OutputValue, signature::inputsig::InputWitness, tokens::{get_tokens_issuance_count, NftIssuance}, - ChainConfig, HtlcActivated, SignedTransaction, TokenIssuanceVersion, Transaction, - TransactionSize, TxOutput, + AccountCommand, ChainConfig, ChangeTokenMetadataUriActivated, HtlcActivated, + SignedTransaction, TokenIssuanceVersion, Transaction, TransactionSize, TxInput, TxOutput, }, primitives::{BlockHeight, CoinOrTokenId, Id, Idable}, }; @@ -62,6 +62,8 @@ pub enum CheckTransactionError { OrdersAreNotActivated(Id), #[error("Orders currencies from tx {0} are the same")] OrdersCurrenciesMustBeDifferent(Id), + #[error("Change token metadata uri not activated yet")] + ChangeTokenMetadataUriNotActivated, } pub fn check_transaction( @@ -199,7 +201,44 @@ fn check_tokens_tx( ),) ); - // Check tokens + let change_token_metadata_uri_activated = chain_config + .chainstate_upgrades() + .version_at_height(block_height) + .1 + .change_token_metadata_uri_activated(); + + // Check token metadata uri change + tx.inputs().iter().try_for_each(|input| match input { + TxInput::Utxo(_) | TxInput::Account(_) => Ok(()), + TxInput::AccountCommand(_, command) => match command { + AccountCommand::MintTokens(_, _) + | AccountCommand::UnmintTokens(_) + | AccountCommand::LockTokenSupply(_) + | AccountCommand::FreezeToken(_, _) + | AccountCommand::UnfreezeToken(_) + | AccountCommand::ChangeTokenAuthority(_, _) + | AccountCommand::ConcludeOrder(_) + | AccountCommand::FillOrder(_, _, _) => Ok(()), + AccountCommand::ChangeTokenMetadataUri(token_id, metadata_uri) => { + match change_token_metadata_uri_activated { + ChangeTokenMetadataUriActivated::Yes => { /* do nothing */ } + ChangeTokenMetadataUriActivated::No => { + return Err(CheckTransactionError::ChangeTokenMetadataUriNotActivated) + } + } + + ensure!( + metadata_uri.len() <= chain_config.token_max_uri_len(), + CheckTransactionError::TokensError(TokensError::TokenMetadataUriTooLarge( + *token_id + )) + ); + Ok(()) + } + }, + })?; + + // Check token issuance tx.outputs() .iter() .try_for_each(|output| match output { diff --git a/chainstate/tx-verifier/src/transaction_verifier/error.rs b/chainstate/tx-verifier/src/transaction_verifier/error.rs index 645cba5a8..fefdc08b1 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/error.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/error.rs @@ -221,6 +221,8 @@ pub enum TokensError { InvariantBrokenUndoIssuanceOnNonexistentToken(TokenId), #[error("Invariant broken - attempt register issuance on non-existent token {0}")] InvariantBrokenRegisterIssuanceWithDuplicateId(TokenId), + #[error("Token {0} metadata uri is to large")] + TokenMetadataUriTooLarge(TokenId), } #[derive(Error, Debug, PartialEq, Eq, Clone)] diff --git a/chainstate/tx-verifier/src/transaction_verifier/mod.rs b/chainstate/tx-verifier/src/transaction_verifier/mod.rs index e50d21257..9af455a4d 100644 --- a/chainstate/tx-verifier/src/transaction_verifier/mod.rs +++ b/chainstate/tx-verifier/src/transaction_verifier/mod.rs @@ -618,6 +618,16 @@ where }); Some(res) } + AccountCommand::ChangeTokenMetadataUri(token_id, new_metadata_uri) => { + let res = self + .spend_input_from_account(*nonce, account_op.clone().into()) + .and_then(|_| { + self.tokens_accounting_cache + .change_metadata_uri(*token_id, new_metadata_uri.clone()) + .map_err(ConnectTransactionError::TokensAccountingError) + }); + Some(res) + } AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => None, }, }) @@ -758,7 +768,8 @@ where | AccountCommand::LockTokenSupply(..) | AccountCommand::FreezeToken(..) | AccountCommand::UnfreezeToken(..) - | AccountCommand::ChangeTokenAuthority(..) => None, + | AccountCommand::ChangeTokenAuthority(..) + | AccountCommand::ChangeTokenMetadataUri(..) => None, AccountCommand::ConcludeOrder(order_id) => { let res = self .spend_input_from_account(*nonce, account_op.clone().into()) diff --git a/common/src/chain/config/builder.rs b/common/src/chain/config/builder.rs index f1dfc7a50..2f4b25108 100644 --- a/common/src/chain/config/builder.rs +++ b/common/src/chain/config/builder.rs @@ -28,10 +28,10 @@ use crate::{ }, pos_initial_difficulty, pow::PoWChainConfigBuilder, - ChainstateUpgrade, CoinUnit, ConsensusUpgrade, DataDepositFeeVersion, Destination, - GenBlock, Genesis, HtlcActivated, NetUpgrades, OrdersActivated, PoSChainConfig, - PoSConsensusVersion, PoWChainConfig, RewardDistributionVersion, TokenIssuanceVersion, - TokensFeeVersion, + ChainstateUpgrade, ChangeTokenMetadataUriActivated, CoinUnit, ConsensusUpgrade, + DataDepositFeeVersion, Destination, GenBlock, Genesis, HtlcActivated, NetUpgrades, + OrdersActivated, PoSChainConfig, PoSConsensusVersion, PoWChainConfig, + RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, }, primitives::{ id::WithId, per_thousand::PerThousand, semver::SemVer, Amount, BlockCount, BlockDistance, @@ -171,6 +171,7 @@ impl ChainType { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V0, + ChangeTokenMetadataUriActivated::No, HtlcActivated::No, OrdersActivated::No, ), @@ -182,6 +183,7 @@ impl ChainType { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -197,6 +199,7 @@ impl ChainType { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), @@ -212,6 +215,7 @@ impl ChainType { RewardDistributionVersion::V0, TokensFeeVersion::V0, DataDepositFeeVersion::V0, + ChangeTokenMetadataUriActivated::No, HtlcActivated::No, OrdersActivated::No, ), @@ -223,6 +227,7 @@ impl ChainType { RewardDistributionVersion::V0, TokensFeeVersion::V0, DataDepositFeeVersion::V0, + ChangeTokenMetadataUriActivated::No, HtlcActivated::No, OrdersActivated::No, ), @@ -234,6 +239,7 @@ impl ChainType { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V0, + ChangeTokenMetadataUriActivated::No, HtlcActivated::No, OrdersActivated::No, ), @@ -245,6 +251,7 @@ impl ChainType { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::No, ), @@ -256,6 +263,7 @@ impl ChainType { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/common/src/chain/config/mod.rs b/common/src/chain/config/mod.rs index 7f840c18b..c5591873a 100644 --- a/common/src/chain/config/mod.rs +++ b/common/src/chain/config/mod.rs @@ -52,8 +52,9 @@ use self::emission_schedule::DEFAULT_INITIAL_MINT; use super::output_value::OutputValue; use super::{stakelock::StakePoolData, RequiredConsensus}; use super::{ - ChainstateUpgrade, ConsensusUpgrade, DataDepositFeeVersion, HtlcActivated, OrdersActivated, - RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + ChainstateUpgrade, ChangeTokenMetadataUriActivated, ConsensusUpgrade, DataDepositFeeVersion, + HtlcActivated, OrdersActivated, RewardDistributionVersion, TokenIssuanceVersion, + TokensFeeVersion, }; const DEFAULT_MAX_FUTURE_BLOCK_TIME_OFFSET_V1: Duration = Duration::from_secs(120); @@ -627,6 +628,11 @@ impl ChainConfig { } } + /// The fee for changing token metadata uri + pub fn token_change_metadata_uri_fee(&self) -> Amount { + TOKEN_CHANGE_METADATA_URI_FEE + } + /// The maximum length of a URI contained in a token #[must_use] pub fn token_max_uri_len(&self) -> usize { @@ -740,6 +746,8 @@ const TOKEN_FREEZE_FEE_V1: Amount = CoinUnit::from_coins(50).to_amount_atoms(); const TOKEN_CHANGE_AUTHORITY_FEE_V0: Amount = CoinUnit::from_coins(100).to_amount_atoms(); const TOKEN_CHANGE_AUTHORITY_FEE_V1: Amount = CoinUnit::from_coins(20).to_amount_atoms(); +const TOKEN_CHANGE_METADATA_URI_FEE: Amount = CoinUnit::from_coins(20).to_amount_atoms(); + const DATA_DEPOSIT_MAX_SIZE_V0: usize = 128; const DATA_DEPOSIT_MAX_SIZE_V1: usize = 384; const DATA_DEPOSIT_FEE_V0: Amount = CoinUnit::from_coins(100).to_amount_atoms(); @@ -893,6 +901,7 @@ pub fn create_unit_test_config_builder() -> Builder { RewardDistributionVersion::V1, TokensFeeVersion::V1, DataDepositFeeVersion::V1, + ChangeTokenMetadataUriActivated::Yes, HtlcActivated::Yes, OrdersActivated::Yes, ), diff --git a/common/src/chain/tokens/tokens_utils.rs b/common/src/chain/tokens/tokens_utils.rs index 47ac24a83..a61b25b3e 100644 --- a/common/src/chain/tokens/tokens_utils.rs +++ b/common/src/chain/tokens/tokens_utils.rs @@ -56,6 +56,7 @@ pub fn get_token_supply_change_count(inputs: &[TxInput]) -> usize { AccountCommand::FreezeToken(_, _) | AccountCommand::UnfreezeToken(_) | AccountCommand::ChangeTokenAuthority(_, _) + | AccountCommand::ChangeTokenMetadataUri(_, _) | AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => false, AccountCommand::MintTokens(_, _) diff --git a/common/src/chain/transaction/account_outpoint.rs b/common/src/chain/transaction/account_outpoint.rs index 9ad087363..9e9b3fa2d 100644 --- a/common/src/chain/transaction/account_outpoint.rs +++ b/common/src/chain/transaction/account_outpoint.rs @@ -52,7 +52,8 @@ impl From for AccountType { | AccountCommand::LockTokenSupply(id) | AccountCommand::FreezeToken(id, _) | AccountCommand::UnfreezeToken(id) - | AccountCommand::ChangeTokenAuthority(id, _) => AccountType::Token(id), + | AccountCommand::ChangeTokenAuthority(id, _) + | AccountCommand::ChangeTokenMetadataUri(id, _) => AccountType::Token(id), AccountCommand::ConcludeOrder(id) | AccountCommand::FillOrder(id, _, _) => { AccountType::Order(id) } @@ -119,6 +120,9 @@ pub enum AccountCommand { ConcludeOrder(OrderId), #[codec(index = 7)] FillOrder(OrderId, OutputValue, Destination), + // Change token metadata uri + #[codec(index = 8)] + ChangeTokenMetadataUri(TokenId, Vec), } /// Type of OutPoint that represents spending from an account diff --git a/common/src/chain/upgrades/chainstate_upgrade.rs b/common/src/chain/upgrades/chainstate_upgrade.rs index 840996558..d1dd94d45 100644 --- a/common/src/chain/upgrades/chainstate_upgrade.rs +++ b/common/src/chain/upgrades/chainstate_upgrade.rs @@ -57,12 +57,19 @@ pub enum DataDepositFeeVersion { V1, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)] +pub enum ChangeTokenMetadataUriActivated { + Yes, + No, +} + #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct ChainstateUpgrade { token_issuance_version: TokenIssuanceVersion, reward_distribution_version: RewardDistributionVersion, tokens_fee_version: TokensFeeVersion, data_deposit_fee_version: DataDepositFeeVersion, + change_token_metadata_uri_activated: ChangeTokenMetadataUriActivated, htlc_activated: HtlcActivated, orders_activated: OrdersActivated, } @@ -73,6 +80,7 @@ impl ChainstateUpgrade { reward_distribution_version: RewardDistributionVersion, tokens_fee_version: TokensFeeVersion, data_deposit_fee_version: DataDepositFeeVersion, + change_token_metadata_uri_activated: ChangeTokenMetadataUriActivated, htlc_activated: HtlcActivated, orders_activated: OrdersActivated, ) -> Self { @@ -81,6 +89,7 @@ impl ChainstateUpgrade { reward_distribution_version, tokens_fee_version, data_deposit_fee_version, + change_token_metadata_uri_activated, htlc_activated, orders_activated, } @@ -109,6 +118,10 @@ impl ChainstateUpgrade { pub fn data_deposit_fee_version(&self) -> DataDepositFeeVersion { self.data_deposit_fee_version } + + pub fn change_token_metadata_uri_activated(&self) -> ChangeTokenMetadataUriActivated { + self.change_token_metadata_uri_activated + } } impl Activate for ChainstateUpgrade {} diff --git a/common/src/chain/upgrades/mod.rs b/common/src/chain/upgrades/mod.rs index ba4e28aa9..20c049e20 100644 --- a/common/src/chain/upgrades/mod.rs +++ b/common/src/chain/upgrades/mod.rs @@ -18,8 +18,8 @@ mod consensus_upgrade; mod netupgrade; pub use chainstate_upgrade::{ - ChainstateUpgrade, DataDepositFeeVersion, HtlcActivated, OrdersActivated, - RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, + ChainstateUpgrade, ChangeTokenMetadataUriActivated, DataDepositFeeVersion, HtlcActivated, + OrdersActivated, RewardDistributionVersion, TokenIssuanceVersion, TokensFeeVersion, }; pub use consensus_upgrade::{ConsensusUpgrade, PoSStatus, PoWStatus, RequiredConsensus}; pub use netupgrade::{Activate, NetUpgrades}; diff --git a/mempool/src/error/ban_score.rs b/mempool/src/error/ban_score.rs index a9e3e1d7a..2db5e0fa3 100644 --- a/mempool/src/error/ban_score.rs +++ b/mempool/src/error/ban_score.rs @@ -434,6 +434,7 @@ impl MempoolBanScore for tokens_accounting::Error { tokens_accounting::Error::CannotUnmintFrozenToken(_) => 0, tokens_accounting::Error::CannotLockFrozenToken(_) => 0, tokens_accounting::Error::CannotChangeAuthorityForFrozenToken(_) => 0, + tokens_accounting::Error::CannotChangeMetadataUriForFrozenToken(_) => 0, tokens_accounting::Error::InvariantErrorNonZeroSupplyForNonExistingToken => 0, tokens_accounting::Error::ViewFail => 0, tokens_accounting::Error::StorageWrite => 0, @@ -446,6 +447,7 @@ impl MempoolBanScore for tokens_accounting::Error { tokens_accounting::Error::CannotUndoFreezeTokenThatIsNotFrozen(_) => 0, tokens_accounting::Error::CannotUndoUnfreezeTokenThatIsFrozen(_) => 0, tokens_accounting::Error::CannotUndoChangeAuthorityForFrozenToken(_) => 0, + tokens_accounting::Error::CannotUndoChangeMetadataUriForFrozenToken(_) => 0, } } } @@ -487,6 +489,7 @@ impl MempoolBanScore for CheckTransactionError { CheckTransactionError::HtlcsAreNotActivated => 100, CheckTransactionError::OrdersAreNotActivated(_) => 100, CheckTransactionError::OrdersCurrenciesMustBeDifferent(_) => 100, + CheckTransactionError::ChangeTokenMetadataUriNotActivated => 100, } } } diff --git a/mempool/src/pool/entry.rs b/mempool/src/pool/entry.rs index ab69831ee..bfbfbdae3 100644 --- a/mempool/src/pool/entry.rs +++ b/mempool/src/pool/entry.rs @@ -72,6 +72,7 @@ impl TxDependency { | AccountCommand::LockTokenSupply(_) | AccountCommand::FreezeToken(_, _) | AccountCommand::UnfreezeToken(_) + | AccountCommand::ChangeTokenMetadataUri(_, _) | AccountCommand::ChangeTokenAuthority(_, _) => { Self::TokenSupplyAccount(TxAccountDependency::new(acct.clone().into(), nonce)) } diff --git a/mintscript/src/translate.rs b/mintscript/src/translate.rs index b641ec897..fc915b181 100644 --- a/mintscript/src/translate.rs +++ b/mintscript/src/translate.rs @@ -226,6 +226,7 @@ impl TranslateInput for SignedTransaction { | AccountCommand::LockTokenSupply(token_id) | AccountCommand::FreezeToken(token_id, _) | AccountCommand::UnfreezeToken(token_id) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) | AccountCommand::ChangeTokenAuthority(token_id, _) => { let dest = ctx .get_tokens_authority(token_id)? @@ -335,6 +336,7 @@ impl TranslateInput for TimelockOnly { | AccountCommand::LockTokenSupply(_token_id) | AccountCommand::FreezeToken(_token_id, _) | AccountCommand::UnfreezeToken(_token_id) + | AccountCommand::ChangeTokenMetadataUri(_token_id, _) | AccountCommand::ChangeTokenAuthority(_token_id, _) => Ok(WitnessScript::TRUE), AccountCommand::ConcludeOrder(_) | AccountCommand::FillOrder(_, _, _) => { Ok(WitnessScript::TRUE) @@ -430,6 +432,7 @@ impl TranslateInput for SignatureOnlyTx { | AccountCommand::LockTokenSupply(token_id) | AccountCommand::FreezeToken(token_id, _) | AccountCommand::UnfreezeToken(token_id) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) | AccountCommand::ChangeTokenAuthority(token_id, _) => { let dest = ctx .get_tokens_authority(token_id)? diff --git a/test/functional/test_framework/wallet_cli_controller.py b/test/functional/test_framework/wallet_cli_controller.py index 477c42c75..c5677aa06 100644 --- a/test/functional/test_framework/wallet_cli_controller.py +++ b/test/functional/test_framework/wallet_cli_controller.py @@ -357,6 +357,9 @@ async def unfreeze_token(self, token_id: str) -> str: async def change_token_authority(self, token_id: str, new_authority: str) -> str: return await self._write_command(f"token-change-authority {token_id} {new_authority}\n") + async def change_token_metadata_uri(self, token_id: str, new_metadata_uri: str) -> str: + return await self._write_command(f"token-change-metadata-uri {token_id} {new_metadata_uri}\n") + async def issue_new_nft(self, destination_address: str, media_hash: str, diff --git a/test/functional/test_framework/wallet_rpc_controller.py b/test/functional/test_framework/wallet_rpc_controller.py index f349c992e..92a7a5627 100644 --- a/test/functional/test_framework/wallet_rpc_controller.py +++ b/test/functional/test_framework/wallet_rpc_controller.py @@ -318,6 +318,9 @@ async def unfreeze_token(self, token_id: str) -> str: async def change_token_authority(self, token_id: str, new_authority: str) -> str: return self._write_command("token_change_authority", [self.account, token_id, new_authority, {'in_top_x_mb': 5}])['result'] + async def change_token_metadata_uri(self, token_id: str, new_metadata_uri: str) -> str: + return self._write_command("token_change_metadata_uri", [self.account, token_id, new_metadata_uri, {'in_top_x_mb': 5}])['result'] + 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 babb7f78f..cff0280aa 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -150,6 +150,8 @@ class UnicodeOnWindowsError(ValueError): 'wallet_tokens_transfer_from_multisig_addr.py', 'wallet_tokens_transfer_from_multisig_addr_rpc.py', 'wallet_tokens_change_authority.py', + 'wallet_tokens_change_metadata_uri.py', + 'wallet_tokens_change_metadata_uri_rpc.py', 'wallet_tokens_change_supply.py', 'wallet_nfts.py', 'wallet_decommission_genesis.py', diff --git a/test/functional/wallet_tokens_change_metadata_uri.py b/test/functional/wallet_tokens_change_metadata_uri.py new file mode 100644 index 000000000..6b382d3b0 --- /dev/null +++ b/test/functional/wallet_tokens_change_metadata_uri.py @@ -0,0 +1,140 @@ +#!/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 metadata uri 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 +* check metadata uri +* change metadata uri +* check that metadata uri is changed +""" + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.mintlayer import (make_tx, reward_input, ATOMS_PER_COIN) +from test_framework.util import assert_in, assert_equal +from test_framework.mintlayer import block_input_data_obj +from test_framework.wallet_cli_controller import WalletCliController + +import asyncio +import sys +import random +import string + +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, [], [], "FillSpaceFromMempool") + 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()) + + # issue a valid token + address = await wallet.new_address() + metadata_uri = "http://uri" + token_id, err = await wallet.issue_new_token("XXX", 2, metadata_uri, address, token_supply='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()) + + token_info = node.chainstate_token_info(token_id) + assert_equal(metadata_uri, token_info['content']['metadata_uri']['text']); + + new_metadata_uri = bytes([random.randint(0, 255) for _ in range(random.randint(1, 128))]).hex() + assert_in("The transaction was submitted successfully", await wallet.change_token_metadata_uri(token_id, new_metadata_uri)) + + self.generate_block() + assert_in("Success", await wallet.sync()) + + token_info = node.chainstate_token_info(token_id) + print(token_info) + assert_equal(new_metadata_uri, token_info['content']['metadata_uri']['hex']); + assert token_info['content']['metadata_uri']['text'] is not metadata_uri + +if __name__ == '__main__': + WalletTokens().main() diff --git a/test/functional/wallet_tokens_change_metadata_uri_rpc.py b/test/functional/wallet_tokens_change_metadata_uri_rpc.py new file mode 100644 index 000000000..5b50bc681 --- /dev/null +++ b/test/functional/wallet_tokens_change_metadata_uri_rpc.py @@ -0,0 +1,34 @@ +#!/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 metadta uri test""" + +from wallet_tokens_change_metadata_uri import WalletTokens +from test_framework.wallet_rpc_controller import WalletRpcController + + +class WalletTokensRpc(WalletTokens): + def set_test_params(self): + super().set_test_params() + self.wallet_controller = WalletRpcController + + def run_test(self): + super().run_test() + + +if __name__ == '__main__': + WalletTokensRpc().main() diff --git a/tokens-accounting/src/cache.rs b/tokens-accounting/src/cache.rs index 4dad02ba4..c49faa11d 100644 --- a/tokens-accounting/src/cache.rs +++ b/tokens-accounting/src/cache.rs @@ -27,8 +27,9 @@ use crate::{ data::{TokenData, TokensAccountingDeltaData}, error::Error, operations::{ - ChangeTokenAuthorityUndo, FreezeTokenUndo, IssueTokenUndo, LockSupplyUndo, MintTokenUndo, - TokenAccountingUndo, TokensAccountingOperations, UnfreezeTokenUndo, UnmintTokenUndo, + ChangeTokenAuthorityUndo, ChangeTokenMetadataUriUndo, FreezeTokenUndo, IssueTokenUndo, + LockSupplyUndo, MintTokenUndo, TokenAccountingUndo, TokensAccountingOperations, + UnfreezeTokenUndo, UnmintTokenUndo, }, view::TokensAccountingView, FlushableTokensAccountingView, @@ -308,6 +309,35 @@ impl TokensAccountingOperations for TokensAccountingCac )) } + fn change_metadata_uri( + &mut self, + id: TokenId, + new_metadata_uri: Vec, + ) -> Result { + log::debug!("Changing token metadata uri: {:?}", id); + let token_data = self.get_token_data(&id)?.ok_or(Error::TokenDataNotFound(id))?; + + let undo_data = match token_data { + TokenData::FungibleToken(data) => { + if data.is_frozen() { + return Err(Error::CannotChangeMetadataUriForFrozenToken(id)); + } + + let new_data = data.clone().change_metadata_uri(new_metadata_uri); + self.data.token_data.merge_delta_data_element( + id, + accounting::DataDelta::new( + Some(TokenData::FungibleToken(data)), + Some(TokenData::FungibleToken(new_data)), + ), + )? + } + }; + Ok(TokenAccountingUndo::ChangeTokenMetadataUri( + ChangeTokenMetadataUriUndo { id, undo_data }, + )) + } + fn undo(&mut self, undo_data: TokenAccountingUndo) -> Result<(), Error> { log::debug!("Undo in tokens: {:?}", undo_data); match undo_data { @@ -408,6 +438,20 @@ impl TokensAccountingOperations for TokensAccountingCac self.data.token_data.undo_merge_delta_data_element(undo.id, undo.undo_data)?; Ok(()) } + TokenAccountingUndo::ChangeTokenMetadataUri(undo) => { + let data = self + .get_token_data(&undo.id)? + .ok_or(Error::TokenDataNotFoundOnReversal(undo.id))?; + match data { + TokenData::FungibleToken(data) => { + if data.is_frozen() { + return Err(Error::CannotUndoChangeMetadataUriForFrozenToken(undo.id)); + } + } + }; + self.data.token_data.undo_merge_delta_data_element(undo.id, undo.undo_data)?; + Ok(()) + } } } } diff --git a/tokens-accounting/src/data.rs b/tokens-accounting/src/data.rs index 020929c29..21b6494b6 100644 --- a/tokens-accounting/src/data.rs +++ b/tokens-accounting/src/data.rs @@ -241,6 +241,18 @@ impl FungibleTokenData { new_authority, ) } + + pub fn change_metadata_uri(self, new_metadata_uri: Vec) -> Self { + Self::new_unchecked( + self.token_ticker, + self.number_of_decimals, + new_metadata_uri, + self.total_supply, + self.locked, + self.frozen, + self.authority, + ) + } } impl From for FungibleTokenData { diff --git a/tokens-accounting/src/error.rs b/tokens-accounting/src/error.rs index f447ef0ac..e4613dc8a 100644 --- a/tokens-accounting/src/error.rs +++ b/tokens-accounting/src/error.rs @@ -71,6 +71,10 @@ pub enum Error { CannotUndoChangeAuthorityForFrozenToken(TokenId), #[error("Non-zero circulating supply of non-existing token")] InvariantErrorNonZeroSupplyForNonExistingToken, + #[error("Cannot change metadata uri for frozen token '{0}`")] + CannotChangeMetadataUriForFrozenToken(TokenId), + #[error("Cannot undo change metadata uri for frozen token '{0}`")] + CannotUndoChangeMetadataUriForFrozenToken(TokenId), // TODO Need a more granular error reporting in the following // https://github.com/mintlayer/mintlayer-core/issues/811 diff --git a/tokens-accounting/src/operations.rs b/tokens-accounting/src/operations.rs index 1d4e03025..2ab727441 100644 --- a/tokens-accounting/src/operations.rs +++ b/tokens-accounting/src/operations.rs @@ -69,6 +69,12 @@ pub struct ChangeTokenAuthorityUndo { pub(crate) undo_data: DataDeltaUndo, } +#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode)] +pub struct ChangeTokenMetadataUriUndo { + pub(crate) id: TokenId, + pub(crate) undo_data: DataDeltaUndo, +} + #[must_use] #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, VariantCount)] pub enum TokenAccountingUndo { @@ -86,6 +92,8 @@ pub enum TokenAccountingUndo { UnfreezeToken(UnfreezeTokenUndo), #[codec(index = 6)] ChangeTokenAuthority(ChangeTokenAuthorityUndo), + #[codec(index = 7)] + ChangeTokenMetadataUri(ChangeTokenMetadataUriUndo), } pub fn random_undo_for_test(rng: &mut impl Rng) -> TokenAccountingUndo { @@ -118,5 +126,11 @@ pub trait TokensAccountingOperations { new_authority: Destination, ) -> Result; + fn change_metadata_uri( + &mut self, + id: TokenId, + metadata_uri: Vec, + ) -> Result; + fn undo(&mut self, undo_data: TokenAccountingUndo) -> Result<()>; } diff --git a/tokens-accounting/src/tests/operations_tests.rs b/tokens-accounting/src/tests/operations_tests.rs index efc7badee..09b8ec9b8 100644 --- a/tokens-accounting/src/tests/operations_tests.rs +++ b/tokens-accounting/src/tests/operations_tests.rs @@ -934,7 +934,7 @@ fn change_authority_twice(#[case] seed: Seed) { #[rstest] #[trace] #[case(Seed::from_entropy())] -fn try_change_authority_for_freezed_token(#[case] seed: Seed) { +fn try_change_authority_for_frozen_token(#[case] seed: Seed) { let mut rng = make_seedable_rng(seed); let token_issuance = @@ -959,7 +959,7 @@ fn try_change_authority_for_freezed_token(#[case] seed: Seed) { let new_token_data = TokenData::FungibleToken(token_data.change_authority(new_authority.clone())); - // Try change authority while token is freezed + // Try change authority while token is frozen let res = cache.change_authority(token_id, new_authority.clone()); assert_eq!( @@ -982,3 +982,134 @@ fn try_change_authority_for_freezed_token(#[case] seed: Seed) { ); assert_eq!(storage, expected_storage); } + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn change_metadata_flush_undo(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let token_id = make_token_id(&mut rng); + let token_data_1 = make_token_data(&mut rng, TokenTotalSupply::Unlimited, false); + let new_metadata = random_ascii_alphanumeric_string(&mut rng, 1..1024).as_bytes().to_vec(); + let TokenData::FungibleToken(token_data_2) = token_data_1.clone(); + let token_data_2 = + TokenData::FungibleToken(token_data_2.change_metadata_uri(new_metadata.clone())); + + let mut storage = InMemoryTokensAccounting::from_values( + BTreeMap::from_iter([(token_id, token_data_1.clone())]), + BTreeMap::new(), + ); + let original_storage = storage.clone(); + + // Change metadata + let mut db = TokensAccountingDB::new(&mut storage); + let mut cache = TokensAccountingCache::new(&mut db); + let undo = cache.change_metadata_uri(token_id, new_metadata).unwrap(); + + let consumed_data = cache.consume(); + db.batch_write_tokens_data(consumed_data).unwrap(); + + let expected_storage = InMemoryTokensAccounting::from_values( + BTreeMap::from_iter([(token_id, token_data_2.clone())]), + BTreeMap::new(), + ); + assert_eq!(storage, expected_storage); + + // undo + let mut db = TokensAccountingDB::new(&mut storage); + let mut cache = TokensAccountingCache::new(&mut db); + cache.undo(undo).unwrap(); + + let consumed_data = cache.consume(); + db.batch_write_tokens_data(consumed_data).unwrap(); + + assert_eq!(storage, original_storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn change_metadata_twice(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let token_id = make_token_id(&mut rng); + let token_data_1 = make_token_data(&mut rng, TokenTotalSupply::Unlimited, false); + + let new_metadata_1 = random_ascii_alphanumeric_string(&mut rng, 1..1024).as_bytes().to_vec(); + + let new_metadata_2 = random_ascii_alphanumeric_string(&mut rng, 1..1024).as_bytes().to_vec(); + let TokenData::FungibleToken(token_data_3) = token_data_1.clone(); + let token_data_3 = + TokenData::FungibleToken(token_data_3.change_metadata_uri(new_metadata_2.clone())); + + let mut storage = InMemoryTokensAccounting::from_values( + BTreeMap::from_iter([(token_id, token_data_1.clone())]), + BTreeMap::new(), + ); + let mut db = TokensAccountingDB::new(&mut storage); + + // Change metadata + let mut cache = TokensAccountingCache::new(&mut db); + let _ = cache.change_metadata_uri(token_id, new_metadata_1).unwrap(); + let _ = cache.change_metadata_uri(token_id, new_metadata_2).unwrap(); + + let consumed_data = cache.consume(); + db.batch_write_tokens_data(consumed_data).unwrap(); + + let expected_storage = InMemoryTokensAccounting::from_values( + BTreeMap::from_iter([(token_id, token_data_3.clone())]), + BTreeMap::new(), + ); + assert_eq!(storage, expected_storage); +} + +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn try_change_metadata_for_frozen_token(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let token_issuance = + make_token_issuance(&mut rng, TokenTotalSupply::Lockable, IsTokenFreezable::Yes); + let token_data = TokenData::FungibleToken(token_issuance.into()); + let token_id = make_token_id(&mut rng); + + let mut storage = InMemoryTokensAccounting::from_values( + BTreeMap::from_iter([(token_id, token_data.clone())]), + BTreeMap::new(), + ); + let mut db = TokensAccountingDB::new(&mut storage); + let mut cache = TokensAccountingCache::new(&mut db); + + // Freeze the token + let _ = cache.freeze_token(token_id, IsTokenUnfreezable::Yes).unwrap(); + + let new_metadata = random_ascii_alphanumeric_string(&mut rng, 1..1024).as_bytes().to_vec(); + let TokenData::FungibleToken(token_data) = token_data.clone(); + let new_token_data = + TokenData::FungibleToken(token_data.change_metadata_uri(new_metadata.clone())); + + // Try change authority while token is frozen + let res = cache.change_metadata_uri(token_id, new_metadata.clone()); + + assert_eq!( + res.unwrap_err(), + crate::Error::CannotChangeMetadataUriForFrozenToken(token_id) + ); + + // Unfreeze token + let _ = cache.unfreeze_token(token_id).unwrap(); + + // Change metadata again + let _ = cache.change_metadata_uri(token_id, new_metadata).unwrap(); + + let consumed_data = cache.consume(); + db.batch_write_tokens_data(consumed_data).unwrap(); + + let expected_storage = InMemoryTokensAccounting::from_values( + BTreeMap::from_iter([(token_id, new_token_data)]), + BTreeMap::new(), + ); + assert_eq!(storage, expected_storage); +} diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index ece1f1f8f..5370225b3 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -1190,6 +1190,31 @@ impl Account { ) } + pub fn change_token_metadata_uri( + &mut self, + db_tx: &mut impl WalletStorageWriteUnlocked, + token_info: &UnconfirmedTokenInfo, + metadata_uri: Vec, + median_time: BlockTimestamp, + fee_rate: CurrentFeeRate, + ) -> WalletResult { + let nonce = token_info.get_next_nonce()?; + let tx_input = TxInput::AccountCommand( + nonce, + AccountCommand::ChangeTokenMetadataUri(*token_info.token_id(), metadata_uri), + ); + let authority = token_info.authority()?.clone(); + + self.change_token_supply_transaction( + authority, + tx_input, + vec![], + db_tx, + median_time, + fee_rate, + ) + } + fn change_token_supply_transaction( &mut self, authority: Destination, @@ -1289,6 +1314,7 @@ impl Account { | AccountCommand::UnmintTokens(token_id) | AccountCommand::LockTokenSupply(token_id) | AccountCommand::ChangeTokenAuthority(token_id, _) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) | AccountCommand::FreezeToken(token_id, _) | AccountCommand::UnfreezeToken(token_id) => self .output_cache @@ -1782,7 +1808,10 @@ impl Account { | AccountCommand::UnmintTokens(token_id) | AccountCommand::LockTokenSupply(token_id) | AccountCommand::FreezeToken(token_id, _) - | AccountCommand::UnfreezeToken(token_id) => self.find_token(token_id).is_ok(), + | AccountCommand::UnfreezeToken(token_id) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) => { + self.find_token(token_id).is_ok() + } AccountCommand::ChangeTokenAuthority(token_id, address) => { self.find_token(token_id).is_ok() || self.is_destination_mine_or_watched(address) @@ -2210,6 +2239,14 @@ fn group_preselected_inputs( .ok_or(WalletError::OutputAmountOverflow)?, )?; } + AccountCommand::ChangeTokenMetadataUri(token_id, _) => { + update_preselected_inputs( + currency_grouper::Currency::Token(*token_id), + Amount::ZERO, + (*fee + chain_config.token_change_metadata_uri_fee()) + .ok_or(WalletError::OutputAmountOverflow)?, + )?; + } // TODO(orders) AccountCommand::ConcludeOrder(_) => unimplemented!(), AccountCommand::FillOrder(_, _, _) => unimplemented!(), diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index 929abc7cc..23c2ff283 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -668,6 +668,7 @@ impl OutputCache { AccountCommand::MintTokens(_, _) | AccountCommand::UnmintTokens(_) | AccountCommand::LockTokenSupply(_) + | AccountCommand::ChangeTokenMetadataUri(_, _) | AccountCommand::ChangeTokenAuthority(_, _) | AccountCommand::UnfreezeToken(_) | AccountCommand::ConcludeOrder(_) @@ -730,6 +731,7 @@ impl OutputCache { | AccountCommand::MintTokens(token_id, _) | AccountCommand::FreezeToken(token_id, _) | AccountCommand::UnfreezeToken(token_id) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) | AccountCommand::ChangeTokenAuthority(token_id, _) | AccountCommand::UnmintTokens(token_id) => frozen_token_id == token_id, // TODO(orders) @@ -886,7 +888,8 @@ impl OutputCache { | AccountCommand::UnmintTokens(token_id) | AccountCommand::LockTokenSupply(token_id) | AccountCommand::FreezeToken(token_id, _) - | AccountCommand::UnfreezeToken(token_id) => { + | AccountCommand::UnfreezeToken(token_id) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) => { if let Some(data) = self.token_issuance.get_mut(token_id) { if !already_present { Self::update_token_issuance_state( @@ -1022,6 +1025,7 @@ impl OutputCache { | AccountCommand::LockTokenSupply(token_id) | AccountCommand::FreezeToken(token_id, _) | AccountCommand::UnfreezeToken(token_id) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) | AccountCommand::ChangeTokenAuthority(token_id, _) => { if let Some(data) = self.token_issuance.get_mut(token_id) { data.last_nonce = nonce.decrement(); @@ -1313,6 +1317,7 @@ impl OutputCache { | AccountCommand::LockTokenSupply(token_id) | AccountCommand::FreezeToken(token_id, _) | AccountCommand::UnfreezeToken(token_id) + | AccountCommand::ChangeTokenMetadataUri(token_id, _) | AccountCommand::ChangeTokenAuthority(token_id, _) => { if let Some(data) = self.token_issuance.get_mut(token_id) @@ -1533,6 +1538,7 @@ fn apply_freeze_mutations_from_tx( AccountCommand::MintTokens(_, _) | AccountCommand::UnmintTokens(_) | AccountCommand::LockTokenSupply(_) + | AccountCommand::ChangeTokenMetadataUri(_, _) | AccountCommand::ChangeTokenAuthority(_, _) => {} // TODO(orders) AccountCommand::ConcludeOrder(_) => unimplemented!(), @@ -1575,6 +1581,7 @@ fn apply_total_supply_mutations_from_tx( } AccountCommand::FreezeToken(_, _) | AccountCommand::UnfreezeToken(_) + | AccountCommand::ChangeTokenMetadataUri(_, _) | AccountCommand::ChangeTokenAuthority(_, _) => {} // TODO(orders) AccountCommand::ConcludeOrder(_) => unimplemented!(), diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 0532885e4..07baec26d 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -1577,6 +1577,29 @@ impl Wallet { }) } + pub fn change_token_metadata_uri( + &mut self, + account_index: U31, + token_info: &UnconfirmedTokenInfo, + metadata_uri: Vec, + current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + ) -> WalletResult { + let latest_median_time = self.latest_median_time; + self.for_account_rw_unlocked_and_check_tx(account_index, |account, db_tx| { + account.change_token_metadata_uri( + db_tx, + token_info, + metadata_uri, + latest_median_time, + CurrentFeeRate { + current_fee_rate, + consolidate_fee_rate, + }, + ) + }) + } + pub fn find_used_tokens( &self, account_index: U31, diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 3764d3ff7..4fc562dec 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -1107,6 +1107,23 @@ where Ok(Self::new_tx_submitted_command(new_tx)) } + WalletCommand::ChangeTokenMetadataUri { + token_id, + metadata_uri, + } => { + let (wallet, selected_account) = wallet_and_selected_acc(&mut self.wallet).await?; + let new_tx = wallet + .change_token_metadata_uri( + selected_account, + token_id, + metadata_uri, + self.config, + ) + .await?; + + Ok(Self::new_tx_submitted_command(new_tx)) + } + WalletCommand::Rescan => { self.non_empty_wallet().await?.rescan().await?; Ok(ConsoleCommand::Print( diff --git a/wallet/wallet-cli-commands/src/lib.rs b/wallet/wallet-cli-commands/src/lib.rs index 1a53eb34a..5075497ea 100644 --- a/wallet/wallet-cli-commands/src/lib.rs +++ b/wallet/wallet-cli-commands/src/lib.rs @@ -376,6 +376,12 @@ pub enum WalletCommand { #[clap(name = "token-change-authority")] ChangeTokenAuthority { token_id: String, address: String }, + #[clap(name = "token-change-metadata-uri")] + ChangeTokenMetadataUri { + token_id: String, + metadata_uri: String, + }, + #[clap(name = "token-mint")] MintTokens { /// The token id of the tokens to be minted diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index 1d77b5492..18649cc56 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -462,6 +462,30 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { .await } + pub async fn change_token_metadata_uri( + &mut self, + token_info: RPCTokenInfo, + metadata_uri: Vec, + ) -> Result> { + self.create_and_send_token_tx( + &token_info, + move |current_fee_rate: FeeRate, + consolidate_fee_rate: FeeRate, + wallet: &mut DefaultWallet, + account_index: U31, + token_info: &UnconfirmedTokenInfo| { + wallet.change_token_metadata_uri( + account_index, + token_info, + metadata_uri, + current_fee_rate, + consolidate_fee_rate, + ) + }, + ) + .await + } + pub async fn deposit_data( &mut self, data: Vec, diff --git a/wallet/wallet-rpc-client/src/handles_client/mod.rs b/wallet/wallet-rpc-client/src/handles_client/mod.rs index e38bc7b34..ce9db948b 100644 --- a/wallet/wallet-rpc-client/src/handles_client/mod.rs +++ b/wallet/wallet-rpc-client/src/handles_client/mod.rs @@ -881,6 +881,24 @@ impl WalletInterface .map_err(WalletRpcHandlesClientError::WalletRpcError) } + async fn change_token_metadata_uri( + &self, + account_index: U31, + token_id: String, + metadata_uri: String, + config: ControllerConfig, + ) -> Result { + self.wallet_rpc + .change_token_metadata_uri( + account_index, + token_id.into(), + RpcHexString::from_str(&metadata_uri)?, + config, + ) + .await + .map_err(WalletRpcHandlesClientError::WalletRpcError) + } + async fn mint_tokens( &self, account_index: U31, diff --git a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs index 08447e6b5..2a4b1a2f0 100644 --- a/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs +++ b/wallet/wallet-rpc-client/src/rpc_client/client_impl.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeMap, future::pending, num::NonZeroUsize, path::PathBuf}; +use std::{collections::BTreeMap, future::pending, num::NonZeroUsize, path::PathBuf, str::FromStr}; use crate::wallet_rpc_traits::{PartialOrSignedTx, SignRawTransactionResult, WalletInterface}; @@ -29,6 +29,7 @@ use common::{ }; use crypto::key::{hdkd::u31::U31, PrivateKey}; use p2p_types::{bannable_address::BannableAddress, socket_address::SocketAddress, PeerId}; +use rpc::types::RpcHexString; use serialization::hex_encoded::HexEncoded; use serialization::DecodeAll; use utils_networking::IpOrSocketAddress; @@ -739,6 +740,25 @@ impl WalletInterface for ClientWalletRpc { .map_err(WalletRpcError::ResponseError) } + async fn change_token_metadata_uri( + &self, + account_index: U31, + token_id: String, + metadata_uri: String, + config: ControllerConfig, + ) -> Result { + let options = TransactionOptions::from_controller_config(&config); + WalletRpcClient::change_token_metadata_uri( + &self.http_client, + account_index.into(), + token_id.into(), + RpcHexString::from_str(&metadata_uri)?, + options, + ) + .await + .map_err(WalletRpcError::ResponseError) + } + async fn mint_tokens( &self, account_index: U31, diff --git a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs index 483e7c4fa..9d45ceed0 100644 --- a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs +++ b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs @@ -391,6 +391,14 @@ pub trait WalletInterface { config: ControllerConfig, ) -> Result; + async fn change_token_metadata_uri( + &self, + account_index: U31, + token_id: String, + metadata_uri: String, + config: ControllerConfig, + ) -> Result; + async fn mint_tokens( &self, account_index: U31, diff --git a/wallet/wallet-rpc-daemon/docs/RPC.md b/wallet/wallet-rpc-daemon/docs/RPC.md index 047a5b6d3..ad77a706a 100644 --- a/wallet/wallet-rpc-daemon/docs/RPC.md +++ b/wallet/wallet-rpc-daemon/docs/RPC.md @@ -955,6 +955,28 @@ Returns: { "tx_id": hex string } ``` +### Method `token_change_metadata_uri` + +Change the metadata URI of a token + + +Parameters: +``` +{ + "account": number, + "token_id": bech32 string, + "metadata_uri": hex string, + "options": { "in_top_x_mb": EITHER OF + 1) number + 2) null }, +} +``` + +Returns: +``` +{ "tx_id": hex string } +``` + ### Method `token_mint` Given a token that is already issued, mint new tokens and increase the total supply diff --git a/wallet/wallet-rpc-lib/src/rpc/interface.rs b/wallet/wallet-rpc-lib/src/rpc/interface.rs index 92335f591..6e75c812f 100644 --- a/wallet/wallet-rpc-lib/src/rpc/interface.rs +++ b/wallet/wallet-rpc-lib/src/rpc/interface.rs @@ -559,6 +559,16 @@ trait WalletRpc { options: TransactionOptions, ) -> rpc::RpcResult; + /// Change the metadata URI of a token + #[method(name = "token_change_metadata_uri")] + async fn change_token_metadata_uri( + &self, + account: AccountArg, + token_id: RpcAddress, + metadata_uri: RpcHexString, + options: TransactionOptions, + ) -> rpc::RpcResult; + /// Given a token that is already issued, mint new tokens and increase the total supply #[method(name = "token_mint")] async fn mint_tokens( diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index bcd594517..584688abd 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -1782,6 +1782,32 @@ impl WalletRpc { .await? } + pub async fn change_token_metadata_uri( + &self, + account_index: U31, + token_id: RpcAddress, + metadata_uri: RpcHexString, + config: ControllerConfig, + ) -> WRpcResult { + let token_id = token_id + .decode_object(&self.chain_config) + .map_err(|_| RpcError::InvalidTokenId)?; + self.wallet + .call_async(move |w| { + Box::pin(async move { + let token_info = w.get_token_info(token_id).await?; + + w.synced_controller(account_index, config) + .await? + .change_token_metadata_uri(token_info, metadata_uri.into_bytes()) + .await + .map_err(RpcError::Controller) + .map(NewTransaction::new) + }) + }) + .await? + } + pub async fn rescan(&self) -> WRpcResult<(), N> { self.wallet .call_async(move |controller| { diff --git a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs index cc2fee3f9..2ae0a75b9 100644 --- a/wallet/wallet-rpc-lib/src/rpc/server_impl.rs +++ b/wallet/wallet-rpc-lib/src/rpc/server_impl.rs @@ -835,6 +835,29 @@ impl WalletRpcServer f ) } + async fn change_token_metadata_uri( + &self, + account_arg: AccountArg, + token_id: RpcAddress, + metadata_uri: RpcHexString, + options: TransactionOptions, + ) -> rpc::RpcResult { + let config = ControllerConfig { + in_top_x_mb: options.in_top_x_mb(), + broadcast_to_mempool: true, + }; + + rpc::handle_result( + self.change_token_metadata_uri( + account_arg.index::()?, + token_id, + metadata_uri, + config, + ) + .await, + ) + } + async fn mint_tokens( &self, account_arg: AccountArg,