diff --git a/contracts/examples/rewards-distribution/sc-config.toml b/contracts/examples/rewards-distribution/sc-config.toml new file mode 100644 index 0000000000..888d97a79f --- /dev/null +++ b/contracts/examples/rewards-distribution/sc-config.toml @@ -0,0 +1,5 @@ +[[proxy]] +path = "src/rewards_distribution_proxy.rs" +[[proxy.path-rename]] +from = "multiversx_sc::types::io::operation_completion_status::" +to = "" diff --git a/contracts/examples/rewards-distribution/src/rewards_distribution.rs b/contracts/examples/rewards-distribution/src/rewards_distribution.rs index eb69173c62..7300fcaf88 100644 --- a/contracts/examples/rewards-distribution/src/rewards_distribution.rs +++ b/contracts/examples/rewards-distribution/src/rewards_distribution.rs @@ -5,7 +5,7 @@ use multiversx_sc_modules::ongoing_operation::{ CONTINUE_OP, DEFAULT_MIN_GAS_TO_SAVE_PROGRESS, STOP_OP, }; -pub mod proxy; +pub mod rewards_distribution_proxy; pub mod seed_nft_minter_proxy; type Epoch = u64; @@ -13,13 +13,15 @@ pub const EPOCHS_IN_WEEK: Epoch = 7; pub const MAX_PERCENTAGE: u64 = 100_000; // 100% pub const DIVISION_SAFETY_CONSTANT: u64 = 1_000_000_000_000; -#[derive(ManagedVecItem, NestedEncode, NestedDecode, TypeAbi)] +#[type_abi] +#[derive(ManagedVecItem, NestedEncode, NestedDecode)] pub struct Bracket { pub index_percent: u64, pub bracket_reward_percent: u64, } -#[derive(ManagedVecItem, NestedEncode, NestedDecode, TypeAbi)] +#[type_abi] +#[derive(ManagedVecItem, NestedEncode, NestedDecode)] pub struct ComputedBracket { pub end_index: u64, pub nft_reward_percent: BigUint, diff --git a/contracts/examples/rewards-distribution/src/proxy.rs b/contracts/examples/rewards-distribution/src/rewards_distribution_proxy.rs similarity index 100% rename from contracts/examples/rewards-distribution/src/proxy.rs rename to contracts/examples/rewards-distribution/src/rewards_distribution_proxy.rs diff --git a/contracts/examples/rewards-distribution/tests/mock_seed_nft_minter_proxy.rs b/contracts/examples/rewards-distribution/tests/mock_seed_nft_minter_proxy.rs new file mode 100644 index 0000000000..22d9e055cd --- /dev/null +++ b/contracts/examples/rewards-distribution/tests/mock_seed_nft_minter_proxy.rs @@ -0,0 +1,70 @@ +use multiversx_sc::proxy_imports::*; + +pub struct MockSeedNftMinterProxy; + +impl TxProxyTrait for MockSeedNftMinterProxy +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + type TxProxyMethods = MockSeedNftMinterProxyMethods; + + fn proxy_methods(self, tx: Tx) -> Self::TxProxyMethods { + MockSeedNftMinterProxyMethods { wrapped_tx: tx } + } +} + +pub struct MockSeedNftMinterProxyMethods +where + Env: TxEnv, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + wrapped_tx: Tx, +} + +#[rustfmt::skip] +impl MockSeedNftMinterProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + Gas: TxGas, +{ + pub fn init< + Arg0: CodecInto>, + >( + self, + nft_token_id: Arg0, + ) -> TxProxyDeploy { + self.wrapped_tx + .raw_deploy() + .argument(&nft_token_id) + .original_result() + } +} + +#[rustfmt::skip] +impl MockSeedNftMinterProxyMethods +where + Env: TxEnv, + Env::Api: VMApi, + From: TxFrom, + To: TxTo, + Gas: TxGas, +{ + pub fn set_nft_count< + Arg0: CodecInto, + >( + self, + nft_count: Arg0, + ) -> TxProxyCall { + self.wrapped_tx + .raw_call("setNftCount") + .argument(&nft_count) + .original_result() + } +} diff --git a/contracts/examples/rewards-distribution/tests/rewards_distribution_blackbox_test.rs b/contracts/examples/rewards-distribution/tests/rewards_distribution_blackbox_test.rs deleted file mode 100644 index e01c9dafcf..0000000000 --- a/contracts/examples/rewards-distribution/tests/rewards_distribution_blackbox_test.rs +++ /dev/null @@ -1,361 +0,0 @@ -mod mock_seed_nft_minter; -mod utils; - -use multiversx_sc_scenario::imports::*; -use std::iter::zip; - -use crate::mock_seed_nft_minter::ProxyTrait as _; -use rewards_distribution::{ - proxy, Bracket, ContractObj, ProxyTrait as _, RewardsDistribution, DIVISION_SAFETY_CONSTANT, -}; - -const NFT_TOKEN_ID: &[u8] = b"NFT-123456"; -const NFT_TOKEN_ID_EXPR: &str = "str:NFT-123456"; - -const ALICE_ADDRESS_EXPR: &str = "address:alice"; -const ALICE_ADDRESS_EXPR_REPL: AddressExpr = AddressExpr("alice"); -const OWNER_ADDRESS_EXPR: &str = "address:owner"; -const REWARDS_DISTRIBUTION_ADDRESS_EXPR: &str = "sc:rewards-distribution"; -const REWARDS_DISTRIBUTION_ADDRESS_EXPR_REPL: ScExpr = ScExpr("rewards-distribution"); -const REWARDS_DISTRIBUTION_PATH_EXPR: &str = "mxsc:output/rewards-distribution.mxsc.json"; -const SEED_NFT_MINTER_ADDRESS_EXPR: &str = "sc:seed-nft-minter"; -const SEED_NFT_MINTER_PATH_EXPR: &str = "mxsc:../seed-nft-minter/output/seed-nft-minter.mxsc.json"; - -type RewardsDistributionContract = ContractInfo>; -type SeedNFTMinterContract = ContractInfo>; - -fn world() -> ScenarioWorld { - let mut blockchain = ScenarioWorld::new(); - blockchain.set_current_dir_from_workspace("contracts/examples/rewards-distribution"); - - blockchain.register_contract( - REWARDS_DISTRIBUTION_PATH_EXPR, - rewards_distribution::ContractBuilder, - ); - blockchain.register_contract( - SEED_NFT_MINTER_PATH_EXPR, - mock_seed_nft_minter::ContractBuilder, - ); - blockchain -} - -struct RewardsDistributionTestState { - world: ScenarioWorld, - seed_nft_minter_address: Address, - seed_nft_minter_contract: SeedNFTMinterContract, - rewards_distribution_contract: RewardsDistributionContract, - rewards_distribution_whitebox: WhiteboxContract>, -} - -impl RewardsDistributionTestState { - fn new() -> Self { - let mut world = world(); - - world.set_state_step( - SetStateStep::new().put_account(OWNER_ADDRESS_EXPR, Account::new().nonce(1)), - ); - - let seed_nft_minter_address = AddressValue::from(SEED_NFT_MINTER_ADDRESS_EXPR).to_address(); - - let seed_nft_minter_contract = SeedNFTMinterContract::new(SEED_NFT_MINTER_ADDRESS_EXPR); - let rewards_distribution_contract = - RewardsDistributionContract::new(REWARDS_DISTRIBUTION_ADDRESS_EXPR); - let rewards_distribution_whitebox = WhiteboxContract::new( - REWARDS_DISTRIBUTION_ADDRESS_EXPR, - rewards_distribution::contract_obj, - ); - - Self { - world, - seed_nft_minter_address, - seed_nft_minter_contract, - rewards_distribution_contract, - rewards_distribution_whitebox, - } - } - - fn deploy_seed_nft_minter_contract(&mut self) -> &mut Self { - let seed_nft_miinter_code = self.world.code_expression(SEED_NFT_MINTER_PATH_EXPR); - - self.world.sc_deploy( - ScDeployStep::new() - .from(OWNER_ADDRESS_EXPR) - .code(seed_nft_miinter_code) - .call( - self.seed_nft_minter_contract - .init(TokenIdentifier::from_esdt_bytes(NFT_TOKEN_ID)), - ), - ); - - self.world.sc_call( - ScCallStep::new() - .from(OWNER_ADDRESS_EXPR) - .call(self.seed_nft_minter_contract.set_nft_count(10_000u64)), - ); - - self - } - - fn deploy_rewards_distribution_contract(&mut self) -> &mut Self { - let rewards_distribution_code = self.world.code_expression(REWARDS_DISTRIBUTION_PATH_EXPR); - - let brackets_vec = &[ - (10, 2_000), - (90, 6_000), - (400, 7_000), - (2_500, 10_000), - (25_000, 35_000), - (72_000, 40_000), - ]; - let mut brackets = ManagedVec::::new(); - for (index_percent, bracket_reward_percent) in brackets_vec.iter().cloned() { - brackets.push(Bracket { - index_percent, - bracket_reward_percent, - }); - } - self.world.sc_deploy( - ScDeployStep::new() - .from(OWNER_ADDRESS_EXPR) - .code(rewards_distribution_code) - .call( - self.rewards_distribution_contract - .init(self.seed_nft_minter_address.clone(), brackets), - ), - ); - - self - } -} - -#[test] -fn test_compute_brackets() { - let mut state = RewardsDistributionTestState::new(); - - let rewards_distribution_code = state.world.code_expression(REWARDS_DISTRIBUTION_PATH_EXPR); - - state.world.set_state_step( - SetStateStep::new().put_account( - REWARDS_DISTRIBUTION_ADDRESS_EXPR, - Account::new() - .nonce(1) - .owner(OWNER_ADDRESS_EXPR) - .code(rewards_distribution_code) - .balance("0"), - ), - ); - - state.world.whitebox_call( - &state.rewards_distribution_whitebox, - ScCallStep::new().from(OWNER_ADDRESS_EXPR), - |sc| { - let brackets = utils::to_brackets(&[ - (10, 2_000), - (90, 6_000), - (400, 7_000), - (2_500, 10_000), - (25_000, 35_000), - (72_000, 40_000), - ]); - - let computed_brackets = sc.compute_brackets(brackets, 10_000); - - let expected_values = vec![ - (1, 2_000 * DIVISION_SAFETY_CONSTANT), - (10, 6_000 * DIVISION_SAFETY_CONSTANT / (10 - 1)), - (50, 7_000 * DIVISION_SAFETY_CONSTANT / (50 - 10)), - (300, 10_000 * DIVISION_SAFETY_CONSTANT / (300 - 50)), - (2_800, 35_000 * DIVISION_SAFETY_CONSTANT / (2_800 - 300)), - (10_000, 40_000 * DIVISION_SAFETY_CONSTANT / (10_000 - 2_800)), - ]; - - assert_eq!(computed_brackets.len(), expected_values.len()); - for (computed, expected) in zip(computed_brackets.iter(), expected_values) { - let (expected_end_index, expected_reward_percent) = expected; - assert_eq!(computed.end_index, expected_end_index); - assert_eq!(computed.nft_reward_percent, expected_reward_percent); - } - }, - ); -} - -#[test] -fn test_raffle_and_claim() { - let mut state = RewardsDistributionTestState::new(); - - let nft_nonces: [u64; 6] = [1, 2, 3, 4, 5, 6]; - let nft_payments: Vec = nft_nonces - .iter() - .map(|nonce| TxESDT { - esdt_token_identifier: NFT_TOKEN_ID.into(), - nonce: (*nonce).into(), - esdt_value: 1u64.into(), - }) - .collect(); - - let mut alice_account = Account::new().nonce(1).balance("2_070_000_000"); - for nonce in nft_nonces.iter() { - alice_account = - alice_account.esdt_nft_balance(NFT_TOKEN_ID_EXPR, *nonce, "1", Option::<&[u8]>::None); - } - - state.world.set_state_step( - SetStateStep::new() - .put_account(ALICE_ADDRESS_EXPR, alice_account) - .new_address(OWNER_ADDRESS_EXPR, 1, SEED_NFT_MINTER_ADDRESS_EXPR) - .new_address(OWNER_ADDRESS_EXPR, 3, REWARDS_DISTRIBUTION_ADDRESS_EXPR), - ); - - state - .deploy_seed_nft_minter_contract() - .deploy_rewards_distribution_contract(); - - // deposit royalties - state.world.sc_call( - ScCallStep::new() - .from(ALICE_ADDRESS_EXPR) - .egld_value("2_070_000_000") - .call(state.rewards_distribution_contract.deposit_royalties()), - ); - - // run the raffle - state - .world - .tx() - .from(ALICE_ADDRESS_EXPR_REPL) - .to(REWARDS_DISTRIBUTION_ADDRESS_EXPR_REPL) - .typed(proxy::RewardsDistributionProxy) - .raffle() - .tx_hash([0u8; 32]) // blockchain rng is deterministic, so we can use a fixed hash - .run(); - - let mut rewards: Vec> = Vec::new(); - // post-raffle reward amount frequency checksstate - for nonce in 1u64..=10_000u64 { - state.world.sc_call_use_result( - ScCallStep::new().from(ALICE_ADDRESS_EXPR).call( - state - .rewards_distribution_contract - .compute_claimable_amount( - 0u64, - &EgldOrEsdtTokenIdentifier::egld(), - 0u64, - nonce, - ), - ), - |r: TypedResponse>| rewards.push(r.result.unwrap()), - ); - } - - assert_eq!(rewards.len() as u64, 10_000u64); - - // check that the reward amounts match in frequency - let expected_reward_amounts = [ - (41_400_000, 1), - (13_799_999, 9), - (3_622_500, 40), - (828_000, 250), - (289_800, 2500), - (114_999, 7200), - ]; - - let total_expected_count: u64 = expected_reward_amounts.iter().map(|(_, count)| count).sum(); - assert_eq!(total_expected_count, 10_000u64); - - for (amount, expected_count) in expected_reward_amounts { - let expected_amount = amount as u64; - assert_eq!( - rewards - .iter() - .filter(|value| *value == &expected_amount) - .count(), - expected_count as usize - ); - } - - let expected_rewards = [114_999, 114_999, 114_999, 828_000, 114_999, 114_999]; - - for (nonce, expected_reward) in std::iter::zip(nft_nonces, expected_rewards) { - state.world.sc_call_use_result( - ScCallStep::new().from(ALICE_ADDRESS_EXPR).call( - state - .rewards_distribution_contract - .compute_claimable_amount( - 0u64, - &EgldOrEsdtTokenIdentifier::egld(), - 0u64, - nonce, - ), - ), - |r: TypedResponse>| { - assert_eq!(r.result.unwrap().to_u64().unwrap(), expected_reward); - }, - ); - } - - // claim rewards - let mut reward_tokens: MultiValueEncoded< - StaticApi, - MultiValue2, u64>, - > = MultiValueEncoded::new(); - reward_tokens.push((EgldOrEsdtTokenIdentifier::egld(), 0).into()); - state.world.sc_call( - ScCallStep::new() - .from(ALICE_ADDRESS_EXPR) - .multi_esdt_transfer(nft_payments.clone()) - .call( - state - .rewards_distribution_contract - .claim_rewards(0u64, 0u64, reward_tokens), - ), - ); - - // check that the rewards were claimed - for nonce in nft_nonces.iter() { - state.world.sc_query( - ScQueryStep::new() - .call(state.rewards_distribution_contract.was_claimed( - 0u64, - &EgldOrEsdtTokenIdentifier::egld(), - 0u64, - nonce, - )) - .expect_value(SingleValue::from(true)), - ); - } - - // confirm the received amount matches the sum of the queried rewards - let alice_balance_after_claim: u64 = expected_rewards.iter().sum(); - let balance_expr = alice_balance_after_claim.to_string(); - - state - .world - .check_state_step(CheckStateStep::new().put_account( - ALICE_ADDRESS_EXPR, - CheckAccount::new().balance(balance_expr.as_str()), - )); - - // a second claim with the same nfts should succeed, but return no more rewards - let mut reward_tokens: MultiValueEncoded< - StaticApi, - MultiValue2, u64>, - > = MultiValueEncoded::new(); - reward_tokens.push((EgldOrEsdtTokenIdentifier::egld(), 0).into()); - state.world.sc_call( - ScCallStep::new() - .from(ALICE_ADDRESS_EXPR) - .multi_esdt_transfer(nft_payments) - .call( - state - .rewards_distribution_contract - .claim_rewards(0u64, 0u64, reward_tokens), - ), - ); - - state - .world - .check_state_step(CheckStateStep::new().put_account( - ALICE_ADDRESS_EXPR, - CheckAccount::new().balance(balance_expr.as_str()), - )); -} diff --git a/contracts/examples/rewards-distribution/tests/rewards_distribution_integration_test.rs b/contracts/examples/rewards-distribution/tests/rewards_distribution_integration_test.rs new file mode 100644 index 0000000000..a375614dab --- /dev/null +++ b/contracts/examples/rewards-distribution/tests/rewards_distribution_integration_test.rs @@ -0,0 +1,327 @@ +mod mock_seed_nft_minter; +mod mock_seed_nft_minter_proxy; +mod utils; + +use multiversx_sc_scenario::imports::*; +use std::iter::zip; + +use rewards_distribution::{ + rewards_distribution_proxy, ContractObj, RewardsDistribution, DIVISION_SAFETY_CONSTANT, +}; + +const NFT_TOKEN_ID: &[u8] = b"NFT-123456"; +const NFT_TOKEN_ID_EXPR: &str = "str:NFT-123456"; + +const ALICE_ADDRESS_EXPR: AddressExpr = AddressExpr("alice"); +const OWNER_ADDRESS_EXPR: AddressExpr = AddressExpr("owner"); +const REWARDS_DISTRIBUTION_ADDRESS_EXPR: ScExpr = ScExpr("rewards-distribution"); +const REWARDS_DISTRIBUTION_PATH_EXPR: MxscExpr = MxscExpr("output/rewards-distribution.mxsc.json"); +const SEED_NFT_MINTER_ADDRESS_EXPR: ScExpr = ScExpr("seed-nft-minter"); +const SEED_NFT_MINTER_PATH_EXPR: MxscExpr = + MxscExpr("../seed-nft-minter/output/seed-nft-minter.mxsc.json"); + +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + + blockchain.register_contract( + REWARDS_DISTRIBUTION_PATH_EXPR.eval_to_expr().as_str(), + rewards_distribution::ContractBuilder, + ); + blockchain.register_contract( + SEED_NFT_MINTER_PATH_EXPR.eval_to_expr().as_str(), + mock_seed_nft_minter::ContractBuilder, + ); + blockchain +} + +struct RewardsDistributionTestState { + world: ScenarioWorld, + rewards_distribution_whitebox: WhiteboxContract>, +} + +impl RewardsDistributionTestState { + fn new() -> Self { + let mut world = world(); + + world.account(OWNER_ADDRESS_EXPR).nonce(1); + + let rewards_distribution_whitebox = WhiteboxContract::new( + REWARDS_DISTRIBUTION_ADDRESS_EXPR, + rewards_distribution::contract_obj, + ); + + Self { + world, + rewards_distribution_whitebox, + } + } + + fn deploy_seed_nft_minter_contract(&mut self) -> &mut Self { + self.world + .tx() + .from(OWNER_ADDRESS_EXPR) + .typed(mock_seed_nft_minter_proxy::MockSeedNftMinterProxy) + .init(TokenIdentifier::from_esdt_bytes(NFT_TOKEN_ID)) + .code(SEED_NFT_MINTER_PATH_EXPR) + .run(); + + self.world + .tx() + .from(OWNER_ADDRESS_EXPR) + .to(SEED_NFT_MINTER_ADDRESS_EXPR) + .typed(mock_seed_nft_minter_proxy::MockSeedNftMinterProxy) + .set_nft_count(10_000u64) + .run(); + + self + } + + fn deploy_rewards_distribution_contract(&mut self) -> &mut Self { + let brackets_vec = &[ + (10, 2_000), + (90, 6_000), + (400, 7_000), + (2_500, 10_000), + (25_000, 35_000), + (72_000, 40_000), + ]; + let mut brackets = ManagedVec::new(); + for (index_percent, bracket_reward_percent) in brackets_vec.iter().cloned() { + brackets.push(rewards_distribution_proxy::Bracket { + index_percent, + bracket_reward_percent, + }); + } + self.world + .tx() + .from(OWNER_ADDRESS_EXPR) + .typed(rewards_distribution_proxy::RewardsDistributionProxy) + .init(SEED_NFT_MINTER_ADDRESS_EXPR.to_address(), brackets) + .code(REWARDS_DISTRIBUTION_PATH_EXPR) + .run(); + + self + } +} + +#[test] +fn test_compute_brackets() { + let mut state = RewardsDistributionTestState::new(); + + let rewards_distribution_code = state + .world + .code_expression(REWARDS_DISTRIBUTION_PATH_EXPR.eval_to_expr().as_str()); + + state + .world + .account(REWARDS_DISTRIBUTION_ADDRESS_EXPR) + .nonce(1) + .owner(OWNER_ADDRESS_EXPR) + .code(rewards_distribution_code); + + state.world.whitebox_call( + &state.rewards_distribution_whitebox, + ScCallStep::new().from(OWNER_ADDRESS_EXPR), + |sc| { + let brackets = utils::to_brackets(&[ + (10, 2_000), + (90, 6_000), + (400, 7_000), + (2_500, 10_000), + (25_000, 35_000), + (72_000, 40_000), + ]); + + let computed_brackets = sc.compute_brackets(brackets, 10_000); + + let expected_values = vec![ + (1, 2_000 * DIVISION_SAFETY_CONSTANT), + (10, 6_000 * DIVISION_SAFETY_CONSTANT / (10 - 1)), + (50, 7_000 * DIVISION_SAFETY_CONSTANT / (50 - 10)), + (300, 10_000 * DIVISION_SAFETY_CONSTANT / (300 - 50)), + (2_800, 35_000 * DIVISION_SAFETY_CONSTANT / (2_800 - 300)), + (10_000, 40_000 * DIVISION_SAFETY_CONSTANT / (10_000 - 2_800)), + ]; + + assert_eq!(computed_brackets.len(), expected_values.len()); + for (computed, expected) in zip(computed_brackets.iter(), expected_values) { + let (expected_end_index, expected_reward_percent) = expected; + assert_eq!(computed.end_index, expected_end_index); + assert_eq!(computed.nft_reward_percent, expected_reward_percent); + } + }, + ); +} + +#[test] +fn test_raffle_and_claim() { + let mut state = RewardsDistributionTestState::new(); + + let nft_nonces: [u64; 6] = [1, 2, 3, 4, 5, 6]; + let mut nft_payments = ManagedVec::new(); + for nonce in nft_nonces.into_iter() { + let payment = EsdtTokenPayment::new(NFT_TOKEN_ID.into(), nonce, 1u64.into()); + nft_payments.push(payment); + } + + { + let mut account_setter = state + .world + .account(ALICE_ADDRESS_EXPR) + .nonce(1) + .balance("2_070_000_000"); + for nft_nonce in nft_nonces { + account_setter = account_setter.esdt_nft_balance( + NFT_TOKEN_ID_EXPR, + nft_nonce, + "1", + Option::<&[u8]>::None, + ); + } + } + + state.world.set_state_step( + SetStateStep::new() + .new_address(OWNER_ADDRESS_EXPR, 1, SEED_NFT_MINTER_ADDRESS_EXPR) + .new_address(OWNER_ADDRESS_EXPR, 3, REWARDS_DISTRIBUTION_ADDRESS_EXPR), + ); + + state + .deploy_seed_nft_minter_contract() + .deploy_rewards_distribution_contract(); + + // deposit royalties + state + .world + .tx() + .from(ALICE_ADDRESS_EXPR) + .to(REWARDS_DISTRIBUTION_ADDRESS_EXPR) + .typed(rewards_distribution_proxy::RewardsDistributionProxy) + .deposit_royalties() + .egld(2_070_000_000) + .run(); + + // run the raffle + state + .world + .tx() + .from(ALICE_ADDRESS_EXPR) + .to(REWARDS_DISTRIBUTION_ADDRESS_EXPR) + .typed(rewards_distribution_proxy::RewardsDistributionProxy) + .raffle() + .tx_hash([0u8; 32]) // blockchain rng is deterministic, so we can use a fixed hash + .run(); + + let mut rewards: Vec> = Vec::new(); + // post-raffle reward amount frequency checksstate + for nonce in 1u64..=10_000u64 { + let reward = state + .world + .tx() + .from(ALICE_ADDRESS_EXPR) + .to(REWARDS_DISTRIBUTION_ADDRESS_EXPR) + .typed(rewards_distribution_proxy::RewardsDistributionProxy) + .compute_claimable_amount(0u64, &EgldOrEsdtTokenIdentifier::egld(), 0u64, nonce) + .returns(ReturnsResult) + .run(); + rewards.push(reward); + } + + assert_eq!(rewards.len() as u64, 10_000u64); + + // check that the reward amounts match in frequency + let expected_reward_amounts = [ + (41_400_000, 1), + (13_799_999, 9), + (3_622_500, 40), + (828_000, 250), + (289_800, 2500), + (114_999, 7200), + ]; + + let total_expected_count: u64 = expected_reward_amounts.iter().map(|(_, count)| count).sum(); + assert_eq!(total_expected_count, 10_000u64); + + for (amount, expected_count) in expected_reward_amounts { + let expected_amount = amount as u64; + assert_eq!( + rewards + .iter() + .filter(|value| *value == &expected_amount) + .count(), + expected_count as usize + ); + } + + let expected_rewards = [114_999, 114_999, 114_999, 828_000, 114_999, 114_999]; + + for (nonce, expected_reward) in std::iter::zip(nft_nonces, expected_rewards) { + state + .world + .tx() + .from(ALICE_ADDRESS_EXPR) + .to(REWARDS_DISTRIBUTION_ADDRESS_EXPR) + .typed(rewards_distribution_proxy::RewardsDistributionProxy) + .compute_claimable_amount(0u64, &EgldOrEsdtTokenIdentifier::egld(), 0u64, nonce) + .returns(ExpectValue(expected_reward)) + .run(); + } + + // claim rewards + let mut reward_tokens: MultiValueEncoded< + StaticApi, + MultiValue2, u64>, + > = MultiValueEncoded::new(); + reward_tokens.push((EgldOrEsdtTokenIdentifier::egld(), 0).into()); + state + .world + .tx() + .from(ALICE_ADDRESS_EXPR) + .to(REWARDS_DISTRIBUTION_ADDRESS_EXPR) + .typed(rewards_distribution_proxy::RewardsDistributionProxy) + .claim_rewards(0u64, 0u64, reward_tokens) + .with_multi_token_transfer(nft_payments.clone()) + .run(); + + // check that the rewards were claimed + for nonce in nft_nonces.iter() { + state + .world + .query() + .to(REWARDS_DISTRIBUTION_ADDRESS_EXPR) + .typed(rewards_distribution_proxy::RewardsDistributionProxy) + .was_claimed(0u64, &EgldOrEsdtTokenIdentifier::egld(), 0u64, nonce) + .returns(ExpectValue(true)) + .run(); + } + + // confirm the received amount matches the sum of the queried rewards + let alice_balance_after_claim: u64 = expected_rewards.iter().sum(); + let balance_expr: &str = &alice_balance_after_claim.to_string(); + + state + .world + .check_account(ALICE_ADDRESS_EXPR) + .balance(balance_expr); + + // a second claim with the same nfts should succeed, but return no more rewards + let mut reward_tokens: MultiValueEncoded< + StaticApi, + MultiValue2, u64>, + > = MultiValueEncoded::new(); + reward_tokens.push((EgldOrEsdtTokenIdentifier::egld(), 0).into()); + state + .world + .tx() + .from(ALICE_ADDRESS_EXPR) + .to(REWARDS_DISTRIBUTION_ADDRESS_EXPR) + .typed(rewards_distribution_proxy::RewardsDistributionProxy) + .claim_rewards(0u64, 0u64, reward_tokens) + .with_multi_token_transfer(nft_payments) + .run(); + + state + .world + .check_account(ALICE_ADDRESS_EXPR) + .balance(balance_expr); +}