mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
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>
This commit is contained in:
parent
1ea04ee75d
commit
4d448a4a21
3 changed files with 490 additions and 0 deletions
298
test/docs/event-utilities-guide.md
Normal file
298
test/docs/event-utilities-guide.md
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
# 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
|
||||
```typescript
|
||||
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
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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
|
||||
```typescript
|
||||
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
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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);
|
||||
}
|
||||
```
|
||||
191
test/utils/events.ts
Normal file
191
test/utils/events.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { firstValueFrom, of } from "rxjs";
|
||||
import { catchError, take, tap, timeout } from "rxjs/operators";
|
||||
import type { Abi, Address, Log, PublicClient } from "viem";
|
||||
import { logger } from "./logger";
|
||||
import type { DataHavenApi } from "./papi";
|
||||
|
||||
/**
|
||||
* Event utilities for DataHaven and Ethereum chains
|
||||
*
|
||||
* This module provides utilities for waiting for events on different chains:
|
||||
* - DataHaven events (substrate-based chain events)
|
||||
* - Ethereum events (using viem event filters)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Result from waiting for a DataHaven event
|
||||
*/
|
||||
export interface DataHavenEventResult<T = unknown> {
|
||||
/** Pallet name */
|
||||
pallet: string;
|
||||
/** Event name */
|
||||
event: string;
|
||||
/** Event data payload (null if timeout or error) */
|
||||
data: T | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for waiting for a single DataHaven event
|
||||
*/
|
||||
export interface WaitForDataHavenEventOptions<T = unknown> {
|
||||
/** DataHaven API instance */
|
||||
api: DataHavenApi;
|
||||
/** Pallet name (e.g., "System", "Balances") */
|
||||
pallet: string;
|
||||
/** Event name (e.g., "ExtrinsicSuccess", "Transfer") */
|
||||
event: string;
|
||||
/** Optional filter function to match specific events */
|
||||
filter?: (event: T) => boolean;
|
||||
/** Timeout in milliseconds (default: 30000) */
|
||||
timeout?: number;
|
||||
/** Callback for matched event */
|
||||
onEvent?: (event: T) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specific event on the DataHaven chain
|
||||
* @param options - Options for event waiting
|
||||
* @returns Event result with pallet, event name, and data
|
||||
*/
|
||||
export async function waitForDataHavenEvent<T = unknown>(
|
||||
options: WaitForDataHavenEventOptions<T>
|
||||
): Promise<DataHavenEventResult<T>> {
|
||||
const { api, pallet, event, filter, timeout: timeoutMs = 30000, onEvent } = options;
|
||||
|
||||
const eventWatcher = (api.event as any)?.[pallet]?.[event];
|
||||
if (!eventWatcher?.watch) {
|
||||
logger.warn(`Event ${pallet}.${event} not found`);
|
||||
return { pallet, event, data: null };
|
||||
}
|
||||
|
||||
let data: T | null;
|
||||
try {
|
||||
data = await firstValueFrom(
|
||||
eventWatcher.watch(filter).pipe(
|
||||
tap((eventData: T) => {
|
||||
logger.debug(`Event ${pallet}.${event} received`);
|
||||
onEvent?.(eventData);
|
||||
}),
|
||||
take(1), // Always stop on first event
|
||||
timeout({
|
||||
first: timeoutMs,
|
||||
with: () => {
|
||||
logger.debug(`Timeout waiting for event ${pallet}.${event} after ${timeoutMs}ms`);
|
||||
return of(null);
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
logger.error(`Error in event subscription ${pallet}.${event}: ${error}`);
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch {
|
||||
data = null;
|
||||
}
|
||||
|
||||
return { pallet, event, data };
|
||||
}
|
||||
|
||||
// ================== Ethereum Event Utilities ==================
|
||||
|
||||
/**
|
||||
* Result from waiting for an Ethereum event
|
||||
*/
|
||||
export interface EthereumEventResult {
|
||||
/** Contract address */
|
||||
address: Address;
|
||||
/** Event name */
|
||||
eventName: string;
|
||||
/** Event log (null if timeout or error) */
|
||||
log: Log | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for waiting for a single Ethereum event
|
||||
*/
|
||||
export interface WaitForEthereumEventOptions<TAbi extends Abi = Abi> {
|
||||
/** Viem public client instance */
|
||||
client: PublicClient;
|
||||
/** Contract address */
|
||||
address: Address;
|
||||
/** Contract ABI */
|
||||
abi: TAbi;
|
||||
/** Event name to watch for */
|
||||
eventName: any;
|
||||
/** Optional event arguments to filter */
|
||||
args?: any;
|
||||
/** Timeout in milliseconds (default: 30000) */
|
||||
timeout?: number;
|
||||
/** Include events from past blocks */
|
||||
fromBlock?: bigint;
|
||||
/** Callback for each matched event */
|
||||
onEvent?: (log: Log) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a specific event on the Ethereum chain
|
||||
* @param options - Options for event waiting
|
||||
* @returns Event result with address, event name, and log
|
||||
*/
|
||||
export async function waitForEthereumEvent<TAbi extends Abi = Abi>(
|
||||
options: WaitForEthereumEventOptions<TAbi>
|
||||
): Promise<EthereumEventResult> {
|
||||
const { client, address, abi, eventName, args, timeout = 30000, fromBlock, onEvent } = options;
|
||||
|
||||
const log = await new Promise<Log | null>((resolve) => {
|
||||
let unwatch: (() => void) | null = null;
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
let matchedLog: Log | null = null;
|
||||
|
||||
const cleanup = () => {
|
||||
if (unwatch) {
|
||||
unwatch();
|
||||
}
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
|
||||
// Set up timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
logger.debug(`Timeout waiting for Ethereum event ${eventName} after ${timeout}ms`);
|
||||
cleanup();
|
||||
resolve(matchedLog);
|
||||
}, timeout);
|
||||
|
||||
// Watch for events
|
||||
try {
|
||||
unwatch = client.watchContractEvent({
|
||||
address,
|
||||
abi,
|
||||
eventName,
|
||||
args,
|
||||
fromBlock,
|
||||
onLogs: (logs) => {
|
||||
logger.debug(`Ethereum event ${eventName} received: ${logs.length} logs`);
|
||||
|
||||
if (logs.length > 0) {
|
||||
matchedLog = logs[0];
|
||||
if (onEvent) {
|
||||
onEvent(matchedLog);
|
||||
}
|
||||
cleanup();
|
||||
resolve(matchedLog);
|
||||
}
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
logger.error(`Error watching Ethereum event ${eventName}: ${error}`);
|
||||
cleanup();
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to watch Ethereum event ${eventName}: ${error}`);
|
||||
cleanup();
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
return { address, eventName, log };
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ export * from "./blockscout";
|
|||
export * from "./constants";
|
||||
export * from "./contracts";
|
||||
export * from "./docker";
|
||||
export * from "./events";
|
||||
export * from "./input";
|
||||
export * from "./kurtosis";
|
||||
export * from "./logger";
|
||||
|
|
|
|||
Loading…
Reference in a new issue