diff --git a/CHANGELOG.md b/CHANGELOG.md index dd8f3ac8bd9..a685a74b806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Make E2E testcases generic over `E2EBackend` trait - [#1867](https://github.com/paritytech/ink/pull/1867) - Modify static buffer size via environmental variables - [#1869](https://github.com/paritytech/ink/pull/1869) - Persist static buffer size in metadata - [#1880](https://github.com/paritytech/ink/pull/1880) +- Add backend choice to the E2E testcase configuration ‒ [#1864](https://github.com/paritytech/ink/pull/1864) ### Added - Schema generation - [#1765](https://github.com/paritytech/ink/pull/1765) diff --git a/Cargo.toml b/Cargo.toml index 80fa60aaa5d..ada0acdb96c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ cargo_metadata = { version = "0.17.0" } cfg-if = { version = "1.0" } contract-build = { version = "3.2.0" } derive_more = { version = "0.99.17", default-features = false } +drink = { version = "0.1.2" } either = { version = "1.5", default-features = false } funty = { version = "2.0.0" } heck = { version = "0.4.0" } @@ -72,6 +73,7 @@ tokio = { version = "1.18.2" } tracing = { version = "0.1.37" } tracing-subscriber = { version = "0.3.17" } trybuild = { version = "1.0.60" } +wasm-instrument = { version = "0.4.0", features = ["sign_ext"] } which = { version = "4.4.0" } xxhash-rust = { version = "0.8" } const_env = { version = "0.1"} diff --git a/crates/e2e/Cargo.toml b/crates/e2e/Cargo.toml index f92a0a909e3..1e0b13f8f6b 100644 --- a/crates/e2e/Cargo.toml +++ b/crates/e2e/Cargo.toml @@ -22,6 +22,7 @@ ink_primitives = { workspace = true, default-features = true } cargo_metadata = { workspace = true } contract-build = { workspace = true } +drink = { workspace = true } funty = { workspace = true } impl-serde = { workspace = true } jsonrpsee = { workspace = true, features = ["ws-client"] } @@ -33,6 +34,7 @@ tracing-subscriber = { workspace = true, features = ["env-filter"] } scale = { package = "parity-scale-codec", workspace = true } subxt = { workspace = true } subxt-signer = { workspace = true, features = ["subxt", "sr25519"] } +wasm-instrument = { workspace = true } # Substrate pallet-contracts-primitives = { workspace = true } diff --git a/crates/e2e/macro/src/codegen.rs b/crates/e2e/macro/src/codegen.rs index 1adca4628c6..1d4e157d513 100644 --- a/crates/e2e/macro/src/codegen.rs +++ b/crates/e2e/macro/src/codegen.rs @@ -12,11 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::ir; +use crate::{ + config::Backend, + ir, +}; use derive_more::From; use proc_macro2::TokenStream as TokenStream2; use quote::quote; +const DEFAULT_CONTRACTS_NODE: &str = "substrate-contracts-node"; + /// Generates code for the `[ink::e2e_test]` macro. #[derive(From)] pub struct InkE2ETest { @@ -61,23 +66,10 @@ impl InkE2ETest { } }; - const DEFAULT_CONTRACTS_NODE: &str = "substrate-contracts-node"; - - // use the user supplied `CONTRACTS_NODE` or default to `substrate-contracts-node` - let contracts_node: &'static str = - option_env!("CONTRACTS_NODE").unwrap_or(DEFAULT_CONTRACTS_NODE); - - // check the specified contracts node. - if which::which(contracts_node).is_err() { - if contracts_node == DEFAULT_CONTRACTS_NODE { - panic!( - "The '{DEFAULT_CONTRACTS_NODE}' executable was not found. Install '{DEFAULT_CONTRACTS_NODE}' on the PATH, \ - or specify the `CONTRACTS_NODE` environment variable.", - ) - } else { - panic!("The contracts node executable '{contracts_node}' was not found.") - } - } + let client_building = match self.test.config.backend() { + Backend::Full => build_full_client(&environment, exec_build_contracts), + Backend::RuntimeOnly => build_runtime_client(exec_build_contracts), + }; quote! { #( #attrs )* @@ -97,24 +89,7 @@ impl InkE2ETest { log_info("creating new client"); let run = async { - // spawn a contracts node process just for this test - let node_proc = ::ink_e2e::TestNodeProcess::<::ink_e2e::PolkadotConfig> - ::build(#contracts_node) - .spawn() - .await - .unwrap_or_else(|err| - ::core::panic!("Error spawning substrate-contracts-node: {:?}", err) - ); - - let contracts = #exec_build_contracts; - - let mut client = ::ink_e2e::Client::< - ::ink_e2e::PolkadotConfig, - #environment - >::new( - node_proc.client(), - contracts, - ).await; + #client_building let __ret = { #block @@ -126,10 +101,52 @@ impl InkE2ETest { return ::ink_e2e::tokio::runtime::Builder::new_current_thread() .enable_all() .build() - .unwrap_or_else(|err| panic!("Failed building the Runtime: {}", err)) + .unwrap_or_else(|err| panic!("Failed building the Runtime: {err}")) .block_on(run); } } } } } + +fn build_full_client(environment: &syn::Path, contracts: TokenStream2) -> TokenStream2 { + // Use the user supplied `CONTRACTS_NODE` or default to `DEFAULT_CONTRACTS_NODE`. + let contracts_node: &'static str = + option_env!("CONTRACTS_NODE").unwrap_or(DEFAULT_CONTRACTS_NODE); + + // Check the specified contracts node. + if which::which(contracts_node).is_err() { + if contracts_node == DEFAULT_CONTRACTS_NODE { + panic!( + "The '{DEFAULT_CONTRACTS_NODE}' executable was not found. Install '{DEFAULT_CONTRACTS_NODE}' on the PATH, \ + or specify the `CONTRACTS_NODE` environment variable.", + ) + } else { + panic!("The contracts node executable '{contracts_node}' was not found.") + } + } + + quote! { + // Spawn a contracts node process just for this test. + let node_proc = ::ink_e2e::TestNodeProcess::<::ink_e2e::PolkadotConfig> + ::build(#contracts_node) + .spawn() + .await + .unwrap_or_else(|err| + ::core::panic!("Error spawning substrate-contracts-node: {err:?}") + ); + + let contracts = #contracts; + let mut client = ::ink_e2e::Client::< + ::ink_e2e::PolkadotConfig, + #environment + >::new(node_proc.client(), contracts).await; + } +} + +fn build_runtime_client(contracts: TokenStream2) -> TokenStream2 { + quote! { + let contracts = #contracts; + let mut client = ::ink_e2e::DrinkClient::new(contracts); + } +} diff --git a/crates/e2e/macro/src/config.rs b/crates/e2e/macro/src/config.rs index 748c01cf847..200793de114 100644 --- a/crates/e2e/macro/src/config.rs +++ b/crates/e2e/macro/src/config.rs @@ -18,6 +18,38 @@ use ink_ir::{ utils::duplicate_config_err, }; +/// The type of the architecture that should be used to run test. +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] +pub enum Backend { + /// The standard approach with running dedicated single-node blockchain in a + /// background process. + #[default] + Full, + /// The lightweight approach skipping node layer. + /// + /// This runs a runtime emulator within `TestExternalities` (using drink! library) in + /// the same process as the test. + RuntimeOnly, +} + +impl TryFrom for Backend { + type Error = syn::Error; + + fn try_from(value: syn::LitStr) -> Result { + match value.value().as_str() { + "full" => Ok(Self::Full), + "runtime_only" | "runtime-only" => Ok(Self::RuntimeOnly), + _ => { + Err(format_err_spanned!( + value, + "unknown backend `{}` for ink! E2E test configuration argument", + value.value() + )) + } + } + } +} + /// The End-to-End test configuration. #[derive(Debug, Default, PartialEq, Eq)] pub struct E2EConfig { @@ -30,6 +62,8 @@ pub struct E2EConfig { /// [`DefaultEnvironment`](https://docs.rs/ink_env/4.1.0/ink_env/enum.DefaultEnvironment.html) /// will be used. environment: Option, + /// The type of the architecture that should be used to run test. + backend: Backend, } impl TryFrom for E2EConfig { @@ -38,6 +72,7 @@ impl TryFrom for E2EConfig { fn try_from(args: ast::AttributeArgs) -> Result { let mut additional_contracts: Option<(syn::LitStr, ast::MetaNameValue)> = None; let mut environment: Option<(syn::Path, ast::MetaNameValue)> = None; + let mut backend: Option<(syn::LitStr, ast::MetaNameValue)> = None; for arg in args.into_iter() { if arg.name.is_ident("additional_contracts") { @@ -69,6 +104,18 @@ impl TryFrom for E2EConfig { "expected a path for `environment` ink! E2E test configuration argument", )); } + } else if arg.name.is_ident("backend") { + if let Some((_, ast)) = backend { + return Err(duplicate_config_err(ast, arg, "backend", "E2E test")) + } + if let ast::MetaValue::Lit(syn::Lit::Str(lit_str)) = &arg.value { + backend = Some((lit_str.clone(), arg)) + } else { + return Err(format_err_spanned!( + arg, + "expected a string literal for `backend` ink! E2E test configuration argument", + )); + } } else { return Err(format_err_spanned!( arg, @@ -80,10 +127,15 @@ impl TryFrom for E2EConfig { .map(|(value, _)| value.value().split(' ').map(String::from).collect()) .unwrap_or_else(Vec::new); let environment = environment.map(|(path, _)| path); + let backend = backend + .map(|(b, _)| Backend::try_from(b)) + .transpose()? + .unwrap_or_default(); Ok(E2EConfig { additional_contracts, environment, + backend, }) } } @@ -99,6 +151,11 @@ impl E2EConfig { pub fn environment(&self) -> Option { self.environment.clone() } + + /// The type of the architecture that should be used to run test. + pub fn backend(&self) -> Backend { + self.backend + } } #[cfg(test)] @@ -180,12 +237,43 @@ mod tests { ); } + #[test] + fn backend_must_be_literal() { + assert_try_from( + syn::parse_quote! { backend = full }, + Err("expected a string literal for `backend` ink! E2E test configuration argument"), + ); + } + + #[test] + fn duplicate_backend_fails() { + assert_try_from( + syn::parse_quote! { + backend = "full", + backend = "runtime-only", + }, + Err("encountered duplicate ink! E2E test `backend` configuration argument"), + ); + } + + #[test] + fn specifying_backend_works() { + assert_try_from( + syn::parse_quote! { backend = "runtime-only" }, + Ok(E2EConfig { + backend: Backend::RuntimeOnly, + ..Default::default() + }), + ); + } + #[test] fn full_config_works() { assert_try_from( syn::parse_quote! { additional_contracts = "adder/Cargo.toml flipper/Cargo.toml", environment = crate::CustomEnvironment, + backend = "full", }, Ok(E2EConfig { additional_contracts: vec![ @@ -193,6 +281,7 @@ mod tests { "flipper/Cargo.toml".into(), ], environment: Some(syn::parse_quote! { crate::CustomEnvironment }), + backend: Backend::Full, }), ); } diff --git a/crates/e2e/src/backend.rs b/crates/e2e/src/backend.rs index 11d31abd350..c25db50c95e 100644 --- a/crates/e2e/src/backend.rs +++ b/crates/e2e/src/backend.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use super::Keypair; use crate::{ builders::CreateBuilderPartial, CallBuilderFinal, @@ -38,10 +39,8 @@ pub trait E2EBackend: /// General chain operations useful in contract testing. #[async_trait] pub trait ChainBackend { - /// Abstract type representing the entity that interacts with the chain. - type Actor: Send; - /// Identifier type for an actor. - type ActorId; + /// Account type. + type AccountId; /// Balance type. type Balance: Send; /// Error type. @@ -49,16 +48,19 @@ pub trait ChainBackend { /// Event log type. type EventLog; - /// Generate a new actor's credentials and fund it with the given amount from the - /// `sender` actor. + /// Generate a new account and fund it with the given `amount` of tokens from the + /// `origin`. async fn create_and_fund_account( &mut self, - origin: &Self::Actor, + origin: &Keypair, amount: Self::Balance, - ) -> Self::Actor; + ) -> Keypair; /// Returns the balance of `actor`. - async fn balance(&self, actor: Self::ActorId) -> Result; + async fn balance( + &mut self, + account: Self::AccountId, + ) -> Result; /// Executes a runtime call `call_name` for the `pallet_name`. /// The `call_data` is a `Vec`. @@ -73,7 +75,7 @@ pub trait ChainBackend { /// events that are associated with this transaction. async fn runtime_call<'a>( &mut self, - actor: &Self::Actor, + origin: &Keypair, pallet_name: &'a str, call_name: &'a str, call_data: Vec, @@ -83,8 +85,6 @@ pub trait ChainBackend { /// Contract-specific operations. #[async_trait] pub trait ContractsBackend { - /// Abstract type representing the entity that interacts with the chain. - type Actor; /// Error type. type Error; /// Event log type. @@ -101,7 +101,7 @@ pub trait ContractsBackend { async fn instantiate( &mut self, contract_name: &str, - caller: &Self::Actor, + caller: &Keypair, constructor: CreateBuilderPartial, value: E::Balance, storage_deposit_limit: Option, @@ -111,7 +111,7 @@ pub trait ContractsBackend { async fn instantiate_dry_run( &mut self, contract_name: &str, - caller: &Self::Actor, + caller: &Keypair, constructor: CreateBuilderPartial, value: E::Balance, storage_deposit_limit: Option, @@ -127,7 +127,7 @@ pub trait ContractsBackend { async fn upload( &mut self, contract_name: &str, - caller: &Self::Actor, + caller: &Keypair, storage_deposit_limit: Option, ) -> Result, Self::Error>; @@ -137,7 +137,7 @@ pub trait ContractsBackend { /// contains all events that are associated with this transaction. async fn call( &mut self, - caller: &Self::Actor, + caller: &Keypair, message: &CallBuilderFinal, value: E::Balance, storage_deposit_limit: Option, @@ -151,7 +151,7 @@ pub trait ContractsBackend { /// invoked message. async fn call_dry_run( &mut self, - caller: &Self::Actor, + caller: &Keypair, message: &CallBuilderFinal, value: E::Balance, storage_deposit_limit: Option, diff --git a/crates/e2e/src/client_utils.rs b/crates/e2e/src/client_utils.rs new file mode 100644 index 00000000000..4fbbaa988cb --- /dev/null +++ b/crates/e2e/src/client_utils.rs @@ -0,0 +1,61 @@ +use crate::log_info; +use std::{ + collections::BTreeMap, + path::PathBuf, +}; + +/// Generate a unique salt based on the system time. +pub fn salt() -> Vec { + use funty::Fundamental as _; + + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_else(|err| panic!("unable to get unix time: {err}")) + .as_millis() + .as_u128() + .to_le_bytes() + .to_vec() +} + +/// A registry of contracts that can be loaded. +pub struct ContractsRegistry { + contracts: BTreeMap, +} + +impl ContractsRegistry { + /// Create a new registry with the given contracts. + pub fn new>(contracts: impl IntoIterator) -> Self { + let contracts = contracts + .into_iter() + .map(|path| { + let wasm_path: PathBuf = path.into(); + let contract_name = wasm_path.file_stem().unwrap_or_else(|| { + panic!("Invalid contract wasm path '{}'", wasm_path.display(),) + }); + (contract_name.to_string_lossy().to_string(), wasm_path) + }) + .collect(); + + Self { contracts } + } + + /// Load the Wasm code for the given contract. + pub fn load_code(&self, contract: &str) -> Vec { + let wasm_path = self + .contracts + .get(&contract.replace('-', "_")) + .unwrap_or_else(|| + panic!( + "Unknown contract {contract}. Available contracts: {:?}.\n\ + For a contract to be built, add it as a dependency to the `Cargo.toml`, or add \ + the manifest path to `#[ink_e2e::test(additional_contracts = ..)]`", + self.contracts.keys() + ) + ); + let code = std::fs::read(wasm_path).unwrap_or_else(|err| { + panic!("Error loading '{}': {:?}", wasm_path.display(), err) + }); + log_info(&format!("{:?} has {} KiB", contract, code.len() / 1024)); + code + } +} diff --git a/crates/e2e/src/contract_build.rs b/crates/e2e/src/contract_build.rs index db4114c6855..1e3dc76847c 100644 --- a/crates/e2e/src/contract_build.rs +++ b/crates/e2e/src/contract_build.rs @@ -188,7 +188,6 @@ fn build_contract(path_to_cargo_toml: &Path) -> PathBuf { .expect("Wasm code artifact not generated") .canonicalize() .expect("Invalid dest bundle path") - .to_path_buf() } Err(err) => { panic!( diff --git a/crates/e2e/src/drink_client.rs b/crates/e2e/src/drink_client.rs new file mode 100644 index 00000000000..6f8884e64e3 --- /dev/null +++ b/crates/e2e/src/drink_client.rs @@ -0,0 +1,298 @@ +use crate::{ + builders::{ + constructor_exec_input, + CreateBuilderPartial, + }, + client_utils::{ + salt, + ContractsRegistry, + }, + log_error, + CallBuilderFinal, + CallDryRunResult, + CallResult, + ChainBackend, + ContractsBackend, + E2EBackend, + InstantiationResult, + UploadResult, +}; +use drink::{ + chain_api::ChainApi, + contract_api::ContractApi, + runtime::{ + MinimalRuntime, + Runtime, + }, + Sandbox, + DEFAULT_GAS_LIMIT, +}; +use ink_env::Environment; +use jsonrpsee::core::async_trait; +use pallet_contracts_primitives::{ + CodeUploadReturnValue, + ContractInstantiateResult, + ContractResult, + InstantiateReturnValue, +}; +use scale::{ + Decode, + Encode, +}; +use sp_core::{ + crypto::AccountId32, + sr25519::Pair, + Pair as _, +}; +use std::{ + marker::PhantomData, + path::PathBuf, +}; +use subxt::dynamic::Value; +use subxt_signer::sr25519::Keypair; + +pub struct Client { + sandbox: Sandbox, + contracts: ContractsRegistry, + _phantom: PhantomData<(AccountId, Hash)>, +} + +unsafe impl Send for Client {} + +impl Client { + pub fn new>(contracts: impl IntoIterator) -> Self { + let mut sandbox = Sandbox::new().expect("Failed to initialize Drink! sandbox"); + Self::fund_accounts(&mut sandbox); + + Self { + sandbox, + contracts: ContractsRegistry::new(contracts), + _phantom: Default::default(), + } + } + + fn fund_accounts(sandbox: &mut Sandbox) { + const TOKENS: u128 = 1_000_000_000_000_000; + + let accounts = [ + crate::alice(), + crate::bob(), + crate::charlie(), + crate::dave(), + crate::eve(), + crate::ferdie(), + crate::one(), + crate::two(), + ] + .map(|kp| kp.public_key().0) + .map(AccountId32::new); + for account in accounts.into_iter() { + sandbox.add_tokens(account, TOKENS); + } + } +} + +#[async_trait] +impl + Send, Hash> ChainBackend for Client { + type AccountId = AccountId; + type Balance = u128; + type Error = (); + type EventLog = (); + + async fn create_and_fund_account( + &mut self, + _origin: &Keypair, + amount: Self::Balance, + ) -> Keypair { + let (pair, seed) = Pair::generate(); + + self.sandbox.add_tokens(pair.public().0.into(), amount); + + Keypair::from_seed(seed).expect("Failed to create keypair") + } + + async fn balance( + &mut self, + account: Self::AccountId, + ) -> Result { + let account = AccountId32::new(*account.as_ref()); + Ok(self.sandbox.balance(&account)) + } + + async fn runtime_call<'a>( + &mut self, + _origin: &Keypair, + _pallet_name: &'a str, + _call_name: &'a str, + _call_data: Vec, + ) -> Result { + todo!("https://github.com/Cardinal-Cryptography/drink/issues/36") + } +} + +#[async_trait] +impl< + AccountId: Clone + Send + Sync + From<[u8; 32]> + AsRef<[u8; 32]>, + Hash: From<[u8; 32]>, + E: Environment + 'static, + > ContractsBackend for Client +{ + type Error = (); + type EventLog = (); + + async fn instantiate( + &mut self, + contract_name: &str, + caller: &Keypair, + constructor: CreateBuilderPartial, + value: E::Balance, + storage_deposit_limit: Option, + ) -> Result, Self::Error> { + let code = self.contracts.load_code(contract_name); + let data = constructor_exec_input(constructor); + + let result = self.sandbox.deploy_contract( + code, + value, + data, + salt(), + keypair_to_account(caller), + DEFAULT_GAS_LIMIT, + storage_deposit_limit, + ); + + let account_id_raw = match &result.result { + Err(err) => { + log_error(&format!("Instantiation failed: {err:?}")); + return Err(()) // todo: make a proper error type + } + Ok(res) => *res.account_id.as_ref(), + }; + let account_id = AccountId::from(account_id_raw); + + Ok(InstantiationResult { + account_id: account_id.clone(), + // We need type remapping here because of the different `EventRecord` types. + dry_run: ContractInstantiateResult { + gas_consumed: result.gas_consumed, + gas_required: result.gas_required, + storage_deposit: result.storage_deposit, + debug_message: result.debug_message, + result: result.result.map(|r| { + InstantiateReturnValue { + result: r.result, + account_id, + } + }), + events: None, + }, + events: (), // todo: https://github.com/Cardinal-Cryptography/drink/issues/32 + }) + } + + async fn instantiate_dry_run( + &mut self, + _contract_name: &str, + _caller: &Keypair, + _constructor: CreateBuilderPartial, + _value: E::Balance, + _storage_deposit_limit: Option, + ) -> ContractInstantiateResult { + todo!("https://github.com/Cardinal-Cryptography/drink/issues/37") + } + + async fn upload( + &mut self, + contract_name: &str, + caller: &Keypair, + storage_deposit_limit: Option, + ) -> Result, Self::Error> { + let code = self.contracts.load_code(contract_name); + + let result = match self.sandbox.upload_contract( + code, + keypair_to_account(caller), + storage_deposit_limit, + ) { + Ok(result) => result, + Err(err) => { + log_error(&format!("Upload failed: {err:?}")); + return Err(()) // todo: make a proper error type + } + }; + + Ok(UploadResult { + code_hash: result.code_hash.0.into(), + dry_run: Ok(CodeUploadReturnValue { + code_hash: result.code_hash.0.into(), + deposit: result.deposit, + }), + events: (), + }) + } + + async fn call( + &mut self, + caller: &Keypair, + message: &CallBuilderFinal, + value: E::Balance, + storage_deposit_limit: Option, + ) -> Result, Self::Error> + where + CallBuilderFinal: Clone, + { + let account_id = message.clone().params().callee().clone(); + let exec_input = Encode::encode(message.clone().params().exec_input()); + let account_id = (*account_id.as_ref()).into(); + + let result = self.sandbox.call_contract( + account_id, + value, + exec_input, + keypair_to_account(caller), + DEFAULT_GAS_LIMIT, + storage_deposit_limit, + ); + + Ok(CallResult { + // We need type remapping here because of the different `EventRecord` types. + dry_run: CallDryRunResult { + exec_result: ContractResult { + gas_consumed: result.gas_consumed, + gas_required: result.gas_required, + storage_deposit: result.storage_deposit, + debug_message: result.debug_message, + result: result.result, + events: None, + }, + _marker: Default::default(), + }, + events: (), // todo: https://github.com/Cardinal-Cryptography/drink/issues/32 + }) + } + + async fn call_dry_run( + &mut self, + _caller: &Keypair, + _message: &CallBuilderFinal, + _value: E::Balance, + _storage_deposit_limit: Option, + ) -> CallDryRunResult + where + CallBuilderFinal: Clone, + { + todo!("https://github.com/Cardinal-Cryptography/drink/issues/37") + } +} + +impl< + AccountId: Clone + Send + Sync + From<[u8; 32]> + AsRef<[u8; 32]>, + Hash: From<[u8; 32]>, + E: Environment + 'static, + > E2EBackend for Client +{ +} + +fn keypair_to_account(keypair: &Keypair) -> AccountId32 { + AccountId32::from(keypair.public_key().0) +} diff --git a/crates/e2e/src/lib.rs b/crates/e2e/src/lib.rs index 14c5d36b1e7..2a09ba6d6bb 100644 --- a/crates/e2e/src/lib.rs +++ b/crates/e2e/src/lib.rs @@ -21,8 +21,10 @@ mod backend; mod builders; +mod client_utils; mod contract_build; mod contract_results; +mod drink_client; mod error; pub mod events; mod node_proc; @@ -44,6 +46,7 @@ pub use contract_results::{ InstantiationResult, UploadResult, }; +pub use drink_client::Client as DrinkClient; pub use ink_e2e_macro::test; pub use node_proc::{ TestNodeProcess, diff --git a/crates/e2e/src/subxt_client.rs b/crates/e2e/src/subxt_client.rs index 23344fc5979..0a2d0a26edc 100644 --- a/crates/e2e/src/subxt_client.rs +++ b/crates/e2e/src/subxt_client.rs @@ -52,14 +52,15 @@ use scale::{ Encode, }; #[cfg(feature = "std")] -use std::{ - collections::BTreeMap, - fmt::Debug, - path::PathBuf, -}; +use std::fmt::Debug; +use std::path::PathBuf; use crate::{ backend::ChainBackend, + client_utils::{ + salt, + ContractsRegistry, + }, events, ContractsBackend, E2EBackend, @@ -101,7 +102,7 @@ where E: Environment, { api: ContractsApi, - contracts: BTreeMap, + contracts: ContractsRegistry, } impl Client @@ -119,50 +120,16 @@ where E::Hash: Debug + scale::Encode, { /// Creates a new [`Client`] instance using a `subxt` client. - pub async fn new

( + pub async fn new>( client: subxt::OnlineClient, contracts: impl IntoIterator, - ) -> Self - where - PathBuf: From

, - { - let contracts = contracts - .into_iter() - .map(|path| { - let wasm_path = PathBuf::from(path); - let contract_name = wasm_path.file_stem().unwrap_or_else(|| { - panic!("Invalid contract wasm path '{}'", wasm_path.display(),) - }); - (contract_name.to_string_lossy().to_string(), wasm_path) - }) - .collect(); - + ) -> Self { Self { api: ContractsApi::new(client).await, - contracts, + contracts: ContractsRegistry::new(contracts), } } - /// Load the Wasm code for the given contract. - fn load_code(&self, contract: &str) -> Vec { - let wasm_path = self - .contracts - .get(&contract.replace('-', "_")) - .unwrap_or_else(|| - panic!( - "Unknown contract {contract}. Available contracts: {:?}.\n\ - For a contract to be built, add it as a dependency to the `Cargo.toml`, or add \ - the manifest path to `#[ink_e2e::test(additional_contracts = ..)]`", - self.contracts.keys() - ) - ); - let code = std::fs::read(wasm_path).unwrap_or_else(|err| { - panic!("Error loading '{}': {:?}", wasm_path.display(), err) - }); - log_info(&format!("{:?} has {} KiB", contract, code.len() / 1024)); - code - } - /// Executes an `instantiate_with_code` call and captures the resulting events. async fn exec_instantiate( &mut self, @@ -175,7 +142,7 @@ where where Args: scale::Encode, { - let salt = Self::salt(); + let salt = salt(); let data = constructor_exec_input(constructor); // dry run the instantiate to calculate the gas limit @@ -255,19 +222,6 @@ where }) } - /// Generate a unique salt based on the system time. - fn salt() -> Vec { - use funty::Fundamental as _; - - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_else(|err| panic!("unable to get unix time: {err}")) - .as_millis() - .as_u128() - .to_le_bytes() - .to_vec() - } - /// Executes an `upload` call and captures the resulting events. async fn exec_upload( &mut self, @@ -364,17 +318,16 @@ where + scale::HasCompact + serde::Serialize, { - type Actor = Keypair; - type ActorId = E::AccountId; + type AccountId = E::AccountId; type Balance = E::Balance; type Error = Error; type EventLog = ExtrinsicEvents; async fn create_and_fund_account( &mut self, - origin: &Self::Actor, + origin: &Keypair, amount: Self::Balance, - ) -> Self::Actor { + ) -> Keypair { let (_, phrase, _) = ::generate_with_phrase(None); let phrase = @@ -401,14 +354,17 @@ where keypair } - async fn balance(&self, actor: Self::ActorId) -> Result { + async fn balance( + &mut self, + account: Self::AccountId, + ) -> Result { let account_addr = subxt::dynamic::storage( "System", "Account", vec![ // Something that encodes to an AccountId32 is what we need for the map // key here: - Value::from_bytes(&actor), + Value::from_bytes(&account), ], ); @@ -440,20 +396,20 @@ where Error::::Balance(format!("{balance:?} failed to convert from u128")) })?; - log_info(&format!("balance of contract {actor:?} is {balance:?}")); + log_info(&format!("balance of contract {account:?} is {balance:?}")); Ok(balance) } async fn runtime_call<'a>( &mut self, - actor: &Self::Actor, + origin: &Keypair, pallet_name: &'a str, call_name: &'a str, call_data: Vec, ) -> Result { let tx_events = self .api - .runtime_call(actor, pallet_name, call_name, call_data) + .runtime_call(origin, pallet_name, call_name, call_data) .await; for evt in tx_events.iter() { @@ -504,19 +460,18 @@ where + serde::Serialize, E::Hash: Debug + Send + scale::Encode, { - type Actor = Keypair; type Error = Error; type EventLog = ExtrinsicEvents; async fn instantiate( &mut self, contract_name: &str, - caller: &Self::Actor, + caller: &Keypair, constructor: CreateBuilderPartial, value: E::Balance, storage_deposit_limit: Option, ) -> Result, Self::Error> { - let code = self.load_code(contract_name); + let code = self.contracts.load_code(contract_name); let ret = self .exec_instantiate::( caller, @@ -533,22 +488,21 @@ where async fn instantiate_dry_run( &mut self, contract_name: &str, - caller: &Self::Actor, + caller: &Keypair, constructor: CreateBuilderPartial, value: E::Balance, storage_deposit_limit: Option, ) -> ContractInstantiateResult { - let code = self.load_code(contract_name); + let code = self.contracts.load_code(contract_name); let data = constructor_exec_input(constructor); - let salt = Self::salt(); self.api .instantiate_with_code_dry_run( value, storage_deposit_limit, code, data, - salt, + salt(), caller, ) .await @@ -557,10 +511,10 @@ where async fn upload( &mut self, contract_name: &str, - caller: &Self::Actor, + caller: &Keypair, storage_deposit_limit: Option, ) -> Result, Self::Error> { - let code = self.load_code(contract_name); + let code = self.contracts.load_code(contract_name); let ret = self .exec_upload(caller, code, storage_deposit_limit) .await?; @@ -570,7 +524,7 @@ where async fn call( &mut self, - caller: &Self::Actor, + caller: &Keypair, message: &CallBuilderFinal, value: E::Balance, storage_deposit_limit: Option, @@ -623,7 +577,7 @@ where async fn call_dry_run( &mut self, - caller: &Self::Actor, + caller: &Keypair, message: &CallBuilderFinal, value: E::Balance, storage_deposit_limit: Option, diff --git a/integration-tests/e2e-runtime-only-backend/.gitignore b/integration-tests/e2e-runtime-only-backend/.gitignore new file mode 100644 index 00000000000..bf910de10af --- /dev/null +++ b/integration-tests/e2e-runtime-only-backend/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/integration-tests/e2e-runtime-only-backend/Cargo.toml b/integration-tests/e2e-runtime-only-backend/Cargo.toml new file mode 100644 index 00000000000..33ea407c48b --- /dev/null +++ b/integration-tests/e2e-runtime-only-backend/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "e2e-runtime-only-backend" +version = "4.2.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../crates/ink", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +ink_e2e = { path = "../../crates/e2e" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/e2e-runtime-only-backend/lib.rs b/integration-tests/e2e-runtime-only-backend/lib.rs new file mode 100644 index 00000000000..6f9eb11b719 --- /dev/null +++ b/integration-tests/e2e-runtime-only-backend/lib.rs @@ -0,0 +1,77 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod flipper { + #[ink(storage)] + pub struct Flipper { + value: bool, + } + + impl Flipper { + /// Creates a new flipper smart contract initialized with the given value. + #[ink(constructor)] + pub fn new(init_value: bool) -> Self { + Self { value: init_value } + } + + /// Creates a new flipper smart contract initialized to `false`. + #[ink(constructor)] + pub fn new_default() -> Self { + Self::new(Default::default()) + } + + /// Flips the current value of the Flipper's boolean. + #[ink(message)] + pub fn flip(&mut self) { + self.value = !self.value; + } + + /// Returns the current value of the Flipper's boolean. + #[ink(message)] + pub fn get(&self) -> bool { + self.value + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + use super::*; + use ink_e2e::ContractsBackend; + + type E2EResult = std::result::Result>; + + #[ink_e2e::test(backend = "runtime-only")] + async fn it_works(mut client: Client) -> E2EResult<()> { + // given + let constructor = FlipperRef::new(false); + let contract = client + .instantiate( + "e2e-runtime-only-backend", + &ink_e2e::alice(), + constructor, + 0, + None, + ) + .await + .expect("instantiate failed"); + + let mut call = contract.call::(); + + // when + let _flip_res = client + .call(&ink_e2e::bob(), &call.flip(), 0, None) + .await + .expect("flip failed"); + + // then + let get_res = client + .call(&ink_e2e::bob(), &call.get(), 0, None) + .await + .expect("get failed"); + + assert!(matches!(get_res.return_value(), true)); + + Ok(()) + } + } +}