Selendra

Documentation

Event Monitoring

Subscribing to and processing blockchain events

Event monitoring enables reactive applications. Subscribe to real-time updates for transactions, transfers, contract events. Selendra fast block times (~1 second) provide quick event delivery.

Event Monitoring Methods

MethodUse CaseLatencyResource Usage
WebSocketReal-time updates, user actions<1 secondLow (persistent connection)
HTTP PollingPeriodic checks, background sync5-30 secondsMedium (repeated requests)
Historical QueryPast events, analysis, backfillInstantHigh (blockchain scan)
IndexerComplex queries, aggregations<2 secondsLowest (pre-indexed)

WebSocket Subscriptions

Basic Event Listening

import { Web3 } from 'web3';

const web3 = new Web3('wss://rpc.selendra.org');

// Subscribe to new blocks
const subscription = await web3.eth.subscribe('newBlockHeaders');

subscription.on('data', (block) => {
  console.log('New block:', block.number);
});

subscription.on('error', console.error);

Contract Events

const contract = new web3.eth.Contract(abi, contractAddress);

// Subscribe to Transfer events
const subscription = contract.events.Transfer({
  filter: { from: userAddress }
});

subscription.on('data', (event) => {
  console.log('Transfer:', event.returnValues);
});

subscription.on('error', console.error);

Multiple Events

// Monitor all events
const allEvents = contract.events.allEvents();

allEvents.on('data', (event) => {
  switch (event.event) {
    case 'Transfer':
      handleTransfer(event.returnValues);
      break;

    case 'Approval':
      handleApproval(event.returnValues);
      break;

    case 'Burn':
      handleBurn(event.returnValues);
      break;
  }
});

Filtered Subscription

// Only transfers to specific address
const filtered = contract.events.Transfer({
  filter: {
    to: recipientAddress
  }
});

filtered.on('data', (event) => {
  notifyUser({
    title: 'Tokens Received',
    amount: web3.utils.fromWei(event.returnValues.value, 'ether'),
    from: event.returnValues.from
  });
});

Historical Event Queries

Fetch Past Events

// Get all Transfer events in last 1000 blocks
const currentBlock = await web3.eth.getBlockNumber();

const events = await contract.getPastEvents('Transfer', {
  fromBlock: currentBlock - 1000,
  toBlock: 'latest'
});

console.log(`Found ${events.length} transfers`);

Paginated Query

async function fetchEventsPaginated(fromBlock, toBlock, batchSize = 1000) {
  const allEvents = [];

  for (let start = fromBlock; start <= toBlock; start += batchSize) {
    const end = Math.min(start + batchSize - 1, toBlock);

    const batch = await contract.getPastEvents('Transfer', {
      fromBlock: start,
      toBlock: end
    });

    allEvents.push(...batch);
  }

  return allEvents;
}

Filtered Query

// Get specific user's transfers
const userTransfers = await contract.getPastEvents('Transfer', {
  filter: {
    from: userAddress
  },
  fromBlock: 0,
  toBlock: 'latest'
});

// Calculate total sent
const totalSent = userTransfers.reduce((sum, event) => {
  return sum + BigInt(event.returnValues.value);
}, 0n);

React Integration

Custom Event Hook

import { useState, useEffect } from 'react';

function useContractEvent(contract, eventName, filter = {}) {
  const [events, setEvents] = useState([]);

  useEffect(() => {
    const subscription = contract.events[eventName]({ filter });

    subscription.on('data', (event) => {
      setEvents(prev => [event, ...prev]);
    });

    return () => subscription.unsubscribe();
  }, [contract, eventName, JSON.stringify(filter)]);

  return events;
}

// Usage
function TransferMonitor({ userAddress }) {
  const transfers = useContractEvent(contract, 'Transfer', {
    to: userAddress
  });

  return (
    <div>
      <h3>Recent Transfers ({transfers.length})</h3>
      {transfers.map(event => (
        <div key={event.transactionHash}>
          {web3.utils.fromWei(event.returnValues.value, 'ether')} SEL
          from {event.returnValues.from}
        </div>
      ))}
    </div>
  );
}

Notification Component

function EventNotifications({ contract }) {
  const [notifications, setNotifications] = useState([]);

  useEffect(() => {
    const sub = contract.events.allEvents();

    sub.on('data', (event) => {
      setNotifications(prev => [{
        id: event.transactionHash,
        type: event.event,
        data: event.returnValues,
        timestamp: Date.now()
      }, ...prev].slice(0, 10)); // Keep last 10
    });

    return () => sub.unsubscribe();
  }, [contract]);

  return (
    <div className="notifications">
      {notifications.map(notif => (
        <div key={notif.id} className="notification">
          {notif.type}: {JSON.stringify(notif.data)}
        </div>
      ))}
    </div>
  );
}

