Skip to main content

Events

The events example demonstrates how to publish events from a contract. This example is an extension of the storing data example.

Open in Gitpod

Run the Example

First go through the Setup process to get your development environment configured, then clone the v20.0.0 tag of soroban-examples repository:

git clone -b v20.0.0 https://github.com/stellar/soroban-examples

Or, skip the development environment setup and open this example in Gitpod.

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

events/src/lib.rs
const COUNTER: Symbol = symbol_short!("COUNTER");

#[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 topics:
// - The "COUNTER" symbol.
// - The "increment" symbol.
// The event data is the count.
env.events()
.publish((COUNTER, symbol_short!("increment")), count);

// Return the count to the caller.
count
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v20.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 using the environments events publish function.

env.events().publish(topics, data);

Event Topics

An event may contain up to four topics.

Topics are conveniently defined using a tuple. In the sample code two topics of Symbol type are used.

env.events().publish((COUNTER, symbol_short!("increment")), ...);
tip

The topics don't have to be made of the same type. You can mix different types as long as the total topic count stays below the limit.

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.

env.events().publish(..., count);

Publishing

Publishing an event is done by calling the publish function and giving it the topics and data. 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.

env.events().publish((COUNTER, symbol_short!("increment")), count);
caution

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.

events/src/test.rs
#[test]
fn test() {
let env = Env::default();
let contract_id = env.register_contract(None, IncrementContract);
let client = IncrementContractClient::new(&env, &contract_id);

assert_eq!(client.increment(), 1);
assert_eq!(client.increment(), 2);
assert_eq!(client.increment(), 3);

assert_eq!(
env.events().all(),
vec![
&env,
(
contract_id.clone(),
(symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env),
1u32.into_val(&env)
),
(
contract_id.clone(),
(symbol_short!("COUNTER"), symbol_short!("increment")).into_val(&env),
2u32.into_val(&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_contract(None, 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 events were 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 soroban contract build command.

soroban contract build

A .wasm file should be outputted in the target directory:

target/wasm32-unknown-unknown/release/soroban_events_contract.wasm

Run the Contract

If you have soroban-cli installed, you can invoke contract functions in the using it.

soroban contract invoke \
--wasm target/wasm32-unknown-unknown/release/soroban_events_contract.wasm \
--id 1 \
-- \
increment

The following output should occur using the code above.

1
#0: event: {"ext":"v0","contractId":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],"type":"contract","body":{"v0":{"topics":[{"symbol":[67,79,85,78,84,69,82]},{"symbol":[105,110,99,114,101,109,101,110,116]}],"data":{"u32":1}}}}

A single event #0 is outputted, which is the contract event the contract published. The event contains the two topics, each a symbol (displayed as bytes), and the data object containing the u32.

info

Soroban is a pre-release and at this time outputs events in an unstable JSON format.