Skip to content

Commit 81c446c

Browse files
authored
A0-1610: Restore contract event support (#838)
* Restore contract event listening * Authenticate when installing protoc on CI This should reduce the number of failures caused by rate limiting. * Bump aleph-client version * Update contract event docs * Improve cosmetics * This reverts commit e21d155.
1 parent 56c6ff4 commit 81c446c

File tree

8 files changed

+230
-84
lines changed

8 files changed

+230
-84
lines changed

aleph-client/Cargo.lock

Lines changed: 22 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aleph-client/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ log = "0.4"
1414
serde_json = { version = "1.0" }
1515
thiserror = "1.0"
1616
contract-metadata = "2.0.0-beta"
17-
contract-transcode = "2.0.0-beta"
17+
contract-transcode = { git = "https://github.com/obrok/cargo-contract", branch = "send-sync-env-types" }
1818
ink_metadata = "4.0.0-beta"
1919
subxt = "0.25.0"
2020
futures = "0.3.25"
@@ -25,3 +25,6 @@ sp-core = { git = "https://github.com/Cardinal-Cryptography/substrate.git", bran
2525
sp-runtime = { git = "https://github.com/Cardinal-Cryptography/substrate.git", branch = "aleph-v0.9.32" }
2626
pallet-contracts-primitives = { git = "https://github.com/Cardinal-Cryptography/substrate.git", branch = "aleph-v0.9.32" }
2727
primitives = { path = "../primitives" }
28+
29+
[dev-dependencies]
30+
tokio = "1.21"

aleph-client/src/contract/event.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//! Utilities for listening for contract events.
2+
//!
3+
//! To use the module you will need to pass a connection, some contracts and an `UnboundedSender` to the
4+
//! [listen_contract_events] function. You most likely want to `tokio::spawn` the resulting future, so that it runs
5+
//! concurrently.
6+
//!
7+
//! ```no_run
8+
//! # use std::sync::Arc;
9+
//! # use std::sync::mpsc::channel;
10+
//! # use std::time::Duration;
11+
//! # use aleph_client::{AccountId, Connection, SignedConnection};
12+
//! # use aleph_client::contract::ContractInstance;
13+
//! # use aleph_client::contract::event::{listen_contract_events};
14+
//! # use anyhow::Result;
15+
//! use futures::{channel::mpsc::unbounded, StreamExt};
16+
//!
17+
//! # async fn example(conn: Connection, signed_conn: SignedConnection, address1: AccountId, address2: AccountId, path1: &str, path2: &str) -> Result<()> {
18+
//! // The `Arc` makes it possible to pass a reference to the contract to another thread
19+
//! let contract1 = Arc::new(ContractInstance::new(address1, path1)?);
20+
//! let contract2 = Arc::new(ContractInstance::new(address2, path2)?);
21+
//!
22+
//! let conn_copy = conn.clone();
23+
//! let contract1_copy = contract1.clone();
24+
//! let contract2_copy = contract2.clone();
25+
//!
26+
//! let (tx, mut rx) = unbounded();
27+
//! let listen = || async move {
28+
//! listen_contract_events(&conn, &[contract1_copy.as_ref(), contract2_copy.as_ref()], tx).await?;
29+
//! <Result<(), anyhow::Error>>::Ok(())
30+
//! };
31+
//! let join = tokio::spawn(listen());
32+
//!
33+
//! contract1.contract_exec0(&signed_conn, "some_method").await?;
34+
//! contract2.contract_exec0(&signed_conn, "some_other_method").await?;
35+
//!
36+
//! println!("Received event {:?}", rx.next().await);
37+
//!
38+
//! rx.close();
39+
//! join.await??;
40+
//!
41+
//! # Ok(())
42+
//! # }
43+
//! ```
44+
45+
use std::collections::HashMap;
46+
47+
use anyhow::{bail, Result};
48+
use contract_transcode::Value;
49+
use futures::{channel::mpsc::UnboundedSender, StreamExt};
50+
51+
use crate::{contract::ContractInstance, AccountId, Connection};
52+
53+
/// Represents a single event emitted by a contract.
54+
#[derive(Debug, Clone, Eq, PartialEq)]
55+
pub struct ContractEvent {
56+
/// The address of the contract that emitted the event.
57+
pub contract: AccountId,
58+
/// The name of the event.
59+
pub name: Option<String>,
60+
/// Data contained in the event.
61+
pub data: HashMap<String, Value>,
62+
}
63+
64+
/// Starts an event listening loop.
65+
///
66+
/// Will send contract event and every error encountered while fetching through the provided [UnboundedSender].
67+
/// Only events coming from the address of one of the `contracts` will be decoded.
68+
///
69+
/// The loop will terminate once `sender` is closed. The loop may also terminate in case of errors while fetching blocks
70+
/// or decoding events (pallet events, contract event decoding errors are sent over the channel).
71+
pub async fn listen_contract_events(
72+
conn: &Connection,
73+
contracts: &[&ContractInstance],
74+
sender: UnboundedSender<Result<ContractEvent>>,
75+
) -> Result<()> {
76+
let mut block_subscription = conn.as_client().blocks().subscribe_finalized().await?;
77+
78+
while let Some(block) = block_subscription.next().await {
79+
if sender.is_closed() {
80+
break;
81+
}
82+
83+
let block = block?;
84+
85+
for event in block.events().await?.iter() {
86+
let event = event?;
87+
88+
if let Some(event) =
89+
event.as_event::<crate::api::contracts::events::ContractEmitted>()?
90+
{
91+
if let Some(contract) = contracts
92+
.iter()
93+
.find(|contract| contract.address() == &event.contract)
94+
{
95+
let data = zero_prefixed(&event.data);
96+
let event = contract
97+
.transcoder
98+
.decode_contract_event(&mut data.as_slice());
99+
100+
sender.unbounded_send(
101+
event.and_then(|event| build_event(contract.address().clone(), event)),
102+
)?;
103+
}
104+
}
105+
}
106+
}
107+
108+
Ok(())
109+
}
110+
111+
/// The contract transcoder assumes there is an extra byte (that it discards) indicating the size of the data. However,
112+
/// data arriving through the subscription as used in this file don't have this extra byte. This function adds it.
113+
fn zero_prefixed(data: &[u8]) -> Vec<u8> {
114+
let mut result = vec![0];
115+
result.extend_from_slice(data);
116+
result
117+
}
118+
119+
fn build_event(address: AccountId, event_data: Value) -> Result<ContractEvent> {
120+
match event_data {
121+
Value::Map(map) => Ok(ContractEvent {
122+
contract: address,
123+
name: map.ident(),
124+
data: map
125+
.iter()
126+
.map(|(key, value)| (key.to_string(), value.clone()))
127+
.collect(),
128+
}),
129+
_ => bail!("Contract event data is not a map"),
130+
}
131+
}

aleph-client/src/contract/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
//! ```
4444
4545
mod convertible_value;
46+
pub mod event;
4647

4748
use std::fmt::{Debug, Formatter};
4849

e2e-tests/Cargo.lock

Lines changed: 36 additions & 56 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)