From 06914b03048bba83d91459952182c1aa1c6af09b Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Mon, 8 Apr 2024 12:37:13 +0100 Subject: [PATCH] Add `integration-test` for calling a contract from a runtime pallet (#2189) * Add files for integration test * WIP call contract from runtime * mv to new folder structure * basic e2e test compiles and works * WIP register pallet * WIP add pallets to sandbox runtime * Add pallet to own crate * WIP rearrange workspace * Fix errors * Fix imports * Fix contract * Fix contract manifest * Test passes * Copy full test over * Implement traits to fix test * revert * Remove println! * Put contract back at workspace root again --- crates/e2e/sandbox/src/macros.rs | 18 +++- .../public/runtime-call-contract/Cargo.toml | 48 ++++++++++ .../public/runtime-call-contract/e2e_tests.rs | 60 +++++++++++++ .../public/runtime-call-contract/lib.rs | 36 ++++++++ .../sandbox-runtime/Cargo.toml | 23 +++++ .../pallet-contract-caller/Cargo.toml | 34 ++++++++ .../pallet-contract-caller/src/lib.rs | 87 +++++++++++++++++++ .../sandbox-runtime/src/lib.rs | 11 +++ .../runtime-call-contract/traits/Cargo.toml | 18 ++++ .../runtime-call-contract/traits/lib.rs | 14 +++ 10 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 integration-tests/public/runtime-call-contract/Cargo.toml create mode 100644 integration-tests/public/runtime-call-contract/e2e_tests.rs create mode 100644 integration-tests/public/runtime-call-contract/lib.rs create mode 100644 integration-tests/public/runtime-call-contract/sandbox-runtime/Cargo.toml create mode 100644 integration-tests/public/runtime-call-contract/sandbox-runtime/pallet-contract-caller/Cargo.toml create mode 100644 integration-tests/public/runtime-call-contract/sandbox-runtime/pallet-contract-caller/src/lib.rs create mode 100644 integration-tests/public/runtime-call-contract/sandbox-runtime/src/lib.rs create mode 100644 integration-tests/public/runtime-call-contract/traits/Cargo.toml create mode 100644 integration-tests/public/runtime-call-contract/traits/lib.rs diff --git a/crates/e2e/sandbox/src/macros.rs b/crates/e2e/sandbox/src/macros.rs index ee99a2cae1c..0812aba55fe 100644 --- a/crates/e2e/sandbox/src/macros.rs +++ b/crates/e2e/sandbox/src/macros.rs @@ -78,15 +78,24 @@ impl< macro_rules! create_sandbox { ($name:ident) => { $crate::paste::paste! { - $crate::create_sandbox!($name, [<$name Runtime>], (), ()); + $crate::create_sandbox!($name, [<$name Runtime>], (), (), {}); } }; ($name:ident, $chain_extension: ty, $debug: ty) => { $crate::paste::paste! { - $crate::create_sandbox!($name, [<$name Runtime>], $chain_extension, $debug); + $crate::create_sandbox!($name, [<$name Runtime>], $chain_extension, $debug, {}); } }; - ($sandbox:ident, $runtime:ident, $chain_extension: ty, $debug: ty) => { + ($name:ident, $chain_extension: ty, $debug: ty, { $( $pallet_name:tt : $pallet:ident ),* $(,)? }) => { + $crate::paste::paste! { + $crate::create_sandbox!($name, [<$name Runtime>], $chain_extension, $debug, { + $( + $pallet_name : $pallet, + )* + }); + } + }; + ($sandbox:ident, $runtime:ident, $chain_extension: ty, $debug: ty, { $( $pallet_name:tt : $pallet:ident ),* $(,)? }) => { // Put all the boilerplate into an auxiliary module @@ -113,6 +122,9 @@ mod construct_runtime { Balances: $crate::pallet_balances, Timestamp: $crate::pallet_timestamp, Contracts: $crate::pallet_contracts, + $( + $pallet_name: $pallet, + )* } ); diff --git a/integration-tests/public/runtime-call-contract/Cargo.toml b/integration-tests/public/runtime-call-contract/Cargo.toml new file mode 100644 index 00000000000..87b0b332747 --- /dev/null +++ b/integration-tests/public/runtime-call-contract/Cargo.toml @@ -0,0 +1,48 @@ +[workspace] +members = ["sandbox-runtime", "traits"] + +[workspace.package] +authors = ["Parity Technologies "] +edition = "2021" +homepage = "https://www.parity.io/" +keywords = ["wasm", "parity", "webassembly", "blockchain", "edsl"] +license = "Apache-2.0" +repository = "https://github.com/paritytech/ink" + +[workspace.dependencies] +frame-support = { version = "31.0.0", default-features = false } +frame-system = { version = "31.0.0", default-features = false } +pallet-contracts = { version = "30.0.0", default-features = false } +codec = { package = "parity-scale-codec", version = "3.6.9", default-features = false } +scale-info = { version = "2.11.1", default-features = false } + +[package] +name = "runtime-call-contract" +version = "5.0.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } +flipper-traits = { path = "traits", default-features = false } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e", features = ["sandbox"] } +sandbox-runtime = { path = "sandbox-runtime", default-features = false } +scale-value = "0.14.1" +# can't use workspace dependency because of `cargo-contract` build not +# working with workspace dependencies +frame-support = { version = "31.0.0", default-features = false } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "sandbox-runtime/std", + "flipper-traits/std", +] +ink-as-dependency = [] diff --git a/integration-tests/public/runtime-call-contract/e2e_tests.rs b/integration-tests/public/runtime-call-contract/e2e_tests.rs new file mode 100644 index 00000000000..805ca414199 --- /dev/null +++ b/integration-tests/public/runtime-call-contract/e2e_tests.rs @@ -0,0 +1,60 @@ +use super::{ + Flipper, + FlipperRef, +}; +use ink_e2e::{ + ChainBackend, + ContractsBackend, +}; + +type E2EResult = Result>; + +/// Just instantiate a contract using non-default runtime. +#[ink_e2e::test(backend(runtime_only(sandbox = sandbox_runtime::ContractCallerSandbox)))] +async fn instantiate_and_get(mut client: Client) -> E2EResult<()> { + use flipper_traits::Flip; + + let initial_value = false; + let mut constructor = FlipperRef::new(initial_value); + + let contract = client + .instantiate("runtime-call-contract", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + + let mut call_builder = contract.call_builder::(); + let flip_dry_run = client + .call(&ink_e2e::bob(), &call_builder.flip()) + .dry_run() + .await?; + let gas_required = flip_dry_run.exec_result.gas_required; + + // call pallet dispatchable + client + .runtime_call( + &ink_e2e::alice(), + "ContractCaller", + "contract_call_flip", + vec![ + scale_value::Value::from_bytes(contract.account_id), + scale_value::serde::to_value(frame_support::weights::Weight::from_parts( + gas_required.ref_time(), + gas_required.proof_size(), + )) + .unwrap(), + ], + ) + .await + .expect("runtime call failed"); + + // now check that the flip was executed via the pallet + let get_result = client + .call(&ink_e2e::alice(), &call_builder.get()) + .dry_run() + .await?; + + assert_eq!(get_result.return_value(), !initial_value); + + Ok(()) +} diff --git a/integration-tests/public/runtime-call-contract/lib.rs b/integration-tests/public/runtime-call-contract/lib.rs new file mode 100644 index 00000000000..58435745766 --- /dev/null +++ b/integration-tests/public/runtime-call-contract/lib.rs @@ -0,0 +1,36 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +pub use flipper::{ + Flipper, + FlipperRef, +}; + +#[ink::contract] +mod flipper { + #[ink(storage)] + pub struct Flipper { + value: bool, + } + + impl Flipper { + #[ink(constructor)] + pub fn new(init_value: bool) -> Self { + Self { value: init_value } + } + } + + impl flipper_traits::Flip for Flipper { + #[ink(message)] + fn flip(&mut self) { + self.value = !self.value; + } + + #[ink(message)] + fn get(&self) -> bool { + self.value + } + } +} + +#[cfg(test)] +mod e2e_tests; diff --git a/integration-tests/public/runtime-call-contract/sandbox-runtime/Cargo.toml b/integration-tests/public/runtime-call-contract/sandbox-runtime/Cargo.toml new file mode 100644 index 00000000000..fabffd81120 --- /dev/null +++ b/integration-tests/public/runtime-call-contract/sandbox-runtime/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sandbox-runtime" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +ink_sandbox = { path = "../../../../crates/e2e/sandbox" } +pallet-contract-caller = { path = "pallet-contract-caller", default-features = false } +frame-support = { workspace = true } +frame-system = { workspace = true } +codec = { workspace = true } +scale-info = { workspace = true } + +[features] +default = ["std"] +std = [ + "pallet-contract-caller/std", +] diff --git a/integration-tests/public/runtime-call-contract/sandbox-runtime/pallet-contract-caller/Cargo.toml b/integration-tests/public/runtime-call-contract/sandbox-runtime/pallet-contract-caller/Cargo.toml new file mode 100644 index 00000000000..689d830d194 --- /dev/null +++ b/integration-tests/public/runtime-call-contract/sandbox-runtime/pallet-contract-caller/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "pallet-contract-caller" +version = "0.1.0" +description = "Demonstrate calling an ink! contract from a pallet" +authors = ["Substrate DevHub "] +homepage = "https://substrate.io" +edition.workspace = true +license = "MIT-0" +publish = false +repository = "https://github.com/paritytech/ink" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true, default-features = false, features = ["derive"] } +scale-info = { workspace = true, default-features = false, features = ["derive"] } +frame-support = { workspace = true, default-features = false } +frame-system = { workspace = true, default-features = false } + +pallet-contracts = { workspace = true, default-features = false } +flipper-traits = { path = "../../traits", default-features = false } +ink = { path = "../../../../../crates/ink", features = ["no-panic-handler", "no-allocator"] } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "pallet-contracts/std", + "ink/std" +] diff --git a/integration-tests/public/runtime-call-contract/sandbox-runtime/pallet-contract-caller/src/lib.rs b/integration-tests/public/runtime-call-contract/sandbox-runtime/pallet-contract-caller/src/lib.rs new file mode 100644 index 00000000000..50dc43f7a2b --- /dev/null +++ b/integration-tests/public/runtime-call-contract/sandbox-runtime/pallet-contract-caller/src/lib.rs @@ -0,0 +1,87 @@ +//! # Contract Caller +//! +//! Demonstrates calling into an `ink!` contract from a pallet. + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use flipper_traits::Flip; + use frame_support::{ + pallet_prelude::*, + traits::fungible::Inspect, + }; + use frame_system::pallet_prelude::*; + use ink::codegen::TraitCallBuilder; + + type AccountIdOf = ::AccountId; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_contracts::Config {} + + #[pallet::error] + pub enum Error {} + + #[pallet::call] + impl Pallet + where + [u8; 32]: From<::AccountId>, + <::Currency as Inspect< + ::AccountId, + >>::Balance: From, + { + /// Call the flip method on the contract at the given `contract` account. + #[pallet::call_index(0)] + #[pallet::weight(::call().saturating_add(*gas_limit))] + pub fn contract_call_flip( + origin: OriginFor, + contract: AccountIdOf, + gas_limit: Weight, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let ink_account_id = + ink::primitives::AccountId::from(<[u8; 32]>::from(contract.clone())); + let mut flipper: ink::contract_ref!(Flip, ink::env::DefaultEnvironment) = + ink_account_id.into(); + let call_builder = flipper.call_mut(); + + let params = call_builder + .flip() + .ref_time_limit(gas_limit.ref_time()) + .proof_size_limit(gas_limit.proof_size()) + .params(); + + // Next step is to explore ways to encapsulate the following into the call + // builder. + let value = *params.transferred_value(); + let data = params.exec_input().encode(); + let weight = + Weight::from_parts(params.ref_time_limit(), params.proof_size_limit()); + let storage_deposit_limit = + params.storage_deposit_limit().map(|limit| (*limit).into()); + + let result = pallet_contracts::Pallet::::bare_call( + who.clone(), + contract.clone(), + value.into(), + weight, + storage_deposit_limit, + data, + pallet_contracts::DebugInfo::UnsafeDebug, + pallet_contracts::CollectEvents::Skip, + pallet_contracts::Determinism::Enforced, + ); + + assert!(!result.result?.did_revert()); + + Ok(()) + } + } +} diff --git a/integration-tests/public/runtime-call-contract/sandbox-runtime/src/lib.rs b/integration-tests/public/runtime-call-contract/sandbox-runtime/src/lib.rs new file mode 100644 index 00000000000..78a39d89705 --- /dev/null +++ b/integration-tests/public/runtime-call-contract/sandbox-runtime/src/lib.rs @@ -0,0 +1,11 @@ +//! # Contract Caller +//! +//! Demonstrates calling into an `ink!` contract from a pallet. + +#![cfg_attr(not(feature = "std"), no_std)] + +ink_sandbox::create_sandbox!(ContractCallerSandbox, ContractCallerSandboxRuntime, (), (), { + ContractCaller: pallet_contract_caller, +}); + +impl pallet_contract_caller::Config for ContractCallerSandboxRuntime {} diff --git a/integration-tests/public/runtime-call-contract/traits/Cargo.toml b/integration-tests/public/runtime-call-contract/traits/Cargo.toml new file mode 100644 index 00000000000..c42ea422cde --- /dev/null +++ b/integration-tests/public/runtime-call-contract/traits/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "flipper-traits" +version = "5.0.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../../crates/ink", default-features = false } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", +] diff --git a/integration-tests/public/runtime-call-contract/traits/lib.rs b/integration-tests/public/runtime-call-contract/traits/lib.rs new file mode 100644 index 00000000000..294077a1cae --- /dev/null +++ b/integration-tests/public/runtime-call-contract/traits/lib.rs @@ -0,0 +1,14 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +/// Allows to flip and get a bool value. +#[ink::trait_definition] +pub trait Flip { + /// Flip the value of the stored `bool` from `true` + /// to `false` and vice versa. + #[ink(message)] + fn flip(&mut self); + + /// Returns the current value of our `bool`. + #[ink(message)] + fn get(&self) -> bool; +}