Hi there! Welcome to my fourth post of my series called "Soroban Contracts 101", where I'll be explaining the basics of Soroban contracts, such as data storage, authentication, custom types, and more. All the code that we're gonna explain throughout this series will mostly come from soroban-contracts-101 github repository.
In this fourth post of the series, I'll be covering soroban contract events funcionalities.So events allow a contract to notify the outside world that some important state change occurred, and pass along details about what changed. This allows building more complex decentralized systems that react to on-chain events, and also provides transparency into what a contract is doing.
The Contract Code
const COUNTER: Symbol = symbol!("COUNTER");
pub struct IncrementContract;
#[contractimpl]
impl IncrementContract {
pub fn increment(env: Env) -> u32 {
let mut count: u32 = env
.storage()
.get(COUNTER)
.unwrap_or(Ok(0)) // If no value set, assume 0.
.unwrap(); // Panic if the value of COUNTER is not u32.
count += 1;
env.storage().set(COUNTER, count);
env.events().publish((COUNTER, symbol!("increment")), count);
count
}
}
This code is evolved version of the Storing Data Contract from the second post, it now :
- Gets the current count from storage (or defaults to 0)
- Increments it
- Stores it back to storage
- Publishes an increment event containing the count
- Returns the new count
Line of code that published the event :
env.events().publish((COUNTER, symbol!("increment")), count);
-
Calls env.events().publish()
- this is how you publish an event from a contract - Passes in a tuple of
(COUNTER, symbol!("increment"))
- this is the event type/category. We're using ourCOUNTER
symbol and anincrement
symbol to categorize this event. - Passes in
count
- this is the event data. In this case we're passing through the current count value.
So altogether, this line is publishing an "increment" event under the COUNTER category, and including the current count value as the event data.
This allows system watching for events from this contract to detect that an increment occurred, and access the current count value.
The Test Code
#[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!("COUNTER"), symbol!("increment")).into_val(&env),
1u32.into_val(&env)
),
(
contract_id.clone(),
(symbol!("COUNTER"), symbol!("increment")).into_val(&env),
2u32.into_val(&env)
),
(
contract_id.clone(),
(symbol!("COUNTER"), symbol!("increment")).into_val(&env),
3u32.into_val(&env)
),
]
);
}
This code is a unit test for our contract. It does the following:
- Creates an Env and registers the contract like before
- Calls increment three times
- Then checks that three increment events were published by the contract, with the expected data (contract ID, event category, and increment count)
So this test is verifying that the contract is correctly publishing an event on each increment, with the right data.
This demonstrates how you can test event publishing in a unit test - by checking the list of published events at the end.
Running Contract Tests
To ensure that the contract functions as intended, you can run the contract tests using the following command:
cargo test
If the tests are successful, you should see an output similar to:
running 1 test
test test::test ... ok
Building The Contract
To build the contract, use the following command:
cargo build --target wasm32-unknown-unknown --release
This should output a .wasm file in the ../target directory:
../target/wasm32-unknown-unknown/release/soroban_events_contract.wasm
Invoking The Contract
To invoke the increment
function of the contract, use the following command with Soroban-CLI:
soroban contract invoke \
--wasm ../target/wasm32-unknown-unknown/release/soroban_events_contract.wasm \
--id 1 \
-- \
increment
You should see the following output:
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}}}}
That output is the event that our contract emitted being logged. It contains:
- The contract ID (all 0s here since this is a test)
- The event category - our
COUNTER
andincrement
symbols - The event data - the
count
value (1)
So this is just showing that our test correctly emitted the expected increment event with a count of 1. In a real deployment, you would see the actual contract ID and increment values here.
Conclusion
We explored an increment contract that emits an event on each increment. This has a few key benefits:
- It allows other systems to be notified when something happens on-chain (an increment in this case)
- It enables reactive systems that can respond to on-chain events
- It provides a transparent log of key operations a contract performs
So events are a useful tool in blockchain development to enable complex, reactive systems and provide transparency. Our simple increment contract shows how easy it is to include event publishing in a Soroban contract.
We hope this article has given you an idea of how Soroban events works. Stay tuned for more post in this "Soroban Contracts 101" Series where we will dive deeper into Soroban Contracts and their functionalities.
Top comments (1)
Cool! And how and where can we actually listen to the events? (subscribe to them?) Other contracts or clients or both?