Files
zeekayandHanzo AI 2453240bd7
Universe CI / Resolve Environment (push) Waiting to run
Universe CI / Update Manifest (push) Blocked by required conditions
Universe CI / E2E (push) Blocked by required conditions
CI/CD / render (push) Waiting to run
chain letters are lowercase
The primary-network chains answer to either case; one spelling beats two.
/v1/chain/c/rpc, /v1/chain/p, /v1/chain/x across the estate.

Co-authored-by: Hanzo AI <[email protected]>
2026-09-08 15:28:19 -07:00

242 lines
7.5 KiB
TypeScript

import { Page, APIRequestContext } from '@playwright/test'
// ---------------------------------------------------------------------------
// Environment & URL resolution
// ---------------------------------------------------------------------------
const ENV = process.env.ENV || 'production'
const NETWORK = process.env.LUX_NETWORK || 'mainnet'
interface ZooUrls {
rpcZoo: string
rpcLux: string
rpcInfo: string
rpcHealth: string
rpcPlatform: string
exploreZoo: string
apiExploreZoo: string
exchange: string
website: string
}
// Per-network Lux API domains
const LUX_API: Record<string, string> = {
mainnet: 'https://api.lux.network',
testnet: 'https://api.lux-test.network',
devnet: 'https://api.lux-dev.network',
}
// Per-network explorer prefixes
const EXPLORE_PREFIX: Record<string, string> = {
mainnet: '',
testnet: 'test-',
devnet: 'dev-',
}
function productionUrls(): ZooUrls {
const luxApi = LUX_API[NETWORK] || LUX_API.mainnet
const ep = EXPLORE_PREFIX[NETWORK] || ''
return {
rpcZoo: `${luxApi}/${NETWORK}/v1/chain/zoo/rpc`,
rpcLux: `${luxApi}/${NETWORK}/v1/chain/c/rpc`,
rpcInfo: `${luxApi}/${NETWORK}/v1/info`,
rpcHealth: `${luxApi}/${NETWORK}/v1/health`,
rpcPlatform: `${luxApi}/${NETWORK}/v1/chain/p`,
exploreZoo: `https://${ep}explore-zoo.lux.network`,
apiExploreZoo: `https://api-${ep}explore-zoo.lux.network`,
exchange: 'https://zoo.exchange',
website: 'https://zoo.network',
}
}
function localUrls(): ZooUrls {
return {
rpcZoo: 'http://127.0.0.1:9630/v1/chain/zoo/rpc',
rpcLux: 'http://127.0.0.1:9630/v1/chain/c/rpc',
rpcInfo: 'http://127.0.0.1:9630/v1/info',
rpcHealth: 'http://127.0.0.1:9630/v1/health',
rpcPlatform: 'http://127.0.0.1:9630/v1/chain/p',
exploreZoo: 'http://localhost:4011',
apiExploreZoo: 'http://localhost:4011',
exchange: 'http://localhost:3001',
website: 'http://localhost:3002',
}
}
export function getUrls(): ZooUrls {
return ENV === 'local' ? localUrls() : productionUrls()
}
export function getEnv(): string {
return ENV
}
export function getNetwork(): string {
return NETWORK
}
export function isLocal(): boolean {
return ENV === 'local'
}
// ---------------------------------------------------------------------------
// Chain IDs — Zoo subnet EVM chain IDs
// ---------------------------------------------------------------------------
export const ZOO_CHAIN_IDS: Record<string, number> = {
mainnet: 200200,
testnet: 200201,
devnet: 200202,
}
// Parent Lux C-Chain IDs
export const LUX_CHAIN_IDS: Record<string, number> = {
mainnet: 96369,
testnet: 96368,
devnet: 96370,
}
export function getZooChainId(): number {
return ZOO_CHAIN_IDS[NETWORK] || ZOO_CHAIN_IDS.mainnet
}
export function getLuxChainId(): number {
return LUX_CHAIN_IDS[NETWORK] || LUX_CHAIN_IDS.mainnet
}
// ---------------------------------------------------------------------------
// JSON-RPC helpers
// ---------------------------------------------------------------------------
export async function rpcCall(
request: APIRequestContext,
url: string,
method: string,
params: unknown = [],
timeout = 15_000,
retries = 3,
): Promise<unknown> {
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const res = await request.post(url, {
data: { jsonrpc: '2.0', method, params, id: 1 },
headers: { 'Content-Type': 'application/json' },
ignoreHTTPSErrors: true,
timeout,
})
if (res.status() === 429 || res.status() >= 500) {
if (attempt < retries) { await new Promise(r => setTimeout(r, 1000 * (attempt + 1))); continue }
}
const json = await res.json()
if (json.error) throw new Error(`RPC ${method}: ${JSON.stringify(json.error)}`)
return json.result
} catch (e) {
if (attempt < retries) { await new Promise(r => setTimeout(r, 1000 * (attempt + 1))); continue }
throw e
}
}
}
export async function ethBlockNumber(request: APIRequestContext, rpcUrl: string): Promise<number> {
const result = await rpcCall(request, rpcUrl, 'eth_blockNumber') as string
return parseInt(result, 16)
}
export async function ethChainId(request: APIRequestContext, rpcUrl: string): Promise<number> {
const result = await rpcCall(request, rpcUrl, 'eth_chainId') as string
return parseInt(result, 16)
}
export async function ethGasPrice(request: APIRequestContext, rpcUrl: string): Promise<bigint> {
const result = await rpcCall(request, rpcUrl, 'eth_gasPrice') as string
return BigInt(result)
}
export async function getNetworkId(request: APIRequestContext, infoUrl: string): Promise<string> {
const result = await rpcCall(request, infoUrl, 'info.getNetworkID', {}) as { networkID: string }
return result.networkID
}
export async function isBootstrapped(request: APIRequestContext, infoUrl: string, chain: string): Promise<boolean> {
const result = await rpcCall(request, infoUrl, 'info.isBootstrapped', { chain }) as { isBootstrapped: boolean }
return result.isBootstrapped
}
export async function getNodeVersion(request: APIRequestContext, infoUrl: string): Promise<string> {
const result = await rpcCall(request, infoUrl, 'info.getNodeVersion', {}) as { version: string }
return result.version
}
export async function getBlockchains(request: APIRequestContext, platformUrl: string): Promise<unknown[]> {
const result = await rpcCall(request, platformUrl, 'platform.getBlockchains', {}) as { blockchains: unknown[] }
return result.blockchains || []
}
// ---------------------------------------------------------------------------
// HTTP helpers
// ---------------------------------------------------------------------------
export async function httpStatus(
request: APIRequestContext,
url: string,
timeout = 15_000
): Promise<number> {
try {
const res = await request.get(url, { ignoreHTTPSErrors: true, timeout })
return res.status()
} catch {
return 0
}
}
export async function httpGetJson(
request: APIRequestContext,
url: string,
timeout = 15_000
): Promise<unknown> {
const res = await request.get(url, {
ignoreHTTPSErrors: true,
timeout,
headers: { Accept: 'application/json' },
})
return res.json()
}
// ---------------------------------------------------------------------------
// Screenshot helper
// ---------------------------------------------------------------------------
export async function snap(page: Page, name: string) {
try {
await page.screenshot({ path: `e2e/${name}.png`, fullPage: false, timeout: 30_000 })
} catch {
console.log(`WARN: Screenshot ${name} timed out, skipping`)
}
}
// ---------------------------------------------------------------------------
// Latency benchmark
// ---------------------------------------------------------------------------
export async function benchmarkRpc(
request: APIRequestContext,
url: string,
method: string,
params: unknown = [],
iterations = 10
): Promise<{ avg: number; p50: number; p99: number; errors: number }> {
const times: number[] = []
let errors = 0
for (let i = 0; i < iterations; i++) {
const t0 = Date.now()
try {
await rpcCall(request, url, method, params, 10_000)
times.push(Date.now() - t0)
} catch {
errors++
}
}
if (times.length === 0) return { avg: 0, p50: 0, p99: 0, errors }
times.sort((a, b) => a - b)
const avg = times.reduce((a, b) => a + b, 0) / times.length
const p50 = times[Math.floor(times.length / 2)]
const p99 = times[times.length - 1]
return { avg: Math.round(avg), p50: Math.round(p50), p99: Math.round(p99), errors }
}