diff --git a/Cargo.lock b/Cargo.lock index 530bc38c..d2bf2ace 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Inflector" @@ -822,7 +822,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object 0.36.0", + "object 0.36.4", "rustc-demangle", ] @@ -7195,9 +7195,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.0" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 0730d05f..0c73e23f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,11 @@ license = "Unlicense" repository = "https://github.com/r0gue-io/pop-node/" [workspace] -exclude = [ "extension/contract", "pop-api", "tests/contracts" ] +exclude = [ + "extension/contract", + "pop-api", + "tests/contracts", +] members = [ "integration-tests", "node", diff --git a/pop-api/examples/.gitignore b/pop-api/examples/.gitignore old mode 100755 new mode 100644 diff --git a/pop-api/examples/balance-transfer/Cargo.toml b/pop-api/examples/balance-transfer/Cargo.toml deleted file mode 100755 index 2a12e532..00000000 --- a/pop-api/examples/balance-transfer/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -authors = [ "[your_name] <[your_email]>" ] -edition = "2021" -name = "balance_transfer" -version = "0.1.0" - -[dependencies] -ink = { version = "5.0.0", default-features = false } -pop-api = { path = "../../../pop-api", default-features = false } -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } -scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } - -[dev-dependencies] -ink_e2e = "5.0.0" - -[lib] -path = "lib.rs" - -[features] -default = [ "std" ] -e2e-tests = [ ] -ink-as-dependency = [ ] -std = [ - "ink/std", - "pop-api/std", - "scale-info/std", - "scale/std", -] diff --git a/pop-api/examples/balance-transfer/lib.rs b/pop-api/examples/balance-transfer/lib.rs deleted file mode 100755 index e75c15b9..00000000 --- a/pop-api/examples/balance-transfer/lib.rs +++ /dev/null @@ -1,135 +0,0 @@ -// DEPRECATED -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -use pop_api::balances::*; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum ContractError { - BalancesError(Error), -} - -impl From for ContractError { - fn from(value: Error) -> Self { - ContractError::BalancesError(value) - } -} - -#[ink::contract] -mod balance_transfer { - use super::*; - - #[ink(storage)] - #[derive(Default)] - pub struct BalanceTransfer; - - impl BalanceTransfer { - #[ink(constructor, payable)] - pub fn new() -> Self { - ink::env::debug_println!("BalanceTransfer::new"); - Default::default() - } - - #[ink(message)] - pub fn transfer( - &mut self, - receiver: AccountId, - value: Balance, - ) -> Result<(), ContractError> { - ink::env::debug_println!( - "BalanceTransfer::transfer: \nreceiver: {:?}, \nvalue: {:?}", - receiver, - value - ); - - transfer_keep_alive(receiver, value)?; - - ink::env::debug_println!("BalanceTransfer::transfer end"); - Ok(()) - } - } - - #[cfg(all(test, feature = "e2e-tests"))] - mod e2e_tests { - use super::*; - use ink_e2e::{ChainBackend, ContractsBackend}; - - use ink::{ - env::{test::default_accounts, DefaultEnvironment}, - primitives::AccountId, - }; - - type E2EResult = Result>; - - /// The base number of indivisible units for balances on the - /// `substrate-contracts-node`. - const UNIT: Balance = 1_000_000_000_000; - - /// The contract will be given 1000 tokens during instantiation. - const CONTRACT_BALANCE: Balance = 1_000 * UNIT; - - /// The receiver will get enough funds to have the required existential deposit. - /// - /// If your chain has this threshold higher, increase the transfer value. - const TRANSFER_VALUE: Balance = 1 / 10 * UNIT; - - /// An amount that is below the existential deposit, so that a transfer to an - /// empty account fails. - /// - /// Must not be zero, because such an operation would be a successful no-op. - const INSUFFICIENT_TRANSFER_VALUE: Balance = 1; - - /// Positive case scenario: - /// - the call is valid - /// - the call execution succeeds - #[ink_e2e::test] - async fn transfer_with_call_runtime_works( - mut client: Client, - ) -> E2EResult<()> { - // given - let mut constructor = RuntimeCallerRef::new(); - let contract = client - .instantiate("call-runtime", &ink_e2e::alice(), &mut constructor) - .value(CONTRACT_BALANCE) - .submit() - .await - .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); - - let accounts = default_accounts::(); - - let receiver: AccountId = accounts.bob; - - let sender_balance_before = client - .free_balance(accounts.alice) - .await - .expect("Failed to get account balance"); - let receiver_balance_before = - client.free_balance(receiver).await.expect("Failed to get account balance"); - - // when - let transfer_message = call_builder.transfer(receiver, TRANSFER_VALUE); - - let call_res = client - .call(&ink_e2e::alice(), &transfer_message) - .submit() - .await - .expect("call failed"); - - assert!(call_res.return_value().is_ok()); - - // then - let sender_balance_after = client - .free_balance(accounts.alice) - .await - .expect("Failed to get account balance"); - let receiver_balance_after = - client.free_balance(receiver).await.expect("Failed to get account balance"); - - assert_eq!(contract_balance_before, contract_balance_after + TRANSFER_VALUE); - assert_eq!(receiver_balance_before, receiver_balance_after - TRANSFER_VALUE); - - Ok(()) - } - } -} diff --git a/pop-api/examples/fungibles/Cargo.toml b/pop-api/examples/fungibles/Cargo.toml old mode 100755 new mode 100644 index 0b79e1b2..1882a879 --- a/pop-api/examples/fungibles/Cargo.toml +++ b/pop-api/examples/fungibles/Cargo.toml @@ -1,12 +1,25 @@ [package] -authors = [ "[your_name] <[your_email]>" ] +authors = [ "R0GUE " ] edition = "2021" name = "fungibles" version = "0.1.0" [dependencies] -ink = { version = "5.0.0", default-features = false } -pop-api = { path = "../../../pop-api", default-features = false, features = [ "fungibles" ] } +ink = { version = "=5.0.0", default-features = false, features = [ "ink-debug" ] } +pop-api = { path = "../../../pop-api", default-features = false, features = [ + "fungibles", +] } + +[dev-dependencies] +drink = { package = "pop-drink", git = "https://github.com/r0gue-io/pop-drink" } +env_logger = { version = "0.11.3" } +serde_json = "1.0.114" + +# TODO: due to compilation issues caused by `sp-runtime`, `frame-support-procedural` and `staging-xcm` this dependency +# (with specific version) has to be added. Will be tackled by #348, please ignore for now. +frame-support-procedural = { version = "=30.0.1", default-features = false } +sp-runtime = { version = "=38.0.0", default-features = false } +staging-xcm = { version = "=14.1.0", default-features = false } [lib] path = "lib.rs" diff --git a/pop-api/examples/fungibles/README.md b/pop-api/examples/fungibles/README.md new file mode 100644 index 00000000..6a9c229d --- /dev/null +++ b/pop-api/examples/fungibles/README.md @@ -0,0 +1,42 @@ +# PSP22 Fungible Token with Pop API + +This [ink!][ink] contract implements a [PSP22-compliant][psp22] fungible token by leveraging the [Pop API Fungibles][pop-api-fungibles]. Unlike typical token contracts, where the contract itself manages the token, tokens created by this contract are managed directly by Pop. This enables seamless integration and interoperability of the token across the Polkadot ecosystem and its applications. + +As the creator of the token, the contract has permissions to mint and burn tokens, but it can only transfer and approve tokens on its own behalf and requires explicit approval to transfer tokens for other accounts. Instead of users interacting with the contract to handle their token approvals, they interact primarily with Pop’s runtime. + +## Key benefits of using the Pop API + +- The token operates live on the Pop Network, beyond just within the contract. +- Simplify token management with high-level interfaces to significantly reduce contract size and complexity. + +[Learn more how Pop API works.](pop-api) + +## Use Cases + +This contract can serve a variety of purposes where owner-controlled token management is essential. Example use cases include: +- **DAO Token**: A DAO can use this contract to manage a governance token, with the DAO overseeing token issuance and removal based on governance decisions. +- **Staking and Rewards**: This contract supports minting tokens specifically for reward distribution. +- **Loyalty Programs**: Businesses or platforms can use this contract to issue loyalty points, with the owner managing token balances for users based on participation or purchases. + +## Test with Pop Drink + +Since this contract interacts directly with Pop’s runtime through the Pop API, it requires [Pop Drink](https://github.com/r0gue-io/pop-drink) for testing. See how the contract is tested in [tests](./tests.rs). + +## Potential Improvements + +- **Multiple owner management**: Instead of restricting ownership to a single `owner`, the contract could be designed to accommodate multiple owners. + +## Support + +Be part of our passionate community of Web3 builders. [Join our Telegram](https://t.me/onpopio)! + +Feel free to raise issues if anything is unclear, you have ideas or want to contribute to Pop! Examples using the fungibles API are always welcome! + +For any questions related to ink! you can also go to [Polkadot Stack Exchange](https://polkadot.stackexchange.com/) or +ask the [ink! community](https://t.me/inkathon/1). + +[ink]: https://use.ink +[psp22]: https://github.com/inkdevhub/standards/blob/master/PSPs/psp-22.md +[pop-api]: https://github.com/r0gue-io/pop-node/tree/main/pop-api/ +[pop-api-fungibles]: https://github.com/r0gue-io/pop-node/tree/main/pop-api/src/v0/fungibles +[pop-drink]: https://github.com/r0gue-io/pop-drink diff --git a/pop-api/examples/fungibles/lib.rs b/pop-api/examples/fungibles/lib.rs old mode 100755 new mode 100644 index 11eafe21..e77ee956 --- a/pop-api/examples/fungibles/lib.rs +++ b/pop-api/examples/fungibles/lib.rs @@ -1,108 +1,288 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] -use ink::prelude::vec::Vec; +use ink::prelude::{string::String, vec::Vec}; use pop_api::{ - fungibles::{self as api}, primitives::TokenId, - StatusCode, + v0::fungibles::{ + self as api, + events::{Approval, Created, Transfer}, + traits::{Psp22, Psp22Burnable, Psp22Metadata, Psp22Mintable}, + Psp22Error, + }, }; -pub type Result = core::result::Result; +#[cfg(test)] +mod tests; #[ink::contract] mod fungibles { use super::*; #[ink(storage)] - #[derive(Default)] - pub struct Fungibles; + pub struct Fungible { + id: TokenId, + owner: AccountId, + } - impl Fungibles { + impl Fungible { + /// Instantiate the contract and create a new token. The token identifier will be stored + /// in contract's storage. + /// + /// # Parameters + /// * - `id` - The identifier of the token. + /// * - `min_balance` - The minimum balance required for accounts holding this token. + // The `min_balance` ensures accounts hold a minimum amount of tokens, preventing tiny, + // inactive balances from bloating the blockchain state and slowing down the network. #[ink(constructor, payable)] - pub fn new() -> Self { - Default::default() + pub fn new(id: TokenId, min_balance: Balance) -> Result { + let instance = Self { id, owner: Self::env().caller() }; + let contract_id = instance.env().account_id(); + api::create(id, contract_id, min_balance).map_err(Psp22Error::from)?; + instance + .env() + .emit_event(Created { id, creator: contract_id, admin: contract_id }); + Ok(instance) } + } + impl Psp22 for Fungible { + /// Returns the total token supply. #[ink(message)] - pub fn total_supply(&self, token: TokenId) -> Result { - api::total_supply(token) + fn total_supply(&self) -> Balance { + api::total_supply(self.id).unwrap_or_default() } + /// Returns the account balance for the specified `owner`. + /// + /// # Parameters + /// - `owner` - The account whose balance is being queried. #[ink(message)] - pub fn balance_of(&self, token: TokenId, owner: AccountId) -> Result { - api::balance_of(token, owner) + fn balance_of(&self, owner: AccountId) -> Balance { + api::balance_of(self.id, owner).unwrap_or_default() } + /// Returns the allowance for a `spender` approved by an `owner`. + /// + /// # Parameters + /// - `owner` - The account that owns the tokens. + /// - `spender` - The account that is allowed to spend the tokens. #[ink(message)] - pub fn allowance( - &self, - token: TokenId, - owner: AccountId, - spender: AccountId, - ) -> Result { - api::allowance(token, owner, spender) + fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance { + api::allowance(self.id, owner, spender).unwrap_or_default() } + /// Transfers `value` amount of tokens from the contract to account `to` with + /// additional `data` in unspecified format. Contract must be pre-approved by `from`. + /// + /// # Parameters + /// - `to` - The recipient account. + /// - `value` - The number of tokens to transfer. + /// - `data` - Additional data in unspecified format. #[ink(message)] - pub fn transfer(&mut self, token: TokenId, to: AccountId, value: Balance) -> Result<()> { - api::transfer(token, to, value) + fn transfer( + &mut self, + to: AccountId, + value: Balance, + _data: Vec, + ) -> Result<(), Psp22Error> { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `to` is the same address or `value` is zero. + if contract == to || value == 0 { + return Ok(()); + } + api::transfer(self.id, to, value).map_err(Psp22Error::from)?; + self.env().emit_event(Transfer { from: Some(contract), to: Some(to), value }); + Ok(()) } + /// Transfers `value` tokens on behalf of `from` to the account `to` + /// with additional `data` in unspecified format. Contract must be pre-approved by `from`. + /// + /// # Parameters + /// - `from` - The account from which the token balance will be withdrawn. + /// - `to` - The recipient account. + /// - `value` - The number of tokens to transfer. + /// - `data` - Additional data with unspecified format. #[ink(message)] - pub fn transfer_from( + fn transfer_from( &mut self, - token: TokenId, from: AccountId, to: AccountId, value: Balance, _data: Vec, - ) -> Result<()> { - api::transfer_from(token, from, to, value) + ) -> Result<(), Psp22Error> { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if `from` and `to` is the same address or `value` is zero. + if from == to || value == 0 { + return Ok(()); + } + // A successful transfer reduces the allowance from `from` to the contract and triggers + // an `Approval` event with the updated allowance amount. + api::transfer_from(self.id, from, to, value).map_err(Psp22Error::from)?; + self.env().emit_event(Transfer { from: Some(contract), to: Some(to), value }); + self.env().emit_event(Approval { + owner: from, + spender: contract, + value: self.allowance(from, contract), + }); + Ok(()) } + /// Approves `spender` to spend `value` amount of tokens on behalf of the contract. + /// + /// Successive calls of this method overwrite previous values. + /// + /// # Parameters + /// - `spender` - The account that is allowed to spend the tokens. + /// - `value` - The number of tokens to approve. #[ink(message)] - pub fn approve( - &mut self, - token: TokenId, - spender: AccountId, - value: Balance, - ) -> Result<()> { - api::approve(token, spender, value) + fn approve(&mut self, spender: AccountId, value: Balance) -> Result<(), Psp22Error> { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `spender` is the same address. + if contract == spender { + return Ok(()); + } + api::approve(self.id, spender, value).map_err(Psp22Error::from)?; + self.env().emit_event(Approval { owner: contract, spender, value }); + Ok(()) } + /// Increases the allowance of `spender` by `value` amount of tokens. + /// + /// # Parameters + /// - `spender` - The account that is allowed to spend the tokens. + /// - `value` - The number of tokens to increase the allowance by. #[ink(message)] - pub fn increase_allowance( + fn increase_allowance( &mut self, - token: TokenId, spender: AccountId, value: Balance, - ) -> Result<()> { - api::increase_allowance(token, spender, value) + ) -> Result<(), Psp22Error> { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `spender` is the same address or `value` is zero. + if contract == spender || value == 0 { + return Ok(()); + } + api::increase_allowance(self.id, spender, value).map_err(Psp22Error::from)?; + let allowance = self.allowance(contract, spender); + self.env().emit_event(Approval { owner: contract, spender, value: allowance }); + Ok(()) } + /// Decreases the allowance of `spender` by `value` amount of tokens. + /// + /// # Parameters + /// - `spender` - The account that is allowed to spend the tokens. + /// - `value` - The number of tokens to decrease the allowance by. #[ink(message)] - pub fn decrease_allowance( + fn decrease_allowance( &mut self, - token: TokenId, spender: AccountId, value: Balance, - ) -> Result<()> { - api::decrease_allowance(token, spender, value) + ) -> Result<(), Psp22Error> { + self.ensure_owner()?; + let contract = self.env().account_id(); + + // No-op if the contract and `spender` is the same address or `value` is zero. + if contract == spender || value == 0 { + return Ok(()); + } + api::decrease_allowance(self.id, spender, value).map_err(Psp22Error::from)?; + let value = self.allowance(contract, spender); + self.env().emit_event(Approval { owner: contract, spender, value }); + Ok(()) + } + } + + impl Psp22Metadata for Fungible { + /// Returns the token name. + #[ink(message)] + fn token_name(&self) -> Option { + api::token_name(self.id) + .unwrap_or_default() + .and_then(|v| String::from_utf8(v).ok()) } + /// Returns the token symbol. #[ink(message)] - pub fn token_name(&self, token: TokenId) -> Result> { - api::token_name(token) + fn token_symbol(&self) -> Option { + api::token_symbol(self.id) + .unwrap_or_default() + .and_then(|v| String::from_utf8(v).ok()) } + /// Returns the token decimals. + #[ink(message)] + fn token_decimals(&self) -> u8 { + api::token_decimals(self.id).unwrap_or_default() + } + } + + impl Psp22Mintable for Fungible { + /// Creates `value` amount of tokens and assigns them to `account`, increasing the total + /// supply. + /// + /// # Parameters + /// - `account` - The account to be credited with the created tokens. + /// - `value` - The number of tokens to mint. #[ink(message)] - pub fn token_symbol(&self, token: TokenId) -> Result> { - api::token_symbol(token) + fn mint(&mut self, account: AccountId, value: Balance) -> Result<(), Psp22Error> { + self.ensure_owner()?; + // No-op if `value` is zero. + if value == 0 { + return Ok(()); + } + api::mint(self.id, account, value).map_err(Psp22Error::from)?; + self.env().emit_event(Transfer { from: None, to: Some(account), value }); + Ok(()) + } + } + + impl Psp22Burnable for Fungible { + /// Destroys `value` amount of tokens from `account`, reducing the total supply. + /// + /// # Parameters + /// - `account` - The account from which the tokens will be destroyed. + /// - `value` - The number of tokens to destroy. + #[ink(message)] + fn burn(&mut self, account: AccountId, value: Balance) -> Result<(), Psp22Error> { + self.ensure_owner()?; + // No-op if `value` is zero. + if value == 0 { + return Ok(()); + } + api::burn(self.id, account, value).map_err(Psp22Error::from)?; + self.env().emit_event(Transfer { from: Some(account), to: None, value }); + Ok(()) + } + } + + impl Fungible { + /// Check if the caller is the owner of the contract. + fn ensure_owner(&self) -> Result<(), Psp22Error> { + if self.owner != self.env().caller() { + return Err(Psp22Error::Custom(String::from("Not the owner"))); + } + Ok(()) } + /// Transfer the ownership of the contract to another account. + /// + /// # Parameters + /// - `owner` - New owner account. #[ink(message)] - pub fn token_decimals(&self, token: TokenId) -> Result { - api::token_decimals(token) + pub fn transfer_ownership(&mut self, owner: AccountId) -> Result<(), Psp22Error> { + self.ensure_owner()?; + self.owner = owner; + Ok(()) } } } diff --git a/pop-api/examples/fungibles/tests.rs b/pop-api/examples/fungibles/tests.rs new file mode 100644 index 00000000..fee1204e --- /dev/null +++ b/pop-api/examples/fungibles/tests.rs @@ -0,0 +1,771 @@ +use drink::{ + assert_err, assert_last_contract_event, assert_ok, call, + devnet::{ + account_id_from_slice, + error::{ + v0::{ApiError::*, ArithmeticError::*, Error}, + Assets, + AssetsError::*, + }, + AccountId, Balance, Runtime, + }, + last_contract_event, + session::Session, + AssetsAPI, TestExternalities, NO_SALT, +}; +use ink::scale::Encode; +use pop_api::{ + primitives::TokenId, + v0::fungibles::events::{Approval, Created, Transfer}, +}; + +use super::*; + +const UNIT: Balance = 10_000_000_000; +const INIT_AMOUNT: Balance = 100_000_000 * UNIT; +const INIT_VALUE: Balance = 100 * UNIT; +const ALICE: AccountId = AccountId::new([1u8; 32]); +const BOB: AccountId = AccountId::new([2_u8; 32]); +const CHARLIE: AccountId = AccountId::new([3_u8; 32]); +const AMOUNT: Balance = MIN_BALANCE * 4; +const MIN_BALANCE: Balance = 10_000; +const TOKEN: TokenId = 1; + +// The contract bundle provider. +// +// See https://github.com/r0gue-io/pop-drink/blob/main/crates/drink/drink/test-macro/src/lib.rs for more information. +#[drink::contract_bundle_provider] +enum BundleProvider {} + +/// Sandbox environment for Pop Devnet Runtime. +pub struct Pop { + ext: TestExternalities, +} + +impl Default for Pop { + fn default() -> Self { + // Initialising genesis state, providing accounts with an initial balance. + let balances: Vec<(AccountId, u128)> = + vec![(ALICE, INIT_AMOUNT), (BOB, INIT_AMOUNT), (CHARLIE, INIT_AMOUNT)]; + let ext = BlockBuilder::::new_ext(balances); + Self { ext } + } +} + +// Implement core functionalities for the `Pop` sandbox. +drink::impl_sandbox!(Pop, Runtime, ALICE); + +// Deployment and constructor method tests. + +fn deploy_with_default(session: &mut Session) -> Result { + deploy(session, "new", vec![TOKEN.to_string(), MIN_BALANCE.to_string()]) +} + +#[drink::test(sandbox = Pop)] +fn new_constructor_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + // Token exists after the deployment. + assert!(session.sandbox().asset_exists(&TOKEN)); + // Successfully emit event. + assert_last_contract_event!( + &session, + Created { + id: TOKEN, + creator: account_id_from_slice(&contract), + admin: account_id_from_slice(&contract), + } + ); +} + +// PSP-22 tests. + +#[drink::test(sandbox = Pop)] +fn total_supply_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + assert_ok!(deploy_with_default(&mut session)); + // No tokens in circulation. + assert_eq!(total_supply(&mut session), 0); + assert_eq!(total_supply(&mut session), session.sandbox().total_supply(&TOKEN)); + // Tokens in circulation. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_eq!(total_supply(&mut session), AMOUNT); + assert_eq!(total_supply(&mut session), session.sandbox().total_supply(&TOKEN)); +} + +#[drink::test(sandbox = Pop)] +fn balance_of_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + assert_ok!(deploy_with_default(&mut session)); + // No tokens in circulation. + assert_eq!(balance_of(&mut session, ALICE), 0); + assert_eq!(balance_of(&mut session, ALICE), session.sandbox().balance_of(&TOKEN, &ALICE)); + // Tokens in circulation. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_eq!(balance_of(&mut session, ALICE), AMOUNT); + assert_eq!(balance_of(&mut session, ALICE), session.sandbox().balance_of(&TOKEN, &ALICE)); +} + +#[drink::test(sandbox = Pop)] +fn allowance_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + // No tokens in circulation. + assert_eq!(allowance(&mut session, contract.clone(), ALICE), 0); + // Tokens in circulation. + assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &ALICE, AMOUNT / 2)); + assert_eq!(allowance(&mut session, contract, ALICE), AMOUNT / 2); +} + +#[drink::test(sandbox = Pop)] +fn transfer_fails_with_no_account() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // `pallet-assets` returns `NoAccount` error. + assert_ok!(session + .sandbox() + .approve(&TOKEN, &contract.clone(), &contract.clone(), AMOUNT * 2)); + assert_err!(transfer(&mut session, ALICE, AMOUNT), Error::Module(Assets(NoAccount))); +} + +#[drink::test(sandbox = Pop)] +fn transfer_noop_works(&mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if `value` is zero, returns success and no events are emitted. + assert_ok!(transfer(&mut session, ALICE, 0)); + assert_eq!(last_contract_event(&session), None); + // No-op if the caller and `to` is the same address, returns success and no events are emitted. + assert_ok!(transfer(&mut session, contract.clone(), AMOUNT)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn transfer_fails_with_insufficient_balance() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + // Failed with `InsufficientBalance`. + assert_ok!(session + .sandbox() + .approve(&TOKEN, &contract.clone(), &contract.clone(), AMOUNT * 2)); + assert_eq!(transfer(&mut session, BOB, AMOUNT + 1), Err(Psp22Error::InsufficientBalance)); +} + +#[drink::test(sandbox = Pop)] +fn transfer_fails_with_token_not_live() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!(transfer(&mut session, BOB, AMOUNT / 2), Error::Module(Assets(AssetNotLive))); +} + +#[drink::test(sandbox = Pop)] +fn transfer_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = AMOUNT / 4; + // Mint tokens. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + // Successfully transfer. + assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &contract.clone(), AMOUNT)); + assert_ok!(transfer(&mut session, BOB, value)); + assert_eq!(session.sandbox().balance_of(&TOKEN, &contract), AMOUNT - value); + assert_eq!(session.sandbox().balance_of(&TOKEN, &BOB), value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Transfer { + from: Some(account_id_from_slice(&contract)), + to: Some(account_id_from_slice(&BOB)), + value, + } + ); +} + +#[drink::test(sandbox = Pop)] +fn transfer_from_noop_works() { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if `value` is zero, returns success and no events are emitted. + assert_ok!(transfer_from(&mut session, ALICE, BOB, 0)); + assert_eq!(last_contract_event(&session), None); + // No-op if the `from` and `to` is the same address, returns success and no events are emitted. + assert_ok!(transfer_from(&mut session, ALICE, ALICE, AMOUNT)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn transfer_from_fails_with_insufficient_balance() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &ALICE, &contract.clone(), AMOUNT * 2)); + // Not enough balance. Failed with `InsufficientBalance`. + assert_eq!( + transfer_from(&mut session, ALICE, contract.clone(), AMOUNT + 1), + Err(Psp22Error::InsufficientBalance) + ); +} + +#[drink::test(sandbox = Pop)] +fn transfer_from_fails_with_insufficient_allowance() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &ALICE, &contract.clone(), AMOUNT)); + // Unapproved transfer. Failed with `InsufficientAllowance`. + assert_eq!( + transfer_from(&mut session, ALICE, contract.clone(), AMOUNT + 1), + Err(Psp22Error::InsufficientAllowance) + ); +} + +#[drink::test(sandbox = Pop)] +fn transfer_from_fails_with_token_not_live() { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &ALICE, &contract.clone(), AMOUNT)); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!( + transfer_from(&mut session, ALICE, BOB, AMOUNT / 2), + Error::Module(Assets(AssetNotLive)) + ); +} + +#[drink::test(sandbox = Pop)] +fn transfer_from_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = AMOUNT / 2; + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &ALICE, &contract.clone(), AMOUNT)); + // Successful transfer. + assert_ok!(transfer_from(&mut session, ALICE, BOB, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &ALICE, &contract.clone()), value); + assert_eq!(session.sandbox().balance_of(&TOKEN, &ALICE), value); + assert_eq!(session.sandbox().balance_of(&TOKEN, &BOB), value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&ALICE), + spender: account_id_from_slice(&contract), + value, + } + ); +} + +#[drink::test(sandbox = Pop)] +fn approve_noop_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if the caller and `spender` is the same address, returns success and no events are + // emitted. + assert_ok!(approve(&mut session, contract.clone(), AMOUNT)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn approve_fails_with_token_not_live(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!(approve(&mut session, ALICE, AMOUNT), Error::Module(Assets(AssetNotLive))); +} + +#[drink::test(sandbox = Pop)] +fn approve_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = AMOUNT / 2; + // Mint tokens. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + // Successfully approve. + assert_ok!(approve(&mut session, BOB, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &BOB), value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&BOB), + value, + } + ); + // Non-additive, sets new value. + assert_ok!(approve(&mut session, ALICE, value - 1)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &ALICE), value - 1); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&ALICE), + value: value - 1, + } + ); +} + +#[drink::test(sandbox = Pop)] +fn increase_allowance_noop_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if the caller and `spender` is the same address, returns success and no events are + // emitted. + assert_ok!(increase_allowance(&mut session, contract.clone(), AMOUNT)); + assert_eq!(last_contract_event(&session), None); + // No-op if the `value` is zero, returns success and no events are emitted. + assert_ok!(increase_allowance(&mut session, contract.clone(), 0)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn increase_allowance_fails_with_token_not_live(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!( + increase_allowance(&mut session, ALICE, AMOUNT), + Error::Module(Assets(AssetNotLive)) + ); +} + +#[drink::test(sandbox = Pop)] +fn increase_allowance_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = AMOUNT / 2; + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &ALICE, AMOUNT)); + // Successfully approve. + assert_ok!(increase_allowance(&mut session, ALICE, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &ALICE), AMOUNT + value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&ALICE), + value: AMOUNT + value, + } + ); + // Additive. + assert_ok!(increase_allowance(&mut session, ALICE, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &ALICE), AMOUNT + value * 2); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&ALICE), + value: AMOUNT + value * 2, + } + ); +} + +#[drink::test(sandbox = Pop)] +fn decrease_allowance_noop_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if the caller and `spender` is the same address, returns success and no events are + // emitted. + assert_ok!(decrease_allowance(&mut session, contract.clone(), AMOUNT)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn decrease_allowance_fails_with_insufficient_allowance(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Failed with `InsufficientAllowance`. + assert_eq!( + decrease_allowance(&mut session, ALICE, AMOUNT), + Err(Psp22Error::InsufficientAllowance) + ); +} + +#[drink::test(sandbox = Pop)] +fn decrease_allowance_fails_with_token_not_live(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &ALICE, AMOUNT)); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!( + decrease_allowance(&mut session, ALICE, AMOUNT), + Error::Module(Assets(AssetNotLive)) + ); +} + +#[drink::test(sandbox = Pop)] +fn decrease_allowance_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = 1; + // Mint tokens and approve. + assert_ok!(session.sandbox().mint_into(&TOKEN, &contract.clone(), AMOUNT)); + assert_ok!(session.sandbox().approve(&TOKEN, &contract.clone(), &ALICE, AMOUNT)); + // Successfully approve. + assert_ok!(decrease_allowance(&mut session, ALICE, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &ALICE), AMOUNT - value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&ALICE), + value: AMOUNT - value, + } + ); + // Additive. + assert_ok!(decrease_allowance(&mut session, ALICE, value)); + assert_eq!(session.sandbox().allowance(&TOKEN, &contract, &ALICE), AMOUNT - value * 2); + // Successfully emit event. + assert_last_contract_event!( + &session, + Approval { + owner: account_id_from_slice(&contract), + spender: account_id_from_slice(&ALICE), + value: AMOUNT - value * 2, + } + ); +} + +// PSP-22 Metadata tests. + +#[drink::test(sandbox = Pop)] +fn token_metadata(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_default(&mut session).unwrap(); + session.set_actor(contract.clone()); + let name: String = String::from("Paseo Token"); + let symbol: String = String::from("PAS"); + let decimals: u8 = 69; + // Token does not exist. + assert_eq!(token_name(&mut session), None); + assert_eq!(token_symbol(&mut session), None); + assert_eq!(token_decimals(&mut session), 0); + // Set token metadata. + let actor = session.get_actor(); + assert_ok!(session.sandbox().set_metadata( + Some(actor), + &TOKEN, + name.clone().into(), + symbol.clone().into(), + decimals + )); + assert_eq!(token_name(&mut session), Some(name)); + assert_eq!(token_symbol(&mut session), Some(symbol)); + assert_eq!(token_decimals(&mut session), decimals); +} + +// PSP-22 Mintable & Burnable tests. + +#[drink::test(sandbox = Pop)] +fn mint_noop_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if minted value is zero, returns success and no events are emitted. + assert_ok!(mint(&mut session, ALICE, 0)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn mint_fails_with_arithmetic_overflow(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + assert_ok!(mint(&mut session, ALICE, AMOUNT)); + // Total supply increased by `value` exceeds maximal value of `u128` type. + assert_err!(mint(&mut session, ALICE, u128::MAX), Error::Raw(Arithmetic(Overflow))); +} + +#[drink::test(sandbox = Pop)] +fn mint_fails_with_token_not_live(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `AssetNotLive` error. + assert_err!(mint(&mut session, ALICE, AMOUNT), Error::Module(Assets(AssetNotLive))); +} + +#[drink::test(sandbox = Pop)] +fn mint_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = AMOUNT; + // Successfully mint tokens. + assert_ok!(mint(&mut session, ALICE, value)); + assert_eq!(session.sandbox().total_supply(&TOKEN), value); + assert_eq!(session.sandbox().balance_of(&TOKEN, &ALICE), value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Transfer { from: None, to: Some(account_id_from_slice(&ALICE)), value } + ); +} + +#[drink::test(sandbox = Pop)] +fn burn_noop_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // No-op if burned value is zero, returns success and no events are emitted. + assert_ok!(burn(&mut session, ALICE, 0)); + assert_eq!(last_contract_event(&session), None); +} + +#[drink::test(sandbox = Pop)] +fn burn_fails_with_insufficient_balance(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + // Failed with `InsufficientBalance`. + assert_eq!(burn(&mut session, ALICE, AMOUNT), Err(Psp22Error::InsufficientBalance)); +} + +#[drink::test(sandbox = Pop)] +fn burn_fails_with_token_not_live(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + // Token is not live, i.e. frozen or being destroyed. + assert_ok!(session.sandbox().start_destroy(&TOKEN)); + // `pallet-assets` returns `IncorrectStatus` error. + assert_err!(burn(&mut session, ALICE, AMOUNT), Error::Module(Assets(IncorrectStatus))); +} + +#[drink::test(sandbox = Pop)] +fn burn_works(mut session: Session) { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_default(&mut session).unwrap(); + session.set_actor(ALICE); + let value = 1; + // Mint tokens. + assert_ok!(session.sandbox().mint_into(&TOKEN, &ALICE, AMOUNT)); + // Successfully burn tokens. + assert_ok!(burn(&mut session, ALICE, value)); + assert_eq!(session.sandbox().total_supply(&TOKEN), AMOUNT - value); + assert_eq!(session.sandbox().balance_of(&TOKEN, &ALICE), AMOUNT - value); + // Successfully emit event. + assert_last_contract_event!( + &session, + Transfer { from: Some(account_id_from_slice(&ALICE)), to: None, value } + ); +} + +// Deploy the contract with `NO_SALT and `INIT_VALUE`. +fn deploy( + session: &mut Session, + method: &str, + input: Vec, +) -> Result { + drink::deploy::( + session, + // The local contract (i.e. `fungibles`). + BundleProvider::local().unwrap(), + method, + input, + NO_SALT, + Some(INIT_VALUE), + ) +} + +// A set of helper methods to test the contract calls. + +fn total_supply(session: &mut Session) -> Balance { + call::(session, "Psp22::total_supply", vec![], None).unwrap() +} + +fn balance_of(session: &mut Session, owner: AccountId) -> Balance { + call::(session, "Psp22::balance_of", vec![owner.to_string()], None) + .unwrap() +} + +fn allowance(session: &mut Session, owner: AccountId, spender: AccountId) -> Balance { + call::( + session, + "Psp22::allowance", + vec![owner.to_string(), spender.to_string()], + None, + ) + .unwrap() +} + +fn transfer(session: &mut Session, to: AccountId, amount: Balance) -> Result<(), Psp22Error> { + call::( + session, + "Psp22::transfer", + vec![to.to_string(), amount.to_string(), serde_json::to_string::<[u8; 0]>(&[]).unwrap()], + None, + ) +} + +fn transfer_from( + session: &mut Session, + from: AccountId, + to: AccountId, + amount: Balance, +) -> Result<(), Psp22Error> { + call::( + session, + "Psp22::transfer_from", + vec![ + from.to_string(), + to.to_string(), + amount.to_string(), + serde_json::to_string::<[u8; 0]>(&[]).unwrap(), + ], + None, + ) +} + +fn approve( + session: &mut Session, + spender: AccountId, + value: Balance, +) -> Result<(), Psp22Error> { + call::( + session, + "Psp22::approve", + vec![spender.to_string(), value.to_string()], + None, + ) +} + +fn increase_allowance( + session: &mut Session, + spender: AccountId, + value: Balance, +) -> Result<(), Psp22Error> { + call::( + session, + "Psp22::increase_allowance", + vec![spender.to_string(), value.to_string()], + None, + ) +} + +fn decrease_allowance( + session: &mut Session, + spender: AccountId, + value: Balance, +) -> Result<(), Psp22Error> { + call::( + session, + "Psp22::decrease_allowance", + vec![spender.to_string(), value.to_string()], + None, + ) +} + +fn token_name(session: &mut Session) -> Option { + call::, Psp22Error>(session, "Psp22Metadata::token_name", vec![], None) + .unwrap() +} + +fn token_symbol(session: &mut Session) -> Option { + call::, Psp22Error>(session, "Psp22Metadata::token_symbol", vec![], None) + .unwrap() +} + +fn token_decimals(session: &mut Session) -> u8 { + call::(session, "Psp22Metadata::token_decimals", vec![], None).unwrap() +} + +fn mint(session: &mut Session, account: AccountId, amount: Balance) -> Result<(), Psp22Error> { + call::( + session, + "Psp22Mintable::mint", + vec![account.to_string(), amount.to_string()], + None, + ) +} + +fn burn(session: &mut Session, account: AccountId, amount: Balance) -> Result<(), Psp22Error> { + call::( + session, + "Psp22Burnable::burn", + vec![account.to_string(), amount.to_string()], + None, + ) +} diff --git a/pop-api/examples/nfts/Cargo.toml b/pop-api/examples/nfts/Cargo.toml deleted file mode 100755 index ef50b7ec..00000000 --- a/pop-api/examples/nfts/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -authors = [ "[your_name] <[your_email]>" ] -edition = "2021" -name = "nfts" -version = "0.1.0" - -[dependencies] -ink = { version = "5.0.0", default-features = false } -pop-api = { path = "../../../pop-api", default-features = false } -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } -scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } - -[lib] -path = "lib.rs" - -[features] -default = [ "std" ] -e2e-tests = [ ] -ink-as-dependency = [ ] -std = [ - "ink/std", - "pop-api/std", - "scale-info/std", - "scale/std", -] diff --git a/pop-api/examples/nfts/lib.rs b/pop-api/examples/nfts/lib.rs deleted file mode 100755 index 0cd0f313..00000000 --- a/pop-api/examples/nfts/lib.rs +++ /dev/null @@ -1,117 +0,0 @@ -// DEPRECATED -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -use pop_api::nfts::*; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum ContractError { - InvalidCollection, - ItemAlreadyExists, - NftsError(Error), - NotOwner, -} - -impl From for ContractError { - fn from(value: Error) -> Self { - ContractError::NftsError(value) - } -} - -#[ink::contract] -mod nfts { - use super::*; - - #[ink(storage)] - #[derive(Default)] - pub struct Nfts; - - impl Nfts { - #[ink(constructor, payable)] - pub fn new() -> Self { - ink::env::debug_println!("Nfts::new"); - Default::default() - } - - #[ink(message)] - pub fn create_nft_collection(&self) -> Result<(), ContractError> { - ink::env::debug_println!("Nfts::create_nft_collection: collection creation started."); - let admin = Self::env().caller(); - let item_settings = ItemSettings(BitFlags::from(ItemSetting::Transferable)); - - let mint_settings = MintSettings { - mint_type: MintType::Issuer, - price: Some(0), - start_block: Some(0), - end_block: Some(0), - default_item_settings: item_settings, - }; - - let config = CollectionConfig { - settings: CollectionSettings(BitFlags::from(CollectionSetting::TransferableItems)), - max_supply: None, - mint_settings, - }; - pop_api::nfts::create(admin, config)?; - ink::env::debug_println!( - "Nfts::create_nft_collection: collection created successfully." - ); - Ok(()) - } - - #[ink(message)] - pub fn mint_nft( - &mut self, - collection_id: u32, - item_id: u32, - receiver: AccountId, - ) -> Result<(), ContractError> { - ink::env::debug_println!( - "Nfts::mint: collection_id: {:?} item_id {:?} receiver: {:?}", - collection_id, - item_id, - receiver - ); - - // Check if item already exists (demo purposes only, unnecessary as would expect check in mint call) - if item(collection_id, item_id)?.is_some() { - return Err(ContractError::ItemAlreadyExists); - } - - // mint api - mint(collection_id, item_id, receiver)?; - ink::env::debug_println!("Nfts::mint: item minted successfully"); - - // check owner - match owner(collection_id, item_id)? { - Some(owner) if owner == receiver => { - ink::env::debug_println!("Nfts::mint success: minted item belongs to receiver"); - }, - _ => { - return Err(ContractError::NotOwner); - }, - } - - ink::env::debug_println!("Nfts::mint end"); - Ok(()) - } - - #[ink(message)] - pub fn read_collection(&self, collection_id: u32) -> Result<(), ContractError> { - ink::env::debug_println!("Nfts::read_collection: collection_id: {:?}", collection_id); - let collection = pop_api::nfts::collection(collection_id)?; - ink::env::debug_println!("Nfts::read_collection: collection: {:?}", collection); - Ok(()) - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn default_works() { - Nfts::new(); - } - } -} diff --git a/pop-api/examples/place-spot-order/Cargo.toml b/pop-api/examples/place-spot-order/Cargo.toml deleted file mode 100755 index f523bea7..00000000 --- a/pop-api/examples/place-spot-order/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -authors = [ "[your_name] <[your_email]>" ] -edition = "2021" -name = "spot_order" -version = "0.1.0" - -[dependencies] -ink = { version = "5.0.0", default-features = false } -pop-api = { path = "../../../pop-api", default-features = false } -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } -scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } - -[lib] -path = "lib.rs" - -[features] -default = [ "std" ] -e2e-tests = [ ] -ink-as-dependency = [ ] -std = [ - "ink/std", - "pop-api/std", - "scale-info/std", - "scale/std", -] diff --git a/pop-api/examples/place-spot-order/lib.rs b/pop-api/examples/place-spot-order/lib.rs deleted file mode 100755 index 965917d1..00000000 --- a/pop-api/examples/place-spot-order/lib.rs +++ /dev/null @@ -1,43 +0,0 @@ -// DEPRECATED -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -mod spot_order { - - #[ink(storage)] - #[derive(Default)] - pub struct SpotOrder; - - impl SpotOrder { - #[ink(constructor, payable)] - pub fn new() -> Self { - ink::env::debug_println!("SpotOrder::new"); - Default::default() - } - - #[ink(message)] - pub fn place_spot_order(&mut self, max_amount: Balance, para_id: u32) { - ink::env::debug_println!( - "SpotOrder::place_spot_order: max_amount {:?} para_id: {:?} ", - max_amount, - para_id, - ); - - #[allow(unused_variables)] - let res = pop_api::cross_chain::coretime::place_spot_order(max_amount, para_id); - ink::env::debug_println!("SpotOrder::place_spot_order: res {:?} ", res,); - - ink::env::debug_println!("SpotOrder::place_spot_order end"); - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[ink::test] - fn default_works() { - SpotOrder::new(); - } - } -} diff --git a/pop-api/examples/read-runtime-state/Cargo.toml b/pop-api/examples/read-runtime-state/Cargo.toml deleted file mode 100755 index f5464730..00000000 --- a/pop-api/examples/read-runtime-state/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -authors = [ "[your_name] <[your_email]>" ] -edition = "2021" -name = "read_relay_blocknumber" -version = "0.1.0" - -[dependencies] -ink = { version = "5.0.0", default-features = false } -pop-api = { path = "../../../pop-api", default-features = false } -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = [ "derive" ] } -scale-info = { version = "2.6", default-features = false, features = [ "derive" ], optional = true } - -[lib] -path = "lib.rs" - -[features] -default = [ "std" ] -e2e-tests = [ ] -ink-as-dependency = [ ] -std = [ - "ink/std", - "pop-api/std", - "scale-info/std", - "scale/std", -] diff --git a/pop-api/examples/read-runtime-state/lib.rs b/pop-api/examples/read-runtime-state/lib.rs deleted file mode 100755 index 092e9f2f..00000000 --- a/pop-api/examples/read-runtime-state/lib.rs +++ /dev/null @@ -1,36 +0,0 @@ -// DEPRECATED -#![cfg_attr(not(feature = "std"), no_std, no_main)] - -#[ink::contract] -mod read_relay_blocknumber { - use pop_api::primitives::storage_keys::{ - ParachainSystemKeys::LastRelayChainBlockNumber, RuntimeStateKeys::ParachainSystem, - }; - - #[ink(event)] - pub struct RelayBlockNumberRead { - value: BlockNumber, - } - - #[ink(storage)] - #[derive(Default)] - pub struct ReadRelayBlockNumber; - - impl ReadRelayBlockNumber { - #[ink(constructor, payable)] - pub fn new() -> Self { - ink::env::debug_println!("ReadRelayBlockNumber::new"); - Default::default() - } - - #[ink(message)] - pub fn read_relay_block_number(&self) { - let result = - pop_api::state::read::(ParachainSystem(LastRelayChainBlockNumber)); - ink::env::debug_println!("Last relay block number read by contract: {:?}", result); - self.env().emit_event(RelayBlockNumberRead { - value: result.expect("Failed to read relay block number."), - }); - } - } -} diff --git a/pop-api/src/lib.rs b/pop-api/src/lib.rs index 546106e5..b9dbf634 100644 --- a/pop-api/src/lib.rs +++ b/pop-api/src/lib.rs @@ -62,6 +62,8 @@ impl From for StatusCode { mod constants { // Error. pub(crate) const DECODING_FAILED: u32 = 255; + // Runtime Errors. + pub(crate) const MODULE_ERROR: u8 = 3; // Function IDs. pub(crate) const DISPATCH: u8 = 0; diff --git a/pop-api/src/v0/fungibles/errors.rs b/pop-api/src/v0/fungibles/errors.rs index a5c548a6..3080a2c4 100644 --- a/pop-api/src/v0/fungibles/errors.rs +++ b/pop-api/src/v0/fungibles/errors.rs @@ -1,7 +1,12 @@ -//! A set of errors for use in smart contracts that interact with the fungibles api. This includes errors compliant to standards. +//! A set of errors for use in smart contracts that interact with the fungibles api. This includes +//! errors compliant to standards. + +use ink::{ + prelude::string::{String, ToString}, + scale::{Decode, Encode}, +}; use super::*; -use ink::prelude::string::{String, ToString}; /// Represents various errors related to fungible tokens. /// @@ -70,7 +75,7 @@ impl From for FungiblesError { // TODO: Issue https://github.com/r0gue-io/pop-node/issues/298 #[derive(Debug, PartialEq, Eq)] #[ink::scale_derive(Encode, Decode, TypeInfo)] -pub enum PSP22Error { +pub enum Psp22Error { /// Custom error type for implementation-based errors. Custom(String), /// Returned when an account does not have enough tokens to complete the operation. @@ -85,25 +90,36 @@ pub enum PSP22Error { SafeTransferCheckFailed(String), } -impl From for PSP22Error { - /// Converts a `StatusCode` to a `PSP22Error`. +#[cfg(feature = "std")] +impl From for u32 { + fn from(value: Psp22Error) -> u32 { + match value { + Psp22Error::InsufficientBalance => u32::from_le_bytes([MODULE_ERROR, ASSETS, 0, 0]), + Psp22Error::InsufficientAllowance => u32::from_le_bytes([MODULE_ERROR, ASSETS, 10, 0]), + Psp22Error::Custom(value) => value.parse::().expect("Failed to parse"), + _ => unimplemented!("Variant is not supported"), + } + } +} + +impl From for Psp22Error { + /// Converts a `StatusCode` to a `Psp22Error`. fn from(value: StatusCode) -> Self { let encoded = value.0.to_le_bytes(); match encoded { // BalanceLow. - [_, ASSETS, 0, _] => PSP22Error::InsufficientBalance, + [MODULE_ERROR, ASSETS, 0, _] => Psp22Error::InsufficientBalance, // Unapproved. - [_, ASSETS, 10, _] => PSP22Error::InsufficientAllowance, - // Unknown. - [_, ASSETS, 3, _] => PSP22Error::Custom(String::from("Unknown")), - _ => PSP22Error::Custom(value.0.to_string()), + [MODULE_ERROR, ASSETS, 10, _] => Psp22Error::InsufficientAllowance, + // Custom error with status code. + _ => Psp22Error::Custom(value.0.to_string()), } } } #[cfg(test)] mod tests { - use super::{FungiblesError, PSP22Error}; + use super::*; use crate::{ constants::{ASSETS, BALANCES}, primitives::{ @@ -114,8 +130,6 @@ mod tests { }, StatusCode, }; - use ink::prelude::string::String; - use ink::scale::{Decode, Encode}; fn error_into_status_code(error: Error) -> StatusCode { let mut encoded_error = error.encode(); @@ -227,21 +241,23 @@ mod tests { ]; for error in other_errors { let status_code: StatusCode = error_into_status_code(error); - let fungibles_error: PSP22Error = status_code.into(); - assert_eq!(fungibles_error, PSP22Error::Custom(status_code.0.to_string())) + let fungibles_error: Psp22Error = status_code.into(); + assert_eq!(fungibles_error, Psp22Error::Custom(status_code.0.to_string())) } assert_eq!( - into_error::(Module { index: ASSETS, error: [0, 0] }), - PSP22Error::InsufficientBalance + into_error::(Module { index: ASSETS, error: [0, 0] }), + Psp22Error::InsufficientBalance ); assert_eq!( - into_error::(Module { index: ASSETS, error: [10, 0] }), - PSP22Error::InsufficientAllowance + into_error::(Module { index: ASSETS, error: [10, 0] }), + Psp22Error::InsufficientAllowance ); assert_eq!( - into_error::(Module { index: ASSETS, error: [3, 0] }), - PSP22Error::Custom(String::from("Unknown")) + into_error::(Module { index: ASSETS, error: [3, 0] }), + Psp22Error::Custom( + error_into_status_code(Module { index: ASSETS, error: [3, 0] }).0.to_string() + ) ); } } diff --git a/pop-api/src/v0/fungibles/events.rs b/pop-api/src/v0/fungibles/events.rs index 130ead65..af454077 100644 --- a/pop-api/src/v0/fungibles/events.rs +++ b/pop-api/src/v0/fungibles/events.rs @@ -13,6 +13,7 @@ use super::*; /// Event emitted when allowance by `owner` to `spender` changes. // Differing style: event name abides by the PSP22 standard. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct Approval { /// The owner providing the allowance. #[ink(topic)] @@ -27,6 +28,7 @@ pub struct Approval { /// Event emitted when transfer of tokens occurs. // Differing style: event name abides by the PSP22 standard. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct Transfer { /// The source of the transfer. `None` when minting. #[ink(topic)] @@ -40,6 +42,7 @@ pub struct Transfer { /// Event emitted when a token is created. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct Created { /// The token identifier. #[ink(topic)] @@ -54,6 +57,7 @@ pub struct Created { /// Event emitted when a token is in the process of being destroyed. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct DestroyStarted { /// The token. #[ink(topic)] @@ -62,6 +66,7 @@ pub struct DestroyStarted { /// Event emitted when new metadata is set for a token. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct MetadataSet { /// The token. #[ink(topic)] @@ -78,6 +83,7 @@ pub struct MetadataSet { /// Event emitted when metadata is cleared for a token. #[ink::event] +#[cfg_attr(feature = "std", derive(Debug))] pub struct MetadataCleared { /// The token. #[ink(topic)] diff --git a/pop-api/src/v0/fungibles/mod.rs b/pop-api/src/v0/fungibles/mod.rs index 6fc1e9e5..1e147982 100644 --- a/pop-api/src/v0/fungibles/mod.rs +++ b/pop-api/src/v0/fungibles/mod.rs @@ -15,7 +15,7 @@ pub use metadata::*; pub use traits::*; use crate::{ - constants::{ASSETS, BALANCES, FUNGIBLES}, + constants::{ASSETS, BALANCES, FUNGIBLES, MODULE_ERROR}, primitives::{AccountId, Balance, TokenId}, ChainExtensionMethodApi, Result, StatusCode, }; diff --git a/pop-api/src/v0/fungibles/traits.rs b/pop-api/src/v0/fungibles/traits.rs index 92ad55e3..e6252233 100644 --- a/pop-api/src/v0/fungibles/traits.rs +++ b/pop-api/src/v0/fungibles/traits.rs @@ -1,21 +1,27 @@ //! Traits that can be used by contracts. Including standard compliant traits. -use super::*; use core::result::Result; + use ink::prelude::string::String; +use super::*; + +/// Function selectors as per the PSP22 standard: https://github.com/inkdevhub/standards/blob/master/PSPs/psp-22.md. +/// The mint and burn selectors are not defined in the standard, but have been created in the same +/// way. + /// The PSP22 trait. #[ink::trait_definition] pub trait Psp22 { /// Returns the total token supply. - #[ink(message)] + #[ink(message, selector = 0x162df8c2)] fn total_supply(&self) -> Balance; /// Returns the account balance for the specified `owner`. /// /// # Parameters /// - `owner` - The account whose balance is being queried. - #[ink(message)] + #[ink(message, selector = 0x6568382f)] fn balance_of(&self, owner: AccountId) -> Balance; /// Returns the allowance for a `spender` approved by an `owner`. @@ -23,7 +29,7 @@ pub trait Psp22 { /// # Parameters /// - `owner` - The account that owns the tokens. /// - `spender` - The account that is allowed to spend the tokens. - #[ink(message)] + #[ink(message, selector = 0x4d47d921)] fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance; /// Transfers `value` amount of tokens from the caller's account to account `to` @@ -33,8 +39,8 @@ pub trait Psp22 { /// - `to` - The recipient account. /// - `value` - The number of tokens to transfer. /// - `data` - Additional data in unspecified format. - #[ink(message)] - fn transfer(&mut self, to: AccountId, value: Balance, data: Vec) -> Result<(), PSP22Error>; + #[ink(message, selector = 0xdb20f9f5)] + fn transfer(&mut self, to: AccountId, value: Balance, data: Vec) -> Result<(), Psp22Error>; /// Transfers `value` tokens on behalf of `from` to the account `to` /// with additional `data` in unspecified format. @@ -44,14 +50,14 @@ pub trait Psp22 { /// - `to` - The recipient account. /// - `value` - The number of tokens to transfer. /// - `data` - Additional data with unspecified format. - #[ink(message)] + #[ink(message, selector = 0x54b3c76e)] fn transfer_from( &mut self, from: AccountId, to: AccountId, value: Balance, data: Vec, - ) -> Result<(), PSP22Error>; + ) -> Result<(), Psp22Error>; /// Approves `spender` to spend `value` amount of tokens on behalf of the caller. /// @@ -60,39 +66,39 @@ pub trait Psp22 { /// # Parameters /// - `spender` - The account that is allowed to spend the tokens. /// - `value` - The number of tokens to approve. - #[ink(message)] - fn approve(&mut self, spender: AccountId, value: Balance) -> Result<(), PSP22Error>; + #[ink(message, selector = 0xb20f1bbd)] + fn approve(&mut self, spender: AccountId, value: Balance) -> Result<(), Psp22Error>; /// Increases the allowance of `spender` by `value` amount of tokens. /// /// # Parameters /// - `spender` - The account that is allowed to spend the tokens. /// - `value` - The number of tokens to increase the allowance by. - #[ink(message)] - fn increase_allowance(&mut self, spender: AccountId, value: Balance) -> Result<(), PSP22Error>; + #[ink(message, selector = 0x96d6b57a)] + fn increase_allowance(&mut self, spender: AccountId, value: Balance) -> Result<(), Psp22Error>; /// Decreases the allowance of `spender` by `value` amount of tokens. /// /// # Parameters /// - `spender` - The account that is allowed to spend the tokens. /// - `value` - The number of tokens to decrease the allowance by. - #[ink(message)] - fn decrease_allowance(&mut self, spender: AccountId, value: Balance) -> Result<(), PSP22Error>; + #[ink(message, selector = 0xfecb57d5)] + fn decrease_allowance(&mut self, spender: AccountId, value: Balance) -> Result<(), Psp22Error>; } /// The PSP22 Metadata trait. #[ink::trait_definition] pub trait Psp22Metadata { /// Returns the token name. - #[ink(message)] + #[ink(message, selector = 0x3d261bd4)] fn token_name(&self) -> Option; /// Returns the token symbol. - #[ink(message)] + #[ink(message, selector = 0x34205be5)] fn token_symbol(&self) -> Option; /// Returns the token decimals. - #[ink(message)] + #[ink(message, selector = 0x7271b782)] fn token_decimals(&self) -> u8; } @@ -101,11 +107,14 @@ pub trait Psp22Metadata { pub trait Psp22Mintable { /// Creates `value` amount of tokens and assigns them to `account`, increasing the total supply. /// + /// The selector for this message is `0xfc3c75d4` (first 4 bytes of + /// `blake2b_256("PSP22Mintable::mint")`). + /// /// # Parameters /// - `account` - The account to be credited with the created tokens. /// - `value` - The number of tokens to mint. - #[ink(message)] - fn mint(&mut self, account: AccountId, value: Balance) -> Result<(), PSP22Error>; + #[ink(message, selector = 0xfc3c75d4)] + fn mint(&mut self, account: AccountId, value: Balance) -> Result<(), Psp22Error>; } /// The PSP22 Burnable trait. @@ -113,9 +122,12 @@ pub trait Psp22Mintable { pub trait Psp22Burnable { /// Destroys `value` amount of tokens from `account`, reducing the total supply. /// + /// The selector for this message is `0x7a9da510` (first 4 bytes of + /// `blake2b_256("PSP22Burnable::burn")`). + /// /// # Parameters /// - `account` - The account from which the tokens will be destroyed. /// - `value` - The number of tokens to destroy. - #[ink(message)] - fn burn(&mut self, account: AccountId, value: Balance) -> Result<(), PSP22Error>; + #[ink(message, selector = 0x7a9da510)] + fn burn(&mut self, account: AccountId, value: Balance) -> Result<(), Psp22Error>; }