diff --git a/contracts/feature-tests/composability/forwarder/tests/forwarder_blackbox_test.rs b/contracts/feature-tests/composability/forwarder/tests/forwarder_blackbox_test.rs new file mode 100644 index 0000000000..de13d2c375 --- /dev/null +++ b/contracts/feature-tests/composability/forwarder/tests/forwarder_blackbox_test.rs @@ -0,0 +1,136 @@ +use forwarder::nft::{Color, ProxyTrait as _}; + +use multiversx_sc_scenario::{ + api::StaticApi, + scenario_model::{ + Account, CheckAccount, CheckStateStep, ScCallStep, SetStateStep, TransferStep, + }, + ContractInfo, ScenarioWorld, +}; + +const USER_ADDRESS_EXPR: &str = "address:user"; +const FORWARDER_ADDRESS_EXPR: &str = "sc:forwarder"; +const FORWARDER_PATH_EXPR: &str = "file:output/forwarder.wasm"; + +const NFT_TOKEN_ID_EXPR: &str = "str:COOL-123456"; +const NFT_TOKEN_ID: &[u8] = b"COOL-123456"; + +type ForwarderContract = ContractInfo>; + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.set_current_dir_from_workspace("contracts/composability/forwarder"); + + blockchain.register_contract(FORWARDER_PATH_EXPR, forwarder::ContractBuilder); + blockchain +} + +struct ForwarderTestState { + world: ScenarioWorld, + forwarder_contract: ForwarderContract, +} + +impl ForwarderTestState { + fn new() -> Self { + let mut world = world(); + + let forwarder_code = world.code_expression(FORWARDER_PATH_EXPR); + let roles = vec![ + "ESDTRoleNFTCreate".to_string(), + "ESDTRoleNFTUpdateAttributes".to_string(), + ]; + + world.set_state_step( + SetStateStep::new() + .put_account(USER_ADDRESS_EXPR, Account::new().nonce(1)) + .put_account( + FORWARDER_ADDRESS_EXPR, + Account::new() + .nonce(1) + .code(forwarder_code.clone()) + .esdt_roles(NFT_TOKEN_ID_EXPR, roles), + ), + ); + + let forwarder_contract = ForwarderContract::new(FORWARDER_ADDRESS_EXPR); + + Self { + world, + forwarder_contract, + } + } +} + +#[test] +fn test_nft_update_attributes_and_send() { + let mut state = ForwarderTestState::new(); + + let original_attributes = Color { r: 0, g: 0, b: 0 }; + + state.world.sc_call( + ScCallStep::new().from(USER_ADDRESS_EXPR).call( + state + .forwarder_contract + .nft_create_compact(NFT_TOKEN_ID, 1u64, original_attributes), + ), + ); + + state.world.transfer_step( + TransferStep::new() + .from(FORWARDER_ADDRESS_EXPR) + .to(USER_ADDRESS_EXPR) + .esdt_transfer(NFT_TOKEN_ID, 1, "1"), + ); + + state + .world + .check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + 1, + "1", + Some(original_attributes), + ), + )); + + let new_attributes = Color { + r: 255, + g: 255, + b: 255, + }; + + state.world.transfer_step( + TransferStep::new() + .from(USER_ADDRESS_EXPR) + .to(FORWARDER_ADDRESS_EXPR) + .esdt_transfer(NFT_TOKEN_ID, 1, "1"), + ); + + state.world.sc_call( + ScCallStep::new().from(USER_ADDRESS_EXPR).call( + state + .forwarder_contract + .nft_update_attributes(NFT_TOKEN_ID, 1u64, new_attributes), + ), + ); + + state.world.transfer_step( + TransferStep::new() + .from(FORWARDER_ADDRESS_EXPR) + .to(USER_ADDRESS_EXPR) + .esdt_transfer(NFT_TOKEN_ID, 1, "1"), + ); + + state + .world + .check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + 1, + "1", + Some(new_attributes), + ), + )); +} diff --git a/contracts/feature-tests/composability/forwarder/tests/forwarder_legacy_whitebox_test.rs b/contracts/feature-tests/composability/forwarder/tests/forwarder_legacy_whitebox_test.rs deleted file mode 100644 index cfa0952d69..0000000000 --- a/contracts/feature-tests/composability/forwarder/tests/forwarder_legacy_whitebox_test.rs +++ /dev/null @@ -1,85 +0,0 @@ -#![allow(deprecated)] // TODO: migrate tests - -use forwarder::nft::{Color, ForwarderNftModule}; -use multiversx_sc::{contract_base::ContractBase, types::EsdtLocalRole}; -use multiversx_sc_scenario::{ - managed_address, managed_biguint, managed_token_id, rust_biguint, - testing_framework::BlockchainStateWrapper, -}; - -static NFT_TOKEN_ID: &[u8] = b"COOL-123456"; - -#[test] -fn nft_update_attributes_and_send_test() { - let mut b_mock = BlockchainStateWrapper::new(); - let rust_zero = rust_biguint!(0); - - let user = b_mock.create_user_account(&rust_zero); - let forw_wrapper = - b_mock.create_sc_account(&rust_zero, None, forwarder::contract_obj, "forwarder path"); - - b_mock.set_esdt_local_roles( - forw_wrapper.address_ref(), - NFT_TOKEN_ID, - &[EsdtLocalRole::NftCreate, EsdtLocalRole::NftUpdateAttributes], - ); - - let original_attributes = Color { r: 0, g: 0, b: 0 }; - b_mock - .execute_tx(&user, &forw_wrapper, &rust_zero, |sc| { - sc.nft_create_compact( - managed_token_id!(NFT_TOKEN_ID), - managed_biguint!(1), - original_attributes, - ); - - sc.send().direct_esdt( - &managed_address!(&user), - &managed_token_id!(NFT_TOKEN_ID), - 1, - &managed_biguint!(1), - ); - }) - .assert_ok(); - - b_mock.check_nft_balance( - &user, - NFT_TOKEN_ID, - 1, - &rust_biguint!(1), - Some(&original_attributes), - ); - - let new_attributes = Color { - r: 255, - g: 255, - b: 255, - }; - b_mock - .execute_esdt_transfer( - &user, - &forw_wrapper, - NFT_TOKEN_ID, - 1, - &rust_biguint!(1), - |sc| { - sc.nft_update_attributes(managed_token_id!(NFT_TOKEN_ID), 1, new_attributes); - - sc.send().direct_esdt( - &managed_address!(&user), - &managed_token_id!(NFT_TOKEN_ID), - 1, - &managed_biguint!(1), - ); - }, - ) - .assert_ok(); - - b_mock.check_nft_balance( - &user, - NFT_TOKEN_ID, - 1, - &rust_biguint!(1), - Some(&new_attributes), - ); -} diff --git a/contracts/feature-tests/composability/forwarder/tests/forwarder_whitebox_test.rs b/contracts/feature-tests/composability/forwarder/tests/forwarder_whitebox_test.rs new file mode 100644 index 0000000000..efd969a716 --- /dev/null +++ b/contracts/feature-tests/composability/forwarder/tests/forwarder_whitebox_test.rs @@ -0,0 +1,117 @@ +use forwarder::nft::{Color, ForwarderNftModule}; +use multiversx_sc::{contract_base::ContractBase, types::Address}; +use multiversx_sc_scenario::{ + managed_address, managed_biguint, managed_token_id, rust_biguint, + scenario_model::{ + Account, AddressValue, CheckAccount, CheckStateStep, ScCallStep, SetStateStep, + }, + ScenarioWorld, WhiteboxContract, +}; + +const USER_ADDRESS_EXPR: &str = "address:user"; +const FORWARDER_ADDRESS_EXPR: &str = "sc:forwarder"; +const FORWARDER_PATH_EXPR: &str = "file:output/forwarder.wasm"; + +const NFT_TOKEN_ID_EXPR: &str = "str:COOL-123456"; +const NFT_TOKEN_ID: &[u8] = b"COOL-123456"; + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.set_current_dir_from_workspace("contracts/composability/forwarder"); + + blockchain.register_contract(FORWARDER_PATH_EXPR, forwarder::ContractBuilder); + blockchain +} + +#[test] +fn test_nft_update_attributes_and_send() { + let mut world = world(); + + let forwarder_code = world.code_expression(FORWARDER_PATH_EXPR); + let roles = vec![ + "ESDTRoleNFTCreate".to_string(), + "ESDTRoleNFTUpdateAttributes".to_string(), + ]; + + world.set_state_step( + SetStateStep::new() + .put_account(USER_ADDRESS_EXPR, Account::new().nonce(1)) + .put_account( + FORWARDER_ADDRESS_EXPR, + Account::new() + .nonce(1) + .code(forwarder_code.clone()) + .esdt_roles(NFT_TOKEN_ID_EXPR, roles), + ), + ); + + let forwarder_whitebox = WhiteboxContract::new(FORWARDER_ADDRESS_EXPR, forwarder::contract_obj); + + let original_attributes = Color { r: 0, g: 0, b: 0 }; + + world.whitebox_call( + &forwarder_whitebox, + ScCallStep::new().from(USER_ADDRESS_EXPR), + |sc| { + sc.nft_create_compact( + managed_token_id!(NFT_TOKEN_ID), + managed_biguint!(1), + original_attributes, + ); + + sc.send().direct_esdt( + &managed_address!(&address_expr_to_address(USER_ADDRESS_EXPR)), + &managed_token_id!(NFT_TOKEN_ID), + 1, + &managed_biguint!(1), + ); + }, + ); + + world.check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + 1, + "1", + Some(original_attributes), + ), + )); + + let new_attributes = Color { + r: 255, + g: 255, + b: 255, + }; + + world.whitebox_call( + &forwarder_whitebox, + ScCallStep::new() + .from(USER_ADDRESS_EXPR) + .esdt_transfer(NFT_TOKEN_ID, 1, rust_biguint!(1)), + |sc| { + sc.nft_update_attributes(managed_token_id!(NFT_TOKEN_ID), 1, new_attributes); + + sc.send().direct_esdt( + &managed_address!(&address_expr_to_address(USER_ADDRESS_EXPR)), + &managed_token_id!(NFT_TOKEN_ID), + 1, + &managed_biguint!(1), + ); + }, + ); + + world.check_state_step(CheckStateStep::new().put_account( + USER_ADDRESS_EXPR, + CheckAccount::new().esdt_nft_balance_and_attributes( + NFT_TOKEN_ID_EXPR, + 1, + "1", + Some(new_attributes), + ), + )); +} + +fn address_expr_to_address(address_expr: &str) -> Address { + AddressValue::from(address_expr).to_address() +} diff --git a/framework/scenario/src/scenario/model/account_data/account_check.rs b/framework/scenario/src/scenario/model/account_data/account_check.rs index f2becf776f..2366941c02 100644 --- a/framework/scenario/src/scenario/model/account_data/account_check.rs +++ b/framework/scenario/src/scenario/model/account_data/account_check.rs @@ -1,3 +1,5 @@ +use multiversx_sc::codec::{top_encode_to_vec_u8_or_panic, TopEncode}; + use crate::{ scenario::model::{ BigUintValue, BytesKey, BytesValue, CheckEsdt, CheckEsdtInstances, CheckEsdtMap, @@ -7,6 +9,7 @@ use crate::{ interpret_trait::{InterpretableFrom, InterpreterContext, IntoRaw}, serde_raw::CheckAccountRaw, }, + scenario_model::CheckEsdtData, }; use std::collections::BTreeMap; @@ -99,6 +102,52 @@ impl CheckAccount { self } + pub fn esdt_nft_balance_and_attributes( + mut self, + token_id_expr: K, + nonce_expr: N, + balance_expr: V, + attributes_expr: Option, + ) -> Self + where + BytesKey: From, + U64Value: From, + BigUintValue: From, + T: TopEncode, + { + let token_id = BytesKey::from(token_id_expr); + + if let CheckEsdtMap::Unspecified = &self.esdt { + let mut check_esdt = CheckEsdt::Full(CheckEsdtData::default()); + + if let Some(attributes_expr) = attributes_expr { + check_esdt.add_balance_and_attributes_check( + nonce_expr, + balance_expr, + top_encode_to_vec_u8_or_panic(&attributes_expr), + ); + } else { + check_esdt.add_balance_and_attributes_check( + nonce_expr, + balance_expr, + Vec::::new(), + ); + } + + let mut new_esdt_map = BTreeMap::new(); + let _ = new_esdt_map.insert(token_id, check_esdt); + + let new_check_esdt_map = CheckEsdtMapContents { + contents: new_esdt_map, + other_esdts_allowed: true, + }; + + self.esdt = CheckEsdtMap::Equal(new_check_esdt_map); + } + + self + } + pub fn check_storage(mut self, key: &str, value: &str) -> Self { let mut details = match self.storage { CheckStorage::Star => CheckStorageDetails::default(), diff --git a/framework/scenario/src/scenario/model/esdt_data/esdt_check.rs b/framework/scenario/src/scenario/model/esdt_data/esdt_check.rs index e708cf59c9..b268d1eb5f 100644 --- a/framework/scenario/src/scenario/model/esdt_data/esdt_check.rs +++ b/framework/scenario/src/scenario/model/esdt_data/esdt_check.rs @@ -5,6 +5,7 @@ use crate::{ interpret_trait::{InterpretableFrom, InterpreterContext, IntoRaw}, serde_raw::{CheckEsdtRaw, ValueSubTree}, }, + scenario_model::BytesValue, }; use num_bigint::BigUint; @@ -100,6 +101,54 @@ impl CheckEsdt { } } } + + pub fn add_balance_and_attributes_check( + &mut self, + nonce_expr: N, + balance_expr: V, + attributes_expr: T, + ) where + U64Value: From, + BigUintValue: From, + BytesValue: From, + { + let nonce = U64Value::from(nonce_expr); + let balance = BigUintValue::from(balance_expr); + let attributes = BytesValue::from(attributes_expr); + + self.convert_to_full(); + + if let CheckEsdt::Full(prev_esdt_check) = self { + match &mut prev_esdt_check.instances { + CheckEsdtInstances::Star => { + let new_instances_check = vec![CheckEsdtInstance { + nonce, + balance: CheckValue::Equal(balance), + attributes: CheckValue::Equal(attributes), + ..Default::default() + }]; + + prev_esdt_check.instances = CheckEsdtInstances::Equal(new_instances_check); + }, + CheckEsdtInstances::Equal(esdt_instance_check) => { + if let Some(i) = esdt_instance_check + .iter() + .position(|item| item.nonce.value == nonce.value) + { + esdt_instance_check[i].balance = CheckValue::Equal(balance); + esdt_instance_check[i].attributes = CheckValue::Equal(attributes); + } else { + esdt_instance_check.push(CheckEsdtInstance { + nonce, + balance: CheckValue::Equal(balance), + attributes: CheckValue::Equal(attributes), + ..Default::default() + }); + } + }, + } + } + } } impl InterpretableFrom for CheckEsdt { diff --git a/framework/scenario/src/scenario/run_vm/check_state.rs b/framework/scenario/src/scenario/run_vm/check_state.rs index 65302d42fa..dfcceb5c73 100644 --- a/framework/scenario/src/scenario/run_vm/check_state.rs +++ b/framework/scenario/src/scenario/run_vm/check_state.rs @@ -140,7 +140,7 @@ pub fn check_account_esdt(address: &AddressKey, expected: &CheckEsdtMap, actual: let single_instance = actual_value .instances .get_by_nonce(0) - .unwrap_or_else(|| panic!("Expected fungible ESDT with none 0")); + .unwrap_or_else(|| panic!("Expected fungible ESDT with nonce 0")); assert_eq!( single_instance.balance, expected_balance.value,