Events
The events example demonstrates how to publish events from a contract. This example is an extension of the storing data example.
With the release of Whisk, Protocol 23, the syntax for publishing smart contract events has changed. In order to provide the most up-to-date information, this example has been updated to include the new patterns. Find more detailed information in the Rust SDK documentation.
Run the Example
First go through the Setup process to get your development environment configured, then clone the v23.0.0 tag of soroban-examples repository:
git clone -b v23.0.0 https://github.com/stellar/soroban-examples
Or, skip the development environment setup and open this example in GitHub Codespaces or Code Anywhere.
To run the tests for the example, navigate to the events directory, and use cargo test.
cd events
cargo test
You should see the output:
running 1 test
test test::test ... ok
Code
const COUNTER: Symbol = symbol_short!("COUNTER");
// Define two static topics for the event: "COUNTER" and "increment".
// Also set the data format to "single-value", which means that the event data
// payload will contain a single value not nested into any data structure.
#[contractevent(topics = ["COUNTER", "increment"], data_format = "single-value")]
struct IncrementEvent {
count: u32,
}
#[contract]
pub struct IncrementContract;
#[contractimpl]
impl IncrementContract {
/// Increment increments an internal counter, and returns the value.
pub fn increment(env: Env) -> u32 {
// Get the current count.
let mut count: u32 = env.storage().instance().get(&COUNTER).unwrap_or(0); // If no value set, assume 0.
// Increment the count.
count += 1;
// Save the count.
env.storage().instance().set(&COUNTER, &count);
// Publish an event about the increment occuring.
// The event has two static topics ("COUNTER", "increment") and actual
// count as the data payload.
IncrementEvent { count }.publish(&env);
// Return the count to the caller.
count
}
}
Ref: https://github.com/stellar/soroban-examples/tree/v23.0.0/events
How it Works
This example contract extends the increment example by publishing an event each time the counter is incremented.
Contract events let contracts emit information about what their contract is doing. Contracts can publish events creating a defined struct and publishing it to the smart contract environments.
First, the #[contractevent] struct must be defined
#[contractevent(topics = ["COUNTER", "increment"], data_format = "single-value")]
struct IncrementEvent {
count: u32,
}
Then, inside the contract's function, we can create and publish the event with the relevant data.
IncrementEvent { count }.publish(&env);
Event Topics
Topics can be defined either statically or dynamically. In the sample code two static topics are used, which will be of the Symbol type: COUNTER and increment.
#[contractevent(topics = ["COUNTER", "increment"], ...)]
The topics don't have to be made of the same type.
Topics can also be defined dynamically, inside the struct. In this case, the struct's snake_case name will be the first topic. For example, the following event will have two topics: the Symbol "increment", followed by an Address.
#[contractevent]
pub struct Increment {
#[topic]
addr: Address,
count: u32,
}
Event Data
An event also contains a data object of any value or type including types defined by contracts using #[contracttype]. In the example the data is the u32 count. The data_format = "single-value" tells the event to publish the data alone, with no surrounding data structure.
#[contractevent(..., data_format = "single-value")]
Event data will, by default, conform to the data structure in the defined struct. The data_format can also be specified as vec or single-value. Again, please refer to the Rust SDK documentation for more details.
Publishing
Publishing an event is done by calling the publish function on the created event struct. The function returns nothing on success, and panics on failure. Possible failure reasons can include malformed inputs (e.g. topic count exceeds limit) and running over the resource budget (TBD). Once successfully published, the new event will be available to applications consuming the events.
IncrementEvent { count }.publish(&env);
Published events are discarded if a contract invocation fails due to a panic, budget exhaustion, or when the contract returns an error.
Tests
Open the events/src/test.rs file to follow along.
#[test]
fn test() {
let env = Env::default();
let contract_id = env.register(IncrementContract, ());
let client = IncrementContractClient::new(&env, &contract_id);
assert_eq!(client.increment(), 1);
assert_eq!(
env.events().all(),
vec![
&env,
(
contract_id.clone(),
(symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env),
1u32.into_val(&env)
),
]
);
assert_eq!(client.increment(), 2);
assert_eq!(
env.events().all(),
vec![
&env,
(
contract_id.clone(),
(symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env),
2u32.into_val(&env)
),
]
);
assert_eq!(client.increment(), 3);
assert_eq!(
env.events().all(),
vec![
&env,
(
contract_id,
(symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env),
3u32.into_val(&env)
),
]
);
}
In any test the first thing that is always required is an Env, which is the Soroban environment that the contract will run in.
let env = Env::default();
The contract is registered with the environment using the contract type.
let contract_id = env.register(IncrementContract, ());
All public functions within an impl block that is annotated with the #[contractimpl] attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with Client appended. For example, in our contract the contract type is IncrementContract, and the client is named IncrementContractClient.
let client = IncrementContractClient::new(&env, &contract_id);
The example invokes the contract several times.
assert_eq!(client.increment(), 1);
The example asserts that the event was published.
assert_eq!(
env.events().all(),
vec![
&env,
(
contract_id.clone(),
(symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env),
1u32.into_val(&env)
),
]
);
Build the Contract
To build the contract, use the stellar contract build command.
stellar contract build
A .wasm file should be outputted in the target directory:
target/wasm32v1-none/release/soroban_events_contract.wasm
Run the Contract
If you have stellar-cli installed, you can invoke contract functions in the using it.
- macOS/Linux
- Windows (PowerShell)
stellar contract invoke \
--wasm target/wasm32v1-none/release/soroban_events_contract.wasm \
--id 1 \
-- \
increment
stellar contract invoke `
--wasm target/wasm32v1-none/release/soroban_events_contract.wasm `
--id 1 `
-- `
increment
The following output should occur using the code above.
📅 CAAA... - Success - Event: [{"symbol":"COUNTER"},{"symbol":"increment"}] = {"u32":1}
1
A single event is outputted, which is the contract event the contract published. The event contains the two topics, each a Symbol, and the data object containing the u32.