Skip to content

Commit dff9656

Browse files
Add contractevent macro and event support to contract spec (#1473)
### What Add `contractevent` macro for defining events, and event support to contract spec. In it's most basic form, as follows, where an automatic prefix topic based on the name will be published, with any topic fields appended to the topic list, and any other fields emitted as map entries in a map: ```rust #[contractevent] #[derive(Clone, Default, Debug, Eq, PartialEq)] pub struct MyEvent { #[topic] pub my_topic: u32, pub my_event_data: u32, pub more_event_data: u64, } ``` But also supporting more advanced customisable forms, where prefix topics can be overriden and a max of two provided, and where the data section can be defined as either being a `vec` or a `single-value` instead. ### Why The `contractevent` macro will achieve a couple things for events. It will allow developers to define the event as a type, in a similar way that they can define an event in Solidity contracts. Defining it as a type makes it easier to reuse the event, and to define the types safely and be clear about what types within the event are being published. It will provide a foundation that the Rust SDK can use to include in the contract interface (spec) an entry for the event. That entry can then be used by downstream systems to bring more meaning to the raw event data that is visible in transaction / ledger close meta. For an example of what that looks like, see https://github.com/orgs/stellar/discussions/1724#discussioncomment-13118291. The advanced customisations are a necessity to be able to make the events and the event schema describe existing contract events. The network already has a variety of events and some contracts use two fields as fixed prefix topics, such as the Soroswap contracts. Some use vecs for the data section, and even a single value such as an `amount: i128`, such as the Stellar Asset Contract. Close #1097 ### Docs for the `contractevent` attribute macro Generates conversions from the struct into a published event. Fields of the struct become topics and data parameters in the published event. Includes the event in the contract spec so that clients can generate bindings for the type and downstream systems can understand the meaning of the event. #### Examples ##### Basic Contract Event Defining a basic contract event. The event will have a single fixed prefix topic matching the name of the struct in lower snake case. The fixed prefix topic will appear before any topics listed as fields. In the example below, the topics for the event will be: * `"my_event"` * u32 value from the `my_topic` field The event’s data will be a [`Map`](struct.Map.html "struct soroban_sdk::Map"), containing a key-value pair for each field with the key being the name as a [`Symbol`](struct.Symbol.html "struct soroban_sdk::Symbol"). In the example below, the data for the event will be: * key: my\_event\_data => val: u32 * key: more\_event\_data => val: u64 ```rust #![no_std] use soroban_sdk::contractevent; // Define the event using the `contractevent` attribute macro. #[contractevent] #[derive(Clone, Default, Debug, Eq, PartialEq)] pub struct MyEvent { // Mark fields as topics, for the value to be included in the events topic list so // that downstream systems know to index it. #[topic] pub my_topic: u32, // Fields not marked as topics will appear in the events data section. pub my_event_data: u32, pub more_event_data: u64, } ``` ##### Prefix Topics Defining a contract event with a custom set of fixed prefix topics. The prefix topic list can be set to another value. In the example below, the topics for the event will be: * `"my_contract"` * `"an_event"` * u32 value from the `my_topic` field ```rust #![no_std] use soroban_sdk::contractevent; // Define the event using the `contractevent` attribute macro. #[contractevent(topics = ["my_contract", "an_event"])] #[derive(Clone, Default, Debug, Eq, PartialEq)] pub struct MyEvent { // Mark fields as topics, for the value to be included in the events topic list so // that downstream systems know to index it. #[topic] pub my_topic: u32, // Fields not marked as topics will appear in the events data section. pub my_event_data: u32, pub more_event_data: u64, } ``` ##### Data Format Defining a contract event with a different data format. The data format of the event is by default a map, but can alternatively be defined as a `vec` or `single-value`. ###### Vec In the example below, the data for the event will be a [`Vec`](struct.Vec.html "struct soroban_sdk::Vec") containing: * u32 * u64 ```rust #![no_std] use soroban_sdk::contractevent; // Define the event using the `contractevent` attribute macro. #[contractevent(data_format = "vec")] #[derive(Clone, Default, Debug, Eq, PartialEq)] pub struct MyEvent { // Mark fields as topics, for the value to be included in the events topic list so // that downstream systems know to index it. #[topic] pub my_topic: u32, // Fields not marked as topics will appear in the events data section. pub my_event_data: u32, pub more_event_data: u64, } ``` ###### Single Value In the example below, the data for the event will be a u32. When the data format is a single value there must be no more than one data field. ```rust #![no_std] use soroban_sdk::contractevent; // Define the event using the `contractevent` attribute macro. #[contractevent(data_format = "single-value")] #[derive(Clone, Default, Debug, Eq, PartialEq)] pub struct MyEvent { // Mark fields as topics, for the value to be included in the events topic list so // that downstream systems know to index it. #[topic] pub my_topic: u32, // Fields not marked as topics will appear in the events data section. pub my_event_data: u32, } ``` ##### A Full Example Defining a contract event, publishing it in a contract, and testing it. ```rust #![no_std] use soroban_sdk::{contract, contractevent, contractimpl, contracttype, symbol_short, Env, Symbol}; // Define the event using the `contractevent` attribute macro. #[contractevent] #[derive(Clone, Default, Debug, Eq, PartialEq)] pub struct Increment { // Mark fields as topics, for the value to be included in the events topic list so // that downstream systems know to index it. #[topic] pub change: u32, // Fields not marked as topics will appear in the events data section. pub count: u32, } #[contracttype] #[derive(Clone, Default, Debug, Eq, PartialEq)] pub struct State { pub count: u32, pub last_incr: u32, } #[contract] pub struct Contract; #[contractimpl] impl Contract { /// Increment increments an internal counter, and returns the value. /// Publishes an event about the change in the counter. pub fn increment(env: Env, incr: u32) -> u32 { // Get the current count. let mut state = Self::get_state(env.clone()); // Increment the count. state.count += incr; state.last_incr = incr; // Save the count. env.storage().persistent().set(&symbol_short!("STATE"), &state); // Publish an event about the change. Increment { change: incr, count: state.count, }.publish(&env); // Return the count to the caller. state.count } /// Return the current state. pub fn get_state(env: Env) -> State { env.storage().persistent() .get(&symbol_short!("STATE")) .unwrap_or_else(|| State::default()) // If no value set, assume 0. } } #[test] fn test() { let env = Env::default(); let contract_id = env.register(Contract, ()); let client = ContractClient::new(&env, &contract_id); assert_eq!(client.increment(&1), 1); assert_eq!(client.increment(&10), 11); assert_eq!( client.get_state(), State { count: 11, last_incr: 10, }, ); } ```
1 parent 72eebb5 commit dff9656

27 files changed

Lines changed: 2888 additions & 32 deletions

Cargo.lock

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

soroban-sdk-macros/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ itertools = "0.10.5"
2626
darling = "0.20.10"
2727
macro-string = "0.1.4"
2828
sha2 = "0.10.7"
29+
heck = "0.5.0"
2930

3031
[features]
3132
testutils = []

soroban-sdk-macros/src/attribute.rs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use syn::Attribute;
1+
use syn::{punctuated::Punctuated, Attribute, Data, Fields, FieldsNamed, FieldsUnnamed};
22

33
/// Returns true if the attribute is an attribute that should be preserved and
44
/// passed through to code generated for the item the attribute is on.
@@ -8,3 +8,84 @@ pub fn pass_through_attr_to_gen_code(attr: &Attribute) -> bool {
88
|| attr.path().is_ident("allow")
99
|| attr.path().is_ident("deny")
1010
}
11+
12+
/// Modifies the input, removing any attributes on struct fields that match the attrs name list.
13+
///
14+
/// Currently implemented only for struct data.
15+
pub fn remove_attributes_from_item(data: &mut Data, attrs: &[&str]) {
16+
let fields = match data {
17+
Data::Struct(data) => match &mut data.fields {
18+
Fields::Named(FieldsNamed { named, .. }) => named,
19+
Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => unnamed,
20+
Fields::Unit => &mut Punctuated::default(), // Unit structs have no fields, nothing to do.
21+
},
22+
_ => unimplemented!("Only structs are supported by remove_attributes_from_item"),
23+
};
24+
for field in fields {
25+
field.attrs.retain(|attr| {
26+
!attr
27+
.path()
28+
.get_ident()
29+
.is_some_and(|ident| attrs.contains(&ident.to_string().as_str()))
30+
});
31+
}
32+
}
33+
34+
#[cfg(test)]
35+
mod test {
36+
use quote::{quote, ToTokens};
37+
use syn::DeriveInput;
38+
39+
use super::remove_attributes_from_item;
40+
41+
#[test]
42+
fn test_remove_attributes_from_item_struct_named() {
43+
let input = quote! {
44+
struct Struct {
45+
f1: u32,
46+
#[topic]
47+
f2: u32,
48+
f3: u32,
49+
#[data]
50+
f4: u32,
51+
}
52+
};
53+
let expect = quote! {
54+
struct Struct {
55+
f1: u32,
56+
f2: u32,
57+
f3: u32,
58+
f4: u32,
59+
}
60+
};
61+
let mut input = syn::parse2::<DeriveInput>(input.into()).unwrap();
62+
remove_attributes_from_item(&mut input.data, &["topic", "data"]);
63+
assert_eq!(input.to_token_stream().to_string(), expect.to_string());
64+
}
65+
66+
#[test]
67+
fn test_remove_attributes_from_item_struct_unnamed() {
68+
let input = quote! {
69+
struct Struct(#[topic] u32, u32, #[data] u64);
70+
};
71+
let expect = quote! {
72+
struct Struct(u32, u32, u64);
73+
};
74+
let mut input = syn::parse2::<DeriveInput>(input.into()).unwrap();
75+
remove_attributes_from_item(&mut input.data, &["topic", "data"]);
76+
assert_eq!(input.to_token_stream().to_string(), expect.to_string());
77+
}
78+
79+
#[test]
80+
fn test_remove_attributes_from_item_struct_unit() {
81+
let input = quote! {
82+
struct Struct;
83+
};
84+
let expect = quote! {
85+
struct Struct;
86+
};
87+
let mut input = syn::parse2::<DeriveInput>(input.into()).unwrap();
88+
remove_attributes_from_item(&mut input.data, &["topic", "data"]);
89+
assert_eq!(input.to_token_stream().to_string(), expect.to_string());
90+
}
91+
}

0 commit comments

Comments
 (0)