diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 9ee5cf49..e1633a8a 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -28,7 +28,7 @@ jobs: - examples/voting/Cargo.toml - examples/eSealing/Cargo.toml - examples/auction/Cargo.toml - - examples/cis2-multi/Cargo.toml + - examples/cis2-multi-sponsored-txs/Cargo.toml - examples/cis2-multi-royalties/Cargo.toml - examples/cis2-nft/Cargo.toml - examples/cis3-nft-sponsored-txs/Cargo.toml @@ -599,7 +599,7 @@ jobs: - examples/two-step-transfer/Cargo.toml - examples/cis2-wccd/Cargo.toml - examples/cis2-nft/Cargo.toml - - examples/cis2-multi/Cargo.toml + - examples/cis2-multi-sponsored-txs/Cargo.toml - examples/cis2-multi-royalties/Cargo.toml - examples/nametoken/Cargo.toml - examples/account-signature-checks/Cargo.toml @@ -677,7 +677,7 @@ jobs: - examples/voting/Cargo.toml - examples/eSealing/Cargo.toml - examples/auction/Cargo.toml - - examples/cis2-multi/Cargo.toml + - examples/cis2-multi-sponsored-txs/Cargo.toml - examples/cis2-multi-royalties/Cargo.toml - examples/cis2-nft/Cargo.toml - examples/cis3-nft-sponsored-txs/Cargo.toml @@ -806,7 +806,7 @@ jobs: - examples/voting - examples/eSealing - examples/auction - - examples/cis2-multi + - examples/cis2-multi-sponsored-txs - examples/cis2-multi-royalties - examples/cis2-nft - examples/cis3-nft-sponsored-txs diff --git a/examples/README.md b/examples/README.md index 2f0c74bd..40e88f29 100644 --- a/examples/README.md +++ b/examples/README.md @@ -23,8 +23,7 @@ The list of contracts is as follows: mimic the memo feature. Normally a transfer between accounts cannot add any information other than the amount being transferred. Making transfers to this intermediate contract instead works around this limitation. -- [cis2-multi](./cis2-multi) An example implementation of the CIS-2 Concordium Token Standard - containing multiple token types. +- [cis2-multi-sponsored-txs](./cis2-multi-sponsored-txs) An example implementation of the CIS-2 Concordium Token Standard and CIS-3 Concordium Sponsored Transaction Standard containing multiple token types. - [cis2-multi-royalties](./cis2-multi-royalties) An example implementation of the CIS-2 Concordium Token Standard which allows the token minter to be paid royalties containing multiple token types. - [cis2-nft](./cis2-nft) An example implementation of the CIS-2 Concordium Token Standard diff --git a/examples/cis2-multi-sponsored-txs/src/lib.rs b/examples/cis2-multi-sponsored-txs/src/lib.rs index 1f8eaf55..e5dbf35d 100644 --- a/examples/cis2-multi-sponsored-txs/src/lib.rs +++ b/examples/cis2-multi-sponsored-txs/src/lib.rs @@ -7,9 +7,10 @@ //! 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. +//! be minted through a `mint` contract function, which can be called by anyone. +//! The `mint` function airdrops the `MINT_AIRDROP` amount of tokens to a +//! specified `owner` address in the input parameter. No functionality to burn +//! token is defined in this example. //! //! Note: The word 'address' refers to either an account address or a //! contract address. @@ -18,7 +19,7 @@ //! implements the CIS3 standard which includes features for sponsored //! transactions. //! -//! The use case for this smart contract is for third-party service providers +//! The use case for CIS3 in this smart contract is for third-party service providers //! (the owner of this contract) that deal with conventional clients/users that //! don't want to acquire crypto (such as CCD) from an exchange. The third-party //! has often traditional fiat channels open (off-chain) with the conventional @@ -77,6 +78,9 @@ const SUPPORTS_PERMIT_ENTRYPOINTS: [EntrypointName; 2] = /// Tag for the CIS3 Nonce event. pub const NONCE_EVENT_TAG: u8 = u8::MAX - 5; +/// The amount of tokens to airdrop when the mint function is invoked. +pub const MINT_AIRDROP: TokenAmountU64 = TokenAmountU64(100); + /// Tagged events to be serialized for the event log. #[derive(Debug, Serial, Deserial, PartialEq, Eq)] #[concordium(repr(u8))] @@ -183,20 +187,17 @@ 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. +/// The parameter for the contract function `mint` which mints/airdrops a number +/// of tokens to the owner 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, + pub owner: Address, + /// The metadata_url of the token (needs to be present for the first time + /// this token_id is minted). + pub metadata_url: MetadataUrl, + /// The token_id to mint/create additional tokens. + pub token_id: ContractTokenId, } /// The state for each address. @@ -389,7 +390,7 @@ impl State { fn mint( &mut self, token_id: &ContractTokenId, - mint_param: &MintParam, + mint_param: &MintParams, owner: &Address, state_builder: &mut StateBuilder, ) { @@ -397,7 +398,7 @@ impl State { 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; + *owner_balance += MINT_AIRDROP; } /// Check that the token ID currently exists in this contract. @@ -570,21 +571,18 @@ fn contract_view(_ctx: &ReceiveContext, host: &Host) -> ReceiveResult, 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, - }))?; - } + // Mint the token in the state. + state.mint(¶ms.token_id, ¶ms, ¶ms.owner, builder); + + // Event for minted token. + logger.log(&Cis2Event::Mint(MintEvent { + token_id: params.token_id, + amount: MINT_AIRDROP, + owner: params.owner, + }))?; + + // Metadata URL for the token. + logger.log(&Cis2Event::TokenMetadata::<_, ContractTokenAmount>(TokenMetadataEvent { + token_id: params.token_id, + metadata_url: params.metadata_url, + }))?; Ok(()) } diff --git a/examples/cis2-multi-sponsored-txs/tests/tests.rs b/examples/cis2-multi-sponsored-txs/tests/tests.rs index add280c4..67359770 100644 --- a/examples/cis2-multi-sponsored-txs/tests/tests.rs +++ b/examples/cis2-multi-sponsored-txs/tests/tests.rs @@ -51,7 +51,7 @@ fn test_minting() { 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())], + balances: vec![(TOKEN_0, 100.into()), (TOKEN_1, 100.into())], operators: Vec::new(), })]); @@ -62,21 +62,9 @@ fn test_minting() { 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), + amount: TokenAmountU64(100), owner: ALICE_ADDR, }), Cis2Event::TokenMetadata(TokenMetadataEvent { @@ -129,7 +117,7 @@ fn test_account_transfer() { 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())], + balances: vec![(TOKEN_0, 99.into()), (TOKEN_1, 100.into())], operators: Vec::new(), }), (BOB_ADDR, ViewAddressState { @@ -302,7 +290,7 @@ fn test_operator_can_transfer() { 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())], + balances: vec![(TOKEN_0, 99.into()), (TOKEN_1, 100.into())], operators: vec![BOB_ADDR], }), (BOB_ADDR, ViewAddressState { @@ -423,7 +411,7 @@ fn test_inside_signature_permit_transfer() { // 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)]); + assert_eq!(balance_of_alice_and_bob.0, [TokenAmountU64(100), TokenAmountU64(0)]); // Create input parameters for the `permit` transfer function. let transfer = concordium_cis2::Transfer { @@ -517,7 +505,7 @@ fn test_inside_signature_permit_transfer() { // 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)]); + assert_eq!(balance_of_alice_and_bob.0, [TokenAmountU64(99), TokenAmountU64(1)]); } // Test `nonceOf` query. We check that the nonce of `ALICE` is 1 when @@ -746,26 +734,36 @@ fn initialize_contract_with_alice_tokens( let (mut chain, keypairs, 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, - }, - }), - ]), + owner: ALICE_ADDR, + token_id: TOKEN_0, + metadata_url: MetadataUrl { + url: "https://some.example/token/02".to_string(), + hash: None, + }, + }; + + // Mint/airdrop TOKEN_0 to Alice as 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_sponsored_txs.mint".to_string(), + ), + address: contract_address, + message: OwnedParameter::from_serial(&mint_params).expect("Mint params"), + }) + .expect("Mint tokens"); + + let mint_params = MintParams { + owner: ALICE_ADDR, + token_id: TOKEN_1, + metadata_url: MetadataUrl { + url: "https://some.example/token/2A".to_string(), + hash: None, + }, }; - // Mint two tokens for which Alice is the owner. + // Mint/airdrop TOKEN_1 to Alice as the owner. let update = chain .contract_update(SIGNER, ALICE, ALICE_ADDR, Energy::from(10000), UpdateContractPayload { amount: Amount::zero(),