Skip to main content

Contract Events

Learn how to listen to and handle events emitted by ink! smart contracts.

Overview

Contract events allow contracts to communicate with the outside world:

  • 📢 Emit events when important state changes occur
  • 👂 Listen to events in real-time
  • 📊 Index events for historical queries
  • 🔔 Trigger actions based on events

Prerequisites

  • ✅ Contract deployed to GLIN Network
  • ✅ Contract emits events (defined in ink! contract)
  • ✅ Connection to GLIN Network

Event Types

Contracts can emit different types of events:

// In your ink! contract
#[ink(event)]
pub struct Transfer {
#[ink(topic)]
from: Option<AccountId>,
#[ink(topic)]
to: Option<AccountId>,
value: Balance,
}

#[ink(event)]
pub struct Approval {
#[ink(topic)]
owner: AccountId,
#[ink(topic)]
spender: AccountId,
value: Balance,
}

Listen to Events

Real-Time Event Listening

listen-events.ts
import { GlinClient } from '@glin-ai/sdk';
import { ContractPromise } from '@polkadot/api-contract';

async function listenToEvents() {
const client = await GlinClient.connect('wss://testnet.glin.ai');

// Load contract
const contract = new ContractPromise(
client.api,
metadata,
contractAddress
);

console.log('👂 Listening for contract events...');

// Subscribe to all events from this contract
const unsub = await client.api.query.system.events((events) => {
events.forEach((record) => {
const { event } = record;

// Filter for contract events
if (client.api.events.contracts.ContractEmitted.is(event)) {
const [contractAddr, data] = event.data;

// Check if event is from our contract
if (contractAddr.toString() === contractAddress) {
// Decode event data
const decodedEvent = contract.abi.decodeEvent(data);

console.log('📊 Contract event:', decodedEvent);
console.log(' Event name:', decodedEvent.event.identifier);
console.log(' Data:', decodedEvent.args);
}
}
});
});

// Keep listening...
// To stop: unsub();
}

listenToEvents().catch(console.error);

Listen to Specific Events

listen-transfer-events.ts
async function listenToTransfers() {
const client = await GlinClient.connect('wss://testnet.glin.ai');
const contract = new ContractPromise(client.api, metadata, contractAddress);

const unsub = await client.api.query.system.events((events) => {
events.forEach((record) => {
const { event } = record;

if (client.api.events.contracts.ContractEmitted.is(event)) {
const [contractAddr, data] = event.data;

if (contractAddr.toString() === contractAddress) {
const decodedEvent = contract.abi.decodeEvent(data);

// Filter for Transfer events only
if (decodedEvent.event.identifier === 'Transfer') {
const { from, to, value } = decodedEvent.args;

console.log('💸 Transfer Event:');
console.log(` From: ${from}`);
console.log(` To: ${to}`);
console.log(` Amount: ${value.toString()}`);
}
}
}
});
});

// Cleanup
// unsub();
}

Event Filtering

Filter by Topics

// Listen to events involving a specific address
async function listenToUserTransfers(userAddress: string) {
const contract = new ContractPromise(client.api, metadata, contractAddress);

const unsub = await client.api.query.system.events((events) => {
events.forEach((record) => {
const { event } = record;

if (client.api.events.contracts.ContractEmitted.is(event)) {
const [contractAddr, data] = event.data;

if (contractAddr.toString() === contractAddress) {
const decodedEvent = contract.abi.decodeEvent(data);

if (decodedEvent.event.identifier === 'Transfer') {
const { from, to } = decodedEvent.args;

// Filter for transfers involving our user
if (from?.toString() === userAddress || to?.toString() === userAddress) {
console.log('📬 Transfer involving user:', decodedEvent.args);
}
}
}
}
});
});
}

Historical Events

Query past events from the blockchain:

query-historical-events.ts
async function getHistoricalEvents(fromBlock: number, toBlock: number) {
const client = await GlinClient.connect('wss://testnet.glin.ai');
const contract = new ContractPromise(client.api, metadata, contractAddress);

const events = [];

// Iterate through blocks
for (let blockNum = fromBlock; blockNum <= toBlock; blockNum++) {
const blockHash = await client.api.rpc.chain.getBlockHash(blockNum);
const apiAt = await client.api.at(blockHash);
const allEvents = await apiAt.query.system.events();

allEvents.forEach((record) => {
const { event } = record;

if (client.api.events.contracts.ContractEmitted.is(event)) {
const [contractAddr, data] = event.data;

if (contractAddr.toString() === contractAddress) {
const decodedEvent = contract.abi.decodeEvent(data);

events.push({
block: blockNum,
event: decodedEvent.event.identifier,
data: decodedEvent.args,
});
}
}
});
}

return events;
}

// Usage
const events = await getHistoricalEvents(1000, 2000);
console.log('Historical events:', events);

