From 0606bae7ef17a228d73ce95ce0df4a895c54e939 Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Tue, 21 Oct 2025 17:31:59 +0200 Subject: [PATCH 01/33] test: assets precompile integration test --- Cargo.lock | 36 ++ Cargo.toml | 2 + crates/e2e/src/contract_results.rs | 25 ++ crates/precompiles/Cargo.toml | 25 ++ crates/precompiles/src/erc20.rs | 234 ++++++++++ crates/precompiles/src/lib.rs | 113 +++++ crates/sandbox/Cargo.toml | 4 + crates/sandbox/src/api.rs | 2 + crates/sandbox/src/api/assets_api.rs | 359 +++++++++++++++ crates/sandbox/src/api/system_api.rs | 2 +- crates/sandbox/src/client.rs | 10 +- crates/sandbox/src/lib.rs | 27 ++ crates/sandbox/src/macros.rs | 39 +- .../public/assets-precompile/Cargo.toml | 37 ++ .../public/assets-precompile/README.md | 12 + .../public/assets-precompile/lib.rs | 422 ++++++++++++++++++ 16 files changed, 1341 insertions(+), 8 deletions(-) create mode 100644 crates/precompiles/Cargo.toml create mode 100644 crates/precompiles/src/erc20.rs create mode 100644 crates/precompiles/src/lib.rs create mode 100644 crates/sandbox/src/api/assets_api.rs create mode 100644 integration-tests/public/assets-precompile/Cargo.toml create mode 100644 integration-tests/public/assets-precompile/README.md create mode 100644 integration-tests/public/assets-precompile/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a2ba1d89af0..523d5d7370d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3815,6 +3815,13 @@ dependencies = [ "serde", ] +[[package]] +name = "ink_precompiles" +version = "6.0.0-alpha.4" +dependencies = [ + "ink", +] + [[package]] name = "ink_prelude" version = "6.0.0-beta" @@ -3911,6 +3918,8 @@ dependencies = [ "ink_primitives 6.0.0-beta", "ink_revive_types", "jsonrpsee", + "pallet-assets", + "pallet-assets-precompiles", "pallet-balances", "pallet-revive", "pallet-timestamp", @@ -4760,6 +4769,33 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "pallet-assets" +version = "29.1.0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "frame-benchmarking", + "frame-support 28.0.0", + "frame-system", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", + "scale-info", + "sp-core 28.0.0 (git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0)", + "sp-runtime 31.0.1", +] + +[[package]] +name = "pallet-assets-precompiles" +version = "0.1.0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +dependencies = [ + "ethereum-standards", + "frame-support 28.0.0", + "pallet-assets", + "pallet-revive", +] + [[package]] name = "pallet-balances" version = "28.0.0" diff --git a/Cargo.toml b/Cargo.toml index c77ec29573a..669ef88c8e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/ink/ir", "crates/ink/macro", "crates/metadata", + "crates/precompiles", "crates/revive-types", "crates/prelude", "crates/primitives", @@ -115,6 +116,7 @@ ink_env = { version = "=6.0.0-beta", path = "crates/env", default-features = fal ink_ir = { version = "=6.0.0-beta", path = "crates/ink/ir", default-features = false } ink_macro = { version = "=6.0.0-beta", path = "crates/ink/macro", default-features = false } ink_metadata = { version = "=6.0.0-beta", path = "crates/metadata", default-features = false } +ink_precompiles = { version = "=6.0.0-beta", path = "crates/precompiles", default-features = false } ink_prelude = { version = "=6.0.0-beta", path = "crates/prelude", default-features = false } ink_primitives = { version = "=6.0.0-beta", path = "crates/primitives", default-features = false } ink_revive_types = { version = "=6.0.0-beta", path = "crates/revive-types", default-features = false } diff --git a/crates/e2e/src/contract_results.rs b/crates/e2e/src/contract_results.rs index 8a070b1a74b..b3ad4c9763a 100644 --- a/crates/e2e/src/contract_results.rs +++ b/crates/e2e/src/contract_results.rs @@ -259,6 +259,31 @@ impl CallResult { pub fn return_data(&self) -> &[u8] { &self.dry_run.exec_return_value().data } + + /// Returns the root cause error from nested contract calls (e.g., precompile errors) + /// if available in the trace, otherwise returns the raw error data. + pub fn extract_error(&self) -> Option { + if !self.dry_run.did_revert() { + return None; + } + + // Check trace for error information + if let Some(trace) = &self.trace { + // // Check nested calls first (more specific errors) + for call in &trace.calls { + if let Some(error) = &call.error { + return Some(error.clone()); + } + } + + // Then check top-level error + if let Some(error) = &trace.error { + return Some(error.clone()); + } + } + // Fallback to raw data + Some(format!("{:?}", self.return_data())) + } } // TODO(#xxx) Improve the `Debug` implementation. diff --git a/crates/precompiles/Cargo.toml b/crates/precompiles/Cargo.toml new file mode 100644 index 00000000000..4deb1c1e635 --- /dev/null +++ b/crates/precompiles/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ink_precompiles" +version.workspace = true +authors = ["Use Ink "] +edition.workspace = true + +license.workspace = true +readme = "README.md" +repository.workspace = true +documentation = "https://docs.rs/ink_precompiles/" +homepage.workspace = true +description = "[ink!] Precompile interfaces for pallet-revive smart contracts." +keywords.workspace = true +categories.workspace = true +include = ["/Cargo.toml", "src/**/*.rs", "/README.md", "/LICENSE"] + +[dependencies] +ink = { workspace = true, default-features = false } + +[features] +default = ["std"] +std = [ + "ink/std", +] + diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs new file mode 100644 index 00000000000..eb37640cced --- /dev/null +++ b/crates/precompiles/src/erc20.rs @@ -0,0 +1,234 @@ +// Copyright (C) Use Ink (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! ERC-20 precompile interface for pallet-assets. +//! +//! This module provides the standard ERC-20 token interface for interacting with +//! assets managed by `pallet-assets` through the precompile mechanism. +//! +//! # Overview +//! +//! The ERC-20 Assets precompile allows smart contracts to interact with fungible assets +//! from `pallet-assets` using the ERC-20 interface. Each asset gets its own +//! precompile address, calculated by encoding the asset ID in the address. +//! +//! # Precompile Address +//! +//! - **Index**: `0x0120` +//! - **Address Format**: `[asset_id][...zeros...][0x0120]0000` +//! +//! Use [`crate::prefixed_address`] to calculate the correct address for a specific asset. +//! +//! # Example +//! +//! ```rust,ignore +//! use ink_precompiles::{erc20::{Erc20Ref, PRECOMPILE_INDEX}, prefixed_address}; +//! +//! #[ink::contract] +//! mod my_contract { +//! use super::*; +//! +//! #[ink(storage)] +//! pub struct MyContract { +//! asset_id: u32, +//! } +//! +//! impl MyContract { +//! #[ink(constructor)] +//! pub fn new(asset_id: u32) -> Self { +//! Self { asset_id } +//! } +//! +//! pub fn get_balance(&self, account: ink::Address) -> ink::U256 { +//! let precompile_addr = prefixed_address(PRECOMPILE_INDEX, self.asset_id); +//! let erc20: Erc20Ref = precompile_addr.into(); +//! erc20.balanceOf(account) +//! } +//! } +//! } +//! ``` +//! +//! # References +//! +//! - [Polkadot SDK Assets Precompile](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/assets/src/precompiles.rs) +//! - [ERC-20 Token Standard](https://eips.ethereum.org/EIPS/eip-20) + +/// ERC-20 Assets precompile index. +pub const PRECOMPILE_INDEX: u16 = 0x0120; + +/// Type alias for asset IDs. +pub type AssetId = u32; + +/// Defines the ERC-20 interface of the Asset Hub precompile. +#[ink::contract_ref(abi = "sol")] +pub trait Erc20 { + /// Returns the total supply of tokens. + /// + /// # Solidity Signature + /// + /// ```solidity + /// function totalSupply() external view returns (uint256); + /// ``` + #[ink(message)] + #[allow(non_snake_case)] + fn totalSupply(&self) -> ink::U256; + + /// Returns the balance of an account. + /// + /// # Arguments + /// * `account` - The address to query the balance of + /// + /// # Solidity Signature + /// + /// ```solidity + /// function balanceOf(address account) external view returns (uint256); + /// ``` + #[ink(message)] + #[allow(non_snake_case)] + fn balanceOf(&self, account: ink::Address) -> ink::U256; + + /// Transfers tokens to another account. + /// + /// # Arguments + /// * `to` - The recipient address + /// * `value` - The amount of tokens to transfer + /// + /// # Returns + /// + /// Returns `true` if the transfer was successful. + /// + /// # Solidity Signature + /// + /// ```solidity + /// function transfer(address to, uint256 value) external returns (bool); + /// ``` + #[ink(message)] + fn transfer(&mut self, to: ink::Address, value: ink::U256) -> bool; + + /// Returns the allowance for a spender on behalf of an owner. + /// + /// This shows how many tokens `spender` is allowed to spend on behalf of `owner`. + /// + /// # Arguments + /// * `owner` - The token owner's address + /// * `spender` - The spender's address + /// + /// # Solidity Signature + /// + /// ```solidity + /// function allowance(address owner, address spender) external view returns (uint256); + /// ``` + #[ink(message)] + fn allowance(&self, owner: ink::Address, spender: ink::Address) -> ink::U256; + + /// Approves a spender to spend tokens on behalf of the caller. + /// + /// # Arguments + /// * `spender` - The address authorized to spend tokens + /// * `value` - The maximum amount the spender can spend + /// + /// # Returns + /// + /// Returns `true` if the approval was successful. + /// + /// # Solidity Signature + /// + /// ```solidity + /// function approve(address spender, uint256 value) external returns (bool); + /// ``` + #[ink(message)] + fn approve(&mut self, spender: ink::Address, value: ink::U256) -> bool; + + /// Transfers tokens from one account to another using allowance. + /// + /// The caller must have sufficient allowance from the `from` account. + /// + /// # Arguments + /// * `from` - The address to transfer tokens from + /// * `to` - The recipient address + /// * `value` - The amount of tokens to transfer + /// + /// # Returns + /// + /// Returns `true` if the transfer was successful. + /// + /// # Solidity Signature + /// + /// ```solidity + /// function transferFrom(address from, address to, uint256 value) external returns (bool); + /// ``` + #[ink(message)] + #[allow(non_snake_case)] + fn transferFrom( + &mut self, + from: ink::Address, + to: ink::Address, + value: ink::U256, + ) -> bool; +} + +/// Creates a new ERC-20 precompile reference for the given asset ID. +/// +/// # Arguments +/// * `asset_id` - The ID of the asset to interact with +/// +/// # Returns +/// +/// Returns an `Erc20Ref` that can be used to call precompile methods. +/// +/// # Example +/// +/// ```rust,ignore +/// use ink_precompiles::erc20::erc20; +/// +/// let asset_id = 1; +/// let erc20_ref = erc20(asset_id); +/// let balance = erc20_ref.balanceOf(account); +/// ``` +pub fn erc20(asset_id: AssetId) -> Erc20Ref { + let address = crate::prefixed_address(PRECOMPILE_INDEX, asset_id); + address.into() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn erc20_precompile_address_format() { + // ERC20 Assets precompile for asset ID 1 should be at the correct address + let expected = [ + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, + ]; + + let address = crate::prefixed_address(PRECOMPILE_INDEX, 1); + let address_bytes: [u8; 20] = address.into(); + + assert_eq!(address_bytes, expected); + } + + #[test] + fn erc20_precompile_address_for_multiple_assets() { + // Test asset ID 42 + let address_42 = crate::prefixed_address(PRECOMPILE_INDEX, 42); + let bytes_42: [u8; 20] = address_42.into(); + + // First 4 bytes should be asset ID (42 = 0x0000002a) + assert_eq!(&bytes_42[0..4], &[0x00, 0x00, 0x00, 0x2a]); + + // Bytes 16-19 should be precompile index (0x0120) + assert_eq!(&bytes_42[16..20], &[0x01, 0x20, 0x00, 0x00]); + } +} diff --git a/crates/precompiles/src/lib.rs b/crates/precompiles/src/lib.rs new file mode 100644 index 00000000000..d325fbc240f --- /dev/null +++ b/crates/precompiles/src/lib.rs @@ -0,0 +1,113 @@ +// Copyright (C) Use Ink (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![doc( + html_logo_url = "https://use.ink/img/crate-docs/logo.png", + html_favicon_url = "https://use.ink/crate-docs/favicon.png" +)] +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod erc20; + +/// Calculates the address of a precompile at index `n`. +/// +/// This creates an address with the precompile index encoded in bytes 16-19 +/// as big-endian: `0x0000000000000000000000000000000000[nn]0000`. +/// +/// # Arguments +/// * `n` - The precompile index (e.g., `0x0120` for ERC20 Assets precompile) +#[inline] +pub fn fixed_address(n: u16) -> ink::Address { + let shifted = (n as u32) << 16; + + let suffix = shifted.to_be_bytes(); + let mut address = [0u8; 20]; + let mut i = 16; + while i < address.len() { + address[i] = suffix[i - 16]; + i = i + 1; + } + ink::Address::from(address) +} + +/// Calculates the address of a precompile at index `n` with an additional prefix. +/// +/// This is used for precompiles that encode additional information in the address, +/// such as the ERC20 Assets precompile which encodes the asset ID in bytes 0-3. +/// +/// The resulting address format is: `[prefix][...00000000000000][nn]0000` +/// where `prefix` occupies bytes 0-3 and `nn` is the precompile index in bytes 16-19. +/// +/// # Arguments +/// * `n` - The precompile index (e.g., `0x0120` for ERC20 Assets precompile) +/// * `prefix` - A 32-bit value to encode in the first 4 bytes (e.g., asset ID) +#[inline] +pub fn prefixed_address(n: u16, prefix: u32) -> ink::Address { + let address = fixed_address(n); + let mut address_bytes: [u8; 20] = address.into(); + address_bytes[..4].copy_from_slice(&prefix.to_be_bytes()); + ink::Address::from(address_bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fixed_address_works() { + let expected = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, + ]; + let result = fixed_address(100); + let address_bytes: [u8; 20] = result.into(); + assert_eq!(address_bytes, expected); + } + + #[test] + fn prefixed_address_works() { + let expected = [ + 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, + ]; + let result = prefixed_address(101, u32::MAX); + let address_bytes: [u8; 20] = result.into(); + assert_eq!(address_bytes, expected); + } + + #[test] + fn system_precompile_address() { + // System precompile is at index 0x0009 + let expected = [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, + ]; + let result = fixed_address(0x0009); + let address_bytes: [u8; 20] = result.into(); + assert_eq!(address_bytes, expected); + } + + #[test] + fn erc20_assets_precompile_address() { + // ERC20 Assets precompile is at index 0x0120 with asset ID 1 + // Index 0x0120 shifted left 16 bits = 0x01200000 in bytes 16-19 + let expected = [ + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, + ]; + let result = prefixed_address(0x0120, 1); + let address_bytes: [u8; 20] = result.into(); + assert_eq!(address_bytes, expected); + } +} diff --git a/crates/sandbox/Cargo.toml b/crates/sandbox/Cargo.toml index 84880293460..6e317235224 100644 --- a/crates/sandbox/Cargo.toml +++ b/crates/sandbox/Cargo.toml @@ -14,6 +14,8 @@ sha3 = "0.10.8" frame-metadata = { workspace = true } frame-system = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } frame-support = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } +pallet-assets = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } +pallet-assets-precompiles = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } pallet-balances = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } pallet-transaction-payment = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } pallet-revive = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4be1941420dd25dc81102fb79d8e2a7f0", default-features = false } @@ -47,6 +49,8 @@ std = [ "frame-metadata/std", "ink_primitives/std", "ink_revive_types/std", + "pallet-assets/std", + "pallet-assets-precompiles/std", "pallet-balances/std", "pallet-transaction-payment/std", "pallet-revive/std", diff --git a/crates/sandbox/src/api.rs b/crates/sandbox/src/api.rs index 190d9ad8f87..5ce3d230471 100644 --- a/crates/sandbox/src/api.rs +++ b/crates/sandbox/src/api.rs @@ -1,3 +1,4 @@ +pub mod assets_api; pub mod balance_api; pub mod revive_api; pub mod system_api; @@ -5,6 +6,7 @@ pub mod timestamp_api; pub mod prelude { pub use super::{ + assets_api::AssetsAPI, balance_api::BalanceAPI, revive_api::ContractAPI, system_api::SystemAPI, diff --git a/crates/sandbox/src/api/assets_api.rs b/crates/sandbox/src/api/assets_api.rs new file mode 100644 index 00000000000..3fc79fb99c2 --- /dev/null +++ b/crates/sandbox/src/api/assets_api.rs @@ -0,0 +1,359 @@ +use crate::{ + AccountIdFor, + Sandbox, +}; +use frame_support::{ + pallet_prelude::DispatchError, + traits::fungibles::{ + Create, + Destroy, + Inspect, + Mutate, + approvals::{ + Inspect as _, + Mutate as _, + }, + metadata::Mutate as _, + }, +}; + +type AssetIdOf = >::AssetId; +type AssetBalanceOf = >::Balance; + +/// Assets API for the sandbox. +/// +/// Provides methods to create, mint, and manage assets in `pallet-assets`. +pub trait AssetsAPI +where + T: Sandbox, + T::Runtime: pallet_assets::Config, + I: 'static, +{ + /// Creates `value` amount of tokens and assigns them to `account`, increasing the + /// total supply. + /// + /// # Arguments + /// * `id` - ID of the new asset to be created. + /// * `owner` - The owner of the created asset. + /// * `min_balance` - The asset amount one account need at least. + fn create( + &mut self, + id: &AssetIdOf, + owner: &AccountIdFor, + min_balance: AssetBalanceOf, + ) -> Result<(), DispatchError>; + + /// Start the destruction an existing fungible asset. + /// + /// # Arguments + /// * `asset` - ID of the asset. + fn start_destroy( + &mut self, + asset: &AssetIdOf, + ) -> Result<(), DispatchError>; + + /// Start the destruction an existing fungible asset. + /// + /// # Arguments + /// * `asset` - ID of the asset. + /// * `name` - Token name. + /// * `symbol` - Token symbol. + /// * `decimals` - Token decimals. + fn set_metadata( + &mut self, + asset: &AssetIdOf, + owner: &AccountIdFor, + name: Vec, + symbol: Vec, + decimals: u8, + ) -> Result<(), DispatchError>; + + /// Approves `spender` to spend `value` amount of tokens on behalf of the caller. + /// + /// Successive calls of this method overwrite previous values. + /// + /// # Arguments + /// * `asset` - ID of the asset. + /// * `spender` - The account that is allowed to spend the tokens. + /// * `value` - The number of tokens to approve. + fn approve( + &mut self, + asset: &AssetIdOf, + owner: &AccountIdFor, + delegate: &AccountIdFor, + amount: AssetBalanceOf, + ) -> Result<(), DispatchError>; + + /// Creates `value` amount of tokens and assigns them to `account`, increasing the + /// total supply. + /// + /// # Arguments + /// * `asset` - ID of the asset. + /// * `account` - The account to be credited with the created tokens. + /// * `value` - The number of tokens to mint. + fn mint_into( + &mut self, + asset: &AssetIdOf, + account: &AccountIdFor, + value: AssetBalanceOf, + ) -> Result, DispatchError>; + + /// Transfer `amount` of tokens from `origin` to `dest`. + /// + /// # Arguments + /// * `asset` - ID of the asset. + /// * `source` - The account from which tokens are transferred. + /// * `dest` - The account to which tokens are transferred. + /// * `amount` - The number of tokens to transfer. + fn transfer( + &mut self, + asset: &AssetIdOf, + source: &AccountIdFor, + dest: &AccountIdFor, + amount: AssetBalanceOf, + ) -> Result<(), DispatchError>; + + /// Returns the account balance for the specified `owner`. + /// + /// # Arguments + /// * `owner` - The account whose balance is being queried. + fn balance_of( + &mut self, + asset: &AssetIdOf, + owner: &AccountIdFor, + ) -> AssetBalanceOf; + + /// Returns the total supply of the `asset`. + /// + /// # Arguments + /// * `asset` - ID of the asset. + fn total_supply( + &mut self, + asset: &AssetIdOf, + ) -> AssetBalanceOf; + + /// Returns the allowance for a `spender` approved by an `owner`. + /// + /// # Arguments + /// * `asset` - ID of the asset. + /// * `owner` - The account that owns the tokens. + /// * `spender` - The account that is allowed to spend the tokens. + fn allowance( + &mut self, + asset: &AssetIdOf, + owner: &AccountIdFor, + delegate: &AccountIdFor, + ) -> AssetBalanceOf; + + /// Check if the asset exists. + /// + /// # Arguments + /// * `asset` - ID of the asset. + fn asset_exists(&mut self, asset: &AssetIdOf) -> bool; +} + +impl AssetsAPI for T +where + T: Sandbox, + T::Runtime: pallet_assets::Config, + I: 'static, +{ + fn create( + &mut self, + id: &AssetIdOf, + owner: &AccountIdFor, + min_balance: AssetBalanceOf, + ) -> Result<(), DispatchError> { + self.execute_with(|| { + as Create< + AccountIdFor, + >>::create(id.clone(), owner.clone(), true, min_balance) + }) + } + + fn start_destroy( + &mut self, + asset: &AssetIdOf, + ) -> Result<(), DispatchError> { + self.execute_with(|| { + as Destroy< + AccountIdFor, + >>::start_destroy(asset.clone(), None) + }) + } + + fn set_metadata( + &mut self, + asset: &AssetIdOf, + owner: &AccountIdFor, + name: Vec, + symbol: Vec, + decimals: u8, + ) -> Result<(), DispatchError> { + self.execute_with(|| { + pallet_assets::Pallet::::set( + asset.clone().into(), + owner, + name, + symbol, + decimals, + ) + }) + } + + fn mint_into( + &mut self, + asset: &AssetIdOf, + account: &AccountIdFor, + value: AssetBalanceOf, + ) -> Result, DispatchError> { + self.execute_with(|| { + pallet_assets::Pallet::::mint_into( + asset.clone(), + account, + value, + ) + }) + } + + fn transfer( + &mut self, + asset: &AssetIdOf, + source: &AccountIdFor, + dest: &AccountIdFor, + amount: AssetBalanceOf, + ) -> Result<(), DispatchError> { + self.execute_with(|| { + as Mutate>>::transfer( + asset.clone(), + source, + dest, + amount, + frame_support::traits::tokens::Preservation::Preserve, + ).map(|_| ()) + }) + } + + fn approve( + &mut self, + asset: &AssetIdOf, + owner: &AccountIdFor, + delegate: &AccountIdFor, + amount: AssetBalanceOf, + ) -> Result<(), DispatchError> { + self.execute_with(|| { + pallet_assets::Pallet::::approve( + asset.clone(), + owner, + delegate, + amount, + ) + }) + } + + fn balance_of( + &mut self, + asset: &AssetIdOf, + owner: &AccountIdFor, + ) -> AssetBalanceOf { + self.execute_with(|| { + pallet_assets::Pallet::::balance(asset.clone(), owner) + }) + } + + fn total_supply( + &mut self, + asset: &AssetIdOf, + ) -> AssetBalanceOf { + self.execute_with(|| { + pallet_assets::Pallet::::total_supply(asset.clone()) + }) + } + + fn allowance( + &mut self, + asset: &AssetIdOf, + owner: &AccountIdFor, + delegate: &AccountIdFor, + ) -> AssetBalanceOf { + self.execute_with(|| { + pallet_assets::Pallet::::allowance( + asset.clone(), + owner, + delegate, + ) + }) + } + + fn asset_exists(&mut self, asset: &AssetIdOf) -> bool { + self.execute_with(|| { + pallet_assets::Pallet::::asset_exists(asset.clone()) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::DefaultSandbox; + + #[test] + fn create_works() { + let mut sandbox = DefaultSandbox::default(); + let admin = DefaultSandbox::default_actor(); + let asset_id = 1u32; + let min_balance = 1u128; + + let result = sandbox.create(&asset_id, &admin, min_balance); + + assert!(result.is_ok()); + assert!(sandbox.asset_exists(&asset_id)); + } + + #[test] + fn mint_asset_works() { + let mut sandbox = DefaultSandbox::default(); + let admin = DefaultSandbox::default_actor(); + let asset_id = 1u32; + + sandbox.create(&asset_id, &admin, 1u128).unwrap(); + + let balance_before = sandbox.balance_of(&asset_id, &admin); + assert_eq!(balance_before, 0); + + sandbox.mint_into(&asset_id, &admin, 100u128).unwrap(); + + let balance_after = sandbox.balance_of(&asset_id, &admin); + assert_eq!(balance_after, 100); + } + + #[test] + fn total_supply_works() { + let mut sandbox = DefaultSandbox::default(); + let admin = DefaultSandbox::default_actor(); + let asset_id = 1u32; + + sandbox.create(&asset_id, &admin, 1u128).unwrap(); + + let supply_before = sandbox.total_supply(&asset_id); + assert_eq!(supply_before, 0); + + sandbox.mint_into(&asset_id, &admin, 1000u128).unwrap(); + + let supply_after = sandbox.total_supply(&asset_id); + assert_eq!(supply_after, 1000); + } + + #[test] + fn asset_exists_works() { + let mut sandbox = DefaultSandbox::default(); + let admin = DefaultSandbox::default_actor(); + let asset_id = 1u32; + + assert!(!sandbox.asset_exists(&asset_id)); + + sandbox.create(&asset_id, &admin, 1u128).unwrap(); + + assert!(sandbox.asset_exists(&asset_id)); + } +} diff --git a/crates/sandbox/src/api/system_api.rs b/crates/sandbox/src/api/system_api.rs index ac333fab8fd..80a7cdf53a4 100644 --- a/crates/sandbox/src/api/system_api.rs +++ b/crates/sandbox/src/api/system_api.rs @@ -143,7 +143,7 @@ mod tests { let initial_balance = sandbox.free_balance(&actor); sandbox.dry_run(|sandbox| { - sandbox.mint_into(&actor, 100).unwrap(); + crate::api::balance_api::BalanceAPI::mint_into(sandbox, &actor, 100).unwrap(); assert_eq!(sandbox.free_balance(&actor), initial_balance + 100); }); diff --git a/crates/sandbox/src/client.rs b/crates/sandbox/src/client.rs index 48e2b298a59..732e1a11714 100644 --- a/crates/sandbox/src/client.rs +++ b/crates/sandbox/src/client.rs @@ -133,6 +133,14 @@ where } } + /// Get a mutable reference to the underlying sandbox. + /// + /// This allows direct access to all sandbox methods and traits, making it easy to + /// interact with runtime pallets like `pallet-assets`: + pub fn sandbox(&mut self) -> &mut S { + &mut self.sandbox + } + fn fund_accounts(sandbox: &mut S) { const TOKENS: u128 = 1_000_000_000_000_000; @@ -485,7 +493,7 @@ where Ok(result) => result, Err(err) => { log_error(&format!("upload failed: {err:?}")); - return Err(SandboxErr::new(format!("bare_upload: {err:?}"))) + return Err(SandboxErr::new(format!("bare_upload: {err:?}"))); } }; diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index 65e33383c63..c80955517f5 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -27,8 +27,10 @@ use ink_revive_types::{ }, }; pub use macros::{ + AssetIdForTrustBackedAssets, BlockBuilder, DefaultSandbox, + TrustBackedAssetsInstance, }; use pallet_revive::{ ContractResult, @@ -47,6 +49,8 @@ pub use { }, }, frame_system, + pallet_assets, + pallet_assets_precompiles, pallet_balances, pallet_revive, pallet_timestamp, @@ -248,3 +252,26 @@ pub fn to_revive_storage_deposit( } } } + +/// Returns Alice's `AccountId32` for testing. +pub fn alice() -> AccountId32 { + AccountId32::from(ink_e2e::alice().public_key().0) +} + +/// Returns Bob's `AccountId32` for testing. +pub fn bob() -> AccountId32 { + AccountId32::from(ink_e2e::bob().public_key().0) +} + +/// Returns Charlie's `AccountId32` for testing. +pub fn charlie() -> AccountId32 { + AccountId32::from(ink_e2e::charlie().public_key().0) +} + +/// Creates an `AccountId32` from an ink contract account ID. +/// +/// This is a convenience function for e2e tests that need to convert contract +/// account IDs to the `AccountId32` type used by the sandbox runtime. +pub fn account_id_from_contract(account_id: &ink_primitives::AccountId) -> AccountId32 { + AccountId32::from(*AsRef::<[u8; 32]>::as_ref(account_id)) +} diff --git a/crates/sandbox/src/macros.rs b/crates/sandbox/src/macros.rs index bb1cb910694..7b6b255f773 100644 --- a/crates/sandbox/src/macros.rs +++ b/crates/sandbox/src/macros.rs @@ -136,6 +136,7 @@ mod construct_runtime { System: $crate::frame_system, Balances: $crate::pallet_balances, Timestamp: $crate::pallet_timestamp, + Assets: $crate::pallet_assets::, Revive: $crate::pallet_revive, TransactionPayment: $crate::pallet_transaction_payment, $( @@ -176,6 +177,34 @@ mod construct_runtime { type WeightInfo = (); } + // Configure pallet-assets (Instance1 for Trust Backed Assets) + pub type TrustBackedAssetsInstance = $crate::pallet_assets::Instance1; + pub type AssetIdForTrustBackedAssets = u32; + + impl $crate::pallet_assets::Config for $runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = AssetIdForTrustBackedAssets; + type AssetIdParameter = scale::Compact; + type Currency = Balances; + type CreateOrigin = $crate::frame_support::traits::AsEnsureOriginWithArg<$crate::frame_system::EnsureSigned>; + type ForceOrigin = $crate::frame_system::EnsureRoot; + type AssetDeposit = ConstU128<1>; + type AssetAccountDeposit = ConstU128<1>; + type MetadataDepositBase = ConstU128<1>; + type MetadataDepositPerByte = ConstU128<1>; + type ApprovalDeposit = ConstU128<1>; + type StringLimit = ConstU32<50>; + type Freezer = (); + type Holder = (); + type Extra = (); + type WeightInfo = (); + type CallbackHandle = $crate::pallet_assets::AutoIncAssetId<$runtime, TrustBackedAssetsInstance>; + type RemoveItemsLimit = ConstU32<1000>; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHelper = (); + } + impl $crate::pallet_transaction_payment::Config for $runtime { type RuntimeEvent = RuntimeEvent; type OnChargeTransaction = FungibleAdapter; @@ -231,10 +260,7 @@ mod construct_runtime { type InstantiateOrigin = $crate::frame_system::EnsureSigned; type FindAuthor = (); type Precompiles = ( - // todo - //ERC20, TrustBackedAssetsInstance>, - //ERC20, PoolAssetsInstance>, - //XcmPrecompile, + $crate::pallet_assets_precompiles::ERC20, TrustBackedAssetsInstance>, ); type AllowEVMBytecode = ConstBool; type FeeInfo = (); @@ -339,8 +365,9 @@ mod construct_runtime { // Export runtime type itself, pallets and useful types from the auxiliary module pub use construct_runtime::{ - $sandbox, $runtime, Balances, Revive, PalletInfo, RuntimeCall, RuntimeEvent, RuntimeHoldReason, - RuntimeOrigin, System, Timestamp, + $sandbox, $runtime, Assets, AssetIdForTrustBackedAssets, Balances, Revive, PalletInfo, + RuntimeCall, RuntimeEvent, RuntimeHoldReason, RuntimeOrigin, System, Timestamp, + TrustBackedAssetsInstance, }; }; } diff --git a/integration-tests/public/assets-precompile/Cargo.toml b/integration-tests/public/assets-precompile/Cargo.toml new file mode 100644 index 00000000000..012fa9be2ab --- /dev/null +++ b/integration-tests/public/assets-precompile/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "assets_precompile" +version = "6.0.0-alpha.4" +authors = ["Use Ink "] +edition = "2024" +publish = false + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } +ink_precompiles = { path = "../../../crates/precompiles", default-features = false } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e" } +ink_sandbox = { path = "../../../crates/sandbox" } +hex = "0.4" + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "ink_precompiles/std", +] +ink-as-dependency = [] +e2e-tests = [] + +[package.metadata.ink-lang] +abi = "ink" + +[lints.rust.unexpected_cfgs] +level = "warn" +check-cfg = [ + 'cfg(ink_abi, values("ink", "sol", "all"))' +] + diff --git a/integration-tests/public/assets-precompile/README.md b/integration-tests/public/assets-precompile/README.md new file mode 100644 index 00000000000..b04a9779e66 --- /dev/null +++ b/integration-tests/public/assets-precompile/README.md @@ -0,0 +1,12 @@ +# Assets Precompile Integration + +This contract demonstrates how to interact with ERC-20 asset precompiles in ink! using the `ink_precompiles` crate. + +## Overview + +This example shows how to: +- Use the `ink_precompiles` crate for precompile interfaces +- Test precompile interactions using the runtime-only e2e test framework +- Work with `pallet-assets` through the ERC-20 precompile +- Set up accounts and assets for e2e tests +- Debug precompile errors with `extract_error()` diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs new file mode 100644 index 00000000000..5c690e213cd --- /dev/null +++ b/integration-tests/public/assets-precompile/lib.rs @@ -0,0 +1,422 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +pub use ink_precompiles::erc20::{ + erc20, + AssetId, +}; + +#[ink::contract] +mod asset_hub_precompile { + use super::{ + AssetId, + erc20, + }; + use ink::prelude::string::String; + use ink_precompiles::erc20::Erc20; + + #[ink(storage)] + pub struct AssetHubPrecompile { + asset_id: AssetId, + } + + impl AssetHubPrecompile { + /// Creates a new contract instance for a specific asset ID. + #[ink(constructor, payable)] + pub fn new(asset_id: AssetId) -> Self { + Self { asset_id } + } + + /// Returns the asset ID this contract is configured for. + #[ink(message)] + pub fn asset_id(&self) -> AssetId { + self.asset_id + } + + /// Gets the total supply by calling the precompile. + #[ink(message)] + pub fn total_supply(&self) -> ink::U256 { + let precompile = erc20(self.asset_id); + precompile.totalSupply() + } + + /// Gets the balance of an account. + #[ink(message)] + pub fn balance_of(&self, account: ink::Address) -> ink::U256 { + let precompile = erc20(self.asset_id); + precompile.balanceOf(account) + } + + /// Transfers tokens to another account. + #[ink(message)] + pub fn transfer( + &mut self, + to: ink::Address, + amount: ink::U256, + ) -> Result { + let mut precompile = erc20(self.asset_id); + Ok(precompile.transfer(to, amount)) + } + + /// Approves a spender. + #[ink(message)] + pub fn approve( + &mut self, + spender: ink::Address, + amount: ink::U256, + ) -> Result { + let mut precompile = erc20(self.asset_id); + Ok(precompile.approve(spender, amount)) + } + + /// Gets the allowance for a spender. + #[ink(message)] + pub fn allowance(&self, owner: ink::Address, spender: ink::Address) -> ink::U256 { + let precompile = erc20(self.asset_id); + precompile.allowance(owner, spender) + } + + /// Transfers tokens from one account to another using allowance. + #[ink(message)] + pub fn transfer_from( + &mut self, + from: ink::Address, + to: ink::Address, + amount: ink::U256, + ) -> Result { + let mut precompile = erc20(self.asset_id); + Ok(precompile.transferFrom(from, to, amount)) + } + } + +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn contract_stores_asset_id() { + use asset_hub_precompile::AssetHubPrecompile; + + let contract = AssetHubPrecompile::new(1337); + + assert_eq!(contract.asset_id(), 1337); + } +} + +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests { + use super::*; + use ink_e2e::ContractsBackend; + use ink_sandbox::{Sandbox, api::prelude::{ContractAPI, AssetsAPI}}; + + type E2EResult = std::result::Result>; + + #[ink_sandbox::test(backend(runtime_only( + sandbox = ink_sandbox::DefaultSandbox, + client = ink_sandbox::SandboxClient + )))] + async fn deployment_works(mut client: Client) -> E2EResult<()> { + let asset_id: u32 = 1; + let mut constructor = + asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + + let contract = client + .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .value(1_000_000_000_000u128) // Transfer native tokens to contract + .submit() + .await + .expect("instantiate failed"); + + let call_builder = + contract.call_builder::(); + let asset_id_call = call_builder.asset_id(); + let result = client + .call(&ink_e2e::alice(), &asset_id_call) + .dry_run() + .await?; + + assert_eq!(result.return_value(), asset_id); + + Ok(()) + } + + #[ink_sandbox::test(backend(runtime_only( + sandbox = ink_sandbox::DefaultSandbox, + client = ink_sandbox::SandboxClient + )))] + async fn total_supply_works(mut client: Client) -> E2EResult<()> { + let asset_id: u32 = 1; + let admin = ink_sandbox::alice(); + + client.sandbox().create(&asset_id, &admin, 1u128).expect("Failed to create asset"); + client.sandbox().mint_into(&asset_id, &admin, 1000u128).expect("Failed to mint asset"); + + let mut constructor = + asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let contract = client + .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = + contract.call_builder::(); + + let total_supply = call_builder.total_supply(); + let result = client + .call(&ink_e2e::alice(), &total_supply) + .submit() + .await?; + + let supply = result.return_value(); + assert_eq!(supply, ink::U256::from(1000)); + + Ok(()) + } + + #[ink_sandbox::test(backend(runtime_only( + sandbox = ink_sandbox::DefaultSandbox, + client = ink_sandbox::SandboxClient + )))] + async fn balance_of_works(mut client: Client) -> E2EResult<()> { + let asset_id: u32 = 1; + let alice = ink_sandbox::alice(); + let bob = ink_sandbox::bob(); + + client.sandbox().create(&asset_id, &alice, 1u128).expect("Failed to create asset"); + client.sandbox().mint_into(&asset_id, &alice, 1000u128).expect("Failed to mint to alice"); + client.sandbox().mint_into(&asset_id, &bob, 500u128).expect("Failed to mint to bob"); + + let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let contract = client + .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + let alice_address = ink_e2e::address_from_account_id(&alice); + let bob_address = ink_e2e::address_from_account_id(&bob); + + // Map bob's account so the precompile can find his assets. + // (alice is already mapped during instantiate) + let bob_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + client.sandbox().map_account(bob_origin).expect("Failed to map bob's account"); + + let alice_balance_call = call_builder.balance_of(alice_address); + let alice_result = client + .call(&ink_e2e::alice(), &alice_balance_call) + .dry_run() + .await?; + let alice_balance = alice_result.return_value(); + + let bob_balance_call = call_builder.balance_of(bob_address); + let bob_result = client + .call(&ink_e2e::alice(), &bob_balance_call) + .dry_run() + .await?; + let bob_balance = bob_result.return_value(); + + assert_eq!(alice_balance, ink::U256::from(1000)); + assert_eq!(bob_balance, ink::U256::from(500)); + + Ok(()) + } + + #[ink_sandbox::test(backend(runtime_only( + sandbox = ink_sandbox::DefaultSandbox, + client = ink_sandbox::SandboxClient + )))] + async fn transfer_works(mut client: Client) -> E2EResult<()> { + let asset_id: u32 = 1; + let alice = ink_sandbox::alice(); + let bob = ink_sandbox::bob(); + + client.sandbox().create(&asset_id, &alice, 1u128).expect("Failed to create asset"); + + let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let contract = client + .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let bob_address = ink_e2e::address_from_account_id(&bob); + let transfer_amount = ink::U256::from(1_000); + + // Get contract's AccountId32 - the precompile will see the contract as caller! + let contract_account = ink_sandbox::account_id_from_contract(&contract.account_id); + client.sandbox().mint_into(&asset_id, &contract_account, 100_000u128).expect("Failed to mint to contract"); + + let bob_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + client.sandbox().map_account(bob_origin).expect("Failed to map bob's account"); + + let transfer = call_builder.transfer(bob_address, transfer_amount); + let result = client.call(&ink_e2e::alice(), &transfer).submit().await?; + + let transfer_result = result.return_value(); + assert!(transfer_result.is_ok()); + let contract_balance = client.sandbox().balance_of(&asset_id, &contract_account); + let bob_balance = client.sandbox().balance_of(&asset_id, &bob); + assert_eq!(contract_balance, 99_000u128); // Contract had 100_000, transferred 1_000 + assert_eq!(bob_balance, 1_000u128); // Bob received 1_000 + + // Show error case with transferring too many tokens. + let transfer = call_builder.transfer(bob_address, ink::U256::from(1_000_000)); + let result = client.call(&ink_e2e::alice(), &transfer).submit().await?; + assert_eq!(result.extract_error(), Some("BalanceLow".to_string())); + + Ok(()) + } + + #[ink_sandbox::test(backend(runtime_only( + sandbox = ink_sandbox::DefaultSandbox, + client = ink_sandbox::SandboxClient + )))] + async fn approve_works(mut client: Client) -> E2EResult<()> { + let asset_id: u32 = 1; + let alice = ink_sandbox::alice(); + let bob = ink_sandbox::bob(); + + client.sandbox().create(&asset_id, &alice, 1u128).expect("Failed to create asset"); + + let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let contract = client + .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + // Contract needs native balance for approval deposit. + .value(100_000) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let contract_account = ink_sandbox::account_id_from_contract(&contract.account_id); + client.sandbox().mint_into(&asset_id, &contract_account, 100_000u128).expect("Failed to mint to contract"); + + let bob_address = ink_e2e::address_from_account_id(&bob); + let approve_amount = ink::U256::from(200); + + let bob_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + client.sandbox().map_account(bob_origin).expect("Failed to map bob's account"); + + let bob_allowance_before = client.sandbox().allowance(&asset_id, &contract_account, &bob); + assert_eq!(bob_allowance_before, 0u128); // Bob's allowance is 0 + + let approve = call_builder.approve(bob_address, approve_amount); + let result = client.call(&ink_e2e::alice(), &approve).submit().await?; + + assert!(result.return_value().is_ok()); + let bob_allowance = client.sandbox().allowance(&asset_id, &contract_account, &bob); + assert_eq!(bob_allowance, 200u128); // Bob's allowance is 200 + + Ok(()) + } + + #[ink_sandbox::test(backend(runtime_only( + sandbox = ink_sandbox::DefaultSandbox, + client = ink_sandbox::SandboxClient + )))] + async fn allowance_works(mut client: Client) -> E2EResult<()> { + let asset_id: u32 = 1; + let alice = ink_sandbox::alice(); + let bob = ink_sandbox::bob(); + + client.sandbox().create(&asset_id, &alice, 1u128).expect("Failed to create asset"); + + let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let contract = client + .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let call_builder = contract.call_builder::(); + + client.sandbox().mint_into(&asset_id, &alice, 100_000u128).expect("Failed to mint to bob"); + + let alice_address = ink_e2e::address_from_account_id(&alice); + let bob_address = ink_e2e::address_from_account_id(&bob); + let bob_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + client.sandbox().map_account(bob_origin).expect("Failed to map bob's account"); + + let allowance_call = call_builder.allowance(alice_address, bob_address); + let result = client + .call(&ink_e2e::alice(), &allowance_call) + .dry_run() + .await?; + let allowance_before = result.return_value(); + + assert_eq!(allowance_before, ink::U256::from(0)); + + // Approve bob to spend alice's tokens + client.sandbox().approve(&asset_id, &alice, &bob, 300u128).expect("Failed to approve"); + + let result = client + .call(&ink_e2e::alice(), &allowance_call) + .dry_run() + .await?; + let allowance_after = result.return_value(); + + assert_eq!(allowance_after, ink::U256::from(300)); + + Ok(()) + } + + /// Tests transferFrom functionality. + #[ink_sandbox::test(backend(runtime_only( + sandbox = ink_sandbox::DefaultSandbox, + client = ink_sandbox::SandboxClient + )))] + async fn transfer_from_works(mut client: Client) -> E2EResult<()> { + let asset_id: u32 = 1; + let alice = ink_sandbox::alice(); + let bob = ink_sandbox::bob(); + let charlie = ink_sandbox::charlie(); + + client.sandbox().create(&asset_id, &alice, 1u128).expect("Failed to create asset"); + + let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let contract = client + .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let contract_account = ink_sandbox::account_id_from_contract(&contract.account_id); + client.sandbox().mint_into(&asset_id, &bob, 100_000u128).expect("Failed to mint to contract"); + + // Approve bob to spend contract's tokens + client.sandbox().approve(&asset_id, &bob, &contract_account, 50_000u128).expect("Failed to approve"); + + let bob_address = ink_e2e::address_from_account_id(&bob); + let charlie_address = ink_e2e::address_from_account_id(&charlie); + let transfer_amount = ink::U256::from(1_500); + + let bob_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + client.sandbox().map_account(bob_origin).expect("Failed to map bob's account"); + + let charlie_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(charlie.clone()); + client.sandbox().map_account(charlie_origin).expect("Failed to map charlie's account"); + + let transfer_from = call_builder.transfer_from(bob_address, charlie_address, transfer_amount); + let result = client.call(&ink_e2e::bob(), &transfer_from).submit().await?; + + assert!(result.return_value().is_ok()); + + let bob_balance = client.sandbox().balance_of(&asset_id, &bob); + let charlie_balance = client.sandbox().balance_of(&asset_id, &charlie); + let contract_allowance = client.sandbox().allowance(&asset_id, &bob, &contract_account); + + assert_eq!(bob_balance, 98_500u128); // 100_000 - 1_500 + assert_eq!(charlie_balance, 1_500u128); + assert_eq!(contract_allowance, 48_500u128); + + // Show error case with transferring more tokens than approved. + let transfer_from = call_builder.transfer_from(bob_address, charlie_address, ink::U256::from(1_000_000)); + let result = client.call(&ink_e2e::bob(), &transfer_from).submit().await?; + assert_eq!(result.extract_error(), Some("Unapproved".to_string())); + + Ok(()) + } +} From 55470d53e45e80ceeb49185d7e8de2e819596edb Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Wed, 22 Oct 2025 12:20:56 +0200 Subject: [PATCH 02/33] fix: fmt --- CHANGELOG.md | 1 + crates/e2e/src/contract_results.rs | 2 +- .../public/assets-precompile/lib.rs | 246 +++++++++++++----- 3 files changed, 176 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca27953caa4..10a2cfa1d4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `ink_revive_types` (and remove `pallet-revive` dependency from `ink_e2e`) - [#2657](https://github.com/use-ink/ink/pull/2657) - non-allocating Solidity ABI encoder - [#2655](https://github.com/use-ink/ink/pull/2655) - Implement XCM precompile, stabilize XCM API - [#2687](https://github.com/use-ink/ink/pull/2687) +- Add `ink_precompiles` crate with ERC-20 assets precompile interface - [#2686](https://github.com/use-ink/ink/pull/2686) ### Changed - Marks the `pallet-revive` host function `account_id` stable - [#2578](https://github.com/use-ink/ink/pull/2578) diff --git a/crates/e2e/src/contract_results.rs b/crates/e2e/src/contract_results.rs index b3ad4c9763a..bf2d0c1f8b7 100644 --- a/crates/e2e/src/contract_results.rs +++ b/crates/e2e/src/contract_results.rs @@ -260,7 +260,7 @@ impl CallResult { &self.dry_run.exec_return_value().data } - /// Returns the root cause error from nested contract calls (e.g., precompile errors) + /// Returns the error from nested contract calls (e.g., precompile errors) /// if available in the trace, otherwise returns the raw error data. pub fn extract_error(&self) -> Option { if !self.dry_run.did_revert() { diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index 5c690e213cd..16d18524d86 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -1,8 +1,8 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] pub use ink_precompiles::erc20::{ - erc20, AssetId, + erc20, }; #[ink::contract] @@ -87,7 +87,6 @@ mod asset_hub_precompile { Ok(precompile.transferFrom(from, to, amount)) } } - } #[cfg(test)] @@ -108,7 +107,13 @@ mod tests { mod e2e_tests { use super::*; use ink_e2e::ContractsBackend; - use ink_sandbox::{Sandbox, api::prelude::{ContractAPI, AssetsAPI}}; + use ink_sandbox::{ + Sandbox, + api::prelude::{ + AssetsAPI, + ContractAPI, + }, + }; type E2EResult = std::result::Result>; @@ -118,8 +123,7 @@ mod e2e_tests { )))] async fn deployment_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; - let mut constructor = - asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); let contract = client .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) @@ -148,12 +152,17 @@ mod e2e_tests { async fn total_supply_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; let admin = ink_sandbox::alice(); - - client.sandbox().create(&asset_id, &admin, 1u128).expect("Failed to create asset"); - client.sandbox().mint_into(&asset_id, &admin, 1000u128).expect("Failed to mint asset"); - let mut constructor = - asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + client + .sandbox() + .create(&asset_id, &admin, 1u128) + .expect("Failed to create asset"); + client + .sandbox() + .mint_into(&asset_id, &admin, 1000u128) + .expect("Failed to mint asset"); + + let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); let contract = client .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) .submit() @@ -182,10 +191,19 @@ mod e2e_tests { let asset_id: u32 = 1; let alice = ink_sandbox::alice(); let bob = ink_sandbox::bob(); - - client.sandbox().create(&asset_id, &alice, 1u128).expect("Failed to create asset"); - client.sandbox().mint_into(&asset_id, &alice, 1000u128).expect("Failed to mint to alice"); - client.sandbox().mint_into(&asset_id, &bob, 500u128).expect("Failed to mint to bob"); + + client + .sandbox() + .create(&asset_id, &alice, 1u128) + .expect("Failed to create asset"); + client + .sandbox() + .mint_into(&asset_id, &alice, 1000u128) + .expect("Failed to mint to alice"); + client + .sandbox() + .mint_into(&asset_id, &bob, 500u128) + .expect("Failed to mint to bob"); let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); let contract = client @@ -193,15 +211,20 @@ mod e2e_tests { .submit() .await .expect("instantiate failed"); - let call_builder = contract.call_builder::(); + let call_builder = + contract.call_builder::(); let alice_address = ink_e2e::address_from_account_id(&alice); let bob_address = ink_e2e::address_from_account_id(&bob); - + // Map bob's account so the precompile can find his assets. // (alice is already mapped during instantiate) - let bob_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); - client.sandbox().map_account(bob_origin).expect("Failed to map bob's account"); + let bob_origin = + ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + client + .sandbox() + .map_account(bob_origin) + .expect("Failed to map bob's account"); let alice_balance_call = call_builder.balance_of(alice_address); let alice_result = client @@ -209,7 +232,7 @@ mod e2e_tests { .dry_run() .await?; let alice_balance = alice_result.return_value(); - + let bob_balance_call = call_builder.balance_of(bob_address); let bob_result = client .call(&ink_e2e::alice(), &bob_balance_call) @@ -231,8 +254,11 @@ mod e2e_tests { let asset_id: u32 = 1; let alice = ink_sandbox::alice(); let bob = ink_sandbox::bob(); - - client.sandbox().create(&asset_id, &alice, 1u128).expect("Failed to create asset"); + + client + .sandbox() + .create(&asset_id, &alice, 1u128) + .expect("Failed to create asset"); let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); let contract = client @@ -240,17 +266,26 @@ mod e2e_tests { .submit() .await .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); + let mut call_builder = + contract.call_builder::(); let bob_address = ink_e2e::address_from_account_id(&bob); let transfer_amount = ink::U256::from(1_000); - + // Get contract's AccountId32 - the precompile will see the contract as caller! - let contract_account = ink_sandbox::account_id_from_contract(&contract.account_id); - client.sandbox().mint_into(&asset_id, &contract_account, 100_000u128).expect("Failed to mint to contract"); - - let bob_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); - client.sandbox().map_account(bob_origin).expect("Failed to map bob's account"); + let contract_account = + ink_sandbox::account_id_from_contract(&contract.account_id); + client + .sandbox() + .mint_into(&asset_id, &contract_account, 100_000u128) + .expect("Failed to mint to contract"); + + let bob_origin = + ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + client + .sandbox() + .map_account(bob_origin) + .expect("Failed to map bob's account"); let transfer = call_builder.transfer(bob_address, transfer_amount); let result = client.call(&ink_e2e::alice(), &transfer).submit().await?; @@ -259,8 +294,8 @@ mod e2e_tests { assert!(transfer_result.is_ok()); let contract_balance = client.sandbox().balance_of(&asset_id, &contract_account); let bob_balance = client.sandbox().balance_of(&asset_id, &bob); - assert_eq!(contract_balance, 99_000u128); // Contract had 100_000, transferred 1_000 - assert_eq!(bob_balance, 1_000u128); // Bob received 1_000 + assert_eq!(contract_balance, 99_000u128); // Contract had 100_000, transferred 1_000 + assert_eq!(bob_balance, 1_000u128); // Bob received 1_000 // Show error case with transferring too many tokens. let transfer = call_builder.transfer(bob_address, ink::U256::from(1_000_000)); @@ -278,8 +313,11 @@ mod e2e_tests { let asset_id: u32 = 1; let alice = ink_sandbox::alice(); let bob = ink_sandbox::bob(); - - client.sandbox().create(&asset_id, &alice, 1u128).expect("Failed to create asset"); + + client + .sandbox() + .create(&asset_id, &alice, 1u128) + .expect("Failed to create asset"); let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); let contract = client @@ -289,26 +327,41 @@ mod e2e_tests { .submit() .await .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); + let mut call_builder = + contract.call_builder::(); - let contract_account = ink_sandbox::account_id_from_contract(&contract.account_id); - client.sandbox().mint_into(&asset_id, &contract_account, 100_000u128).expect("Failed to mint to contract"); + let contract_account = + ink_sandbox::account_id_from_contract(&contract.account_id); + client + .sandbox() + .mint_into(&asset_id, &contract_account, 100_000u128) + .expect("Failed to mint to contract"); let bob_address = ink_e2e::address_from_account_id(&bob); let approve_amount = ink::U256::from(200); - - let bob_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); - client.sandbox().map_account(bob_origin).expect("Failed to map bob's account"); - let bob_allowance_before = client.sandbox().allowance(&asset_id, &contract_account, &bob); - assert_eq!(bob_allowance_before, 0u128); // Bob's allowance is 0 + let bob_origin = + ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + client + .sandbox() + .map_account(bob_origin) + .expect("Failed to map bob's account"); + + let bob_allowance_before = + client + .sandbox() + .allowance(&asset_id, &contract_account, &bob); + assert_eq!(bob_allowance_before, 0u128); // Bob's allowance is 0 let approve = call_builder.approve(bob_address, approve_amount); let result = client.call(&ink_e2e::alice(), &approve).submit().await?; assert!(result.return_value().is_ok()); - let bob_allowance = client.sandbox().allowance(&asset_id, &contract_account, &bob); - assert_eq!(bob_allowance, 200u128); // Bob's allowance is 200 + let bob_allowance = + client + .sandbox() + .allowance(&asset_id, &contract_account, &bob); + assert_eq!(bob_allowance, 200u128); // Bob's allowance is 200 Ok(()) } @@ -321,8 +374,11 @@ mod e2e_tests { let asset_id: u32 = 1; let alice = ink_sandbox::alice(); let bob = ink_sandbox::bob(); - - client.sandbox().create(&asset_id, &alice, 1u128).expect("Failed to create asset"); + + client + .sandbox() + .create(&asset_id, &alice, 1u128) + .expect("Failed to create asset"); let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); let contract = client @@ -330,14 +386,22 @@ mod e2e_tests { .submit() .await .expect("instantiate failed"); - let call_builder = contract.call_builder::(); + let call_builder = + contract.call_builder::(); + + client + .sandbox() + .mint_into(&asset_id, &alice, 100_000u128) + .expect("Failed to mint to bob"); - client.sandbox().mint_into(&asset_id, &alice, 100_000u128).expect("Failed to mint to bob"); - let alice_address = ink_e2e::address_from_account_id(&alice); let bob_address = ink_e2e::address_from_account_id(&bob); - let bob_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); - client.sandbox().map_account(bob_origin).expect("Failed to map bob's account"); + let bob_origin = + ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + client + .sandbox() + .map_account(bob_origin) + .expect("Failed to map bob's account"); let allowance_call = call_builder.allowance(alice_address, bob_address); let result = client @@ -349,7 +413,10 @@ mod e2e_tests { assert_eq!(allowance_before, ink::U256::from(0)); // Approve bob to spend alice's tokens - client.sandbox().approve(&asset_id, &alice, &bob, 300u128).expect("Failed to approve"); + client + .sandbox() + .approve(&asset_id, &alice, &bob, 300u128) + .expect("Failed to approve"); let result = client .call(&ink_e2e::alice(), &allowance_call) @@ -367,13 +434,18 @@ mod e2e_tests { sandbox = ink_sandbox::DefaultSandbox, client = ink_sandbox::SandboxClient )))] - async fn transfer_from_works(mut client: Client) -> E2EResult<()> { + async fn transfer_from_works( + mut client: Client, + ) -> E2EResult<()> { let asset_id: u32 = 1; let alice = ink_sandbox::alice(); let bob = ink_sandbox::bob(); let charlie = ink_sandbox::charlie(); - - client.sandbox().create(&asset_id, &alice, 1u128).expect("Failed to create asset"); + + client + .sandbox() + .create(&asset_id, &alice, 1u128) + .expect("Failed to create asset"); let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); let contract = client @@ -381,40 +453,70 @@ mod e2e_tests { .submit() .await .expect("instantiate failed"); - let mut call_builder = contract.call_builder::(); + let mut call_builder = + contract.call_builder::(); + + let contract_account = + ink_sandbox::account_id_from_contract(&contract.account_id); + client + .sandbox() + .mint_into(&asset_id, &bob, 100_000u128) + .expect("Failed to mint to contract"); - let contract_account = ink_sandbox::account_id_from_contract(&contract.account_id); - client.sandbox().mint_into(&asset_id, &bob, 100_000u128).expect("Failed to mint to contract"); - // Approve bob to spend contract's tokens - client.sandbox().approve(&asset_id, &bob, &contract_account, 50_000u128).expect("Failed to approve"); + client + .sandbox() + .approve(&asset_id, &bob, &contract_account, 50_000u128) + .expect("Failed to approve"); let bob_address = ink_e2e::address_from_account_id(&bob); let charlie_address = ink_e2e::address_from_account_id(&charlie); let transfer_amount = ink::U256::from(1_500); - - let bob_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); - client.sandbox().map_account(bob_origin).expect("Failed to map bob's account"); - - let charlie_origin = ink_sandbox::DefaultSandbox::convert_account_to_origin(charlie.clone()); - client.sandbox().map_account(charlie_origin).expect("Failed to map charlie's account"); - let transfer_from = call_builder.transfer_from(bob_address, charlie_address, transfer_amount); - let result = client.call(&ink_e2e::bob(), &transfer_from).submit().await?; + let bob_origin = + ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + client + .sandbox() + .map_account(bob_origin) + .expect("Failed to map bob's account"); + + let charlie_origin = + ink_sandbox::DefaultSandbox::convert_account_to_origin(charlie.clone()); + client + .sandbox() + .map_account(charlie_origin) + .expect("Failed to map charlie's account"); + + let transfer_from = + call_builder.transfer_from(bob_address, charlie_address, transfer_amount); + let result = client + .call(&ink_e2e::bob(), &transfer_from) + .submit() + .await?; assert!(result.return_value().is_ok()); - + let bob_balance = client.sandbox().balance_of(&asset_id, &bob); let charlie_balance = client.sandbox().balance_of(&asset_id, &charlie); - let contract_allowance = client.sandbox().allowance(&asset_id, &bob, &contract_account); - - assert_eq!(bob_balance, 98_500u128); // 100_000 - 1_500 + let contract_allowance = + client + .sandbox() + .allowance(&asset_id, &bob, &contract_account); + + assert_eq!(bob_balance, 98_500u128); // 100_000 - 1_500 assert_eq!(charlie_balance, 1_500u128); assert_eq!(contract_allowance, 48_500u128); // Show error case with transferring more tokens than approved. - let transfer_from = call_builder.transfer_from(bob_address, charlie_address, ink::U256::from(1_000_000)); - let result = client.call(&ink_e2e::bob(), &transfer_from).submit().await?; + let transfer_from = call_builder.transfer_from( + bob_address, + charlie_address, + ink::U256::from(1_000_000), + ); + let result = client + .call(&ink_e2e::bob(), &transfer_from) + .submit() + .await?; assert_eq!(result.extract_error(), Some("Unapproved".to_string())); Ok(()) From ae40b0af3fd1244a36b5c49ee73d440bf7435ddd Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Thu, 23 Oct 2025 14:16:07 +0200 Subject: [PATCH 03/33] feat: test events in sandbox environment --- crates/ink/macro/src/contract_ref.rs | 2 +- crates/precompiles/src/erc20.rs | 22 +- crates/sandbox/Cargo.toml | 8 + crates/sandbox/src/client.rs | 124 +++++++- crates/sandbox/src/lib.rs | 39 +++ .../public/assets-precompile/lib.rs | 271 +++++++++++------- 6 files changed, 348 insertions(+), 118 deletions(-) diff --git a/crates/ink/macro/src/contract_ref.rs b/crates/ink/macro/src/contract_ref.rs index 56265a40aaa..3e3bd6f04c6 100644 --- a/crates/ink/macro/src/contract_ref.rs +++ b/crates/ink/macro/src/contract_ref.rs @@ -68,7 +68,7 @@ pub fn analyze_or_err( #trait_def_impl // Type alias for contract ref. - type #contract_ref_name = + pub type #contract_ref_name = <<::ink::reflect::TraitDefinitionRegistry<#env> as #trait_name> ::__ink_TraitInfo as ::ink::codegen::TraitCallForwarder>::Forwarder<#abi_ty>; )) diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs index eb37640cced..3717c4b6bab 100644 --- a/crates/precompiles/src/erc20.rs +++ b/crates/precompiles/src/erc20.rs @@ -64,6 +64,11 @@ //! - [Polkadot SDK Assets Precompile](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/assets/src/precompiles.rs) //! - [ERC-20 Token Standard](https://eips.ethereum.org/EIPS/eip-20) +use ink::{ + Address, + U256, +}; + /// ERC-20 Assets precompile index. pub const PRECOMPILE_INDEX: u16 = 0x0120; @@ -82,7 +87,7 @@ pub trait Erc20 { /// ``` #[ink(message)] #[allow(non_snake_case)] - fn totalSupply(&self) -> ink::U256; + fn totalSupply(&self) -> U256; /// Returns the balance of an account. /// @@ -96,7 +101,7 @@ pub trait Erc20 { /// ``` #[ink(message)] #[allow(non_snake_case)] - fn balanceOf(&self, account: ink::Address) -> ink::U256; + fn balanceOf(&self, account: Address) -> U256; /// Transfers tokens to another account. /// @@ -114,7 +119,7 @@ pub trait Erc20 { /// function transfer(address to, uint256 value) external returns (bool); /// ``` #[ink(message)] - fn transfer(&mut self, to: ink::Address, value: ink::U256) -> bool; + fn transfer(&mut self, to: Address, value: U256) -> bool; /// Returns the allowance for a spender on behalf of an owner. /// @@ -130,7 +135,7 @@ pub trait Erc20 { /// function allowance(address owner, address spender) external view returns (uint256); /// ``` #[ink(message)] - fn allowance(&self, owner: ink::Address, spender: ink::Address) -> ink::U256; + fn allowance(&self, owner: Address, spender: Address) -> U256; /// Approves a spender to spend tokens on behalf of the caller. /// @@ -148,7 +153,7 @@ pub trait Erc20 { /// function approve(address spender, uint256 value) external returns (bool); /// ``` #[ink(message)] - fn approve(&mut self, spender: ink::Address, value: ink::U256) -> bool; + fn approve(&mut self, spender: Address, value: U256) -> bool; /// Transfers tokens from one account to another using allowance. /// @@ -170,12 +175,7 @@ pub trait Erc20 { /// ``` #[ink(message)] #[allow(non_snake_case)] - fn transferFrom( - &mut self, - from: ink::Address, - to: ink::Address, - value: ink::U256, - ) -> bool; + fn transferFrom(&mut self, from: Address, to: Address, value: U256) -> bool; } /// Creates a new ERC-20 precompile reference for the given asset ID. diff --git a/crates/sandbox/Cargo.toml b/crates/sandbox/Cargo.toml index 6e317235224..49adaa52635 100644 --- a/crates/sandbox/Cargo.toml +++ b/crates/sandbox/Cargo.toml @@ -62,3 +62,11 @@ std = [ "sp-io/std", "ink_e2e_macro/std", ] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-revive/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", +] diff --git a/crates/sandbox/src/client.rs b/crates/sandbox/src/client.rs index 732e1a11714..2e8a49e6a45 100644 --- a/crates/sandbox/src/client.rs +++ b/crates/sandbox/src/client.rs @@ -14,6 +14,7 @@ use crate::{ AccountIdFor, + EventRecordOf, RuntimeCall, Sandbox, api::prelude::*, @@ -179,7 +180,7 @@ where type AccountId = AccountId; type Balance = BalanceOf; type Error = SandboxErr; - type EventLog = (); + type EventLog = Vec>; async fn create_and_fund_account( &mut self, @@ -234,7 +235,8 @@ where RuntimeCall::::decode(&mut encoded_call.as_slice()) .expect("Failed to decode runtime call"); - // Execute the call. + // Execute the call and record events emitted during this operation. + let start = self.sandbox.events().len(); self.sandbox .runtime_call( decoded_call, @@ -243,8 +245,9 @@ where .map_err(|err| { SandboxErr::new(format!("runtime_call: execution error {:?}", err.error)) })?; - - Ok(()) + let all = self.sandbox.events(); + let events = all[start..].to_vec(); + Ok(events) } async fn transfer_allow_death( @@ -342,6 +345,8 @@ where let mut tracer = self.sandbox.evm_tracer(tracer_type); let mut code_hash: Option = None; + // Record events emitted during instantiation + let start = self.sandbox.events().len(); let result = pallet_revive::tracing::trace(tracer.as_tracing(), || { code_hash = Some(H256(ink_e2e::code_hash(&code[..]))); self.sandbox.deploy_contract( @@ -379,10 +384,13 @@ where .as_ref() .to_owned(); + let all = self.sandbox.events(); + let events = all[start..].to_vec(); + Ok(BareInstantiationResult { addr: addr_raw, account_id: account_id.into(), - events: (), // todo: https://github.com/Cardinal-Cryptography/drink/issues/32 + events, trace, code_hash: code_hash.expect("code_hash must have been calculated"), }) @@ -484,7 +492,8 @@ where storage_deposit_limit: Option, ) -> Result, Self::Error> { let code = self.contracts.load_code(contract_name); - + // Record events emitted during upload + let start = self.sandbox.events().len(); let result = match self.sandbox.upload_contract( code, caller_to_origin::(caller), @@ -497,13 +506,15 @@ where } }; + let all = self.sandbox.events(); + let events = all[start..].to_vec(); Ok(UploadResult { code_hash: result.code_hash, dry_run: Ok(CodeUploadReturnValue { code_hash: result.code_hash, deposit: result.deposit, }), - events: (), + events, }) } @@ -561,6 +572,8 @@ where storage_deposit_limit: E::Balance, signer: &Keypair, ) -> Result<(Self::EventLog, Option), Self::Error> { + // Record events emitted during the contract call + let start = self.sandbox.events().len(); // todo let tracer_type = TracerType::CallTracer(Some(CallTracerConfig::default())); let mut tracer = self.sandbox.evm_tracer(tracer_type); @@ -586,7 +599,9 @@ where _ => None, }; - Ok(((), trace)) + let all = self.sandbox.events(); + let events = all[start..].to_vec(); + Ok((events, trace)) } /// Important: For an uncomplicated UX of the E2E testing environment we @@ -704,6 +719,97 @@ where } } +impl Client +where + S::Runtime: pallet_revive::Config, + ::RuntimeEvent: + TryInto>, +{ + /// Returns true if any events were emitted during the last operation. + pub fn has_events(&mut self) -> bool { + !self.sandbox.events().is_empty() + } + + /// Returns the last event that was emitted, if any. + pub fn last_event(&mut self) -> Option> { + self.sandbox.events().last().cloned() + } + + pub fn contract_events(&mut self) -> Vec> + where + S::Runtime: pallet_revive::Config, + ::RuntimeEvent: + TryInto>, + { + self.sandbox + .events() + .iter() + .filter_map(|event_record| { + if let Ok(pallet_event) = &event_record.event.clone().try_into() { + match pallet_event { + pallet_revive::Event::::ContractEmitted { + data, + .. + } => Some(data.clone()), + _ => None, + } + } else { + None + } + }) + .collect::>>() + } + + /// Returns the last contract event that was emitted, if any. + pub fn last_contract_event(&mut self) -> Option> + where + S::Runtime: pallet_revive::Config, + ::RuntimeEvent: + TryInto>, + { + self.contract_events().last().cloned() + } +} + +/// Helper function for the `assert_last_contract_event!` macro. +/// +/// # Parameters: +/// - `client` - The client for interacting with the sandbox. +/// - `event` - The expected event. +#[track_caller] +pub fn assert_last_contract_event_inner( + client: &mut Client, + event: E, +) where + S: Sandbox, + S::Runtime: pallet_revive::Config, + ::RuntimeEvent: + TryInto>, + E: Decode + scale::Encode + core::fmt::Debug, +{ + match client.last_contract_event() { + Some(last_event) => { + if last_event != event.encode().as_slice() { + let decoded = E::decode(&mut &last_event[..]).expect("Decoding failed"); + panic!("{}", assert_message(&decoded, &event)); + } + } + None => panic!("{}", assert_message(&"None", &event)), + } +} + +fn assert_message( + left: &L, + right: &R, +) -> String { + format!( + r#"assertion `left == right` failed + left: {:?} + right: {:?}"#, + left, right + ) +} + impl< AccountId: Clone + Send + Sync + From<[u8; 32]> + AsRef<[u8; 32]>, Config: Sandbox, @@ -734,7 +840,7 @@ where AccountIdFor: From<[u8; 32]> + AsRef<[u8; 32]>, { type Error = SandboxErr; - type EventLog = (); + type EventLog = Vec>; } /// Exposes preset sandbox configurations to be used in tests. diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index c80955517f5..d0d805a434d 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -70,6 +70,45 @@ pub use client::{ }; pub use ink_e2e_macro::test; +/// Asserts that the latest contract event matches an expected event. +/// +/// This macro verifies that the last emitted contract event from the sandbox +/// matches the provided expected event. +/// +/// # Example +/// +/// ```no_run +/// use ink_sandbox::{ +/// DefaultSandbox, +/// SandboxClient, +/// assert_last_contract_event, +/// }; +/// +/// # async fn test() { +/// let mut client: SandboxClient<_, DefaultSandbox> = todo!(); +/// +/// // After a contract call that emits an event... +/// assert_last_contract_event!( +/// &mut client, +/// Transfer { +/// from: alice_address, +/// to: bob_address, +/// value: 100u128, +/// } +/// ); +/// # } +/// ``` +/// +/// # Parameters +/// - `client` - Mutable reference to the sandbox client +/// - `event` - The expected event (must implement `Encode`, `Decode`, and `Debug`) +#[macro_export] +macro_rules! assert_last_contract_event { + ($client:expr, $event:expr $(,)?) => { + $crate::client::assert_last_contract_event_inner($client, $event) + }; +} + /// A snapshot of the storage. #[derive(Clone, Debug)] pub struct Snapshot { diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index 16d18524d86..ef984dd0973 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -1,29 +1,34 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] -pub use ink_precompiles::erc20::{ - AssetId, - erc20, -}; +use ink::prelude::string::ToString; +use ink::{H160, U256}; +pub use ink_precompiles::erc20::{AssetId, erc20}; #[ink::contract] mod asset_hub_precompile { - use super::{ - AssetId, - erc20, - }; + use super::*; use ink::prelude::string::String; - use ink_precompiles::erc20::Erc20; + use ink_precompiles::erc20::{Erc20, Erc20Ref}; #[ink(storage)] pub struct AssetHubPrecompile { asset_id: AssetId, + /// The owner of this contract. Only the owner can call transfer, approve, and transfer_from. + /// This is necessary because the contract holds tokens and without access control, + /// anyone could transfer tokens that the contract holds, which would be a security issue. + owner: H160, + precompile: Erc20Ref, } impl AssetHubPrecompile { /// Creates a new contract instance for a specific asset ID. #[ink(constructor, payable)] pub fn new(asset_id: AssetId) -> Self { - Self { asset_id } + Self { + asset_id, + owner: Self::env().caller(), + precompile: erc20(asset_id), + } } /// Returns the asset ID this contract is configured for. @@ -32,61 +37,106 @@ mod asset_hub_precompile { self.asset_id } + /// Returns the owner of this contract. + #[ink(message)] + pub fn owner(&self) -> H160 { + self.owner + } + + /// Ensures only the owner can call this function. + fn ensure_owner(&self) -> Result<(), String> { + if self.env().caller() != self.owner { + return Err("Only owner can call this function".to_string()); + } + Ok(()) + } + /// Gets the total supply by calling the precompile. #[ink(message)] - pub fn total_supply(&self) -> ink::U256 { - let precompile = erc20(self.asset_id); - precompile.totalSupply() + pub fn total_supply(&self) -> U256 { + self.precompile.totalSupply() } /// Gets the balance of an account. #[ink(message)] - pub fn balance_of(&self, account: ink::Address) -> ink::U256 { - let precompile = erc20(self.asset_id); - precompile.balanceOf(account) + pub fn balance_of(&self, account: Address) -> U256 { + self.precompile.balanceOf(account) } /// Transfers tokens to another account. #[ink(message)] - pub fn transfer( - &mut self, - to: ink::Address, - amount: ink::U256, - ) -> Result { - let mut precompile = erc20(self.asset_id); - Ok(precompile.transfer(to, amount)) + pub fn transfer(&mut self, to: Address, value: U256) -> Result { + self.ensure_owner()?; + if !self.precompile.transfer(to, value) { + return Err("Transfer failed".to_string()); + } + self.env().emit_event(Transfer { + from: self.env().address(), + to, + value, + }); + Ok(true) } /// Approves a spender. #[ink(message)] - pub fn approve( - &mut self, - spender: ink::Address, - amount: ink::U256, - ) -> Result { - let mut precompile = erc20(self.asset_id); - Ok(precompile.approve(spender, amount)) + pub fn approve(&mut self, spender: Address, value: U256) -> Result { + self.ensure_owner()?; + if !self.precompile.approve(spender, value) { + return Err("Approval failed".to_string()); + } + self.env().emit_event(Approval { + owner: self.env().address(), + spender, + value, + }); + Ok(true) } /// Gets the allowance for a spender. #[ink(message)] - pub fn allowance(&self, owner: ink::Address, spender: ink::Address) -> ink::U256 { - let precompile = erc20(self.asset_id); - precompile.allowance(owner, spender) + pub fn allowance(&self, owner: Address, spender: Address) -> U256 { + self.precompile.allowance(owner, spender) } /// Transfers tokens from one account to another using allowance. #[ink(message)] pub fn transfer_from( &mut self, - from: ink::Address, - to: ink::Address, - amount: ink::U256, + from: Address, + to: Address, + value: U256, ) -> Result { - let mut precompile = erc20(self.asset_id); - Ok(precompile.transferFrom(from, to, amount)) + self.ensure_owner()?; + if !self.precompile.transferFrom(from, to, value) { + return Err("Transfer failed".to_string()); + } + self.env().emit_event(Transfer { from, to, value }); + Ok(true) } } + + /// Event emitted when allowance by `owner` to `spender` changes. + #[derive(Debug)] + #[ink::event] + pub struct Approval { + #[ink(topic)] + pub owner: Address, + #[ink(topic)] + pub spender: Address, + pub value: U256, + } + + /// Event emitted when transfer of tokens occurs. + #[derive(Debug)] + #[ink::event] + pub struct Transfer { + #[ink(topic)] + pub from: Address, + #[ink(topic)] + pub to: Address, + pub value: U256, + } } #[cfg(test)] @@ -101,29 +151,38 @@ mod tests { assert_eq!(contract.asset_id(), 1337); } + + #[test] + fn contract_stores_owner() { + use asset_hub_precompile::AssetHubPrecompile; + + let contract = AssetHubPrecompile::new(1337); + + assert_eq!(contract.asset_id(), 1337); + // Note: In unit tests, the caller is always the zero address + assert_eq!(contract.owner(), H160::from([0u8; 20])); + } } #[cfg(all(test, feature = "e2e-tests"))] mod e2e_tests { use super::*; + use crate::asset_hub_precompile::{Transfer, Approval, AssetHubPrecompile, AssetHubPrecompileRef}; use ink_e2e::ContractsBackend; use ink_sandbox::{ - Sandbox, - api::prelude::{ - AssetsAPI, - ContractAPI, - }, + Sandbox, assert_last_contract_event, DefaultSandbox, SandboxClient, account_id_from_contract, + api::prelude::{AssetsAPI, ContractAPI}, }; type E2EResult = std::result::Result>; #[ink_sandbox::test(backend(runtime_only( - sandbox = ink_sandbox::DefaultSandbox, - client = ink_sandbox::SandboxClient + sandbox = DefaultSandbox, + client = SandboxClient )))] async fn deployment_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; - let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) @@ -133,7 +192,7 @@ mod e2e_tests { .expect("instantiate failed"); let call_builder = - contract.call_builder::(); + contract.call_builder::(); let asset_id_call = call_builder.asset_id(); let result = client .call(&ink_e2e::alice(), &asset_id_call) @@ -146,8 +205,8 @@ mod e2e_tests { } #[ink_sandbox::test(backend(runtime_only( - sandbox = ink_sandbox::DefaultSandbox, - client = ink_sandbox::SandboxClient + sandbox = DefaultSandbox, + client = SandboxClient )))] async fn total_supply_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; @@ -178,14 +237,14 @@ mod e2e_tests { .await?; let supply = result.return_value(); - assert_eq!(supply, ink::U256::from(1000)); + assert_eq!(supply, U256::from(1000)); Ok(()) } #[ink_sandbox::test(backend(runtime_only( - sandbox = ink_sandbox::DefaultSandbox, - client = ink_sandbox::SandboxClient + sandbox = DefaultSandbox, + client = SandboxClient )))] async fn balance_of_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; @@ -220,7 +279,7 @@ mod e2e_tests { // Map bob's account so the precompile can find his assets. // (alice is already mapped during instantiate) let bob_origin = - ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() .map_account(bob_origin) @@ -240,15 +299,15 @@ mod e2e_tests { .await?; let bob_balance = bob_result.return_value(); - assert_eq!(alice_balance, ink::U256::from(1000)); - assert_eq!(bob_balance, ink::U256::from(500)); + assert_eq!(alice_balance, U256::from(1000)); + assert_eq!(bob_balance, U256::from(500)); Ok(()) } #[ink_sandbox::test(backend(runtime_only( - sandbox = ink_sandbox::DefaultSandbox, - client = ink_sandbox::SandboxClient + sandbox = DefaultSandbox, + client = SandboxClient )))] async fn transfer_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; @@ -270,18 +329,19 @@ mod e2e_tests { contract.call_builder::(); let bob_address = ink_e2e::address_from_account_id(&bob); - let transfer_amount = ink::U256::from(1_000); + let transfer_amount = U256::from(1_000); // Get contract's AccountId32 - the precompile will see the contract as caller! let contract_account = - ink_sandbox::account_id_from_contract(&contract.account_id); + account_id_from_contract(&contract.account_id); + let contract_address = ink_e2e::address_from_account_id(&contract_account); client .sandbox() .mint_into(&asset_id, &contract_account, 100_000u128) .expect("Failed to mint to contract"); let bob_origin = - ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() .map_account(bob_origin) @@ -292,13 +352,21 @@ mod e2e_tests { let transfer_result = result.return_value(); assert!(transfer_result.is_ok()); + assert_last_contract_event!( + &mut client, + Transfer { + from: contract_address, + to: bob_address, + value: transfer_amount + } + ); let contract_balance = client.sandbox().balance_of(&asset_id, &contract_account); let bob_balance = client.sandbox().balance_of(&asset_id, &bob); assert_eq!(contract_balance, 99_000u128); // Contract had 100_000, transferred 1_000 assert_eq!(bob_balance, 1_000u128); // Bob received 1_000 // Show error case with transferring too many tokens. - let transfer = call_builder.transfer(bob_address, ink::U256::from(1_000_000)); + let transfer = call_builder.transfer(bob_address, U256::from(1_000_000)); let result = client.call(&ink_e2e::alice(), &transfer).submit().await?; assert_eq!(result.extract_error(), Some("BalanceLow".to_string())); @@ -306,8 +374,8 @@ mod e2e_tests { } #[ink_sandbox::test(backend(runtime_only( - sandbox = ink_sandbox::DefaultSandbox, - client = ink_sandbox::SandboxClient + sandbox = DefaultSandbox, + client = SandboxClient )))] async fn approve_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; @@ -331,17 +399,18 @@ mod e2e_tests { contract.call_builder::(); let contract_account = - ink_sandbox::account_id_from_contract(&contract.account_id); + account_id_from_contract(&contract.account_id); + let contract_address = ink_e2e::address_from_account_id(&contract_account); client .sandbox() .mint_into(&asset_id, &contract_account, 100_000u128) .expect("Failed to mint to contract"); let bob_address = ink_e2e::address_from_account_id(&bob); - let approve_amount = ink::U256::from(200); + let approve_amount = U256::from(200); let bob_origin = - ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() .map_account(bob_origin) @@ -357,6 +426,14 @@ mod e2e_tests { let result = client.call(&ink_e2e::alice(), &approve).submit().await?; assert!(result.return_value().is_ok()); + assert_last_contract_event!( + &mut client, + Approval { + owner: contract_address, + spender: bob_address, + value: approve_amount, + } + ); let bob_allowance = client .sandbox() @@ -367,8 +444,8 @@ mod e2e_tests { } #[ink_sandbox::test(backend(runtime_only( - sandbox = ink_sandbox::DefaultSandbox, - client = ink_sandbox::SandboxClient + sandbox = DefaultSandbox, + client = SandboxClient )))] async fn allowance_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; @@ -397,7 +474,7 @@ mod e2e_tests { let alice_address = ink_e2e::address_from_account_id(&alice); let bob_address = ink_e2e::address_from_account_id(&bob); let bob_origin = - ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() .map_account(bob_origin) @@ -410,7 +487,7 @@ mod e2e_tests { .await?; let allowance_before = result.return_value(); - assert_eq!(allowance_before, ink::U256::from(0)); + assert_eq!(allowance_before, U256::from(0)); // Approve bob to spend alice's tokens client @@ -424,15 +501,15 @@ mod e2e_tests { .await?; let allowance_after = result.return_value(); - assert_eq!(allowance_after, ink::U256::from(300)); + assert_eq!(allowance_after, U256::from(300)); Ok(()) } /// Tests transferFrom functionality. #[ink_sandbox::test(backend(runtime_only( - sandbox = ink_sandbox::DefaultSandbox, - client = ink_sandbox::SandboxClient + sandbox = DefaultSandbox, + client = SandboxClient )))] async fn transfer_from_works( mut client: Client, @@ -440,7 +517,6 @@ mod e2e_tests { let asset_id: u32 = 1; let alice = ink_sandbox::alice(); let bob = ink_sandbox::bob(); - let charlie = ink_sandbox::charlie(); client .sandbox() @@ -457,64 +533,65 @@ mod e2e_tests { contract.call_builder::(); let contract_account = - ink_sandbox::account_id_from_contract(&contract.account_id); + account_id_from_contract(&contract.account_id); client .sandbox() - .mint_into(&asset_id, &bob, 100_000u128) + .mint_into(&asset_id, &alice, 100_000u128) .expect("Failed to mint to contract"); - // Approve bob to spend contract's tokens + // Approve alice to spend contract's tokens client .sandbox() - .approve(&asset_id, &bob, &contract_account, 50_000u128) + .approve(&asset_id, &alice, &contract_account, 50_000u128) .expect("Failed to approve"); + let alice_address = ink_e2e::address_from_account_id(&alice); let bob_address = ink_e2e::address_from_account_id(&bob); - let charlie_address = ink_e2e::address_from_account_id(&charlie); - let transfer_amount = ink::U256::from(1_500); + let transfer_amount = U256::from(1_500); let bob_origin = - ink_sandbox::DefaultSandbox::convert_account_to_origin(bob.clone()); + DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() .map_account(bob_origin) .expect("Failed to map bob's account"); - let charlie_origin = - ink_sandbox::DefaultSandbox::convert_account_to_origin(charlie.clone()); - client - .sandbox() - .map_account(charlie_origin) - .expect("Failed to map charlie's account"); - let transfer_from = - call_builder.transfer_from(bob_address, charlie_address, transfer_amount); + call_builder.transfer_from(alice_address, bob_address, transfer_amount); let result = client - .call(&ink_e2e::bob(), &transfer_from) + .call(&ink_e2e::alice(), &transfer_from) .submit() .await?; assert!(result.return_value().is_ok()); + assert_last_contract_event!( + &mut client, + Transfer { + from: alice_address, + to: bob_address, + value: transfer_amount, + } + ); + let alice_balance = client.sandbox().balance_of(&asset_id, &alice); let bob_balance = client.sandbox().balance_of(&asset_id, &bob); - let charlie_balance = client.sandbox().balance_of(&asset_id, &charlie); let contract_allowance = client .sandbox() - .allowance(&asset_id, &bob, &contract_account); + .allowance(&asset_id, &alice, &contract_account); - assert_eq!(bob_balance, 98_500u128); // 100_000 - 1_500 - assert_eq!(charlie_balance, 1_500u128); + assert_eq!(alice_balance, 98_500u128); // 100_000 - 1_500 + assert_eq!(bob_balance, 1_500u128); assert_eq!(contract_allowance, 48_500u128); // Show error case with transferring more tokens than approved. let transfer_from = call_builder.transfer_from( + alice_address, bob_address, - charlie_address, - ink::U256::from(1_000_000), + U256::from(1_000_000), ); let result = client - .call(&ink_e2e::bob(), &transfer_from) + .call(&ink_e2e::alice(), &transfer_from) .submit() .await?; assert_eq!(result.extract_error(), Some("Unapproved".to_string())); From c46d9f2a280bb2543e5dcfd8de0e558b7cdc3f39 Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Thu, 23 Oct 2025 14:30:39 +0200 Subject: [PATCH 04/33] last minor changes --- Cargo.lock | 10 +++ crates/precompiles/src/erc20.rs | 42 ---------- crates/sandbox/src/client.rs | 10 --- crates/sandbox/src/lib.rs | 26 +------ .../public/assets-precompile/lib.rs | 76 +++++++++++-------- 5 files changed, 54 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 523d5d7370d..c3758c305e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4841,6 +4841,7 @@ dependencies = [ "polkavm", "polkavm-common 0.29.0", "rand 0.8.5", + "rand_pcg", "revm", "ripemd", "rlp 0.6.1", @@ -5665,6 +5666,15 @@ dependencies = [ "serde", ] +[[package]] +name = "rand_pcg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" +dependencies = [ + "rand_core 0.6.4", +] + [[package]] name = "rand_xorshift" version = "0.4.0" diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs index 3717c4b6bab..a3c87868555 100644 --- a/crates/precompiles/src/erc20.rs +++ b/crates/precompiles/src/erc20.rs @@ -17,48 +17,6 @@ //! This module provides the standard ERC-20 token interface for interacting with //! assets managed by `pallet-assets` through the precompile mechanism. //! -//! # Overview -//! -//! The ERC-20 Assets precompile allows smart contracts to interact with fungible assets -//! from `pallet-assets` using the ERC-20 interface. Each asset gets its own -//! precompile address, calculated by encoding the asset ID in the address. -//! -//! # Precompile Address -//! -//! - **Index**: `0x0120` -//! - **Address Format**: `[asset_id][...zeros...][0x0120]0000` -//! -//! Use [`crate::prefixed_address`] to calculate the correct address for a specific asset. -//! -//! # Example -//! -//! ```rust,ignore -//! use ink_precompiles::{erc20::{Erc20Ref, PRECOMPILE_INDEX}, prefixed_address}; -//! -//! #[ink::contract] -//! mod my_contract { -//! use super::*; -//! -//! #[ink(storage)] -//! pub struct MyContract { -//! asset_id: u32, -//! } -//! -//! impl MyContract { -//! #[ink(constructor)] -//! pub fn new(asset_id: u32) -> Self { -//! Self { asset_id } -//! } -//! -//! pub fn get_balance(&self, account: ink::Address) -> ink::U256 { -//! let precompile_addr = prefixed_address(PRECOMPILE_INDEX, self.asset_id); -//! let erc20: Erc20Ref = precompile_addr.into(); -//! erc20.balanceOf(account) -//! } -//! } -//! } -//! ``` -//! //! # References //! //! - [Polkadot SDK Assets Precompile](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/assets/src/precompiles.rs) diff --git a/crates/sandbox/src/client.rs b/crates/sandbox/src/client.rs index 2e8a49e6a45..19e879165d0 100644 --- a/crates/sandbox/src/client.rs +++ b/crates/sandbox/src/client.rs @@ -725,16 +725,6 @@ where ::RuntimeEvent: TryInto>, { - /// Returns true if any events were emitted during the last operation. - pub fn has_events(&mut self) -> bool { - !self.sandbox.events().is_empty() - } - - /// Returns the last event that was emitted, if any. - pub fn last_event(&mut self) -> Option> { - self.sandbox.events().last().cloned() - } - pub fn contract_events(&mut self) -> Vec> where S::Runtime: pallet_revive::Config, diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index d0d805a434d..e507f4208c0 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -75,33 +75,9 @@ pub use ink_e2e_macro::test; /// This macro verifies that the last emitted contract event from the sandbox /// matches the provided expected event. /// -/// # Example -/// -/// ```no_run -/// use ink_sandbox::{ -/// DefaultSandbox, -/// SandboxClient, -/// assert_last_contract_event, -/// }; -/// -/// # async fn test() { -/// let mut client: SandboxClient<_, DefaultSandbox> = todo!(); -/// -/// // After a contract call that emits an event... -/// assert_last_contract_event!( -/// &mut client, -/// Transfer { -/// from: alice_address, -/// to: bob_address, -/// value: 100u128, -/// } -/// ); -/// # } -/// ``` -/// /// # Parameters /// - `client` - Mutable reference to the sandbox client -/// - `event` - The expected event (must implement `Encode`, `Decode`, and `Debug`) +/// - `event` - The expected event #[macro_export] macro_rules! assert_last_contract_event { ($client:expr, $event:expr $(,)?) => { diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index ef984dd0973..9ee2905fa05 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -1,21 +1,31 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] -use ink::prelude::string::ToString; -use ink::{H160, U256}; -pub use ink_precompiles::erc20::{AssetId, erc20}; +use ink::{ + H160, + U256, + prelude::string::ToString, +}; +pub use ink_precompiles::erc20::{ + AssetId, + erc20, +}; #[ink::contract] mod asset_hub_precompile { use super::*; use ink::prelude::string::String; - use ink_precompiles::erc20::{Erc20, Erc20Ref}; + use ink_precompiles::erc20::{ + Erc20, + Erc20Ref, + }; #[ink(storage)] pub struct AssetHubPrecompile { asset_id: AssetId, - /// The owner of this contract. Only the owner can call transfer, approve, and transfer_from. - /// This is necessary because the contract holds tokens and without access control, - /// anyone could transfer tokens that the contract holds, which would be a security issue. + /// The owner of this contract. Only the owner can call transfer, approve, and + /// transfer_from. This is necessary because the contract holds tokens + /// and without access control, anyone could transfer tokens that the + /// contract holds, which would be a security issue. owner: H160, precompile: Erc20Ref, } @@ -167,11 +177,23 @@ mod tests { #[cfg(all(test, feature = "e2e-tests"))] mod e2e_tests { use super::*; - use crate::asset_hub_precompile::{Transfer, Approval, AssetHubPrecompile, AssetHubPrecompileRef}; + use crate::asset_hub_precompile::{ + Approval, + AssetHubPrecompile, + AssetHubPrecompileRef, + Transfer, + }; use ink_e2e::ContractsBackend; use ink_sandbox::{ - Sandbox, assert_last_contract_event, DefaultSandbox, SandboxClient, account_id_from_contract, - api::prelude::{AssetsAPI, ContractAPI}, + DefaultSandbox, + Sandbox, + SandboxClient, + account_id_from_contract, + api::prelude::{ + AssetsAPI, + ContractAPI, + }, + assert_last_contract_event, }; type E2EResult = std::result::Result>; @@ -191,8 +213,7 @@ mod e2e_tests { .await .expect("instantiate failed"); - let call_builder = - contract.call_builder::(); + let call_builder = contract.call_builder::(); let asset_id_call = call_builder.asset_id(); let result = client .call(&ink_e2e::alice(), &asset_id_call) @@ -278,8 +299,7 @@ mod e2e_tests { // Map bob's account so the precompile can find his assets. // (alice is already mapped during instantiate) - let bob_origin = - DefaultSandbox::convert_account_to_origin(bob.clone()); + let bob_origin = DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() .map_account(bob_origin) @@ -332,16 +352,14 @@ mod e2e_tests { let transfer_amount = U256::from(1_000); // Get contract's AccountId32 - the precompile will see the contract as caller! - let contract_account = - account_id_from_contract(&contract.account_id); + let contract_account = account_id_from_contract(&contract.account_id); let contract_address = ink_e2e::address_from_account_id(&contract_account); client .sandbox() .mint_into(&asset_id, &contract_account, 100_000u128) .expect("Failed to mint to contract"); - let bob_origin = - DefaultSandbox::convert_account_to_origin(bob.clone()); + let bob_origin = DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() .map_account(bob_origin) @@ -398,8 +416,7 @@ mod e2e_tests { let mut call_builder = contract.call_builder::(); - let contract_account = - account_id_from_contract(&contract.account_id); + let contract_account = account_id_from_contract(&contract.account_id); let contract_address = ink_e2e::address_from_account_id(&contract_account); client .sandbox() @@ -409,8 +426,7 @@ mod e2e_tests { let bob_address = ink_e2e::address_from_account_id(&bob); let approve_amount = U256::from(200); - let bob_origin = - DefaultSandbox::convert_account_to_origin(bob.clone()); + let bob_origin = DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() .map_account(bob_origin) @@ -473,8 +489,7 @@ mod e2e_tests { let alice_address = ink_e2e::address_from_account_id(&alice); let bob_address = ink_e2e::address_from_account_id(&bob); - let bob_origin = - DefaultSandbox::convert_account_to_origin(bob.clone()); + let bob_origin = DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() .map_account(bob_origin) @@ -532,8 +547,7 @@ mod e2e_tests { let mut call_builder = contract.call_builder::(); - let contract_account = - account_id_from_contract(&contract.account_id); + let contract_account = account_id_from_contract(&contract.account_id); client .sandbox() .mint_into(&asset_id, &alice, 100_000u128) @@ -549,8 +563,7 @@ mod e2e_tests { let bob_address = ink_e2e::address_from_account_id(&bob); let transfer_amount = U256::from(1_500); - let bob_origin = - DefaultSandbox::convert_account_to_origin(bob.clone()); + let bob_origin = DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() .map_account(bob_origin) @@ -585,11 +598,8 @@ mod e2e_tests { assert_eq!(contract_allowance, 48_500u128); // Show error case with transferring more tokens than approved. - let transfer_from = call_builder.transfer_from( - alice_address, - bob_address, - U256::from(1_000_000), - ); + let transfer_from = + call_builder.transfer_from(alice_address, bob_address, U256::from(1_000_000)); let result = client .call(&ink_e2e::alice(), &transfer_from) .submit() From 62deaf4f4628485c750e9ca1af11bbadad683615 Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Fri, 24 Oct 2025 12:45:36 +0200 Subject: [PATCH 05/33] fix: compilation error for custom sandbox usage --- crates/sandbox/src/lib.rs | 1 + crates/sandbox/src/macros.rs | 2 +- integration-tests/public/runtime-call-contract/Cargo.toml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index e507f4208c0..cafec122aa8 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -56,6 +56,7 @@ pub use { pallet_timestamp, pallet_transaction_payment, paste, + scale, sp_core::crypto::Ss58Codec, sp_externalities::{ self, diff --git a/crates/sandbox/src/macros.rs b/crates/sandbox/src/macros.rs index 7b6b255f773..51fc1bc7c7e 100644 --- a/crates/sandbox/src/macros.rs +++ b/crates/sandbox/src/macros.rs @@ -185,7 +185,7 @@ mod construct_runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; type AssetId = AssetIdForTrustBackedAssets; - type AssetIdParameter = scale::Compact; + type AssetIdParameter = $crate::scale::Compact; type Currency = Balances; type CreateOrigin = $crate::frame_support::traits::AsEnsureOriginWithArg<$crate::frame_system::EnsureSigned>; type ForceOrigin = $crate::frame_system::EnsureRoot; diff --git a/integration-tests/public/runtime-call-contract/Cargo.toml b/integration-tests/public/runtime-call-contract/Cargo.toml index a355c0ac849..08616179bb0 100644 --- a/integration-tests/public/runtime-call-contract/Cargo.toml +++ b/integration-tests/public/runtime-call-contract/Cargo.toml @@ -51,6 +51,7 @@ std = [ "flipper-traits/std", ] ink-as-dependency = [] +e2e-tests = [] [package.metadata.ink-lang] abi = "ink" From 11c568706930b76d793ebc52340fe5f7dc575901 Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Fri, 24 Oct 2025 13:31:18 +0200 Subject: [PATCH 06/33] chore: Cargo.toml file edit precompile crate --- crates/precompiles/Cargo.toml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/precompiles/Cargo.toml b/crates/precompiles/Cargo.toml index 4deb1c1e635..5f9b5f83470 100644 --- a/crates/precompiles/Cargo.toml +++ b/crates/precompiles/Cargo.toml @@ -3,16 +3,12 @@ name = "ink_precompiles" version.workspace = true authors = ["Use Ink "] edition.workspace = true - license.workspace = true -readme = "README.md" +description = "[ink!] Precompile interfaces for pallet-revive smart contracts." repository.workspace = true -documentation = "https://docs.rs/ink_precompiles/" homepage.workspace = true -description = "[ink!] Precompile interfaces for pallet-revive smart contracts." keywords.workspace = true categories.workspace = true -include = ["/Cargo.toml", "src/**/*.rs", "/README.md", "/LICENSE"] [dependencies] ink = { workspace = true, default-features = false } From 615b111e212376b85dc5c244e7fb1d99b18cd859 Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Fri, 24 Oct 2025 14:01:03 +0200 Subject: [PATCH 07/33] test: assets apis tests --- crates/precompiles/src/lib.rs | 25 ---- crates/sandbox/src/api/assets_api.rs | 181 +++++++++++++++++++++++---- 2 files changed, 157 insertions(+), 49 deletions(-) diff --git a/crates/precompiles/src/lib.rs b/crates/precompiles/src/lib.rs index d325fbc240f..dc9af353829 100644 --- a/crates/precompiles/src/lib.rs +++ b/crates/precompiles/src/lib.rs @@ -85,29 +85,4 @@ mod tests { let address_bytes: [u8; 20] = result.into(); assert_eq!(address_bytes, expected); } - - #[test] - fn system_precompile_address() { - // System precompile is at index 0x0009 - let expected = [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, - ]; - let result = fixed_address(0x0009); - let address_bytes: [u8; 20] = result.into(); - assert_eq!(address_bytes, expected); - } - - #[test] - fn erc20_assets_precompile_address() { - // ERC20 Assets precompile is at index 0x0120 with asset ID 1 - // Index 0x0120 shifted left 16 bits = 0x01200000 in bytes 16-19 - let expected = [ - 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, - ]; - let result = prefixed_address(0x0120, 1); - let address_bytes: [u8; 20] = result.into(); - assert_eq!(address_bytes, expected); - } } diff --git a/crates/sandbox/src/api/assets_api.rs b/crates/sandbox/src/api/assets_api.rs index 3fc79fb99c2..b8b5be70a5d 100644 --- a/crates/sandbox/src/api/assets_api.rs +++ b/crates/sandbox/src/api/assets_api.rs @@ -6,14 +6,16 @@ use frame_support::{ pallet_prelude::DispatchError, traits::fungibles::{ Create, - Destroy, Inspect, Mutate, approvals::{ Inspect as _, Mutate as _, }, - metadata::Mutate as _, + metadata::{ + Inspect as MetadataInspect, + Mutate as _, + }, }, }; @@ -43,19 +45,11 @@ where min_balance: AssetBalanceOf, ) -> Result<(), DispatchError>; - /// Start the destruction an existing fungible asset. - /// - /// # Arguments - /// * `asset` - ID of the asset. - fn start_destroy( - &mut self, - asset: &AssetIdOf, - ) -> Result<(), DispatchError>; - - /// Start the destruction an existing fungible asset. + /// Sets the metadata for an existing fungible asset. /// /// # Arguments /// * `asset` - ID of the asset. + /// * `owner` - The owner of the asset. /// * `name` - Token name. /// * `symbol` - Token symbol. /// * `decimals` - Token decimals. @@ -68,6 +62,15 @@ where decimals: u8, ) -> Result<(), DispatchError>; + /// Returns the metadata for an asset. + /// + /// # Arguments + /// * `asset` - ID of the asset. + /// + /// # Returns + /// A tuple of (name, symbol, decimals). + fn metadata(&mut self, asset: &AssetIdOf) -> (Vec, Vec, u8); + /// Approves `spender` to spend `value` amount of tokens on behalf of the caller. /// /// Successive calls of this method overwrite previous values. @@ -171,17 +174,6 @@ where }) } - fn start_destroy( - &mut self, - asset: &AssetIdOf, - ) -> Result<(), DispatchError> { - self.execute_with(|| { - as Destroy< - AccountIdFor, - >>::start_destroy(asset.clone(), None) - }) - } - fn set_metadata( &mut self, asset: &AssetIdOf, @@ -201,6 +193,21 @@ where }) } + fn metadata(&mut self, asset: &AssetIdOf) -> (Vec, Vec, u8) { + self.execute_with(|| { + let name = as MetadataInspect< + AccountIdFor, + >>::name(asset.clone()); + let symbol = as MetadataInspect< + AccountIdFor, + >>::symbol(asset.clone()); + let decimals = as MetadataInspect< + AccountIdFor, + >>::decimals(asset.clone()); + (name, symbol, decimals) + }) + } + fn mint_into( &mut self, asset: &AssetIdOf, @@ -311,7 +318,69 @@ mod tests { } #[test] - fn mint_asset_works() { + fn set_metadata_works() { + let mut sandbox = DefaultSandbox::default(); + let admin = DefaultSandbox::default_actor(); + let asset_id = 1u32; + + sandbox.create(&asset_id, &admin, 1u128).unwrap(); + + let name = b"Test Token".to_vec(); + let symbol = b"TEST".to_vec(); + let decimals = 18u8; + + let result = sandbox.set_metadata(&asset_id, &admin, name, symbol, decimals); + + assert!(result.is_ok()); + } + + #[test] + fn metadata_works() { + let mut sandbox = DefaultSandbox::default(); + let admin = DefaultSandbox::default_actor(); + let asset_id = 1u32; + + sandbox.create(&asset_id, &admin, 1u128).unwrap(); + + let name = b"Test Token".to_vec(); + let symbol = b"TEST".to_vec(); + let decimals = 18u8; + + sandbox + .set_metadata(&asset_id, &admin, name.clone(), symbol.clone(), decimals) + .unwrap(); + + let (retrieved_name, retrieved_symbol, retrieved_decimals) = + sandbox.metadata(&asset_id); + + assert_eq!(retrieved_name, name); + assert_eq!(retrieved_symbol, symbol); + assert_eq!(retrieved_decimals, decimals); + } + + #[test] + fn approve_works() { + let mut sandbox = DefaultSandbox::default(); + let admin = DefaultSandbox::default_actor(); + let spender = crate::bob(); + let asset_id = 1u32; + + sandbox.create(&asset_id, &admin, 1u128).unwrap(); + sandbox.mint_into(&asset_id, &admin, 1000u128).unwrap(); + + let allowance_before = sandbox.allowance(&asset_id, &admin, &spender); + assert_eq!(allowance_before, 0); + + let result = sandbox.approve(&asset_id, &admin, &spender, 500u128); + + assert!(result.is_ok()); + + let allowance_after = sandbox.allowance(&asset_id, &admin, &spender); + assert_eq!(allowance_after, 500); + } + + #[test] + fn mint_into_works() { let mut sandbox = DefaultSandbox::default(); let admin = DefaultSandbox::default_actor(); let asset_id = 1u32; @@ -327,6 +396,50 @@ mod tests { assert_eq!(balance_after, 100); } + #[test] + fn transfer_works() { + let mut sandbox = DefaultSandbox::default(); + let admin = DefaultSandbox::default_actor(); + let recipient = crate::bob(); + let asset_id = 1u32; + + sandbox.create(&asset_id, &admin, 1u128).unwrap(); + sandbox.mint_into(&asset_id, &admin, 1000u128).unwrap(); + + let admin_balance_before = sandbox.balance_of(&asset_id, &admin); + let recipient_balance_before = sandbox.balance_of(&asset_id, &recipient); + + assert_eq!(admin_balance_before, 1000); + assert_eq!(recipient_balance_before, 0); + + let result = sandbox.transfer(&asset_id, &admin, &recipient, 300u128); + + assert!(result.is_ok()); + + let admin_balance_after = sandbox.balance_of(&asset_id, &admin); + let recipient_balance_after = sandbox.balance_of(&asset_id, &recipient); + + assert_eq!(admin_balance_after, 700); + assert_eq!(recipient_balance_after, 300); + } + + #[test] + fn balance_of_works() { + let mut sandbox = DefaultSandbox::default(); + let admin = DefaultSandbox::default_actor(); + let asset_id = 1u32; + + sandbox.create(&asset_id, &admin, 1u128).unwrap(); + + let balance = sandbox.balance_of(&asset_id, &admin); + assert_eq!(balance, 0); + + sandbox.mint_into(&asset_id, &admin, 500u128).unwrap(); + + let balance = sandbox.balance_of(&asset_id, &admin); + assert_eq!(balance, 500); + } + #[test] fn total_supply_works() { let mut sandbox = DefaultSandbox::default(); @@ -344,6 +457,26 @@ mod tests { assert_eq!(supply_after, 1000); } + #[test] + fn allowance_works() { + let mut sandbox = DefaultSandbox::default(); + let admin = DefaultSandbox::default_actor(); + let spender = crate::bob(); + let asset_id = 1u32; + + sandbox.create(&asset_id, &admin, 1u128).unwrap(); + + let allowance = sandbox.allowance(&asset_id, &admin, &spender); + assert_eq!(allowance, 0); + + sandbox + .approve(&asset_id, &admin, &spender, 250u128) + .unwrap(); + + let allowance = sandbox.allowance(&asset_id, &admin, &spender); + assert_eq!(allowance, 250); + } + #[test] fn asset_exists_works() { let mut sandbox = DefaultSandbox::default(); From cbd8c668209c0ea310c4685ec5ec25f7afe638ff Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Fri, 24 Oct 2025 18:33:35 +0200 Subject: [PATCH 08/33] refactor: account handling e2e framework --- crates/e2e/src/lib.rs | 34 ++-- crates/sandbox/src/api/assets_api.rs | 98 ++++++----- crates/sandbox/src/api/revive_api.rs | 23 ++- crates/sandbox/src/client.rs | 7 +- crates/sandbox/src/lib.rs | 72 ++++++-- .../public/assets-precompile/lib.rs | 155 ++++++++---------- 6 files changed, 214 insertions(+), 175 deletions(-) diff --git a/crates/e2e/src/lib.rs b/crates/e2e/src/lib.rs index 534ef5ca2e5..f575b3c8e8b 100644 --- a/crates/e2e/src/lib.rs +++ b/crates/e2e/src/lib.rs @@ -153,27 +153,23 @@ pub fn address(account: Sr25519Keyring) -> Address { AccountIdMapper::to_address(account.to_account_id().as_ref()) } -/// Returns the [`ink::Address`] for a given account id. -/// -/// # Developer Note -/// -/// We take the `AccountId` and return only the first twenty bytes, this -/// is what `pallet-revive` does as well. -pub fn address_from_account_id>(account_id: AccountId) -> Address { - AccountIdMapper::to_address(account_id.as_ref()) +/// Extension trait for converting various types to Address (H160). +pub trait IntoAddress { + /// Convert to an Address (H160). + fn address(&self) -> Address; } -/// Returns the [`ink::Address`] for a given `Keypair`. -/// -/// # Developer Note -/// -/// We take the `AccountId` and return only the first twenty bytes, this -/// is what `pallet-revive` does as well. -pub fn address_from_keypair + AsRef<[u8]>>( - keypair: &Keypair, -) -> Address { - let account_id: AccountId = keypair_to_account(keypair); - address_from_account_id(account_id) +impl IntoAddress for Keypair { + fn address(&self) -> Address { + AccountIdMapper::to_address(&self.public_key().0) + } +} + +impl IntoAddress for ink_primitives::AccountId { + fn address(&self) -> Address { + let bytes = *AsRef::<[u8; 32]>::as_ref(self); + AccountIdMapper::to_address(&bytes) + } } /// Transforms a `Keypair` into an account id. diff --git a/crates/sandbox/src/api/assets_api.rs b/crates/sandbox/src/api/assets_api.rs index b8b5be70a5d..1a199a5911a 100644 --- a/crates/sandbox/src/api/assets_api.rs +++ b/crates/sandbox/src/api/assets_api.rs @@ -1,5 +1,6 @@ use crate::{ AccountIdFor, + IntoAccountId, Sandbox, }; use frame_support::{ @@ -36,12 +37,12 @@ where /// /// # Arguments /// * `id` - ID of the new asset to be created. - /// * `owner` - The owner of the created asset. + /// * `owner` - The owner of the created asset (accepts any type convertible to AccountId). /// * `min_balance` - The asset amount one account need at least. fn create( &mut self, id: &AssetIdOf, - owner: &AccountIdFor, + owner: impl IntoAccountId>, min_balance: AssetBalanceOf, ) -> Result<(), DispatchError>; @@ -49,14 +50,14 @@ where /// /// # Arguments /// * `asset` - ID of the asset. - /// * `owner` - The owner of the asset. + /// * `owner` - The owner of the asset (accepts any type convertible to AccountId). /// * `name` - Token name. /// * `symbol` - Token symbol. /// * `decimals` - Token decimals. fn set_metadata( &mut self, asset: &AssetIdOf, - owner: &AccountIdFor, + owner: impl IntoAccountId>, name: Vec, symbol: Vec, decimals: u8, @@ -77,13 +78,14 @@ where /// /// # Arguments /// * `asset` - ID of the asset. - /// * `spender` - The account that is allowed to spend the tokens. - /// * `value` - The number of tokens to approve. + /// * `owner` - The account that owns the tokens (accepts any type convertible to AccountId). + /// * `delegate` - The account that is allowed to spend the tokens (accepts any type convertible to AccountId). + /// * `amount` - The number of tokens to approve. fn approve( &mut self, asset: &AssetIdOf, - owner: &AccountIdFor, - delegate: &AccountIdFor, + owner: impl IntoAccountId>, + delegate: impl IntoAccountId>, amount: AssetBalanceOf, ) -> Result<(), DispatchError>; @@ -92,12 +94,12 @@ where /// /// # Arguments /// * `asset` - ID of the asset. - /// * `account` - The account to be credited with the created tokens. + /// * `account` - The account to be credited with the created tokens (accepts any type convertible to AccountId). /// * `value` - The number of tokens to mint. fn mint_into( &mut self, asset: &AssetIdOf, - account: &AccountIdFor, + account: impl IntoAccountId>, value: AssetBalanceOf, ) -> Result, DispatchError>; @@ -105,25 +107,25 @@ where /// /// # Arguments /// * `asset` - ID of the asset. - /// * `source` - The account from which tokens are transferred. - /// * `dest` - The account to which tokens are transferred. + /// * `source` - The account from which tokens are transferred (accepts any type convertible to AccountId). + /// * `dest` - The account to which tokens are transferred (accepts any type convertible to AccountId). /// * `amount` - The number of tokens to transfer. fn transfer( &mut self, asset: &AssetIdOf, - source: &AccountIdFor, - dest: &AccountIdFor, + source: impl IntoAccountId>, + dest: impl IntoAccountId>, amount: AssetBalanceOf, ) -> Result<(), DispatchError>; /// Returns the account balance for the specified `owner`. /// /// # Arguments - /// * `owner` - The account whose balance is being queried. + /// * `owner` - The account whose balance is being queried (accepts any type convertible to AccountId). fn balance_of( &mut self, asset: &AssetIdOf, - owner: &AccountIdFor, + owner: impl IntoAccountId>, ) -> AssetBalanceOf; /// Returns the total supply of the `asset`. @@ -139,13 +141,13 @@ where /// /// # Arguments /// * `asset` - ID of the asset. - /// * `owner` - The account that owns the tokens. - /// * `spender` - The account that is allowed to spend the tokens. + /// * `owner` - The account that owns the tokens (accepts any type convertible to AccountId). + /// * `delegate` - The account that is allowed to spend the tokens (accepts any type convertible to AccountId). fn allowance( &mut self, asset: &AssetIdOf, - owner: &AccountIdFor, - delegate: &AccountIdFor, + owner: impl IntoAccountId>, + delegate: impl IntoAccountId>, ) -> AssetBalanceOf; /// Check if the asset exists. @@ -164,28 +166,30 @@ where fn create( &mut self, id: &AssetIdOf, - owner: &AccountIdFor, + owner: impl IntoAccountId>, min_balance: AssetBalanceOf, ) -> Result<(), DispatchError> { + let owner = owner.into_account_id(); self.execute_with(|| { as Create< AccountIdFor, - >>::create(id.clone(), owner.clone(), true, min_balance) + >>::create(id.clone(), owner, true, min_balance) }) } fn set_metadata( &mut self, asset: &AssetIdOf, - owner: &AccountIdFor, + owner: impl IntoAccountId>, name: Vec, symbol: Vec, decimals: u8, ) -> Result<(), DispatchError> { + let owner = owner.into_account_id(); self.execute_with(|| { pallet_assets::Pallet::::set( asset.clone().into(), - owner, + &owner, name, symbol, decimals, @@ -211,13 +215,14 @@ where fn mint_into( &mut self, asset: &AssetIdOf, - account: &AccountIdFor, + account: impl IntoAccountId>, value: AssetBalanceOf, ) -> Result, DispatchError> { + let account = account.into_account_id(); self.execute_with(|| { pallet_assets::Pallet::::mint_into( asset.clone(), - account, + &account, value, ) }) @@ -226,15 +231,17 @@ where fn transfer( &mut self, asset: &AssetIdOf, - source: &AccountIdFor, - dest: &AccountIdFor, + source: impl IntoAccountId>, + dest: impl IntoAccountId>, amount: AssetBalanceOf, ) -> Result<(), DispatchError> { + let source = source.into_account_id(); + let dest = dest.into_account_id(); self.execute_with(|| { as Mutate>>::transfer( asset.clone(), - source, - dest, + &source, + &dest, amount, frame_support::traits::tokens::Preservation::Preserve, ).map(|_| ()) @@ -244,15 +251,17 @@ where fn approve( &mut self, asset: &AssetIdOf, - owner: &AccountIdFor, - delegate: &AccountIdFor, + owner: impl IntoAccountId>, + delegate: impl IntoAccountId>, amount: AssetBalanceOf, ) -> Result<(), DispatchError> { + let owner = owner.into_account_id(); + let delegate = delegate.into_account_id(); self.execute_with(|| { pallet_assets::Pallet::::approve( asset.clone(), - owner, - delegate, + &owner, + &delegate, amount, ) }) @@ -261,10 +270,11 @@ where fn balance_of( &mut self, asset: &AssetIdOf, - owner: &AccountIdFor, + owner: impl IntoAccountId>, ) -> AssetBalanceOf { + let owner = owner.into_account_id(); self.execute_with(|| { - pallet_assets::Pallet::::balance(asset.clone(), owner) + pallet_assets::Pallet::::balance(asset.clone(), &owner) }) } @@ -280,14 +290,16 @@ where fn allowance( &mut self, asset: &AssetIdOf, - owner: &AccountIdFor, - delegate: &AccountIdFor, + owner: impl IntoAccountId>, + delegate: impl IntoAccountId>, ) -> AssetBalanceOf { + let owner = owner.into_account_id(); + let delegate = delegate.into_account_id(); self.execute_with(|| { pallet_assets::Pallet::::allowance( asset.clone(), - owner, - delegate, + &owner, + &delegate, ) }) } @@ -362,7 +374,7 @@ mod tests { fn approve_works() { let mut sandbox = DefaultSandbox::default(); let admin = DefaultSandbox::default_actor(); - let spender = crate::bob(); + let spender = ink_e2e::bob().into_account_id(); let asset_id = 1u32; sandbox.create(&asset_id, &admin, 1u128).unwrap(); @@ -400,7 +412,7 @@ mod tests { fn transfer_works() { let mut sandbox = DefaultSandbox::default(); let admin = DefaultSandbox::default_actor(); - let recipient = crate::bob(); + let recipient = ink_e2e::bob().into_account_id(); let asset_id = 1u32; sandbox.create(&asset_id, &admin, 1u128).unwrap(); @@ -461,7 +473,7 @@ mod tests { fn allowance_works() { let mut sandbox = DefaultSandbox::default(); let admin = DefaultSandbox::default_actor(); - let spender = crate::bob(); + let spender = ink_e2e::bob().into_account_id(); let asset_id = 1u32; sandbox.create(&asset_id, &admin, 1u128).unwrap(); diff --git a/crates/sandbox/src/api/revive_api.rs b/crates/sandbox/src/api/revive_api.rs index 8e84b86143d..bcfcdb6be7d 100644 --- a/crates/sandbox/src/api/revive_api.rs +++ b/crates/sandbox/src/api/revive_api.rs @@ -55,7 +55,10 @@ pub trait ContractAPI { /// * `gas_limit` - The gas limit for the contract call. /// * `storage_deposit_limit` - The storage deposit limit for the contract call. #[allow(clippy::type_complexity, clippy::too_many_arguments)] - fn map_account(&mut self, account: OriginFor) -> Result<(), DispatchError>; + fn map_account( + &mut self, + account: impl crate::IntoAccountId>, + ) -> Result<(), DispatchError>; /// `pallet-revive` uses a dedicated "pallet" account for tracking /// storage deposits. The static account is returned by the @@ -166,9 +169,11 @@ where fn map_account( &mut self, - account_id: OriginFor, + account: impl crate::IntoAccountId>, ) -> Result<(), DispatchError> { - self.execute_with(|| pallet_revive::Pallet::::map_account(account_id)) + let account_id = account.into_account_id(); + let origin = Self::convert_account_to_origin(account_id); + self.execute_with(|| pallet_revive::Pallet::::map_account(origin)) } /// `pallet-revive` uses a dedicated "pallet" account for tracking @@ -374,9 +379,9 @@ mod tests { warm_up::(&mut sandbox); - let origin = - DefaultSandbox::convert_account_to_origin(DefaultSandbox::default_actor()); - sandbox.map_account(origin.clone()).expect("cannot map"); + let default_actor = DefaultSandbox::default_actor(); + sandbox.map_account(default_actor.clone()).expect("cannot map"); + let origin = DefaultSandbox::convert_account_to_origin(default_actor); let result = sandbox.deploy_contract( contract_binary.clone(), 0, @@ -411,9 +416,9 @@ mod tests { let contract_binary = compile_module("dummy"); warm_up::(&mut sandbox); - let origin = - DefaultSandbox::convert_account_to_origin(DefaultSandbox::default_actor()); - sandbox.map_account(origin.clone()).expect("unable to map"); + let default_actor = DefaultSandbox::default_actor(); + sandbox.map_account(default_actor.clone()).expect("unable to map"); + let origin = DefaultSandbox::convert_account_to_origin(default_actor); let result = sandbox.deploy_contract( contract_binary, 0, diff --git a/crates/sandbox/src/client.rs b/crates/sandbox/src/client.rs index 19e879165d0..d783ef33d6a 100644 --- a/crates/sandbox/src/client.rs +++ b/crates/sandbox/src/client.rs @@ -699,12 +699,11 @@ where &mut self, caller: &Keypair, ) -> Result, Self::Error> { - let caller = keypair_to_account(caller); - let origin = RawOrigin::Signed(caller); - let origin = OriginFor::::from(origin); + let caller_account: AccountIdFor = keypair_to_account(caller); + let origin = S::convert_account_to_origin(caller_account); self.sandbox - .map_account(origin) + .execute_with(|| pallet_revive::Pallet::::map_account(origin)) .map_err(|err| { SandboxErr::new(format!("map_account: execution error {err:?}")) }) diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index cafec122aa8..3c93ca68589 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -269,25 +269,67 @@ pub fn to_revive_storage_deposit( } } -/// Returns Alice's `AccountId32` for testing. -pub fn alice() -> AccountId32 { - AccountId32::from(ink_e2e::alice().public_key().0) +/// Trait for types that can be converted into a runtime AccountId. +/// +/// This allows sandbox APIs to accept various account types (ink! `AccountId`, +/// `Keypair`, `AccountId32`, raw bytes) without requiring manual conversion. +/// +pub trait IntoAccountId { + fn into_account_id(self) -> AccountId; } -/// Returns Bob's `AccountId32` for testing. -pub fn bob() -> AccountId32 { - AccountId32::from(ink_e2e::bob().public_key().0) +// Identity conversion for AccountId32 +impl IntoAccountId for AccountId32 { + fn into_account_id(self) -> AccountId32 { + self + } } -/// Returns Charlie's `AccountId32` for testing. -pub fn charlie() -> AccountId32 { - AccountId32::from(ink_e2e::charlie().public_key().0) +// Borrowed AccountId32 +impl IntoAccountId for &AccountId32 { + fn into_account_id(self) -> AccountId32 { + self.clone() + } } -/// Creates an `AccountId32` from an ink contract account ID. -/// -/// This is a convenience function for e2e tests that need to convert contract -/// account IDs to the `AccountId32` type used by the sandbox runtime. -pub fn account_id_from_contract(account_id: &ink_primitives::AccountId) -> AccountId32 { - AccountId32::from(*AsRef::<[u8; 32]>::as_ref(account_id)) +// ink!'s AccountId +impl IntoAccountId for ink_primitives::AccountId { + fn into_account_id(self) -> AccountId32 { + AccountId32::from(*AsRef::<[u8; 32]>::as_ref(&self)) + } +} + +// Borrowed ink! AccountId +impl IntoAccountId for &ink_primitives::AccountId { + fn into_account_id(self) -> AccountId32 { + AccountId32::from(*AsRef::<[u8; 32]>::as_ref(self)) + } +} + +// Keypair from e2e tests +impl IntoAccountId for ink_e2e::Keypair { + fn into_account_id(self) -> AccountId32 { + AccountId32::from(self.public_key().0) + } +} + +// Borrowed Keypair from e2e tests +impl IntoAccountId for &ink_e2e::Keypair { + fn into_account_id(self) -> AccountId32 { + AccountId32::from(self.public_key().0) + } } + +// Raw bytes [u8; 32] +impl IntoAccountId for [u8; 32] { + fn into_account_id(self) -> AccountId32 { + AccountId32::from(self) + } +} + +// Borrowed raw bytes +impl IntoAccountId for &[u8; 32] { + fn into_account_id(self) -> AccountId32 { + AccountId32::from(*self) + } +} \ No newline at end of file diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index 9ee2905fa05..e919544a875 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -183,12 +183,14 @@ mod e2e_tests { AssetHubPrecompileRef, Transfer, }; - use ink_e2e::ContractsBackend; + use ink_e2e::{ + ContractsBackend, + IntoAddress, + alice, bob, + }; use ink_sandbox::{ DefaultSandbox, - Sandbox, SandboxClient, - account_id_from_contract, api::prelude::{ AssetsAPI, ContractAPI, @@ -207,7 +209,7 @@ mod e2e_tests { let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .instantiate("assets_precompile", &alice(), &mut constructor) .value(1_000_000_000_000u128) // Transfer native tokens to contract .submit() .await @@ -216,7 +218,7 @@ mod e2e_tests { let call_builder = contract.call_builder::(); let asset_id_call = call_builder.asset_id(); let result = client - .call(&ink_e2e::alice(), &asset_id_call) + .call(&alice(), &asset_id_call) .dry_run() .await?; @@ -231,7 +233,7 @@ mod e2e_tests { )))] async fn total_supply_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; - let admin = ink_sandbox::alice(); + let admin = alice(); client .sandbox() @@ -242,18 +244,18 @@ mod e2e_tests { .mint_into(&asset_id, &admin, 1000u128) .expect("Failed to mint asset"); - let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .instantiate("assets_precompile", &admin, &mut constructor) .submit() .await .expect("instantiate failed"); let call_builder = - contract.call_builder::(); + contract.call_builder::(); let total_supply = call_builder.total_supply(); let result = client - .call(&ink_e2e::alice(), &total_supply) + .call(&admin, &total_supply) .submit() .await?; @@ -269,8 +271,8 @@ mod e2e_tests { )))] async fn balance_of_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; - let alice = ink_sandbox::alice(); - let bob = ink_sandbox::bob(); + let alice = alice(); + let bob = bob(); client .sandbox() @@ -285,36 +287,31 @@ mod e2e_tests { .mint_into(&asset_id, &bob, 500u128) .expect("Failed to mint to bob"); - let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .instantiate("assets_precompile", &alice, &mut constructor) .submit() .await .expect("instantiate failed"); let call_builder = - contract.call_builder::(); + contract.call_builder::(); - let alice_address = ink_e2e::address_from_account_id(&alice); - let bob_address = ink_e2e::address_from_account_id(&bob); - - // Map bob's account so the precompile can find his assets. - // (alice is already mapped during instantiate) - let bob_origin = DefaultSandbox::convert_account_to_origin(bob.clone()); + // Map bob's account otherwise it fails. client .sandbox() - .map_account(bob_origin) + .map_account(&bob) .expect("Failed to map bob's account"); - let alice_balance_call = call_builder.balance_of(alice_address); + let alice_balance_call = call_builder.balance_of(alice.address()); let alice_result = client - .call(&ink_e2e::alice(), &alice_balance_call) + .call(&alice, &alice_balance_call) .dry_run() .await?; let alice_balance = alice_result.return_value(); - let bob_balance_call = call_builder.balance_of(bob_address); + let bob_balance_call = call_builder.balance_of(bob.address()); let bob_result = client - .call(&ink_e2e::alice(), &bob_balance_call) + .call(&alice, &bob_balance_call) .dry_run() .await?; let bob_balance = bob_result.return_value(); @@ -331,61 +328,55 @@ mod e2e_tests { )))] async fn transfer_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; - let alice = ink_sandbox::alice(); - let bob = ink_sandbox::bob(); + let alice = alice(); + let bob = bob(); client .sandbox() .create(&asset_id, &alice, 1u128) .expect("Failed to create asset"); - let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .instantiate("assets_precompile", &alice, &mut constructor) .submit() .await .expect("instantiate failed"); let mut call_builder = - contract.call_builder::(); + contract.call_builder::(); - let bob_address = ink_e2e::address_from_account_id(&bob); + let bob_address = bob.address(); let transfer_amount = U256::from(1_000); - - // Get contract's AccountId32 - the precompile will see the contract as caller! - let contract_account = account_id_from_contract(&contract.account_id); - let contract_address = ink_e2e::address_from_account_id(&contract_account); client .sandbox() - .mint_into(&asset_id, &contract_account, 100_000u128) + .mint_into(&asset_id, &contract.account_id, 100_000u128) .expect("Failed to mint to contract"); - - let bob_origin = DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() - .map_account(bob_origin) + .map_account(&bob) .expect("Failed to map bob's account"); let transfer = call_builder.transfer(bob_address, transfer_amount); - let result = client.call(&ink_e2e::alice(), &transfer).submit().await?; + let result = client.call(&alice, &transfer).submit().await?; let transfer_result = result.return_value(); assert!(transfer_result.is_ok()); assert_last_contract_event!( &mut client, Transfer { - from: contract_address, + from: contract.addr, to: bob_address, value: transfer_amount } ); - let contract_balance = client.sandbox().balance_of(&asset_id, &contract_account); + let contract_balance = client.sandbox().balance_of(&asset_id, &contract.account_id); let bob_balance = client.sandbox().balance_of(&asset_id, &bob); assert_eq!(contract_balance, 99_000u128); // Contract had 100_000, transferred 1_000 assert_eq!(bob_balance, 1_000u128); // Bob received 1_000 // Show error case with transferring too many tokens. let transfer = call_builder.transfer(bob_address, U256::from(1_000_000)); - let result = client.call(&ink_e2e::alice(), &transfer).submit().await?; + let result = client.call(&alice, &transfer).submit().await?; assert_eq!(result.extract_error(), Some("BalanceLow".to_string())); Ok(()) @@ -397,55 +388,51 @@ mod e2e_tests { )))] async fn approve_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; - let alice = ink_sandbox::alice(); - let bob = ink_sandbox::bob(); + let alice = alice(); + let bob = bob(); client .sandbox() .create(&asset_id, &alice, 1u128) .expect("Failed to create asset"); - let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .instantiate("assets_precompile", &alice, &mut constructor) // Contract needs native balance for approval deposit. .value(100_000) .submit() .await .expect("instantiate failed"); let mut call_builder = - contract.call_builder::(); + contract.call_builder::(); - let contract_account = account_id_from_contract(&contract.account_id); - let contract_address = ink_e2e::address_from_account_id(&contract_account); client .sandbox() - .mint_into(&asset_id, &contract_account, 100_000u128) + .mint_into(&asset_id, &contract.account_id, 100_000u128) .expect("Failed to mint to contract"); - - let bob_address = ink_e2e::address_from_account_id(&bob); + let bob_address = bob.address(); let approve_amount = U256::from(200); - let bob_origin = DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() - .map_account(bob_origin) + .map_account(&bob) .expect("Failed to map bob's account"); let bob_allowance_before = client .sandbox() - .allowance(&asset_id, &contract_account, &bob); + .allowance(&asset_id, &contract.account_id, &bob); assert_eq!(bob_allowance_before, 0u128); // Bob's allowance is 0 let approve = call_builder.approve(bob_address, approve_amount); - let result = client.call(&ink_e2e::alice(), &approve).submit().await?; + let result = client.call(&alice, &approve).submit().await?; assert!(result.return_value().is_ok()); assert_last_contract_event!( &mut client, Approval { - owner: contract_address, + owner: contract.addr, spender: bob_address, value: approve_amount, } @@ -453,7 +440,7 @@ mod e2e_tests { let bob_allowance = client .sandbox() - .allowance(&asset_id, &contract_account, &bob); + .allowance(&asset_id, &contract.account_id, &bob); assert_eq!(bob_allowance, 200u128); // Bob's allowance is 200 Ok(()) @@ -465,39 +452,39 @@ mod e2e_tests { )))] async fn allowance_works(mut client: Client) -> E2EResult<()> { let asset_id: u32 = 1; - let alice = ink_sandbox::alice(); - let bob = ink_sandbox::bob(); + let alice = alice(); + let bob = bob(); client .sandbox() .create(&asset_id, &alice, 1u128) .expect("Failed to create asset"); - let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .instantiate("assets_precompile", &alice, &mut constructor) .submit() .await .expect("instantiate failed"); let call_builder = - contract.call_builder::(); + contract.call_builder::(); client .sandbox() .mint_into(&asset_id, &alice, 100_000u128) .expect("Failed to mint to bob"); - let alice_address = ink_e2e::address_from_account_id(&alice); - let bob_address = ink_e2e::address_from_account_id(&bob); - let bob_origin = DefaultSandbox::convert_account_to_origin(bob.clone()); + let alice_address = alice.address(); + let bob_address = bob.address(); + client .sandbox() - .map_account(bob_origin) + .map_account(&bob) .expect("Failed to map bob's account"); let allowance_call = call_builder.allowance(alice_address, bob_address); let result = client - .call(&ink_e2e::alice(), &allowance_call) + .call(&alice, &allowance_call) .dry_run() .await?; let allowance_before = result.return_value(); @@ -511,7 +498,7 @@ mod e2e_tests { .expect("Failed to approve"); let result = client - .call(&ink_e2e::alice(), &allowance_call) + .call(&alice, &allowance_call) .dry_run() .await?; let allowance_after = result.return_value(); @@ -530,24 +517,23 @@ mod e2e_tests { mut client: Client, ) -> E2EResult<()> { let asset_id: u32 = 1; - let alice = ink_sandbox::alice(); - let bob = ink_sandbox::bob(); + let alice = alice(); + let bob = bob(); client .sandbox() .create(&asset_id, &alice, 1u128) .expect("Failed to create asset"); - let mut constructor = asset_hub_precompile::AssetHubPrecompileRef::new(asset_id); + let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &ink_e2e::alice(), &mut constructor) + .instantiate("assets_precompile", &alice, &mut constructor) .submit() .await .expect("instantiate failed"); let mut call_builder = - contract.call_builder::(); + contract.call_builder::(); - let contract_account = account_id_from_contract(&contract.account_id); client .sandbox() .mint_into(&asset_id, &alice, 100_000u128) @@ -556,23 +542,22 @@ mod e2e_tests { // Approve alice to spend contract's tokens client .sandbox() - .approve(&asset_id, &alice, &contract_account, 50_000u128) + .approve(&asset_id, &alice, &contract.account_id, 50_000u128) .expect("Failed to approve"); - let alice_address = ink_e2e::address_from_account_id(&alice); - let bob_address = ink_e2e::address_from_account_id(&bob); + let alice_address = alice.address(); + let bob_address = bob.address(); let transfer_amount = U256::from(1_500); - let bob_origin = DefaultSandbox::convert_account_to_origin(bob.clone()); client .sandbox() - .map_account(bob_origin) + .map_account(&bob) .expect("Failed to map bob's account"); let transfer_from = call_builder.transfer_from(alice_address, bob_address, transfer_amount); let result = client - .call(&ink_e2e::alice(), &transfer_from) + .call(&alice, &transfer_from) .submit() .await?; @@ -591,7 +576,7 @@ mod e2e_tests { let contract_allowance = client .sandbox() - .allowance(&asset_id, &alice, &contract_account); + .allowance(&asset_id, &alice, &contract.account_id); assert_eq!(alice_balance, 98_500u128); // 100_000 - 1_500 assert_eq!(bob_balance, 1_500u128); @@ -601,7 +586,7 @@ mod e2e_tests { let transfer_from = call_builder.transfer_from(alice_address, bob_address, U256::from(1_000_000)); let result = client - .call(&ink_e2e::alice(), &transfer_from) + .call(&alice, &transfer_from) .submit() .await?; assert_eq!(result.extract_error(), Some("Unapproved".to_string())); From 36560759daf97678c9e1763217c63d3967b31d2b Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Mon, 27 Oct 2025 12:24:58 +0100 Subject: [PATCH 09/33] test: test macro helpers --- crates/sandbox/src/error.rs | 28 ++ crates/sandbox/src/lib.rs | 118 +++++++- .../public/assets-precompile/lib.rs | 251 +++++++----------- 3 files changed, 234 insertions(+), 163 deletions(-) diff --git a/crates/sandbox/src/error.rs b/crates/sandbox/src/error.rs index 0f62dd28abb..314ba570c2f 100644 --- a/crates/sandbox/src/error.rs +++ b/crates/sandbox/src/error.rs @@ -39,3 +39,31 @@ impl fmt::Display for SandboxErr { write!(f, "SandboxErr: {}", self.msg) } } + +/// Unified error type for sandbox E2E testing. +/// +/// This error type allows seamless error propagation with the `?` operator +/// across sandbox APIs (which return `DispatchError`) and contract calls +/// (which return `SandboxErr`). +#[derive(Debug, thiserror::Error)] +pub enum E2EError { + /// Error from FRAME dispatch (e.g., pallet extrinsic failures). + /// + /// Returned by sandbox APIs like `create()`, `mint_into()`, `map_account()`, etc. + /// when the underlying FRAME pallet operation fails. + #[error("Dispatch error: {0:?}")] + Dispatch(frame_support::sp_runtime::DispatchError), + + /// Error from sandbox operations. + /// + /// Returned by contract instantiation and call operations when they fail + /// at the sandbox client level. + #[error("Sandbox error: {0}")] + Sandbox(#[from] SandboxErr), +} + +impl From for E2EError { + fn from(err: frame_support::sp_runtime::DispatchError) -> Self { + E2EError::Dispatch(err) + } +} diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index 3c93ca68589..4a50932cc9c 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -2,7 +2,7 @@ use core::any::Any; pub mod api; pub mod client; -mod error; +pub mod error; pub mod macros; pub use frame_metadata::RuntimeMetadataPrefixed; @@ -69,8 +69,122 @@ pub use client::{ Client as SandboxClient, preset, }; +pub use error::E2EError; pub use ink_e2e_macro::test; +/// Asserts that a contract call succeeded without reverting. +/// +/// This macro follows FRAME's `assert_ok!` convention for consistency across +/// the Polkadot ecosystem. It verifies that a contract call completed successfully +/// and did not revert. If the call reverted, the macro panics with a detailed +/// error message extracted from the call trace. +/// +/// # Behavior +/// +/// - Takes a `CallResult` as input +/// - Checks if `dry_run.did_revert()` is `false` +/// - Panics with error details if the call reverted +/// - Returns the `CallResult` for further inspection if successful +/// +/// # Examples +/// +/// ```ignore +/// let result = client.call(&alice, &transfer).submit().await?; +/// assert_ok!(result); +/// ``` +#[macro_export] +macro_rules! assert_ok { + ($result:expr) => {{ + let result = $result; + if result.dry_run.did_revert() { + panic!( + "Expected call to succeed but it reverted.\nError: {:?}", + result.extract_error() + ); + } + result + }}; + ($result:expr, $($msg:tt)+) => {{ + let result = $result; + if result.dry_run.did_revert() { + panic!( + "{}\nExpected call to succeed but it reverted.\nError: {:?}", + format_args!($($msg)+), + result.extract_error() + ); + } + result + }}; +} + +/// Asserts that a contract call reverted with a specific error. +/// +/// This macro follows FRAME's `assert_noop!` convention, which stands for +/// "assert no operation" - meaning the call should fail without changing state. +/// It verifies that a contract call reverted and that the revert reason matches +/// the expected error string. +/// +/// # Behavior +/// +/// - Takes a `CallResult` and an expected error string as input +/// - Checks if `dry_run.did_revert()` is `true` +/// - Panics if the call succeeded (did not revert) +/// - Extracts the error from the call trace using `extract_error()` +/// - Panics if the actual error doesn't match the expected error +/// - Returns the `CallResult` if both checks pass +/// +/// # Examples +/// +/// ```ignore +/// let result = client.call(&alice, &insufficient_transfer).submit().await?; +/// assert_noop!(result, "BalanceLow"); +/// ``` +#[macro_export] +macro_rules! assert_noop { + ($result:expr, $expected_error:expr) => {{ + let result = $result; + if !result.dry_run.did_revert() { + panic!( + "Expected call to revert with '{}' but it succeeded", + $expected_error + ); + } + + let actual_error = result.extract_error(); + if actual_error != Some($expected_error.to_string()) { + panic!( + "Expected error '{}' but got {:?}", + $expected_error, + actual_error + ); + } + + result + }}; + ($result:expr, $expected_error:expr, $($msg:tt)+) => {{ + let result = $result; + if !result.dry_run.did_revert() { + panic!( + "{}\nExpected call to revert with '{}' but it succeeded", + format_args!($($msg)+), + $expected_error + ); + } + + let actual_error = result.extract_error(); + if actual_error != Some($expected_error.to_string()) { + panic!( + "{}\nExpected error '{}' but got {:?}", + format_args!($($msg)+), + $expected_error, + actual_error + ); + } + + result + }}; +} + /// Asserts that the latest contract event matches an expected event. /// /// This macro verifies that the last emitted contract event from the sandbox @@ -80,7 +194,7 @@ pub use ink_e2e_macro::test; /// - `client` - Mutable reference to the sandbox client /// - `event` - The expected event #[macro_export] -macro_rules! assert_last_contract_event { +macro_rules! assert_last_event { ($client:expr, $event:expr $(,)?) => { $crate::client::assert_last_contract_event_inner($client, $event) }; diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index e919544a875..4f7d58d7328 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -190,15 +190,18 @@ mod e2e_tests { }; use ink_sandbox::{ DefaultSandbox, + E2EError, SandboxClient, api::prelude::{ AssetsAPI, ContractAPI, }, - assert_last_contract_event, + assert_last_event, + assert_noop, + assert_ok, }; - type E2EResult = std::result::Result>; + type E2EResult = std::result::Result; #[ink_sandbox::test(backend(runtime_only( sandbox = DefaultSandbox, @@ -212,13 +215,11 @@ mod e2e_tests { .instantiate("assets_precompile", &alice(), &mut constructor) .value(1_000_000_000_000u128) // Transfer native tokens to contract .submit() - .await - .expect("instantiate failed"); + .await?; - let call_builder = contract.call_builder::(); - let asset_id_call = call_builder.asset_id(); + let contract_call = contract.call_builder::(); let result = client - .call(&alice(), &asset_id_call) + .call(&alice(), &contract_call.asset_id()) .dry_run() .await?; @@ -237,31 +238,24 @@ mod e2e_tests { client .sandbox() - .create(&asset_id, &admin, 1u128) - .expect("Failed to create asset"); + .create(&asset_id, &admin, 1u128)?; client .sandbox() - .mint_into(&asset_id, &admin, 1000u128) - .expect("Failed to mint asset"); + .mint_into(&asset_id, &admin, 1000u128)?; - let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &admin, &mut constructor) + .instantiate("assets_precompile", &admin, &mut AssetHubPrecompileRef::new(asset_id)) .submit() - .await - .expect("instantiate failed"); - let call_builder = - contract.call_builder::(); + .await?; - let total_supply = call_builder.total_supply(); + let contract_call = + contract.call_builder::(); let result = client - .call(&admin, &total_supply) + .call(&admin, &contract_call.total_supply()) .submit() .await?; - let supply = result.return_value(); - assert_eq!(supply, U256::from(1000)); - + assert_eq!(result.return_value(), U256::from(1000)); Ok(()) } @@ -276,48 +270,36 @@ mod e2e_tests { client .sandbox() - .create(&asset_id, &alice, 1u128) - .expect("Failed to create asset"); + .create(&asset_id, &alice, 1u128)?; client .sandbox() - .mint_into(&asset_id, &alice, 1000u128) - .expect("Failed to mint to alice"); + .mint_into(&asset_id, &alice, 1000u128)?; client .sandbox() - .mint_into(&asset_id, &bob, 500u128) - .expect("Failed to mint to bob"); + .mint_into(&asset_id, &bob, 500u128)?; - let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &alice, &mut constructor) + .instantiate("assets_precompile", &alice, &mut AssetHubPrecompileRef::new(asset_id)) .submit() - .await - .expect("instantiate failed"); - let call_builder = - contract.call_builder::(); + .await?; // Map bob's account otherwise it fails. client .sandbox() - .map_account(&bob) - .expect("Failed to map bob's account"); + .map_account(&bob)?; - let alice_balance_call = call_builder.balance_of(alice.address()); - let alice_result = client - .call(&alice, &alice_balance_call) + let contract_call = + contract.call_builder::(); + let alice_balance = client + .call(&alice, &contract_call.balance_of(alice.address())) .dry_run() .await?; - let alice_balance = alice_result.return_value(); - - let bob_balance_call = call_builder.balance_of(bob.address()); - let bob_result = client - .call(&alice, &bob_balance_call) + assert_eq!(alice_balance.return_value(), U256::from(1000)); + let bob_balance = client + .call(&alice, &contract_call.balance_of(bob.address())) .dry_run() .await?; - let bob_balance = bob_result.return_value(); - - assert_eq!(alice_balance, U256::from(1000)); - assert_eq!(bob_balance, U256::from(500)); + assert_eq!(bob_balance.return_value(), U256::from(500)); Ok(()) } @@ -333,35 +315,28 @@ mod e2e_tests { client .sandbox() - .create(&asset_id, &alice, 1u128) - .expect("Failed to create asset"); + .create(&asset_id, &alice, 1u128)?; - let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &alice, &mut constructor) + .instantiate("assets_precompile", &alice, &mut AssetHubPrecompileRef::new(asset_id)) .submit() - .await - .expect("instantiate failed"); - let mut call_builder = - contract.call_builder::(); + .await?; - let bob_address = bob.address(); - let transfer_amount = U256::from(1_000); client .sandbox() - .mint_into(&asset_id, &contract.account_id, 100_000u128) - .expect("Failed to mint to contract"); + .mint_into(&asset_id, &contract.account_id, 100_000u128)?; client .sandbox() - .map_account(&bob) - .expect("Failed to map bob's account"); + .map_account(&bob)?; - let transfer = call_builder.transfer(bob_address, transfer_amount); - let result = client.call(&alice, &transfer).submit().await?; + let mut contract_call = + contract.call_builder::(); + let bob_address = bob.address(); + let transfer_amount = U256::from(1_000); - let transfer_result = result.return_value(); - assert!(transfer_result.is_ok()); - assert_last_contract_event!( + let result = client.call(&alice, &contract_call.transfer(bob_address, transfer_amount)).submit().await?; + assert_ok!(result); + assert_last_event!( &mut client, Transfer { from: contract.addr, @@ -369,15 +344,14 @@ mod e2e_tests { value: transfer_amount } ); + let contract_balance = client.sandbox().balance_of(&asset_id, &contract.account_id); let bob_balance = client.sandbox().balance_of(&asset_id, &bob); - assert_eq!(contract_balance, 99_000u128); // Contract had 100_000, transferred 1_000 - assert_eq!(bob_balance, 1_000u128); // Bob received 1_000 + assert_eq!(contract_balance, 99_000u128); + assert_eq!(bob_balance, 1_000u128); - // Show error case with transferring too many tokens. - let transfer = call_builder.transfer(bob_address, U256::from(1_000_000)); - let result = client.call(&alice, &transfer).submit().await?; - assert_eq!(result.extract_error(), Some("BalanceLow".to_string())); + let result = client.call(&alice, &contract_call.transfer(bob_address, U256::from(1_000_000))).submit().await?; + assert_noop!(result, "BalanceLow"); Ok(()) } @@ -393,43 +367,35 @@ mod e2e_tests { client .sandbox() - .create(&asset_id, &alice, 1u128) - .expect("Failed to create asset"); + .create(&asset_id, &alice, 1u128)?; - let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &alice, &mut constructor) + .instantiate("assets_precompile", &alice, &mut AssetHubPrecompileRef::new(asset_id)) // Contract needs native balance for approval deposit. .value(100_000) .submit() - .await - .expect("instantiate failed"); - let mut call_builder = - contract.call_builder::(); + .await?; client .sandbox() - .mint_into(&asset_id, &contract.account_id, 100_000u128) - .expect("Failed to mint to contract"); - let bob_address = bob.address(); - let approve_amount = U256::from(200); - + .mint_into(&asset_id, &contract.account_id, 100_000u128)?; client .sandbox() - .map_account(&bob) - .expect("Failed to map bob's account"); - + .map_account(&bob)?; let bob_allowance_before = client .sandbox() .allowance(&asset_id, &contract.account_id, &bob); assert_eq!(bob_allowance_before, 0u128); // Bob's allowance is 0 - let approve = call_builder.approve(bob_address, approve_amount); - let result = client.call(&alice, &approve).submit().await?; + let mut contract_call = + contract.call_builder::(); + let bob_address = bob.address(); + let approve_amount = U256::from(200); - assert!(result.return_value().is_ok()); - assert_last_contract_event!( + let result = client.call(&alice, &contract_call.approve(bob_address, approve_amount)).submit().await?; + assert_ok!(result); + assert_last_event!( &mut client, Approval { owner: contract.addr, @@ -437,11 +403,12 @@ mod e2e_tests { value: approve_amount, } ); + let bob_allowance = client .sandbox() .allowance(&asset_id, &contract.account_id, &bob); - assert_eq!(bob_allowance, 200u128); // Bob's allowance is 200 + assert_eq!(bob_allowance, 200u128); Ok(()) } @@ -457,58 +424,43 @@ mod e2e_tests { client .sandbox() - .create(&asset_id, &alice, 1u128) - .expect("Failed to create asset"); + .create(&asset_id, &alice, 1u128)?; - let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &alice, &mut constructor) + .instantiate("assets_precompile", &alice, &mut AssetHubPrecompileRef::new(asset_id)) .submit() - .await - .expect("instantiate failed"); - let call_builder = - contract.call_builder::(); + .await?; + let contract_call = + contract.call_builder::(); client .sandbox() - .mint_into(&asset_id, &alice, 100_000u128) - .expect("Failed to mint to bob"); - - let alice_address = alice.address(); - let bob_address = bob.address(); - + .mint_into(&asset_id, &alice, 100_000u128)?; client .sandbox() - .map_account(&bob) - .expect("Failed to map bob's account"); + .map_account(&bob)?; - let allowance_call = call_builder.allowance(alice_address, bob_address); + let allowance_call = &contract_call.allowance(alice.address(), bob.address()); let result = client - .call(&alice, &allowance_call) + .call(&alice, allowance_call) .dry_run() .await?; - let allowance_before = result.return_value(); - - assert_eq!(allowance_before, U256::from(0)); + assert_eq!(result.return_value(), U256::from(0)); // Approve bob to spend alice's tokens client .sandbox() - .approve(&asset_id, &alice, &bob, 300u128) - .expect("Failed to approve"); + .approve(&asset_id, &alice, &bob, 300u128)?; let result = client - .call(&alice, &allowance_call) + .call(&alice, allowance_call) .dry_run() .await?; - let allowance_after = result.return_value(); - - assert_eq!(allowance_after, U256::from(300)); + assert_eq!(result.return_value(), U256::from(300)); Ok(()) } - /// Tests transferFrom functionality. #[ink_sandbox::test(backend(runtime_only( sandbox = DefaultSandbox, client = SandboxClient @@ -522,47 +474,32 @@ mod e2e_tests { client .sandbox() - .create(&asset_id, &alice, 1u128) - .expect("Failed to create asset"); + .create(&asset_id, &alice, 1u128)?; - let mut constructor = AssetHubPrecompileRef::new(asset_id); let contract = client - .instantiate("assets_precompile", &alice, &mut constructor) + .instantiate("assets_precompile", &alice, &mut AssetHubPrecompileRef::new(asset_id)) .submit() - .await - .expect("instantiate failed"); - let mut call_builder = - contract.call_builder::(); + .await?; client .sandbox() - .mint_into(&asset_id, &alice, 100_000u128) - .expect("Failed to mint to contract"); - + .mint_into(&asset_id, &alice, 100_000u128)?; // Approve alice to spend contract's tokens client .sandbox() - .approve(&asset_id, &alice, &contract.account_id, 50_000u128) - .expect("Failed to approve"); + .approve(&asset_id, &alice, &contract.account_id, 50_000u128)?; + client + .sandbox() + .map_account(&bob)?; + let mut contract_call = + contract.call_builder::(); let alice_address = alice.address(); let bob_address = bob.address(); let transfer_amount = U256::from(1_500); - - client - .sandbox() - .map_account(&bob) - .expect("Failed to map bob's account"); - - let transfer_from = - call_builder.transfer_from(alice_address, bob_address, transfer_amount); - let result = client - .call(&alice, &transfer_from) - .submit() - .await?; - - assert!(result.return_value().is_ok()); - assert_last_contract_event!( + let result = client.call(&alice, &contract_call.transfer_from(alice_address, bob_address, transfer_amount)).submit().await?; + assert_ok!(result); + assert_last_event!( &mut client, Transfer { from: alice_address, @@ -577,20 +514,12 @@ mod e2e_tests { client .sandbox() .allowance(&asset_id, &alice, &contract.account_id); - - assert_eq!(alice_balance, 98_500u128); // 100_000 - 1_500 + assert_eq!(alice_balance, 98_500u128); assert_eq!(bob_balance, 1_500u128); assert_eq!(contract_allowance, 48_500u128); - // Show error case with transferring more tokens than approved. - let transfer_from = - call_builder.transfer_from(alice_address, bob_address, U256::from(1_000_000)); - let result = client - .call(&alice, &transfer_from) - .submit() - .await?; - assert_eq!(result.extract_error(), Some("Unapproved".to_string())); - + let result = client.call(&alice, &contract_call.transfer_from(alice_address, bob_address, U256::from(1_000_000))).submit().await?; + assert_noop!(result, "Unapproved"); Ok(()) } } From 2a5bb22b7eb06e0dc2dca27d9f25ef444fedaf1b Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Mon, 27 Oct 2025 12:27:01 +0100 Subject: [PATCH 10/33] fix: remove benchmark in sandbox --- Cargo.lock | 10 ---------- crates/sandbox/Cargo.toml | 10 +--------- crates/sandbox/src/macros.rs | 2 -- 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3758c305e6..523d5d7370d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4841,7 +4841,6 @@ dependencies = [ "polkavm", "polkavm-common 0.29.0", "rand 0.8.5", - "rand_pcg", "revm", "ripemd", "rlp 0.6.1", @@ -5666,15 +5665,6 @@ dependencies = [ "serde", ] -[[package]] -name = "rand_pcg" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rand_xorshift" version = "0.4.0" diff --git a/crates/sandbox/Cargo.toml b/crates/sandbox/Cargo.toml index 49adaa52635..f9ab498658c 100644 --- a/crates/sandbox/Cargo.toml +++ b/crates/sandbox/Cargo.toml @@ -61,12 +61,4 @@ std = [ "sp-externalities/std", "sp-io/std", "ink_e2e_macro/std", -] -runtime-benchmarks = [ - "frame-support/runtime-benchmarks", - "frame-system/runtime-benchmarks", - "pallet-assets/runtime-benchmarks", - "pallet-balances/runtime-benchmarks", - "pallet-revive/runtime-benchmarks", - "pallet-timestamp/runtime-benchmarks", -] +] \ No newline at end of file diff --git a/crates/sandbox/src/macros.rs b/crates/sandbox/src/macros.rs index 51fc1bc7c7e..bacefd48c2c 100644 --- a/crates/sandbox/src/macros.rs +++ b/crates/sandbox/src/macros.rs @@ -201,8 +201,6 @@ mod construct_runtime { type WeightInfo = (); type CallbackHandle = $crate::pallet_assets::AutoIncAssetId<$runtime, TrustBackedAssetsInstance>; type RemoveItemsLimit = ConstU32<1000>; - #[cfg(feature = "runtime-benchmarks")] - type BenchmarkHelper = (); } impl $crate::pallet_transaction_payment::Config for $runtime { From 30ec40060df2145207e53bd72580630699bcb21e Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Mon, 27 Oct 2025 12:59:17 +0100 Subject: [PATCH 11/33] small place changes --- crates/e2e/src/lib.rs | 53 ++++++--- crates/sandbox/src/api/revive_api.rs | 4 +- crates/sandbox/src/lib.rs | 169 +-------------------------- crates/sandbox/src/macros.rs | 128 ++++++++++++++++++++ 4 files changed, 169 insertions(+), 185 deletions(-) diff --git a/crates/e2e/src/lib.rs b/crates/e2e/src/lib.rs index f575b3c8e8b..0e3b32a5635 100644 --- a/crates/e2e/src/lib.rs +++ b/crates/e2e/src/lib.rs @@ -153,23 +153,27 @@ pub fn address(account: Sr25519Keyring) -> Address { AccountIdMapper::to_address(account.to_account_id().as_ref()) } -/// Extension trait for converting various types to Address (H160). -pub trait IntoAddress { - /// Convert to an Address (H160). - fn address(&self) -> Address; -} - -impl IntoAddress for Keypair { - fn address(&self) -> Address { - AccountIdMapper::to_address(&self.public_key().0) - } +/// Returns the [`ink::Address`] for a given account id. +/// +/// # Developer Note +/// +/// We take the `AccountId` and return only the first twenty bytes, this +/// is what `pallet-revive` does as well. +pub fn address_from_account_id>(account_id: AccountId) -> Address { + AccountIdMapper::to_address(account_id.as_ref()) } -impl IntoAddress for ink_primitives::AccountId { - fn address(&self) -> Address { - let bytes = *AsRef::<[u8; 32]>::as_ref(self); - AccountIdMapper::to_address(&bytes) - } +/// Returns the [`ink::Address`] for a given `Keypair`. +/// +/// # Developer Note +/// +/// We take the `AccountId` and return only the first twenty bytes, this +/// is what `pallet-revive` does as well. +pub fn address_from_keypair + AsRef<[u8]>>( + keypair: &Keypair, +) -> Address { + let account_id: AccountId = keypair_to_account(keypair); + address_from_account_id(account_id) } /// Transforms a `Keypair` into an account id. @@ -202,3 +206,22 @@ where { <::Type as FromAddr>::from_addr(acc_id) } + +/// Extension trait for converting various types to Address (H160). +pub trait IntoAddress { + /// Convert to an Address (H160). + fn address(&self) -> Address; +} + +impl IntoAddress for Keypair { + fn address(&self) -> Address { + AccountIdMapper::to_address(&self.public_key().0) + } +} + +impl IntoAddress for ink_primitives::AccountId { + fn address(&self) -> Address { + let bytes = *AsRef::<[u8; 32]>::as_ref(self); + AccountIdMapper::to_address(&bytes) + } +} diff --git a/crates/sandbox/src/api/revive_api.rs b/crates/sandbox/src/api/revive_api.rs index bcfcdb6be7d..37b08500964 100644 --- a/crates/sandbox/src/api/revive_api.rs +++ b/crates/sandbox/src/api/revive_api.rs @@ -380,7 +380,7 @@ mod tests { warm_up::(&mut sandbox); let default_actor = DefaultSandbox::default_actor(); - sandbox.map_account(default_actor.clone()).expect("cannot map"); + sandbox.map_account(&default_actor).expect("cannot map"); let origin = DefaultSandbox::convert_account_to_origin(default_actor); let result = sandbox.deploy_contract( contract_binary.clone(), @@ -417,7 +417,7 @@ mod tests { warm_up::(&mut sandbox); let default_actor = DefaultSandbox::default_actor(); - sandbox.map_account(default_actor.clone()).expect("unable to map"); + sandbox.map_account(&default_actor).expect("unable to map"); let origin = DefaultSandbox::convert_account_to_origin(default_actor); let result = sandbox.deploy_contract( contract_binary, diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index 4a50932cc9c..d45f61516a2 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -72,133 +72,6 @@ pub use client::{ pub use error::E2EError; pub use ink_e2e_macro::test; -/// Asserts that a contract call succeeded without reverting. -/// -/// This macro follows FRAME's `assert_ok!` convention for consistency across -/// the Polkadot ecosystem. It verifies that a contract call completed successfully -/// and did not revert. If the call reverted, the macro panics with a detailed -/// error message extracted from the call trace. -/// -/// # Behavior -/// -/// - Takes a `CallResult` as input -/// - Checks if `dry_run.did_revert()` is `false` -/// - Panics with error details if the call reverted -/// - Returns the `CallResult` for further inspection if successful -/// -/// # Examples -/// -/// ```ignore -/// let result = client.call(&alice, &transfer).submit().await?; -/// assert_ok!(result); -/// ``` -#[macro_export] -macro_rules! assert_ok { - ($result:expr) => {{ - let result = $result; - if result.dry_run.did_revert() { - panic!( - "Expected call to succeed but it reverted.\nError: {:?}", - result.extract_error() - ); - } - result - }}; - ($result:expr, $($msg:tt)+) => {{ - let result = $result; - if result.dry_run.did_revert() { - panic!( - "{}\nExpected call to succeed but it reverted.\nError: {:?}", - format_args!($($msg)+), - result.extract_error() - ); - } - result - }}; -} - -/// Asserts that a contract call reverted with a specific error. -/// -/// This macro follows FRAME's `assert_noop!` convention, which stands for -/// "assert no operation" - meaning the call should fail without changing state. -/// It verifies that a contract call reverted and that the revert reason matches -/// the expected error string. -/// -/// # Behavior -/// -/// - Takes a `CallResult` and an expected error string as input -/// - Checks if `dry_run.did_revert()` is `true` -/// - Panics if the call succeeded (did not revert) -/// - Extracts the error from the call trace using `extract_error()` -/// - Panics if the actual error doesn't match the expected error -/// - Returns the `CallResult` if both checks pass -/// -/// # Examples -/// -/// ```ignore -/// let result = client.call(&alice, &insufficient_transfer).submit().await?; -/// assert_noop!(result, "BalanceLow"); -/// ``` -#[macro_export] -macro_rules! assert_noop { - ($result:expr, $expected_error:expr) => {{ - let result = $result; - if !result.dry_run.did_revert() { - panic!( - "Expected call to revert with '{}' but it succeeded", - $expected_error - ); - } - - let actual_error = result.extract_error(); - if actual_error != Some($expected_error.to_string()) { - panic!( - "Expected error '{}' but got {:?}", - $expected_error, - actual_error - ); - } - - result - }}; - ($result:expr, $expected_error:expr, $($msg:tt)+) => {{ - let result = $result; - if !result.dry_run.did_revert() { - panic!( - "{}\nExpected call to revert with '{}' but it succeeded", - format_args!($($msg)+), - $expected_error - ); - } - - let actual_error = result.extract_error(); - if actual_error != Some($expected_error.to_string()) { - panic!( - "{}\nExpected error '{}' but got {:?}", - format_args!($($msg)+), - $expected_error, - actual_error - ); - } - - result - }}; -} - -/// Asserts that the latest contract event matches an expected event. -/// -/// This macro verifies that the last emitted contract event from the sandbox -/// matches the provided expected event. -/// -/// # Parameters -/// - `client` - Mutable reference to the sandbox client -/// - `event` - The expected event -#[macro_export] -macro_rules! assert_last_event { - ($client:expr, $event:expr $(,)?) => { - $crate::client::assert_last_contract_event_inner($client, $event) - }; -} /// A snapshot of the storage. #[derive(Clone, Debug)] @@ -385,65 +258,25 @@ pub fn to_revive_storage_deposit( /// Trait for types that can be converted into a runtime AccountId. /// -/// This allows sandbox APIs to accept various account types (ink! `AccountId`, -/// `Keypair`, `AccountId32`, raw bytes) without requiring manual conversion. -/// +/// This allows sandbox APIs to accept various account types without requiring manual conversion. pub trait IntoAccountId { fn into_account_id(self) -> AccountId; } -// Identity conversion for AccountId32 -impl IntoAccountId for AccountId32 { - fn into_account_id(self) -> AccountId32 { - self - } -} - -// Borrowed AccountId32 impl IntoAccountId for &AccountId32 { fn into_account_id(self) -> AccountId32 { self.clone() } } -// ink!'s AccountId -impl IntoAccountId for ink_primitives::AccountId { - fn into_account_id(self) -> AccountId32 { - AccountId32::from(*AsRef::<[u8; 32]>::as_ref(&self)) - } -} - -// Borrowed ink! AccountId impl IntoAccountId for &ink_primitives::AccountId { fn into_account_id(self) -> AccountId32 { AccountId32::from(*AsRef::<[u8; 32]>::as_ref(self)) } } -// Keypair from e2e tests -impl IntoAccountId for ink_e2e::Keypair { - fn into_account_id(self) -> AccountId32 { - AccountId32::from(self.public_key().0) - } -} - -// Borrowed Keypair from e2e tests impl IntoAccountId for &ink_e2e::Keypair { fn into_account_id(self) -> AccountId32 { AccountId32::from(self.public_key().0) } -} - -// Raw bytes [u8; 32] -impl IntoAccountId for [u8; 32] { - fn into_account_id(self) -> AccountId32 { - AccountId32::from(self) - } -} - -// Borrowed raw bytes -impl IntoAccountId for &[u8; 32] { - fn into_account_id(self) -> AccountId32 { - AccountId32::from(*self) - } } \ No newline at end of file diff --git a/crates/sandbox/src/macros.rs b/crates/sandbox/src/macros.rs index bacefd48c2c..6fcce8b4763 100644 --- a/crates/sandbox/src/macros.rs +++ b/crates/sandbox/src/macros.rs @@ -13,6 +13,134 @@ use frame_support::{ use frame_system::pallet_prelude::BlockNumberFor; use sp_io::TestExternalities; +/// Asserts that a contract call succeeded without reverting. +/// +/// This macro follows FRAME's `assert_ok!` convention for consistency across +/// the Polkadot ecosystem. It verifies that a contract call completed successfully +/// and did not revert. If the call reverted, the macro panics with a detailed +/// error message extracted from the call trace. +/// +/// # Behavior +/// +/// - Takes a `CallResult` as input +/// - Checks if `dry_run.did_revert()` is `false` +/// - Panics with error details if the call reverted +/// - Returns the `CallResult` for further inspection if successful +/// +/// # Examples +/// +/// ```ignore +/// let result = client.call(&alice, &transfer).submit().await?; +/// assert_ok!(result); +/// ``` +#[macro_export] +macro_rules! assert_ok { + ($result:expr) => {{ + let result = $result; + if result.dry_run.did_revert() { + panic!( + "Expected call to succeed but it reverted.\nError: {:?}", + result.extract_error() + ); + } + result + }}; + ($result:expr, $($msg:tt)+) => {{ + let result = $result; + if result.dry_run.did_revert() { + panic!( + "{}\nExpected call to succeed but it reverted.\nError: {:?}", + format_args!($($msg)+), + result.extract_error() + ); + } + result + }}; +} + +/// Asserts that a contract call reverted with a specific error. +/// +/// This macro follows FRAME's `assert_noop!` convention, which stands for +/// "assert no operation" - meaning the call should fail without changing state. +/// It verifies that a contract call reverted and that the revert reason matches +/// the expected error string. +/// +/// # Behavior +/// +/// - Takes a `CallResult` and an expected error string as input +/// - Checks if `dry_run.did_revert()` is `true` +/// - Panics if the call succeeded (did not revert) +/// - Extracts the error from the call trace using `extract_error()` +/// - Panics if the actual error doesn't match the expected error +/// - Returns the `CallResult` if both checks pass +/// +/// # Examples +/// +/// ```ignore +/// let result = client.call(&alice, &insufficient_transfer).submit().await?; +/// assert_noop!(result, "BalanceLow"); +/// ``` +#[macro_export] +macro_rules! assert_noop { + ($result:expr, $expected_error:expr) => {{ + let result = $result; + if !result.dry_run.did_revert() { + panic!( + "Expected call to revert with '{}' but it succeeded", + $expected_error + ); + } + + let actual_error = result.extract_error(); + if actual_error != Some($expected_error.to_string()) { + panic!( + "Expected error '{}' but got {:?}", + $expected_error, + actual_error + ); + } + + result + }}; + ($result:expr, $expected_error:expr, $($msg:tt)+) => {{ + let result = $result; + if !result.dry_run.did_revert() { + panic!( + "{}\nExpected call to revert with '{}' but it succeeded", + format_args!($($msg)+), + $expected_error + ); + } + + let actual_error = result.extract_error(); + if actual_error != Some($expected_error.to_string()) { + panic!( + "{}\nExpected error '{}' but got {:?}", + format_args!($($msg)+), + $expected_error, + actual_error + ); + } + + result + }}; +} + +/// Asserts that the latest contract event matches an expected event. +/// +/// This macro verifies that the last emitted contract event from the sandbox +/// matches the provided expected event. +/// +/// # Parameters +/// - `client` - Mutable reference to the sandbox client +/// - `event` - The expected event +#[macro_export] +macro_rules! assert_last_event { + ($client:expr, $event:expr $(,)?) => { + $crate::client::assert_last_contract_event_inner($client, $event) + }; +} + /// A helper struct for initializing and finalizing blocks. pub struct BlockBuilder(std::marker::PhantomData); From 28968be9206aa10cafe314d4527389f3b4faf108 Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Mon, 27 Oct 2025 13:02:32 +0100 Subject: [PATCH 12/33] fmt --- crates/sandbox/src/api/assets_api.rs | 27 ++- crates/sandbox/src/lib.rs | 6 +- .../public/assets-precompile/lib.rs | 173 +++++++++--------- 3 files changed, 111 insertions(+), 95 deletions(-) diff --git a/crates/sandbox/src/api/assets_api.rs b/crates/sandbox/src/api/assets_api.rs index 1a199a5911a..d198d89833a 100644 --- a/crates/sandbox/src/api/assets_api.rs +++ b/crates/sandbox/src/api/assets_api.rs @@ -37,7 +37,8 @@ where /// /// # Arguments /// * `id` - ID of the new asset to be created. - /// * `owner` - The owner of the created asset (accepts any type convertible to AccountId). + /// * `owner` - The owner of the created asset (accepts any type convertible to + /// AccountId). /// * `min_balance` - The asset amount one account need at least. fn create( &mut self, @@ -78,8 +79,10 @@ where /// /// # Arguments /// * `asset` - ID of the asset. - /// * `owner` - The account that owns the tokens (accepts any type convertible to AccountId). - /// * `delegate` - The account that is allowed to spend the tokens (accepts any type convertible to AccountId). + /// * `owner` - The account that owns the tokens (accepts any type convertible to + /// AccountId). + /// * `delegate` - The account that is allowed to spend the tokens (accepts any type + /// convertible to AccountId). /// * `amount` - The number of tokens to approve. fn approve( &mut self, @@ -94,7 +97,8 @@ where /// /// # Arguments /// * `asset` - ID of the asset. - /// * `account` - The account to be credited with the created tokens (accepts any type convertible to AccountId). + /// * `account` - The account to be credited with the created tokens (accepts any type + /// convertible to AccountId). /// * `value` - The number of tokens to mint. fn mint_into( &mut self, @@ -107,8 +111,10 @@ where /// /// # Arguments /// * `asset` - ID of the asset. - /// * `source` - The account from which tokens are transferred (accepts any type convertible to AccountId). - /// * `dest` - The account to which tokens are transferred (accepts any type convertible to AccountId). + /// * `source` - The account from which tokens are transferred (accepts any type + /// convertible to AccountId). + /// * `dest` - The account to which tokens are transferred (accepts any type + /// convertible to AccountId). /// * `amount` - The number of tokens to transfer. fn transfer( &mut self, @@ -121,7 +127,8 @@ where /// Returns the account balance for the specified `owner`. /// /// # Arguments - /// * `owner` - The account whose balance is being queried (accepts any type convertible to AccountId). + /// * `owner` - The account whose balance is being queried (accepts any type + /// convertible to AccountId). fn balance_of( &mut self, asset: &AssetIdOf, @@ -141,8 +148,10 @@ where /// /// # Arguments /// * `asset` - ID of the asset. - /// * `owner` - The account that owns the tokens (accepts any type convertible to AccountId). - /// * `delegate` - The account that is allowed to spend the tokens (accepts any type convertible to AccountId). + /// * `owner` - The account that owns the tokens (accepts any type convertible to + /// AccountId). + /// * `delegate` - The account that is allowed to spend the tokens (accepts any type + /// convertible to AccountId). fn allowance( &mut self, asset: &AssetIdOf, diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index d45f61516a2..2dfaa1e823c 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -72,7 +72,6 @@ pub use client::{ pub use error::E2EError; pub use ink_e2e_macro::test; - /// A snapshot of the storage. #[derive(Clone, Debug)] pub struct Snapshot { @@ -258,7 +257,8 @@ pub fn to_revive_storage_deposit( /// Trait for types that can be converted into a runtime AccountId. /// -/// This allows sandbox APIs to accept various account types without requiring manual conversion. +/// This allows sandbox APIs to accept various account types without requiring manual +/// conversion. pub trait IntoAccountId { fn into_account_id(self) -> AccountId; } @@ -279,4 +279,4 @@ impl IntoAccountId for &ink_e2e::Keypair { fn into_account_id(self) -> AccountId32 { AccountId32::from(self.public_key().0) } -} \ No newline at end of file +} diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index 4f7d58d7328..45218f3677c 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -186,7 +186,8 @@ mod e2e_tests { use ink_e2e::{ ContractsBackend, IntoAddress, - alice, bob, + alice, + bob, }; use ink_sandbox::{ DefaultSandbox, @@ -236,20 +237,19 @@ mod e2e_tests { let asset_id: u32 = 1; let admin = alice(); - client - .sandbox() - .create(&asset_id, &admin, 1u128)?; - client - .sandbox() - .mint_into(&asset_id, &admin, 1000u128)?; + client.sandbox().create(&asset_id, &admin, 1u128)?; + client.sandbox().mint_into(&asset_id, &admin, 1000u128)?; let contract = client - .instantiate("assets_precompile", &admin, &mut AssetHubPrecompileRef::new(asset_id)) + .instantiate( + "assets_precompile", + &admin, + &mut AssetHubPrecompileRef::new(asset_id), + ) .submit() .await?; - let contract_call = - contract.call_builder::(); + let contract_call = contract.call_builder::(); let result = client .call(&admin, &contract_call.total_supply()) .submit() @@ -268,28 +268,23 @@ mod e2e_tests { let alice = alice(); let bob = bob(); - client - .sandbox() - .create(&asset_id, &alice, 1u128)?; - client - .sandbox() - .mint_into(&asset_id, &alice, 1000u128)?; - client - .sandbox() - .mint_into(&asset_id, &bob, 500u128)?; + client.sandbox().create(&asset_id, &alice, 1u128)?; + client.sandbox().mint_into(&asset_id, &alice, 1000u128)?; + client.sandbox().mint_into(&asset_id, &bob, 500u128)?; let contract = client - .instantiate("assets_precompile", &alice, &mut AssetHubPrecompileRef::new(asset_id)) + .instantiate( + "assets_precompile", + &alice, + &mut AssetHubPrecompileRef::new(asset_id), + ) .submit() .await?; // Map bob's account otherwise it fails. - client - .sandbox() - .map_account(&bob)?; + client.sandbox().map_account(&bob)?; - let contract_call = - contract.call_builder::(); + let contract_call = contract.call_builder::(); let alice_balance = client .call(&alice, &contract_call.balance_of(alice.address())) .dry_run() @@ -313,28 +308,33 @@ mod e2e_tests { let alice = alice(); let bob = bob(); - client - .sandbox() - .create(&asset_id, &alice, 1u128)?; + client.sandbox().create(&asset_id, &alice, 1u128)?; let contract = client - .instantiate("assets_precompile", &alice, &mut AssetHubPrecompileRef::new(asset_id)) + .instantiate( + "assets_precompile", + &alice, + &mut AssetHubPrecompileRef::new(asset_id), + ) .submit() .await?; client .sandbox() .mint_into(&asset_id, &contract.account_id, 100_000u128)?; - client - .sandbox() - .map_account(&bob)?; + client.sandbox().map_account(&bob)?; - let mut contract_call = - contract.call_builder::(); + let mut contract_call = contract.call_builder::(); let bob_address = bob.address(); let transfer_amount = U256::from(1_000); - let result = client.call(&alice, &contract_call.transfer(bob_address, transfer_amount)).submit().await?; + let result = client + .call( + &alice, + &contract_call.transfer(bob_address, transfer_amount), + ) + .submit() + .await?; assert_ok!(result); assert_last_event!( &mut client, @@ -345,12 +345,19 @@ mod e2e_tests { } ); - let contract_balance = client.sandbox().balance_of(&asset_id, &contract.account_id); + let contract_balance = + client.sandbox().balance_of(&asset_id, &contract.account_id); let bob_balance = client.sandbox().balance_of(&asset_id, &bob); assert_eq!(contract_balance, 99_000u128); assert_eq!(bob_balance, 1_000u128); - let result = client.call(&alice, &contract_call.transfer(bob_address, U256::from(1_000_000))).submit().await?; + let result = client + .call( + &alice, + &contract_call.transfer(bob_address, U256::from(1_000_000)), + ) + .submit() + .await?; assert_noop!(result, "BalanceLow"); Ok(()) @@ -365,9 +372,7 @@ mod e2e_tests { let alice = alice(); let bob = bob(); - client - .sandbox() - .create(&asset_id, &alice, 1u128)?; + client.sandbox().create(&asset_id, &alice, 1u128)?; let contract = client .instantiate("assets_precompile", &alice, &mut AssetHubPrecompileRef::new(asset_id)) @@ -379,21 +384,21 @@ mod e2e_tests { client .sandbox() .mint_into(&asset_id, &contract.account_id, 100_000u128)?; - client - .sandbox() - .map_account(&bob)?; + client.sandbox().map_account(&bob)?; let bob_allowance_before = client .sandbox() .allowance(&asset_id, &contract.account_id, &bob); assert_eq!(bob_allowance_before, 0u128); // Bob's allowance is 0 - let mut contract_call = - contract.call_builder::(); + let mut contract_call = contract.call_builder::(); let bob_address = bob.address(); let approve_amount = U256::from(200); - let result = client.call(&alice, &contract_call.approve(bob_address, approve_amount)).submit().await?; + let result = client + .call(&alice, &contract_call.approve(bob_address, approve_amount)) + .submit() + .await?; assert_ok!(result); assert_last_event!( &mut client, @@ -422,40 +427,29 @@ mod e2e_tests { let alice = alice(); let bob = bob(); - client - .sandbox() - .create(&asset_id, &alice, 1u128)?; + client.sandbox().create(&asset_id, &alice, 1u128)?; let contract = client - .instantiate("assets_precompile", &alice, &mut AssetHubPrecompileRef::new(asset_id)) + .instantiate( + "assets_precompile", + &alice, + &mut AssetHubPrecompileRef::new(asset_id), + ) .submit() .await?; - let contract_call = - contract.call_builder::(); - client - .sandbox() - .mint_into(&asset_id, &alice, 100_000u128)?; - client - .sandbox() - .map_account(&bob)?; + let contract_call = contract.call_builder::(); + client.sandbox().mint_into(&asset_id, &alice, 100_000u128)?; + client.sandbox().map_account(&bob)?; let allowance_call = &contract_call.allowance(alice.address(), bob.address()); - let result = client - .call(&alice, allowance_call) - .dry_run() - .await?; + let result = client.call(&alice, allowance_call).dry_run().await?; assert_eq!(result.return_value(), U256::from(0)); // Approve bob to spend alice's tokens - client - .sandbox() - .approve(&asset_id, &alice, &bob, 300u128)?; + client.sandbox().approve(&asset_id, &alice, &bob, 300u128)?; - let result = client - .call(&alice, allowance_call) - .dry_run() - .await?; + let result = client.call(&alice, allowance_call).dry_run().await?; assert_eq!(result.return_value(), U256::from(300)); Ok(()) @@ -472,32 +466,35 @@ mod e2e_tests { let alice = alice(); let bob = bob(); - client - .sandbox() - .create(&asset_id, &alice, 1u128)?; + client.sandbox().create(&asset_id, &alice, 1u128)?; let contract = client - .instantiate("assets_precompile", &alice, &mut AssetHubPrecompileRef::new(asset_id)) + .instantiate( + "assets_precompile", + &alice, + &mut AssetHubPrecompileRef::new(asset_id), + ) .submit() .await?; - client - .sandbox() - .mint_into(&asset_id, &alice, 100_000u128)?; + client.sandbox().mint_into(&asset_id, &alice, 100_000u128)?; // Approve alice to spend contract's tokens client .sandbox() .approve(&asset_id, &alice, &contract.account_id, 50_000u128)?; - client - .sandbox() - .map_account(&bob)?; + client.sandbox().map_account(&bob)?; - let mut contract_call = - contract.call_builder::(); + let mut contract_call = contract.call_builder::(); let alice_address = alice.address(); let bob_address = bob.address(); let transfer_amount = U256::from(1_500); - let result = client.call(&alice, &contract_call.transfer_from(alice_address, bob_address, transfer_amount)).submit().await?; + let result = client + .call( + &alice, + &contract_call.transfer_from(alice_address, bob_address, transfer_amount), + ) + .submit() + .await?; assert_ok!(result); assert_last_event!( &mut client, @@ -518,7 +515,17 @@ mod e2e_tests { assert_eq!(bob_balance, 1_500u128); assert_eq!(contract_allowance, 48_500u128); - let result = client.call(&alice, &contract_call.transfer_from(alice_address, bob_address, U256::from(1_000_000))).submit().await?; + let result = client + .call( + &alice, + &contract_call.transfer_from( + alice_address, + bob_address, + U256::from(1_000_000), + ), + ) + .submit() + .await?; assert_noop!(result, "Unapproved"); Ok(()) } From 0035549cebaa6ece606ed4c4ecf135db5c2f0fdd Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Mon, 27 Oct 2025 15:39:42 +0100 Subject: [PATCH 13/33] fix ci and resolve copilot comment --- crates/sandbox/Cargo.toml | 1 + crates/sandbox/src/lib.rs | 1 + crates/sandbox/src/macros.rs | 3 ++- .../solidity-abi/sol-cross-contract/e2e_tests.rs | 4 ++-- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/sandbox/Cargo.toml b/crates/sandbox/Cargo.toml index f9ab498658c..02152e3863d 100644 --- a/crates/sandbox/Cargo.toml +++ b/crates/sandbox/Cargo.toml @@ -28,6 +28,7 @@ sp-io = { git = "https://github.com/use-ink/polkadot-sdk.git", rev = "cbab8ed4b ink_primitives = { workspace = true } ink_revive_types = { workspace = true } +ink_precompiles = { workspace = true } ink_env = { workspace = true, default-features = true } ink_e2e = { path = "../e2e" } ink_e2e_macro = { workspace = true, default-features = true } diff --git a/crates/sandbox/src/lib.rs b/crates/sandbox/src/lib.rs index 2dfaa1e823c..56a7ca4a569 100644 --- a/crates/sandbox/src/lib.rs +++ b/crates/sandbox/src/lib.rs @@ -49,6 +49,7 @@ pub use { }, }, frame_system, + ink_precompiles, pallet_assets, pallet_assets_precompiles, pallet_balances, diff --git a/crates/sandbox/src/macros.rs b/crates/sandbox/src/macros.rs index 6fcce8b4763..bbfe6bffb17 100644 --- a/crates/sandbox/src/macros.rs +++ b/crates/sandbox/src/macros.rs @@ -255,6 +255,7 @@ mod construct_runtime { use $crate::pallet_transaction_payment::{FungibleAdapter}; use $crate::Snapshot; + use $crate::ink_precompiles::erc20; pub type Balance = u128; @@ -386,7 +387,7 @@ mod construct_runtime { type InstantiateOrigin = $crate::frame_system::EnsureSigned; type FindAuthor = (); type Precompiles = ( - $crate::pallet_assets_precompiles::ERC20, TrustBackedAssetsInstance>, + $crate::pallet_assets_precompiles::ERC20, TrustBackedAssetsInstance>, ); type AllowEVMBytecode = ConstBool; type FeeInfo = (); diff --git a/integration-tests/solidity-abi/sol-cross-contract/e2e_tests.rs b/integration-tests/solidity-abi/sol-cross-contract/e2e_tests.rs index 23953813e81..1d07da4e643 100644 --- a/integration-tests/solidity-abi/sol-cross-contract/e2e_tests.rs +++ b/integration-tests/solidity-abi/sol-cross-contract/e2e_tests.rs @@ -3,7 +3,7 @@ use ink_e2e::ContractsRegistry; use ink_sandbox::{ DefaultSandbox, Sandbox, - api::prelude::*, + api::prelude::{BalanceAPI, ContractAPI}, }; use ink::{ @@ -30,7 +30,7 @@ fn call_sol_encoded_message() { .mint_into(&caller.public_key().0.into(), 1_000_000_000_000_000u128) .unwrap_or_else(|_| panic!("Failed to mint tokens")); - sandbox.map_account(origin.clone()).expect("unable to map"); + sandbox.map_account(&DefaultSandbox::default_actor()).expect("unable to map"); // upload other contract (callee) let constructor = other_contract_sol::OtherContractRef::new(false); From 7ed1765cb75c01187bc8e6f2667d76617359d9fd Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Mon, 27 Oct 2025 17:53:18 +0100 Subject: [PATCH 14/33] fmt --- .../solidity-abi/sol-cross-contract/e2e_tests.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/integration-tests/solidity-abi/sol-cross-contract/e2e_tests.rs b/integration-tests/solidity-abi/sol-cross-contract/e2e_tests.rs index 1d07da4e643..3a35774bfd8 100644 --- a/integration-tests/solidity-abi/sol-cross-contract/e2e_tests.rs +++ b/integration-tests/solidity-abi/sol-cross-contract/e2e_tests.rs @@ -3,7 +3,10 @@ use ink_e2e::ContractsRegistry; use ink_sandbox::{ DefaultSandbox, Sandbox, - api::prelude::{BalanceAPI, ContractAPI}, + api::prelude::{ + BalanceAPI, + ContractAPI, + }, }; use ink::{ @@ -30,7 +33,9 @@ fn call_sol_encoded_message() { .mint_into(&caller.public_key().0.into(), 1_000_000_000_000_000u128) .unwrap_or_else(|_| panic!("Failed to mint tokens")); - sandbox.map_account(&DefaultSandbox::default_actor()).expect("unable to map"); + sandbox + .map_account(&DefaultSandbox::default_actor()) + .expect("unable to map"); // upload other contract (callee) let constructor = other_contract_sol::OtherContractRef::new(false); From f695d198f708f9eca563b1b0e88434db0cb768d3 Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Fri, 31 Oct 2025 14:27:24 +0100 Subject: [PATCH 15/33] refactor: copy assert_last_event functionality as in frame --- crates/precompiles/src/erc20.rs | 2 +- crates/sandbox/src/client.rs | 41 +++++++++---------- .../public/assets-precompile/lib.rs | 4 +- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs index a3c87868555..bd6a729103c 100644 --- a/crates/precompiles/src/erc20.rs +++ b/crates/precompiles/src/erc20.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! ERC-20 precompile interface for pallet-assets. +//! ERC-20 precompile interface for `pallet-assets`. //! //! This module provides the standard ERC-20 token interface for interacting with //! assets managed by `pallet-assets` through the precompile mechanism. diff --git a/crates/sandbox/src/client.rs b/crates/sandbox/src/client.rs index d783ef33d6a..3fd8c71f71a 100644 --- a/crates/sandbox/src/client.rs +++ b/crates/sandbox/src/client.rs @@ -774,31 +774,30 @@ pub fn assert_last_contract_event_inner( S::Runtime: pallet_revive::Config, ::RuntimeEvent: TryInto>, - E: Decode + scale::Encode + core::fmt::Debug, + E: Decode + scale::Encode + core::fmt::Debug + std::cmp::PartialEq, { - match client.last_contract_event() { - Some(last_event) => { - if last_event != event.encode().as_slice() { - let decoded = E::decode(&mut &last_event[..]).expect("Decoding failed"); - panic!("{}", assert_message(&decoded, &event)); - } - } - None => panic!("{}", assert_message(&"None", &event)), + let expected_event = event; + let last_event = client + .last_contract_event() + .unwrap_or_else(|| panic!("contract events expected but none were emitted yet")); + + let decoded_event = E::decode(&mut &last_event[..]).unwrap_or_else(|error| { + panic!( + "failed to decode last contract event as {}: bytes={:?}, error={:?}", + core::any::type_name::(), + last_event, + error + ); + }); + + if decoded_event != expected_event { + panic!( + "expected contract event {:?} but found {:?}", + expected_event, decoded_event + ); } } -fn assert_message( - left: &L, - right: &R, -) -> String { - format!( - r#"assertion `left == right` failed - left: {:?} - right: {:?}"#, - left, right - ) -} - impl< AccountId: Clone + Send + Sync + From<[u8; 32]> + AsRef<[u8; 32]>, Config: Sandbox, diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index 45218f3677c..3b1d7d81283 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -127,7 +127,7 @@ mod asset_hub_precompile { } /// Event emitted when allowance by `owner` to `spender` changes. - #[derive(Debug)] + #[derive(Debug, PartialEq)] #[ink::event] pub struct Approval { #[ink(topic)] @@ -138,7 +138,7 @@ mod asset_hub_precompile { } /// Event emitted when transfer of tokens occurs. - #[derive(Debug)] + #[derive(Debug, PartialEq)] #[ink::event] pub struct Transfer { #[ink(topic)] From 47d60153830866d900f748e7da5a5503473fe96b Mon Sep 17 00:00:00 2001 From: Daanvdplas Date: Fri, 31 Oct 2025 18:34:34 +0100 Subject: [PATCH 16/33] fix: ci --- Cargo.lock | 5 +++-- integration-tests/solidity-abi/sol-encoding/e2e_tests.rs | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 523d5d7370d..496c9837924 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3817,7 +3817,7 @@ dependencies = [ [[package]] name = "ink_precompiles" -version = "6.0.0-alpha.4" +version = "6.0.0-beta" dependencies = [ "ink", ] @@ -3915,6 +3915,7 @@ dependencies = [ "ink_e2e", "ink_e2e_macro", "ink_env", + "ink_precompiles", "ink_primitives 6.0.0-beta", "ink_revive_types", "jsonrpsee", @@ -4781,7 +4782,7 @@ dependencies = [ "log", "parity-scale-codec", "scale-info", - "sp-core 28.0.0 (git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0)", + "sp-core 28.0.0", "sp-runtime 31.0.1", ] diff --git a/integration-tests/solidity-abi/sol-encoding/e2e_tests.rs b/integration-tests/solidity-abi/sol-encoding/e2e_tests.rs index 1b9187cb305..e189a1b3cd1 100644 --- a/integration-tests/solidity-abi/sol-encoding/e2e_tests.rs +++ b/integration-tests/solidity-abi/sol-encoding/e2e_tests.rs @@ -9,7 +9,10 @@ use ink_revive_types::ExecReturnValue; use ink_sandbox::{ DefaultSandbox, Sandbox, - api::prelude::*, + api::prelude::{ + BalanceAPI, + ContractAPI, + }, frame_system::pallet_prelude::OriginFor, }; @@ -29,7 +32,9 @@ fn call_solidity_encoded_message() { .mint_into(&caller.public_key().0.into(), 1_000_000_000_000_000u128) .unwrap_or_else(|_| panic!("Failed to mint tokens")); - sandbox.map_account(origin.clone()).expect("unable to map"); + sandbox + .map_account(&DefaultSandbox::default_actor()) + .expect("unable to map"); let constructor = SolEncodingRef::new(false); let params = constructor From bdcf5f0c102d58e4abd51da1a349dc84f17d91d9 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Sun, 9 Nov 2025 21:03:33 +0100 Subject: [PATCH 17/33] Revert `pub` --- crates/ink/macro/src/contract_ref.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ink/macro/src/contract_ref.rs b/crates/ink/macro/src/contract_ref.rs index 3e3bd6f04c6..56265a40aaa 100644 --- a/crates/ink/macro/src/contract_ref.rs +++ b/crates/ink/macro/src/contract_ref.rs @@ -68,7 +68,7 @@ pub fn analyze_or_err( #trait_def_impl // Type alias for contract ref. - pub type #contract_ref_name = + type #contract_ref_name = <<::ink::reflect::TraitDefinitionRegistry<#env> as #trait_name> ::__ink_TraitInfo as ::ink::codegen::TraitCallForwarder>::Forwarder<#abi_ty>; )) From a4fc09b18c9d3242f7a169513956433f8f325d75 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Sun, 9 Nov 2025 21:07:36 +0100 Subject: [PATCH 18/33] Fix links and doc comment --- crates/precompiles/src/erc20.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs index bd6a729103c..d3180604feb 100644 --- a/crates/precompiles/src/erc20.rs +++ b/crates/precompiles/src/erc20.rs @@ -15,11 +15,12 @@ //! ERC-20 precompile interface for `pallet-assets`. //! //! This module provides the standard ERC-20 token interface for interacting with -//! assets managed by `pallet-assets` through the precompile mechanism. +//! assets managed by `pallet-assets`. //! //! # References //! -//! - [Polkadot SDK Assets Precompile](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/assets/src/precompiles.rs) +//! - [Polkadot SDK Assets Precompile](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/frame/assets/precompiles/src/lib.rs) +//! - [Polkadot SDK Assets Precompile Solidity Interface](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/primitives/ethereum-standards/src/IERC20.sol) //! - [ERC-20 Token Standard](https://eips.ethereum.org/EIPS/eip-20) use ink::{ From 8bdff3dd112ac68ce8fe60fb14c3a1b69635efd8 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Sun, 9 Nov 2025 21:26:51 +0100 Subject: [PATCH 19/33] Revert "Revert `pub`" This reverts commit bdcf5f0c102d58e4abd51da1a349dc84f17d91d9. --- crates/ink/macro/src/contract_ref.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ink/macro/src/contract_ref.rs b/crates/ink/macro/src/contract_ref.rs index 56265a40aaa..3e3bd6f04c6 100644 --- a/crates/ink/macro/src/contract_ref.rs +++ b/crates/ink/macro/src/contract_ref.rs @@ -68,7 +68,7 @@ pub fn analyze_or_err( #trait_def_impl // Type alias for contract ref. - type #contract_ref_name = + pub type #contract_ref_name = <<::ink::reflect::TraitDefinitionRegistry<#env> as #trait_name> ::__ink_TraitInfo as ::ink::codegen::TraitCallForwarder>::Forwarder<#abi_ty>; )) From a0df1c2bb6bb7c4bc5fbfd5533de105f311dfe70 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Sun, 9 Nov 2025 22:14:34 +0100 Subject: [PATCH 20/33] Update lockfile --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 07a515f327a..5373bf939b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2306,9 +2306,9 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "duct" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7478638a31d1f1f3d6c9f5e57c76b906a04ac4879d6fd0fb6245bc88f73fd0b" +checksum = "7e66e9c0c03d094e1a0ba1be130b849034aa80c3a2ab8ee94316bc809f3fa684" dependencies = [ "libc", "os_pipe", @@ -4865,7 +4865,7 @@ dependencies = [ [[package]] name = "pallet-assets" version = "29.1.0" -source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=9136565addc23a552f6960a7581f13c8dfc651f1#9136565addc23a552f6960a7581f13c8dfc651f1" dependencies = [ "frame-benchmarking", "frame-support 28.0.0", @@ -4881,7 +4881,7 @@ dependencies = [ [[package]] name = "pallet-assets-precompiles" version = "0.1.0" -source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=cbab8ed4be1941420dd25dc81102fb79d8e2a7f0#cbab8ed4be1941420dd25dc81102fb79d8e2a7f0" +source = "git+https://github.com/use-ink/polkadot-sdk.git?rev=9136565addc23a552f6960a7581f13c8dfc651f1#9136565addc23a552f6960a7581f13c8dfc651f1" dependencies = [ "ethereum-standards", "frame-support 28.0.0", From f13b8a892407e54c8296c8cb25ac28dd2ca1db10 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Mon, 10 Nov 2025 12:22:11 +0100 Subject: [PATCH 21/33] Add trust-backed + pool-assets config to `Environment` --- crates/ink/codegen/src/generator/env.rs | 4 +- .../ui/contract/pass/config-custom-env.rs | 2 + crates/metadata/src/specs.rs | 219 ++++++++++++++++-- crates/metadata/src/tests.rs | 10 +- crates/precompiles/src/erc20.rs | 7 +- crates/primitives/src/types.rs | 13 ++ crates/sandbox/src/macros.rs | 3 +- .../public/assets-precompile/lib.rs | 5 +- .../public/custom-environment/lib.rs | 4 + 9 files changed, 235 insertions(+), 32 deletions(-) diff --git a/crates/ink/codegen/src/generator/env.rs b/crates/ink/codegen/src/generator/env.rs index ae94989f92d..7bbcf8bb7f4 100644 --- a/crates/ink/codegen/src/generator/env.rs +++ b/crates/ink/codegen/src/generator/env.rs @@ -39,8 +39,10 @@ impl GenerateCode for Env<'_> { type Hash = <<#storage_ident as ::ink::env::ContractEnv>::Env as ::ink::env::Environment>::Hash; type Timestamp = <<#storage_ident as ::ink::env::ContractEnv>::Env as ::ink::env::Environment>::Timestamp; type BlockNumber = <<#storage_ident as ::ink::env::ContractEnv>::Env as ::ink::env::Environment>::BlockNumber; - const NATIVE_TO_ETH_RATIO: u32 = <<#storage_ident as ::ink::env::ContractEnv>::Env as ::ink::env::Environment>::NATIVE_TO_ETH_RATIO; type EventRecord = <<#storage_ident as ::ink::env::ContractEnv>::Env as ::ink::env::Environment>::EventRecord; + const NATIVE_TO_ETH_RATIO: u32 = <<#storage_ident as ::ink::env::ContractEnv>::Env as ::ink::env::Environment>::NATIVE_TO_ETH_RATIO; + const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = <<#storage_ident as ::ink::env::ContractEnv>::Env as ::ink::env::Environment>::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX; + const POOL_ASSETS_PRECOMPILE_INDEX: u16 = <<#storage_ident as ::ink::env::ContractEnv>::Env as ::ink::env::Environment>::POOL_ASSETS_PRECOMPILE_INDEX; type Address = ::ink::primitives::Address; } diff --git a/crates/ink/tests/ui/contract/pass/config-custom-env.rs b/crates/ink/tests/ui/contract/pass/config-custom-env.rs index 64281f4f93c..4eb3343a761 100644 --- a/crates/ink/tests/ui/contract/pass/config-custom-env.rs +++ b/crates/ink/tests/ui/contract/pass/config-custom-env.rs @@ -5,6 +5,8 @@ pub struct CustomEnv; impl ink_env::Environment for CustomEnv { const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; + const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; + const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; type AccountId = [u8; 32]; type Balance = u64; type Hash = [u8; 32]; diff --git a/crates/metadata/src/specs.rs b/crates/metadata/src/specs.rs index 3e8d1917675..4debd34a6b9 100644 --- a/crates/metadata/src/specs.rs +++ b/crates/metadata/src/specs.rs @@ -657,6 +657,11 @@ mod state { pub struct BlockNumber; /// Type state for the native to eth ratio specified in the environment. pub struct NativeToEthRatio; + /// Type state for the trust backed assets precompile index specified in the + /// environment. + pub struct TrustBackedAssetsPrecompileIndex; + /// Type state for the pool assets precompile index specified in the environment. + pub struct PoolAssetsPrecompileIndex; /// Type state for the size of the static buffer configured via environment variable.` pub struct BufferSize; } @@ -1549,6 +1554,8 @@ where timestamp: TypeSpec, block_number: TypeSpec, native_to_eth_ratio: u32, + trust_backed_assets_precompile_index: u16, + pool_assets_precompile_index: u16, static_buffer_size: usize, } @@ -1565,6 +1572,8 @@ where timestamp: Default::default(), block_number: Default::default(), native_to_eth_ratio: Default::default(), + trust_backed_assets_precompile_index: Default::default(), + pool_assets_precompile_index: Default::default(), static_buffer_size: Default::default(), } } @@ -1581,6 +1590,9 @@ impl IntoPortable for EnvironmentSpec { timestamp: self.timestamp.into_portable(registry), block_number: self.block_number.into_portable(registry), native_to_eth_ratio: self.native_to_eth_ratio, + trust_backed_assets_precompile_index: self + .trust_backed_assets_precompile_index, + pool_assets_precompile_index: self.pool_assets_precompile_index, static_buffer_size: self.static_buffer_size, } } @@ -1615,6 +1627,14 @@ where pub fn native_to_eth_ratio(&self) -> u32 { self.native_to_eth_ratio } + /// Returns the `TRUST_BACKED_ASSETS_PRECOMPILE_INDEX` value of the environment. + pub fn trust_backed_assets_precompile_index(&self) -> u16 { + self.trust_backed_assets_precompile_index + } + /// Returns the `POOL_ASSETS_PRECOMPILE_INDEX` value of the environment. + pub fn pool_assets_precompile_index(&self) -> u16 { + self.pool_assets_precompile_index + } } #[allow(clippy::type_complexity)] @@ -1632,6 +1652,8 @@ where Missing, Missing, Missing, + Missing, + Missing, Missing, > { EnvironmentSpecBuilder { @@ -1644,18 +1666,29 @@ where /// An environment specification builder. #[allow(clippy::type_complexity)] #[must_use] -pub struct EnvironmentSpecBuilder +pub struct EnvironmentSpecBuilder where F: Form, TypeSpec: Default, EnvironmentSpec: Default, { spec: EnvironmentSpec, - marker: PhantomData (A, B, H, T, BN, NTER, BS)>, + marker: PhantomData (A, B, H, T, BN, NTER, TBAPI, PAPI, BS)>, } -impl - EnvironmentSpecBuilder, B, H, T, BN, NTER, BS> +impl + EnvironmentSpecBuilder< + F, + Missing, + B, + H, + T, + BN, + NTER, + TBAPI, + PAPI, + BS, + > where F: Form, TypeSpec: Default, @@ -1665,7 +1698,8 @@ where pub fn account_id( self, account_id: TypeSpec, - ) -> EnvironmentSpecBuilder { + ) -> EnvironmentSpecBuilder + { EnvironmentSpecBuilder { spec: EnvironmentSpec { account_id, @@ -1676,8 +1710,8 @@ where } } -impl - EnvironmentSpecBuilder, H, T, BN, NTER, BS> +impl + EnvironmentSpecBuilder, H, T, BN, NTER, TBAPI, PAPI, BS> where F: Form, TypeSpec: Default, @@ -1687,7 +1721,8 @@ where pub fn balance( self, balance: TypeSpec, - ) -> EnvironmentSpecBuilder { + ) -> EnvironmentSpecBuilder + { EnvironmentSpecBuilder { spec: EnvironmentSpec { balance, @@ -1698,8 +1733,8 @@ where } } -impl - EnvironmentSpecBuilder, T, BN, NTER, BS> +impl + EnvironmentSpecBuilder, T, BN, NTER, TBAPI, PAPI, BS> where F: Form, TypeSpec: Default, @@ -1709,7 +1744,7 @@ where pub fn hash( self, hash: TypeSpec, - ) -> EnvironmentSpecBuilder { + ) -> EnvironmentSpecBuilder { EnvironmentSpecBuilder { spec: EnvironmentSpec { hash, ..self.spec }, marker: PhantomData, @@ -1717,8 +1752,19 @@ where } } -impl - EnvironmentSpecBuilder, BN, NTER, BS> +impl + EnvironmentSpecBuilder< + F, + A, + B, + H, + Missing, + BN, + NTER, + TBAPI, + PAPI, + BS, + > where F: Form, TypeSpec: Default, @@ -1728,7 +1774,8 @@ where pub fn timestamp( self, timestamp: TypeSpec, - ) -> EnvironmentSpecBuilder { + ) -> EnvironmentSpecBuilder + { EnvironmentSpecBuilder { spec: EnvironmentSpec { timestamp, @@ -1739,8 +1786,19 @@ where } } -impl - EnvironmentSpecBuilder, NTER, BS> +impl + EnvironmentSpecBuilder< + F, + A, + B, + H, + T, + Missing, + NTER, + TBAPI, + PAPI, + BS, + > where F: Form, TypeSpec: Default, @@ -1750,7 +1808,8 @@ where pub fn block_number( self, block_number: TypeSpec, - ) -> EnvironmentSpecBuilder { + ) -> EnvironmentSpecBuilder + { EnvironmentSpecBuilder { spec: EnvironmentSpec { block_number, @@ -1761,8 +1820,19 @@ where } } -impl - EnvironmentSpecBuilder, BS> +impl + EnvironmentSpecBuilder< + F, + A, + B, + H, + T, + BN, + Missing, + PBAPI, + PAPI, + BS, + > where F: Form, TypeSpec: Default, @@ -1772,7 +1842,8 @@ where pub fn native_to_eth_ratio( self, native_to_eth_ratio: u32, - ) -> EnvironmentSpecBuilder { + ) -> EnvironmentSpecBuilder + { EnvironmentSpecBuilder { spec: EnvironmentSpec { native_to_eth_ratio, @@ -1783,8 +1854,107 @@ where } } -impl - EnvironmentSpecBuilder> +impl + EnvironmentSpecBuilder< + F, + A, + B, + H, + T, + BN, + NTER, + Missing, + PAPI, + BS, + > +where + F: Form, + TypeSpec: Default, + EnvironmentSpec: Default, +{ + /// Sets the `TRUST_BACKED_ASSETS_PRECOMPILE_INDEX` value of the environment. + pub fn trust_backed_assets_precompile_index( + self, + trust_backed_assets_precompile_index: u16, + ) -> EnvironmentSpecBuilder< + F, + A, + B, + H, + T, + BN, + NTER, + state::TrustBackedAssetsPrecompileIndex, + PAPI, + BS, + > { + EnvironmentSpecBuilder { + spec: EnvironmentSpec { + trust_backed_assets_precompile_index, + ..self.spec + }, + marker: PhantomData, + } + } +} + +impl + EnvironmentSpecBuilder< + F, + A, + B, + H, + T, + BN, + NTER, + TBAPI, + Missing, + BS, + > +where + F: Form, + TypeSpec: Default, + EnvironmentSpec: Default, +{ + /// Sets the `POOL_ASSETS_PRECOMPILE_INDEX` value of the environment. + pub fn pool_assets_precompile_index( + self, + pool_assets_precompile_index: u16, + ) -> EnvironmentSpecBuilder< + F, + A, + B, + H, + T, + BN, + NTER, + TBAPI, + state::PoolAssetsPrecompileIndex, + BS, + > { + EnvironmentSpecBuilder { + spec: EnvironmentSpec { + pool_assets_precompile_index, + ..self.spec + }, + marker: PhantomData, + } + } +} + +impl + EnvironmentSpecBuilder< + F, + A, + B, + H, + T, + BN, + NTER, + TBAPI, + PAPI, + Missing, + > where F: Form, TypeSpec: Default, @@ -1794,7 +1964,8 @@ where pub fn static_buffer_size( self, static_buffer_size: usize, - ) -> EnvironmentSpecBuilder { + ) -> EnvironmentSpecBuilder + { EnvironmentSpecBuilder { spec: EnvironmentSpec { static_buffer_size, @@ -1814,6 +1985,8 @@ impl state::Timestamp, state::BlockNumber, state::NativeToEthRatio, + state::TrustBackedAssetsPrecompileIndex, + state::PoolAssetsPrecompileIndex, state::BufferSize, > where diff --git a/crates/metadata/src/tests.rs b/crates/metadata/src/tests.rs index 29ec7e01daf..cb22fd942be 100644 --- a/crates/metadata/src/tests.rs +++ b/crates/metadata/src/tests.rs @@ -202,6 +202,8 @@ fn spec_contract_event_definition_signature_topic_collision() { const SIGNATURE_TOPIC: Option<[u8; 32]> = Some([42u8; 32]); const BUFFER_SIZE: usize = 1 << 14; const NATIVE_TO_ETH_RATIO: u32 = 100000000; + const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; + const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; ContractSpec::new() .constructors(vec![ @@ -271,6 +273,10 @@ fn spec_contract_event_definition_signature_topic_collision() { .timestamp(TypeSpec::of_type::()) .block_number(TypeSpec::of_type::()) .native_to_eth_ratio(NATIVE_TO_ETH_RATIO) + .trust_backed_assets_precompile_index( + TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, + ) + .pool_assets_precompile_index(POOL_ASSETS_PRECOMPILE_INDEX) .static_buffer_size(BUFFER_SIZE) .done(), ) @@ -284,8 +290,10 @@ fn spec_contract_json() { type Hash = ink_primitives::Hash; type Timestamp = u64; type BlockNumber = u128; - const NATIVE_TO_ETH_RATIO: u32 = 100000000; const BUFFER_SIZE: usize = 1 << 14; + const NATIVE_TO_ETH_RATIO: u32 = 100000000; + const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; + const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; // given let contract: ContractSpec = ContractSpec::new() diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs index d3180604feb..8de487caeff 100644 --- a/crates/precompiles/src/erc20.rs +++ b/crates/precompiles/src/erc20.rs @@ -28,9 +28,6 @@ use ink::{ U256, }; -/// ERC-20 Assets precompile index. -pub const PRECOMPILE_INDEX: u16 = 0x0120; - /// Type alias for asset IDs. pub type AssetId = u32; @@ -155,8 +152,8 @@ pub trait Erc20 { /// let erc20_ref = erc20(asset_id); /// let balance = erc20_ref.balanceOf(account); /// ``` -pub fn erc20(asset_id: AssetId) -> Erc20Ref { - let address = crate::prefixed_address(PRECOMPILE_INDEX, asset_id); +pub fn erc20(precompile_index: u16, asset_id: AssetId) -> Erc20Ref { + let address = crate::prefixed_address(precompile_index, asset_id); address.into() } diff --git a/crates/primitives/src/types.rs b/crates/primitives/src/types.rs index 86a890adb49..8ba31f45f21 100644 --- a/crates/primitives/src/types.rs +++ b/crates/primitives/src/types.rs @@ -306,6 +306,14 @@ pub trait Environment: Clone { /// and the ETH token. const NATIVE_TO_ETH_RATIO: u32; + /// ERC-20 Assets precompile index, for trust backed assets + /// (i.e. trust comes from the issuer). + const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16; + + /// ERC-20 Assets precompile index, for pool assets + /// (i.e. trust comes from on-chain collateral accounting). + const POOL_ASSETS_PRECOMPILE_INDEX: u16; + /// The account id type. type AccountId: 'static + scale::Codec @@ -396,6 +404,11 @@ impl Environment for DefaultEnvironment { // `ink_sandbox` and the `ink-node`. const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; + // These const's correspond to the settings of Asset Hub and + // `ink-node`. + const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; + const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; + type AccountId = AccountId; type Balance = Balance; type Hash = Hash; diff --git a/crates/sandbox/src/macros.rs b/crates/sandbox/src/macros.rs index 32217aa6fa4..8e1404a4631 100644 --- a/crates/sandbox/src/macros.rs +++ b/crates/sandbox/src/macros.rs @@ -387,7 +387,8 @@ mod construct_runtime { type InstantiateOrigin = $crate::frame_system::EnsureSigned; type FindAuthor = (); type Precompiles = ( - $crate::pallet_assets_precompiles::ERC20, TrustBackedAssetsInstance>, + $crate::pallet_assets_precompiles::ERC20, TrustBackedAssetsInstance>, + // todo add `PoolAssetsInstance` ); type AllowEVMBytecode = ConstBool; type FeeInfo = (); diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index 3b1d7d81283..c449367ef4e 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -37,7 +37,10 @@ mod asset_hub_precompile { Self { asset_id, owner: Self::env().caller(), - precompile: erc20(asset_id), + precompile: erc20( + ink::env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, + asset_id, + ), } } diff --git a/integration-tests/public/custom-environment/lib.rs b/integration-tests/public/custom-environment/lib.rs index da6305353fe..7a15d3c452e 100644 --- a/integration-tests/public/custom-environment/lib.rs +++ b/integration-tests/public/custom-environment/lib.rs @@ -14,6 +14,10 @@ pub enum EnvironmentWithManyTopics {} impl Environment for EnvironmentWithManyTopics { const NATIVE_TO_ETH_RATIO: u32 = ::NATIVE_TO_ETH_RATIO; + const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u32 = + ::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX; + const POOL_ASSETS_PRECOMPILE_INDEX: u32 = + ::POOL_ASSETS_PRECOMPILE_INDEX; type AccountId = ::AccountId; type Balance = ::Balance; From 1627411fe82c693842152d2f01b10b687e287f7d Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Mon, 10 Nov 2025 12:45:15 +0100 Subject: [PATCH 22/33] Add missing env props --- crates/ink/codegen/src/generator/metadata.rs | 4 ++++ crates/metadata/src/tests.rs | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/crates/ink/codegen/src/generator/metadata.rs b/crates/ink/codegen/src/generator/metadata.rs index 185fb3c22ce..459cb711ea6 100644 --- a/crates/ink/codegen/src/generator/metadata.rs +++ b/crates/ink/codegen/src/generator/metadata.rs @@ -358,6 +358,10 @@ impl Metadata<'_> { .timestamp(#timestamp) .block_number(#block_number) .native_to_eth_ratio(NATIVE_TO_ETH_RATIO) + .trust_backed_assets_precompile_index( + TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, + ) + .pool_assets_precompile_index(POOL_ASSETS_PRECOMPILE_INDEX) .static_buffer_size(#buffer_size_const) .done() ) diff --git a/crates/metadata/src/tests.rs b/crates/metadata/src/tests.rs index cb22fd942be..e94f75e0409 100644 --- a/crates/metadata/src/tests.rs +++ b/crates/metadata/src/tests.rs @@ -408,6 +408,10 @@ fn spec_contract_json() { ), )) .native_to_eth_ratio(NATIVE_TO_ETH_RATIO) + .trust_backed_assets_precompile_index( + TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, + ) + .pool_assets_precompile_index(POOL_ASSETS_PRECOMPILE_INDEX) .static_buffer_size(BUFFER_SIZE) .done(), ) @@ -731,6 +735,8 @@ fn environment_spec() -> EnvironmentSpec { .block_number(Default::default()) .static_buffer_size(16384) .native_to_eth_ratio(100_000_000) + .trust_backed_assets_precompile_index(0x0120) + .pool_assets_precompile_index(0x0320) .done() } From a8ea0e31463f7cd57bd3744cab3170971d25559c Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Mon, 10 Nov 2025 14:56:41 +0100 Subject: [PATCH 23/33] Fix config --- crates/sandbox/src/macros.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/sandbox/src/macros.rs b/crates/sandbox/src/macros.rs index 8e1404a4631..94528b747da 100644 --- a/crates/sandbox/src/macros.rs +++ b/crates/sandbox/src/macros.rs @@ -255,7 +255,6 @@ mod construct_runtime { use $crate::pallet_transaction_payment::{FungibleAdapter}; use $crate::Snapshot; - use $crate::ink_precompiles::erc20; pub type Balance = u128; @@ -387,7 +386,7 @@ mod construct_runtime { type InstantiateOrigin = $crate::frame_system::EnsureSigned; type FindAuthor = (); type Precompiles = ( - $crate::pallet_assets_precompiles::ERC20, TrustBackedAssetsInstance>, + $crate::pallet_assets_precompiles::ERC20, TrustBackedAssetsInstance>, // todo add `PoolAssetsInstance` ); type AllowEVMBytecode = ConstBool; From f5e170467fb249295ccac4554951d31d4958b4d8 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Mon, 10 Nov 2025 15:57:15 +0100 Subject: [PATCH 24/33] Fix tests --- crates/precompiles/src/erc20.rs | 11 +++++++++-- crates/primitives/src/contract.rs | 4 ++++ integration-tests/public/assets-precompile/lib.rs | 5 +---- integration-tests/public/custom-environment/lib.rs | 4 ++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs index 8de487caeff..5aaadce76e0 100644 --- a/crates/precompiles/src/erc20.rs +++ b/crates/precompiles/src/erc20.rs @@ -160,6 +160,7 @@ pub fn erc20(precompile_index: u16, asset_id: AssetId) -> Erc20Ref { #[cfg(test)] mod tests { use super::*; + use ink::env::Environment; #[test] fn erc20_precompile_address_format() { @@ -169,7 +170,10 @@ mod tests { 0x00, 0x00, 0x00, 0x01, 0x20, 0x00, 0x00, ]; - let address = crate::prefixed_address(PRECOMPILE_INDEX, 1); + let address = crate::prefixed_address( + ink::env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, + 1, + ); let address_bytes: [u8; 20] = address.into(); assert_eq!(address_bytes, expected); @@ -178,7 +182,10 @@ mod tests { #[test] fn erc20_precompile_address_for_multiple_assets() { // Test asset ID 42 - let address_42 = crate::prefixed_address(PRECOMPILE_INDEX, 42); + let address_42 = crate::prefixed_address( + ink::env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, + 42, + ); let bytes_42: [u8; 20] = address_42.into(); // First 4 bytes should be asset ID (42 = 0x0000002a) diff --git a/crates/primitives/src/contract.rs b/crates/primitives/src/contract.rs index 5e38e5dc40c..36390eb79e7 100644 --- a/crates/primitives/src/contract.rs +++ b/crates/primitives/src/contract.rs @@ -68,6 +68,10 @@ use crate::types::Environment; /// impl Environment for CustomEnvironment { /// const NATIVE_TO_ETH_RATIO: u32 = /// ::NATIVE_TO_ETH_RATIO; +/// const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = +/// ::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX; +/// const POOL_ASSETS_PRECOMPILE_INDEX: u16 = +/// ::POOL_ASSETS_PRECOMPILE_INDEX; /// /// type AccountId = ::AccountId; /// type Balance = u64; diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index c449367ef4e..bc37c157ef3 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -37,10 +37,7 @@ mod asset_hub_precompile { Self { asset_id, owner: Self::env().caller(), - precompile: erc20( - ink::env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, - asset_id, - ), + precompile: erc20(TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, asset_id), } } diff --git a/integration-tests/public/custom-environment/lib.rs b/integration-tests/public/custom-environment/lib.rs index 7a15d3c452e..495ffe5ed68 100644 --- a/integration-tests/public/custom-environment/lib.rs +++ b/integration-tests/public/custom-environment/lib.rs @@ -14,9 +14,9 @@ pub enum EnvironmentWithManyTopics {} impl Environment for EnvironmentWithManyTopics { const NATIVE_TO_ETH_RATIO: u32 = ::NATIVE_TO_ETH_RATIO; - const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u32 = + const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = ::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX; - const POOL_ASSETS_PRECOMPILE_INDEX: u32 = + const POOL_ASSETS_PRECOMPILE_INDEX: u16 = ::POOL_ASSETS_PRECOMPILE_INDEX; type AccountId = ::AccountId; From d2c3c436c0a5f0ce15ba2407d36a82bb346c5410 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Mon, 10 Nov 2025 16:03:57 +0100 Subject: [PATCH 25/33] Reexport `ink_precompiles` --- crates/ink/Cargo.toml | 1 + crates/ink/src/lib.rs | 1 + crates/precompiles/Cargo.toml | 6 +++--- crates/precompiles/src/erc20.rs | 4 ++-- crates/precompiles/src/lib.rs | 10 ++++++---- integration-tests/public/assets-precompile/lib.rs | 4 ++-- 6 files changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/ink/Cargo.toml b/crates/ink/Cargo.toml index 0704d4aff42..bc0348b303b 100644 --- a/crates/ink/Cargo.toml +++ b/crates/ink/Cargo.toml @@ -19,6 +19,7 @@ include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"] ink_env = { workspace = true } ink_storage = { workspace = true } ink_primitives = { workspace = true } +ink_precompiles = { workspace = true } ink_metadata = { workspace = true, optional = true } ink_prelude = { workspace = true } ink_macro = { workspace = true } diff --git a/crates/ink/src/lib.rs b/crates/ink/src/lib.rs index d5b20b81e94..01504f7791f 100644 --- a/crates/ink/src/lib.rs +++ b/crates/ink/src/lib.rs @@ -40,6 +40,7 @@ pub use ink_env as env; #[cfg(feature = "std")] pub use ink_metadata as metadata; pub use ink_prelude as prelude; +pub use ink_precompiles as precompiles; pub use ink_primitives as primitives; pub use ink_primitives::abi; pub use scale; diff --git a/crates/precompiles/Cargo.toml b/crates/precompiles/Cargo.toml index 5f9b5f83470..257b48ad533 100644 --- a/crates/precompiles/Cargo.toml +++ b/crates/precompiles/Cargo.toml @@ -4,18 +4,18 @@ version.workspace = true authors = ["Use Ink "] edition.workspace = true license.workspace = true -description = "[ink!] Precompile interfaces for pallet-revive smart contracts." +description = "[ink!] Precompile interfaces for Polkadot SDK `pallet-revive` smart contracts." repository.workspace = true homepage.workspace = true keywords.workspace = true categories.workspace = true [dependencies] -ink = { workspace = true, default-features = false } +ink_primitives = { workspace = true, default-features = false } [features] default = ["std"] std = [ - "ink/std", + "ink_primitives/std", ] diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs index 5aaadce76e0..be198cd2c83 100644 --- a/crates/precompiles/src/erc20.rs +++ b/crates/precompiles/src/erc20.rs @@ -23,7 +23,7 @@ //! - [Polkadot SDK Assets Precompile Solidity Interface](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/primitives/ethereum-standards/src/IERC20.sol) //! - [ERC-20 Token Standard](https://eips.ethereum.org/EIPS/eip-20) -use ink::{ +use ink_primitives::{ Address, U256, }; @@ -146,7 +146,7 @@ pub trait Erc20 { /// # Example /// /// ```rust,ignore -/// use ink_precompiles::erc20::erc20; +/// use ink::precompiles::erc20::erc20; /// /// let asset_id = 1; /// let erc20_ref = erc20(asset_id); diff --git a/crates/precompiles/src/lib.rs b/crates/precompiles/src/lib.rs index dc9af353829..ec80e8c05a5 100644 --- a/crates/precompiles/src/lib.rs +++ b/crates/precompiles/src/lib.rs @@ -18,6 +18,8 @@ )] #![cfg_attr(not(feature = "std"), no_std)] +use ink_primitives::Address; + pub mod erc20; /// Calculates the address of a precompile at index `n`. @@ -28,7 +30,7 @@ pub mod erc20; /// # Arguments /// * `n` - The precompile index (e.g., `0x0120` for ERC20 Assets precompile) #[inline] -pub fn fixed_address(n: u16) -> ink::Address { +pub fn fixed_address(n: u16) -> Address { let shifted = (n as u32) << 16; let suffix = shifted.to_be_bytes(); @@ -38,7 +40,7 @@ pub fn fixed_address(n: u16) -> ink::Address { address[i] = suffix[i - 16]; i = i + 1; } - ink::Address::from(address) + Address::from(address) } /// Calculates the address of a precompile at index `n` with an additional prefix. @@ -53,11 +55,11 @@ pub fn fixed_address(n: u16) -> ink::Address { /// * `n` - The precompile index (e.g., `0x0120` for ERC20 Assets precompile) /// * `prefix` - A 32-bit value to encode in the first 4 bytes (e.g., asset ID) #[inline] -pub fn prefixed_address(n: u16, prefix: u32) -> ink::Address { +pub fn prefixed_address(n: u16, prefix: u32) -> Address { let address = fixed_address(n); let mut address_bytes: [u8; 20] = address.into(); address_bytes[..4].copy_from_slice(&prefix.to_be_bytes()); - ink::Address::from(address_bytes) + Address::from(address_bytes) } #[cfg(test)] diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index bc37c157ef3..ee6511f4931 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -5,7 +5,7 @@ use ink::{ U256, prelude::string::ToString, }; -pub use ink_precompiles::erc20::{ +pub use ink::precompiles::erc20::{ AssetId, erc20, }; @@ -14,7 +14,7 @@ pub use ink_precompiles::erc20::{ mod asset_hub_precompile { use super::*; use ink::prelude::string::String; - use ink_precompiles::erc20::{ + use ink::precompiles::erc20::{ Erc20, Erc20Ref, }; From 418d789d480b4ee9f5de9b5a0f32a0851f794b37 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Mon, 10 Nov 2025 18:02:27 +0100 Subject: [PATCH 26/33] Reexport `ink_precompiles` from `ink` --- Cargo.lock | 3 ++ crates/precompiles/Cargo.toml | 8 ++++++ crates/precompiles/src/erc20.rs | 49 ++++++++++++--------------------- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5373bf939b9..c78f2c851c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3684,6 +3684,7 @@ dependencies = [ "ink_ir", "ink_macro", "ink_metadata 6.0.0-beta", + "ink_precompiles", "ink_prelude 6.0.0-beta", "ink_primitives 6.0.0-beta", "ink_storage", @@ -3898,6 +3899,8 @@ name = "ink_precompiles" version = "6.0.0-beta" dependencies = [ "ink", + "ink_env", + "ink_primitives 6.0.0-beta", ] [[package]] diff --git a/crates/precompiles/Cargo.toml b/crates/precompiles/Cargo.toml index 257b48ad533..2ebf0110475 100644 --- a/crates/precompiles/Cargo.toml +++ b/crates/precompiles/Cargo.toml @@ -12,10 +12,18 @@ categories.workspace = true [dependencies] ink_primitives = { workspace = true, default-features = false } +ink_env = { workspace = true, default-features = false } +# Note: The contract_ref macro is applied in the ink crate to avoid circular dependencies +# This crate only defines the plain trait + +[dev-dependencies] +# Only used in tests +ink = { workspace = true } [features] default = ["std"] std = [ "ink_primitives/std", + "ink_env/std", ] diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs index be198cd2c83..2efaffa78b6 100644 --- a/crates/precompiles/src/erc20.rs +++ b/crates/precompiles/src/erc20.rs @@ -23,6 +23,9 @@ //! - [Polkadot SDK Assets Precompile Solidity Interface](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/primitives/ethereum-standards/src/IERC20.sol) //! - [ERC-20 Token Standard](https://eips.ethereum.org/EIPS/eip-20) +// Note: When ink_precompiles is used standalone (not through the ink facade), +// users need to have ink_env and related crates in scope. When used through +// `ink::precompiles`, the ink crate provides all necessary items. use ink_primitives::{ Address, U256, @@ -32,7 +35,15 @@ use ink_primitives::{ pub type AssetId = u32; /// Defines the ERC-20 interface of the Asset Hub precompile. -#[ink::contract_ref(abi = "sol")] +/// +/// **Note:** To use this trait, it should be consumed through the `ink` crate which +/// applies the `#[ink::contract_ref]` macro and generates the necessary contract +/// reference types (`Erc20Ref`). The macro is applied in the `ink` crate to avoid +/// circular dependency issues. +/// +/// # Solidity ABI +/// +/// This trait uses Solidity ABI for its methods. pub trait Erc20 { /// Returns the total supply of tokens. /// @@ -41,7 +52,6 @@ pub trait Erc20 { /// ```solidity /// function totalSupply() external view returns (uint256); /// ``` - #[ink(message)] #[allow(non_snake_case)] fn totalSupply(&self) -> U256; @@ -55,7 +65,6 @@ pub trait Erc20 { /// ```solidity /// function balanceOf(address account) external view returns (uint256); /// ``` - #[ink(message)] #[allow(non_snake_case)] fn balanceOf(&self, account: Address) -> U256; @@ -74,7 +83,6 @@ pub trait Erc20 { /// ```solidity /// function transfer(address to, uint256 value) external returns (bool); /// ``` - #[ink(message)] fn transfer(&mut self, to: Address, value: U256) -> bool; /// Returns the allowance for a spender on behalf of an owner. @@ -90,7 +98,6 @@ pub trait Erc20 { /// ```solidity /// function allowance(address owner, address spender) external view returns (uint256); /// ``` - #[ink(message)] fn allowance(&self, owner: Address, spender: Address) -> U256; /// Approves a spender to spend tokens on behalf of the caller. @@ -108,7 +115,6 @@ pub trait Erc20 { /// ```solidity /// function approve(address spender, uint256 value) external returns (bool); /// ``` - #[ink(message)] fn approve(&mut self, spender: Address, value: U256) -> bool; /// Transfers tokens from one account to another using allowance. @@ -129,38 +135,17 @@ pub trait Erc20 { /// ```solidity /// function transferFrom(address from, address to, uint256 value) external returns (bool); /// ``` - #[ink(message)] #[allow(non_snake_case)] fn transferFrom(&mut self, from: Address, to: Address, value: U256) -> bool; } -/// Creates a new ERC-20 precompile reference for the given asset ID. -/// -/// # Arguments -/// * `asset_id` - The ID of the asset to interact with -/// -/// # Returns -/// -/// Returns an `Erc20Ref` that can be used to call precompile methods. -/// -/// # Example -/// -/// ```rust,ignore -/// use ink::precompiles::erc20::erc20; -/// -/// let asset_id = 1; -/// let erc20_ref = erc20(asset_id); -/// let balance = erc20_ref.balanceOf(account); -/// ``` -pub fn erc20(precompile_index: u16, asset_id: AssetId) -> Erc20Ref { - let address = crate::prefixed_address(precompile_index, asset_id); - address.into() -} +// Note: The `erc20()` helper function and `Erc20Ref` type are generated when +// this trait is used through the `ink` crate with the `#[ink::contract_ref]` macro. #[cfg(test)] mod tests { use super::*; - use ink::env::Environment; + use ink_env::Environment; #[test] fn erc20_precompile_address_format() { @@ -171,7 +156,7 @@ mod tests { ]; let address = crate::prefixed_address( - ink::env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, + ink_env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, 1, ); let address_bytes: [u8; 20] = address.into(); @@ -183,7 +168,7 @@ mod tests { fn erc20_precompile_address_for_multiple_assets() { // Test asset ID 42 let address_42 = crate::prefixed_address( - ink::env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, + ink_env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, 42, ); let bytes_42: [u8; 20] = address_42.into(); From dff5f9cb26ddaaf49bc05940fc31a1aaaf8cbc17 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Mon, 10 Nov 2025 18:08:17 +0100 Subject: [PATCH 27/33] Revert "Reexport `ink_precompiles` from `ink`" This reverts commit 418d789d480b4ee9f5de9b5a0f32a0851f794b37. --- Cargo.lock | 3 -- crates/precompiles/Cargo.toml | 8 ------ crates/precompiles/src/erc20.rs | 49 +++++++++++++++++++++------------ 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c78f2c851c0..5373bf939b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3684,7 +3684,6 @@ dependencies = [ "ink_ir", "ink_macro", "ink_metadata 6.0.0-beta", - "ink_precompiles", "ink_prelude 6.0.0-beta", "ink_primitives 6.0.0-beta", "ink_storage", @@ -3899,8 +3898,6 @@ name = "ink_precompiles" version = "6.0.0-beta" dependencies = [ "ink", - "ink_env", - "ink_primitives 6.0.0-beta", ] [[package]] diff --git a/crates/precompiles/Cargo.toml b/crates/precompiles/Cargo.toml index 2ebf0110475..257b48ad533 100644 --- a/crates/precompiles/Cargo.toml +++ b/crates/precompiles/Cargo.toml @@ -12,18 +12,10 @@ categories.workspace = true [dependencies] ink_primitives = { workspace = true, default-features = false } -ink_env = { workspace = true, default-features = false } -# Note: The contract_ref macro is applied in the ink crate to avoid circular dependencies -# This crate only defines the plain trait - -[dev-dependencies] -# Only used in tests -ink = { workspace = true } [features] default = ["std"] std = [ "ink_primitives/std", - "ink_env/std", ] diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs index 2efaffa78b6..be198cd2c83 100644 --- a/crates/precompiles/src/erc20.rs +++ b/crates/precompiles/src/erc20.rs @@ -23,9 +23,6 @@ //! - [Polkadot SDK Assets Precompile Solidity Interface](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/primitives/ethereum-standards/src/IERC20.sol) //! - [ERC-20 Token Standard](https://eips.ethereum.org/EIPS/eip-20) -// Note: When ink_precompiles is used standalone (not through the ink facade), -// users need to have ink_env and related crates in scope. When used through -// `ink::precompiles`, the ink crate provides all necessary items. use ink_primitives::{ Address, U256, @@ -35,15 +32,7 @@ use ink_primitives::{ pub type AssetId = u32; /// Defines the ERC-20 interface of the Asset Hub precompile. -/// -/// **Note:** To use this trait, it should be consumed through the `ink` crate which -/// applies the `#[ink::contract_ref]` macro and generates the necessary contract -/// reference types (`Erc20Ref`). The macro is applied in the `ink` crate to avoid -/// circular dependency issues. -/// -/// # Solidity ABI -/// -/// This trait uses Solidity ABI for its methods. +#[ink::contract_ref(abi = "sol")] pub trait Erc20 { /// Returns the total supply of tokens. /// @@ -52,6 +41,7 @@ pub trait Erc20 { /// ```solidity /// function totalSupply() external view returns (uint256); /// ``` + #[ink(message)] #[allow(non_snake_case)] fn totalSupply(&self) -> U256; @@ -65,6 +55,7 @@ pub trait Erc20 { /// ```solidity /// function balanceOf(address account) external view returns (uint256); /// ``` + #[ink(message)] #[allow(non_snake_case)] fn balanceOf(&self, account: Address) -> U256; @@ -83,6 +74,7 @@ pub trait Erc20 { /// ```solidity /// function transfer(address to, uint256 value) external returns (bool); /// ``` + #[ink(message)] fn transfer(&mut self, to: Address, value: U256) -> bool; /// Returns the allowance for a spender on behalf of an owner. @@ -98,6 +90,7 @@ pub trait Erc20 { /// ```solidity /// function allowance(address owner, address spender) external view returns (uint256); /// ``` + #[ink(message)] fn allowance(&self, owner: Address, spender: Address) -> U256; /// Approves a spender to spend tokens on behalf of the caller. @@ -115,6 +108,7 @@ pub trait Erc20 { /// ```solidity /// function approve(address spender, uint256 value) external returns (bool); /// ``` + #[ink(message)] fn approve(&mut self, spender: Address, value: U256) -> bool; /// Transfers tokens from one account to another using allowance. @@ -135,17 +129,38 @@ pub trait Erc20 { /// ```solidity /// function transferFrom(address from, address to, uint256 value) external returns (bool); /// ``` + #[ink(message)] #[allow(non_snake_case)] fn transferFrom(&mut self, from: Address, to: Address, value: U256) -> bool; } -// Note: The `erc20()` helper function and `Erc20Ref` type are generated when -// this trait is used through the `ink` crate with the `#[ink::contract_ref]` macro. +/// Creates a new ERC-20 precompile reference for the given asset ID. +/// +/// # Arguments +/// * `asset_id` - The ID of the asset to interact with +/// +/// # Returns +/// +/// Returns an `Erc20Ref` that can be used to call precompile methods. +/// +/// # Example +/// +/// ```rust,ignore +/// use ink::precompiles::erc20::erc20; +/// +/// let asset_id = 1; +/// let erc20_ref = erc20(asset_id); +/// let balance = erc20_ref.balanceOf(account); +/// ``` +pub fn erc20(precompile_index: u16, asset_id: AssetId) -> Erc20Ref { + let address = crate::prefixed_address(precompile_index, asset_id); + address.into() +} #[cfg(test)] mod tests { use super::*; - use ink_env::Environment; + use ink::env::Environment; #[test] fn erc20_precompile_address_format() { @@ -156,7 +171,7 @@ mod tests { ]; let address = crate::prefixed_address( - ink_env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, + ink::env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, 1, ); let address_bytes: [u8; 20] = address.into(); @@ -168,7 +183,7 @@ mod tests { fn erc20_precompile_address_for_multiple_assets() { // Test asset ID 42 let address_42 = crate::prefixed_address( - ink_env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, + ink::env::DefaultEnvironment::TRUST_BACKED_ASSETS_PRECOMPILE_INDEX, 42, ); let bytes_42: [u8; 20] = address_42.into(); From dc622135d4e1480229dd8e1c014b9cde44ed23bc Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Mon, 10 Nov 2025 18:14:56 +0100 Subject: [PATCH 28/33] Fix tests --- Cargo.lock | 3 ++- crates/ink/macro/src/lib.rs | 8 ++++++++ crates/ink/src/lib.rs | 2 +- .../ui/contract/pass/event/event-max-topics.rs | 2 +- .../ui/contract/pass/event/event-topics.rs | 6 ++++-- crates/metadata/src/tests.rs | 2 ++ .../public/assets-precompile/lib.rs | 18 ++++++++++-------- 7 files changed, 28 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5373bf939b9..7d871c40123 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3684,6 +3684,7 @@ dependencies = [ "ink_ir", "ink_macro", "ink_metadata 6.0.0-beta", + "ink_precompiles", "ink_prelude 6.0.0-beta", "ink_primitives 6.0.0-beta", "ink_storage", @@ -3897,7 +3898,7 @@ dependencies = [ name = "ink_precompiles" version = "6.0.0-beta" dependencies = [ - "ink", + "ink_primitives 6.0.0-beta", ] [[package]] diff --git a/crates/ink/macro/src/lib.rs b/crates/ink/macro/src/lib.rs index d29b2545478..a562adf44b3 100644 --- a/crates/ink/macro/src/lib.rs +++ b/crates/ink/macro/src/lib.rs @@ -169,6 +169,8 @@ pub fn selector_bytes(input: TokenStream) -> TokenStream { /// /// impl ink_env::Environment for MyEnvironment { /// const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; +/// const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; +/// const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// type AccountId = [u8; 16]; /// type Balance = u128; /// type Hash = [u8; 32]; @@ -187,6 +189,8 @@ pub fn selector_bytes(input: TokenStream) -> TokenStream { /// # /// # impl ink_env::Environment for MyEnvironment { /// # const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; +/// # const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; +/// # const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// # type AccountId = [u8; 16]; /// # type Balance = u128; /// # type Hash = [u8; 32]; @@ -776,6 +780,8 @@ pub fn trait_definition(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// impl ink_env::Environment for MyEnvironment { /// const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; +/// const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; +/// const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// type AccountId = [u8; 16]; /// type Balance = u128; /// type Hash = [u8; 32]; @@ -800,6 +806,8 @@ pub fn trait_definition(attr: TokenStream, item: TokenStream) -> TokenStream { /// # /// # impl ink_env::Environment for MyEnvironment { /// # const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; +/// # const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; +/// # const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// # type AccountId = [u8; 16]; /// # type Balance = u128; /// # type Hash = [u8; 32]; diff --git a/crates/ink/src/lib.rs b/crates/ink/src/lib.rs index 01504f7791f..0107235b759 100644 --- a/crates/ink/src/lib.rs +++ b/crates/ink/src/lib.rs @@ -39,8 +39,8 @@ pub mod sol; pub use ink_env as env; #[cfg(feature = "std")] pub use ink_metadata as metadata; -pub use ink_prelude as prelude; pub use ink_precompiles as precompiles; +pub use ink_prelude as prelude; pub use ink_primitives as primitives; pub use ink_primitives::abi; pub use scale; diff --git a/crates/ink/tests/ui/contract/pass/event/event-max-topics.rs b/crates/ink/tests/ui/contract/pass/event/event-max-topics.rs index 7dd14b27bfe..2ff8c99670d 100644 --- a/crates/ink/tests/ui/contract/pass/event/event-max-topics.rs +++ b/crates/ink/tests/ui/contract/pass/event/event-max-topics.rs @@ -27,4 +27,4 @@ mod contract { } } -fn main() {} \ No newline at end of file +fn main() {} diff --git a/crates/ink/tests/ui/contract/pass/event/event-topics.rs b/crates/ink/tests/ui/contract/pass/event/event-topics.rs index bc41c12eab0..53fc9f8e8b6 100644 --- a/crates/ink/tests/ui/contract/pass/event/event-topics.rs +++ b/crates/ink/tests/ui/contract/pass/event/event-topics.rs @@ -40,7 +40,8 @@ mod contract { arg_2: i16, #[ink(topic)] arg_3: i32, - // #[ink(topic)] <- Cannot have more than 4 topics by default (i.e. event signature + 3 indexed fields). + // #[ink(topic)] <- Cannot have more than 4 topics by default (i.e. event + // signature + 3 indexed fields). arg_4: i64, } @@ -52,7 +53,8 @@ mod contract { arg_2: i16, #[ink(topic)] arg_3: i32, - // #[ink(topic)] <- Cannot have more than 4 topics by default (i.e. event signature + 3 indexed fields). + // #[ink(topic)] <- Cannot have more than 4 topics by default (i.e. event + // signature + 3 indexed fields). arg_4: i64, // #[ink(topic)] arg_5: i128, diff --git a/crates/metadata/src/tests.rs b/crates/metadata/src/tests.rs index e94f75e0409..06ea58bbe41 100644 --- a/crates/metadata/src/tests.rs +++ b/crates/metadata/src/tests.rs @@ -511,12 +511,14 @@ fn spec_contract_json() { "type": 10, }, "nativeToEthRatio": 100000000, + "poolAssetsPrecompileIndex": 800, "timestamp": { "displayName": [ "Timestamp", ], "type": 9, }, + "trustBackedAssetsPrecompileIndex": 288, }, "events": [], "lang_error": { diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index ee6511f4931..4c1191f5438 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -1,22 +1,24 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] +pub use ink::precompiles::erc20::{ + AssetId, + erc20, +}; use ink::{ H160, U256, prelude::string::ToString, }; -pub use ink::precompiles::erc20::{ - AssetId, - erc20, -}; #[ink::contract] mod asset_hub_precompile { use super::*; - use ink::prelude::string::String; - use ink::precompiles::erc20::{ - Erc20, - Erc20Ref, + use ink::{ + precompiles::erc20::{ + Erc20, + Erc20Ref, + }, + prelude::string::String, }; #[ink(storage)] From 8449e34ae6fba52ff0b689d0d0cf6b49df78a686 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Tue, 11 Nov 2025 15:58:30 +0100 Subject: [PATCH 29/33] Revert "Fix tests" This reverts commit dc622135d4e1480229dd8e1c014b9cde44ed23bc. --- Cargo.lock | 3 +-- crates/ink/macro/src/lib.rs | 8 -------- crates/ink/src/lib.rs | 2 +- .../ui/contract/pass/event/event-max-topics.rs | 2 +- .../ui/contract/pass/event/event-topics.rs | 6 ++---- crates/metadata/src/tests.rs | 2 -- .../public/assets-precompile/lib.rs | 18 ++++++++---------- 7 files changed, 13 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d871c40123..5373bf939b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3684,7 +3684,6 @@ dependencies = [ "ink_ir", "ink_macro", "ink_metadata 6.0.0-beta", - "ink_precompiles", "ink_prelude 6.0.0-beta", "ink_primitives 6.0.0-beta", "ink_storage", @@ -3898,7 +3897,7 @@ dependencies = [ name = "ink_precompiles" version = "6.0.0-beta" dependencies = [ - "ink_primitives 6.0.0-beta", + "ink", ] [[package]] diff --git a/crates/ink/macro/src/lib.rs b/crates/ink/macro/src/lib.rs index a562adf44b3..d29b2545478 100644 --- a/crates/ink/macro/src/lib.rs +++ b/crates/ink/macro/src/lib.rs @@ -169,8 +169,6 @@ pub fn selector_bytes(input: TokenStream) -> TokenStream { /// /// impl ink_env::Environment for MyEnvironment { /// const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; -/// const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; -/// const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// type AccountId = [u8; 16]; /// type Balance = u128; /// type Hash = [u8; 32]; @@ -189,8 +187,6 @@ pub fn selector_bytes(input: TokenStream) -> TokenStream { /// # /// # impl ink_env::Environment for MyEnvironment { /// # const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; -/// # const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; -/// # const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// # type AccountId = [u8; 16]; /// # type Balance = u128; /// # type Hash = [u8; 32]; @@ -780,8 +776,6 @@ pub fn trait_definition(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// impl ink_env::Environment for MyEnvironment { /// const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; -/// const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; -/// const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// type AccountId = [u8; 16]; /// type Balance = u128; /// type Hash = [u8; 32]; @@ -806,8 +800,6 @@ pub fn trait_definition(attr: TokenStream, item: TokenStream) -> TokenStream { /// # /// # impl ink_env::Environment for MyEnvironment { /// # const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; -/// # const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; -/// # const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// # type AccountId = [u8; 16]; /// # type Balance = u128; /// # type Hash = [u8; 32]; diff --git a/crates/ink/src/lib.rs b/crates/ink/src/lib.rs index 0107235b759..01504f7791f 100644 --- a/crates/ink/src/lib.rs +++ b/crates/ink/src/lib.rs @@ -39,8 +39,8 @@ pub mod sol; pub use ink_env as env; #[cfg(feature = "std")] pub use ink_metadata as metadata; -pub use ink_precompiles as precompiles; pub use ink_prelude as prelude; +pub use ink_precompiles as precompiles; pub use ink_primitives as primitives; pub use ink_primitives::abi; pub use scale; diff --git a/crates/ink/tests/ui/contract/pass/event/event-max-topics.rs b/crates/ink/tests/ui/contract/pass/event/event-max-topics.rs index 2ff8c99670d..7dd14b27bfe 100644 --- a/crates/ink/tests/ui/contract/pass/event/event-max-topics.rs +++ b/crates/ink/tests/ui/contract/pass/event/event-max-topics.rs @@ -27,4 +27,4 @@ mod contract { } } -fn main() {} +fn main() {} \ No newline at end of file diff --git a/crates/ink/tests/ui/contract/pass/event/event-topics.rs b/crates/ink/tests/ui/contract/pass/event/event-topics.rs index 53fc9f8e8b6..bc41c12eab0 100644 --- a/crates/ink/tests/ui/contract/pass/event/event-topics.rs +++ b/crates/ink/tests/ui/contract/pass/event/event-topics.rs @@ -40,8 +40,7 @@ mod contract { arg_2: i16, #[ink(topic)] arg_3: i32, - // #[ink(topic)] <- Cannot have more than 4 topics by default (i.e. event - // signature + 3 indexed fields). + // #[ink(topic)] <- Cannot have more than 4 topics by default (i.e. event signature + 3 indexed fields). arg_4: i64, } @@ -53,8 +52,7 @@ mod contract { arg_2: i16, #[ink(topic)] arg_3: i32, - // #[ink(topic)] <- Cannot have more than 4 topics by default (i.e. event - // signature + 3 indexed fields). + // #[ink(topic)] <- Cannot have more than 4 topics by default (i.e. event signature + 3 indexed fields). arg_4: i64, // #[ink(topic)] arg_5: i128, diff --git a/crates/metadata/src/tests.rs b/crates/metadata/src/tests.rs index 06ea58bbe41..e94f75e0409 100644 --- a/crates/metadata/src/tests.rs +++ b/crates/metadata/src/tests.rs @@ -511,14 +511,12 @@ fn spec_contract_json() { "type": 10, }, "nativeToEthRatio": 100000000, - "poolAssetsPrecompileIndex": 800, "timestamp": { "displayName": [ "Timestamp", ], "type": 9, }, - "trustBackedAssetsPrecompileIndex": 288, }, "events": [], "lang_error": { diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index 4c1191f5438..ee6511f4931 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -1,24 +1,22 @@ #![cfg_attr(not(feature = "std"), no_std, no_main)] -pub use ink::precompiles::erc20::{ - AssetId, - erc20, -}; use ink::{ H160, U256, prelude::string::ToString, }; +pub use ink::precompiles::erc20::{ + AssetId, + erc20, +}; #[ink::contract] mod asset_hub_precompile { use super::*; - use ink::{ - precompiles::erc20::{ - Erc20, - Erc20Ref, - }, - prelude::string::String, + use ink::prelude::string::String; + use ink::precompiles::erc20::{ + Erc20, + Erc20Ref, }; #[ink(storage)] From a20f80e56ab96556524e1e29f29ee1916c037051 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Tue, 11 Nov 2025 15:58:43 +0100 Subject: [PATCH 30/33] Revert "Reexport `ink_precompiles`" This reverts commit d2c3c436c0a5f0ce15ba2407d36a82bb346c5410. --- crates/ink/Cargo.toml | 1 - crates/ink/src/lib.rs | 1 - crates/precompiles/Cargo.toml | 6 +++--- crates/precompiles/src/erc20.rs | 4 ++-- crates/precompiles/src/lib.rs | 10 ++++------ integration-tests/public/assets-precompile/lib.rs | 4 ++-- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/crates/ink/Cargo.toml b/crates/ink/Cargo.toml index bc0348b303b..0704d4aff42 100644 --- a/crates/ink/Cargo.toml +++ b/crates/ink/Cargo.toml @@ -19,7 +19,6 @@ include = ["Cargo.toml", "src/**/*.rs", "README.md", "LICENSE"] ink_env = { workspace = true } ink_storage = { workspace = true } ink_primitives = { workspace = true } -ink_precompiles = { workspace = true } ink_metadata = { workspace = true, optional = true } ink_prelude = { workspace = true } ink_macro = { workspace = true } diff --git a/crates/ink/src/lib.rs b/crates/ink/src/lib.rs index 01504f7791f..d5b20b81e94 100644 --- a/crates/ink/src/lib.rs +++ b/crates/ink/src/lib.rs @@ -40,7 +40,6 @@ pub use ink_env as env; #[cfg(feature = "std")] pub use ink_metadata as metadata; pub use ink_prelude as prelude; -pub use ink_precompiles as precompiles; pub use ink_primitives as primitives; pub use ink_primitives::abi; pub use scale; diff --git a/crates/precompiles/Cargo.toml b/crates/precompiles/Cargo.toml index 257b48ad533..5f9b5f83470 100644 --- a/crates/precompiles/Cargo.toml +++ b/crates/precompiles/Cargo.toml @@ -4,18 +4,18 @@ version.workspace = true authors = ["Use Ink "] edition.workspace = true license.workspace = true -description = "[ink!] Precompile interfaces for Polkadot SDK `pallet-revive` smart contracts." +description = "[ink!] Precompile interfaces for pallet-revive smart contracts." repository.workspace = true homepage.workspace = true keywords.workspace = true categories.workspace = true [dependencies] -ink_primitives = { workspace = true, default-features = false } +ink = { workspace = true, default-features = false } [features] default = ["std"] std = [ - "ink_primitives/std", + "ink/std", ] diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs index be198cd2c83..5aaadce76e0 100644 --- a/crates/precompiles/src/erc20.rs +++ b/crates/precompiles/src/erc20.rs @@ -23,7 +23,7 @@ //! - [Polkadot SDK Assets Precompile Solidity Interface](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/primitives/ethereum-standards/src/IERC20.sol) //! - [ERC-20 Token Standard](https://eips.ethereum.org/EIPS/eip-20) -use ink_primitives::{ +use ink::{ Address, U256, }; @@ -146,7 +146,7 @@ pub trait Erc20 { /// # Example /// /// ```rust,ignore -/// use ink::precompiles::erc20::erc20; +/// use ink_precompiles::erc20::erc20; /// /// let asset_id = 1; /// let erc20_ref = erc20(asset_id); diff --git a/crates/precompiles/src/lib.rs b/crates/precompiles/src/lib.rs index ec80e8c05a5..dc9af353829 100644 --- a/crates/precompiles/src/lib.rs +++ b/crates/precompiles/src/lib.rs @@ -18,8 +18,6 @@ )] #![cfg_attr(not(feature = "std"), no_std)] -use ink_primitives::Address; - pub mod erc20; /// Calculates the address of a precompile at index `n`. @@ -30,7 +28,7 @@ pub mod erc20; /// # Arguments /// * `n` - The precompile index (e.g., `0x0120` for ERC20 Assets precompile) #[inline] -pub fn fixed_address(n: u16) -> Address { +pub fn fixed_address(n: u16) -> ink::Address { let shifted = (n as u32) << 16; let suffix = shifted.to_be_bytes(); @@ -40,7 +38,7 @@ pub fn fixed_address(n: u16) -> Address { address[i] = suffix[i - 16]; i = i + 1; } - Address::from(address) + ink::Address::from(address) } /// Calculates the address of a precompile at index `n` with an additional prefix. @@ -55,11 +53,11 @@ pub fn fixed_address(n: u16) -> Address { /// * `n` - The precompile index (e.g., `0x0120` for ERC20 Assets precompile) /// * `prefix` - A 32-bit value to encode in the first 4 bytes (e.g., asset ID) #[inline] -pub fn prefixed_address(n: u16, prefix: u32) -> Address { +pub fn prefixed_address(n: u16, prefix: u32) -> ink::Address { let address = fixed_address(n); let mut address_bytes: [u8; 20] = address.into(); address_bytes[..4].copy_from_slice(&prefix.to_be_bytes()); - Address::from(address_bytes) + ink::Address::from(address_bytes) } #[cfg(test)] diff --git a/integration-tests/public/assets-precompile/lib.rs b/integration-tests/public/assets-precompile/lib.rs index ee6511f4931..bc37c157ef3 100644 --- a/integration-tests/public/assets-precompile/lib.rs +++ b/integration-tests/public/assets-precompile/lib.rs @@ -5,7 +5,7 @@ use ink::{ U256, prelude::string::ToString, }; -pub use ink::precompiles::erc20::{ +pub use ink_precompiles::erc20::{ AssetId, erc20, }; @@ -14,7 +14,7 @@ pub use ink::precompiles::erc20::{ mod asset_hub_precompile { use super::*; use ink::prelude::string::String; - use ink::precompiles::erc20::{ + use ink_precompiles::erc20::{ Erc20, Erc20Ref, }; From 8edeb013f048967addc68cda793d4027b4f53dd3 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Tue, 11 Nov 2025 16:03:09 +0100 Subject: [PATCH 31/33] Fix CI --- crates/precompiles/src/erc20.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/precompiles/src/erc20.rs b/crates/precompiles/src/erc20.rs index 5aaadce76e0..31620b732c1 100644 --- a/crates/precompiles/src/erc20.rs +++ b/crates/precompiles/src/erc20.rs @@ -159,7 +159,6 @@ pub fn erc20(precompile_index: u16, asset_id: AssetId) -> Erc20Ref { #[cfg(test)] mod tests { - use super::*; use ink::env::Environment; #[test] From 9f0bdc44db6555e4e5839bc765f6018cb1b09dbd Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Tue, 11 Nov 2025 16:52:21 +0100 Subject: [PATCH 32/33] Fix tests --- crates/ink/macro/src/lib.rs | 8 ++++++++ crates/metadata/src/tests.rs | 2 ++ 2 files changed, 10 insertions(+) diff --git a/crates/ink/macro/src/lib.rs b/crates/ink/macro/src/lib.rs index d29b2545478..a562adf44b3 100644 --- a/crates/ink/macro/src/lib.rs +++ b/crates/ink/macro/src/lib.rs @@ -169,6 +169,8 @@ pub fn selector_bytes(input: TokenStream) -> TokenStream { /// /// impl ink_env::Environment for MyEnvironment { /// const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; +/// const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; +/// const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// type AccountId = [u8; 16]; /// type Balance = u128; /// type Hash = [u8; 32]; @@ -187,6 +189,8 @@ pub fn selector_bytes(input: TokenStream) -> TokenStream { /// # /// # impl ink_env::Environment for MyEnvironment { /// # const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; +/// # const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; +/// # const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// # type AccountId = [u8; 16]; /// # type Balance = u128; /// # type Hash = [u8; 32]; @@ -776,6 +780,8 @@ pub fn trait_definition(attr: TokenStream, item: TokenStream) -> TokenStream { /// /// impl ink_env::Environment for MyEnvironment { /// const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; +/// const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; +/// const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// type AccountId = [u8; 16]; /// type Balance = u128; /// type Hash = [u8; 32]; @@ -800,6 +806,8 @@ pub fn trait_definition(attr: TokenStream, item: TokenStream) -> TokenStream { /// # /// # impl ink_env::Environment for MyEnvironment { /// # const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; +/// # const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; +/// # const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// # type AccountId = [u8; 16]; /// # type Balance = u128; /// # type Hash = [u8; 32]; diff --git a/crates/metadata/src/tests.rs b/crates/metadata/src/tests.rs index e94f75e0409..06ea58bbe41 100644 --- a/crates/metadata/src/tests.rs +++ b/crates/metadata/src/tests.rs @@ -511,12 +511,14 @@ fn spec_contract_json() { "type": 10, }, "nativeToEthRatio": 100000000, + "poolAssetsPrecompileIndex": 800, "timestamp": { "displayName": [ "Timestamp", ], "type": 9, }, + "trustBackedAssetsPrecompileIndex": 288, }, "events": [], "lang_error": { From 05bdd67831bc3ee7b8386b10ff271bf3b3756bd6 Mon Sep 17 00:00:00 2001 From: Michael Mueller Date: Tue, 11 Nov 2025 18:25:03 +0100 Subject: [PATCH 33/33] Fix doc tests --- crates/ink/src/contract_ref.rs | 2 ++ crates/ink/src/message_builder.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/crates/ink/src/contract_ref.rs b/crates/ink/src/contract_ref.rs index 3b16b174e48..13c2645352c 100644 --- a/crates/ink/src/contract_ref.rs +++ b/crates/ink/src/contract_ref.rs @@ -153,6 +153,8 @@ use ink_primitives::Address; /// /// impl ink_env::Environment for CustomEnv { /// const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; +/// const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; +/// const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// type AccountId = [u8; 32]; /// type Balance = u64; /// type Hash = [u8; 32]; diff --git a/crates/ink/src/message_builder.rs b/crates/ink/src/message_builder.rs index 3b0b1fa9319..9c52a987533 100644 --- a/crates/ink/src/message_builder.rs +++ b/crates/ink/src/message_builder.rs @@ -78,6 +78,8 @@ /// /// impl ink_env::Environment for CustomEnv { /// const NATIVE_TO_ETH_RATIO: u32 = 100_000_000; +/// const TRUST_BACKED_ASSETS_PRECOMPILE_INDEX: u16 = 0x0120; +/// const POOL_ASSETS_PRECOMPILE_INDEX: u16 = 0x0320; /// type AccountId = [u8; 32]; /// type Balance = u64; /// type Hash = [u8; 32];