diff --git a/aleph-client/Cargo.lock b/aleph-client/Cargo.lock index e39596ee91..f0ce7e0d9c 100644 --- a/aleph-client/Cargo.lock +++ b/aleph-client/Cargo.lock @@ -53,7 +53,7 @@ version = "2.6.0" dependencies = [ "anyhow", "async-trait", - "contract-metadata", + "contract-metadata 2.0.0-beta.1", "contract-transcode", "frame-support", "futures", @@ -69,6 +69,7 @@ dependencies = [ "sp-runtime 6.0.0", "subxt", "thiserror", + "tokio", ] [[package]] @@ -362,8 +363,21 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" [[package]] name = "contract-metadata" version = "2.0.0-beta" +source = "git+https://github.com/obrok/cargo-contract?branch=send-sync-env-types#c0ade59847f09ef558c8bca0dc2cf8f78b031188" +dependencies = [ + "anyhow", + "impl-serde", + "semver", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "contract-metadata" +version = "2.0.0-beta.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40248c6a648b3679ea52bb83d8921246d0347644eaf7b0eec4c5601fa8feb651" +checksum = "7bff7703529b16e9d8ba0d54e842b2051691772a822eb9bc130a91183ff9f6a6" dependencies = [ "anyhow", "impl-serde", @@ -376,11 +390,10 @@ dependencies = [ [[package]] name = "contract-transcode" version = "2.0.0-beta" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61159f8e266d4892be25f2b1e7ff2c4c6dd4338ca26498d907e5532a52a28e5f" +source = "git+https://github.com/obrok/cargo-contract?branch=send-sync-env-types#c0ade59847f09ef558c8bca0dc2cf8f78b031188" dependencies = [ "anyhow", - "contract-metadata", + "contract-metadata 2.0.0-beta", "env_logger", "escape8259", "hex", @@ -2578,18 +2591,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.148" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc" +checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.148" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c" +checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" dependencies = [ "proc-macro2", "quote", diff --git a/aleph-client/Cargo.toml b/aleph-client/Cargo.toml index 3b61da710f..7b6850dd1d 100644 --- a/aleph-client/Cargo.toml +++ b/aleph-client/Cargo.toml @@ -14,7 +14,7 @@ log = "0.4" serde_json = { version = "1.0" } thiserror = "1.0" contract-metadata = "2.0.0-beta" -contract-transcode = "2.0.0-beta" +contract-transcode = { git = "https://github.com/obrok/cargo-contract", branch = "send-sync-env-types" } ink_metadata = "4.0.0-beta" subxt = "0.25.0" futures = "0.3.25" @@ -25,3 +25,6 @@ sp-core = { git = "https://github.com/Cardinal-Cryptography/substrate.git", bran sp-runtime = { git = "https://github.com/Cardinal-Cryptography/substrate.git", branch = "aleph-v0.9.32" } pallet-contracts-primitives = { git = "https://github.com/Cardinal-Cryptography/substrate.git", branch = "aleph-v0.9.32" } primitives = { path = "../primitives" } + +[dev-dependencies] +tokio = "1.21" diff --git a/aleph-client/src/contract/event.rs b/aleph-client/src/contract/event.rs new file mode 100644 index 0000000000..7f8a855d86 --- /dev/null +++ b/aleph-client/src/contract/event.rs @@ -0,0 +1,131 @@ +//! Utilities for listening for contract events. +//! +//! To use the module you will need to pass a connection, some contracts and an `UnboundedSender` to the +//! [listen_contract_events] function. You most likely want to `tokio::spawn` the resulting future, so that it runs +//! concurrently. +//! +//! ```no_run +//! # use std::sync::Arc; +//! # use std::sync::mpsc::channel; +//! # use std::time::Duration; +//! # use aleph_client::{AccountId, Connection, SignedConnection}; +//! # use aleph_client::contract::ContractInstance; +//! # use aleph_client::contract::event::{listen_contract_events}; +//! # use anyhow::Result; +//! use futures::{channel::mpsc::unbounded, StreamExt}; +//! +//! # async fn example(conn: Connection, signed_conn: SignedConnection, address1: AccountId, address2: AccountId, path1: &str, path2: &str) -> Result<()> { +//! // The `Arc` makes it possible to pass a reference to the contract to another thread +//! let contract1 = Arc::new(ContractInstance::new(address1, path1)?); +//! let contract2 = Arc::new(ContractInstance::new(address2, path2)?); +//! +//! let conn_copy = conn.clone(); +//! let contract1_copy = contract1.clone(); +//! let contract2_copy = contract2.clone(); +//! +//! let (tx, mut rx) = unbounded(); +//! let listen = || async move { +//! listen_contract_events(&conn, &[contract1_copy.as_ref(), contract2_copy.as_ref()], tx).await?; +//! >::Ok(()) +//! }; +//! let join = tokio::spawn(listen()); +//! +//! contract1.contract_exec0(&signed_conn, "some_method").await?; +//! contract2.contract_exec0(&signed_conn, "some_other_method").await?; +//! +//! println!("Received event {:?}", rx.next().await); +//! +//! rx.close(); +//! join.await??; +//! +//! # Ok(()) +//! # } +//! ``` + +use std::collections::HashMap; + +use anyhow::{bail, Result}; +use contract_transcode::Value; +use futures::{channel::mpsc::UnboundedSender, StreamExt}; + +use crate::{contract::ContractInstance, AccountId, Connection}; + +/// Represents a single event emitted by a contract. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct ContractEvent { + /// The address of the contract that emitted the event. + pub contract: AccountId, + /// The name of the event. + pub name: Option, + /// Data contained in the event. + pub data: HashMap, +} + +/// Starts an event listening loop. +/// +/// Will send contract event and every error encountered while fetching through the provided [UnboundedSender]. +/// Only events coming from the address of one of the `contracts` will be decoded. +/// +/// The loop will terminate once `sender` is closed. The loop may also terminate in case of errors while fetching blocks +/// or decoding events (pallet events, contract event decoding errors are sent over the channel). +pub async fn listen_contract_events( + conn: &Connection, + contracts: &[&ContractInstance], + sender: UnboundedSender>, +) -> Result<()> { + let mut block_subscription = conn.as_client().blocks().subscribe_finalized().await?; + + while let Some(block) = block_subscription.next().await { + if sender.is_closed() { + break; + } + + let block = block?; + + for event in block.events().await?.iter() { + let event = event?; + + if let Some(event) = + event.as_event::()? + { + if let Some(contract) = contracts + .iter() + .find(|contract| contract.address() == &event.contract) + { + let data = zero_prefixed(&event.data); + let event = contract + .transcoder + .decode_contract_event(&mut data.as_slice()); + + sender.unbounded_send( + event.and_then(|event| build_event(contract.address().clone(), event)), + )?; + } + } + } + } + + Ok(()) +} + +/// The contract transcoder assumes there is an extra byte (that it discards) indicating the size of the data. However, +/// data arriving through the subscription as used in this file don't have this extra byte. This function adds it. +fn zero_prefixed(data: &[u8]) -> Vec { + let mut result = vec![0]; + result.extend_from_slice(data); + result +} + +fn build_event(address: AccountId, event_data: Value) -> Result { + match event_data { + Value::Map(map) => Ok(ContractEvent { + contract: address, + name: map.ident(), + data: map + .iter() + .map(|(key, value)| (key.to_string(), value.clone())) + .collect(), + }), + _ => bail!("Contract event data is not a map"), + } +} diff --git a/aleph-client/src/contract/mod.rs b/aleph-client/src/contract/mod.rs index ddff119754..d692194026 100644 --- a/aleph-client/src/contract/mod.rs +++ b/aleph-client/src/contract/mod.rs @@ -43,6 +43,7 @@ //! ``` mod convertible_value; +pub mod event; use std::fmt::{Debug, Formatter}; diff --git a/e2e-tests/Cargo.lock b/e2e-tests/Cargo.lock index faad2015b8..c4cdb3df49 100644 --- a/e2e-tests/Cargo.lock +++ b/e2e-tests/Cargo.lock @@ -54,7 +54,6 @@ dependencies = [ "aleph_client", "anyhow", "assert2", - "clap 3.2.23", "env_logger 0.8.4", "frame-support", "frame-system", @@ -83,7 +82,7 @@ version = "2.6.0" dependencies = [ "anyhow", "async-trait", - "contract-metadata", + "contract-metadata 2.0.0-beta.1", "contract-transcode", "frame-support", "futures", @@ -407,49 +406,19 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.23" +version = "4.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39" dependencies = [ - "atty", "bitflags", - "clap_derive 3.2.18", - "clap_lex 0.2.4", - "indexmap", - "once_cell", - "strsim", - "termcolor", - "textwrap", -] - -[[package]] -name = "clap" -version = "4.0.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d63b9e9c07271b9957ad22c173bae2a4d9a81127680962039296abcd2f8251d" -dependencies = [ - "bitflags", - "clap_derive 4.0.21", - "clap_lex 0.3.0", + "clap_derive", + "clap_lex", "is-terminal", "once_cell", "strsim", "termcolor", ] -[[package]] -name = "clap_derive" -version = "3.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "clap_derive" version = "4.0.21" @@ -463,15 +432,6 @@ dependencies = [ "syn", ] -[[package]] -name = "clap_lex" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - [[package]] name = "clap_lex" version = "0.3.0" @@ -497,6 +457,19 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" +[[package]] +name = "contract-metadata" +version = "2.0.0-beta" +source = "git+https://github.com/obrok/cargo-contract?branch=send-sync-env-types#c0ade59847f09ef558c8bca0dc2cf8f78b031188" +dependencies = [ + "anyhow", + "impl-serde", + "semver", + "serde", + "serde_json", + "url", +] + [[package]] name = "contract-metadata" version = "2.0.0-beta.1" @@ -513,12 +486,12 @@ dependencies = [ [[package]] name = "contract-transcode" -version = "2.0.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25e184ac4c29748e28c026f4c443fb94c002ad0877f0b190dccd624df8f893f" +version = "2.0.0-beta" +source = "git+https://github.com/obrok/cargo-contract?branch=send-sync-env-types#c0ade59847f09ef558c8bca0dc2cf8f78b031188" dependencies = [ "anyhow", - "contract-metadata", + "contract-metadata 2.0.0-beta", + "env_logger 0.9.3", "escape8259", "hex", "indexmap", @@ -922,6 +895,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "env_logger" version = "0.10.0" @@ -4480,7 +4466,7 @@ name = "synthetic-link" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.0.29", + "clap", "env_logger 0.10.0", "log", "reqwest", @@ -4519,12 +4505,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" version = "1.0.37" diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index edc7a56303..2f78045ad2 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -6,7 +6,6 @@ license = "Apache 2.0" [dependencies] anyhow = "1.0" -clap = { version = "3.0", features = ["derive"] } env_logger = "0.8" hex = "0.4.3" log = "0.4" diff --git a/e2e-tests/src/test/adder.rs b/e2e-tests/src/test/adder.rs index 1deeea6285..c1d71be842 100644 --- a/e2e-tests/src/test/adder.rs +++ b/e2e-tests/src/test/adder.rs @@ -1,10 +1,13 @@ -use std::str::FromStr; +use std::{fmt::Debug, str::FromStr, sync::Arc}; use aleph_client::{ - contract::ContractInstance, AccountId, Connection, ConnectionApi, SignedConnectionApi, + contract::{event::listen_contract_events, ContractInstance}, + contract_transcode::Value, + AccountId, ConnectionApi, SignedConnectionApi, }; use anyhow::{Context, Result}; use assert2::assert; +use futures::{channel::mpsc::unbounded, StreamExt}; use crate::{config::setup_test, test::helpers::basic_test_context}; @@ -15,34 +18,50 @@ pub async fn adder() -> Result<()> { let config = setup_test(); let (conn, _authority, account) = basic_test_context(config).await?; - let contract = AdderInstance::new( + + let contract = Arc::new(AdderInstance::new( &config.test_case_params.adder, &config.test_case_params.adder_metadata, - )?; + )?); + + let listen_conn = conn.clone(); + let listen_contract = contract.clone(); + let (tx, mut rx) = unbounded(); + let listen = || async move { + listen_contract_events(&listen_conn, &[listen_contract.as_ref().into()], tx).await?; + >::Ok(()) + }; + let join = tokio::spawn(listen()); let increment = 10; let before = contract.get(&conn).await?; - let raw_connection = Connection::new(&config.node).await; - contract - .add(&account.sign(&raw_connection), increment) - .await?; + + contract.add(&account.sign(&conn), increment).await?; + + let event = rx.next().await.context("No event received")??; + assert!(event.name == Some("ValueChanged".to_string())); + assert!(event.contract == *contract.contract.address()); + assert!(event.data["new_value"] == Value::UInt(before as u128 + 10)); + let after = contract.get(&conn).await?; assert!(after == before + increment); let new_name = "test"; - contract - .set_name(&account.sign(&raw_connection), None) - .await?; + contract.set_name(&account.sign(&conn), None).await?; assert!(contract.get_name(&conn).await?.is_none()); contract - .set_name(&account.sign(&raw_connection), Some(new_name)) + .set_name(&account.sign(&conn), Some(new_name)) .await?; assert!(contract.get_name(&conn).await? == Some(new_name.to_string())); + rx.close(); + join.await??; + Ok(()) } -pub(super) struct AdderInstance { +#[derive(Debug)] +struct AdderInstance { contract: ContractInstance, } diff --git a/e2e-tests/src/test/helpers.rs b/e2e-tests/src/test/helpers.rs index 4ebe74a377..b8b30a35b8 100644 --- a/e2e-tests/src/test/helpers.rs +++ b/e2e-tests/src/test/helpers.rs @@ -73,12 +73,12 @@ pub fn alephs(basic_unit_amount: Balance) -> Balance { /// Prepares a `(conn, authority, account)` triple with some money in `account` for fees. pub async fn basic_test_context( config: &Config, -) -> Result<(SignedConnection, KeyPairWrapper, KeyPairWrapper)> { - let conn = config.get_first_signed_connection().await; +) -> Result<(Connection, KeyPairWrapper, KeyPairWrapper)> { + let conn = Connection::new(&config.node).await; let authority = KeyPairWrapper(aleph_client::keypair_from_string(&config.sudo_seed)); let account = random_account(); - transfer(&conn, &account, alephs(100)).await?; + transfer(&authority.sign(&conn), &account, alephs(100)).await?; - Ok((conn.clone(), authority, account)) + Ok((conn, authority, account)) }