diff --git a/Cargo.toml b/Cargo.toml index ada0acdb96..c5969f2466 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +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" } +drink = { version = "=0.1.3" } either = { version = "1.5", default-features = false } funty = { version = "2.0.0" } heck = { version = "0.4.0" } @@ -66,6 +66,7 @@ sha2 = { version = "0.10" } sha3 = { version = "0.10" } static_assertions = { version = "1.1" } subxt = { version = "0.31.0" } +subxt-metadata = { version = "0.31.0" } subxt-signer = { version = "0.31.0" } syn = { version = "2" } synstructure = { version = "0.13.0" } diff --git a/crates/e2e/Cargo.toml b/crates/e2e/Cargo.toml index 1a84ff241f..36d7ab1258 100644 --- a/crates/e2e/Cargo.toml +++ b/crates/e2e/Cargo.toml @@ -33,6 +33,7 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } scale = { package = "parity-scale-codec", workspace = true } subxt = { workspace = true } +subxt-metadata = { workspace = true, optional = true } subxt-signer = { workspace = true, features = ["subxt", "sr25519"] } wasm-instrument = { workspace = true } @@ -52,5 +53,6 @@ default = ["std"] std = [] drink = [ "dep:drink", + "subxt-metadata", "ink_e2e_macro/drink", ] diff --git a/crates/e2e/src/backend.rs b/crates/e2e/src/backend.rs index c25db50c95..6abb0351c1 100644 --- a/crates/e2e/src/backend.rs +++ b/crates/e2e/src/backend.rs @@ -73,6 +73,9 @@ pub trait ChainBackend { /// /// Returns when the transaction is included in a block. The return value contains all /// events that are associated with this transaction. + /// + /// Since we might run node with an arbitrary runtime, this method inherently must + /// support dynamic calls. async fn runtime_call<'a>( &mut self, origin: &Keypair, diff --git a/crates/e2e/src/drink_client.rs b/crates/e2e/src/drink_client.rs index 6f8884e64e..a1e6eaec71 100644 --- a/crates/e2e/src/drink_client.rs +++ b/crates/e2e/src/drink_client.rs @@ -18,7 +18,10 @@ use crate::{ UploadResult, }; use drink::{ - chain_api::ChainApi, + chain_api::{ + ChainApi, + RuntimeCall, + }, contract_api::ContractApi, runtime::{ MinimalRuntime, @@ -48,7 +51,10 @@ use std::{ marker::PhantomData, path::PathBuf, }; -use subxt::dynamic::Value; +use subxt::{ + dynamic::Value, + tx::TxPayload, +}; use subxt_signer::sr25519::Keypair; pub struct Client { @@ -121,12 +127,39 @@ impl + Send, Hash> ChainBackend for Client( &mut self, - _origin: &Keypair, - _pallet_name: &'a str, - _call_name: &'a str, - _call_data: Vec, + origin: &Keypair, + pallet_name: &'a str, + call_name: &'a str, + call_data: Vec, ) -> Result { - todo!("https://github.com/Cardinal-Cryptography/drink/issues/36") + // Since in general, `ChainBackend::runtime_call` must be dynamic, we have to + // perform some translation here in order to invoke strongly-typed drink! + // API. + + // Get metadata of the drink! runtime, so that we can encode the call object. + // Panic on error - metadata of the static im-memory runtime should always be + // available. + let raw_metadata: Vec = MinimalRuntime::metadata().into(); + let metadata = subxt_metadata::Metadata::decode(&mut raw_metadata.as_slice()) + .expect("Failed to decode metadata"); + + // Encode the call object. + let call = subxt::dynamic::tx(pallet_name, call_name, call_data); + let encoded_call = call.encode_call_data(&metadata.into()).map_err(|_| ())?; + + // Decode the call object. + // Panic on error - we just encoded a validated call object, so it should be + // decodable. + let decoded_call = + RuntimeCall::::decode(&mut encoded_call.as_slice()) + .expect("Failed to decode runtime call"); + + // Execute the call. + self.sandbox + .runtime_call(decoded_call, Some(keypair_to_account(origin)).into()) + .map_err(|_| ())?; + + Ok(()) } } diff --git a/integration-tests/e2e-runtime-only-backend/lib.rs b/integration-tests/e2e-runtime-only-backend/lib.rs index 6f9eb11b71..53ba83a47b 100644 --- a/integration-tests/e2e-runtime-only-backend/lib.rs +++ b/integration-tests/e2e-runtime-only-backend/lib.rs @@ -31,20 +31,44 @@ pub mod flipper { pub fn get(&self) -> bool { self.value } + + /// Returns the current balance of the Flipper. + #[ink(message)] + pub fn get_contract_balance(&self) -> Balance { + self.env().balance() + } } #[cfg(all(test, feature = "e2e-tests"))] mod e2e_tests { use super::*; - use ink_e2e::ContractsBackend; + use ink::env::DefaultEnvironment; + use ink_e2e::{ + subxt::dynamic::Value, + ChainBackend, + ContractsBackend, + E2EBackend, + InstantiationResult, + }; 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 + /// Deploys the flipper contract with `initial_value` and returns the contract + /// instantiation result. + /// + /// Uses `ink_e2e::alice()` as the caller. + async fn deploy( + client: &mut Client, + initial_value: bool, + ) -> Result< + InstantiationResult< + DefaultEnvironment, + >::EventLog, + >, + >::Error, + > { + let constructor = FlipperRef::new(initial_value); + client .instantiate( "e2e-runtime-only-backend", &ink_e2e::alice(), @@ -53,11 +77,23 @@ pub mod flipper { None, ) .await - .expect("instantiate failed"); + } - let mut call = contract.call::(); + /// Tests standard flipper scenario: + /// - deploy the flipper contract with initial value `false` + /// - flip the flipper + /// - get the flipper's value + /// - assert that the value is `true` + #[ink_e2e::test(backend = "runtime-only")] + async fn it_works(mut client: Client) -> E2EResult<()> { + // given + const INITIAL_VALUE: bool = false; + let contract = deploy(&mut client, INITIAL_VALUE) + .await + .expect("deploy failed"); // when + let mut call = contract.call::(); let _flip_res = client .call(&ink_e2e::bob(), &call.flip(), 0, None) .await @@ -68,9 +104,49 @@ pub mod flipper { .call(&ink_e2e::bob(), &call.get(), 0, None) .await .expect("get failed"); + assert_eq!(get_res.return_value(), !INITIAL_VALUE); - assert!(matches!(get_res.return_value(), true)); + Ok(()) + } + + /// Tests runtime call scenario: + /// - deploy the flipper contract + /// - get the contract's balance + /// - transfer some funds to the contract using runtime call + /// - get the contract's balance again + /// - assert that the contract's balance increased by the transferred amount + #[ink_e2e::test(backend = "runtime-only")] + async fn runtime_call_works() -> E2EResult<()> { + // given + let contract = deploy(&mut client, false).await.expect("deploy failed"); + let call = contract.call::(); + + let old_balance = client + .call(&ink_e2e::alice(), &call.get_contract_balance(), 0, None) + .await + .expect("get_contract_balance failed") + .return_value(); + + const ENDOWMENT: u128 = 10; + + // when + let call_data = vec![ + Value::from_bytes(&contract.account_id), + Value::u128(ENDOWMENT), + ]; + client + .runtime_call(&ink_e2e::alice(), "Balances", "transfer", call_data) + .await + .expect("runtime call failed"); + + // then + let new_balance = client + .call(&ink_e2e::alice(), &call.get_contract_balance(), 0, None) + .await + .expect("get_contract_balance failed") + .return_value(); + assert_eq!(old_balance + ENDOWMENT, new_balance); Ok(()) } }