diff --git a/examples/cis2-multi-sponsored-txs/Cargo.toml b/examples/cis2-multi-sponsored-txs/Cargo.toml index 2951e4d1..9e7db1f7 100644 --- a/examples/cis2-multi-sponsored-txs/Cargo.toml +++ b/examples/cis2-multi-sponsored-txs/Cargo.toml @@ -16,6 +16,7 @@ concordium-cis2 = {path = "../../concordium-cis2", default-features = false} [dev-dependencies] concordium-smart-contract-testing = {path = "../../contract-testing"} +rand = "0.7.0" [lib] crate-type=["cdylib", "rlib"] diff --git a/examples/cis2-multi-sponsored-txs/src/lib.rs b/examples/cis2-multi-sponsored-txs/src/lib.rs index fd02ac98..1f8eaf55 100644 --- a/examples/cis2-multi-sponsored-txs/src/lib.rs +++ b/examples/cis2-multi-sponsored-txs/src/lib.rs @@ -1065,11 +1065,11 @@ fn contract_update_operator( /// Parameter type for the CIS-2 function `balanceOf` specialized to the subset /// of TokenIDs used by this contract. -type ContractBalanceOfQueryParams = BalanceOfQueryParams; +pub type ContractBalanceOfQueryParams = BalanceOfQueryParams; /// Response type for the CIS-2 function `balanceOf` specialized to the subset /// of TokenAmounts used by this contract. -type ContractBalanceOfQueryResponse = BalanceOfQueryResponse; +pub type ContractBalanceOfQueryResponse = BalanceOfQueryResponse; /// Get the balance of given token IDs and addresses. /// diff --git a/examples/cis2-multi-sponsored-txs/tests/tests.rs b/examples/cis2-multi-sponsored-txs/tests/tests.rs index a9decc00..add280c4 100644 --- a/examples/cis2-multi-sponsored-txs/tests/tests.rs +++ b/examples/cis2-multi-sponsored-txs/tests/tests.rs @@ -1,14 +1,18 @@ //! Tests for the `cis2_multi` contract. -use cis2_multi_sponsored_txs::*; +use cis2_multi_sponsored_txs::{ContractBalanceOfQueryParams, ContractBalanceOfQueryResponse, *}; use concordium_cis2::*; use concordium_smart_contract_testing::*; -use concordium_std::collections::BTreeMap; +use concordium_std::{ + collections::BTreeMap, AccountPublicKeys, AccountSignatures, CredentialSignatures, HashSha2256, + SignatureEd25519, Timestamp, +}; /// The tests accounts. const ALICE: AccountAddress = AccountAddress([0; 32]); const ALICE_ADDR: Address = Address::Account(ALICE); const BOB: AccountAddress = AccountAddress([1; 32]); const BOB_ADDR: Address = Address::Account(BOB); +const NON_EXISTING_ACCOUNT: AccountAddress = AccountAddress([2u8; 32]); /// Token IDs. const TOKEN_0: ContractTokenId = TokenIdU8(2); @@ -20,11 +24,16 @@ const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); /// A signer for all the transactions. const SIGNER: Signer = Signer::with_one_key(); +const DUMMY_SIGNATURE: SignatureEd25519 = SignatureEd25519([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +]); + /// Test minting succeeds and the tokens are owned by the given address and /// the appropriate events are logged. #[test] fn test_minting() { - let (chain, contract_address, update) = initialize_contract_with_alice_tokens(); + let (chain, _keypairs, contract_address, update) = initialize_contract_with_alice_tokens(); // Invoke the view entrypoint and check that the tokens are owned by Alice. let invoke = chain @@ -83,7 +92,7 @@ fn test_minting() { /// Test regular transfer where sender is the owner. #[test] fn test_account_transfer() { - let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); // Transfer one token from Alice to Bob. let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { @@ -148,7 +157,7 @@ fn test_account_transfer() { /// Then add Bob as an operator for Alice. #[test] fn test_add_operator() { - let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); // Add Bob as an operator for Alice. let params = UpdateOperatorParams(vec![UpdateOperator { @@ -208,7 +217,7 @@ fn test_add_operator() { /// himself. #[test] fn test_unauthorized_sender() { - let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); // Construct a transfer of `TOKEN_0` from Alice to Bob, which will be submitted // by Bob. @@ -240,7 +249,7 @@ fn test_unauthorized_sender() { /// Test that an operator can make a transfer. #[test] fn test_operator_can_transfer() { - let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); + let (mut chain, _keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); // Add Bob as an operator for Alice. let params = UpdateOperatorParams(vec![UpdateOperator { @@ -303,10 +312,438 @@ fn test_operator_can_transfer() { ]); } +/// Test permit update operator function. The signature is generated in the test +/// case. ALICE adds BOB as an operator. +#[test] +fn test_inside_signature_permit_update_operator() { + let (mut chain, keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Check operator in state + let bob_is_operator_of_alice = operator_of(&chain, contract_address); + + assert_eq!(bob_is_operator_of_alice, OperatorOfQueryResponse(vec![false])); + + // Create input parameters for the `permit` updateOperator function. + let update_operator = UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }; + let payload = UpdateOperatorParams(vec![update_operator]); + + // The `viewMessageHash` function uses the same input parameter `PermitParam` as + // the `permit` function. The `PermitParam` type includes a `signature` and + // a `signer`. Because these two values (`signature` and `signer`) are not + // read in the `viewMessageHash` function, any value can be used and we choose + // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. + let signature_map = BTreeMap::from([(0u8, CredentialSignatures { + sigs: BTreeMap::from([(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE))]), + })]); + + let mut permit_update_operator_param = PermitParam { + signature: AccountSignatures { + sigs: signature_map, + }, + signer: ALICE, + message: PermitMessage { + timestamp: Timestamp::from_timestamp_millis(10_000_000_000), + contract_address: ContractAddress::new(0, 0), + entry_point: OwnedEntrypointName::new_unchecked("updateOperator".into()), + nonce: 0, + payload: to_bytes(&payload), + }, + }; + + // Get the message hash to be signed. + let invoke = chain + .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_sponsored_txs.viewMessageHash".to_string(), + ), + message: OwnedParameter::from_serial(&permit_update_operator_param) + .expect("Should be a valid inut parameter"), + }) + .expect("Should be able to query viewMessageHash"); + + let message_hash: HashSha2256 = + from_bytes(&invoke.return_value).expect("Should return a valid result"); + + permit_update_operator_param.signature = keypairs.sign_message(&to_bytes(&message_hash)); + + // Update operator with the permit function. + let update = chain + .contract_update( + Signer::with_one_key(), + BOB, + Address::Account(BOB), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_sponsored_txs.permit".to_string(), + ), + message: OwnedParameter::from_serial(&permit_update_operator_param) + .expect("Should be a valid inut parameter"), + }, + ) + .expect("Should be able to update operator with permit"); + + // Check that the correct events occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>(); + + assert_eq!(events, [ + Event::Cis2Event(Cis2Event::UpdateOperator(UpdateOperatorEvent { + update: OperatorUpdate::Add, + owner: ALICE_ADDR, + operator: BOB_ADDR, + })), + Event::Nonce(NonceEvent { + account: ALICE, + nonce: 0, + }) + ]); + + // Check operator in state + let bob_is_operator_of_alice = operator_of(&chain, contract_address); + + assert_eq!(bob_is_operator_of_alice, OperatorOfQueryResponse(vec![true])); +} + +/// Test permit transfer function. The signature is generated in the test case. +/// TOKEN_1 is transferred from Alice to Bob. +#[test] +fn test_inside_signature_permit_transfer() { + let (mut chain, keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // Check balances in state. + let balance_of_alice_and_bob = get_balances(&chain, contract_address); + + assert_eq!(balance_of_alice_and_bob.0, [TokenAmountU64(1), TokenAmountU64(0)]); + + // Create input parameters for the `permit` transfer function. + let transfer = concordium_cis2::Transfer { + from: ALICE_ADDR, + to: Receiver::from_account(BOB), + token_id: TOKEN_1, + amount: ContractTokenAmount::from(1), + data: AdditionalData::empty(), + }; + let payload = TransferParams::from(vec![transfer]); + + // The `viewMessageHash` function uses the same input parameter `PermitParam` as + // the `permit` function. The `PermitParam` type includes a `signature` and + // a `signer`. Because these two values (`signature` and `signer`) are not + // read in the `viewMessageHash` function, any value can be used and we choose + // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. + let signature_map = BTreeMap::from([(0u8, CredentialSignatures { + sigs: BTreeMap::from([(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE))]), + })]); + + let mut permit_transfer_param = PermitParam { + signature: AccountSignatures { + sigs: signature_map, + }, + signer: ALICE, + message: PermitMessage { + timestamp: Timestamp::from_timestamp_millis(10_000_000_000), + contract_address: ContractAddress::new(0, 0), + entry_point: OwnedEntrypointName::new_unchecked("transfer".into()), + nonce: 0, + payload: to_bytes(&payload), + }, + }; + + // Get the message hash to be signed. + let invoke = chain + .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_sponsored_txs.viewMessageHash".to_string(), + ), + message: OwnedParameter::from_serial(&permit_transfer_param) + .expect("Should be a valid inut parameter"), + }) + .expect("Should be able to query viewMessageHash"); + + let message_hash: HashSha2256 = + from_bytes(&invoke.return_value).expect("Should return a valid result"); + + permit_transfer_param.signature = keypairs.sign_message(&to_bytes(&message_hash)); + + // Transfer token with the permit function. + let update = chain + .contract_update( + Signer::with_one_key(), + BOB, + BOB_ADDR, + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_sponsored_txs.permit".to_string(), + ), + message: OwnedParameter::from_serial(&permit_transfer_param) + .expect("Should be a valid inut parameter"), + }, + ) + .expect("Should be able to transfer token with permit"); + + // Check that the correct events occurred. + let events = update + .events() + .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) + .collect::>(); + + assert_eq!(events, [ + Event::Cis2Event(Cis2Event::Transfer(TransferEvent { + token_id: TOKEN_1, + amount: ContractTokenAmount::from(1), + from: ALICE_ADDR, + to: BOB_ADDR, + })), + Event::Nonce(NonceEvent { + account: ALICE, + nonce: 0, + }) + ]); + + // Check balances in state. + let balance_of_alice_and_bob = get_balances(&chain, contract_address); + + assert_eq!(balance_of_alice_and_bob.0, [TokenAmountU64(0), TokenAmountU64(1)]); +} + +// Test `nonceOf` query. We check that the nonce of `ALICE` is 1 when +/// the account already sent one sponsored transaction. We check that the nonce +/// of `BOB` is 0 when the account did not send any sponsored +/// transaction. We check that the nonce of `NON_EXISTING_ACCOUNT` is 0. +#[test] +fn test_nonce_of_query() { + let (mut chain, keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + + // To increase the nonce of `ALICE's` account, we invoke the + // `update_permit` function with a valid signature from ALICE account. + + // Create input parameters for the `permit` updateOperator function. + let update_operator = UpdateOperator { + update: OperatorUpdate::Add, + operator: BOB_ADDR, + }; + let payload = UpdateOperatorParams(vec![update_operator]); + + let mut inner_signature_map = BTreeMap::new(); + + // The `viewMessageHash` function uses the same input parameter `PermitParam` as + // the `permit` function. The `PermitParam` type includes a `signature` and + // a `signer`. Because these two values (`signature` and `signer`) are not + // read in the `viewMessageHash` function, any value can be used and we choose + // to use `DUMMY_SIGNATURE` and `ALICE` in the test case below. + inner_signature_map.insert(0u8, concordium_std::Signature::Ed25519(DUMMY_SIGNATURE)); + + let mut signature_map = BTreeMap::new(); + signature_map.insert(0u8, CredentialSignatures { + sigs: inner_signature_map, + }); + + let mut permit_update_operator_param = PermitParam { + signature: AccountSignatures { + sigs: signature_map, + }, + signer: ALICE, + message: PermitMessage { + timestamp: Timestamp::from_timestamp_millis(10_000_000_000), + contract_address: ContractAddress::new(0, 0), + entry_point: OwnedEntrypointName::new_unchecked("updateOperator".into()), + nonce: 0, + payload: to_bytes(&payload), + }, + }; + + // Get the message hash to be signed. + let invoke = chain + .contract_invoke(BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_sponsored_txs.viewMessageHash".to_string(), + ), + message: OwnedParameter::from_serial(&permit_update_operator_param) + .expect("Should be a valid inut parameter"), + }) + .expect("Should be able to query viewMessageHash"); + + let message_hash: HashSha2256 = + from_bytes(&invoke.return_value).expect("Should return a valid result"); + + permit_update_operator_param.signature = keypairs.sign_message(&to_bytes(&message_hash)); + + // Update operator with the permit function. + let _update = chain + .contract_update( + Signer::with_one_key(), + BOB, + Address::Account(BOB), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_sponsored_txs.permit".to_string(), + ), + message: OwnedParameter::from_serial(&permit_update_operator_param) + .expect("Should be a valid inut parameter"), + }, + ) + .expect("Should be able to update operator with permit"); + + // Check if correct nonces are returned by the `nonceOf` function. + let nonce_query_vector = VecOfAccountAddresses { + queries: vec![ALICE, BOB, NON_EXISTING_ACCOUNT], + }; + + let invoke = chain + .contract_invoke( + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_sponsored_txs.nonceOf".to_string(), + ), + message: OwnedParameter::from_serial(&nonce_query_vector) + .expect("Should be a valid inut parameter"), + }, + ) + .expect("Should be able to query publicKeyOf"); + + let nonces: NonceOfQueryResponse = + from_bytes(&invoke.return_value).expect("Should return a valid result"); + + assert_eq!( + nonces.0[0], 1, + "Nonce of ALICE should be 1 because the account already sent one sponsored transaction" + ); + assert_eq!( + nonces.0[1], 0, + "Nonce of BOB should be 0 because the account did not send any sponsored transaction" + ); + assert_eq!(nonces.0[2], 0, "Nonce of non-existing account should be 0"); +} + +/// Test `publicKeyOf` query. `ALICE` should have its correct keys +/// returned. `NON_EXISTING_ACCOUNT` should have `None` returned because it does +/// not exist on chain. +#[test] +fn test_public_key_of_query() { + let (chain, keypairs, contract_address, _update) = initialize_contract_with_alice_tokens(); + + let public_key_of_query_vector = VecOfAccountAddresses { + queries: vec![ALICE, NON_EXISTING_ACCOUNT], + }; + + let invoke = chain + .contract_invoke( + ALICE, + Address::Account(ALICE), + Energy::from(10000), + UpdateContractPayload { + amount: Amount::zero(), + address: contract_address, + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_sponsored_txs.publicKeyOf".to_string(), + ), + message: OwnedParameter::from_serial(&public_key_of_query_vector) + .expect("Should be a valid inut parameter"), + }, + ) + .expect("Should be able to query publicKeyOf"); + + let public_keys_of: PublicKeyOfQueryResponse = + from_bytes(&invoke.return_value).expect("Should return a valid result"); + + let account_public_keys: AccountPublicKeys = (keypairs).into(); + + assert_eq!( + public_keys_of.0[0], + Some(account_public_keys), + "An existing account should have correct public keys returned" + ); + assert!(public_keys_of.0[1].is_none(), "Non existing account should have no public keys"); +} + +/// Check if Bob is an operator of Alice. +fn operator_of(chain: &Chain, contract_address: ContractAddress) -> OperatorOfQueryResponse { + let operator_of_params = OperatorOfQueryParams { + queries: vec![OperatorOfQuery { + address: BOB_ADDR, + owner: ALICE_ADDR, + }], + }; + + // Check operator in state + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_sponsored_txs.operatorOf".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&operator_of_params) + .expect("OperatorOf params"), + }) + .expect("Invoke operatorOf"); + let rv: OperatorOfQueryResponse = invoke.parse_return_value().expect("OperatorOf return value"); + rv +} + +/// Get the `TOKEN_1` balances for Alice and Bob. +fn get_balances( + chain: &Chain, + contract_address: ContractAddress, +) -> ContractBalanceOfQueryResponse { + let balance_of_params = ContractBalanceOfQueryParams { + queries: vec![ + BalanceOfQuery { + token_id: TOKEN_1, + address: ALICE_ADDR, + }, + BalanceOfQuery { + token_id: TOKEN_1, + address: BOB_ADDR, + }, + ], + }; + + let invoke = chain + .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { + amount: Amount::zero(), + receive_name: OwnedReceiveName::new_unchecked( + "cis2_multi_sponsored_txs.balanceOf".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&balance_of_params) + .expect("BalanceOf params"), + }) + .expect("Invoke balanceOf"); + let rv: ContractBalanceOfQueryResponse = + invoke.parse_return_value().expect("BalanceOf return value"); + rv +} + /// Helper function that sets up the contract with two types of tokens minted to /// Alice. She has 400 of `TOKEN_0` and 1 of `TOKEN_1`. -fn initialize_contract_with_alice_tokens() -> (Chain, ContractAddress, ContractInvokeSuccess) { - let (mut chain, contract_address) = initialize_chain_and_contract(); +fn initialize_contract_with_alice_tokens( +) -> (Chain, AccountKeys, ContractAddress, ContractInvokeSuccess) { + let (mut chain, keypairs, contract_address) = initialize_chain_and_contract(); let mint_params = MintParams { owner: ALICE_ADDR, @@ -340,7 +777,7 @@ fn initialize_contract_with_alice_tokens() -> (Chain, ContractAddress, ContractI }) .expect("Mint tokens"); - (chain, contract_address, update) + (chain, keypairs, contract_address, update) } /// Setup chain and contract. @@ -348,11 +785,21 @@ fn initialize_contract_with_alice_tokens() -> (Chain, ContractAddress, ContractI /// Also creates the two accounts, Alice and Bob. /// /// Alice is the owner of the contract. -fn initialize_chain_and_contract() -> (Chain, ContractAddress) { +fn initialize_chain_and_contract() -> (Chain, AccountKeys, ContractAddress) { let mut chain = Chain::new(); + let rng = &mut rand::thread_rng(); + + let keypairs = AccountKeys::singleton(rng); + + let balance = AccountBalance { + total: ACC_INITIAL_BALANCE, + staked: Amount::zero(), + locked: Amount::zero(), + }; + // Create some accounts accounts on the chain. - chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); + chain.create_account(Account::new_with_keys(ALICE, balance, (&keypairs).into())); chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); // Load and deploy the module. @@ -371,5 +818,5 @@ fn initialize_chain_and_contract() -> (Chain, ContractAddress) { }) .expect("Initialize contract"); - (chain, init.contract_address) + (chain, keypairs, init.contract_address) } diff --git a/examples/cis2-multi/Cargo.toml b/examples/cis2-multi/Cargo.toml deleted file mode 100644 index 789c13af..00000000 --- a/examples/cis2-multi/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "cis2-multi" -version = "0.1.0" -authors = ["Concordium "] -edition = "2021" -license = "MPL-2.0" - -[features] -default = ["std", "wee_alloc"] -std = ["concordium-std/std", "concordium-cis2/std"] -wee_alloc = ["concordium-std/wee_alloc"] - -[dependencies] -concordium-std = {path = "../../concordium-std", default-features = false} -concordium-cis2 = {path = "../../concordium-cis2", default-features = false} - -[dev-dependencies] -concordium-smart-contract-testing = {path = "../../contract-testing"} - -[lib] -crate-type=["cdylib", "rlib"] - -[profile.release] -codegen-units = 1 -opt-level = "s" diff --git a/examples/cis2-multi/src/lib.rs b/examples/cis2-multi/src/lib.rs deleted file mode 100644 index ecd98c30..00000000 --- a/examples/cis2-multi/src/lib.rs +++ /dev/null @@ -1,737 +0,0 @@ -//! A multi token example implementation of the Concordium Token Standard CIS2. -//! -//! # Description -//! An instance of this smart contract can contain a number of different token -//! types each identified by a token ID. A token type is then globally -//! identified by the contract address together with the token ID. -//! -//! In this example the contract is initialized with no tokens, and tokens can -//! be minted through a `mint` contract function, which will only succeed for -//! the contract owner. No functionality to burn token is defined in this -//! example. -//! -//! Note: The word 'address' refers to either an account address or a -//! contract address. -//! -//! As follows from the CIS2 specification, the contract has a `transfer` -//! function for transferring an amount of a specific token type from one -//! address to another address. An address can enable and disable one or more -//! addresses as operators. An operator of some address is allowed to transfer -//! any tokens owned by this address. -//! -//! This contract also contains an example of a function to be called when -//! receiving tokens. In which case the contract will forward the tokens to -//! the contract owner. -//! This function is not very useful and is only there to showcase a simple -//! implementation of a token receive hook. - -#![cfg_attr(not(feature = "std"), no_std)] -use concordium_cis2::*; -use concordium_std::*; - -/// List of supported standards by this contract address. -const SUPPORTS_STANDARDS: [StandardIdentifier<'static>; 2] = - [CIS0_STANDARD_IDENTIFIER, CIS2_STANDARD_IDENTIFIER]; - -// Types - -/// Contract token ID type. -/// To save bytes we use a small token ID type, but is limited to be represented -/// by a `u8`. -pub type ContractTokenId = TokenIdU8; - -/// Contract token amount type. -pub type ContractTokenAmount = TokenAmountU64; - -#[derive(Serialize, SchemaType)] -pub struct MintParam { - pub token_amount: ContractTokenAmount, - pub metadata_url: MetadataUrl, -} - -/// The parameter for the contract function `mint` which mints a number of -/// token types and/or amounts of tokens to a given address. -#[derive(Serialize, SchemaType)] -pub struct MintParams { - /// Owner of the newly minted tokens. - pub owner: Address, - /// A collection of tokens to mint. - pub tokens: collections::BTreeMap, -} - -/// The parameter type for the contract function `setImplementors`. -/// Takes a standard identifier and a list of contract addresses providing -/// implementations of this standard. -#[derive(Debug, Serialize, SchemaType)] -pub struct SetImplementorsParams { - /// The identifier for the standard. - id: StandardIdentifierOwned, - /// The addresses of the implementors of the standard. - implementors: Vec, -} - -/// The state for each address. -#[derive(Serial, DeserialWithState, Deletable)] -#[concordium(state_parameter = "S")] -struct AddressState { - /// The amount of tokens owned by this address. - balances: StateMap, - /// The address which are currently enabled as operators for this address. - operators: StateSet, -} - -impl AddressState { - fn empty(state_builder: &mut StateBuilder) -> Self { - AddressState { - balances: state_builder.new_map(), - operators: state_builder.new_set(), - } - } -} - -/// The contract state, -/// -/// Note: The specification does not specify how to structure the contract state -/// and this could be structured in a more space efficient way. -#[derive(Serial, DeserialWithState)] -#[concordium(state_parameter = "S")] -struct State { - /// The state of addresses. - state: StateMap, S>, - /// All of the token IDs - tokens: StateMap, - /// Map with contract addresses providing implementations of additional - /// standards. - implementors: StateMap, S>, -} - -/// The different errors the contract can produce. -#[derive(Serialize, Debug, PartialEq, Eq, Reject, SchemaType)] -pub enum CustomContractError { - /// Failed parsing the parameter. - #[from(ParseError)] - ParseParams, - /// Failed logging: Log is full. - LogFull, - /// Failed logging: Log is malformed. - LogMalformed, - /// Invalid contract name. - InvalidContractName, - /// Only a smart contract can call this function. - ContractOnly, - /// Failed to invoke a contract. - InvokeContractError, -} - -pub type ContractError = Cis2Error; - -pub type ContractResult = Result; - -/// Mapping the logging errors to ContractError. -impl From for CustomContractError { - fn from(le: LogError) -> Self { - match le { - LogError::Full => Self::LogFull, - LogError::Malformed => Self::LogMalformed, - } - } -} - -/// Mapping errors related to contract invocations to CustomContractError. -impl From> for CustomContractError { - fn from(_cce: CallContractError) -> Self { Self::InvokeContractError } -} - -/// Mapping CustomContractError to ContractError -impl From for ContractError { - fn from(c: CustomContractError) -> Self { Cis2Error::Custom(c) } -} - -impl From for CustomContractError { - fn from(_: NewReceiveNameError) -> Self { Self::InvalidContractName } -} - -impl From for CustomContractError { - fn from(_: NewContractNameError) -> Self { Self::InvalidContractName } -} - -impl State { - /// Construct a state with no tokens - fn empty(state_builder: &mut StateBuilder) -> Self { - State { - state: state_builder.new_map(), - tokens: state_builder.new_map(), - implementors: state_builder.new_map(), - } - } - - /// Mints an amount of tokens with a given address as the owner. - fn mint( - &mut self, - token_id: &ContractTokenId, - mint_param: &MintParam, - owner: &Address, - state_builder: &mut StateBuilder, - ) { - self.tokens.insert(*token_id, mint_param.metadata_url.to_owned()); - let mut owner_state = - self.state.entry(*owner).or_insert_with(|| AddressState::empty(state_builder)); - let mut owner_balance = owner_state.balances.entry(*token_id).or_insert(0.into()); - *owner_balance += mint_param.token_amount; - } - - /// Check that the token ID currently exists in this contract. - #[inline(always)] - fn contains_token(&self, token_id: &ContractTokenId) -> bool { - self.get_token(token_id).is_some() - } - - fn get_token(&self, token_id: &ContractTokenId) -> Option { - self.tokens.get(token_id).map(|x| x.to_owned()) - } - - /// Get the current balance of a given token id for a given address. - /// Results in an error if the token id does not exist in the state. - fn balance( - &self, - token_id: &ContractTokenId, - address: &Address, - ) -> ContractResult { - ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); - let balance = self.state.get(address).map_or(0.into(), |address_state| { - address_state.balances.get(token_id).map_or(0.into(), |x| *x) - }); - Ok(balance) - } - - /// Check if an address is an operator of a given owner address. - fn is_operator(&self, address: &Address, owner: &Address) -> bool { - self.state - .get(owner) - .map(|address_state| address_state.operators.contains(address)) - .unwrap_or(false) - } - - /// Update the state with a transfer. - /// Results in an error if the token id does not exist in the state or if - /// the from address have insufficient tokens to do the transfer. - fn transfer( - &mut self, - token_id: &ContractTokenId, - amount: ContractTokenAmount, - from: &Address, - to: &Address, - state_builder: &mut StateBuilder, - ) -> ContractResult<()> { - ensure!(self.contains_token(token_id), ContractError::InvalidTokenId); - // A zero transfer does not modify the state. - if amount == 0.into() { - return Ok(()); - } - - // Get the `from` state and balance, if not present it will fail since the - // balance is interpreted as 0 and the transfer amount must be more than - // 0 as this point.; - { - let mut from_address_state = - self.state.entry(*from).occupied_or(ContractError::InsufficientFunds)?; - let mut from_balance = from_address_state - .balances - .entry(*token_id) - .occupied_or(ContractError::InsufficientFunds)?; - ensure!(*from_balance >= amount, ContractError::InsufficientFunds); - *from_balance -= amount; - } - - let mut to_address_state = - self.state.entry(*to).or_insert_with(|| AddressState::empty(state_builder)); - let mut to_address_balance = to_address_state.balances.entry(*token_id).or_insert(0.into()); - *to_address_balance += amount; - - Ok(()) - } - - /// Update the state adding a new operator for a given address. - /// Succeeds even if the `operator` is already an operator for the - /// `address`. - fn add_operator( - &mut self, - owner: &Address, - operator: &Address, - state_builder: &mut StateBuilder, - ) { - let mut owner_state = - self.state.entry(*owner).or_insert_with(|| AddressState::empty(state_builder)); - owner_state.operators.insert(*operator); - } - - /// Update the state removing an operator for a given address. - /// Succeeds even if the `operator` is not an operator for the `address`. - fn remove_operator(&mut self, owner: &Address, operator: &Address) { - self.state.entry(*owner).and_modify(|address_state| { - address_state.operators.remove(operator); - }); - } - - /// Check if state contains any implementors for a given standard. - fn have_implementors(&self, std_id: &StandardIdentifierOwned) -> SupportResult { - if let Some(addresses) = self.implementors.get(std_id) { - SupportResult::SupportBy(addresses.to_vec()) - } else { - SupportResult::NoSupport - } - } - - /// Set implementors for a given standard. - fn set_implementors( - &mut self, - std_id: StandardIdentifierOwned, - implementors: Vec, - ) { - self.implementors.insert(std_id, implementors); - } -} - -// Contract functions - -/// Initialize contract instance with a no token types. -#[init(contract = "cis2_multi", event = "Cis2Event")] -fn contract_init(_ctx: &InitContext, state_builder: &mut StateBuilder) -> InitResult { - // Construct the initial contract state. - Ok(State::empty(state_builder)) -} - -#[derive(Serialize, SchemaType, PartialEq, Eq, Debug)] -pub struct ViewAddressState { - pub balances: Vec<(ContractTokenId, ContractTokenAmount)>, - pub operators: Vec
, -} - -#[derive(Serialize, SchemaType, PartialEq, Eq)] -pub struct ViewState { - pub state: Vec<(Address, ViewAddressState)>, - pub tokens: Vec, -} - -/// View function for testing. This reports on the entire state of the contract -/// for testing purposes. In a realistic example there `balance_of` and similar -/// functions with a smaller response. -#[receive(contract = "cis2_multi", name = "view", return_value = "ViewState")] -fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult { - let state = host.state(); - - let mut inner_state = Vec::new(); - for (k, a_state) in state.state.iter() { - let mut balances = Vec::new(); - let mut operators = Vec::new(); - for (token_id, amount) in a_state.balances.iter() { - balances.push((*token_id, *amount)); - } - for o in a_state.operators.iter() { - operators.push(*o); - } - - inner_state.push((*k, ViewAddressState { - balances, - operators, - })); - } - let mut tokens = Vec::new(); - for v in state.tokens.iter() { - tokens.push(*v.0); - } - - Ok(ViewState { - state: inner_state, - tokens, - }) -} - -/// Mint new tokens with a given address as the owner of these tokens. -/// Can only be called by the contract owner. -/// Logs a `Mint` and a `TokenMetadata` event for each token. -/// The url for the token metadata is the token ID encoded in hex, appended on -/// the `TOKEN_METADATA_BASE_URL`. -/// -/// It rejects if: -/// - The sender is not the contract instance owner. -/// - Fails to parse parameter. -/// - Any of the tokens fails to be minted, which could be if: -/// - Fails to log Mint event. -/// - Fails to log TokenMetadata event. -/// -/// Note: Can at most mint 32 token types in one call due to the limit on the -/// number of logs a smart contract can produce on each function call. -#[receive( - contract = "cis2_multi", - name = "mint", - parameter = "MintParams", - error = "ContractError", - enable_logger, - mutable -)] -fn contract_mint( - ctx: &ReceiveContext, - host: &mut Host, - logger: &mut impl HasLogger, -) -> ContractResult<()> { - // Get the contract owner - let owner = ctx.owner(); - // Get the sender of the transaction - let sender = ctx.sender(); - - ensure!(sender.matches_account(&owner), ContractError::Unauthorized); - - // Parse the parameter. - let params: MintParams = ctx.parameter_cursor().get()?; - - let (state, builder) = host.state_and_builder(); - for (token_id, mint_param) in params.tokens { - // Mint the token in the state. - state.mint(&token_id, &mint_param, ¶ms.owner, builder); - - // Event for minted token. - logger.log(&Cis2Event::Mint(MintEvent { - token_id, - amount: mint_param.token_amount, - owner: params.owner, - }))?; - - // Metadata URL for the token. - logger.log(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>(TokenMetadataEvent { - token_id, - metadata_url: mint_param.metadata_url, - }))?; - } - Ok(()) -} - -type TransferParameter = TransferParams; - -/// Execute a list of token transfers, in the order of the list. -/// -/// Logs a `Transfer` event and invokes a receive hook function for every -/// transfer in the list. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Any of the transfers fail to be executed, which could be if: -/// - The `token_id` does not exist. -/// - The sender is not the owner of the token, or an operator for this -/// specific `token_id` and `from` address. -/// - The token is not owned by the `from`. -/// - Fails to log event. -/// - Any of the receive hook function calls rejects. -#[receive( - contract = "cis2_multi", - name = "transfer", - parameter = "TransferParameter", - error = "ContractError", - enable_logger, - mutable -)] -fn contract_transfer( - ctx: &ReceiveContext, - host: &mut Host, - logger: &mut impl HasLogger, -) -> ContractResult<()> { - // Parse the parameter. - let TransferParams(transfers): TransferParameter = ctx.parameter_cursor().get()?; - // Get the sender who invoked this contract function. - let sender = ctx.sender(); - - for Transfer { - token_id, - amount, - from, - to, - data, - } in transfers - { - let (state, builder) = host.state_and_builder(); - // Authenticate the sender for this transfer - ensure!(from == sender || state.is_operator(&sender, &from), ContractError::Unauthorized); - let to_address = to.address(); - // Update the contract state - state.transfer(&token_id, amount, &from, &to_address, builder)?; - - // Log transfer event - logger.log(&Cis2Event::Transfer(TransferEvent { - token_id, - amount, - from, - to: to_address, - }))?; - - // If the receiver is a contract we invoke it. - if let Receiver::Contract(address, entrypoint_name) = to { - let parameter = OnReceivingCis2Params { - token_id, - amount, - from, - data, - }; - host.invoke_contract( - &address, - ¶meter, - entrypoint_name.as_entrypoint_name(), - Amount::zero(), - )?; - } - } - Ok(()) -} - -/// Enable or disable addresses as operators of the sender address. -/// Logs an `UpdateOperator` event. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Fails to log event. -#[receive( - contract = "cis2_multi", - name = "updateOperator", - parameter = "UpdateOperatorParams", - error = "ContractError", - enable_logger, - mutable -)] -fn contract_update_operator( - ctx: &ReceiveContext, - host: &mut Host, - logger: &mut impl HasLogger, -) -> ContractResult<()> { - // Parse the parameter. - let UpdateOperatorParams(params) = ctx.parameter_cursor().get()?; - // Get the sender who invoked this contract function. - let sender = ctx.sender(); - - let (state, builder) = host.state_and_builder(); - for param in params { - // Update the operator in the state. - match param.update { - OperatorUpdate::Add => state.add_operator(&sender, ¶m.operator, builder), - OperatorUpdate::Remove => state.remove_operator(&sender, ¶m.operator), - } - - // Log the appropriate event - logger.log(&Cis2Event::::UpdateOperator( - UpdateOperatorEvent { - owner: sender, - operator: param.operator, - update: param.update, - }, - ))?; - } - Ok(()) -} - -/// Parameter type for the CIS-2 function `balanceOf` specialized to the subset -/// of TokenIDs used by this contract. -type ContractBalanceOfQueryParams = BalanceOfQueryParams; - -/// Response type for the CIS-2 function `balanceOf` specialized to the subset -/// of TokenAmounts used by this contract. -type ContractBalanceOfQueryResponse = BalanceOfQueryResponse; - -/// Get the balance of given token IDs and addresses. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Any of the queried `token_id` does not exist. -#[receive( - contract = "cis2_multi", - name = "balanceOf", - parameter = "ContractBalanceOfQueryParams", - return_value = "ContractBalanceOfQueryResponse", - error = "ContractError" -)] -fn contract_balance_of( - ctx: &ReceiveContext, - host: &Host, -) -> ContractResult { - // Parse the parameter. - let params: ContractBalanceOfQueryParams = ctx.parameter_cursor().get()?; - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for query in params.queries { - // Query the state for balance. - let amount = host.state().balance(&query.token_id, &query.address)?; - response.push(amount); - } - let result = ContractBalanceOfQueryResponse::from(response); - Ok(result) -} - -/// Takes a list of queries. Each query is an owner address and some address to -/// check as an operator of the owner address. -/// -/// It rejects if: -/// - It fails to parse the parameter. -#[receive( - contract = "cis2_multi", - name = "operatorOf", - parameter = "OperatorOfQueryParams", - return_value = "OperatorOfQueryResponse", - error = "ContractError" -)] -fn contract_operator_of( - ctx: &ReceiveContext, - host: &Host, -) -> ContractResult { - // Parse the parameter. - let params: OperatorOfQueryParams = ctx.parameter_cursor().get()?; - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for query in params.queries { - // Query the state for address being an operator of owner. - let is_operator = host.state().is_operator(&query.address, &query.owner); - response.push(is_operator); - } - let result = OperatorOfQueryResponse::from(response); - Ok(result) -} - -/// Parameter type for the CIS-2 function `tokenMetadata` specialized to the -/// subset of TokenIDs used by this contract. -type ContractTokenMetadataQueryParams = TokenMetadataQueryParams; - -/// Get the token metadata URLs and checksums given a list of token IDs. -/// -/// It rejects if: -/// - It fails to parse the parameter. -/// - Any of the queried `token_id` does not exist. -#[receive( - contract = "cis2_multi", - name = "tokenMetadata", - parameter = "ContractTokenMetadataQueryParams", - return_value = "TokenMetadataQueryResponse", - error = "ContractError" -)] -fn contract_token_metadata( - ctx: &ReceiveContext, - host: &Host, -) -> ContractResult { - // Parse the parameter. - let params: ContractTokenMetadataQueryParams = ctx.parameter_cursor().get()?; - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for token_id in params.queries { - let metadata_url = match host.state().tokens.get(&token_id) { - Some(metadata_url) => metadata_url.clone(), - None => bail!(ContractError::InvalidTokenId), - }; - response.push(metadata_url); - } - let result = TokenMetadataQueryResponse::from(response); - Ok(result) -} - -/// Example of implementing a function for receiving transfers. -/// It is not required to be implemented by the token contract, but is required -/// to implement such a function by any contract which should receive CIS2 -/// tokens. -/// -/// This contract function is called when a token is transferred to an instance -/// of this contract and should only be called by a contract implementing CIS2. -/// The parameter include a `data` field which can be used to -/// implement some arbitrary functionality. In this example we choose not to use -/// it, and define the function to forward any transfers to the owner of the -/// contract instance. -/// -/// Note: The name of this function is not part the CIS2, and a contract can -/// have multiple functions for receiving tokens. -/// -/// It rejects if: -/// - Sender is not a contract. -/// - It fails to parse the parameter. -/// - Contract name part of the parameter is invalid. -/// - Calling back `transfer` to sender contract rejects. -#[receive(contract = "cis2_multi", name = "onReceivingCIS2", error = "ContractError")] -fn contract_on_cis2_received(ctx: &ReceiveContext, host: &Host) -> ContractResult<()> { - // Ensure the sender is a contract. - let sender = if let Address::Contract(contract) = ctx.sender() { - contract - } else { - bail!(CustomContractError::ContractOnly.into()) - }; - - // Parse the parameter. - let params: OnReceivingCis2Params = - ctx.parameter_cursor().get()?; - - // Build the transfer from this contract to the contract owner. - let transfer = Transfer { - token_id: params.token_id, - amount: params.amount, - from: Address::Contract(ctx.self_address()), - to: Receiver::from_account(ctx.owner()), - data: AdditionalData::empty(), - }; - - let parameter = TransferParams::from(vec![transfer]); - - // Send back a transfer - host.invoke_contract_read_only( - &sender, - ¶meter, - EntrypointName::new("transfer")?, - Amount::zero(), - )?; - Ok(()) -} - -/// Get the supported standards or addresses for a implementation given list of -/// standard identifiers. -/// -/// It rejects if: -/// - It fails to parse the parameter. -#[receive( - contract = "cis2_multi", - name = "supports", - parameter = "SupportsQueryParams", - return_value = "SupportsQueryResponse", - error = "ContractError" -)] -fn contract_supports( - ctx: &ReceiveContext, - host: &Host, -) -> ContractResult { - // Parse the parameter. - let params: SupportsQueryParams = ctx.parameter_cursor().get()?; - - // Build the response. - let mut response = Vec::with_capacity(params.queries.len()); - for std_id in params.queries { - if SUPPORTS_STANDARDS.contains(&std_id.as_standard_identifier()) { - response.push(SupportResult::Support); - } else { - response.push(host.state().have_implementors(&std_id)); - } - } - let result = SupportsQueryResponse::from(response); - Ok(result) -} - -/// Set the addresses for an implementation given a standard identifier and a -/// list of contract addresses. -/// -/// It rejects if: -/// - Sender is not the owner of the contract instance. -/// - It fails to parse the parameter. -#[receive( - contract = "cis2_multi", - name = "setImplementors", - parameter = "SetImplementorsParams", - error = "ContractError", - mutable -)] -fn contract_set_implementor(ctx: &ReceiveContext, host: &mut Host) -> ContractResult<()> { - // Authorize the sender. - ensure!(ctx.sender().matches_account(&ctx.owner()), ContractError::Unauthorized); - // Parse the parameter. - let params: SetImplementorsParams = ctx.parameter_cursor().get()?; - // Update the implementors in the state - host.state_mut().set_implementors(params.id, params.implementors); - Ok(()) -} diff --git a/examples/cis2-multi/tests/tests.rs b/examples/cis2-multi/tests/tests.rs deleted file mode 100644 index b1a8f918..00000000 --- a/examples/cis2-multi/tests/tests.rs +++ /dev/null @@ -1,353 +0,0 @@ -//! Tests for the `cis2_multi` contract. -use cis2_multi::*; -use concordium_cis2::*; -use concordium_smart_contract_testing::*; -use concordium_std::collections::BTreeMap; - -/// The tests accounts. -const ALICE: AccountAddress = AccountAddress([0; 32]); -const ALICE_ADDR: Address = Address::Account(ALICE); -const BOB: AccountAddress = AccountAddress([1; 32]); -const BOB_ADDR: Address = Address::Account(BOB); - -/// Token IDs. -const TOKEN_0: ContractTokenId = TokenIdU8(2); -const TOKEN_1: ContractTokenId = TokenIdU8(42); - -/// Initial balance of the accounts. -const ACC_INITIAL_BALANCE: Amount = Amount::from_ccd(10000); - -/// A signer for all the transactions. -const SIGNER: Signer = Signer::with_one_key(); - -/// Test minting succeeds and the tokens are owned by the given address and -/// the appropriate events are logged. -#[test] -fn test_minting() { - let (chain, contract_address, update) = initialize_contract_with_alice_tokens(); - - // Invoke the view entrypoint and check that the tokens are owned by Alice. - let invoke = chain - .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.view".to_string()), - address: contract_address, - message: OwnedParameter::empty(), - }) - .expect("Invoke view"); - - // Check that the tokens are owned by Alice. - let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); - assert_eq!(rv.tokens[..], [TOKEN_0, TOKEN_1]); - assert_eq!(rv.state, vec![(ALICE_ADDR, ViewAddressState { - balances: vec![(TOKEN_0, 400.into()), (TOKEN_1, 1.into())], - operators: Vec::new(), - })]); - - // Check that the events are logged. - let events = update.events().flat_map(|(_addr, events)| events); - - let events: Vec> = - events.map(|e| e.parse().expect("Deserialize event")).collect(); - - assert_eq!(events, [ - Cis2Event::Mint(MintEvent { - token_id: TokenIdU8(2), - amount: TokenAmountU64(400), - owner: ALICE_ADDR, - }), - Cis2Event::TokenMetadata(TokenMetadataEvent { - token_id: TokenIdU8(2), - metadata_url: MetadataUrl { - url: "https://some.example/token/02".to_string(), - hash: None, - }, - }), - Cis2Event::Mint(MintEvent { - token_id: TokenIdU8(42), - amount: TokenAmountU64(1), - owner: ALICE_ADDR, - }), - Cis2Event::TokenMetadata(TokenMetadataEvent { - token_id: TokenIdU8(42), - metadata_url: MetadataUrl { - url: "https://some.example/token/2A".to_string(), - hash: None, - }, - }), - ]); -} - -/// Test regular transfer where sender is the owner. -#[test] -fn test_account_transfer() { - let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); - - // Transfer one token from Alice to Bob. - let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { - from: ALICE_ADDR, - to: Receiver::Account(BOB), - token_id: TOKEN_0, - amount: TokenAmountU64(1), - data: AdditionalData::empty(), - }]); - - let update = chain - .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), - address: contract_address, - message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), - }) - .expect("Transfer tokens"); - - // Check that Bob has 1 `TOKEN_0` and Alice has 399. Also check that Alice still - // has 1 `TOKEN_1`. - let invoke = chain - .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.view".to_string()), - address: contract_address, - message: OwnedParameter::empty(), - }) - .expect("Invoke view"); - let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); - assert_eq!(rv.state, vec![ - (ALICE_ADDR, ViewAddressState { - balances: vec![(TOKEN_0, 399.into()), (TOKEN_1, 1.into())], - operators: Vec::new(), - }), - (BOB_ADDR, ViewAddressState { - balances: vec![(TOKEN_0, 1.into())], - operators: Vec::new(), - }), - ]); - - // Check that the events are logged. - let events = update - .events() - .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) - .collect::>>(); - - assert_eq!(events, [Cis2Event::Transfer(TransferEvent { - token_id: TOKEN_0, - amount: TokenAmountU64(1), - from: ALICE_ADDR, - to: BOB_ADDR, - }),]); -} - -/// Test that you can add an operator. -/// Initialize the contract with two tokens owned by Alice. -/// Then add Bob as an operator for Alice. -#[test] -fn test_add_operator() { - let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); - - // Add Bob as an operator for Alice. - let params = UpdateOperatorParams(vec![UpdateOperator { - update: OperatorUpdate::Add, - operator: BOB_ADDR, - }]); - - let update = chain - .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.updateOperator".to_string()), - address: contract_address, - message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), - }) - .expect("Update operator"); - - // Check that an operator event occurred. - let events = update - .events() - .flat_map(|(_addr, events)| events.iter().map(|e| e.parse().expect("Deserialize event"))) - .collect::>>(); - assert_eq!(events, [Cis2Event::UpdateOperator(UpdateOperatorEvent { - operator: BOB_ADDR, - owner: ALICE_ADDR, - update: OperatorUpdate::Add, - }),]); - - // Construct a query parameter to check whether Bob is an operator for Alice. - let query_params = OperatorOfQueryParams { - queries: vec![OperatorOfQuery { - owner: ALICE_ADDR, - address: BOB_ADDR, - }], - }; - - // Invoke the operatorOf view entrypoint and check that Bob is an operator for - // Alice. - let invoke = chain - .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.operatorOf".to_string()), - address: contract_address, - message: OwnedParameter::from_serial(&query_params).expect("OperatorOf params"), - }) - .expect("Invoke view"); - - let rv: OperatorOfQueryResponse = invoke.parse_return_value().expect("OperatorOf return value"); - assert_eq!(rv, OperatorOfQueryResponse(vec![true])); -} - -/// Test that a transfer fails when the sender is neither an operator or the -/// owner. In particular, Bob will attempt to transfer some of Alice's tokens to -/// himself. -#[test] -fn test_unauthorized_sender() { - let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); - - // Construct a transfer of `TOKEN_0` from Alice to Bob, which will be submitted - // by Bob. - let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { - from: ALICE_ADDR, - to: Receiver::Account(BOB), - token_id: TOKEN_0, - amount: TokenAmountU64(1), - data: AdditionalData::empty(), - }]); - - // Notice that Bob is the sender/invoker. - let update = chain - .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), - address: contract_address, - message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), - }) - .expect_err("Transfer tokens"); - - // Check that the correct error is returned. - let rv: ContractError = update.parse_return_value().expect("ContractError return value"); - assert_eq!(rv, ContractError::Unauthorized); -} - -/// Test that an operator can make a transfer. -#[test] -fn test_operator_can_transfer() { - let (mut chain, contract_address, _update) = initialize_contract_with_alice_tokens(); - - // Add Bob as an operator for Alice. - let params = UpdateOperatorParams(vec![UpdateOperator { - update: OperatorUpdate::Add, - operator: BOB_ADDR, - }]); - chain - .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.updateOperator".to_string()), - address: contract_address, - message: OwnedParameter::from_serial(¶ms).expect("UpdateOperator params"), - }) - .expect("Update operator"); - - // Let Bob make a transfer to himself on behalf of Alice. - let transfer_params = TransferParams::from(vec![concordium_cis2::Transfer { - from: ALICE_ADDR, - to: Receiver::Account(BOB), - token_id: TOKEN_0, - amount: TokenAmountU64(1), - data: AdditionalData::empty(), - }]); - - chain - .contract_update(SIGNER, BOB, BOB_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.transfer".to_string()), - address: contract_address, - message: OwnedParameter::from_serial(&transfer_params).expect("Transfer params"), - }) - .expect("Transfer tokens"); - - // Check that Bob now has 1 of `TOKEN_0` and Alice has 399. Also check that - // Alice still has 1 `TOKEN_1`. - let invoke = chain - .contract_invoke(ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.view".to_string()), - address: contract_address, - message: OwnedParameter::empty(), - }) - .expect("Invoke view"); - let rv: ViewState = invoke.parse_return_value().expect("ViewState return value"); - assert_eq!(rv.state, vec![ - (ALICE_ADDR, ViewAddressState { - balances: vec![(TOKEN_0, 399.into()), (TOKEN_1, 1.into())], - operators: vec![BOB_ADDR], - }), - (BOB_ADDR, ViewAddressState { - balances: vec![(TOKEN_0, 1.into())], - operators: Vec::new(), - }), - ]); -} - -/// Helper function that sets up the contract with two types of tokens minted to -/// Alice. She has 400 of `TOKEN_0` and 1 of `TOKEN_1`. -fn initialize_contract_with_alice_tokens() -> (Chain, ContractAddress, ContractInvokeSuccess) { - let (mut chain, contract_address) = initialize_chain_and_contract(); - - let mint_params = MintParams { - owner: ALICE_ADDR, - tokens: BTreeMap::from_iter(vec![ - (TOKEN_0, MintParam { - token_amount: 400.into(), - metadata_url: MetadataUrl { - url: "https://some.example/token/02".to_string(), - hash: None, - }, - }), - (TOKEN_1, MintParam { - token_amount: 1.into(), - metadata_url: MetadataUrl { - url: "https://some.example/token/2A".to_string(), - hash: None, - }, - }), - ]), - }; - - // Mint two tokens for which Alice is the owner. - let update = chain - .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { - amount: Amount::zero(), - receive_name: OwnedReceiveName::new_unchecked("cis2_multi.mint".to_string()), - address: contract_address, - message: OwnedParameter::from_serial(&mint_params).expect("Mint params"), - }) - .expect("Mint tokens"); - - (chain, contract_address, update) -} - -/// Setup chain and contract. -/// -/// Also creates the two accounts, Alice and Bob. -/// -/// Alice is the owner of the contract. -fn initialize_chain_and_contract() -> (Chain, ContractAddress) { - let mut chain = Chain::new(); - - // Create some accounts accounts on the chain. - chain.create_account(Account::new(ALICE, ACC_INITIAL_BALANCE)); - chain.create_account(Account::new(BOB, ACC_INITIAL_BALANCE)); - - // Load and deploy the module. - let module = module_load_v1("concordium-out/module.wasm.v1").expect("Module exists"); - let deployment = chain.module_deploy_v1(SIGNER, ALICE, module).expect("Deploy valid module"); - - // Initialize the auction contract. - let init = chain - .contract_init(SIGNER, ALICE, Energy::from(10000), InitContractPayload { - amount: Amount::zero(), - mod_ref: deployment.module_reference, - init_name: OwnedContractName::new_unchecked("init_cis2_multi".to_string()), - param: OwnedParameter::empty(), - }) - .expect("Initialize contract"); - - (chain, init.contract_address) -}