diff --git a/Cargo.lock b/Cargo.lock index d798562b..cf7abf73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9675,8 +9675,6 @@ version = "0.0.0" dependencies = [ "bounded-collections 0.1.9", "parity-scale-codec", - "scale-decode", - "scale-encode", "scale-info", ] @@ -9744,6 +9742,7 @@ dependencies = [ "polkadot-runtime-common", "pop-primitives", "pop-runtime-common", + "rand", "scale-info", "smallvec", "sp-api", diff --git a/pop-api/Cargo.toml b/pop-api/Cargo.toml index 82ee7613..dc48ea8a 100644 --- a/pop-api/Cargo.toml +++ b/pop-api/Cargo.toml @@ -9,9 +9,8 @@ edition = "2021" enumflags2 = { version = "0.7.7" } ink = { version = "5.0.0", default-features = false } sp-io = { version = "31.0.0", default-features = false, features = ["disable_panic_handler", "disable_oom", "disable_allocator"] } -sp-runtime = { version = "32.0.0", default-features = false } -pop-primitives = { path = "../primitives", features = ["devnet"], default-features = false } +pop-primitives = { path = "../primitives", default-features = false } [lib] name = "pop_api" @@ -25,7 +24,6 @@ std = [ "ink/std", "pop-primitives/std", "sp-io/std", - "sp-runtime/std", ] assets = ["pop-primitives/assets"] balances = [] diff --git a/pop-api/examples/fungibles/lib.rs b/pop-api/examples/fungibles/lib.rs index 1794fbe5..03841b74 100755 --- a/pop-api/examples/fungibles/lib.rs +++ b/pop-api/examples/fungibles/lib.rs @@ -8,13 +8,13 @@ use ink::prelude::vec::Vec; use pop_api::{ assets::fungibles::{self as api}, - error::StatusCode, - primitives::{AccountId as AccountId32, AssetId}, + primitives::AssetId, + StatusCode, }; pub type Result = core::result::Result; -#[ink::contract(env = pop_api::Environment)] +#[ink::contract] mod fungibles { use super::*; @@ -41,141 +41,84 @@ mod fungibles { #[ink(message)] pub fn total_supply(&self, id: AssetId) -> Result { - // api::total_supply(id).map_err(|e| e.into()) api::total_supply(id) } #[ink(message)] - pub fn balance_of(&self, id: AssetId, owner: AccountId32) -> Result { - // api::balance_of(id, owner).map_err(|e| e.into()) + pub fn balance_of(&self, id: AssetId, owner: AccountId) -> Balance { api::balance_of(id, owner) } #[ink(message)] - pub fn allowance( - &self, - id: AssetId, - owner: AccountId32, - spender: AccountId32, - ) -> Result { - // api::allowance(id, owner, spender).map_err(|e| e.into()) + pub fn allowance(&self, id: AssetId, owner: AccountId, spender: AccountId) -> Balance { api::allowance(id, owner, spender) } #[ink(message)] - pub fn transfer(&self, id: AssetId, to: AccountId32, value: Balance) -> Result<()> { - ink::env::debug_println!( - "PopApiFungiblesExample::transfer: id: {:?}, to: {:?} value: {:?}", - id, - to, - value, - ); - - let result = api::transfer(id, to, value); - ink::env::debug_println!("Result: {:?}", result); - // result.map_err(|e| e.into()) - result + pub fn transfer(&self, id: AssetId, to: AccountId, value: Balance) -> Result<()> { + api::transfer(id, to, value)?; + Ok(()) } #[ink(message)] pub fn transfer_from( &self, id: AssetId, - from: AccountId32, - to: AccountId32, + from: AccountId, + to: AccountId, value: Balance, // In the standard a `[u8]`, but the size needs to be known at compile time. _data: Vec, ) -> Result<()> { - ink::env::debug_println!( - "PopApiFungiblesExample::transfer_from: id: {:?}, from: {:?}, to: {:?} value: {:?}", - id, - from, - to, - value, - ); - - let result = api::transfer_from(id, from, to, value); - ink::env::debug_println!("Result: {:?}", result); - // result.map_err(|e| e.into()) - result + api::transfer_from(id, from, to, value)?; + Ok(()) } #[ink(message)] - pub fn approve(&self, id: AssetId, spender: AccountId32, value: Balance) -> Result<()> { - ink::env::debug_println!( - "PopApiFungiblesExample::approve: id: {:?}, spender {:?}, value: {:?}", - id, - spender, - value, - ); - - let result = api::approve(id, spender, value); - ink::env::debug_println!("Result: {:?}", result); - // result.map_err(|e| e.into()) - result + pub fn approve(&self, id: AssetId, spender: AccountId, value: Balance) -> Result<()> { + api::approve(id, spender, value)?; + Ok(()) } #[ink(message)] pub fn increase_allowance( &self, id: AssetId, - spender: AccountId32, + spender: AccountId, value: Balance, ) -> Result<()> { - ink::env::debug_println!( - "PopApiFungiblesExample::increase_allowance: id: {:?}, spender {:?}, value: {:?}", - id, - spender, - value, - ); - - let result = api::increase_allowance(id, spender, value); - ink::env::debug_println!("Result: {:?}", result); - // result.map_err(|e| e.into()) - result + api::increase_allowance(id, spender, value)?; + Ok(()) } #[ink(message)] pub fn decrease_allowance( &self, id: AssetId, - spender: AccountId32, + spender: AccountId, value: Balance, ) -> Result<()> { - ink::env::debug_println!( - "PopApiFungiblesExample::decrease_allowance: id: {:?}, spender {:?}, value: {:?}", - id, - spender, - value, - ); - - let result = api::decrease_allowance(id, spender, value); - ink::env::debug_println!("Result: {:?}", result); - // result.map_err(|e| e.into()) - result + api::decrease_allowance(id, spender, value)?; + Ok(()) } - /// 2. PSP-22 Metadata Interface: - /// - token_name - /// - token_symbol - /// - token_decimals + // 2. PSP-22 Metadata Interface: + // - token_name + // - token_symbol + // - token_decimals #[ink(message)] - pub fn token_name(&self, id: AssetId) -> Result> { - // api::token_name(id).map_err(|e| e.into()) + pub fn token_name(&self, id: AssetId) -> Vec { api::token_name(id) } #[ink(message)] - pub fn token_symbol(&self, id: AssetId) -> Result> { - // api::token_symbol(id).map_err(|e| e.into()) + pub fn token_symbol(&self, id: AssetId) -> Vec { api::token_symbol(id) } #[ink(message)] - pub fn token_decimals(&self, id: AssetId) -> Result { - // api::token_decimals(id).map_err(|e| e.into()) + pub fn token_decimals(&self, id: AssetId) -> u8 { api::token_decimals(id) } @@ -189,7 +132,7 @@ mod fungibles { // - clear_metadata // #[ink(message)] - // pub fn create(&self, id: AssetId, admin: AccountId32, min_balance: Balance) -> Result<()> { + // pub fn create(&self, id: AssetId, admin: AccountId, min_balance: Balance) -> Result<()> { // ink::env::debug_println!( // "PopApiFungiblesExample::create: id: {:?} admin: {:?} min_balance: {:?}", // id, @@ -198,8 +141,8 @@ mod fungibles { // ); // let result = api::create(id, admin, min_balance); // ink::env::debug_println!("Result: {:?}", result); - // // result.map_err(|e| e.into()) - // result + // result.map_err(|e| e.into()) + // result // } // #[ink(message)] diff --git a/pop-api/integration-tests/Cargo.toml b/pop-api/integration-tests/Cargo.toml index 84769433..c855a884 100644 --- a/pop-api/integration-tests/Cargo.toml +++ b/pop-api/integration-tests/Cargo.toml @@ -10,7 +10,7 @@ frame-support = { version = "29.0.0", default-features = false } frame-system = { version = "29.0.0", default-features = false } pallet-balances = { version = "29.0.2", default-features = false } pallet-contracts = { version = "28.0.0", default-features = false } -pop-api = { path = "../.", default-features = false, features = ["assets"] } +pop-primitives = { path = "../../primitives", default-features = false, features = ["assets"] } pop-runtime-devnet = { path = "../../runtime/devnet", default-features = false } sp-io = { version = "31.0.0", default-features = false } sp-runtime = { version = "32.0.0", default-features = false } @@ -23,7 +23,7 @@ std = [ "frame-system/std", "pallet-balances/std", "pallet-contracts/std", - "pop-api/std", + "pop-primitives/std", "pop-runtime-devnet/std", "scale/std", "sp-io/std", diff --git a/pop-api/integration-tests/src/local_fungibles.rs b/pop-api/integration-tests/src/local_fungibles.rs index 1b3c6633..968c43eb 100644 --- a/pop-api/integration-tests/src/local_fungibles.rs +++ b/pop-api/integration-tests/src/local_fungibles.rs @@ -1,5 +1,5 @@ use super::*; -use pop_api::error::{ +use pop_primitives::error::{ ArithmeticError::*, Error::{self, *}, TokenError::*, @@ -8,7 +8,10 @@ use pop_api::error::{ const ASSET_ID: AssetId = 1; fn decoded(result: ExecReturnValue) -> T { - ::decode(&mut &result.data[2..]).unwrap() + match ::decode(&mut &result.data[2..]) { + Ok(value) => value, + Err(_) => panic!("\nTest failed by trying to decode result: {:?} into `T`\n", result), + } } fn allowance( @@ -78,7 +81,9 @@ fn transfer( ) -> ExecReturnValue { let function = function_selector("transfer"); let params = [function, asset_id.encode(), to.encode(), value.encode()].concat(); - do_bare_call(addr, params, 0).expect("should work") + let result = do_bare_call(addr, params, 0).expect("should work"); + println!("Transfer result: {:?}", result); + result } fn transfer_from( diff --git a/pop-api/src/error.rs b/pop-api/src/error.rs deleted file mode 100644 index 8989464f..00000000 --- a/pop-api/src/error.rs +++ /dev/null @@ -1,335 +0,0 @@ -use ink::{env::chain_extension::FromStatusCode, scale::Decode}; - -#[derive(Debug, PartialEq, Eq)] -#[ink::scale_derive(Encode, Decode, TypeInfo)] -pub struct StatusCode(pub u32); - -impl From for StatusCode { - fn from(value: u32) -> Self { - StatusCode(value) - } -} -impl FromStatusCode for StatusCode { - fn from_status_code(status_code: u32) -> Result<(), Self> { - match status_code { - 0 => Ok(()), - _ => Err(StatusCode(status_code)), - } - } -} - -impl From for StatusCode { - fn from(_: ink::scale::Error) -> Self { - StatusCode(255u32) - } -} - -#[derive(Debug, PartialEq, Eq)] -#[ink::scale_derive(Encode, Decode, TypeInfo)] -#[repr(u8)] -pub enum Error { - /// Some unknown error occurred. Go to the Pop API docs section `Pop API error`. - Other { - // Index within the `DispatchError` - dispatch_error_index: u8, - // Index within the `DispatchError` variant. - error_index: u8, - // Index for further nesting, e.g. pallet error. - error: u8, - } = 0, - /// Failed to lookup some data. - CannotLookup = 1, - /// A bad origin. - BadOrigin = 2, - /// A custom error in a module. - Module { - index: u8, - error: u8, - } = 3, - /// At least one consumer is remaining so the account cannot be destroyed. - ConsumerRemaining = 4, - /// There are no providers so the account cannot be created. - NoProviders = 5, - /// There are too many consumers so the account cannot be created. - TooManyConsumers = 6, - /// An error to do with tokens. - Token(TokenError) = 7, - /// An arithmetic error. - Arithmetic(ArithmeticError) = 8, - /// The number of transactional layers has been reached, or we are not in a transactional - /// layer. - Transactional(TransactionalError) = 9, - /// Resources exhausted, e.g. attempt to read/write data which is too large to manipulate. - Exhausted = 10, - /// The state is corrupt; this is generally not going to fix itself. - Corruption = 11, - /// Some resource (e.g. a preimage) is unavailable right now. This might fix itself later. - Unavailable = 12, - /// Root origin is not allowed. - RootNotAllowed = 13, - UnknownFunctionId = 254, - DecodingFailed = 255, -} - -#[cfg(test)] -impl From for StatusCode { - fn from(value: Error) -> Self { - let mut encoded_error = value.encode(); - // Resize the encoded value to 4 bytes in order to decode the value in a u32 (4 bytes). - encoded_error.resize(4, 0); - StatusCode::from( - u32::decode(&mut &encoded_error[..]).expect("qid, resized to 4 bytes line above"), - ) - } -} - -impl From for Error { - fn from(value: StatusCode) -> Self { - let mut encoded: [u8; 4] = value.0.to_le_bytes(); - match Error::decode(&mut &encoded[..]) { - Err(_) => { - encoded[..].rotate_right(1); - encoded[0] = 0u8; - Error::decode(&mut &encoded[..]).unwrap_or(Error::DecodingFailed) - }, - Ok(error) => error, - } - } -} - -#[derive(Debug, PartialEq, Eq)] -#[ink::scale_derive(Encode, Decode, TypeInfo)] -pub enum TokenError { - /// Funds are unavailable. - FundsUnavailable, - /// Some part of the balance gives the only provider reference to the account and thus cannot - /// be (re)moved. - OnlyProvider, - /// Account cannot exist with the funds that would be given. - BelowMinimum, - /// Account cannot be created. - CannotCreate, - /// The asset in question is unknown. - UnknownAsset, - /// Funds exist but are frozen. - Frozen, - /// Operation is not supported by the asset. - Unsupported, - /// Account cannot be created for a held balance. - CannotCreateHold, - /// Withdrawal would cause unwanted loss of account. - NotExpendable, - /// Account cannot receive the assets. - Blocked, -} - -#[derive(Debug, PartialEq, Eq)] -#[ink::scale_derive(Encode, Decode, TypeInfo)] -pub enum ArithmeticError { - /// Underflow. - Underflow, - /// Overflow. - Overflow, - /// Division by zero. - DivisionByZero, -} - -#[derive(Debug, PartialEq, Eq)] -#[ink::scale_derive(Encode, Decode, TypeInfo)] -pub enum TransactionalError { - /// Too many transactional layers have been spawned. - LimitReached, - /// A transactional layer was expected, but does not exist. - NoLayer, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::error::{ArithmeticError::*, TokenError::*, TransactionalError::*}; - - #[test] - fn u32_always_encodes_to_4_bytes() { - assert_eq!(0u32.encode().len(), 4); - assert_eq!(u32::MAX.encode().len(), 4); - } - - // Decodes 4 bytes into a `u32` and converts it into `StatusCode`. - fn into_status_code(encoded_error: [u8; 4]) -> StatusCode { - let decoded_u32 = u32::decode(&mut &encoded_error[..]).unwrap(); - StatusCode::from_status_code(decoded_u32).unwrap_err() - } - - // Decodes 4 bytes into a `u32` and converts it into `Error`. - fn into_error(encoded_error: [u8; 4]) -> Error { - let decoded_u32 = u32::decode(&mut &encoded_error[..]).unwrap(); - let status_code = StatusCode::from_status_code(decoded_u32).unwrap_err(); - status_code.into() - } - - // Tests the `From` implementation for `Error`. - // - // Unit variants: - // If the encoded value indicates a nested `Error` which is known by the Pop API version as a - // unit variant, the encoded value is converted into `Error::Other`. - // - // Example: the error `BadOrigin` (encoded: `[2, 0, 0, 0]`) with a non-zero value for one - // of the bytes [1..4]: `[2, 0, 1, 0]` is converted into `[0, 2, 0, 1]`. This is decoded to - // `Error::Other { dispatch_error: 2, index: 0, error: 1 }`. - #[test] - fn unit_error_variants() { - let errors = vec![ - CannotLookup, - BadOrigin, - ConsumerRemaining, - NoProviders, - TooManyConsumers, - Exhausted, - Corruption, - Unavailable, - RootNotAllowed, - DecodingFailed, - ]; - // Four scenarios, 2 tests each: - // 1. Compare a `StatusCode`, which is converted from an encoded value, with a `StatusCode` - // converted from an `Error`. - // 2. Compare an `Error, which is converted from an encoded value, with the expected `Error`. - for (i, &error_code) in UNIT_ERRORS.iter().enumerate() { - // No nesting and unit variant correctly returned. - assert_eq!(into_status_code([error_code, 0, 0, 0]), errors[i].into()); - assert_eq!(into_error([error_code, 0, 0, 0]), errors[i]); - // Unexpected second byte nested. - assert_eq!( - into_status_code([error_code, 1, 0, 0]), - (Other { dispatch_error_index: error_code, error_index: 1, error: 0 }).into(), - ); - assert_eq!(into_error([error_code, 1, 0, 0]), errors[i]); - // Unexpected third byte nested. - assert_eq!( - into_status_code([error_code, 1, 1, 0]), - (Other { dispatch_error_index: error_code, error_index: 1, error: 1 }).into(), - ); - assert_eq!(into_error([error_code, 1, 1, 0]), errors[i]); - // Unexpected fourth byte nested. - assert_eq!( - into_status_code([error_code, 1, 1, 1]), - (Other { dispatch_error_index: error_code, error_index: 1, error: 1 }).into(), - ); - assert_eq!(into_error([error_code, 1, 1, 1]), errors[i]); - } - } - - // Single nested variants: - // If the encoded value indicates a double nested `Error` which is known by the Pop API version - // as a single nested variant, the encoded value is converted into `Error::Other`. - // - // Example: the error `Arithmetic(Overflow)` (encoded: `[8, 1, 0, 0]`) with a non-zero - // value for one of the bytes [2..4]: `[8, 1, 1, 0]` is converted into `[0, 8, 1, 1]`. This is - // decoded to `Error::Other { dispatch_error: 8, index: 1, error: 1 }`. - #[test] - fn single_nested_error_variants() { - let errors = vec![ - [Token(FundsUnavailable), Token(OnlyProvider)], - [Arithmetic(Underflow), Arithmetic(Overflow)], - [Transactional(LimitReached), Transactional(NoLayer)], - ]; - // Four scenarios, 2 tests each: - // 1. Compare a `StatusCode`, which is converted from an encoded value, with a `StatusCode` - // converted from an `Error`. - // 2. Compare an `Error, which is converted from an encoded value, with the expected `Error`. - for (i, &error_code) in SINGLE_NESTED_ERRORS.iter().enumerate() { - // No nesting and unit variant correctly returned. - assert_eq!(into_status_code([error_code, 0, 0, 0]), errors[i][0].into()); - assert_eq!(into_error([error_code, 0, 0, 0]), errors[i][0]); - // Allowed single nesting variant correctly returned. - assert_eq!(into_status_code([error_code, 1, 0, 0]), errors[i][1].into()); - assert_eq!(into_error([error_code, 1, 0, 0]), errors[i][1]); - // Unexpected third byte nested. - assert_eq!( - into_status_code([error_code, 1, 1, 0]), - (Other { dispatch_error_index: error_code, error_index: 1, error: 1 }).into(), - ); - assert_eq!(into_error([error_code, 1, 1, 0]), errors[i][1]); - // Unexpected fourth byte nested. - assert_eq!( - into_status_code([error_code, 1, 1, 1]), - (Other { dispatch_error_index: error_code, error_index: 1, error: 1 }).into(), - ); - assert_eq!(into_error([error_code, 1, 1, 1]), errors[i][1]); - } - } - - // Double nested variants: - // If the encoded value indicates a triple nested `Error` which is known by the Pop API version - // as a double nested variant, the encoded value is converted into `Error::Other`. - // - // Example: the error `Module { index: 10, error 5 }` (encoded: `[3, 10, 5, 0]`) with a non-zero - // value for the last byte: `[3, 10, 5, 3]` is converted into `[0, 3, 10, 5]`. This is - // decoded to `Error::Other { dispatch_error: 3, index: 10, error: 5 }`. - #[test] - fn double_nested_error_variants() { - // Four scenarios, 2 tests each: - // 1. Compare a `StatusCode`, which is converted from an encoded value, with a `StatusCode` - // converted from an `Error`. - // 2. Compare an `Error, which is converted from an encoded value, with the expected `Error`. - // - // No nesting and unit variant correctly returned. - assert_eq!(into_status_code([3, 0, 0, 0]), (Module { index: 0, error: 0 }).into()); - assert_eq!(into_error([3, 0, 0, 0]), Module { index: 0, error: 0 }); - // Allowed single nesting and variant correctly returned. - assert_eq!(into_status_code([3, 1, 0, 0]), (Module { index: 1, error: 0 }).into()); - assert_eq!(into_error([3, 1, 0, 0]), Module { index: 1, error: 0 }); - // Allowed double nesting and variant correctly returned. - assert_eq!(into_status_code([3, 1, 1, 0]), (Module { index: 1, error: 1 }).into()); - assert_eq!(into_error([3, 1, 1, 0]), Module { index: 1, error: 1 }); - // Unexpected fourth byte nested. - assert_eq!( - into_status_code([3, 1, 1, 1]), - (Other { dispatch_error_index: 3, error_index: 1, error: 1 }).into(), - ); - assert_eq!(into_error([3, 1, 1, 1]), Module { index: 1, error: 1 }); - } - - #[test] - fn single_nested_unknown_variants() { - // Unknown `TokenError` variant. - assert_eq!( - into_error([7, 10, 0, 0]), - Other { dispatch_error_index: 7, error_index: 10, error: 0 } - ); - assert_eq!( - into_status_code([7, 10, 0, 0]), - Other { dispatch_error_index: 7, error_index: 10, error: 0 }.into() - ); - // Unknown `Arithmetic` variant. - assert_eq!( - into_error([8, 3, 0, 0]), - Other { dispatch_error_index: 8, error_index: 3, error: 0 } - ); - assert_eq!( - into_status_code([8, 3, 0, 0]), - Other { dispatch_error_index: 8, error_index: 3, error: 0 }.into() - ); - // Unknown `Transactional` variant. - assert_eq!( - into_error([9, 2, 0, 0]), - Other { dispatch_error_index: 9, error_index: 2, error: 0 } - ); - assert_eq!( - into_status_code([9, 2, 0, 0]), - Other { dispatch_error_index: 9, error_index: 2, error: 0 }.into() - ); - } - - #[test] - fn test_random_encoded_values() { - assert_eq!( - into_error([100, 100, 100, 100]), - Other { dispatch_error_index: 100, error_index: 100, error: 100 } - ); - assert_eq!( - into_error([200, 200, 200, 200]), - Other { dispatch_error_index: 200, error_index: 200, error: 200 } - ); - } -} diff --git a/pop-api/src/lib.rs b/pop-api/src/lib.rs index 55307f3e..cb27b17f 100644 --- a/pop-api/src/lib.rs +++ b/pop-api/src/lib.rs @@ -1,10 +1,8 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] -use ink::{prelude::vec::Vec, ChainExtensionInstance}; -pub use sp_runtime::MultiAddress; +use ink::env::{chain_extension::FromStatusCode, DefaultEnvironment, Environment}; +use primitives::error::Error; -use crate::error::StatusCode; -use primitives::{storage_keys::*, AccountId as AccountId32}; #[cfg(feature = "assets")] pub use v0::assets; #[cfg(feature = "balances")] @@ -13,72 +11,49 @@ pub use v0::balances; pub use v0::cross_chain; #[cfg(feature = "nfts")] pub use v0::nfts; -use v0::{state, RuntimeCall}; -pub mod error; pub mod primitives; pub mod v0; -type AccountId = AccountId32; -// TODO: do the same as the AccountId above and check expanded macro code. -type Balance = ::Balance; +type AccountId = ::AccountId; +type Balance = ::Balance; #[cfg(any(feature = "nfts", feature = "cross-chain"))] -type BlockNumber = ::BlockNumber; +type BlockNumber = ::BlockNumber; pub type Result = core::result::Result; -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[ink::scale_derive(Encode, Decode, TypeInfo)] -pub enum Environment {} +pub struct StatusCode(pub u32); -impl ink::env::Environment for Environment { - const MAX_EVENT_TOPICS: usize = - ::MAX_EVENT_TOPICS; - - type AccountId = ::AccountId; - type Balance = ::Balance; - type Hash = ::Hash; - type BlockNumber = ::BlockNumber; - type Timestamp = ::Timestamp; - - type ChainExtension = PopApi; +impl From for StatusCode { + fn from(value: u32) -> Self { + StatusCode(value) + } } - -#[ink::chain_extension(extension = 909)] -pub trait PopApi { - type ErrorCode = StatusCode; - - #[ink(function = 0)] - #[allow(private_interfaces)] - fn dispatch(call: RuntimeCall) -> Result<()>; - - #[ink(function = 1)] - #[allow(private_interfaces)] - fn read_state(key: RuntimeStateKeys) -> Result>; - - #[cfg(feature = "cross-chain")] - #[ink(function = 2)] - #[allow(private_interfaces)] - fn send_xcm(xcm: primitives::cross_chain::CrossChainMessage) -> Result<()>; +impl FromStatusCode for StatusCode { + fn from_status_code(status_code: u32) -> Result<()> { + match status_code { + 0 => Ok(()), + _ => Err(StatusCode(status_code)), + } + } } -#[inline] -fn dispatch(call: RuntimeCall) -> Result<()> { - <::ChainExtension as ChainExtensionInstance>::instantiate( - ) - .dispatch(call) +impl From for StatusCode { + fn from(_: ink::scale::Error) -> Self { + StatusCode(255u32) + } } -#[inline] -fn read_state(key: RuntimeStateKeys) -> Result> { - <::ChainExtension as ChainExtensionInstance>::instantiate( - ) - .read_state(key) +impl From for Error { + fn from(value: StatusCode) -> Self { + value.0.into() + } } -#[cfg(feature = "cross-chain")] -fn send_xcm(xcm: primitives::cross_chain::CrossChainMessage) -> Result<()> { - <::ChainExtension as ChainExtensionInstance>::instantiate( - ) - .send_xcm(xcm) +impl From for StatusCode { + fn from(value: Error) -> Self { + StatusCode::from(u32::from(value)) + } } diff --git a/pop-api/src/primitives.rs b/pop-api/src/primitives.rs index 17419d5b..e174a111 100644 --- a/pop-api/src/primitives.rs +++ b/pop-api/src/primitives.rs @@ -1,2 +1 @@ pub use pop_primitives::*; -pub use sp_runtime::MultiAddress; diff --git a/pop-api/src/v0/assets/fungibles.rs b/pop-api/src/v0/assets/fungibles.rs index 376ca898..8d0de768 100644 --- a/pop-api/src/v0/assets/fungibles.rs +++ b/pop-api/src/v0/assets/fungibles.rs @@ -1,12 +1,6 @@ use ink::prelude::vec::Vec; -use crate::{ - assets, - primitives::{AssetId, MultiAddress}, - AccountId, Balance, StatusCode, -}; - -type Result = core::result::Result; +use crate::{assets, primitives::AssetId, AccountId, Balance, Result, StatusCode}; /// Local Fungibles: /// 1. PSP-22 Interface @@ -45,7 +39,7 @@ pub fn total_supply(id: AssetId) -> Result { /// # Returns /// The balance of the specified account, or an error if the operation fails. #[inline] -pub fn balance_of(id: AssetId, owner: AccountId) -> Result { +pub fn balance_of(id: AssetId, owner: AccountId) -> Balance { assets::balance_of(id, owner) } @@ -60,7 +54,7 @@ pub fn balance_of(id: AssetId, owner: AccountId) -> Result { /// # Returns /// The remaining allowance, or an error if the operation fails. #[inline] -pub fn allowance(id: AssetId, owner: AccountId, spender: AccountId) -> Result { +pub fn allowance(id: AssetId, owner: AccountId, spender: AccountId) -> Balance { assets::allowance(id, owner, spender) } @@ -75,11 +69,7 @@ pub fn allowance(id: AssetId, owner: AccountId, spender: AccountId) -> Result>, - value: Balance, -) -> Result<()> { +pub fn transfer(id: AssetId, to: AccountId, value: Balance) -> Result<()> { assets::transfer(id, to, value) } @@ -96,12 +86,7 @@ pub fn transfer( /// # Returns /// Returns `Ok(())` if successful, or an error if the transfer fails. #[inline] -pub fn transfer_from( - id: AssetId, - from: impl Into>, - to: impl Into>, - value: Balance, -) -> Result<()> { +pub fn transfer_from(id: AssetId, from: AccountId, to: AccountId, value: Balance) -> Result<()> { assets::transfer_approved(id, from, to, value) } @@ -161,7 +146,7 @@ pub fn decrease_allowance(id: AssetId, spender: AccountId, value: Balance) -> Re /// # Returns /// The name of the token as a byte vector, or an error if the operation fails. #[inline] -pub fn token_name(id: AssetId) -> Result> { +pub fn token_name(id: AssetId) -> Vec { assets::token_name(id) } @@ -173,7 +158,7 @@ pub fn token_name(id: AssetId) -> Result> { /// # Returns /// The symbol of the token as a byte vector, or an error if the operation fails. #[inline] -pub fn token_symbol(id: AssetId) -> Result> { +pub fn token_symbol(id: AssetId) -> Vec { assets::token_symbol(id) } @@ -185,7 +170,7 @@ pub fn token_symbol(id: AssetId) -> Result> { /// # Returns /// The number of decimals of the token as a byte vector, or an error if the operation fails. #[inline] -pub fn token_decimals(id: AssetId) -> Result { +pub fn token_decimals(id: AssetId) -> u8 { assets::token_decimals(id) } @@ -291,6 +276,7 @@ pub fn token_decimals(id: AssetId) -> Result { // assets::asset_exists(id) // } +// TODO: further implement the rest of the interfaces and conclude on the FungiblesError. #[derive(Debug, PartialEq, Eq)] #[ink::scale_derive(Encode, Decode, TypeInfo)] pub enum FungiblesError { @@ -311,13 +297,13 @@ pub enum FungiblesError { NoPermission, /// The given asset ID is unknown. Unknown, - // // TODO: - // // - Originally `InsufficientBalance` for the deposit but this would result in the same error - // // as the error when there is insufficient balance for transferring an asset. + // - Originally `InsufficientBalance` for the deposit but this would result in the same error + // as the error when there is insufficient balance for transferring an asset. /// No balance for creation of assets or fees. NoBalance, } +// TODO: include conversions from TokenError and add conversions based on added interfaces. impl From for FungiblesError { fn from(value: StatusCode) -> Self { let encoded = value.0.to_le_bytes(); @@ -339,10 +325,10 @@ impl From for FungiblesError { #[cfg(test)] mod tests { - use scale::Decode; + use ink::scale::Decode; use super::FungiblesError; - use crate::error::{ + use crate::primitives::error::{ ArithmeticError::*, Error::{self, *}, TokenError::*, @@ -355,6 +341,7 @@ mod tests { 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); @@ -373,6 +360,7 @@ mod tests { Other { dispatch_error_index: 5, error_index: 5, error: 1 }, CannotLookup, BadOrigin, + // `ModuleError` other than assets module. Module { index: 2, error: 5 }, ConsumerRemaining, NoProviders, @@ -384,6 +372,7 @@ mod tests { Corruption, Unavailable, RootNotAllowed, + UnknownFunctionId, DecodingFailed, ]; for error in errors { diff --git a/pop-api/src/v0/assets/mod.rs b/pop-api/src/v0/assets/mod.rs index 241c5020..22657323 100644 --- a/pop-api/src/v0/assets/mod.rs +++ b/pop-api/src/v0/assets/mod.rs @@ -1,17 +1,18 @@ -use ink::{prelude::vec::Vec, scale::Compact}; +use ink::{env::chain_extension::ChainExtensionMethod, prelude::vec::Vec, scale::Decode}; -use crate::{state::read, Balance, RuntimeCall, *}; -use primitives::{AssetId, MultiAddress}; +use crate::{primitives::AssetId, AccountId, Balance, Result, StatusCode}; pub mod fungibles; -type Result = core::result::Result; +const ASSETS_MODULE: u8 = 52; +const VERSION: u8 = 0; /// [Pallet Assets](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/assets/src/lib.rs): /// 1. Dispatchables /// 2. Read state functions /// /// 1. Dispatchables within pallet assets (TrustBackedAssets instance): +const DISPATCH: u8 = 0; /// - create /// - start_destroy /// - destroy_accounts @@ -21,25 +22,12 @@ type Result = core::result::Result; /// - burn /// - transfer /// - transfer_keep_alive -/// - force_transfer -/// - freeze -/// - thaw -/// - freeze_asset -/// - thaw_asset -/// - transfer_ownership -/// - set_team +const TRANSFER_KEEP_ALIVE: u8 = 9; /// - set_metadata /// - clear_metadata /// - approve_transfer /// - cancel_approval -/// - force_cancel_approval /// - transfer_approved -/// - touch -/// - refund -/// - set_min_balance -/// - touch_other -/// - refund_other -/// - block /// Issue a new class of fungible assets from a public origin. // pub(crate) fn create( @@ -98,16 +86,18 @@ type Result = core::result::Result; /// Move some assets from the sender account to another. #[inline] -pub(crate) fn transfer( - id: AssetId, - target: impl Into>, - amount: Balance, -) -> Result<()> { - dispatch(RuntimeCall::Assets(AssetsCall::TransferKeepAlive { - id: id.into(), - target: target.into(), - amount: Compact(amount), - })) +pub fn transfer(id: AssetId, target: AccountId, amount: Balance) -> Result<()> { + ChainExtensionMethod::build(u32::from_le_bytes([ + VERSION, + DISPATCH, + ASSETS_MODULE, + // TODO: E.D. is always respected with transferring tokens via the API. + TRANSFER_KEEP_ALIVE, + ])) + .input::<(AssetId, AccountId, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(id, target, amount)) } // /// Move some assets from the sender account to another, keeping the sender account alive. @@ -122,68 +112,8 @@ pub(crate) fn transfer( // amount: Compact(amount), // })) // } -// -// /// Move some assets from one account to another. Sender should be the Admin of the asset `id`. -// pub(crate) fn force_transfer( -// id: AssetId, -// source: impl Into>, -// dest: impl Into>, -// amount: Balance, -// ) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::ForceTransfer { -// id: id.into(), -// source: source.into(), -// dest: dest.into(), -// amount: Compact(amount), -// })) -// } -// -// /// Disallow further unprivileged transfers of an asset `id` from an account `who`. `who` -// /// must already exist as an entry in `Account`s of the asset. If you want to freeze an -// /// account that does not have an entry, use `touch_other` first. -// pub(crate) fn freeze(id: AssetId, who: impl Into>) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::Freeze { id: id.into(), who: who.into() })) -// } -// -// /// Allow unprivileged transfers to and from an account again. -// pub(crate) fn thaw(id: AssetId, who: impl Into>) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::Thaw { id: id.into(), who: who.into() })) -// } -// -// /// Disallow further unprivileged transfers for the asset class. -// pub(crate) fn freeze_asset(id: AssetId) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::FreezeAsset { id: id.into() })) -// } -// -// /// Allow unprivileged transfers for the asset again. -// pub(crate) fn thaw_asset(id: AssetId) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::ThawAsset { id: id.into() })) -// } -// -// /// Change the Owner of an asset. -// pub(crate) fn transfer_ownership(id: AssetId, owner: impl Into>) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::TransferOwnership { -// id: id.into(), -// owner: owner.into(), -// })) -// } -// -// /// Change the Issuer, Admin and Freezer of an asset. -// pub(crate) fn set_team( -// id: AssetId, -// issuer: impl Into>, -// admin: impl Into>, -// freezer: impl Into>, -// ) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::SetTeam { -// id: id.into(), -// issuer: issuer.into(), -// admin: admin.into(), -// freezer: freezer.into(), -// })) -// } -/// Set the metadata for an asset. +// /// Set the metadata for an asset. // pub(crate) fn set_metadata( // id: AssetId, // name: Vec, @@ -200,315 +130,111 @@ pub(crate) fn transfer( /// Approve an amount of asset for transfer by a delegated third-party account. #[inline] -pub(crate) fn approve_transfer( - id: AssetId, - delegate: impl Into>, - amount: Balance, -) -> Result<()> { - dispatch(RuntimeCall::Assets(AssetsCall::ApproveTransfer { - id: id.into(), - delegate: delegate.into(), - amount: Compact(amount), - })) +pub fn approve_transfer(id: AssetId, delegate: AccountId, amount: Balance) -> Result<()> { + ChainExtensionMethod::build(u32::from_le_bytes([0u8, 0, 52, 69])) + .input::<(AssetId, AccountId, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(id, delegate, amount)) } /// Cancel all of some asset approved for delegated transfer by a third-party account. #[inline] -pub(crate) fn cancel_approval( - id: AssetId, - delegate: impl Into>, -) -> Result<()> { - dispatch(RuntimeCall::Assets(AssetsCall::CancelApproval { - id: id.into(), - delegate: delegate.into(), - })) +pub fn cancel_approval(id: AssetId, delegate: AccountId) -> Result<()> { + ChainExtensionMethod::build(0) + .input::<(AssetId, AccountId)>() + .output::, true>() + .handle_error_code::() + .call(&(id, delegate)) } -// /// Cancel all of some asset approved for delegated transfer by a third-party account. -// pub(crate) fn force_cancel_approval( -// id: AssetId, -// owner: impl Into>, -// delegate: impl Into>, -// ) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::ForceCancelApproval { -// id: id.into(), -// owner: owner.into(), -// delegate: delegate.into(), -// })) -// } - /// Transfer some asset balance from a previously delegated account to some third-party /// account. #[inline] -pub(crate) fn transfer_approved( +pub fn transfer_approved( id: AssetId, - owner: impl Into>, - destination: impl Into>, + from: AccountId, + to: AccountId, amount: Balance, ) -> Result<()> { - dispatch(RuntimeCall::Assets(AssetsCall::TransferApproved { - id: id.into(), - owner: owner.into(), - destination: destination.into(), - amount: Compact(amount), - })) + ChainExtensionMethod::build(0) + .input::<(AssetId, AccountId, AccountId, Balance)>() + .output::, true>() + .handle_error_code::() + .call(&(id, from, to, amount)) } -// -// /// Create an asset account for non-provider assets. -// pub(crate) fn touch(id: AssetId) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::Touch { id: id.into() })) -// } -// -// /// Return the deposit (if any) of an asset account or a consumer reference (if any) of an -// /// account. -// pub(crate) fn refund(id: AssetId, allow_burn: bool) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::Refund { id: id.into(), allow_burn })) -// } -// -// /// Sets the minimum balance of an asset. -// pub(crate) fn set_min_balance(id: AssetId, min_balance: Balance) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::SetMinBalance { -// id: id.into(), -// min_balance: Compact(min_balance), -// })) -// } -// -// /// Create an asset account for `who`. -// pub(crate) fn touch_other(id: AssetId, who: impl Into>) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::TouchOther { id: id.into(), who: who.into() })) -// } -// -// /// Return the deposit (if any) of a target asset account. Useful if you are the depositor. -// pub(crate) fn refund_other(id: AssetId, who: impl Into>) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::RefundOther { id: id.into(), who: who.into() })) -// } -// -// /// Disallow further unprivileged transfers of an asset `id` to and from an account `who`. -// pub(crate) fn block(id: AssetId, who: impl Into>) -> Result<()> { -// dispatch(RuntimeCall::Assets(AssetsCall::Block { id: id.into(), who: who.into() })) -// } /// 2. Read state functions +const READ_STATE: u8 = 1; /// - total_supply +const TOTAL_SUPPLY: u8 = 0; /// - balance_of /// - allowance /// - asset_exists /// - token_name /// - token_symbol /// - token_decimals -// + #[inline] -pub(crate) fn total_supply(id: AssetId) -> Result { - read(RuntimeStateKeys::Assets(AssetsKeys::TotalSupply(id))) +pub fn total_supply(id: AssetId) -> Result { + ChainExtensionMethod::build(u32::from_le_bytes([ + VERSION, + READ_STATE, + ASSETS_MODULE, + TOTAL_SUPPLY, + ])) + .input::() + .output::>, true>() + .handle_error_code::() + .call(&(id)) + .and_then(|v| Balance::decode(&mut &v[..]).map_err(|_e| StatusCode(255u32))) } #[inline] -pub(crate) fn balance_of(id: AssetId, owner: AccountId) -> Result { - read(RuntimeStateKeys::Assets(AssetsKeys::BalanceOf(id, owner))) +pub fn balance_of(id: AssetId, owner: AccountId) -> Balance { + ChainExtensionMethod::build(1) + .input::<(AssetId, AccountId)>() + .output::() + .ignore_error_code() + .call(&(id, owner)) } #[inline] -pub(crate) fn allowance(id: AssetId, owner: AccountId, spender: AccountId) -> Result { - read(RuntimeStateKeys::Assets(AssetsKeys::Allowance(id, owner, spender))) +pub fn allowance(id: AssetId, owner: AccountId, spender: AccountId) -> Balance { + ChainExtensionMethod::build(1) + .input::<(AssetId, AccountId, AccountId)>() + .output::() + .ignore_error_code() + .call(&(id, owner, spender)) } -// pub(crate) fn asset_exists(id: AssetId) -> Result { -// state::read(RuntimeStateKeys::Assets(AssetsKeys::AssetExists(id))) -// } #[inline] -pub(crate) fn token_name(id: AssetId) -> Result> { - read(RuntimeStateKeys::Assets(AssetsKeys::TokenName(id))) +pub fn token_name(id: AssetId) -> Vec { + ChainExtensionMethod::build(1) + .input::() + .output::, false>() + .ignore_error_code() + .call(&(id)) } // #[inline] -pub(crate) fn token_symbol(id: AssetId) -> Result> { - read(RuntimeStateKeys::Assets(AssetsKeys::TokenSymbol(id))) +pub fn token_symbol(id: AssetId) -> Vec { + ChainExtensionMethod::build(1) + .input::() + .output::, false>() + .ignore_error_code() + .call(&(id)) } #[inline] -pub(crate) fn token_decimals(id: AssetId) -> Result { - read(RuntimeStateKeys::Assets(AssetsKeys::TokenDecimals(id))) +pub fn token_decimals(id: AssetId) -> u8 { + ChainExtensionMethod::build(1) + .input::() + .output::() + .ignore_error_code() + .call(&(id)) } -// Parameters to extrinsics representing an asset id (`AssetIdParameter`) and a balance amount (`Balance`) are expected -// to be compact encoded. The pop api handles that for the developer. -// -// reference: https://substrate.stackexchange.com/questions/1873/what-is-the-meaning-of-palletcompact-in-pallet-development -// -// Asset id that is compact encoded. -type AssetIdParameter = Compact; -// Balance amount that is compact encoded. -type BalanceParameter = Compact; -// -#[derive(Debug, PartialEq, Eq)] -#[ink::scale_derive(Encode, Decode, TypeInfo)] -pub(crate) enum AssetsCall { - // #[codec(index = 0)] - // Create { id: AssetIdParameter, admin: MultiAddress, min_balance: Balance }, - // #[codec(index = 2)] - // StartDestroy { id: AssetIdParameter }, - // #[codec(index = 3)] - // DestroyAccounts { id: AssetIdParameter }, - // #[codec(index = 4)] - // DestroyApprovals { id: AssetIdParameter }, - // #[codec(index = 5)] - // FinishDestroy { id: AssetIdParameter }, - // #[codec(index = 6)] - // Mint { - // id: AssetIdParameter, - // beneficiary: MultiAddress, - // amount: BalanceParameter, - // }, - // #[codec(index = 7)] - // Burn { id: AssetIdParameter, who: MultiAddress, amount: BalanceParameter }, - // #[codec(index = 8)] - // Transfer { id: AssetIdParameter, target: MultiAddress, amount: BalanceParameter }, - #[codec(index = 9)] - TransferKeepAlive { - id: AssetIdParameter, - target: MultiAddress, - amount: BalanceParameter, - }, - // #[codec(index = 10)] - // ForceTransfer { - // id: AssetIdParameter, - // source: MultiAddress, - // dest: MultiAddress, - // amount: BalanceParameter, - // }, - // #[codec(index = 11)] - // Freeze { id: AssetIdParameter, who: MultiAddress }, - // #[codec(index = 12)] - // Thaw { id: AssetIdParameter, who: MultiAddress }, - // #[codec(index = 13)] - // FreezeAsset { id: AssetIdParameter }, - // #[codec(index = 14)] - // ThawAsset { id: AssetIdParameter }, - // #[codec(index = 15)] - // TransferOwnership { id: AssetIdParameter, owner: MultiAddress }, - // #[codec(index = 16)] - // SetTeam { - // id: AssetIdParameter, - // issuer: MultiAddress, - // admin: MultiAddress, - // freezer: MultiAddress, - // }, - // #[codec(index = 17)] - // SetMetadata { id: AssetIdParameter, name: Vec, symbol: Vec, decimals: u8 }, - // #[codec(index = 18)] - // ClearMetadata { id: AssetIdParameter }, - #[codec(index = 22)] - ApproveTransfer { - id: AssetIdParameter, - delegate: MultiAddress, - amount: BalanceParameter, - }, - #[codec(index = 23)] - CancelApproval { id: AssetIdParameter, delegate: MultiAddress }, - // #[codec(index = 24)] - // ForceCancelApproval { - // id: AssetIdParameter, - // owner: MultiAddress, - // delegate: MultiAddress, - // }, - #[codec(index = 25)] - TransferApproved { - id: AssetIdParameter, - owner: MultiAddress, - destination: MultiAddress, - amount: BalanceParameter, - }, - // // #[codec(index = 26)] - // // Touch { id: AssetIdParameter }, - // // #[codec(index = 27)] - // // Refund { id: AssetIdParameter, allow_burn: bool }, - // // #[codec(index = 28)] - // // SetMinBalance { id: AssetIdParameter, min_balance: BalanceParameter }, - // // #[codec(index = 29)] - // // TouchOther { id: AssetIdParameter, who: MultiAddress }, - // // #[codec(index = 30)] - // // RefundOther { id: AssetIdParameter, who: MultiAddress }, - // // #[codec(index = 31)] - // // Block { id: AssetIdParameter, who: MultiAddress }, - // } - - // // TODO: Not being used atm but necessary if we want to provide access to the - // // rest of the pallet, outside of the use cases. - // #[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, scale::Decode)] - // #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] - // pub enum AssetsError { - // /// Account balance must be greater than or equal to the transfer amount. - // BalanceLow, - // /// The account to alter does not exist. - // NoAccount, - // /// The signing account has no permission to do the operation. - // NoPermission, - // /// The given asset ID is unknown. - // Unknown, - // /// The origin account is frozen. - // Frozen, - // /// The asset ID is already taken. - // InUse, - // /// Invalid witness data given. - // BadWitness, - // /// Minimum balance should be non-zero. - // MinBalanceZero, - // /// Unable to increment the consumer reference counters on the account. Either no provider - // /// reference exists to allow a non-zero balance of a non-self-sufficient asset, or one - // /// fewer then the maximum number of consumers has been reached. - // UnavailableConsumer, - // /// Invalid metadata given. - // BadMetadata, - // /// No approval exists that would allow the transfer. - // Unapproved, - // /// The source account would not survive the transfer and it needs to stay alive. - // WouldDie, - // /// The asset-account already exists. - // AlreadyExists, - // /// The asset-account doesn't have an associated deposit. - // NoDeposit, - // /// The operation would result in funds being burned. - // WouldBurn, - // /// The asset is a live asset and is actively being used. Usually emit for operations such - // /// as `start_destroy` which require the asset to be in a destroying state. - // LiveAsset, - // /// The asset is not live, and likely being destroyed. - // AssetNotLive, - // /// The asset status is not the expected status. - // IncorrectStatus, - // /// The asset should be frozen before the given operation. - // NotFrozen, - // /// Callback action resulted in error. - // CallbackFailed, -} - -// -// impl TryFrom for AssetsError { -// type Error = Error; -// -// fn try_from(status_code: u32) -> core::result::Result { -// use AssetsError::*; -// match status_code { -// 0 => Ok(BalanceLow), -// 1 => Ok(NoAccount), -// 2 => Ok(NoPermission), -// 3 => Ok(Unknown), -// 4 => Ok(Frozen), -// 5 => Ok(InUse), -// 6 => Ok(BadWitness), -// 7 => Ok(MinBalanceZero), -// 8 => Ok(UnavailableConsumer), -// 9 => Ok(BadMetadata), -// 10 => Ok(Unapproved), -// 11 => Ok(WouldDie), -// 12 => Ok(AlreadyExists), -// 13 => Ok(NoDeposit), -// 14 => Ok(WouldBurn), -// 15 => Ok(LiveAsset), -// 16 => Ok(AssetNotLive), -// 17 => Ok(IncorrectStatus), -// 18 => Ok(NotFrozen), -// _ => todo!(), -// } -// } +// pub(crate) fn asset_exists(id: AssetId) -> Result { +// state::read(RuntimeStateKeys::Assets(AssetsKeys::AssetExists(id))) // } diff --git a/pop-api/src/v0/balances.rs b/pop-api/src/v0/balances.rs index e14e6e32..bf029178 100644 --- a/pop-api/src/v0/balances.rs +++ b/pop-api/src/v0/balances.rs @@ -1,6 +1,9 @@ -use crate::{dispatch, primitives::MultiAddress, v0::RuntimeCall, AccountId, Error, StatusCode}; +use crate::{ + dispatch, primitives::MultiAddress, v0::RuntimeCall, AccountId, PopApiError, + PopApiError::UnknownStatusCode, +}; -type Result = core::result::Result; +type Result = core::result::Result; pub fn transfer_keep_alive( dest: impl Into>, @@ -14,7 +17,7 @@ pub fn transfer_keep_alive( #[derive(scale::Encode)] #[allow(dead_code)] -pub enum BalancesCall { +pub(crate) enum BalancesCall { #[codec(index = 3)] TransferKeepAlive { dest: MultiAddress, @@ -23,11 +26,9 @@ pub enum BalancesCall { }, } -// TODO: Not being used atm but necessary if we want to provide access to the -// rest of the pallet, outside of the use cases. #[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum BalancesError { +pub enum Error { /// Vesting balance too high to send value. VestingBalance, /// Account liquidity restrictions prevent withdrawal. @@ -54,11 +55,11 @@ pub enum BalancesError { DeltaZero, } -impl TryFrom for BalancesError { - type Error = Error; +impl TryFrom for Error { + type Error = PopApiError; fn try_from(status_code: u32) -> core::result::Result { - use BalancesError::*; + use Error::*; match status_code { 0 => Ok(VestingBalance), 1 => Ok(LiquidityRestrictions), @@ -72,7 +73,16 @@ impl TryFrom for BalancesError { 9 => Ok(TooManyFreezes), 10 => Ok(IssuanceDeactivated), 11 => Ok(DeltaZero), - _ => todo!(), + _ => Err(UnknownStatusCode(status_code)), + } + } +} + +impl From for Error { + fn from(error: PopApiError) -> Self { + match error { + PopApiError::Balances(e) => e, + _ => panic!("expected balances error"), } } } diff --git a/pop-api/src/v0/cross_chain/mod.rs b/pop-api/src/v0/cross_chain/mod.rs index ad58e0e8..6732c119 100644 --- a/pop-api/src/v0/cross_chain/mod.rs +++ b/pop-api/src/v0/cross_chain/mod.rs @@ -1,16 +1,12 @@ -use crate::{BlockNumber, ParachainSystemKeys, Result, RuntimeStateKeys}; - pub mod coretime; -pub fn relay_chain_block_number() -> Result { - crate::v0::state::read(RuntimeStateKeys::ParachainSystem( - ParachainSystemKeys::LastRelayChainBlockNumber, - )) -} +use crate::{PopApiError::UnknownStatusCode, *}; + +type Result = core::result::Result; #[derive(Debug, Copy, Clone, PartialEq, Eq, scale::Encode, scale::Decode)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum CrossChainError { +pub enum Error { /// The desired destination was unreachable, generally because there is a no way of routing /// to it. Unreachable, @@ -66,11 +62,11 @@ pub enum CrossChainError { LocalExecutionIncomplete, } -impl TryFrom for CrossChainError { - type Error = crate::error::Error; +impl TryFrom for Error { + type Error = PopApiError; fn try_from(status_code: u32) -> core::result::Result { - use CrossChainError::*; + use Error::*; match status_code { 0 => Ok(Unreachable), 1 => Ok(SendFailure), @@ -96,7 +92,16 @@ impl TryFrom for CrossChainError { 21 => Ok(InvalidAssetUnknownReserve), 22 => Ok(InvalidAssetUnsupportedReserve), 23 => Ok(TooManyReserves), - _ => todo!(), + _ => Err(UnknownStatusCode(status_code)), + } + } +} + +impl From for Error { + fn from(error: PopApiError) -> Self { + match error { + PopApiError::Xcm(e) => e, + _ => panic!("expected xcm error"), } } } diff --git a/pop-api/src/v0/mod.rs b/pop-api/src/v0/mod.rs index f91c4271..310b360c 100644 --- a/pop-api/src/v0/mod.rs +++ b/pop-api/src/v0/mod.rs @@ -6,18 +6,3 @@ pub mod balances; pub mod cross_chain; #[cfg(feature = "nfts")] pub mod nfts; -pub mod state; - -#[derive(Debug, PartialEq, Eq)] -#[ink::scale_derive(Encode, Decode, TypeInfo)] -pub(crate) enum RuntimeCall { - #[codec(index = 10)] - #[cfg(feature = "balances")] - Balances(balances::BalancesCall), - #[codec(index = 50)] - #[cfg(feature = "nfts")] - Nfts(nfts::NftCalls), - #[codec(index = 52)] - #[cfg(feature = "assets")] - Assets(assets::AssetsCall), -} diff --git a/pop-api/src/v0/nfts.rs b/pop-api/src/v0/nfts.rs index 32539576..e111c8dc 100644 --- a/pop-api/src/v0/nfts.rs +++ b/pop-api/src/v0/nfts.rs @@ -1,21 +1,17 @@ +use super::RuntimeCall; +use crate::{PopApiError::UnknownStatusCode, *}; use ink::prelude::vec::Vec; +use primitives::{ApprovalsLimit, BoundedBTreeMap, KeyLimit, MultiAddress}; +pub use primitives::{CollectionId, ItemId}; use scale::Encode; - -use crate::{ - dispatch, - primitives::{ - nfts::{ApprovalsLimit, CollectionId, ItemId, KeyLimit}, - BoundedBTreeMap, BoundedVec, - }, - state, AccountId, Balance, BlockNumber, MultiAddress, NftsKeys, RuntimeCall, RuntimeStateKeys, - StatusCode, -}; pub use types::*; type Result = core::result::Result; type StringLimit = u32; type MaxTips = u32; +type Result = core::result::Result; + /// Issue a new collection of non-fungible items pub fn create( admin: impl Into>, @@ -524,7 +520,7 @@ pub(crate) enum NftCalls { #[derive(Debug, Copy, Clone, PartialEq, Eq, Encode, scale::Decode)] #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] -pub enum NftsError { +pub enum Error { /// The signing account has no permission to do the operation. NoPermission, /// The given item ID is unknown. @@ -617,11 +613,11 @@ pub enum NftsError { WitnessRequired, } -impl TryFrom for NftsError { - type Error = crate::error::Error; +impl TryFrom for Error { + type Error = PopApiError; fn try_from(status_code: u32) -> core::result::Result { - use NftsError::*; + use Error::*; match status_code { 0 => Ok(NoPermission), 1 => Ok(UnknownCollection), @@ -668,7 +664,16 @@ impl TryFrom for NftsError { 42 => Ok(WrongNamespace), 43 => Ok(CollectionNotEmpty), 44 => Ok(WitnessRequired), - _ => todo!(), + _ => Err(UnknownStatusCode(status_code)), + } + } +} + +impl From for Error { + fn from(error: PopApiError) -> Self { + match error { + PopApiError::Nfts(e) => e, + _ => panic!("expected nfts error"), } } } @@ -677,7 +682,7 @@ impl TryFrom for NftsError { mod types { use super::*; use crate::{ - primitives::nfts::{CollectionId, ItemId}, + primitives::{CollectionId, ItemId}, Balance, BlockNumber, }; pub use enumflags2::{bitflags, BitFlags}; diff --git a/pop-api/src/v0/state.rs b/pop-api/src/v0/state.rs deleted file mode 100644 index afe48ba8..00000000 --- a/pop-api/src/v0/state.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::{error::StatusCode, primitives::storage_keys::RuntimeStateKeys, read_state}; -use ink::scale::Decode; - -#[inline] -pub fn read(key: RuntimeStateKeys) -> crate::Result { - read_state(key).and_then(|v| T::decode(&mut &v[..]).map_err(|_e| StatusCode(255u32))) -} diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index cb19b0ab..5cbd6d6c 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -6,10 +6,7 @@ edition = "2021" [dependencies] bounded-collections = { version = "0.1", default-features = false } - scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } -scale-decode = { version = "0.10.0", default-features = false, features = ["derive"], optional = true } -scale-encode = { version = "0.5.0", default-features = false, features = ["derive"], optional = true } scale-info = { version = "2.10", default-features = false, features = ["derive"], optional = true } [features] @@ -17,12 +14,8 @@ default = ["std"] std = [ "bounded-collections/std", "scale/std", - "scale-decode/std", - "scale-encode/std", "scale-info/std", ] -devnet = [] -testnet = [] assets = [] cross-chain = [] nfts = [] diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index dc0a5245..9d31653a 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -3,14 +3,15 @@ pub use bounded_collections::{BoundedBTreeMap, BoundedBTreeSet, BoundedVec}; use scale::{Decode, Encode, MaxEncodedLen}; #[cfg(feature = "std")] -use {scale_decode::DecodeAsType, scale_encode::EncodeAsType, scale_info::TypeInfo}; +use scale_info::TypeInfo; +pub use v0::error; #[cfg(feature = "cross-chain")] pub mod cross_chain; pub mod storage_keys; -#[derive(Encode, Decode, Clone, Debug, MaxEncodedLen, Eq, PartialEq, Ord, PartialOrd)] -#[cfg_attr(feature = "std", derive(TypeInfo, DecodeAsType, EncodeAsType))] +#[derive(Encode, Decode, Debug, MaxEncodedLen, Eq, PartialEq)] +#[cfg_attr(feature = "std", derive(TypeInfo))] pub struct AccountId(pub [u8; 32]); // Identifier for the class of asset. @@ -29,3 +30,120 @@ pub mod nfts { /// The maximum approvals an item could have. pub type ApprovalsLimit = ConstU32<20>; } + +pub mod v0 { + use super::*; + pub mod error { + use super::*; + + #[derive(Encode, Decode, Debug, Eq, PartialEq)] + #[cfg_attr(feature = "std", derive(TypeInfo))] + #[repr(u8)] + pub enum Error { + /// Some unknown error occurred. Go to the Pop API docs section `Pop API error`. + Other { + // Index within the `DispatchError` + dispatch_error_index: u8, + // Index within the `DispatchError` variant. + error_index: u8, + // Index for further nesting, e.g. pallet error. + error: u8, + } = 0, + /// Failed to lookup some data. + CannotLookup = 1, + /// A bad origin. + BadOrigin = 2, + /// A custom error in a module. + Module { index: u8, error: u8 } = 3, + /// At least one consumer is remaining so the account cannot be destroyed. + ConsumerRemaining = 4, + /// There are no providers so the account cannot be created. + NoProviders = 5, + /// There are too many consumers so the account cannot be created. + TooManyConsumers = 6, + /// An error to do with tokens. + Token(TokenError) = 7, + /// An arithmetic error. + Arithmetic(ArithmeticError) = 8, + /// The number of transactional layers has been reached, or we are not in a transactional + /// layer. + Transactional(TransactionalError) = 9, + /// Resources exhausted, e.g. attempt to read/write data which is too large to manipulate. + Exhausted = 10, + /// The state is corrupt; this is generally not going to fix itself. + Corruption = 11, + /// Some resource (e.g. a preimage) is unavailable right now. This might fix itself later. + Unavailable = 12, + /// Root origin is not allowed. + RootNotAllowed = 13, + /// Unknown function id. + UnknownFunctionId = 254, + /// Decoding failed on the runtime. + DecodingFailed = 255, + } + + impl From for Error { + fn from(value: u32) -> Self { + let encoded = value.to_le_bytes(); + Error::decode(&mut &encoded[..]).unwrap_or(Error::DecodingFailed) + } + } + + impl From for u32 { + fn from(value: Error) -> Self { + let mut encoded_error = value.encode(); + // Resize the encoded value to 4 bytes in order to decode the value in a u32 (4 bytes). + encoded_error.resize(4, 0); + u32::from_le_bytes( + encoded_error.try_into().expect("qid, resized to 4 bytes line above"), + ) + } + } + + #[derive(Encode, Decode, Clone, Debug, MaxEncodedLen, Eq, PartialEq, Ord, PartialOrd)] + #[cfg_attr(feature = "std", derive(TypeInfo))] + pub enum TokenError { + /// Funds are unavailable. + FundsUnavailable, + /// Some part of the balance gives the only provider reference to the account and thus cannot + /// be (re)moved. + OnlyProvider, + /// Account cannot exist with the funds that would be given. + BelowMinimum, + /// Account cannot be created. + CannotCreate, + /// The asset in question is unknown. + UnknownAsset, + /// Funds exist but are frozen. + Frozen, + /// Operation is not supported by the asset. + Unsupported, + /// Account cannot be created for a held balance. + CannotCreateHold, + /// Withdrawal would cause unwanted loss of account. + NotExpendable, + /// Account cannot receive the assets. + Blocked, + } + + #[derive(Encode, Decode, Debug, Eq, PartialEq)] + #[cfg_attr(feature = "std", derive(TypeInfo))] + pub enum ArithmeticError { + /// Underflow. + Underflow, + /// Overflow. + Overflow, + /// Division by zero. + DivisionByZero, + } + + #[derive(Encode, Decode, Debug, Eq, PartialEq)] + #[cfg_attr(feature = "std", derive(TypeInfo))] + pub enum TransactionalError { + /// Too many transactional layers have been spawned. + LimitReached, + /// A transactional layer was expected, but does not exist. + NoLayer, + } + } +} diff --git a/primitives/src/storage_keys.rs b/primitives/src/storage_keys.rs index 7992089b..e42dbca0 100644 --- a/primitives/src/storage_keys.rs +++ b/primitives/src/storage_keys.rs @@ -46,14 +46,11 @@ pub enum NftsKeys { #[cfg(feature = "assets")] #[derive(Encode, Decode, Debug, MaxEncodedLen)] pub enum AssetsKeys { - Allowance(AssetId, AccountId, AccountId), - // /// Check if the asset exists. - // // AssetExists(AssetId), - /// Check balance. - BalanceOf(AssetId, AccountId), - /// Returns the total token supply for a given asset ID. TotalSupply(AssetId), - TokenDecimals(AssetId), - TokenSymbol(AssetId), + BalanceOf(AssetId, AccountId), + Allowance(AssetId, AccountId, AccountId), TokenName(AssetId), + TokenSymbol(AssetId), + TokenDecimals(AssetId), + // AssetExists(AssetId), } diff --git a/runtime/devnet/Cargo.toml b/runtime/devnet/Cargo.toml index 5f52b855..7db68576 100644 --- a/runtime/devnet/Cargo.toml +++ b/runtime/devnet/Cargo.toml @@ -89,8 +89,9 @@ parachain-info.workspace = true [dev-dependencies] env_logger = "0.11.2" -hex = "0.4.3" enumflags2 = "0.7.9" +hex = "0.4.3" +rand = "0.8.5" [features] default = ["std"] diff --git a/runtime/devnet/src/extensions.rs b/runtime/devnet/src/extensions/mod.rs similarity index 79% rename from runtime/devnet/src/extensions.rs rename to runtime/devnet/src/extensions/mod.rs index 6f98bf96..ac52d5bd 100644 --- a/runtime/devnet/src/extensions.rs +++ b/runtime/devnet/src/extensions/mod.rs @@ -1,18 +1,22 @@ +use codec::{Compact, Decode, Encode}; use cumulus_pallet_parachain_system::RelaychainDataProvider; use frame_support::traits::{Contains, OriginTrait}; use frame_support::{ dispatch::{GetDispatchInfo, RawOrigin}, pallet_prelude::*, traits::{ - fungibles::{approvals::Inspect as ApprovalInspect, Inspect}, + fungibles::approvals::Inspect as ApprovalInspect, nonfungibles_v2::Inspect as NonFungiblesInspect, }, }; use pallet_contracts::chain_extension::{ - BufInBufOutState, ChainExtension, ChargedAmount, Environment, Ext, InitState, RetVal, + BufInBufOutState, ChainExtension, Environment, Ext, InitState, RetVal, }; use sp_core::crypto::UncheckedFrom; -use sp_runtime::traits::{BlockNumberProvider, Dispatchable}; +use sp_runtime::{ + traits::{BlockNumberProvider, Dispatchable}, + DispatchError, MultiAddress, +}; use sp_std::{boxed::Box, vec::Vec}; use xcm::{ latest::{prelude::*, OriginKind::SovereignAccount}, @@ -20,17 +24,24 @@ use xcm::{ }; use crate::{ - config::assets::TrustBackedAssetsInstance, AccountId, AllowedApiCalls, RuntimeCall, - RuntimeOrigin, UNIT, + config::assets::TrustBackedAssetsInstance, AccountId, AllowedApiCalls, Balance, Runtime, + RuntimeCall, RuntimeOrigin, UNIT, }; use pop_primitives::{ cross_chain::CrossChainMessage, nfts::{CollectionId, ItemId}, - storage_keys::{AssetsKeys, NftsKeys, ParachainSystemKeys, RuntimeStateKeys}, + storage_keys::{ + AssetsKeys::{self, *}, + NftsKeys, ParachainSystemKeys, RuntimeStateKeys, + }, AssetId, }; +mod v0; + const LOG_TARGET: &str = "pop-api::extension"; +// Versions: +const V0: u8 = 0; type ContractSchedule = ::Schedule; @@ -58,53 +69,162 @@ where { log::debug!(target:LOG_TARGET, " extension called "); let mut env = env.buf_in_buf_out(); - let contract_host_weight = ContractSchedule::::get().host_fn_weights; - // debug_message weight is a good approximation of the additional overhead of going + // Charge weight for making a call from a contract to the runtime. + // `debug_message` weight is a good approximation of the additional overhead of going // from contract layer to substrate layer. // reference: https://github.com/paritytech/ink-examples/blob/b8d2caa52cf4691e0ddd7c919e4462311deb5ad0/psp22-extension/runtime/psp22-extension-example.rs#L236 + let contract_host_weight = ContractSchedule::::get().host_fn_weights; env.charge_weight(contract_host_weight.debug_message)?; - let result = match v0::FuncId::try_from(env.func_id()) { - Ok(function) => { - // calculate weight for reading bytes of `len` + // Extract version and function_id from first two bytes. + let (version, function_id) = { + let bytes = env.func_id().to_le_bytes(); + (bytes[0], bytes[1]) + }; + // Extract pallet index and call / key index from last two bytes. + let (pallet_index, call_index) = { + let bytes = env.ext_id().to_le_bytes(); + (bytes[0], bytes[1]) + }; + + let result = match FuncId::try_from(function_id) { + Ok(function_id) => { + // Read encoded parameters from buffer and calculate weight for reading `len` bytes`. // reference: https://github.com/paritytech/polkadot-sdk/blob/117a9433dac88d5ac00c058c9b39c511d47749d2/substrate/frame/contracts/src/wasm/runtime.rs#L267 let len = env.in_len(); env.charge_weight(contract_host_weight.return_per_byte.saturating_mul(len.into()))?; - match function { - v0::FuncId::Dispatch => dispatch::(&mut env, len), - v0::FuncId::ReadState => read_state::(&mut env), - v0::FuncId::SendXcm => send_xcm::(&mut env), + let params = env.read(len)?; + log::debug!(target: LOG_TARGET, "Read input successfully"); + match function_id { + FuncId::Dispatch => { + dispatch::(&mut env, version, pallet_index, call_index, params) + }, + FuncId::ReadState => { + read_state::(&mut env, version, pallet_index, call_index, params) + }, + // TODO + FuncId::SendXcm => send_xcm::(&mut env), } }, Err(e) => Err(e), }; - // Convert any error to a status code and return Ok with RetVal::Converging match result { Ok(_) => Ok(RetVal::Converging(0)), - Err(e) => Ok(RetVal::Converging(convert_to_status_code(e))), + Err(e) => Ok(RetVal::Converging(convert_to_status_code(e, version))), } } } -fn dispatch(env: &mut Environment, len: u32) -> Result<(), DispatchError> +fn dispatch( + env: &mut Environment, + version: u8, + pallet_index: u8, + call_index: u8, + params: Vec, +) -> Result<(), DispatchError> where - T: pallet_contracts::Config - + frame_system::Config, + T: frame_system::Config, RuntimeOrigin: From>, E: Ext, { const LOG_PREFIX: &str = " dispatch |"; - - // read the input as RuntimeCall - let call: RuntimeCall = env.read_as_unbounded(len)?; - log::debug!(target: LOG_TARGET, "Read input as call successfully"); - // contract is the origin by default + let call = construct_call(version, pallet_index, call_index, params) + .map_err(|_| DispatchError::Other("DecodingFailed"))?; + // Contract is the origin by default. let origin: RuntimeOrigin = RawOrigin::Signed(env.ext().address().clone()).into(); dispatch_call::(env, call, origin, LOG_PREFIX) } -fn read_state(env: &mut Environment) -> Result<(), DispatchError> +fn dispatch_call( + env: &mut Environment, + call: RuntimeCall, + mut origin: RuntimeOrigin, + log_prefix: &str, +) -> Result<(), DispatchError> +where + T: frame_system::Config, + RuntimeOrigin: From>, + E: Ext, +{ + let charged_dispatch_weight = env.charge_weight(call.get_dispatch_info().weight)?; + log::debug!(target:LOG_TARGET, "{} Inputted RuntimeCall: {:?}", log_prefix, call); + origin.add_filter(AllowedApiCalls::contains); + match call.dispatch(origin) { + Ok(info) => { + log::debug!(target:LOG_TARGET, "{} success, actual weight: {:?}", log_prefix, info.actual_weight); + // Refund weight if the actual weight is less than the charged weight. + if let Some(actual_weight) = info.actual_weight { + env.adjust_weight(charged_dispatch_weight, actual_weight); + } + Ok(()) + }, + Err(err) => { + log::debug!(target:LOG_TARGET, "{} failed: error: {:?}", log_prefix, err.error); + Err(err.error) + }, + } +} + +fn construct_call( + version: u8, + pallet_index: u8, + call_index: u8, + params: Vec, +) -> Result { + match pallet_index { + 52 => { + let call = versioned_construct_assets_call(version, call_index, params)?; + Ok(RuntimeCall::Assets(call)) + }, + _ => Err(DispatchError::Other("UnknownFunctionId")), + } +} + +fn construct_key( + version: u8, + pallet_index: u8, + call_index: u8, + params: Vec, +) -> Result { + match pallet_index { + 52 => { + let key = versioned_construct_assets_key(version, call_index, params)?; + Ok(RuntimeStateKeys::Assets(key)) + }, + _ => Err(DispatchError::Other("UnknownFunctionId")), + } +} + +fn versioned_construct_assets_call( + version: u8, + call_index: u8, + params: Vec, +) -> Result, DispatchError> { + match version { + V0 => v0::assets::construct_assets_call(call_index, params), + _ => Err(DispatchError::Other("UnknownFunctionId")), + } +} + +fn versioned_construct_assets_key( + version: u8, + call_index: u8, + params: Vec, +) -> Result { + match version { + V0 => v0::assets::construct_assets_key(call_index, params), + _ => Err(DispatchError::Other("UnknownFunctionId")), + } +} + +fn read_state( + env: &mut Environment, + version: u8, + pallet_index: u8, + call_index: u8, + params: Vec, +) -> Result<(), DispatchError> where T: pallet_contracts::Config + pallet_assets::Config @@ -114,15 +234,13 @@ where E: Ext, { const LOG_PREFIX: &str = " read_state |"; - - let key: RuntimeStateKeys = env.read_as()?; + let key = construct_key(version, pallet_index, call_index, params)?; let result = match key { RuntimeStateKeys::Nfts(key) => read_nfts_state::(key, env), RuntimeStateKeys::ParachainSystem(key) => read_parachain_system_state::(key, env), RuntimeStateKeys::Assets(key) => read_assets_state::(key, env), }? .encode(); - log::trace!( target:LOG_TARGET, "{} result: {:?}.", LOG_PREFIX, result @@ -141,10 +259,9 @@ where E: Ext, { const LOG_PREFIX: &str = " send_xcm |"; - - // read the input as CrossChainMessage + // Read the input as CrossChainMessage. let xc_call: CrossChainMessage = env.read_as::()?; - // Determine the call to dispatch + // Determine the call to dispatch. let (dest, message) = match xc_call { CrossChainMessage::Relay(message) => { let dest = Location::parent().into_versioned(); @@ -165,91 +282,85 @@ where (dest, message) }, }; - // TODO: revisit to replace with signed contract origin let origin: RuntimeOrigin = RawOrigin::Root.into(); - - // Generate runtime call to dispatch + // Generate runtime call to dispatch. let call = RuntimeCall::PolkadotXcm(pallet_xcm::Call::send { dest: Box::new(dest), message: Box::new(VersionedXcm::V4(message)), }); - dispatch_call::(env, call, origin, LOG_PREFIX) } -pub(crate) fn convert_to_status_code(error: DispatchError) -> u32 { +// Converts a `DispatchError` to a `u32` status code based on the version of the API the contract uses. +// The contract calling the chain extension can convert the status code to the descriptive `Error`. +// +// For `Error` see `pop_primitives::::error::Error`. +// +// The error encoding can vary per version, allowing for flexible and backward-compatible error handling. +// As a result, contracts maintain compatibility across different versions of the runtime. +// +// # Parameters +// +// - `error`: The `DispatchError` encountered during contract execution. +// - `version`: The version of the chain extension, used to determine the known errors. +pub(crate) fn convert_to_status_code(error: DispatchError, version: u8) -> u32 { + // "UnknownFunctionId" and "DecodingFailed" are mapped to specific errors in the API and will + // never change. let mut encoded_error = match error { DispatchError::Other("UnknownFunctionId") => vec![254, 0, 0, 0], + DispatchError::Other("DecodingFailed") => vec![255, 0, 0, 0], _ => error.encode(), }; // Resize the encoded value to 4 bytes in order to decode the value in a u32 (4 bytes). encoded_error.resize(4, 0); - u32::decode(&mut &encoded_error[..]).expect("qid, resized to 4 bytes line above") + let mut encoded_error = encoded_error.try_into().expect("qid, resized to 4 bytes line above"); + match version { + // If an unknown variant of the `DispatchError` is detected the error needs to be converted + // into the encoded value of `Error::Other`. This conversion is performed by shifting the bytes one + // position forward (discarding the last byte as it is not used) and setting the first byte to the + // encoded value of `Other` (0u8). This ensures the error is correctly categorized as an `Other` + // variant which provides all the necessary information to debug which error occurred in the runtime. + // + // Byte layout explanation: + // - Byte 0: index of the variant within `Error` + // - Byte 1: + // - Must be zero for `UNIT_ERRORS`. + // - Represents the nested error in `SINGLE_NESTED_ERRORS`. + // - Represents the first level of nesting in `DOUBLE_NESTED_ERRORS`. + // - Byte 2: + // - Represents the second level of nesting in `DOUBLE_NESTED_ERRORS`. + // - Byte 3: + // - Unused or represents further nested information. + 0 => v0::error::handle_unknown_error(&mut encoded_error), + _ => encoded_error = [254, 0, 0, 0], + } + u32::from_le_bytes(encoded_error) } -pub mod v0 { - #[derive(Debug)] - pub enum FuncId { - Dispatch, - ReadState, - SendXcm, - } +#[derive(Debug)] +pub enum FuncId { + Dispatch, + ReadState, + SendXcm, } -impl TryFrom for v0::FuncId { +impl TryFrom for FuncId { type Error = DispatchError; - fn try_from(func_id: u16) -> Result { + fn try_from(func_id: u8) -> Result { let id = match func_id { - 0x0 => Self::Dispatch, - 0x1 => Self::ReadState, - 0x2 => Self::SendXcm, + 0 => Self::Dispatch, + 1 => Self::ReadState, + 2 => Self::SendXcm, _ => { - log::error!("called an unregistered `func_id`: {:}", func_id); return Err(DispatchError::Other("UnknownFuncId")); }, }; - Ok(id) } } -fn dispatch_call( - env: &mut Environment, - call: RuntimeCall, - mut origin: RuntimeOrigin, - log_prefix: &str, -) -> Result<(), DispatchError> -where - T: frame_system::Config, - RuntimeOrigin: From>, - E: Ext, -{ - let charged_dispatch_weight = env.charge_weight(call.get_dispatch_info().weight)?; - - log::debug!(target:LOG_TARGET, "{} Inputted RuntimeCall: {:?}", log_prefix, call); - - origin.add_filter(AllowedApiCalls::contains); - - match call.dispatch(origin) { - Ok(info) => { - log::debug!(target:LOG_TARGET, "{} success, actual weight: {:?}", log_prefix, info.actual_weight); - - // refund weight if the actual weight is less than the charged weight - if let Some(actual_weight) = info.actual_weight { - env.adjust_weight(charged_dispatch_weight, actual_weight); - } - - Ok(()) - }, - Err(err) => { - log::debug!(target:LOG_TARGET, "{} failed: error: {:?}", log_prefix, err.error); - Err(err.error) - }, - } -} - fn read_parachain_system_state( key: ParachainSystemKeys, env: &mut Environment, @@ -323,7 +434,7 @@ where T: frame_system::Config, { match key { - AssetsKeys::Allowance(id, owner, spender) => { + Allowance(id, owner, spender) => { env.charge_weight(T::DbWeight::get().reads(1_u64))?; Ok(pallet_assets::Pallet::::allowance( id, @@ -336,12 +447,12 @@ where // env.charge_weight(T::DbWeight::get().reads(1_u64))?; // Ok(pallet_assets::Pallet::::asset_exists(id).encode()) // }, - AssetsKeys::BalanceOf(id, owner) => { + BalanceOf(id, owner) => { env.charge_weight(T::DbWeight::get().reads(1_u64))?; Ok(pallet_assets::Pallet::::balance(id, &owner.0.into()) .encode()) }, - AssetsKeys::TotalSupply(id) => { + TotalSupply(id) => { env.charge_weight(T::DbWeight::get().reads(1_u64))?; Ok(pallet_assets::Pallet::::total_supply(id).encode()) }, @@ -354,88 +465,51 @@ mod tests { use super::*; use crate::{Assets, Runtime, System}; use sp_runtime::BuildStorage; + // Test ensuring `func_id()` and `ext_id()` work as expected, i.e. extracting the first two + // bytes and the last two bytes, respectively, from a 4 byte array. + #[test] + fn test_byte_extraction() { + use rand::Rng; - fn new_test_ext() -> sp_io::TestExternalities { - let t = frame_system::GenesisConfig::::default() - .build_storage() - .expect("Frame system builds valid default genesis config"); - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| System::set_block_number(1)); - ext - } + // Helper functions + fn func_id(id: u32) -> u16 { + (id & 0x0000FFFF) as u16 + } + fn ext_id(id: u32) -> u16 { + (id >> 16) as u16 + } - #[test] - fn encoding_decoding_dispatch_error() { - use codec::{Decode, Encode}; - use sp_runtime::{ArithmeticError, DispatchError, ModuleError, TokenError}; + // Number of test iterations + let test_iterations = 1_000_000; - new_test_ext().execute_with(|| { - let error = DispatchError::Module(ModuleError { - index: 255, - error: [2, 0, 0, 0], - message: Some("error message"), - }); - let encoded = error.encode(); - let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); - assert_eq!(encoded, vec![3, 255, 2, 0, 0, 0]); - assert_eq!( - decoded, - // `message` is skipped for encoding. - DispatchError::Module(ModuleError { - index: 255, - error: [2, 0, 0, 0], - message: None - }) - ); - println!("Encoded Module Error: {:?}", encoded); + // Create a random number generator + let mut rng = rand::thread_rng(); - // Example pallet assets Error into ModuleError. - let index = - <::PalletInfo as frame_support::traits::PalletInfo>::index::< - Assets, - >() - .expect("Every active module has an index in the runtime; qed") as u8; + // Run the test for a large number of random 4-byte arrays + for _ in 0..test_iterations { + // Generate a random 4-byte array + let bytes: [u8; 4] = rng.gen(); - let mut error = - pallet_assets::Error::NotFrozen::.encode(); - error.resize(MAX_MODULE_ERROR_ENCODED_SIZE, 0); - let error = DispatchError::Module(ModuleError { - index, - error: TryInto::try_into(error).expect("should work"), - message: None, - }); - let encoded = error.encode(); - let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); - assert_eq!(encoded, vec![3, 52, 18, 0, 0, 0]); - assert_eq!( - decoded, - DispatchError::Module(ModuleError { - index: 52, - error: [18, 0, 0, 0], - message: None - }) - ); - println!("Encoded Module Error: {:?}", encoded); + // Convert the 4-byte array to a u32 value + let value = u32::from_le_bytes(bytes); - // Example DispatchError::Token - let error = DispatchError::Token(TokenError::UnknownAsset); - let encoded = error.encode(); - assert_eq!(encoded, vec![7, 4]); - println!("Encoded Token Error: {:?}", encoded); + // Extract the first two bytes (least significant 2 bytes) + let first_two_bytes = func_id(value); - // Example DispatchError::Arithmetic - let error = DispatchError::Arithmetic(ArithmeticError::Overflow); - let encoded = error.encode(); - assert_eq!(encoded, vec![8, 1]); - println!("Encoded Arithmetic Error: {:?}", encoded); - }); + // Extract the last two bytes (most significant 2 bytes) + let last_two_bytes = ext_id(value); + + // Check if the first two bytes match the expected value + assert_eq!([bytes[0], bytes[1]], first_two_bytes.to_le_bytes()); + + // Check if the last two bytes match the expected value + assert_eq!([bytes[2], bytes[3]], last_two_bytes.to_le_bytes()); + } } + // Test showing all the different type of variants and its encoding. #[test] fn encoding_of_enum() { - use codec::{Decode, Encode}; - - // Comprehensive enum with all different type of variants. #[derive(Debug, PartialEq, Encode, Decode)] enum ComprehensiveEnum { SimpleVariant, @@ -494,39 +568,77 @@ mod tests { println!("{:?} -> {:?}", enum_nested_enum_struct, enum_nested_enum_struct.encode()); } + fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } + #[test] - fn dispatch_error_to_status_code() { - // Create all the different `DispatchError` variants with its respective `PopApiError`. - let test_cases = vec![ - (DispatchError::Other("hallo"), [0, 0, 0, 0]), - (DispatchError::CannotLookup, [1, 0, 0, 0]), - (DispatchError::BadOrigin, [2, 0, 0, 0]), - ( - DispatchError::Module(sp_runtime::ModuleError { - index: 1, + fn encoding_decoding_dispatch_error() { + use sp_runtime::{ArithmeticError, DispatchError, ModuleError, TokenError}; + + new_test_ext().execute_with(|| { + let error = DispatchError::Module(ModuleError { + index: 255, + error: [2, 0, 0, 0], + message: Some("error message"), + }); + let encoded = error.encode(); + let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); + assert_eq!(encoded, vec![3, 255, 2, 0, 0, 0]); + assert_eq!( + decoded, + // `message` is skipped for encoding. + DispatchError::Module(ModuleError { + index: 255, error: [2, 0, 0, 0], - message: Some("hallo"), - }), - [3, 1, 2, 0], - ), - (DispatchError::ConsumerRemaining, [4, 0, 0, 0]), - (DispatchError::NoProviders, [5, 0, 0, 0]), - (DispatchError::TooManyConsumers, [6, 0, 0, 0]), - (DispatchError::Token(sp_runtime::TokenError::BelowMinimum), [7, 2, 0, 0]), - (DispatchError::Arithmetic(sp_runtime::ArithmeticError::Overflow), [8, 1, 0, 0]), - ( - DispatchError::Transactional(sp_runtime::TransactionalError::LimitReached), - [9, 0, 0, 0], - ), - (DispatchError::Exhausted, [10, 0, 0, 0]), - (DispatchError::Corruption, [11, 0, 0, 0]), - (DispatchError::Unavailable, [12, 0, 0, 0]), - (DispatchError::RootNotAllowed, [13, 0, 0, 0]), - ]; - for (error, encoded_error) in test_cases { - let status_code = crate::extensions::convert_to_status_code(error); - assert_eq!(status_code, u32::decode(&mut &encoded_error[..]).unwrap()); - } + message: None + }) + ); + + // Example pallet assets Error into ModuleError. + let index = <::PalletInfo as frame_support::traits::PalletInfo>::index::< + Assets, + >() + .expect("Every active module has an index in the runtime; qed") as u8; + let mut error = + pallet_assets::Error::NotFrozen::.encode(); + error.resize(MAX_MODULE_ERROR_ENCODED_SIZE, 0); + let error = DispatchError::Module(ModuleError { + index, + error: TryInto::try_into(error).expect("should work"), + message: None, + }); + let encoded = error.encode(); + let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); + assert_eq!(encoded, vec![3, 52, 18, 0, 0, 0]); + assert_eq!( + decoded, + DispatchError::Module(ModuleError { + index: 52, + error: [18, 0, 0, 0], + message: None + }) + ); + + // Example DispatchError::Token + let error = DispatchError::Token(TokenError::UnknownAsset); + let encoded = error.encode(); + let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); + assert_eq!(encoded, vec![7, 4]); + assert_eq!(decoded, error); + + // Example DispatchError::Arithmetic + let error = DispatchError::Arithmetic(ArithmeticError::Overflow); + let encoded = error.encode(); + let decoded = DispatchError::decode(&mut &encoded[..]).unwrap(); + assert_eq!(encoded, vec![8, 1]); + assert_eq!(decoded, error); + }); } } // use enumflags2::BitFlags; diff --git a/runtime/devnet/src/extensions/v0/assets.rs b/runtime/devnet/src/extensions/v0/assets.rs new file mode 100644 index 00000000..912b116f --- /dev/null +++ b/runtime/devnet/src/extensions/v0/assets.rs @@ -0,0 +1,39 @@ +use crate::extensions::{ + AccountId, AssetId, + AssetsKeys::{self, TotalSupply}, + Balance, Compact, Decode, DispatchError, MultiAddress, Runtime, TrustBackedAssetsInstance, +}; + +pub(crate) fn construct_assets_key( + call_index: u8, + params: Vec, +) -> Result { + match call_index { + 0 => { + let id = ::decode(&mut ¶ms[..]) + .map_err(|_| DispatchError::Other("DecodingFailed"))?; + Ok(TotalSupply(id)) + }, + // other calls + _ => Err(DispatchError::Other("UnknownFunctionId")), + } +} + +pub(crate) fn construct_assets_call( + call_index: u8, + params: Vec, +) -> Result, DispatchError> { + match call_index { + 9 => { + let (id, target, amount) = <(AssetId, AccountId, Balance)>::decode(&mut ¶ms[..]) + .map_err(|_| DispatchError::Other("DecodingFailed"))?; + Ok(pallet_assets::Call::::transfer_keep_alive { + id: Compact(id), + target: MultiAddress::Id(target), + amount, + }) + }, + // other calls + _ => Err(DispatchError::Other("UnknownFunctionId")), + } +} diff --git a/runtime/devnet/src/extensions/v0/error.rs b/runtime/devnet/src/extensions/v0/error.rs new file mode 100644 index 00000000..b8af2250 --- /dev/null +++ b/runtime/devnet/src/extensions/v0/error.rs @@ -0,0 +1,298 @@ +#[cfg(test)] +use crate::extensions::convert_to_status_code; + +pub(crate) fn handle_unknown_error(encoded_error: &mut [u8; 4]) { + let unknown = match encoded_error[0] { + code if UNIT_ERRORS.contains(&code) => nested_errors(&encoded_error[1..], None), + // Single nested errors with a limit in their nesting. + // + // `TokenError`: has ten variants - translated to a limit of nine. + 7 => nested_errors(&encoded_error[1..], Some(9)), + // `ArithmeticError`: has 3 variants - translated to a limit of two. + 8 => nested_errors(&encoded_error[1..], Some(2)), + // `TransactionalError`: has 2 variants - translated to a limit of one. + 9 => nested_errors(&encoded_error[1..], Some(1)), + code if DOUBLE_NESTED_ERRORS.contains(&code) => nested_errors(&encoded_error[3..], None), + _ => true, + }; + if unknown { + encoded_error[..].rotate_right(1); + encoded_error[0] = 0u8; + } +} + +// Unit `Error` variants. +// (variant: index): +// - CannotLookup: 1, +// - BadOrigin: 2, +// - ConsumerRemaining: 4, +// - NoProviders: 5, +// - TooManyConsumers: 6, +// - Exhausted: 10, +// - Corruption: 11, +// - Unavailable: 12, +// - RootNotAllowed: 13, +// - UnknownFunctionId: 254, +// - DecodingFailed: 255, +const UNIT_ERRORS: [u8; 11] = [1, 2, 4, 5, 6, 10, 11, 12, 13, 254, 255]; + +#[cfg(test)] +const SINGLE_NESTED_ERRORS: [u8; 3] = [7, 8, 9]; + +// Double nested `Error` variants +// (variant: index): +// - Module: 3, +const DOUBLE_NESTED_ERRORS: [u8; 1] = [3]; + +// Checks for unknown nested errors within the `DispatchError`. +// - For single nested errors with a limit, it verifies if the nested value exceeds the limit. +// - For other nested errors, it checks if any subsequent bytes are non-zero. +// +// `nested_error` - The slice of bytes representing the nested error. +// `limit` - An optional limit for single nested errors. +fn nested_errors(nested_error: &[u8], limit: Option) -> bool { + match limit { + Some(l) => nested_error[0] > l || nested_error[1..].iter().any(|&x| x != 0u8), + None => nested_error.iter().any(|&x| x != 0u8), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pop_primitives::error::{ + ArithmeticError::*, + Error::{self, *}, + TokenError::*, + TransactionalError::*, + }; + use sp_runtime::DispatchError; + + // Compare all the different `DispatchError` variants with the expected `Error`. + #[test] + fn dispatch_error_to_error() { + let test_cases = vec![ + ( + DispatchError::Other(""), + (Other { dispatch_error_index: 0, error_index: 0, error: 0 }), + ), + (DispatchError::Other("UnknownFunctionId"), UnknownFunctionId), + (DispatchError::Other("DecodingFailed"), DecodingFailed), + (DispatchError::CannotLookup, CannotLookup), + (DispatchError::BadOrigin, BadOrigin), + ( + DispatchError::Module(sp_runtime::ModuleError { + index: 1, + error: [2, 0, 0, 0], + message: Some("hallo"), + }), + Module { index: 1, error: 2 }, + ), + (DispatchError::ConsumerRemaining, ConsumerRemaining), + (DispatchError::NoProviders, NoProviders), + (DispatchError::TooManyConsumers, TooManyConsumers), + (DispatchError::Token(sp_runtime::TokenError::BelowMinimum), Token(BelowMinimum)), + ( + DispatchError::Arithmetic(sp_runtime::ArithmeticError::Overflow), + Arithmetic(Overflow), + ), + ( + DispatchError::Transactional(sp_runtime::TransactionalError::LimitReached), + Transactional(LimitReached), + ), + (DispatchError::Exhausted, Exhausted), + (DispatchError::Corruption, Corruption), + (DispatchError::Unavailable, Unavailable), + (DispatchError::RootNotAllowed, RootNotAllowed), + ]; + for (dispatch_error, expected) in test_cases { + let status_code = crate::extensions::convert_to_status_code(dispatch_error, 0); + let error: Error = status_code.into(); + assert_eq!(error, expected); + } + } + + // Compare all the different `DispatchError::Other` possibilities with the expected `Error`. + #[test] + fn other_error() { + let test_cases = vec![ + ( + DispatchError::Other("Random"), + (Other { dispatch_error_index: 0, error_index: 0, error: 0 }), + ), + (DispatchError::Other("UnknownFunctionId"), UnknownFunctionId), + (DispatchError::Other("DecodingFailed"), DecodingFailed), + ]; + for (dispatch_error, expected) in test_cases { + let status_code = convert_to_status_code(dispatch_error, 0); + let error: Error = status_code.into(); + assert_eq!(error, expected); + } + } + + // Compare all the different `DispatchError::Module` nesting possibilities, which can not be + // handled, with the expected `Error`. See `double_nested_error_variants` fourth byte nesting. + #[test] + fn test_module_error() { + let test_cases = vec![ + DispatchError::Module(sp_runtime::ModuleError { + index: 1, + error: [2, 2, 0, 0], + message: Some("Random"), + }), + DispatchError::Module(sp_runtime::ModuleError { + index: 1, + error: [2, 2, 2, 0], + message: Some("Random"), + }), + DispatchError::Module(sp_runtime::ModuleError { + index: 1, + error: [2, 2, 2, 2], + message: Some("Random"), + }), + ]; + for dispatch_error in test_cases { + let status_code = convert_to_status_code(dispatch_error, 0); + let error: Error = status_code.into(); + assert_eq!(error, Other { dispatch_error_index: 3, error_index: 1, error: 2 }); + } + } + + // Converts 4 bytes into `Error` and handles unknown errors (used in `convert_to_status_code`). + fn into_error(mut error_bytes: [u8; 4]) -> Error { + handle_unknown_error(&mut error_bytes); + u32::from_le_bytes(error_bytes).into() + } + + // Tests the `handle_unknown_error` for `Error`, version 0. + // + // Unit variants: + // If the encoded value indicates a nested `Error` which is known by V0 as a + // unit variant, the encoded value is converted into `Error::Other`. + // + // Example: the error `BadOrigin` (encoded: `[2, 0, 0, 0]`) with a non-zero value for one + // of the bytes [1..4]: `[2, 0, 1, 0]` is converted into `[0, 2, 0, 1]` (shifting the bits + // one forward). This is decoded to `Error::Other { dispatch_error: 2, index: 0, error: 1 }`. + #[test] + fn unit_error_variants() { + let errors = vec![ + CannotLookup, + BadOrigin, + ConsumerRemaining, + NoProviders, + TooManyConsumers, + Exhausted, + Corruption, + Unavailable, + RootNotAllowed, + UnknownFunctionId, + DecodingFailed, + ]; + // Compare an `Error`, which is converted from an encoded value, with the expected `Error`. + for (i, &error_code) in UNIT_ERRORS.iter().enumerate() { + // No nesting and unit variant correctly returned. + assert_eq!(into_error([error_code, 0, 0, 0]), errors[i]); + // Unexpected second byte nested. + assert_eq!( + into_error([error_code, 1, 0, 0]), + (Other { dispatch_error_index: error_code, error_index: 1, error: 0 }), + ); + // Unexpected third byte nested. + assert_eq!( + into_error([error_code, 1, 1, 0]), + (Other { dispatch_error_index: error_code, error_index: 1, error: 1 }), + ); + // Unexpected fourth byte nested. + assert_eq!( + into_error([error_code, 1, 1, 1]), + (Other { dispatch_error_index: error_code, error_index: 1, error: 1 }), + ); + } + } + + // Single nested variants: + // If the encoded value indicates a double nested `Error` which is known by V0 + // as a single nested variant, the encoded value is converted into `Error::Other`. + // + // Example: the error `Arithmetic(Overflow)` (encoded: `[8, 1, 0, 0]`) with a non-zero + // value for one of the bytes [2..4]: `[8, 1, 1, 0]` is converted into `[0, 8, 1, 1]`. This is + // decoded to `Error::Other { dispatch_error: 8, index: 1, error: 1 }`. + #[test] + fn single_nested_error_variants() { + let errors = vec![ + [Token(FundsUnavailable), Token(OnlyProvider)], + [Arithmetic(Underflow), Arithmetic(Overflow)], + [Transactional(LimitReached), Transactional(NoLayer)], + ]; + // Compare an `Error`, which is converted from an encoded value, with the expected `Error`. + for (i, &error_code) in SINGLE_NESTED_ERRORS.iter().enumerate() { + // No nested and single nested variant correctly returned. + assert_eq!(into_error([error_code, 0, 0, 0]), errors[i][0]); + assert_eq!(into_error([error_code, 1, 0, 0]), errors[i][1]); + // Unexpected third byte nested. + assert_eq!( + into_error([error_code, 1, 1, 0]), + (Other { dispatch_error_index: error_code, error_index: 1, error: 1 }), + ); + // Unexpected fourth byte nested. + assert_eq!( + into_error([error_code, 1, 1, 1]), + Other { dispatch_error_index: error_code, error_index: 1, error: 1 }, + ); + } + } + + #[test] + fn single_nested_unknown_variants() { + // Unknown `TokenError` variant. + assert_eq!( + into_error([7, 10, 0, 0]), + Other { dispatch_error_index: 7, error_index: 10, error: 0 } + ); + // Unknown `Arithmetic` variant. + assert_eq!( + into_error([8, 3, 0, 0]), + Other { dispatch_error_index: 8, error_index: 3, error: 0 } + ); + // Unknown `Transactional` variant. + assert_eq!( + into_error([9, 2, 0, 0]), + Other { dispatch_error_index: 9, error_index: 2, error: 0 } + ); + } + + // Double nested variants: + // If the encoded value indicates a triple nested `Error` which is known by V0 + // as a double nested variant, the encoded value is converted into `Error::Other`. + // + // Example: the error `Module { index: 10, error 5 }` (encoded: `[3, 10, 5, 0]`) with a non-zero + // value for the last byte: `[3, 10, 5, 3]` is converted into `[0, 3, 10, 5]`. This is + // decoded to `Error::Other { dispatch_error: 3, index: 10, error: 5 }`. + #[test] + fn double_nested_error_variants() { + // Compare an `Error`, which is converted from an encoded value, with the expected `Error`. + // No nesting and unit variant correctly returned. + assert_eq!(into_error([3, 0, 0, 0]), Module { index: 0, error: 0 }); + // Allowed single nesting and variant correctly returned. + assert_eq!(into_error([3, 1, 0, 0]), Module { index: 1, error: 0 }); + // Allowed double nesting and variant correctly returned. + assert_eq!(into_error([3, 1, 1, 0]), Module { index: 1, error: 1 }); + // Unexpected fourth byte nested. + assert_eq!( + into_error([3, 1, 1, 1]), + Other { dispatch_error_index: 3, error_index: 1, error: 1 }, + ); + } + + #[test] + fn test_random_encoded_values() { + assert_eq!( + into_error([100, 100, 100, 100]), + Other { dispatch_error_index: 100, error_index: 100, error: 100 } + ); + assert_eq!( + into_error([200, 200, 200, 200]), + Other { dispatch_error_index: 200, error_index: 200, error: 200 } + ); + } +} diff --git a/runtime/devnet/src/extensions/v0/mod.rs b/runtime/devnet/src/extensions/v0/mod.rs new file mode 100644 index 00000000..6406e08f --- /dev/null +++ b/runtime/devnet/src/extensions/v0/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod assets; +pub(crate) mod error; diff --git a/runtime/testnet/Cargo.toml b/runtime/testnet/Cargo.toml index b7aa7b76..b04c3102 100644 --- a/runtime/testnet/Cargo.toml +++ b/runtime/testnet/Cargo.toml @@ -22,7 +22,7 @@ scale-info.workspace = true smallvec.workspace = true # Local -pop-primitives = { workspace = true, features = ["assets", "nfts", "cross-chain"] } +pop-primitives = { workspace = true, features = ["nfts", "cross-chain"] } pop-runtime-common = { workspace = true, default-features = false } # Substrate diff --git a/runtime/testnet/src/extensions.rs b/runtime/testnet/src/extensions.rs index dd2c52bb..4c224544 100644 --- a/runtime/testnet/src/extensions.rs +++ b/runtime/testnet/src/extensions.rs @@ -3,7 +3,7 @@ use frame_support::traits::{Contains, OriginTrait}; use frame_support::{ dispatch::{GetDispatchInfo, RawOrigin}, pallet_prelude::*, - traits::nonfungibles_v2::Inspect as NonFungiblesInspect, + traits::nonfungibles_v2::Inspect, }; use pallet_contracts::chain_extension::{ BufInBufOutState, ChainExtension, ChargedAmount, Environment, Ext, InitState, RetVal, @@ -86,7 +86,6 @@ impl TryFrom for v0::FuncId { 0x1 => Self::ReadState, _ => { log::error!("called an unregistered `func_id`: {:}", func_id); - // TODO: Other error. return Err(DispatchError::Other("unimplemented func_id")); }, }; @@ -207,8 +206,6 @@ where RuntimeStateKeys::ParachainSystem(key) => { read_parachain_system_state::(key, &mut env) }, - // TODO: devnet / testnet feature. - _ => Err(DispatchError::Other("Unknown state keys")), }? .encode(); @@ -218,8 +215,7 @@ where ); env.write(&result, false, None).map_err(|e| { log::trace!(target: LOG_TARGET, "{:?}", e); - // TODO: Other error. - DispatchError::Other("Unable to write results to contract memory") + DispatchError::Other("unable to write results to contract memory") }) } @@ -485,7 +481,7 @@ mod tests { log::debug!("result: {:?}", result); } - // Check for revert. + // check for revert assert!(!result.result.unwrap().did_revert(), "Contract reverted!"); assert_eq!(Nfts::owner(collection_id, item_id), Some(BOB.into())); @@ -550,7 +546,7 @@ mod tests { log::debug!("result: {:?}", result); } - // Check for revert with expected error. + // check for revert with expected error let result = result.result.unwrap(); assert!(result.did_revert()); }); @@ -610,7 +606,7 @@ mod tests { log::debug!("result: {:?}", result); } - // Check for revert. + // check for revert assert!(!result.result.unwrap().did_revert(), "Contract reverted!"); }); } @@ -673,7 +669,7 @@ mod tests { log::debug!("result: {:?}", result); } - // Check for revert. + // check for revert assert!( result.result.is_err(), "Contract execution should have failed - unimplemented runtime call!" @@ -735,7 +731,7 @@ mod tests { log::debug!("filtered result: {:?}", result); } - // Check for revert. + // check for revert assert!(!result.result.unwrap().did_revert(), "Contract reverted!"); }); }