diff --git a/Cargo.toml b/Cargo.toml index 0c44755a..5699915b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,12 @@ license = "Unlicense" repository = "https://github.com/r0gue-io/pop-node/" [workspace] -exclude = [ "extension/contract", "pop-api", "tests/contracts" ] +exclude = [ + "extension/contract", + "pop-api", + "pop-sandbox", + "tests/contracts", +] members = [ "integration-tests", "node", diff --git a/extension/src/environment.rs b/extension/src/environment.rs index dc3818e8..e57c28bf 100644 --- a/extension/src/environment.rs +++ b/extension/src/environment.rs @@ -1,7 +1,10 @@ use core::fmt::Debug; use frame_support::pallet_prelude::Weight; -use pallet_contracts::chain_extension::{BufInBufOutState, ChargedAmount, Result, State}; +use pallet_contracts::{ + chain_extension::{BufInBufOutState, ChargedAmount, Result, State}, + Origin, +}; use sp_std::vec::Vec; use crate::AccountIdOf; @@ -168,14 +171,14 @@ pub trait Ext { type AccountId; /// Returns a reference to the account id of the current contract. - fn address(&self) -> &Self::AccountId; + fn address(&self) -> Self::AccountId; } impl Ext for () { type AccountId = (); - fn address(&self) -> &Self::AccountId { - &() + fn address(&self) -> Self::AccountId { + () } } @@ -185,12 +188,16 @@ pub(crate) struct ExternalEnvironment<'a, T: pallet_contracts::chain_extension:: impl<'a, E: pallet_contracts::chain_extension::Ext> Ext for ExternalEnvironment<'a, E> { type AccountId = AccountIdOf; - fn address(&self) -> &Self::AccountId { - self.0.address() + fn address(&self) -> Self::AccountId { + // TODO: Need to decide which address to return. + match self.0.caller() { + Origin::Root => self.0.address().clone(), + Origin::Signed(caller) => caller, + } } } #[test] fn default_ext_works() { - assert_eq!(().address(), &()) + assert_eq!(().address(), ()) } diff --git a/extension/src/functions.rs b/extension/src/functions.rs index 73a5fdf9..0cbfbf48 100644 --- a/extension/src/functions.rs +++ b/extension/src/functions.rs @@ -53,7 +53,7 @@ impl< let charged = env.charge_weight(dispatch_info.weight)?; log::debug!(target: Logger::LOG_TARGET, "pre-dispatch weight charged: charged={charged:?}"); // Contract is the origin by default. - let origin = RawOrigin::Signed(env.ext().address().clone()); + let origin = RawOrigin::Signed(env.ext().address()); log::debug!(target: Logger::LOG_TARGET, "contract origin: origin={origin:?}"); let mut origin: Config::RuntimeOrigin = origin.into(); // Ensure call allowed. diff --git a/extension/src/mock.rs b/extension/src/mock.rs index c085aebe..b267b3e3 100644 --- a/extension/src/mock.rs +++ b/extension/src/mock.rs @@ -350,8 +350,8 @@ pub(crate) struct MockExt { impl environment::Ext for MockExt { type AccountId = AccountIdOf; - fn address(&self) -> &Self::AccountId { - &self.address + fn address(&self) -> Self::AccountId { + self.address } } 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..a2960895 --- a/pop-api/examples/fungibles/Cargo.toml +++ b/pop-api/examples/fungibles/Cargo.toml @@ -1,12 +1,31 @@ [package] authors = [ "[your_name] <[your_email]>" ] edition = "2021" -name = "fungibles" +name = "api_example_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", +] } +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] +env_logger = { version = "0.11.3" } +serde_json = "1.0.114" + +# Local +drink = { path = "../../../../pop-drink/drink" } +pop-sandbox = { path = "../../../pop-sandbox", default-features = false } + +# Substrate +frame-support = { version = "36.0.0", default-features = false } [lib] path = "lib.rs" @@ -18,4 +37,7 @@ ink-as-dependency = [ ] std = [ "ink/std", "pop-api/std", + "pop-sandbox/std", + "scale-info/std", + "scale/std", ] diff --git a/pop-api/examples/fungibles/lib.rs b/pop-api/examples/fungibles/lib.rs old mode 100755 new mode 100644 index 11eafe21..04b432b9 --- a/pop-api/examples/fungibles/lib.rs +++ b/pop-api/examples/fungibles/lib.rs @@ -1,108 +1,280 @@ #![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; +#[cfg(test)] +mod utils; #[ink::contract] mod fungibles { use super::*; #[ink(storage)] - #[derive(Default)] - pub struct Fungibles; + pub struct Fungibles { + id: TokenId, + } impl Fungibles { + fn emit_created_event(&mut self, id: u32, creator: AccountId, admin: AccountId) { + self.env().emit_event(Created { id, creator, admin }); + } + + fn emit_transfer_event( + &mut self, + from: Option, + to: Option, + value: Balance, + ) { + self.env().emit_event(Transfer { from, to, value }); + } + + fn emit_approval_event(&mut self, owner: AccountId, spender: AccountId, value: Balance) { + self.env().emit_event(Approval { owner, spender, value }); + } + + /// Instantiate the contract and wrap around an existing token. + /// + /// # Parameters + /// * - `token` - The token. #[ink(constructor, payable)] - pub fn new() -> Self { - Default::default() + pub fn new_existing(id: TokenId) -> Result { + // Make sure token exists. + if !api::token_exists(id).unwrap_or_default() { + return Err(PSP22Error::Custom(String::from("Unknown"))); + } + let contract = Self { id }; + Ok(contract) } - #[ink(message)] - pub fn total_supply(&self, token: TokenId) -> Result { - api::total_supply(token) + /// 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. + /// * - `admin` - The account that will administer the token. + /// * - `min_balance` - The minimum balance required for accounts holding this token. + #[ink(constructor, payable)] + pub fn new( + id: TokenId, + admin: AccountId, + min_balance: Balance, + ) -> Result { + let mut contract = Self { id }; + let contract_id = contract.env().account_id(); + api::create(id, admin, min_balance).map_err(PSP22Error::from)?; + contract.emit_created_event(id, contract_id, admin); + Ok(contract) } + } + impl Psp22 for Fungibles { + /// Returns the total token supply. #[ink(message)] - pub fn balance_of(&self, token: TokenId, owner: AccountId) -> Result { - api::balance_of(token, owner) + fn total_supply(&self) -> Balance { + api::total_supply(self.id).unwrap_or_default() } + /// Returns the account balance for the specified `owner` #[ink(message)] - pub fn allowance( - &self, - token: TokenId, - owner: AccountId, - spender: AccountId, - ) -> Result { - api::allowance(token, owner, spender) + fn balance_of(&self, owner: AccountId) -> Balance { + api::balance_of(self.id, owner).unwrap_or_default() } + /// Returns the amount which `spender` is still allowed to withdraw from `owner` #[ink(message)] - pub fn transfer(&mut self, token: TokenId, to: AccountId, value: Balance) -> Result<()> { - api::transfer(token, to, value) + fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance { + api::allowance(self.id, owner, spender).unwrap_or_default() } + /// Transfers `value` amount of tokens from the caller's account to account `to` + /// with additional `data` in unspecified format. #[ink(message)] - pub fn transfer_from( + fn transfer( &mut self, - token: TokenId, - from: AccountId, to: AccountId, value: Balance, _data: Vec, - ) -> Result<()> { - api::transfer_from(token, from, to, value) + ) -> Result<(), PSP22Error> { + let caller = self.env().caller(); + // No-op if the caller and `to` is the same address or `value` is zero, returns success + // and no events are emitted. + if caller == to || value == 0 { + return Ok(()); + } + + // Reverts with `InsufficientBalance` if the `value` exceeds the caller's balance. + if value > self.balance_of(caller) { + return Err(PSP22Error::InsufficientBalance); + } + + api::transfer(self.id, to, value).map_err(PSP22Error::from)?; + self.emit_transfer_event(Some(caller), Some(to), value); + Ok(()) } + /// Transfers `value` tokens on the behalf of `from` to the account `to` + /// with additional `data` in unspecified format. #[ink(message)] - pub fn approve( + fn transfer_from( &mut self, - token: TokenId, - spender: AccountId, + from: AccountId, + to: AccountId, value: Balance, - ) -> Result<()> { - api::approve(token, spender, value) + _data: Vec, + ) -> Result<(), PSP22Error> { + let caller = self.env().caller(); + // No-op if `from` and `to` is the same address or `value` is zero, returns success and + // no events are emitted. + if from == to || value == 0 { + return Ok(()); + } + + // Reverts with `InsufficientBalance` if the `value` exceeds the balance of the account + // `from`. + let allowance = self.allowance(from, caller); + if allowance < value { + return Err(PSP22Error::InsufficientAllowance); + } + + // Reverts with `InsufficientAllowance` if `from` and the caller are different addresses + // and the `value` exceeds the allowance granted by `from` to the caller. + let from_balance = self.balance_of(from); + if from_balance < value { + return Err(PSP22Error::InsufficientBalance); + } + + // If `from` and the caller are different addresses, a successful transfer results + // in decreased allowance by `from` to the caller and an `Approval` event with + // the new allowance amount is emitted. + api::transfer_from(self.id, from, to, value).map_err(PSP22Error::from)?; + // Emit events. + self.emit_transfer_event(Some(caller), Some(to), value); + let allowance = self.allowance(from, caller).saturating_sub(value); + self.emit_approval_event(from, caller, allowance); + Ok(()) + } + + /// Allows `spender` to withdraw from the caller's account multiple times, up to + /// the total amount of `value`. + #[ink(message)] + fn approve(&mut self, spender: AccountId, value: Balance) -> Result<(), PSP22Error> { + let caller = self.env().caller(); + // No-op if the caller and `spender` is the same address, returns success and no events + // are emitted. + if caller == spender { + return Ok(()); + } + + api::approve(self.id, spender, value).map_err(PSP22Error::from)?; + self.emit_approval_event(caller, spender, value); + Ok(()) } + /// Increases by `value` the allowance granted to `spender` by the caller. #[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> { + let caller = self.env().caller(); + // No-op if the caller and `spender` is the same address or `value` is zero, returns + // success and no events are emitted. + if caller == spender || value == 0 { + return Ok(()); + } + + let allowance = self.allowance(caller, spender); + api::increase_allowance(self.id, spender, value).map_err(PSP22Error::from)?; + self.emit_approval_event(caller, spender, allowance.saturating_add(value)); + Ok(()) } + /// Decreases by `value` the allowance granted to `spender` by the caller. #[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> { + let caller = self.env().caller(); + // No-op if the caller and `spender` is the same address or `value` is zero, returns + // success and no events are emitted. + if caller == spender || value == 0 { + return Ok(()); + } + // Reverts with `InsufficientAllowance` if `spender` and the caller are different + // addresses and the `value` exceeds the allowance granted by the caller to + // `spender`. + let allowance = self.allowance(caller, spender); + if allowance < value { + return Err(PSP22Error::InsufficientAllowance); + } + + api::decrease_allowance(self.id, spender, value).map_err(PSP22Error::from)?; + self.emit_approval_event(caller, spender, allowance.saturating_sub(value)); + Ok(()) } + } + impl Psp22Metadata for Fungibles { + /// Returns the token name. #[ink(message)] - pub fn token_name(&self, token: TokenId) -> Result> { - api::token_name(token) + fn token_name(&self) -> Option { + api::token_name(self.id).ok().and_then(|v| String::from_utf8(v).ok()) } + /// Returns the token symbol. #[ink(message)] - pub fn token_symbol(&self, token: TokenId) -> Result> { - api::token_symbol(token) + fn token_symbol(&self) -> Option { + api::token_symbol(self.id).ok().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 Fungibles { + /// Mints `value` tokens to the senders account. + #[ink(message)] + fn mint(&mut self, account: AccountId, value: Balance) -> Result<(), PSP22Error> { + if value == 0 { + return Ok(()); + } + api::mint(self.id, account, value).map_err(PSP22Error::from)?; + self.emit_transfer_event(None, Some(account), value); + Ok(()) + } + } + + impl Psp22Burnable for Fungibles { + /// Burns `value` tokens from the senders account. #[ink(message)] - pub fn token_decimals(&self, token: TokenId) -> Result { - api::token_decimals(token) + fn burn(&mut self, account: AccountId, value: Balance) -> Result<(), PSP22Error> { + if value == 0 { + return Ok(()); + } + let balance = self.balance_of(account); + if balance < value { + return Err(PSP22Error::InsufficientBalance); + } + api::burn(self.id, account, value).map_err(PSP22Error::from)?; + self.emit_transfer_event(Some(account), None, value); + Ok(()) } } } diff --git a/pop-api/examples/fungibles/tests.rs b/pop-api/examples/fungibles/tests.rs new file mode 100644 index 00000000..c04ab566 --- /dev/null +++ b/pop-api/examples/fungibles/tests.rs @@ -0,0 +1,423 @@ +use drink::{ + sandbox_api::assets_api::AssetsAPI, + session::{error::SessionError, Session}, +}; +use frame_support::assert_ok; +use pop_api::{ + primitives::TokenId, + v0::fungibles::events::{Approval, Created, Transfer}, +}; +use pop_sandbox::{Balance, Sandbox, ALICE, BOB}; +use scale::Encode; +use utils::*; + +use super::*; + +const TOKEN_ID: TokenId = 1; +const TOKEN_MIN_BALANCE: Balance = 10_000; + +#[drink::contract_bundle_provider] +enum BundleProvider {} + +#[drink::test(sandbox = Sandbox)] +fn new_constructor_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + // Token exists after the deployment. + assert!(session.sandbox().asset_exists(&TOKEN_ID)); + // Successfully emit event. + let expected = Created { + id: TOKEN_ID, + creator: account_id_from_slice(contract.as_ref()), + admin: account_id_from_slice(ALICE.as_ref()), + } + .encode(); + assert_eq!(last_contract_event(&session).unwrap(), expected.as_slice()); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn new_existing_constructor_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + + // Create token. + let actor = session.get_actor(); + session.sandbox().create(&TOKEN_ID, &actor, TOKEN_MIN_BALANCE).unwrap(); + // Deploy a new contract. + deploy_with_new_existing_constructor(&mut session, BundleProvider::local()?, TOKEN_ID)?; + // Token is created successfully. + assert!(session.sandbox().asset_exists(&TOKEN_ID)); + // No event emitted. + assert_eq!(last_contract_event(&session), None); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn new_existing_constructor_deployment_fails( + mut session: Session, +) -> Result<(), Box> { + let _ = env_logger::try_init(); + + let result = + deploy_with_new_existing_constructor(&mut session, BundleProvider::local()?, TOKEN_ID); + assert!(result.is_err()); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn balance_of_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + // Mint tokens. + const AMOUNT: Balance = 12_000; + assert_ok!(mint(&mut session, ALICE, AMOUNT)); + // Tokens were minted with the right amount. + assert_eq!(balance_of(&mut session, ALICE), AMOUNT); + assert_eq!(balance_of(&mut session, ALICE), session.sandbox().balance_of(&TOKEN_ID, &ALICE)); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn mint_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + // Mint tokens. + const AMOUNT: Balance = 12_000; + assert_ok!(mint(&mut session, ALICE, AMOUNT)); + // Successfully emit event. + let expected = + Transfer { from: None, to: Some(account_id_from_slice(ALICE.as_ref())), value: AMOUNT } + .encode(); + assert_eq!(last_contract_event(&session).unwrap(), expected.as_slice()); + // Tokens were minted with the right amount. + assert_eq!(total_supply(&mut session), AMOUNT); + assert_eq!(balance_of(&mut session, ALICE), AMOUNT); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn mint_zero_value_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + // Mint tokens. + assert_ok!(mint(&mut session, ALICE, 0)); + // No event emitted. + assert_eq!(last_contract_event(&session), None); + // Tokens were minted with the right amount. + assert_eq!(total_supply(&mut session), 0); + assert_eq!(balance_of(&mut session, ALICE), 0); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn burn_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + // Mint tokens. + const AMOUNT: Balance = 12_000; + assert_ok!(mint(&mut session, ALICE, AMOUNT)); + // Burn tokens. + assert_ok!(burn(&mut session, ALICE, 1)); + // Successfully emit event. + let expected = + Transfer { from: Some(account_id_from_slice(ALICE.as_ref())), to: None, value: 1 }.encode(); + assert_eq!(last_contract_event(&session).unwrap(), expected.as_slice()); + + assert_eq!(total_supply(&mut session), AMOUNT - 1); + assert_eq!(balance_of(&mut session, ALICE), AMOUNT - 1); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn burn_zero_value_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + // Burn tokens. + assert_ok!(burn(&mut session, ALICE, 0)); + // No event emitted. + assert_eq!(last_contract_event(&session), None); + // Tokens were minted with the right amount. + assert_eq!(total_supply(&mut session), 0); + assert_eq!(balance_of(&mut session, ALICE), 0); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn burn_fails_with_insufficient_balance( + mut session: Session, +) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + // Mint tokens. + const AMOUNT: Balance = 12_000; + assert_ok!(mint(&mut session, ALICE, AMOUNT)); + // Failed with `InsufficientBalance`. + expect_call_reverted( + &mut session, + BURN, + vec![ALICE.to_string(), (AMOUNT + 1).to_string()], + PSP22Error::InsufficientBalance, + ); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn transfer_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + // Mint tokens. + const AMOUNT: Balance = 12_000; + const TRANSFERRED: Balance = 500; + assert_ok!(mint(&mut session, contract.clone(), AMOUNT)); + assert_ok!(mint(&mut session, BOB, AMOUNT)); + // Transfer tokens from `contract` to `account`. + session.set_actor(contract.clone()); + assert_ok!(transfer(&mut session, BOB, TRANSFERRED)); + // Successfully emit event. + let expected = Transfer { + from: Some(account_id_from_slice(contract.clone().as_ref())), + to: Some(account_id_from_slice(BOB.as_ref())), + value: TRANSFERRED, + } + .encode(); + assert_eq!(last_contract_event(&session).unwrap(), expected.as_slice()); + + assert_eq!(balance_of(&mut session, contract), AMOUNT - TRANSFERRED); + assert_eq!(balance_of(&mut session, BOB), AMOUNT + TRANSFERRED); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn transfer_zero_value_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + assert_ok!(transfer(&mut session, ALICE, 0)); + // No event emitted. + assert_eq!(last_contract_event(&session), None); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn transfer_fails_with_insufficient_balance( + mut session: Session, +) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + // Mint tokens. + const AMOUNT: Balance = 12_000; + assert_ok!(mint(&mut session, contract.clone(), AMOUNT)); + assert_ok!(mint(&mut session, BOB, AMOUNT)); + + session.set_actor(contract.clone()); + // Failed with `InsufficientBalance`. + expect_call_reverted( + &mut session, + TRANSFER, + vec![BOB.to_string(), (AMOUNT + 1).to_string()], + PSP22Error::InsufficientBalance, + ); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn approve_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + + const AMOUNT: Balance = 12_000; + // Mint tokens. + assert_ok!(mint(&mut session, contract.clone(), AMOUNT)); + // Successfully apporve. + session.set_actor(contract.clone()); + assert_ok!(approve(&mut session, ALICE, AMOUNT / 2)); + // Successfully emit event. + let expected = Approval { + owner: account_id_from_slice(contract.clone().as_ref()), + spender: account_id_from_slice(ALICE.as_ref()), + value: AMOUNT / 2, + } + .encode(); + assert_eq!(last_contract_event(&session).unwrap(), expected.as_slice()); + assert_eq!(allowance(&mut session, contract, ALICE), AMOUNT / 2); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn increase_allowance_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + + const AMOUNT: Balance = 12_000; + // Mint tokens. + assert_ok!(mint(&mut session, contract.clone(), AMOUNT)); + // Successfully apporve. + session.set_actor(contract.clone()); + assert_ok!(approve(&mut session, ALICE, AMOUNT / 2)); + assert_ok!(increase_allowance(&mut session, ALICE, AMOUNT / 2)); + // Successfully emit event. + let expected = Approval { + owner: account_id_from_slice(contract.clone().as_ref()), + spender: account_id_from_slice(ALICE.as_ref()), + value: AMOUNT, + } + .encode(); + assert_eq!(last_contract_event(&session).unwrap(), expected.as_slice()); + assert_eq!(allowance(&mut session, contract, ALICE), AMOUNT); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn decrease_allowance_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + + const AMOUNT: Balance = 12_000; + // Mint tokens. + assert_ok!(mint(&mut session, contract.clone(), AMOUNT)); + // Successfully apporve. + session.set_actor(contract.clone()); + assert_ok!(approve(&mut session, ALICE, AMOUNT / 2)); + assert_ok!(decrease_allowance(&mut session, ALICE, 1)); + // Successfully emit event. + let expected = Approval { + owner: account_id_from_slice(contract.clone().as_ref()), + spender: account_id_from_slice(ALICE.as_ref()), + value: AMOUNT / 2 - 1, + } + .encode(); + assert_eq!(last_contract_event(&session).unwrap(), expected.as_slice()); + assert_eq!(allowance(&mut session, contract, ALICE), AMOUNT / 2 - 1); + Ok(()) +} + +#[drink::test(sandbox = Sandbox)] +fn transfer_from_works(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Deploy a new contract. + let contract = deploy_with_new_constructor( + &mut session, + BundleProvider::local()?, + TOKEN_ID, + ALICE, + TOKEN_MIN_BALANCE, + )?; + + const AMOUNT: Balance = TOKEN_MIN_BALANCE * 4; + const TRANSFERRED: Balance = TOKEN_MIN_BALANCE * 2; + // Mint tokens. + assert_ok!(mint(&mut session, contract.clone(), AMOUNT)); + // Successfully transfer from `contract` by `ALICE`. + session.set_actor(contract.clone()); + assert_ok!(approve(&mut session, ALICE, AMOUNT)); + assert_eq!(allowance(&mut session, contract.clone(), ALICE), AMOUNT); + assert_eq!(balance_of(&mut session, ALICE), 0); + assert_eq!(balance_of(&mut session, BOB), 0); + assert_eq!(balance_of(&mut session, contract.clone()), AMOUNT); + + session.set_actor(ALICE); + assert_ok!(transfer_from(&mut session, contract.clone(), BOB, TRANSFERRED)); + // Successfully emit event. + let expected = Approval { + owner: account_id_from_slice(contract.clone().as_ref()), + spender: account_id_from_slice(ALICE.as_ref()), + value: TRANSFERRED, + } + .encode(); + // assert_eq!(last_contract_event(&session).unwrap(), expected.as_slice()); + assert_eq!(allowance(&mut session, contract.clone(), ALICE), AMOUNT - TRANSFERRED); + assert_eq!(balance_of(&mut session, ALICE), 0); + assert_eq!(balance_of(&mut session, BOB), TRANSFERRED); + assert_eq!(balance_of(&mut session, contract), AMOUNT - TRANSFERRED); + Ok(()) +} diff --git a/pop-api/examples/fungibles/utils.rs b/pop-api/examples/fungibles/utils.rs new file mode 100644 index 00000000..7262f718 --- /dev/null +++ b/pop-api/examples/fungibles/utils.rs @@ -0,0 +1,197 @@ +// A set of helper methods to test the contract calls. + +use drink::{ + session::{bundle::ContractBundle, error::SessionError, Session, NO_SALT}, + DispatchError, +}; +use pop_api::primitives::{AccountId, TokenId}; +use pop_sandbox::{AccountId32, Balance, Sandbox, INIT_VALUE}; +use scale::{Decode, Encode}; + +use super::*; + +// PSP22 functions. +pub const ALLOWANCE: &str = "Psp22::allowance"; +pub const BALANCE_OF: &str = "Psp22::balance_of"; +pub const TOTAL_SUPPLY: &str = "Psp22::total_supply"; +pub const TRANSFER: &str = "Psp22::transfer"; +pub const TRANSFER_FROM: &str = "Psp22::transfer_from"; +pub const APPROVE: &str = "Psp22::approve"; +pub const INCREASE_ALLOWANCE: &str = "Psp22::increase_allowance"; +pub const DECREASE_ALLOWANCE: &str = "Psp22::decrease_allowance"; +// PSP22Metadata functions. +pub const TOKEN_NAME: &str = "Psp22Metadata::token_name"; +pub const TOKEN_SYMBOL: &str = "Psp22Metadata::token_symbol"; +pub const TOKEN_DECIMALS: &str = "Psp22Metadata::token_decimals"; +// PSP22Mintable functions. +pub const MINT: &str = "Psp22Mintable::mint"; +// PSP22Burnable functions. +pub const BURN: &str = "Psp22Burnable::burn"; + +/// This is used to resolve type mismatches between the `AccountId` in the quasi testing environment and the +/// contract environment. +pub(super) fn account_id_from_slice(s: &[u8; 32]) -> AccountId { + AccountId::decode(&mut &s[..]).expect("Should be decoded to AccountId") +} + +/// Get the last event from pallet contracts. +pub(super) fn last_contract_event(session: &Session) -> Option> { + session.record().last_event_batch().contract_events().last().cloned() +} + +/// Execute a contract method and exepct CallReverted error to be returned. +pub(super) fn expect_call_reverted( + session: &mut Session, + function: &str, + params: Vec, + err: PSP22Error, +) { + let call = session.call::(function, ¶ms, None); + if let Err(SessionError::CallReverted(error)) = call { + assert_eq!(error[1..], Err::<(), PSP22Error>(err).encode()); + } +} + +// Call a contract method and decode the returned data. +pub(super) fn decoded_call( + session: &mut Session, + func_name: &str, + input: Vec, + endowment: Option, +) -> Result> { + session.call(func_name, &input, endowment)??; + Ok(session.record().last_call_return_decoded::()??) +} + +// Check if the event emitted correctly. +pub(super) fn assert_event(session: &mut Session, event: Vec) { + let contract_events = session.record().last_event_batch().contract_events(); + let last_event = contract_events.last().unwrap().to_vec(); + assert_eq!(last_event, event.as_slice()); +} + +// Test methods for deployment with constructor function. + +pub(super) fn deploy_with_new_constructor( + session: &mut Session, + bundle: ContractBundle, + id: TokenId, + admin: AccountId32, + min_balance: Balance, +) -> Result { + session.deploy_bundle( + bundle, + "new", + &[id.to_string(), admin.to_string(), min_balance.to_string()], + NO_SALT, + Some(INIT_VALUE), + ) +} + +pub(super) fn deploy_with_new_existing_constructor( + session: &mut Session, + bundle: ContractBundle, + id: TokenId, +) -> Result { + session.deploy_bundle(bundle, "new_existing", &[id.to_string()], NO_SALT, Some(INIT_VALUE)) +} + +// Test methods for `PSP22`. + +pub(super) fn total_supply(session: &mut Session) -> Balance { + decoded_call::(session, TOTAL_SUPPLY, vec![], None).unwrap() +} + +pub(super) fn balance_of(session: &mut Session, owner: AccountId32) -> Balance { + decoded_call::(session, BALANCE_OF, vec![owner.to_string()], None).unwrap() +} + +pub(super) fn allowance( + session: &mut Session, + owner: AccountId32, + spender: AccountId32, +) -> Balance { + decoded_call::(session, ALLOWANCE, vec![owner.to_string(), spender.to_string()], None) + .unwrap() +} + +pub(super) fn transfer( + session: &mut Session, + to: AccountId32, + amount: Balance, +) -> Result<(), Box> { + let data = serde_json::to_string::<[u8; 0]>(&[]).unwrap(); + Ok(session.call(TRANSFER, &vec![to.to_string(), amount.to_string(), data], None)??) +} + +pub(super) fn transfer_from( + session: &mut Session, + from: AccountId32, + to: AccountId32, + amount: Balance, +) -> Result<(), Box> { + let data = serde_json::to_string::<[u8; 0]>(&[]).unwrap(); + Ok(session.call( + TRANSFER_FROM, + &vec![from.to_string(), to.to_string(), amount.to_string(), data], + None, + )??) +} + +pub(super) fn approve( + session: &mut Session, + spender: AccountId32, + value: Balance, +) -> Result<(), Box> { + Ok(session.call(APPROVE, &vec![spender.to_string(), value.to_string()], None)??) +} + +pub(super) fn increase_allowance( + session: &mut Session, + spender: AccountId32, + value: Balance, +) -> Result<(), Box> { + Ok(session.call(INCREASE_ALLOWANCE, &vec![spender.to_string(), value.to_string()], None)??) +} + +pub(super) fn decrease_allowance( + session: &mut Session, + spender: AccountId32, + value: Balance, +) -> Result<(), Box> { + Ok(session.call(DECREASE_ALLOWANCE, &vec![spender.to_string(), value.to_string()], None)??) +} + +// Test methods for `PSP22Metadata``. + +pub(super) fn token_name(session: &mut Session) -> String { + decoded_call::(session, TOKEN_NAME, vec![], None).unwrap() +} + +pub(super) fn token_symbol(session: &mut Session) -> String { + decoded_call::(session, TOKEN_SYMBOL, vec![], None).unwrap() +} + +pub(super) fn token_decimals(session: &mut Session) -> u8 { + decoded_call::(session, TOKEN_DECIMALS, vec![], None).unwrap() +} + +// Test methods for `PSP22Mintable``. + +pub(super) fn mint( + session: &mut Session, + account: AccountId32, + amount: Balance, +) -> Result<(), Box> { + Ok(session.call(MINT, &vec![account.to_string(), amount.to_string()], None)??) +} + +// Test methods for `PSP22MPsp22Burnablentable``. + +pub(super) fn burn( + session: &mut Session, + account: AccountId32, + amount: Balance, +) -> Result<(), Box> { + Ok(session.call(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/v0/fungibles/errors.rs b/pop-api/src/v0/fungibles/errors.rs new file mode 100644 index 00000000..a6df4348 --- /dev/null +++ b/pop-api/src/v0/fungibles/errors.rs @@ -0,0 +1,250 @@ +//! 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; + +use super::*; + +/// Represents various errors related to fungible tokens. +/// +/// The `FungiblesError` provides a detailed and specific set of error types that can occur when +/// interacting with fungible tokens. Each variant signifies a particular error +/// condition, facilitating precise error handling and debugging. +/// +/// It is designed to be lightweight, including only the essential errors relevant to fungible token +/// operations. The `Other` variant serves as a catch-all for any unexpected errors. For more +/// detailed debugging, the `Other` variant can be converted into the richer `Error` type defined in +/// the primitives crate. +/// NOTE: The `FungiblesError` is WIP +#[derive(Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub enum FungiblesError { + /// An unspecified or unknown error occurred. + Other(StatusCode), + /// The token is not live; either frozen or being destroyed. + NotLive, + /// Not enough allowance to fulfill a request is available. + InsufficientAllowance, + /// Not enough balance to fulfill a request is available. + InsufficientBalance, + /// The token ID is already taken. + InUse, + /// Minimum balance should be non-zero. + MinBalanceZero, + /// The account to alter does not exist. + NoAccount, + /// The signing account has no permission to do the operation. + NoPermission, + /// The given token ID is unknown. + Unknown, + /// No balance for creation of tokens or fees. + // TODO: Originally `pallet_balances::Error::InsufficientBalance` but collides with the + // `InsufficientBalance` error that is used for `pallet_assets::Error::BalanceLow` to adhere + // to the standard. This deserves a second look. + NoBalance, +} + +impl From for FungiblesError { + /// Converts a `StatusCode` to a `FungiblesError`. + /// + /// This conversion maps a `StatusCode`, returned by the runtime, to a more descriptive + /// `FungiblesError`. This provides better context and understanding of the error, allowing + /// developers to handle the most important errors effectively. + fn from(value: StatusCode) -> Self { + let encoded = value.0.to_le_bytes(); + match encoded { + // Balances. + [_, BALANCES, 2, _] => FungiblesError::NoBalance, + // Assets. + [_, ASSETS, 0, _] => FungiblesError::NoAccount, + [_, ASSETS, 1, _] => FungiblesError::NoPermission, + [_, ASSETS, 2, _] => FungiblesError::Unknown, + [_, ASSETS, 3, _] => FungiblesError::InUse, + [_, ASSETS, 5, _] => FungiblesError::MinBalanceZero, + [_, ASSETS, 7, _] => FungiblesError::InsufficientAllowance, + [_, ASSETS, 10, _] => FungiblesError::NotLive, + _ => FungiblesError::Other(value), + } + } +} + +/// The PSP22 error. +// TODO: Issue https://github.com/r0gue-io/pop-node/issues/298 +#[derive(Debug, PartialEq, Eq)] +#[ink::scale_derive(Encode, Decode, TypeInfo)] +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. + InsufficientBalance, + /// Returned if there is not enough allowance to complete the operation. + InsufficientAllowance, + /// Returned if recipient's address is zero. + ZeroRecipientAddress, + /// Returned if sender's address is zero. + ZeroSenderAddress, + /// Returned if a safe transfer check failed. + SafeTransferCheckFailed(String), +} + +impl From for PSP22Error { + /// Converts a `StatusCode` to a `PSP22Error`. + fn from(value: StatusCode) -> Self { + let encoded = value.0.to_le_bytes(); + match encoded { + [_, ASSETS, 3, _] => PSP22Error::Custom(String::from("Unknown")), + [_, ASSETS, 7, _] => PSP22Error::InsufficientAllowance, + _ => PSP22Error::Custom(String::from("Other")), + } + } +} + +#[cfg(test)] +mod tests { + use ink::{ + prelude::string::String, + scale::{Decode, Encode}, + }; + + use super::{FungiblesError, PSP22Error}; + use crate::{ + constants::{ASSETS, BALANCES}, + primitives::{ + ArithmeticError::*, + Error::{self, *}, + TokenError::*, + TransactionalError::*, + }, + StatusCode, + }; + + fn error_into_status_code(error: Error) -> StatusCode { + let mut encoded_error = error.encode(); + encoded_error.resize(4, 0); + let value = u32::from_le_bytes( + encoded_error.try_into().expect("qed, resized to 4 bytes line above"), + ); + value.into() + } + + fn into_fungibles_error(error: Error) -> FungiblesError { + let status_code: StatusCode = error_into_status_code(error); + status_code.into() + } + + fn into_psp22_error(error: Error) -> PSP22Error { + let status_code: StatusCode = error_into_status_code(error); + status_code.into() + } + + // If we ever want to change the conversion from bytes to `u32`. + #[test] + fn status_code_vs_encoded() { + assert_eq!(u32::decode(&mut &[3u8, 10, 2, 0][..]).unwrap(), 133635u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 0, 0][..]).unwrap(), 13315u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 1, 0][..]).unwrap(), 78851u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 2, 0][..]).unwrap(), 144387u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 3, 0][..]).unwrap(), 209923u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 5, 0][..]).unwrap(), 340995u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 7, 0][..]).unwrap(), 472067u32); + assert_eq!(u32::decode(&mut &[3u8, 52, 10, 0][..]).unwrap(), 668675u32); + } + + #[test] + fn converting_status_code_into_fungibles_error_works() { + let other_errors = vec![ + Other, + CannotLookup, + BadOrigin, + // `ModuleError` other than assets module. + Module { index: 2, error: [5, 0] }, + ConsumerRemaining, + NoProviders, + TooManyConsumers, + Token(OnlyProvider), + Arithmetic(Overflow), + Transactional(NoLayer), + Exhausted, + Corruption, + Unavailable, + RootNotAllowed, + Unknown { dispatch_error_index: 5, error_index: 5, error: 1 }, + DecodingFailed, + ]; + for error in other_errors { + let status_code: StatusCode = error_into_status_code(error); + let fungibles_error: FungiblesError = status_code.into(); + assert_eq!(fungibles_error, FungiblesError::Other(status_code)) + } + + assert_eq!( + into_fungibles_error(Module { index: BALANCES, error: [2, 0] }), + FungiblesError::NoBalance + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [0, 0] }), + FungiblesError::NoAccount + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [1, 0] }), + FungiblesError::NoPermission + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [2, 0] }), + FungiblesError::Unknown + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [3, 0] }), + FungiblesError::InUse + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [5, 0] }), + FungiblesError::MinBalanceZero + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [7, 0] }), + FungiblesError::InsufficientAllowance + ); + assert_eq!( + into_fungibles_error(Module { index: ASSETS, error: [10, 0] }), + FungiblesError::NotLive + ); + } + + #[test] + fn converting_status_code_into_psp22_error_works() { + let other_errors = vec![ + Other, + CannotLookup, + BadOrigin, + // `ModuleError` other than assets module. + Module { index: 2, error: [5, 0] }, + ConsumerRemaining, + NoProviders, + TooManyConsumers, + Token(OnlyProvider), + Arithmetic(Overflow), + Transactional(NoLayer), + Exhausted, + Corruption, + Unavailable, + RootNotAllowed, + Unknown { dispatch_error_index: 5, error_index: 5, error: 1 }, + DecodingFailed, + ]; + 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(String::from("Other"))) + } + + assert_eq!( + into_psp22_error(Module { index: ASSETS, error: [3, 0] }), + PSP22Error::Custom(String::from("Unknown")) + ); + assert_eq!( + into_psp22_error(Module { index: ASSETS, error: [7, 0] }), + PSP22Error::InsufficientAllowance + ); + } +} diff --git a/pop-api/src/v0/fungibles/events.rs b/pop-api/src/v0/fungibles/events.rs new file mode 100644 index 00000000..130ead65 --- /dev/null +++ b/pop-api/src/v0/fungibles/events.rs @@ -0,0 +1,85 @@ +//! A set of events for use in smart contracts interacting with the fungibles API. +//! +//! The `Transfer` and `Approval` events conform to the PSP-22 standard. The other events +//! (`Create`, `StartDestroy`, `SetMetadata`, `ClearMetadata`) are provided for convenience. +//! +//! These events are not emitted by the API itself but can be used in your contracts to +//! track token operations. Be mindful of the costs associated with emitting events. +//! +//! For more details, refer to [ink! events](https://use.ink/basics/events). + +use super::*; + +/// Event emitted when allowance by `owner` to `spender` changes. +// Differing style: event name abides by the PSP22 standard. +#[ink::event] +pub struct Approval { + /// The owner providing the allowance. + #[ink(topic)] + pub owner: AccountId, + /// The beneficiary of the allowance. + #[ink(topic)] + pub spender: AccountId, + /// The new allowance amount. + pub value: u128, +} + +/// Event emitted when transfer of tokens occurs. +// Differing style: event name abides by the PSP22 standard. +#[ink::event] +pub struct Transfer { + /// The source of the transfer. `None` when minting. + #[ink(topic)] + pub from: Option, + /// The recipient of the transfer. `None` when burning. + #[ink(topic)] + pub to: Option, + /// The amount transferred (or minted/burned). + pub value: u128, +} + +/// Event emitted when a token is created. +#[ink::event] +pub struct Created { + /// The token identifier. + #[ink(topic)] + pub id: TokenId, + /// The creator of the token. + #[ink(topic)] + pub creator: AccountId, + /// The administrator of the token. + #[ink(topic)] + pub admin: AccountId, +} + +/// Event emitted when a token is in the process of being destroyed. +#[ink::event] +pub struct DestroyStarted { + /// The token. + #[ink(topic)] + pub token: TokenId, +} + +/// Event emitted when new metadata is set for a token. +#[ink::event] +pub struct MetadataSet { + /// The token. + #[ink(topic)] + pub token: TokenId, + /// The name of the token. + #[ink(topic)] + pub name: Vec, + /// The symbol of the token. + #[ink(topic)] + pub symbol: Vec, + /// The decimals of the token. + pub decimals: u8, +} + +/// Event emitted when metadata is cleared for a token. +#[ink::event] +pub struct MetadataCleared { + /// The token. + #[ink(topic)] + pub token: TokenId, +} diff --git a/pop-api/src/v0/fungibles.rs b/pop-api/src/v0/fungibles/mod.rs similarity index 55% rename from pop-api/src/v0/fungibles.rs rename to pop-api/src/v0/fungibles/mod.rs index 99c1261a..7d1594c0 100644 --- a/pop-api/src/v0/fungibles.rs +++ b/pop-api/src/v0/fungibles/mod.rs @@ -1,15 +1,18 @@ //! The `fungibles` module provides an API for interacting and managing fungible tokens. //! -//! The API includes the following interfaces: +//! The API includes the following traits: //! 1. PSP-22 //! 2. PSP-22 Metadata //! 3. Management //! 4. PSP-22 Mintable & Burnable use constants::*; +pub use errors::*; +pub use events::*; use ink::prelude::vec::Vec; pub use management::*; pub use metadata::*; +pub use traits::*; use crate::{ constants::{ASSETS, BALANCES, FUNGIBLES}, @@ -17,134 +20,9 @@ use crate::{ ChainExtensionMethodApi, Result, StatusCode, }; -// Helper method to build a dispatch call. -// -// Parameters: -// - 'dispatchable': The index of the dispatchable function within the module. -fn build_dispatch(dispatchable: u8) -> ChainExtensionMethodApi { - crate::v0::build_dispatch(FUNGIBLES, dispatchable) -} - -// Helper method to build a call to read state. -// -// Parameters: -// - 'state_query': The index of the runtime state query. -fn build_read_state(state_query: u8) -> ChainExtensionMethodApi { - crate::v0::build_read_state(FUNGIBLES, state_query) -} - -mod constants { - /// 1. PSP-22 Interface: - pub(super) const TOTAL_SUPPLY: u8 = 0; - pub(super) const BALANCE_OF: u8 = 1; - pub(super) const ALLOWANCE: u8 = 2; - pub(super) const TRANSFER: u8 = 3; - pub(super) const TRANSFER_FROM: u8 = 4; - pub(super) const APPROVE: u8 = 5; - pub(super) const INCREASE_ALLOWANCE: u8 = 6; - pub(super) const DECREASE_ALLOWANCE: u8 = 7; - - /// 2. PSP-22 Metadata Interface: - pub(super) const TOKEN_NAME: u8 = 8; - pub(super) const TOKEN_SYMBOL: u8 = 9; - pub(super) const TOKEN_DECIMALS: u8 = 10; - - /// 3. Asset Management: - pub(super) const CREATE: u8 = 11; - pub(super) const START_DESTROY: u8 = 12; - pub(super) const SET_METADATA: u8 = 16; - pub(super) const CLEAR_METADATA: u8 = 17; - pub(super) const TOKEN_EXISTS: u8 = 18; - - /// 4. PSP-22 Mintable & Burnable interface: - pub(super) const MINT: u8 = 19; - pub(super) const BURN: u8 = 20; -} - -/// A set of events for use in smart contracts interacting with the fungibles API. -/// -/// The `Transfer` and `Approval` events conform to the PSP-22 standard. The other events -/// (`Create`, `StartDestroy`, `SetMetadata`, `ClearMetadata`) are provided for convenience. -/// -/// These events are not emitted by the API itself but can be used in your contracts to -/// track token operations. Be mindful of the costs associated with emitting events. -/// -/// For more details, refer to [ink! events](https://use.ink/basics/events). -pub mod events { - use super::*; - - /// Event emitted when allowance by `owner` to `spender` changes. - #[ink::event] - pub struct Approval { - /// The owner providing the allowance. - #[ink(topic)] - pub owner: AccountId, - /// The beneficiary of the allowance. - #[ink(topic)] - pub spender: AccountId, - /// The new allowance amount. - pub value: u128, - } - - /// Event emitted when transfer of tokens occurs. - #[ink::event] - pub struct Transfer { - /// The source of the transfer. `None` when minting. - #[ink(topic)] - pub from: Option, - /// The recipient of the transfer. `None` when burning. - #[ink(topic)] - pub to: Option, - /// The amount transferred (or minted/burned). - pub value: u128, - } - - /// Event emitted when a token is created. - #[ink::event] - pub struct Created { - /// The token identifier. - #[ink(topic)] - pub id: TokenId, - /// The creator of the token. - #[ink(topic)] - pub creator: AccountId, - /// The administrator of the token. - #[ink(topic)] - pub admin: AccountId, - } - - /// Event emitted when a token is in the process of being destroyed. - #[ink::event] - pub struct DestroyStarted { - /// The token. - #[ink(topic)] - pub token: TokenId, - } - - /// Event emitted when new metadata is set for a token. - #[ink::event] - pub struct MetadataSet { - /// The token. - #[ink(topic)] - pub token: TokenId, - /// The name of the token. - #[ink(topic)] - pub name: Vec, - /// The symbol of the token. - #[ink(topic)] - pub symbol: Vec, - /// The decimals of the token. - pub decimals: u8, - } - - /// Event emitted when metadata is cleared for a token. - #[ink::event] - pub struct MetadataCleared { - /// The token. - #[ink(topic)] - pub token: TokenId, - } -} +pub mod errors; +pub mod events; +pub mod traits; /// Returns the total token supply for a specified token. /// @@ -420,169 +298,46 @@ pub mod management { } } -/// Represents various errors related to fungible tokens. -/// -/// The `FungiblesError` provides a detailed and specific set of error types that can occur when -/// interacting with fungible tokens. Each variant signifies a particular error -/// condition, facilitating precise error handling and debugging. -/// -/// It is designed to be lightweight, including only the essential errors relevant to fungible token -/// operations. The `Other` variant serves as a catch-all for any unexpected errors. For more -/// detailed debugging, the `Other` variant can be converted into the richer `Error` type defined in -/// the primitives crate. -#[derive(Debug, PartialEq, Eq)] -#[ink::scale_derive(Encode, Decode, TypeInfo)] -pub enum FungiblesError { - /// An unspecified or unknown error occurred. - Other(StatusCode), - /// The token is not live; either frozen or being destroyed. - NotLive, - /// Not enough allowance to fulfill a request is available. - InsufficientAllowance, - /// Not enough balance to fulfill a request is available. - InsufficientBalance, - /// The token ID is already taken. - InUse, - /// Minimum balance should be non-zero. - MinBalanceZero, - /// The account to alter does not exist. - NoAccount, - /// The signing account has no permission to do the operation. - NoPermission, - /// The given token ID is unknown. - Unknown, - /// No balance for creation of tokens or fees. - // TODO: Originally `pallet_balances::Error::InsufficientBalance` but collides with the - // `InsufficientBalance` error that is used for `pallet_assets::Error::BalanceLow` to adhere - // to the standard. This deserves a second look. - NoBalance, -} - -impl From for FungiblesError { - /// Converts a `StatusCode` to a `FungiblesError`. - /// - /// This conversion maps a `StatusCode`, returned by the runtime, to a more descriptive - /// `FungiblesError`. This provides better context and understanding of the error, allowing - /// developers to handle the most important errors effectively. - fn from(value: StatusCode) -> Self { - let encoded = value.0.to_le_bytes(); - match encoded { - // Balances. - [_, BALANCES, 2, _] => FungiblesError::NoBalance, - // Assets. - [_, ASSETS, 0, _] => FungiblesError::NoAccount, - [_, ASSETS, 1, _] => FungiblesError::NoPermission, - [_, ASSETS, 2, _] => FungiblesError::Unknown, - [_, ASSETS, 3, _] => FungiblesError::InUse, - [_, ASSETS, 5, _] => FungiblesError::MinBalanceZero, - [_, ASSETS, 7, _] => FungiblesError::InsufficientAllowance, - [_, ASSETS, 10, _] => FungiblesError::NotLive, - _ => FungiblesError::Other(value), - } - } -} - -#[cfg(test)] -mod tests { - use ink::scale::{Decode, Encode}; - - use super::FungiblesError; - use crate::{ - constants::{ASSETS, BALANCES}, - primitives::{ - ArithmeticError::*, - Error::{self, *}, - TokenError::*, - TransactionalError::*, - }, - StatusCode, - }; +mod constants { + /// 1. PSP-22 + pub(super) const TOTAL_SUPPLY: u8 = 0; + pub(super) const BALANCE_OF: u8 = 1; + pub(super) const ALLOWANCE: u8 = 2; + pub(super) const TRANSFER: u8 = 3; + pub(super) const TRANSFER_FROM: u8 = 4; + pub(super) const APPROVE: u8 = 5; + pub(super) const INCREASE_ALLOWANCE: u8 = 6; + pub(super) const DECREASE_ALLOWANCE: u8 = 7; - fn error_into_status_code(error: Error) -> StatusCode { - let mut encoded_error = error.encode(); - encoded_error.resize(4, 0); - let value = u32::from_le_bytes( - encoded_error.try_into().expect("qed, resized to 4 bytes line above"), - ); - value.into() - } + /// 2. PSP-22 Metadata + pub(super) const TOKEN_NAME: u8 = 8; + pub(super) const TOKEN_SYMBOL: u8 = 9; + pub(super) const TOKEN_DECIMALS: u8 = 10; - fn into_fungibles_error(error: Error) -> FungiblesError { - let status_code: StatusCode = error_into_status_code(error); - status_code.into() - } + /// 3. Management + pub(super) const CREATE: u8 = 11; + pub(super) const START_DESTROY: u8 = 12; + pub(super) const SET_METADATA: u8 = 16; + pub(super) const CLEAR_METADATA: u8 = 17; + pub(super) const TOKEN_EXISTS: u8 = 18; - // If we ever want to change the conversion from bytes to `u32`. - #[test] - fn status_code_vs_encoded() { - assert_eq!(u32::decode(&mut &[3u8, 10, 2, 0][..]).unwrap(), 133635u32); - assert_eq!(u32::decode(&mut &[3u8, 52, 0, 0][..]).unwrap(), 13315u32); - assert_eq!(u32::decode(&mut &[3u8, 52, 1, 0][..]).unwrap(), 78851u32); - assert_eq!(u32::decode(&mut &[3u8, 52, 2, 0][..]).unwrap(), 144387u32); - assert_eq!(u32::decode(&mut &[3u8, 52, 3, 0][..]).unwrap(), 209923u32); - assert_eq!(u32::decode(&mut &[3u8, 52, 5, 0][..]).unwrap(), 340995u32); - assert_eq!(u32::decode(&mut &[3u8, 52, 7, 0][..]).unwrap(), 472067u32); - assert_eq!(u32::decode(&mut &[3u8, 52, 10, 0][..]).unwrap(), 668675u32); - } + /// 4. PSP-22 Mintable & Burnable + pub(super) const MINT: u8 = 19; + pub(super) const BURN: u8 = 20; +} - #[test] - fn converting_status_code_into_fungibles_error_works() { - let other_errors = vec![ - Other, - CannotLookup, - BadOrigin, - // `ModuleError` other than assets module. - Module { index: 2, error: [5, 0] }, - ConsumerRemaining, - NoProviders, - TooManyConsumers, - Token(OnlyProvider), - Arithmetic(Overflow), - Transactional(NoLayer), - Exhausted, - Corruption, - Unavailable, - RootNotAllowed, - Unknown { dispatch_error_index: 5, error_index: 5, error: 1 }, - DecodingFailed, - ]; - for error in other_errors { - let status_code: StatusCode = error_into_status_code(error); - let fungibles_error: FungiblesError = status_code.into(); - assert_eq!(fungibles_error, FungiblesError::Other(status_code)) - } +// Helper method to build a dispatch call. +// +// Parameters: +// - 'dispatchable': The index of the dispatchable function within the module. +fn build_dispatch(dispatchable: u8) -> ChainExtensionMethodApi { + crate::v0::build_dispatch(FUNGIBLES, dispatchable) +} - assert_eq!( - into_fungibles_error(Module { index: BALANCES, error: [2, 0] }), - FungiblesError::NoBalance - ); - assert_eq!( - into_fungibles_error(Module { index: ASSETS, error: [0, 0] }), - FungiblesError::NoAccount - ); - assert_eq!( - into_fungibles_error(Module { index: ASSETS, error: [1, 0] }), - FungiblesError::NoPermission - ); - assert_eq!( - into_fungibles_error(Module { index: ASSETS, error: [2, 0] }), - FungiblesError::Unknown - ); - assert_eq!( - into_fungibles_error(Module { index: ASSETS, error: [3, 0] }), - FungiblesError::InUse - ); - assert_eq!( - into_fungibles_error(Module { index: ASSETS, error: [5, 0] }), - FungiblesError::MinBalanceZero - ); - assert_eq!( - into_fungibles_error(Module { index: ASSETS, error: [7, 0] }), - FungiblesError::InsufficientAllowance - ); - assert_eq!( - into_fungibles_error(Module { index: ASSETS, error: [10, 0] }), - FungiblesError::NotLive - ); - } +// Helper method to build a call to read state. +// +// Parameters: +// - 'state_query': The index of the runtime state query. +fn build_read_state(state_query: u8) -> ChainExtensionMethodApi { + crate::v0::build_read_state(FUNGIBLES, state_query) } diff --git a/pop-api/src/v0/fungibles/traits.rs b/pop-api/src/v0/fungibles/traits.rs new file mode 100644 index 00000000..6d7bda79 --- /dev/null +++ b/pop-api/src/v0/fungibles/traits.rs @@ -0,0 +1,123 @@ +//! A set of traits that implement the PSP22 token standard. + +use core::result::Result; + +use ink::prelude::string::String; + +use super::*; + +/// The PSP22 trait. +#[ink::trait_definition] +pub trait Psp22 { + /// Returns the total token supply. + #[ink(message)] + fn total_supply(&self) -> Balance; + + /// Returns the account balance for the specified `owner`. + /// + /// # Parameters + /// - `owner` - The account whose balance is being queried. + #[ink(message)] + fn balance_of(&self, owner: AccountId) -> Balance; + + /// 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)] + fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance; + + /// Transfers `value` amount of tokens from the caller's account to account `to` + /// with additional `data` in unspecified format. + /// + /// # Parameters + /// - `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>; + + /// Transfers `value` tokens on behalf of `from` to the account `to` + /// with additional `data` in unspecified format. + /// + /// # 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)] + fn transfer_from( + &mut self, + from: AccountId, + to: AccountId, + value: Balance, + data: Vec, + ) -> Result<(), PSP22Error>; + + /// Approves `spender` to spend `value` amount of tokens on behalf of the caller. + /// + /// 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)] + 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>; + + /// 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>; +} + +/// The PSP22 Metadata trait. +#[ink::trait_definition] +pub trait Psp22Metadata { + /// Returns the token name. + #[ink(message)] + fn token_name(&self) -> Option; + + /// Returns the token symbol. + #[ink(message)] + fn token_symbol(&self) -> Option; + + /// Returns the token decimals. + #[ink(message)] + fn token_decimals(&self) -> u8; +} + +/// The PSP22 Mintable trait. +#[ink::trait_definition] +pub trait Psp22Mintable { + /// 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)] + fn mint(&mut self, account: AccountId, value: Balance) -> Result<(), PSP22Error>; +} + +/// The PSP22 Burnable trait. +#[ink::trait_definition] +pub trait Psp22Burnable { + /// 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>; +} diff --git a/pop-sandbox/Cargo.toml b/pop-sandbox/Cargo.toml new file mode 100644 index 00000000..7a65f593 --- /dev/null +++ b/pop-sandbox/Cargo.toml @@ -0,0 +1,50 @@ +[package] +authors = [ "[your_name] <[your_email]>" ] +description = "Sandboxing environment for contract quasi testing with Pop Network runtimes." +edition = "2021" +license = "GPL-3.0-only" +name = "pop-sandbox" +version = "0.0.0" + +[dependencies] +codec = { package = "parity-scale-codec", version = "3", default-features = false, features = [ + "derive", +] } +log = { version = "0.4.21", default-features = false } +scale-info = { version = "2.11", default-features = false } + +drink = { path = "../../pop-drink/drink", default-features = true } + +# Substrate +frame-metadata = { version = "16.0.0", default-features = false } +frame-support = { version = "36.0.0", default-features = false } +frame-support-procedural = { version = "=30.0.1", default-features = false } +frame-system = { version = "36.1.0", default-features = false } +pallet-assets = { version = "37.0.0", default-features = false } +pallet-balances = { version = "37.0.0", default-features = false } +pallet-contracts = { version = "35.0.0", default-features = false } +pallet-timestamp = { version = "35.0.0", default-features = false } +pop-runtime-devnet = { path = "../runtime/devnet", default-features = false } +sp-io = { version = "37.0.0", default-features = false } +sp-runtime = { version = "=38.0.0", default-features = false } + +[lib] +crate-type = [ "rlib" ] +name = "pop_sandbox" +path = "src/lib.rs" + +[features] +default = [ "std" ] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "pallet-assets/std", + "pallet-balances/std", + "pallet-contracts/std", + "pallet-timestamp/std", + "pop-runtime-devnet/std", + "scale-info/std", + "sp-io/std", + "sp-runtime/std", +] diff --git a/pop-sandbox/README.md b/pop-sandbox/README.md new file mode 100644 index 00000000..b911cd17 --- /dev/null +++ b/pop-sandbox/README.md @@ -0,0 +1,46 @@ +# Pop Sandbox + +Implementation of the `pop_drink::Sandbox` struct for the Pop Network runtimes required for the quasi testing with `drink`. + +## Getting Started + +### Installation + +```toml +pop_drink = { version = "1.0.0", package = "pop-drink" } +``` + +### Import Sandbox for the specific runtime + +For mainnet + +```rs +use pop_sandbox::MainnetSandbox; +``` + +For devnet + +```rs +use pop_sandbox::DevnetSandbox; +``` + +For testnet + +```rs +use pop_sandbox::TestnetSandbox; +``` + +### Setup test environment for your contract + +```rs +use drink::session::Session; +use pop_sandbox::DevnetSandbox as Sandbox; + +#[drink::contract_bundle_provider] +enum BundleProvider {} + +#[drink::test(sandbox = Sandbox)] +fn test(mut session: Session) { + // Your test case +} +``` diff --git a/pop-sandbox/examples/.gitignore b/pop-sandbox/examples/.gitignore new file mode 100644 index 00000000..d60800c8 --- /dev/null +++ b/pop-sandbox/examples/.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 diff --git a/pop-sandbox/examples/flipper/Cargo.toml b/pop-sandbox/examples/flipper/Cargo.toml new file mode 100644 index 00000000..1080f85e --- /dev/null +++ b/pop-sandbox/examples/flipper/Cargo.toml @@ -0,0 +1,28 @@ +[package] +authors = [ "R0GUE " ] +edition = "2021" +name = "api_example_flipper" +version = "0.1.0" + +[dependencies] +ink = { version = "=5.0.0", default-features = false, features = [ "ink-debug" ] } + +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] +drink = { path = "../../../../pop-drink/drink" } +env_logger = { version = "0.11.3" } +pop-sandbox = { path = "../../../pop-sandbox", default-features = false } + +[lib] +path = "lib.rs" + +[features] +default = [ "std" ] +ink-as-dependency = [ ] +std = [ "ink/std", "pop-sandbox/std", "scale-info/std", "scale/std" ] diff --git a/pop-sandbox/examples/flipper/lib.rs b/pop-sandbox/examples/flipper/lib.rs new file mode 100644 index 00000000..cf0352a9 --- /dev/null +++ b/pop-sandbox/examples/flipper/lib.rs @@ -0,0 +1,169 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +/// This is the classical flipper contract. It stores a single `bool` value in its storage. The +/// contract exposes: +/// - a constructor (`new`) that initializes the `bool` value to the given value, +/// - a message `flip` that flips the stored `bool` value from `true` to `false` or vice versa, +/// - a getter message `get` that returns the current `bool` value. +/// +/// Additionally, we use the `debug_println` macro from the `ink_env` crate to produce some debug +/// logs from the contract. +#[ink::contract] +mod flipper { + use ink::env::debug_println; + + #[ink(storage)] + pub struct Flipper { + value: bool, + } + + impl Flipper { + #[ink(constructor, payable)] + pub fn new(init: bool) -> Self { + debug_println!("Initializing contract with: `{init}`"); + Self { value: init } + } + + #[ink(message)] + pub fn flip(&mut self) { + debug_println!("Previous value: `{}`", self.value); + self.value = !self.value; + debug_println!("Flipped to: `{}`", self.value); + } + + #[ink(message)] + pub fn get(&self) -> bool { + debug_println!("Reading value from storage"); + self.value + } + } +} + +/// We put `drink`-based tests as usual unit tests, into a test module. +#[cfg(test)] +mod tests { + use drink::{ + sandbox_api::contracts_api::decode_debug_buffer, + session::{Session, NO_ARGS, NO_SALT}, + }; + + /// `drink` automatically discovers all the contract projects that your tests will need. For + /// every such dependency (including the contract from the current crate), it will generate a + /// [`ContractBundle`](drink::session::ContractBundle) object that contains the compiled + /// contract's code and a special transcoder, which is used to encode and decode the contract's + /// message arguments. Such a bundle will be useful when deploying a contract. + /// + /// To get a convenient way for obtaining such bundles, we can define an empty enum and mark + /// it with the [`drink::contract_bundle_provider`](drink::contract_bundle_provider) attribute. + /// From now on, we can use it in all testcases in this module. + #[drink::contract_bundle_provider] + enum BundleProvider {} + + /// Now we write the simplest contract test, that will: + /// 1. Deploy the contract. + /// 2. Call its `flip` method. + /// 3. Call its `get` method and ensure that the stored value has been flipped. + /// + /// We can use the [`drink::test`](drink::test) attribute to mark a function as a `drink` test. + /// This way we ensure that all the required contracts are compiled and built, so that we don't + /// have to run `cargo contract build` manually for every contract dependency. + /// + /// For convenience of using `?` operator, we mark the test function as returning a `Result`. + /// + /// `drink::test` will already provide us with a `Session` object. It is a wrapper around a + /// runtime and it exposes a broad API for interacting with it. Session is generic over the + /// runtime type, but usually and by default, we use `MinimalSandbox`, which is a minimalistic + /// runtime that allows using smart contracts. + #[drink::test(sandbox = pop_sandbox::PopSandbox)] + fn deploy_and_call_a_contract(mut session: Session) -> Result<(), Box> { + let _ = env_logger::try_init(); + // Now we get the contract bundle from the `BundleProvider` enum. Since the current crate + // comes with a contract, we can use the `local` method to get the bundle for it. + let contract_bundle = BundleProvider::local()?; + + // We can now deploy the contract. + let _contract_address = session.deploy_bundle( + // The bundle that we want to deploy. + contract_bundle, + // The constructor that we want to call. + "new", + // The constructor arguments (as stringish objects). + &["true"], + // Salt for the contract address derivation. + NO_SALT, + // Initial endowment (the amount of tokens that we want to transfer to the contract). + None, + )?; + + // Once the contract is instantiated, we can call the `flip` method on the contract. + session.call( + // The message that we want to call. + "flip", + // The message arguments (as stringish objects). If none, then we can use the `NO_ARGS` + // constant, which spares us from typing `&[]`. + NO_ARGS, + // Endowment (the amount of tokens that we want to transfer to the contract). + None, + )??; + + // Finally, we can call the `get` method on the contract and ensure that the value has been + // flipped. + // + // `Session::call` returns a `Result, SessionError>`, where `T` is the + // type of the message result. In this case, the `get` message returns a `bool`, and we have + // to explicitly hint the compiler about it. + let result: bool = session.call("get", NO_ARGS, None)??; + assert_eq!(result, false); + + Ok(()) + } + + /// In this testcase we will see how to get and read debug logs from the contract. + #[drink::test(sandbox = pop_sandbox::PopSandbox)] + fn get_debug_logs(mut session: Session) -> Result<(), Box> { + session.deploy_bundle(BundleProvider::local()?, "new", &["true"], NO_SALT, None)?; + + // `deploy_bundle` returns just a contract address. If we are interested in more details + // about last operation (either deploy or call), we can get a `Record` object and use its + // `last_deploy_result` (or analogously `last_call_result`) method, which will provide us + // with a full report from the last contract interaction. + // + // In particular, we can get the decoded debug buffer from the contract. The buffer is + // just a vector of bytes, which we can decode using the `decode_debug_buffer` function. + let decoded_buffer = &session.record().last_deploy_result().debug_message; + let encoded_buffer = decode_debug_buffer(decoded_buffer); + + assert_eq!(encoded_buffer, vec!["Initializing contract with: `true`"]); + + Ok(()) + } + + /// In this testcase we will see how to work with multiple contracts. + #[drink::test(sandbox = pop_sandbox::PopSandbox)] + fn work_with_multiple_contracts( + mut session: Session, + ) -> Result<(), Box> { + let bundle = BundleProvider::local()?; + + // We can deploy the same contract multiple times. However, we have to ensure that the + // derived contract addresses are different. We can do this by providing using different + // arguments for the constructor or by providing a different salt. + let first_address = + session.deploy_bundle(bundle.clone(), "new", &["true"], NO_SALT, None)?; + let _second_address = + session.deploy_bundle(bundle.clone(), "new", &["true"], vec![0], None)?; + let _third_address = session.deploy_bundle(bundle, "new", &["false"], NO_SALT, None)?; + + // By default, when we run `session.call`, `drink` will interact with the last deployed + // contract. + let value_at_third_contract: bool = session.call("get", NO_ARGS, None)??; + assert_eq!(value_at_third_contract, false); + + // However, we can also call a specific contract by providing its address. + let value_at_first_contract: bool = + session.call_with_address(first_address, "get", NO_ARGS, None)??; + assert_eq!(value_at_first_contract, true); + + Ok(()) + } +} diff --git a/pop-sandbox/src/lib.rs b/pop-sandbox/src/lib.rs new file mode 100644 index 00000000..dbdb9a06 --- /dev/null +++ b/pop-sandbox/src/lib.rs @@ -0,0 +1,87 @@ +use frame_support::{ + sp_runtime::{ + traits::{Header, One}, + }, + traits::Hooks, +}; +use frame_system::pallet_prelude::BlockNumberFor; +use pop_runtime_devnet::{BuildStorage, Runtime}; +pub use pop_runtime_devnet::Balance; +pub use frame_support::sp_runtime::AccountId32; + +/// Alias for the account ID type. +pub type AccountIdFor = ::AccountId; + +/// Default initial balance for the default account. +pub const UNIT: Balance = 10_000_000_000; +pub const INIT_AMOUNT: Balance = 100_000_000 * UNIT; +pub const INIT_VALUE: Balance = 100 * UNIT; +pub const ALICE: AccountId32 = AccountId32::new([1u8; 32]); +pub const BOB: AccountId32 = AccountId32::new([2_u8; 32]); +pub const CHARLIE: AccountId32 = AccountId32::new([3_u8; 32]); + +/// A helper struct for initializing and finalizing blocks. +pub struct BlockBuilder(std::marker::PhantomData); + +impl< + T: pallet_balances::Config + pallet_timestamp::Config + pallet_contracts::Config, + > BlockBuilder +{ + /// Create a new externalities with the given balances. + pub fn new_ext(balances: Vec<(T::AccountId, T::Balance)>) -> sp_io::TestExternalities { + let mut storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { balances } + .assimilate_storage(&mut storage) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(storage); + + ext.execute_with(|| Self::initialize_block(BlockNumberFor::::one(), Default::default())); + ext + } + + /// Initialize a new block at particular height. + pub fn initialize_block( + height: frame_system::pallet_prelude::BlockNumberFor, + parent_hash: ::Hash, + ) { + frame_system::Pallet::::reset_events(); + frame_system::Pallet::::initialize(&height, &parent_hash, &Default::default()); + pallet_balances::Pallet::::on_initialize(height); + // TODO: Resolve an issue with pallet-aura to simulate the time. + // pallet_timestamp::Pallet::::set_timestamp( + // SystemTime::now() + // .duration_since(SystemTime::UNIX_EPOCH) + // .expect("Time went backwards") + // .as_secs(), + // ); + pallet_timestamp::Pallet::::on_initialize(height); + pallet_contracts::Pallet::::on_initialize(height); + frame_system::Pallet::::note_finished_initialize(); + } + + /// Finalize a block at particular height. + pub fn finalize_block( + height: frame_system::pallet_prelude::BlockNumberFor, + ) -> ::Hash { + pallet_contracts::Pallet::::on_finalize(height); + pallet_timestamp::Pallet::::on_finalize(height); + pallet_balances::Pallet::::on_finalize(height); + frame_system::Pallet::::finalize().hash() + } +} + +pub struct Sandbox { + ext: sp_io::TestExternalities, +} + +impl Default for Sandbox { + fn default() -> Self { + let balances: Vec<(AccountId32, u128)> = vec![(ALICE, INIT_AMOUNT), (BOB, INIT_AMOUNT), (CHARLIE, INIT_AMOUNT)]; + let ext = BlockBuilder::::new_ext(balances); + Self { ext } + } +} + +drink::impl_sandbox!(Sandbox, Runtime, BlockBuilder, ALICE);