Events
Events are the mechanism that applications off-chain can use to monitor changes and events in contracts on-chain.
How are events emitted?
ContractEvents
are emitted in Stellar Core's TransactionMeta
. You can see in the TransactionMetaV3 XDR below that there is a list of OperationEvents
called events
. Each OperationEvent
corresponds to an operation in a transaction, and itself contains a list of ContractEvents
. Note that events
will only be populated if the transaction succeeds.
ContractEvent
An event's topics don't have to be made of the same type: you can mix different types.
An event also contains a data object of any value or type, including custom types defined by contracts using #[contracttype]
:
struct ContractEvent
{
// We can use this to add more fields, or because it
// is first, to change ContractEvent into a union.
ExtensionPoint ext;
Hash* contractID;
ContractEventType type;
union switch (int v)
{
case 0:
struct
{
SCVec topics;
SCVal data;
} v0;
}
body;
};
OperationEvents
struct OperationEvents
{
ContractEvent events<>;
};
TransactionMetaV3
struct TransactionMetaV3
{
LedgerEntryChanges txChangesBefore; // tx level changes before operations
// are applied if any
OperationMeta operations<>; // meta for each operation
LedgerEntryChanges txChangesAfter; // tx level changes after operations are
// applied if any
OperationEvents events<>; // custom events populated by the
// contracts themselves. One list per operation.
TransactionResult txResult;
Hash hashes[3]; // stores sha256(txChangesBefore, operations, txChangesAfter),
// sha256(events), and sha256(txResult)
// Diagnostics events that are not hashed. One list per operation.
// This will contain all contract and diagnostic events. Even ones
// that were emitted in a failed contract call.
OperationDiagnosticEvents diagnosticEvents<>;
};
Link to the XDR above.
Event types
There are three ContractEventType
's -
CONTRACT
events are events emitted by contracts that use thecontract_event
host function to convey state changes.SYSTEM
events are events emitted by the host. At the moment, there's only one system event emitted by the host. It is emitted when theupdate_current_contract_wasm
host function is called, wheretopics = ["executable_update", old_executable: ContractExecutable, old_executable: ContractExecutable]
anddata = []
.DIAGNOSTIC
events are meant for debugging and will not be emitted unless the host instance explictly enables it. You can read more about this below.
What are diagnosticEvents?
While looking at the TransactionMetaV3
XDR struct above, you may have noticed the diagnosticEvents
field. This list will be empty by default unless your stellar-core instance has ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true
in its config file. If diagnostic events are enabled, this list will not only include all ContractEvents in events
, but will also include events from failed contract calls, errors from the host, events to trace the contract call stack, and logs from the log_from_linear_memory
host function. These events can be identified by type == DIAGNOSTIC
. The diagnostic events emitted by the host to track the call stack are defined below.
fn_call
The fn_call
diagnostic event is emitted when a contract is called and contains -
- Topics
- The symbol "fn_call".
- The contract id of the contract about to be called.
- A symbol containing the name of the function being called.
- Data
- A vector of the arguments passed to the function being called.
fn_return
The fn_return
diagnostic event is emitted when a contract call completes and contains -
- Topics
- The symbol "fn_return".
- A symbol containing the name of the function that is about to return.
- Data
- The value returned by the contract function.
When should diagnostic events be enabled?
events
contain ContractEvents
that should convey information about state changes. diagnosticEvents
on the other hand contain events that are not useful for most users, but may be helpful in debugging issues or building the contract call stack. Because they won't be used by most users, they can be optionally enabled because they are not hashed into the ledger, and therefore are not part of the protocol. This is done so a stellar-core node can stay in sync with the network while emitting these events that normally would not be useful for most users.
Due to the fact that a node with diagnostic events enabled will be executing code paths that diverge from a regular node, we highly encourage only using this feature on watcher node (nodes where NODE_IS_VALIDATOR=false
is set).
Reading events
You can use the getEvents
endpoint of any RPC service to fetch and filter events by type, contract, and topic.
Events are ephemeral: RPC providers typically only keep short chunks (less than a week) of history around.
To learn more about working with events, take a look at the events guides and this example contract.
For a quick high-level demonstration, though, we'll use the TypeScript SDK to infinitely fetch all transfer
events (defined by the Soroban Token Interface) involving the XLM contract and display them in a human-friendly format.
- JavaScript
import {
humanizeEvents,
nativeToScVal,
scValToNative,
Address,
Networks,
Asset,
xdr,
} from "@stellar/stellar-sdk";
import { Server } from "@stellar/stellar-sdk/rpc";
const s = new Server("https://soroban-testnet.stellar.org");
async function main() {
const response = await s.getLatestLedger();
const xlmFilter = {
type: "contract",
contractIds: [Asset.native().contractId(Networks.TESTNET)],
topics: [
// Defined in https://stellar.org/protocol/sep-41#interface
// for all compatible transfer events.
[
nativeToScVal("transfer", { type: "symbol" }).toXDR("base64"),
"*", // from anyone
"*", // to anyone
"*", // any asset (it'll be XLM anyway)
],
],
};
let page = await s.getEvents({
startLedger: response.sequence - 120, // start ~10m in the past
filters: [xlmFilter],
limit: 10,
});
// Run forever until Ctrl+C'd by user
while (true) {
if (!page.events.length) {
await new Promise((r) => setTimeout(r, 2000));
} else {
//
// Two ways to output a human-friendly version:
// 1. the RPC response itself for human-readable text
// 2. a helper for the XDR structured-equivalent for human-readable JSON
//
console.log(cereal(simpleEventLog(page.events)));
console.log(cereal(fullEventLog(page.events)));
}
// Fetch the next page until events are exhausted, then wait.
page = await s.getEvents({
filters: [xlmFilter],
cursor: page.cursor,
limit: 10,
});
}
}
function simpleEventLog(events) {
return events.map((event) => {
return {
topics: event.topic.map((t) => scValToNative(t)),
value: scValToNative(event.value),
};
});
}
function fullEventLog(events) {
return humanizeEvents(
events.map((event) => {
// rebuild the decomposed response into its original XDR structure
return new xdr.ContractEvent({
contractId: event.contractId.address().toBuffer(),
type: xdr.ContractEventType.contract(), // since we filtered on 'contract'
body: new xdr.ContractEventBody(
0,
new xdr.ContractEventV0({
topics: event.topic,
data: event.value,
}),
),
});
}),
);
}
// A custom JSONification method to handle bigints.
function cereal(data) {
return JSON.stringify(
data,
(k, v) => (typeof v === "bigint" ? v.toString() : v),
2,
);
}
main().catch((e) => console.error(e));
You can also leverage RPC's alternate XDR encoding formats like JSON to see human-readable events from the command-line directly, for example by passing xdrFormat: "json"
as an additional parameter to the getEvents
example.