Event Processing

Aggregation

class EventAggregator {
  constructor(contract) {
    this.contract = contract;
    this.stats = {
      totalTransfers: 0,
      totalVolume: 0n
    };
  }

  async start() {
    const sub = this.contract.events.Transfer();

    sub.on('data', (event) => {
      this.stats.totalTransfers++;
      this.stats.totalVolume += BigInt(event.returnValues.value);
    });
  }

  getStats() {
    return {
      transfers: this.stats.totalTransfers,
      volume: web3.utils.fromWei(this.stats.totalVolume.toString(), 'ether')
    };
  }
}

Debouncing

function debounce(fn, delay) {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), delay);
  };
}

const debouncedHandler = debounce((event) => {
  updateUI(event);
}, 1000);

contract.events.Transfer().on('data', debouncedHandler);

Block Monitoring

New Blocks

const blockSub = await web3.eth.subscribe('newBlockHeaders');

blockSub.on('data', async (blockHeader) => {
  console.log(`Block #${blockHeader.number}`);
  console.log(`Transactions: ${blockHeader.transactions?.length || 0}`);

  // Fetch full block if needed
  const fullBlock = await web3.eth.getBlock(blockHeader.number, true);
});

Transaction Receipts

async function monitorUserTransactions(userAddress) {
  const blockSub = await web3.eth.subscribe('newBlockHeaders');

  blockSub.on('data', async (block) => {
    const fullBlock = await web3.eth.getBlock(block.number, true);

    for (const tx of fullBlock.transactions) {
      if (tx.from === userAddress || tx.to === userAddress) {
        const receipt = await web3.eth.getTransactionReceipt(tx.hash);

        console.log('User transaction:', {
          hash: tx.hash,
          status: receipt.status ? 'success' : 'failed'
        });
      }
    }
  });
}

Error Handling

class ConnectionManager {
  constructor(wsUrl) {
    this.wsUrl = wsUrl;
    this.reconnecting = false;
  }

  async connect() {
    try {
      this.web3 = new Web3(this.wsUrl);
      this.setupSubscriptions();

    } catch (error) {
      console.error('Connection failed:', error);
      this.reconnect();
    }
  }

  setupSubscriptions() {
    const sub = this.web3.eth.subscribe('newBlockHeaders');

    sub.on('error', () => {
      if (!this.reconnecting) {
        this.reconnect();
      }
    });

    sub.on('data', (block) => {
      this.reconnecting = false;
    });
  }

  reconnect() {
    if (this.reconnecting) return;

    this.reconnecting = true;
    setTimeout(() => this.connect(), 5000);
  }
}

Performance Optimization

Event Caching

class EventCache {
  constructor(maxSize = 1000) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }

  add(event) {
    this.cache.set(event.transactionHash, event);

    if (this.cache.size > this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
  }

  get(txHash) {
    return this.cache.get(txHash);
  }
}

Batch Processing

class EventBatcher {
  constructor(processFn, delay = 1000) {
    this.batch = [];
    this.processFn = processFn;
    this.delay = delay;
  }

  add(event) {
    this.batch.push(event);

    if (!this.timer) {
      this.timer = setTimeout(() => this.flush(), this.delay);
    }
  }

  flush() {
    if (this.batch.length > 0) {
      this.processFn(this.batch);
      this.batch = [];
    }
    this.timer = null;
  }
}

StadiumX Implementation

StadiumX monitors 4 key events:

  • TicketPurchased: Update seat map, confirm payment
  • TicketTransferred: Update ownership, notify recipient
  • EventCancelled: Trigger refunds, update status
  • RefundProcessed: Confirm refund completion

Lessons Learned

  • Reconnection: Implement automatic reconnect with exponential backoff
  • Deduplication: Cache processed events to prevent duplicate handling
  • Rate Limiting: Batch updates to avoid overwhelming UI
  • Error Recovery: Gracefully handle connection failures

Transaction Handling | Using Selendra SDK

Community

Telegram: t.me/selendranetwork | X: @selendranetwork | GitHub: github.com/selendra/selendra

Contribute

Found an issue or want to contribute?

Help us improve this documentation by editing this page on GitHub.

Edit this page on GitHub