Events
Let's, first of all, understand the interface of an Event
in polkadot-api. This interface will be common for every section after this one. The main idea is to give the developer as much information as possible:
type BlockInfo = {
hash: string
number: number
parent: string
}
type EventPhase =
| { type: "ApplyExtrinsic"; value: number }
| { type: "Finalization" }
| { type: "Initialization" }
type Event<T> = {
meta: {
block: BlockInfo
phase: EventPhase
}
payload: T
}
As one could notice, the structure directly comes from how events are shaped in Substrate. Fairly straight-forward, the phase
comes directly from the structure of events in the node, blockInfo
holds the information about the block in which the event is found, and the payload
depends on which kind of event we're querying. Let's see the three methods:
As seen in previous sections, we can access each event by typedApi.event.<pallet>.<event>
. For example, we could have typedApi.event.Balances.Burned
or typedApi.event.Proxy.PureCreated
as examples. Every event has the following EvClient interface:
type EvClient<T> = {
pull: EvPull<T>
watch: EvWatch<T>
filter: EvFilter<T>
isCompatible: IsCompatible
getCompatibilityLevel: GetCompatibilityLevel
}
We already learnt about isCompatible
and getCompatibilityLevel
, let's see step by step the other methods:
Pull
This is the simpler method. It'll allow us to fetch (Promise-based) all events (matching the event kind chosen) available in the latest known finalized
block. Let's see its interface and an example:
type EvPull<T> = () => Promise<Array<Event<T>>>
// this is an array of `Balances.Burned` events
const burnedEvents = await typedApi.events.Balances.Burned.pull()
Watch
This method is similar to the previous one, but Observable
-based. It'll allow us to subscribe to events (matching the event kind chosen) available on every finalized
block. This observable is multicast (multiple subscriptions will share the execution and results) and stateful (once subscribing you'll get the latest state available). This subscription will never complete since events will be emitted every time a new block is finalized. Note that the events will come in order of block number. Let's see the interface and an example:
export type EvWatch<T> = (
filter?: (value: T) => boolean,
) => Observable<Event<T>>
// note that we can add a filter function, for example we only want burned
// events with over 1 DOT in amount
// we console.log the first 5 blocks and complete
typedApi.event.Balances.Burned.watch(({ amount }) => amount > 10n ** 10n)
.pipe(take(5))
.forEach(console.log)
Filter
Filter hits the nail when you have a bunch of SystemEvents
(i.e. events got from finalized transactions, from a block using bestBlock$
observable, etc) and you want to get only a particular event kind from them all. Compared to the other two methods, you just get the payload
in this case, because there's no context around where you got this events from. Let's see the interface, and an example:
type EvFilter<T> = (collection: SystemEvent["event"][]) => Array<T>
// we get the finalized tx
const finalizedTx = await typedApi.tx.Balances.transfer_keep_alive({
dest: addr,
value: 10n ** 10n,
}).signAndSubmit(signer)
// it's synchronous!
// we have here the typed payload of the events
const filteredEvents = typedApi.events.Balances.Transfer.filter(
finalizedTx.events,
)