diff --git a/.config/cargo_spellcheck.dic b/.config/cargo_spellcheck.dic index e154b64a976..16f0852d6a7 100644 --- a/.config/cargo_spellcheck.dic +++ b/.config/cargo_spellcheck.dic @@ -12,6 +12,7 @@ Polkadot RPC SHA UI +URI Wasm Wasm32 WebAssembly diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f7ed486a55b..6034ef4b3c3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -267,6 +267,7 @@ examples-test-experimental-engine: # We test only the examples for which the tests have already been migrated to # use the experimental engine. - cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/erc20/Cargo.toml + - cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/erc1155/Cargo.toml - cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/contract-terminate/Cargo.toml - cargo test --no-default-features --features std, ink-experimental-engine --verbose --manifest-path examples/contract-transfer/Cargo.toml diff --git a/examples/erc1155/.gitignore b/examples/erc1155/.gitignore new file mode 100644 index 00000000000..bf910de10af --- /dev/null +++ b/examples/erc1155/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/examples/erc1155/Cargo.toml b/examples/erc1155/Cargo.toml new file mode 100644 index 00000000000..87a0a4abee1 --- /dev/null +++ b/examples/erc1155/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "erc1155" +version = "3.0.0-rc3" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +ink_primitives = { version = "3.0.0-rc3", path = "../../crates/primitives", default-features = false } +ink_metadata = { version = "3.0.0-rc3", path = "../../crates/metadata", default-features = false, features = ["derive"], optional = true } +ink_env = { version = "3.0.0-rc3", path = "../../crates/env", default-features = false, features = ["ink-debug"] } +ink_storage = { version = "3.0.0-rc3", path = "../../crates/storage", default-features = false } +ink_lang = { version = "3.0.0-rc3", path = "../../crates/lang", default-features = false } +ink_prelude = { version = "3.0.0-rc3", path = "../../crates/prelude", default-features = false } + +scale = { package = "parity-scale-codec", version = "2.1", default-features = false, features = ["derive"] } +scale-info = { version = "0.6", default-features = false, features = ["derive"], optional = true } + +[lib] +name = "erc1155" +path = "lib.rs" +crate-type = ["cdylib"] + +[features] +default = ["std"] +std = [ + "ink_primitives/std", + "ink_metadata", + "ink_metadata/std", + "ink_env/std", + "ink_storage/std", + "ink_lang/std", + "ink_prelude/std", + "scale/std", + "scale-info", + "scale-info/std", +] +ink-as-dependency = [] +ink-experimental-engine = ["ink_env/ink-experimental-engine"] diff --git a/examples/erc1155/lib.rs b/examples/erc1155/lib.rs new file mode 100644 index 00000000000..7ced21add26 --- /dev/null +++ b/examples/erc1155/lib.rs @@ -0,0 +1,827 @@ +// Copyright 2018-2021 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] + +use ink_env::AccountId; +use ink_lang as ink; +use ink_prelude::vec::Vec; + +// This is the return value that we expect if a smart contract supports receiving ERC-1155 +// tokens. +// +// It is calculated with +// `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`, and corresponds +// to 0xf23a6e61. +#[cfg_attr(test, allow(dead_code))] +const ON_ERC_1155_RECEIVED_SELECTOR: [u8; 4] = [0xF2, 0x3A, 0x6E, 0x61]; + +// This is the return value that we expect if a smart contract supports batch receiving ERC-1155 +// tokens. +// +// It is calculated with +// `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`, and +// corresponds to 0xbc197c81. +const _ON_ERC_1155_BATCH_RECEIVED_SELECTOR: [u8; 4] = [0xBC, 0x19, 0x7C, 0x81]; + +/// A type representing the unique IDs of tokens managed by this contract. +pub type TokenId = u128; + +type Balance = ::Balance; + +// The ERC-1155 error types. +#[derive(Debug, PartialEq, scale::Encode, scale::Decode)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub enum Error { + /// This token ID has not yet been created by the contract. + UnexistentToken, + /// The caller tried to sending tokens to the zero-address (`0x00`). + ZeroAddressTransfer, + /// The caller is not approved to transfer tokens on behalf of the account. + NotApproved, + /// The account does not have enough funds to complete the transfer. + InsufficientBalance, + /// An account does not need to approve themselves to transfer tokens. + SelfApproval, + /// The number of tokens being transferred does not match the specified number of transfers. + BatchTransferMismatch, +} + +// The ERC-1155 result types. +pub type Result = core::result::Result; + +/// Evaluate `$x:expr` and if not true return `Err($y:expr)`. +/// +/// Used as `ensure!(expression_to_ensure, expression_to_return_on_false)`. +macro_rules! ensure { + ( $condition:expr, $error:expr $(,)? ) => {{ + if !$condition { + return ::core::result::Result::Err(::core::convert::Into::into($error)) + } + }}; +} + +/// The interface for an ERC-1155 compliant contract. +/// +/// The interface is defined here: . +/// +/// The goal of ERC-1155 is to allow a single deployed contract to manage a variety of assets. +/// These assets can be fungible, non-fungible, or a combination. +/// +/// By tracking multiple assets the ERC-1155 standard is able to support batch transfers, which +/// make it easy to transfer a mix of multiple tokens at once. +#[ink::trait_definition] +pub trait Erc1155 { + /// Transfer a `value` amount of `token_id` tokens to the `to` account from the `from` + /// account. + /// + /// Note that the call does not have to originate from the `from` account, and may originate + /// from any account which is approved to transfer `from`'s tokens. + #[ink(message)] + fn safe_transfer_from( + &mut self, + from: AccountId, + to: AccountId, + token_id: TokenId, + value: Balance, + data: Vec, + ) -> Result<()>; + + /// Perform a batch transfer of `token_ids` to the `to` account from the `from` account. + /// + /// The number of `values` specified to be transfer must match the number of `token_ids`, + /// otherwise this call will revert. + /// + /// Note that the call does not have to originate from the `from` account, and may originate + /// from any account which is approved to transfer `from`'s tokens. + #[ink(message)] + fn safe_batch_transfer_from( + &mut self, + from: AccountId, + to: AccountId, + token_ids: Vec, + values: Vec, + data: Vec, + ) -> Result<()>; + + /// Query the balance of a specific token for the provided account. + #[ink(message)] + fn balance_of(&self, owner: AccountId, token_id: TokenId) -> Balance; + + /// Query the balances for a set of tokens for a set of accounts. + /// + /// E.g use this call if you want to query what Alice and Bob's balances are for Tokens ID 1 and + /// ID 2. + /// + /// This will return all the balances for a given owner before moving on to the next owner. In + /// the example above this means that the return value should look like: + /// + /// [Alice Balance of Token ID 1, Alice Balance of Token ID 2, Bob Balance of Token ID 1, Bob Balance of Token ID 2] + #[ink(message)] + fn balance_of_batch( + &self, + owners: Vec, + token_ids: Vec, + ) -> Vec; + + /// Enable or disable a third party, known as an `operator`, to control all tokens on behalf of + /// the caller. + #[ink(message)] + fn set_approval_for_all(&mut self, operator: AccountId, approved: bool) + -> Result<()>; + + /// Query if the given `operator` is allowed to control all of `owner`'s tokens. + #[ink(message)] + fn is_approved_for_all(&self, owner: AccountId, operator: AccountId) -> bool; +} + +/// The interface for an ERC-1155 Token Receiver contract. +/// +/// The interface is defined here: . +/// +/// Smart contracts which want to accept token transfers must implement this interface. By default +/// if a contract does not support this interface any transactions originating from an ERC-1155 +/// compliant contract which attempt to transfer tokens directly to the contract's address must be +/// reverted. +#[ink::trait_definition] +pub trait Erc1155TokenReceiver { + /// Handle the receipt of a single ERC-1155 token. + /// + /// This should be called by a compliant ERC-1155 contract if the intended recipient is a smart + /// contract. + /// + /// If the smart contract implementing this interface accepts token transfers then it must + /// return `ON_ERC_1155_RECEIVED_SELECTOR` from this function. To reject a transfer it must revert. + /// + /// Any callers must revert if they receive anything other than `ON_ERC_1155_RECEIVED_SELECTOR` as a return + /// value. + #[ink(message)] + fn on_received( + &mut self, + operator: AccountId, + from: AccountId, + token_id: TokenId, + value: Balance, + data: Vec, + ) -> Vec; + + /// Handle the receipt of multiple ERC-1155 tokens. + /// + /// This should be called by a compliant ERC-1155 contract if the intended recipient is a smart + /// contract. + /// + /// If the smart contract implementing this interface accepts token transfers then it must + /// return `BATCH_ON_ERC_1155_RECEIVED_SELECTOR` from this function. To reject a transfer it must revert. + /// + /// Any callers must revert if they receive anything other than `BATCH_ON_ERC_1155_RECEIVED_SELECTOR` as a return + /// value. + #[ink(message)] + fn on_batch_received( + &mut self, + operator: AccountId, + from: AccountId, + token_ids: Vec, + values: Vec, + data: Vec, + ) -> Vec; +} + +#[ink::contract] +mod erc1155 { + use super::*; + + use ink_prelude::collections::BTreeMap; + use ink_storage::traits::{ + PackedLayout, + SpreadLayout, + }; + + /// Indicate that a token transfer has occured. + /// + /// This must be emitted even if a zero value transfer occurs. + #[ink(event)] + pub struct TransferSingle { + #[ink(topic)] + operator: Option, + #[ink(topic)] + from: Option, + #[ink(topic)] + to: Option, + token_id: TokenId, + value: Balance, + } + + /// Indicate that an approval event has happened. + #[ink(event)] + pub struct ApprovalForAll { + #[ink(topic)] + owner: AccountId, + #[ink(topic)] + operator: AccountId, + approved: bool, + } + + /// Indicate that a token's URI has been updated. + #[ink(event)] + pub struct Uri { + value: ink_prelude::string::String, + #[ink(topic)] + token_id: TokenId, + } + + /// Represents an (Owner, Operator) pair, in which the operator is allowed to spend funds on + /// behalf of the operator. + #[derive( + Copy, + Clone, + Debug, + Ord, + PartialOrd, + Eq, + PartialEq, + PackedLayout, + SpreadLayout, + scale::Encode, + scale::Decode, + )] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + struct Approval { + owner: AccountId, + operator: AccountId, + } + + /// An ERC-1155 contract. + #[ink(storage)] + #[derive(Default)] + pub struct Contract { + /// Tracks the balances of accounts across the different tokens that they might be holding. + balances: BTreeMap<(AccountId, TokenId), Balance>, + /// Which accounts (called operators) have been approved to spend funds on behalf of an owner. + approvals: BTreeMap, + /// A unique identifier for the tokens which have been minted (and are therefore supported) + /// by this contract. + token_id_nonce: TokenId, + } + + impl Contract { + /// Initialize a default instance of this ERC-1155 implementation. + #[ink(constructor)] + pub fn new() -> Self { + Default::default() + } + + /// Create the initial supply for a token. + /// + /// The initial supply will be provided to the caller (a.k.a the minter), and the + /// `token_id` will be assigned by the smart contract. + /// + /// Note that as implemented anyone can create tokens. If you were to deploy this to a + /// production environment you'd probably want to lock down the addresses that are allowed + /// to create tokens. + #[ink(message)] + pub fn create(&mut self, value: Balance) -> TokenId { + let caller = self.env().caller(); + + // Given that TokenId is a `u128` the likelihood of this overflowing is pretty slim. + self.token_id_nonce += 1; + self.balances.insert((caller, self.token_id_nonce), value); + + // Emit transfer event but with mint semantics + self.env().emit_event(TransferSingle { + operator: Some(caller), + from: None, + to: if value == 0 { None } else { Some(caller) }, + token_id: self.token_id_nonce, + value, + }); + + self.token_id_nonce + } + + /// Mint a `value` amount of `token_id` tokens. + /// + /// It is assumed that the token has already been `create`-ed. The newly minted supply will + /// be assigned to the caller (a.k.a the minter). + /// + /// Note that as implemented anyone can mint tokens. If you were to deploy this to a + /// production environment you'd probably want to lock down the addresses that are allowed + /// to mint tokens. + #[ink(message)] + pub fn mint(&mut self, token_id: TokenId, value: Balance) -> Result<()> { + ensure!(token_id <= self.token_id_nonce, Error::UnexistentToken); + + let caller = self.env().caller(); + self.balances.insert((caller, token_id), value); + + // Emit transfer event but with mint semantics + self.env().emit_event(TransferSingle { + operator: Some(caller), + from: None, + to: Some(caller), + token_id, + value, + }); + + Ok(()) + } + + // Helper function for performing single token transfers. + // + // Should not be used directly since it's missing certain checks which are important to the + // ERC-1155 standard (it is expected that the caller has already performed these). + fn perform_transfer( + &mut self, + from: AccountId, + to: AccountId, + token_id: TokenId, + value: Balance, + ) { + self.balances + .entry((from, token_id)) + .and_modify(|b| *b -= value); + + self.balances + .entry((to, token_id)) + .and_modify(|b| *b += value) + .or_insert(value); + + let caller = self.env().caller(); + self.env().emit_event(TransferSingle { + operator: Some(caller), + from: Some(from), + to: Some(from), + token_id, + value, + }); + } + + // Check if the address at `to` is a smart contract which accepts ERC-1155 token transfers. + // + // If they're a smart contract which **doesn't** accept tokens transfers this call will + // revert. Otherwise we risk locking user funds at in that contract with no chance of + // recovery. + #[cfg_attr(test, allow(unused_variables))] + fn transfer_acceptance_check( + &mut self, + caller: AccountId, + from: AccountId, + to: AccountId, + token_id: TokenId, + value: Balance, + data: Vec, + ) { + // This is disabled during tests due to the use of `eval_contract()` not being + // supported (tests end up panicking). + #[cfg(not(test))] + { + use ink_env::call::{ + build_call, + utils::ReturnType, + ExecutionInput, + Selector, + }; + + // If our recipient is a smart contract we need to see if they accept or + // reject this transfer. If they reject it we need to revert the call. + let params = build_call::() + .callee(to) + .gas_limit(5000) + .exec_input( + ExecutionInput::new(Selector::new(ON_ERC_1155_RECEIVED_SELECTOR)) + .push_arg(caller) + .push_arg(from) + .push_arg(token_id) + .push_arg(value) + .push_arg(data), + ) + .returns::>>() + .params(); + + match ink_env::eval_contract(¶ms) { + Ok(v) => { + ink_env::debug_println!( + "Received return value \"{:?}\" from contract {:?}", + v, + from + ); + assert_eq!( + v, + &ON_ERC_1155_RECEIVED_SELECTOR[..], + "The recipient contract at {:?} does not accept token transfers.\n + Expected: {:?}, Got {:?}", to, ON_ERC_1155_RECEIVED_SELECTOR, v + ) + } + Err(e) => { + match e { + ink_env::Error::CodeNotFound + | ink_env::Error::NotCallable => { + // Our recipient wasn't a smart contract, so there's nothing more for + // us to do + ink_env::debug_println!("Recipient at {:?} from is not a smart contract ({:?})", from, e); + } + _ => { + // We got some sort of error from the call to our recipient smart + // contract, and as such we must revert this call + let msg = ink_prelude::format!( + "Got error \"{:?}\" while trying to call {:?}", + e, + from + ); + ink_env::debug_println!("{}", &msg); + panic!("{}", &msg) + } + } + } + } + } + } + } + + impl super::Erc1155 for Contract { + #[ink(message)] + fn safe_transfer_from( + &mut self, + from: AccountId, + to: AccountId, + token_id: TokenId, + value: Balance, + data: Vec, + ) -> Result<()> { + let caller = self.env().caller(); + if caller != from { + ensure!(self.is_approved_for_all(from, caller), Error::NotApproved); + } + + ensure!(to != AccountId::default(), Error::ZeroAddressTransfer); + + let balance = self.balance_of(from, token_id); + ensure!(balance >= value, Error::InsufficientBalance); + + self.perform_transfer(from, to, token_id, value); + self.transfer_acceptance_check(caller, from, to, token_id, value, data); + + Ok(()) + } + + #[ink(message)] + fn safe_batch_transfer_from( + &mut self, + from: AccountId, + to: AccountId, + token_ids: Vec, + values: Vec, + data: Vec, + ) -> Result<()> { + let caller = self.env().caller(); + if caller != from { + ensure!(self.is_approved_for_all(from, caller), Error::NotApproved); + } + + ensure!(to != AccountId::default(), Error::ZeroAddressTransfer); + ensure!(!token_ids.is_empty(), Error::BatchTransferMismatch); + ensure!( + token_ids.len() == values.len(), + Error::BatchTransferMismatch, + ); + + let transfers = token_ids.iter().zip(values.iter()); + for (&id, &v) in transfers.clone() { + let balance = self.balance_of(from, id); + ensure!(balance >= v, Error::InsufficientBalance); + } + + for (&id, &v) in transfers { + self.perform_transfer(from, to, id, v); + } + + // Can use the any token ID/value here, we really just care about knowing if `to` is a + // smart contract which accepts transfers + self.transfer_acceptance_check( + caller, + from, + to, + token_ids[0], + values[0], + data, + ); + + Ok(()) + } + + #[ink(message)] + fn balance_of(&self, owner: AccountId, token_id: TokenId) -> Balance { + *self.balances.get(&(owner, token_id)).unwrap_or(&0) + } + + #[ink(message)] + fn balance_of_batch( + &self, + owners: Vec, + token_ids: Vec, + ) -> Vec { + let mut output = Vec::new(); + for o in &owners { + for t in &token_ids { + let amount = self.balance_of(*o, *t); + output.push(amount); + } + } + output + } + + #[ink(message)] + fn set_approval_for_all( + &mut self, + operator: AccountId, + approved: bool, + ) -> Result<()> { + let caller = self.env().caller(); + ensure!(operator != caller, Error::SelfApproval); + + let approval = Approval { + owner: caller, + operator, + }; + + if approved { + self.approvals.insert(approval, ()); + } else { + self.approvals.remove(&approval); + } + + self.env().emit_event(ApprovalForAll { + owner: approval.owner, + operator, + approved, + }); + + Ok(()) + } + + #[ink(message)] + fn is_approved_for_all(&self, owner: AccountId, operator: AccountId) -> bool { + self.approvals.get(&Approval { owner, operator }).is_some() + } + } + + impl super::Erc1155TokenReceiver for Contract { + #[ink(message, selector = "0xF23A6E61")] + fn on_received( + &mut self, + _operator: AccountId, + _from: AccountId, + _token_id: TokenId, + _value: Balance, + _data: Vec, + ) -> Vec { + // The ERC-1155 standard dictates that if a contract does not accept token transfers + // directly to the contract, then the contract must revert. + // + // This prevents a user from unintentionally transferring tokens to a smart contract + // and getting their funds stuck without any sort of recovery mechanism. + // + // Note that the choice of whether or not to accept tokens is implementation specific, + // and we've decided to not accept them in this implementation. + unimplemented!("This smart contract does not accept token transfer.") + } + + #[ink(message, selector = "0xBC197C81")] + fn on_batch_received( + &mut self, + _operator: AccountId, + _from: AccountId, + _token_ids: Vec, + _values: Vec, + _data: Vec, + ) -> Vec { + // The ERC-1155 standard dictates that if a contract does not accept token transfers + // directly to the contract, then the contract must revert. + // + // This prevents a user from unintentionally transferring tokens to a smart contract + // and getting their funds stuck without any sort of recovery mechanism. + // + // Note that the choice of whether or not to accept tokens is implementation specific, + // and we've decided to not accept them in this implementation. + unimplemented!("This smart contract does not accept batch token transfers.") + } + } + + #[cfg(test)] + mod tests { + /// Imports all the definitions from the outer scope so we can use them here. + use super::*; + use crate::Erc1155; + + use ink_lang as ink; + + #[cfg(feature = "ink-experimental-engine")] + fn set_sender(sender: AccountId) { + ink_env::test::set_caller::(sender); + } + + #[cfg(not(feature = "ink-experimental-engine"))] + fn set_sender(sender: AccountId) { + const WALLET: [u8; 32] = [7; 32]; + ink_env::test::push_execution_context::( + sender, + WALLET.into(), + 1000000, + 1000000, + ink_env::test::CallData::new(ink_env::call::Selector::new([0x00; 4])), /* dummy */ + ); + } + + #[cfg(feature = "ink-experimental-engine")] + fn default_accounts() -> ink_env::test::DefaultAccounts { + ink_env::test::default_accounts::() + } + + #[cfg(not(feature = "ink-experimental-engine"))] + fn default_accounts() -> ink_env::test::DefaultAccounts { + ink_env::test::default_accounts::() + .expect("off-chain environment should have been initialized already") + } + + fn alice() -> AccountId { + default_accounts().alice + } + + fn bob() -> AccountId { + default_accounts().bob + } + + fn charlie() -> AccountId { + default_accounts().charlie + } + + fn init_contract() -> Contract { + let mut erc = Contract::new(); + erc.balances.insert((alice(), 1), 10); + erc.balances.insert((alice(), 2), 20); + erc.balances.insert((bob(), 1), 10); + + erc + } + + #[ink::test] + fn can_get_correct_balance_of() { + let erc = init_contract(); + + assert_eq!(erc.balance_of(alice(), 1), 10); + assert_eq!(erc.balance_of(alice(), 2), 20); + assert_eq!(erc.balance_of(alice(), 3), 0); + assert_eq!(erc.balance_of(bob(), 2), 0); + } + + #[ink::test] + fn can_get_correct_batch_balance_of() { + let erc = init_contract(); + + assert_eq!( + erc.balance_of_batch(vec![alice()], vec![1, 2, 3]), + vec![10, 20, 0] + ); + assert_eq!( + erc.balance_of_batch(vec![alice(), bob()], vec![1]), + vec![10, 10] + ); + + assert_eq!( + erc.balance_of_batch(vec![alice(), bob(), charlie()], vec![1, 2]), + vec![10, 20, 10, 0, 0, 0] + ); + } + + #[ink::test] + fn can_send_tokens_between_accounts() { + let mut erc = init_contract(); + + assert!(erc.safe_transfer_from(alice(), bob(), 1, 5, vec![]).is_ok()); + assert_eq!(erc.balance_of(alice(), 1), 5); + assert_eq!(erc.balance_of(bob(), 1), 15); + + assert!(erc.safe_transfer_from(alice(), bob(), 2, 5, vec![]).is_ok()); + assert_eq!(erc.balance_of(alice(), 2), 15); + assert_eq!(erc.balance_of(bob(), 2), 5); + } + + #[ink::test] + fn sending_too_many_tokens_fails() { + let mut erc = init_contract(); + let res = erc.safe_transfer_from(alice(), bob(), 1, 99, vec![]); + assert_eq!(res.unwrap_err(), Error::InsufficientBalance); + } + + #[ink::test] + fn sending_tokens_to_zero_address_fails() { + let burn: AccountId = [0; 32].into(); + + let mut erc = init_contract(); + let res = erc.safe_transfer_from(alice(), burn, 1, 10, vec![]); + assert_eq!(res.unwrap_err(), Error::ZeroAddressTransfer); + } + + #[ink::test] + fn can_send_batch_tokens() { + let mut erc = init_contract(); + assert!(erc + .safe_batch_transfer_from(alice(), bob(), vec![1, 2], vec![5, 10], vec![]) + .is_ok()); + + let balances = erc.balance_of_batch(vec![alice(), bob()], vec![1, 2]); + assert_eq!(balances, vec![5, 10, 15, 10]) + } + + #[ink::test] + fn rejects_batch_if_lengths_dont_match() { + let mut erc = init_contract(); + let res = erc.safe_batch_transfer_from( + alice(), + bob(), + vec![1, 2, 3], + vec![5], + vec![], + ); + assert_eq!(res.unwrap_err(), Error::BatchTransferMismatch); + } + + #[ink::test] + fn batch_transfers_fail_if_len_is_zero() { + let mut erc = init_contract(); + let res = + erc.safe_batch_transfer_from(alice(), bob(), vec![], vec![], vec![]); + assert_eq!(res.unwrap_err(), Error::BatchTransferMismatch); + } + + #[ink::test] + fn operator_can_send_tokens() { + let mut erc = init_contract(); + + let owner = alice(); + let operator = bob(); + + set_sender(owner); + assert!(erc.set_approval_for_all(operator, true).is_ok()); + + set_sender(operator); + assert!(erc + .safe_transfer_from(owner, charlie(), 1, 5, vec![]) + .is_ok()); + assert_eq!(erc.balance_of(alice(), 1), 5); + assert_eq!(erc.balance_of(charlie(), 1), 5); + } + + #[ink::test] + fn approvals_work() { + let mut erc = init_contract(); + let owner = alice(); + let operator = bob(); + let another_operator = charlie(); + + // Note: All of these tests are from the context of the owner who is either allowing or + // disallowing an operator to control their funds. + set_sender(owner); + assert!(erc.is_approved_for_all(owner, operator) == false); + + assert!(erc.set_approval_for_all(operator, true).is_ok()); + assert!(erc.is_approved_for_all(owner, operator)); + + assert!(erc.set_approval_for_all(another_operator, true).is_ok()); + assert!(erc.is_approved_for_all(owner, another_operator)); + + assert!(erc.set_approval_for_all(operator, false).is_ok()); + assert!(erc.is_approved_for_all(owner, operator) == false); + } + + #[ink::test] + fn minting_tokens_works() { + let mut erc = Contract::new(); + + set_sender(alice()); + assert_eq!(erc.create(0), 1); + assert_eq!(erc.balance_of(alice(), 1), 0); + + assert!(erc.mint(1, 123).is_ok()); + assert_eq!(erc.balance_of(alice(), 1), 123); + } + + #[ink::test] + fn minting_not_allowed_for_nonexistent_tokens() { + let mut erc = Contract::new(); + + let res = erc.mint(1, 123); + assert_eq!(res.unwrap_err(), Error::UnexistentToken); + } + } +}