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
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]>
242 lines
7.5 KiB
TypeScript
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 }
|
|
}
|