datahaven/test/docs/event-utilities-guide.md
Ahmad Kaouk 4d448a4a21
test: wait for event utils (#121)
## Summary
This PR introduces comprehensive event waiting utilities for both
DataHaven (Substrate) and Ethereum chains, providing a unified
  interface for handling blockchain events in E2E tests.

  ## What's New
- **Event Utilities** (`test/utils/events.ts`): New utilities for
waiting on blockchain events
- `waitForDataHavenEvent`: Type-safe event waiting for Substrate chain
events
    - `waitForEthereumEvent`: Event waiting for Ethereum contract events
    - Graceful timeout handling (returns null instead of throwing)
    - Support for event filtering, callbacks, and custom timeouts

- **Documentation** (`test/docs/event-utilities-guide.md`):
Comprehensive guide covering usage examples for both DataHaven and
Ethereum.

  ## Test Plan
  - [ ] New event utilities work as expected
  - [ ] Event filtering works correctly for both chains
  - [ ] Timeout handling behaves as documented
  - [ ] Parallel event waiting with `Promise.all()` works

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-01 20:56:46 +02:00

6.6 KiB

Event Utilities Usage Guide

This guide demonstrates how to use event utilities for waiting and handling blockchain events in DataHaven (Substrate) and Ethereum chains.

Overview

The event utilities provide a unified, type-safe interface for handling blockchain events with:

  • Consistent API: Similar patterns for both DataHaven and Ethereum
  • Composable design: Use Promise.all() for parallel event waiting
  • Graceful timeouts: Functions return null on timeout (no errors thrown)
  • Type safety: Full TypeScript support with proper event typing

Quick Start

DataHaven Events

import { waitForDataHavenEvent } from '@test/e2e-suite/utils/datahaven';

const result = await waitForDataHavenEvent({
  api: dhApi,
  pallet: "Balances",
  event: "Transfer",
  timeout: 10000
});

if (result.data) {
  console.log(`Transfer: ${result.data.amount}`);
}

Ethereum Events

import { waitForEthereumEvent } from '@test/e2e-suite/utils/ethereum';

const result = await waitForEthereumEvent({
  client: publicClient,
  address: tokenAddress,
  abi: erc20Abi,
  eventName: "Transfer",
  timeout: 10000
});

if (result.log) {
  console.log(`Transfer: ${result.log.args.value}`);
}

DataHaven Event Handling

Transaction Submission (Direct Events)

When you submit your own transaction, you can get immediate access to all events:

const result = await dhApi.tx.Balances
  .transfer({ dest: recipient, value: amount })
  .signAndSubmit(signer);

// result type: TxFinalized
if (result.ok) {
  // Access all events from the transaction
  const transfer = result.events.find(
    e => e.pallet === "Balances" && e.name === "Transfer"
  );
  
  if (transfer) {
    console.log(`Transferred: ${transfer.value.amount}`);
  }
} else {
  console.error(`Failed:`, result.dispatchError);
}

Use this approach when:

  • You're submitting the transaction yourself
  • You need events from that specific transaction
  • You want synchronous access to results

Waiting for External Events

Use waitForDataHavenEvent when monitoring for events from other sources:

const result = await waitForDataHavenEvent({
  api: dhApi,
  pallet: "Balances", 
  event: "Transfer",
  filter: (e) => e.to === myAddress,
  timeout: 10000
});

if (result.data) {
  console.log(`Received transfer: ${result.data.amount}`);
}

Use this approach when:

  • Waiting for events from other transactions
  • Monitoring cross-chain events
  • Watching for external activity
  • Implementing time-based conditions

With Filtering

const result = await waitForDataHavenEvent({
  api: dhApi,
  pallet: "Balances",
  event: "Transfer",
  timeout: 10000,
  // Only match transfers from specific sender with amount > 1000
  filter: (event) => event.from === senderAddress && event.amount > 1000n
});

With Callbacks

const result = await waitForDataHavenEvent({
  api: dhApi,
  pallet: "Staking",
  event: "Rewarded",
  timeout: 30000,
  // Real-time processing as events are found
  onEvent: (event) => {
    console.log(`✅ Reward received: ${event.amount}`);
    updateRewardsDisplay(event.amount);
    // Callback doesn't affect the return value
  }
});

Multiple Events

Wait for multiple events in parallel:

const [transfer, reward, slash] = await Promise.all([
  waitForDataHavenEvent({
    api: dhApi,
    pallet: "Balances",
    event: "Transfer",
    timeout: 10000,
    filter: (e) => e.to === myAddress
  }),
  waitForDataHavenEvent({
    api: dhApi,
    pallet: "Staking",
    event: "Rewarded",
    timeout: 5000 // Shorter timeout for optional event
  }),
  waitForDataHavenEvent({
    api: dhApi,
    pallet: "Staking",
    event: "Slashed",
    timeout: 5000
  })
]);

// Handle results - some may be null (timeout)
if (!transfer.data) {
  throw new Error("Expected transfer not received");
}

if (reward.data) {
  console.log(`Received reward: ${reward.data.amount}`);
} else {
  console.log("No rewards in this period (normal)");
}

if (slash.data) {
  console.warn(`Got slashed: ${slash.data.amount}`);
}

Ethereum Event Handling

Basic Usage

const result = await waitForEthereumEvent({
  client: publicClient,
  address: contractAddress,
  abi: contractAbi,
  eventName: "StateChanged",
  timeout: 30000
});

if (result.log) {
  console.log(`New state: ${result.log.args.newState}`);
  console.log(`Block: ${result.log.blockNumber}`);
  console.log(`Tx: ${result.log.transactionHash}`);
}

With Argument Filtering

Filter events by their arguments:

const result = await waitForEthereumEvent({
  client: publicClient,
  address: tokenAddress,
  abi: erc20Abi,
  eventName: "Transfer",
  // Only match specific transfers
  args: {
    from: myAddress,      // Must be FROM myAddress
    to: recipientAddress  // Must be TO recipientAddress
    // Omit 'value' to match any amount
  },
  timeout: 30000
});

With Callbacks

Process events in real-time:

const result = await waitForEthereumEvent({
  client: publicClient,
  address: dexAddress,
  abi: dexAbi,
  eventName: "Swap",
  args: {
    tokenIn: wethAddress,
    tokenOut: usdcAddress
  },
  onEvent: (log) => {
    const { amountIn, amountOut } = log.args;
    const rate = Number(amountOut) / Number(amountIn);
    
    console.log(`Swap at rate: ${rate}`);
    console.log(`Block: ${log.blockNumber}`);
    
    // Update UI, send notifications, etc.
    updatePriceDisplay(rate);
  },
  timeout: 60000
});

Error Handling

Timeout Handling

Events that timeout return null, not an error:

const result = await waitForDataHavenEvent({
  api: dhApi,
  pallet: "Staking",
  event: "Rewarded",
  timeout: 5000
});

if (!result.data) {
  // Timeout - decide how to handle
  console.log("No rewards within 5 seconds");
  
  // Option 1: Continue (if event is optional)
  // Option 2: Retry with longer timeout
  // Option 3: Fail the test
  throw new Error("Expected rewards not received");
}

// Safe to use result.data here
console.log(`Rewards: ${result.data.amount}`);

Error vs Timeout

try {
  const result = await waitForEthereumEvent({
    client: publicClient,
    address: tokenAddress,
    abi: erc20Abi,
    eventName: "Transfer",
    timeout: 10000
  });
  
  if (!result.log) {
    // Timeout - not an error
    console.log("No transfer within 10 seconds");
    // Handle based on your needs
  } else {
    // Event found
    processTransfer(result.log);
  }
} catch (error) {
  // Only actual errors reach here:
  // - Network issues
  // - Invalid parameters
  // - Contract/API errors
  console.error("Unexpected error:", error);
}