datahaven/test/framework/connectors.ts
Ahmad Kaouk 41788d56bb
test: refactor e2e tests (#365)
This PR significantly refactors and improves the end-to-end testing
framework and infrastructure. The primary focus was on simplifying the
test suites, improving reliability through better resource management,
and hardening the relayer infrastructure.

All E2E tests are now passing on the CI and demonstrate consistent
reliability when run locally.

### Key Changes

#### 1. E2E Test Suite Refactor & Cleanup
* **Simplified Test Logic**: Heavily refactored the core test suites
(`native-token-transfer.test.ts`, `rewards-message.test.ts`, and
`validator-set-update.test.ts`). The new implementation is much cleaner,
utilizing shared helpers to reduce boilerplate.
* **Utility Consolidation**: Removed redundant utility files
(`storage.ts`, `rewards-helpers.ts`) and simplified `events.ts`. Event
waiting now uses `rxjs` for Substrate and native `viem` watchers for
Ethereum, which is more robust and easier to maintain.
* **Better Connector Management**: Unified the creation and cleanup of
test clients in `ConnectorFactory`. It now handles the lifecycle of
WebSocket connections more gracefully, including clearing the
`socketClientCache` to prevent reconnection noise during teardown.

#### 2. Infrastructure & Stability
* **Relayer Relaunch Policy**: Added a restart policy for Snowbridge
relayer containers. They are now configured with `--restart
on-failure:5`, ensuring that relayers automatically relaunch if they
crash during the sensitive initialization phase.
*   **WebSocket Integration**: 
* Updated the `ConnectorFactory` to prefer **WebSockets** for the
Ethereum public client, which is essential for efficient, event-heavy
E2E testing.
* Enhanced `launchKurtosisNetwork` to correctly identify and register
the Execution Layer's WebSocket endpoint from Kurtosis.
* **Disabled Contract Injection**: This PR temporarily disables the
automatic injection of contracts into the genesis state by default.
* *Reason*: I encountered issues generating a valid `state-diff.json`
for the latest contract versions. Even after applying several
workarounds, the injected state remained unstable. As a result, I've
reverted to manual contract deployment during the launch sequence for
better reliability for now.

#### 3. Documentation & Maintenance
* Removed obsolete documentation (`event-utilities-guide.md`) that no
longer reflects the simplified event-handling API.
* Cleaned up `test/launcher/validators.ts` and moved logic into more
appropriate helpers.

---------

Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
2025-12-24 13:31:40 +01:00

146 lines
4.3 KiB
TypeScript

import { datahaven } from "@polkadot-api/descriptors";
import { createClient as createPapiClient, type PolkadotClient } from "polkadot-api";
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
import { getWsProvider } from "polkadot-api/ws-provider/node";
import { ANVIL_FUNDED_ACCOUNTS, type DataHavenApi, logger } from "utils";
import {
type Account,
createPublicClient,
createWalletClient,
fallback,
http,
type PublicClient,
type WalletClient,
webSocket
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { anvil } from "viem/chains";
import { socketClientCache } from "viem/utils";
import type { LaunchNetworkResult } from "../launcher";
export interface TestConnectors {
// Ethereum connectors
publicClient: PublicClient;
walletClient: WalletClient<any, any, Account>;
// DataHaven connectors
papiClient: PolkadotClient;
dhApi: DataHavenApi;
// Raw URLs
elRpcUrl: string;
dhRpcUrl: string;
}
export class ConnectorFactory {
private connectors: LaunchNetworkResult;
constructor(connectors: LaunchNetworkResult) {
this.connectors = connectors;
}
/**
* Create test connectors for interacting with the launched networks
*/
async createTestConnectors(): Promise<TestConnectors> {
logger.debug("Creating test connectors...");
// Prefer WebSocket for event-heavy public client; fall back to HTTP when WS is unavailable.
const wsUrl = this.connectors.ethereumWsUrl;
const publicTransport = wsUrl?.startsWith("ws")
? fallback([webSocket(wsUrl), http(this.connectors.ethereumRpcUrl)])
: http(this.connectors.ethereumRpcUrl);
// Create Ethereum clients
const publicClient = createPublicClient({
chain: anvil,
transport: publicTransport
});
const account = privateKeyToAccount(ANVIL_FUNDED_ACCOUNTS[0].privateKey);
const walletClient = createWalletClient({
account,
chain: anvil,
transport: http(this.connectors.ethereumRpcUrl)
});
// Create DataHaven/Substrate clients
// Note: polkadot-api can handle HTTP RPC URLs even when passed to getWsProvider
const wsProvider = getWsProvider(this.connectors.dataHavenRpcUrl);
const papiClient = createPapiClient(withPolkadotSdkCompat(wsProvider));
// Get typed API
const dhApi = papiClient.getTypedApi(datahaven);
logger.debug("Test connectors created successfully");
return {
publicClient,
walletClient,
papiClient,
dhApi,
elRpcUrl: this.connectors.ethereumRpcUrl,
dhRpcUrl: this.connectors.dataHavenRpcUrl
};
}
/**
* Create a wallet client with a specific account
*/
createWalletClient(privateKey: `0x${string}`): WalletClient<any, any, Account> {
const account = privateKeyToAccount(privateKey);
return createWalletClient({
account,
chain: anvil,
transport: http(this.connectors.ethereumRpcUrl)
});
}
/**
* Clean up connections
*/
async cleanup(connectors: TestConnectors): Promise<void> {
logger.debug("Cleaning up test connectors...");
// Close any cached WebSocket clients used by viem to prevent reconnect noise after teardown.
try {
for (const client of socketClientCache.values()) {
try {
client.close();
} catch {
// Ignore individual socket close errors
}
}
socketClientCache.clear();
} catch {
// Ignore cache errors during cleanup
}
// Destroy PAPI client
if (connectors.papiClient) {
try {
connectors.papiClient.destroy();
} catch (error) {
// Ignore DisjointError - it occurs when ChainHead subscriptions are already disjointed
// This is harmless and expected during cleanup
const errorName = error instanceof Error ? error.name : String(error);
const errorMessage = error instanceof Error ? error.message : String(error);
if (
errorName === "DisjointError" ||
errorName.includes("disjoint") ||
errorMessage.includes("disjoint") ||
errorMessage.includes("ChainHead disjointed")
) {
// Ignore - this is expected and harmless
} else {
// Re-throw unexpected errors
throw error;
}
}
}
logger.debug("Test connectors cleaned up");
}
}