Event-Driven Actions

Trigger actions based on events:

event-driven.ts
async function eventDrivenBot() {
const client = await GlinClient.connect('wss://testnet.glin.ai');
const contract = new ContractPromise(client.api, metadata, contractAddress);

const unsub = await client.api.query.system.events((events) => {
events.forEach(async (record) => {
const { event } = record;

if (client.api.events.contracts.ContractEmitted.is(event)) {
const [contractAddr, data] = event.data;

if (contractAddr.toString() === contractAddress) {
const decodedEvent = contract.abi.decodeEvent(data);

// React to specific events
if (decodedEvent.event.identifier === 'LargeTransfer') {
const { from, to, value } = decodedEvent.args;

console.log('🚨 Large transfer detected!');
console.log(` Amount: ${value.toString()}`);

// Send notification
await sendAlert({
type: 'large_transfer',
from: from.toString(),
to: to.toString(),
amount: value.toString(),
});
}

if (decodedEvent.event.identifier === 'TokenMinted') {
console.log('🎉 New tokens minted!');
// Update database, send webhook, etc.
}
}
}
});
});
}

async function sendAlert(alert: any) {
// Send to webhook, database, notification service, etc.
console.log('Sending alert:', alert);
}

Event Indexing

Build an indexer to store and query events:

indexer.ts
import { GlinClient } from '@glin-ai/sdk';
import Database from 'better-sqlite3';

async function indexEvents() {
const client = await GlinClient.connect('wss://testnet.glin.ai');
const contract = new ContractPromise(client.api, metadata, contractAddress);

// Setup database
const db = new Database('events.db');
db.exec(`
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
block_number INTEGER,
event_name TEXT,
from_address TEXT,
to_address TEXT,
value TEXT,
timestamp INTEGER
)
`);

const insert = db.prepare(`
INSERT INTO events (block_number, event_name, from_address, to_address, value, timestamp)
VALUES (?, ?, ?, ?, ?, ?)
`);

// Subscribe to events
let currentBlock = await client.api.rpc.chain.getHeader();

const unsub = await client.api.query.system.events((events) => {
events.forEach((record) => {
const { event } = record;

if (client.api.events.contracts.ContractEmitted.is(event)) {
const [contractAddr, data] = event.data;

if (contractAddr.toString() === contractAddress) {
const decodedEvent = contract.abi.decodeEvent(data);

if (decodedEvent.event.identifier === 'Transfer') {
const { from, to, value } = decodedEvent.args;

insert.run(
currentBlock.number.toNumber(),
'Transfer',
from?.toString() || null,
to?.toString() || null,
value.toString(),
Date.now()
);

console.log('📝 Indexed Transfer event');
}
}
}
});
});
}

// Query indexed events
function queryEvents(db: Database, userAddress: string) {
const stmt = db.prepare(`
SELECT * FROM events
WHERE from_address = ? OR to_address = ?
ORDER BY block_number DESC
LIMIT 100
`);

return stmt.all(userAddress, userAddress);
}

Best Practices

1. Handle Connection Drops

async function resilientEventListener() {
async function subscribe() {
try {
const client = await GlinClient.connect('wss://testnet.glin.ai');

const unsub = await client.api.query.system.events((events) => {
// Process events...
});

// Handle disconnect
client.api.on('disconnected', () => {
console.log('⚠️ Disconnected, reconnecting...');
unsub();
setTimeout(subscribe, 5000); // Reconnect after 5s
});
} catch (error) {
console.error('Connection error:', error);
setTimeout(subscribe, 5000); // Retry after 5s
}
}

subscribe();
}

2. Batch Event Processing

// Process events in batches for efficiency
const eventBatch = [];
const BATCH_SIZE = 100;

const unsub = await client.api.query.system.events((events) => {
events.forEach((record) => {
// ... decode event ...
eventBatch.push(decodedEvent);

if (eventBatch.length >= BATCH_SIZE) {
processBatch(eventBatch.splice(0, BATCH_SIZE));
}
});
});

async function processBatch(events: any[]) {
// Bulk insert to database, send webhook, etc.
await db.insert(events);
}

3. Filter Early

// ❌ Bad - decode all events first
const unsub = await client.api.query.system.events((events) => {
events.forEach((record) => {
const decodedEvent = contract.abi.decodeEvent(data);
if (decodedEvent.event.identifier === 'Transfer') {
// Process transfer
}
});
});

// ✅ Good - filter before decoding
const unsub = await client.api.query.system.events((events) => {
events.forEach((record) => {
const { event } = record;

if (client.api.events.contracts.ContractEmitted.is(event)) {
const [contractAddr] = event.data;

if (contractAddr.toString() === contractAddress) {
const decodedEvent = contract.abi.decodeEvent(data);
// Now process
}
}
});
});

Next Steps


Need help? Join our Discord or check the ink! documentation.