Skip to main content

contract_extrinsics/
events.rs

1// Copyright (C) Use Ink (UK) Ltd.
2// This file is part of cargo-contract.
3//
4// cargo-contract is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// cargo-contract is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with cargo-contract.  If not, see <http://www.gnu.org/licenses/>.
16
17use super::{
18    BalanceVariant,
19    TokenMetadata,
20};
21use crate::DEFAULT_KEY_COL_WIDTH;
22use colored::Colorize as _;
23use contract_build::Verbosity;
24use contract_transcode::{
25    ContractMessageTranscoder,
26    Hex,
27    TranscoderBuilder,
28    Value,
29};
30
31use anyhow::Result;
32use ink_env::Environment;
33use scale::Encode;
34use scale_info::form::PortableForm;
35use std::{
36    fmt::{
37        Display,
38        Write,
39    },
40    str::FromStr,
41};
42use subxt::{
43    self,
44    Config,
45    blocks::ExtrinsicEvents,
46    events::StaticEvent,
47    ext::{
48        scale_decode::{
49            self,
50            IntoVisitor,
51        },
52        scale_encode,
53    },
54    utils::{
55        H160,
56        H256,
57    },
58};
59
60/// A custom event emitted by the contract.
61#[derive(
62    scale::Decode,
63    scale::Encode,
64    scale_decode::DecodeAsType,
65    scale_encode::EncodeAsType,
66    Debug,
67)]
68#[decode_as_type(crate_path = "subxt::ext::scale_decode")]
69#[encode_as_type(crate_path = "subxt::ext::scale_encode")]
70pub struct ContractEmitted {
71    /// The contract that emitted the event.
72    contract: H160,
73    /// Data supplied by the contract. Metadata generated during contract compilation
74    /// is needed to decode it.
75    data: Vec<u8>,
76    // A list of topics used to index the event.
77    // Number of topics is capped by [`limits::NUM_EVENT_TOPICS`].
78    topics: Vec<H256>,
79}
80
81impl StaticEvent for ContractEmitted {
82    const PALLET: &'static str = "Revive";
83    const EVENT: &'static str = "ContractEmitted";
84}
85
86/// Contract deployed by deployer at the specified address.
87#[derive(
88    scale::Decode,
89    scale::Encode,
90    scale_decode::DecodeAsType,
91    scale_encode::EncodeAsType,
92    Debug,
93)]
94#[decode_as_type(crate_path = "subxt::ext::scale_decode")]
95#[encode_as_type(crate_path = "subxt::ext::scale_encode")]
96pub struct ContractInstantiated {
97    /// Address of the deployer.
98    pub deployer: H160,
99    /// Address where the contract was instantiated to.
100    pub contract: H160,
101}
102
103impl StaticEvent for ContractInstantiated {
104    const PALLET: &'static str = "Revive";
105    const EVENT: &'static str = "Instantiated";
106}
107
108/// Field that represent data of an event from invoking a contract extrinsic.
109#[derive(serde::Serialize)]
110pub struct Field {
111    /// name of a field
112    pub name: String,
113    /// value of a field
114    pub value: Value,
115    /// The name of a type as defined in the pallet Source Code
116    #[serde(skip_serializing)]
117    pub type_name: Option<String>,
118}
119
120impl Field {
121    pub fn new(name: String, value: Value, type_name: Option<String>) -> Self {
122        Field {
123            name,
124            value,
125            type_name,
126        }
127    }
128}
129
130/// An event produced from invoking a contract extrinsic.
131#[derive(serde::Serialize)]
132pub struct Event {
133    /// name of a pallet
134    pub pallet: String,
135    /// name of the event
136    pub name: String,
137    /// data associated with the event
138    pub fields: Vec<Field>,
139}
140
141/// Events produced from invoking a contract extrinsic.
142#[derive(serde::Serialize)]
143#[allow(dead_code)]
144pub struct Events(Vec<Event>);
145
146/// Displays events produced from invoking a contract extrinsic.
147#[derive(serde::Serialize)]
148pub struct DisplayEvents(Vec<Event>);
149
150#[allow(clippy::needless_borrows_for_generic_args)]
151impl DisplayEvents {
152    /// Parses events and returns an object which can be serialised
153    pub fn from_events<C: Config, E: Environment>(
154        result: &ExtrinsicEvents<C>,
155        transcoder: Option<&ContractMessageTranscoder>,
156        subxt_metadata: &subxt::Metadata,
157    ) -> Result<DisplayEvents>
158    where
159        C::AccountId: IntoVisitor,
160    {
161        let mut events: Vec<Event> = vec![];
162
163        let events_transcoder = TranscoderBuilder::new(subxt_metadata.types())
164            .with_default_custom_type_transcoders()
165            .done();
166
167        for event in result.iter() {
168            let event = event?;
169            tracing::debug!(
170                "displaying event {}:{}",
171                event.pallet_name(),
172                event.variant_name()
173            );
174
175            let event_metadata = event.event_metadata();
176            let event_fields = &event_metadata.variant.fields;
177
178            let mut event_entry = Event {
179                pallet: event.pallet_name().to_string(),
180                name: event.variant_name().to_string(),
181                fields: vec![],
182            };
183
184            // For ContractEmitted events, decode to get the event signature topic and
185            // data
186            let contract_emitted = if <ContractEmitted as StaticEvent>::is_event(
187                event.pallet_name(),
188                event.variant_name(),
189            ) {
190                event.as_event::<ContractEmitted>().ok().flatten()
191            } else {
192                None
193            };
194
195            let event_data = &mut event.field_bytes();
196            tracing::debug!("event data: {:?}", hex::encode(&event_data));
197            let mut unnamed_field_name = 0;
198            for field_metadata in event_fields {
199                if let Some(ref ce) = contract_emitted {
200                    if field_metadata.name == Some("data".to_string()) {
201                        // Decode the contract event data using the transcoder.
202                        // The transcoder expects the data to be prefixed with its length
203                        // as Compact<u32>.
204                        let mut encoded_data =
205                            scale::Compact(ce.data.len() as u32).encode();
206                        encoded_data.extend_from_slice(&ce.data);
207                        let mut data_slice = encoded_data.as_slice();
208                        let field = contract_event_vec_field(
209                            transcoder,
210                            field_metadata,
211                            ce.topics.first(),
212                            &mut data_slice,
213                            field_metadata.name.as_ref().expect("must exist"),
214                        )?;
215                        event_entry.fields.push(field);
216                    } else if field_metadata.name == Some("topics".to_string()) {
217                        // Skip the topics field or display it as hex
218                        // Topics are already used for event signature matching
219                        continue;
220                    } else {
221                        // Non-data/topics fields in ContractEmitted (e.g., contract
222                        // address)
223                        let field_name = field_metadata
224                            .name
225                            .as_ref()
226                            .map(|s| s.to_string())
227                            .unwrap_or_else(|| {
228                                let name = unnamed_field_name.to_string();
229                                unnamed_field_name += 1;
230                                name
231                            });
232
233                        let decoded_field = events_transcoder.decode(
234                            subxt_metadata.types(),
235                            field_metadata.ty.id,
236                            event_data,
237                        )?;
238                        let field = Field::new(
239                            field_name,
240                            decoded_field,
241                            field_metadata.type_name.as_ref().map(|s| s.to_string()),
242                        );
243                        event_entry.fields.push(field);
244                    }
245                } else {
246                    let field_name = field_metadata
247                        .name
248                        .as_ref()
249                        .map(|s| s.to_string())
250                        .unwrap_or_else(|| {
251                            let name = unnamed_field_name.to_string();
252                            unnamed_field_name += 1;
253                            name
254                        });
255
256                    let decoded_field = events_transcoder.decode(
257                        subxt_metadata.types(),
258                        field_metadata.ty.id,
259                        event_data,
260                    )?;
261                    let field = Field::new(
262                        field_name,
263                        decoded_field,
264                        field_metadata.type_name.as_ref().map(|s| s.to_string()),
265                    );
266                    event_entry.fields.push(field);
267                }
268            }
269            events.push(event_entry);
270        }
271
272        Ok(DisplayEvents(events))
273    }
274
275    /// Displays events in a human readable format
276    pub fn display_events<E: Environment>(
277        &self,
278        verbosity: Verbosity,
279        token_metadata: &TokenMetadata,
280    ) -> Result<String>
281    where
282        E::Balance: Display + From<u128>,
283    {
284        let event_field_indent: usize = DEFAULT_KEY_COL_WIDTH - 3;
285        let mut out = format!(
286            "{:>width$}\n",
287            "Events".bright_purple().bold(),
288            width = DEFAULT_KEY_COL_WIDTH
289        );
290        for event in &self.0 {
291            let _ = writeln!(
292                out,
293                "{:>width$} {} ➜ {}",
294                "Event".bright_green().bold(),
295                event.pallet.bright_white(),
296                event.name.bright_white().bold(),
297                width = DEFAULT_KEY_COL_WIDTH
298            );
299
300            for field in &event.fields {
301                if verbosity.is_verbose() {
302                    let mut value: String = field.value.to_string();
303                    if (field.type_name == Some("T::Balance".to_string())
304                        || field.type_name == Some("BalanceOf<T>".to_string()))
305                        && let Value::UInt(balance) = field.value
306                    {
307                        value = BalanceVariant::<E::Balance>::from(
308                            balance,
309                            Some(token_metadata),
310                        )?
311                        .to_string();
312                    }
313                    if field.type_name == Some("H160".to_string()) {
314                        // Value is in the format `H160([bytes])`.
315                        // Extract the byte array between the brackets and convert it to a
316                        // hexadecimal string.
317                        if let (Some(start), Some(end)) =
318                            (value.find('['), value.find(']'))
319                        {
320                            let byte_str = &value[start + 1..end];
321                            let bytes: Vec<u8> = byte_str
322                                .split(", ")
323                                .filter_map(|s| s.parse::<u8>().ok())
324                                .collect();
325                            let h160_value = H160::from_slice(&bytes);
326                            value = format!("0x{}", hex::encode(h160_value.as_bytes()));
327                        }
328                    }
329                    let _ = writeln!(
330                        out,
331                        "{:width$}{}: {}",
332                        "",
333                        field.name.bright_white(),
334                        value,
335                        width = event_field_indent,
336                    );
337                }
338            }
339        }
340        Ok(out)
341    }
342
343    /// Returns an event result in json format
344    pub fn to_json(&self) -> Result<String> {
345        Ok(serde_json::to_string_pretty(self)?)
346    }
347}
348
349/// Construct the contract event data field, attempting to decode the event using the
350/// [`ContractMessageTranscoder`] if available.
351#[allow(clippy::needless_borrows_for_generic_args)]
352fn contract_event_vec_field(
353    transcoder: Option<&ContractMessageTranscoder>,
354    field_metadata: &scale_info::Field<PortableForm>,
355    event_sig_topic: Option<&H256>,
356    event_data: &mut &[u8],
357    field_name: &String,
358) -> Result<Field> {
359    let event_value = if let Some(transcoder) = transcoder {
360        if let Some(event_sig_topic) = event_sig_topic {
361            match transcoder.decode_contract_event(event_sig_topic, event_data) {
362                Ok(contract_event) => contract_event,
363                Err(err) => {
364                    tracing::warn!(
365                        "Decoding contract event failed: {:?}. It might have come from another contract.",
366                        err
367                    );
368                    Value::Hex(Hex::from_str(&hex::encode(&event_data))?)
369                }
370            }
371        } else {
372            tracing::info!("Anonymous event not decoded. Data displayed as raw hex.");
373            Value::Hex(Hex::from_str(&hex::encode(event_data))?)
374        }
375    } else {
376        Value::Hex(Hex::from_str(&hex::encode(event_data))?)
377    };
378    Ok(Field::new(
379        field_name.to_string(),
380        event_value,
381        field_metadata.type_name.as_ref().map(|s| s.to_string()),
382    ))
383}
384
385#[cfg(all(test, feature = "std"))]
386// For contract re-exports and `cfg`s.
387#[allow(unused_imports, unexpected_cfgs)]
388mod tests {
389    use super::*;
390    use anyhow::Result;
391    use ink::{
392        metadata::InkProject,
393        prelude::vec::Vec,
394    };
395    use ink_env::Event as _;
396    use scale::Encode;
397    use scale_info::{
398        Field as ScaleField,
399        IntoPortable as _,
400    };
401    use subxt::utils::H256;
402
403    #[allow(clippy::extra_unused_lifetimes, non_local_definitions)]
404    #[ink::contract]
405    pub mod event_contract {
406        #[ink(storage)]
407        pub struct EventHarness {}
408
409        #[ink(event)]
410        pub struct BalanceChanged {
411            pub value: bool,
412            pub amount: u32,
413        }
414
415        impl Default for EventHarness {
416            fn default() -> Self {
417                Self::new()
418            }
419        }
420
421        impl EventHarness {
422            #[ink(constructor)]
423            pub fn new() -> Self {
424                Self {}
425            }
426
427            #[ink(message)]
428            pub fn touch(&self) {}
429        }
430    }
431
432    fn contract_data_field_metadata() -> scale_info::Field<PortableForm> {
433        let meta_field = ScaleField::new(
434            Some("data"),
435            scale_info::MetaType::new::<Vec<u8>>(),
436            Some("Vec<u8>"),
437            vec![],
438        );
439        let mut registry = scale_info::Registry::new();
440        meta_field.into_portable(&mut registry)
441    }
442
443    fn generate_metadata() -> InkProject {
444        unsafe extern "Rust" {
445            fn __ink_generate_metadata() -> InkProject;
446        }
447        unsafe { __ink_generate_metadata() }
448    }
449
450    /// Without a transcoder we fall back to raw hex representation.
451    #[test]
452    fn contract_event_without_transcoder_returns_hex() {
453        let field_meta = contract_data_field_metadata();
454
455        // Sample event data (would normally be SCALE-encoded event fields)
456        let event_data_bytes = vec![0x04, 0x00, 0x01, 0x02, 0x03];
457        let mut event_data = event_data_bytes.as_slice();
458
459        let result = contract_event_vec_field(
460            None,
461            &field_meta,
462            None,
463            &mut event_data,
464            &"data".to_string(),
465        );
466
467        assert!(result.is_ok(), "decoding without transcoder should succeed");
468        let field = result.unwrap();
469
470        match field.value {
471            Value::Hex(_) => {}
472            other => panic!("expected raw hex fallback, got {other:?}"),
473        }
474    }
475
476    /// With a transcoder the contract event data is decoded into its fields.
477    #[test]
478    fn contract_event_with_transcoder_decodes_payload() -> Result<()> {
479        let field_meta = contract_data_field_metadata();
480
481        let metadata = generate_metadata();
482        let transcoder = ContractMessageTranscoder::new(metadata);
483
484        let payload = event_contract::BalanceChanged {
485            value: true,
486            amount: 7u32,
487        };
488        let payload_bytes = payload.encode();
489        let mut encoded_data = scale::Compact(payload_bytes.len() as u32).encode();
490        encoded_data.extend_from_slice(&payload_bytes);
491        let mut data_slice = encoded_data.as_slice();
492
493        let signature_topic_bytes = event_contract::BalanceChanged::SIGNATURE_TOPIC
494            .expect("event has a signature topic");
495        let signature_topic = H256::from(signature_topic_bytes);
496
497        let field = contract_event_vec_field(
498            Some(&transcoder),
499            &field_meta,
500            Some(&signature_topic),
501            &mut data_slice,
502            &"data".to_string(),
503        )?;
504
505        let Value::Map(map) = field.value else {
506            panic!("expected decoded event to be a map");
507        };
508        let decoded_value = map
509            .get_by_str("value")
510            .expect("decoded event contains `value` field");
511        assert_eq!(decoded_value, &Value::Bool(true));
512        let decoded_amount = map
513            .get_by_str("amount")
514            .expect("decoded event contains `amount` field");
515        assert_eq!(decoded_amount, &Value::UInt(7u128));
516
517        Ok(())
518    }
519}