Skip to content
Merged
46 changes: 25 additions & 21 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ Deno 2.x test suite that validates Ethereum JSON-RPC compatibility for Polkadot'

## Quick Reference

| Command | What it does |
| ---------------------------- | -------------------------------------------------------------------- |
| `deno task test:evm` | Build + test EVM bytecode on Revive (auto-starts dev-node + eth-rpc) |
| `deno task test:pvm` | Build + test PVM bytecode on Revive (auto-starts dev-node + eth-rpc) |
| `deno task test:geth` | Build + test EVM bytecode on Geth (reference) |
| `deno task test:evm:manual` | EVM tests without auto-starting services |
| `deno task test:pvm:manual` | PVM tests without auto-starting services |
| `deno task test:update` | Update snapshots from Geth |
| `deno task build` | Compile contracts (solc + resolc) |
| `deno task build --solcOnly` | Compile contracts (solc only, faster) |
| `deno task lint` | Format check + lint |
| Command | What it does |
| ------------------------------- | -------------------------------------------------------------------- |
| `deno task test:evm` | Build + test EVM bytecode on Revive (auto-starts dev-node + eth-rpc) |
| `deno task test:pvm` | Build + test PVM bytecode on Revive (auto-starts dev-node + eth-rpc) |
| `deno task test:geth` | Build + test EVM bytecode on Geth (reference) |
| `deno task test:evm:manual` | EVM tests without auto-starting services |
| `deno task test:pvm:manual` | PVM tests without auto-starting services |
| `deno task test:westend` | Build + test against live Westend runtime (auto-starts services) |
| `deno task test:westend:manual` | Westend tests without auto-starting services |
| `deno task test:update` | Update snapshots from Geth |
| `deno task build` | Compile contracts (solc + resolc) |
| `deno task build --solcOnly` | Compile contracts (solc only, faster) |
| `deno task lint` | Format check + lint |

## Architecture

Expand All @@ -26,16 +28,18 @@ Deno 2.x test suite that validates Ethereum JSON-RPC compatibility for Polkadot'

## Environment Variables

| Variable | Purpose | Default |
| ----------------------- | ----------------------- | --------------------------------------------- |
| `USE_BYTECODE` | `evm` or `pvm` | — |
| `START_REVIVE_DEV_NODE` | Auto-start dev-node | — |
| `START_ETH_RPC` | Auto-start eth-rpc | — |
| `START_GETH` | Auto-start geth | — |
| `REVIVE_DEV_NODE_PATH` | Path to dev-node binary | `~/polkadot-sdk/target/debug/revive-dev-node` |
| `ETH_RPC_PATH` | Path to eth-rpc binary | `~/polkadot-sdk/target/debug/eth-rpc` |
| `GETH_PATH` | Path to geth binary | `geth` |
| `RPC_PORT` | JSON-RPC port | `8545` |
| Variable | Purpose | Default |
| ----------------------- | ----------------------------- | --------------------------------------------- |
| `USE_BYTECODE` | `evm` or `pvm` | — |
| `START_REVIVE_DEV_NODE` | Auto-start dev-node | — |
| `START_ETH_RPC` | Auto-start eth-rpc | — |
| `START_GETH` | Auto-start geth | — |
| `REVIVE_DEV_NODE_PATH` | Path to dev-node binary | `~/polkadot-sdk/target/debug/revive-dev-node` |
| `ETH_RPC_PATH` | Path to eth-rpc binary | `~/polkadot-sdk/target/debug/eth-rpc` |
| `GETH_PATH` | Path to geth binary | `geth` |
| `RPC_PORT` | JSON-RPC port | `8545` |
| `USE_LIVE_RUNTIME` | Download live Westend runtime | — |
| `WESTEND_RPC_URL` | Westend Asset Hub RPC URL | `https://westend-asset-hub-rpc.polkadot.io` |

## CI Jobs (polkadot-sdk)

Expand Down
2 changes: 2 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"test:update": "deno task build --solcOnly && USE_BYTECODE=evm START_GETH=true deno test -P -- --update",
"test:polkadot": "USE_BYTECODE=evm START_ASSET_HUB_WESTEND=true START_ETH_RPC=true deno test -P src/polkadot-tests.ts",
"test:polkadot:manual": "USE_BYTECODE=evm deno test -P src/polkadot-tests.ts",
"test:westend": "deno task build --solcOnly && USE_BYTECODE=evm START_ASSET_HUB_WESTEND=true USE_LIVE_RUNTIME=true START_ETH_RPC=true deno test -P src/westend-tests.ts",
"test:westend:manual": "USE_BYTECODE=evm deno test -P src/westend-tests.ts",
"lint": "deno fmt --check && deno task build && deno check && deno lint"
},
"test": {
Expand Down
87 changes: 77 additions & 10 deletions src/test-setup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { killProcessOnPort, waitForHealth } from './util.ts'
import { hexToBytes, toHex } from 'viem/utils'
import { getRpcUrl, killProcessOnPort, waitForHealth } from './util.ts'

let processes: Deno.ChildProcess[] = []
let tmpDirs: string[] = []
let setupComplete = false

async function detectAndSetPlatform(url: string) {
Expand Down Expand Up @@ -76,7 +78,12 @@ export async function setupTests() {
`${Deno.env.get('HOME')}/polkadot-sdk`
const omniNode = Deno.env.get('OMNI_NODE_PATH') ??
`${sdkDir}/target/release/polkadot-omni-node`
const chainSpec = await buildAssetHubWestendSpec(sdkDir, omniNode)
const useLiveRuntime = !!Deno.env.get('USE_LIVE_RUNTIME')
const chainSpec = await buildAssetHubWestendSpec(
sdkDir,
omniNode,
useLiveRuntime,
)

const nodeArgs = [
'--dev',
Expand Down Expand Up @@ -142,13 +149,11 @@ export async function setupTests() {
}

// Detect and set the PLATFORM variable
const port = Deno.env.get('RPC_PORT') ?? '8545'
const url = `http://localhost:${port}`
await detectAndSetPlatform(url)
await detectAndSetPlatform(getRpcUrl())
}

export function cleanupTests() {
if (processes.length === 0) {
if (processes.length === 0 && tmpDirs.length === 0) {
return
}
console.log('🔌 Shutting down servers...')
Expand All @@ -159,7 +164,15 @@ export function cleanupTests() {
// Process might already be dead
}
}
for (const dir of tmpDirs) {
try {
Deno.removeSync(dir, { recursive: true })
} catch {
// Directory might already be gone
}
}
processes = []
tmpDirs = []
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -200,14 +213,59 @@ async function runCommand(
return new TextDecoder().decode(output.stdout)
}

async function downloadLiveRuntime(
tmpDir: string,
rpcUrl: string,
): Promise<string> {
const codeStorageKey = toHex(':code')
console.log(`📦 Downloading runtime from ${rpcUrl} ...`)
const resp = await fetch(rpcUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(120_000 /* 2 min */),
body: JSON.stringify({
jsonrpc: '2.0',
method: 'state_getStorage',
params: [codeStorageKey],
id: 1,
}),
})
if (!resp.ok) {
throw new Error(
`RPC request failed: ${resp.status} ${resp.statusText}`,
)
}
const { result } = (await resp.json()) as { result: string }
if (!result || result === '0x') {
throw new Error('Failed to fetch runtime wasm from live network')
}
const runtimeCode = hexToBytes(result as `0x${string}`)
const path = `${tmpDir}/westend-asset-hub-runtime.wasm`
await Deno.writeFile(path, runtimeCode)
console.log(
`📦 Runtime downloaded (${
(runtimeCode.length / 1024 / 1024).toFixed(1)
} MB)`,
)
return path
}

async function buildAssetHubWestendSpec(
sdkDir: string,
omniNode: string,
useLiveRuntime = false,
): Promise<string> {
const runtime =
`${sdkDir}/target/release/wbuild/asset-hub-westend-runtime/asset_hub_westend_runtime.compact.compressed.wasm`
const basePath = '/tmp/ah-westend-base.json'
const rawPath = '/tmp/ah-westend-raw.json'
const tmpDir = await Deno.makeTempDir({ prefix: 'ah-westend-' })
tmpDirs.push(tmpDir)
const runtime = useLiveRuntime
? await downloadLiveRuntime(
tmpDir,
Deno.env.get('WESTEND_RPC_URL') ??
'https://westend-asset-hub-rpc.polkadot.io',
Comment thread
marian-radu marked this conversation as resolved.
)
: `${sdkDir}/target/release/wbuild/asset-hub-westend-runtime/asset_hub_westend_runtime.compact.compressed.wasm`
const basePath = `${tmpDir}/ah-westend-base.json`
const rawPath = `${tmpDir}/ah-westend-raw.json`

// Step 1: Generate base chain spec (writes to file via --chain-spec-path)
console.log('📋 Generating asset-hub-westend chain spec ...')
Expand All @@ -234,6 +292,15 @@ async function buildAssetHubWestendSpec(
baseSpec.genesis.runtimeGenesis ??= {}
baseSpec.genesis.runtimeGenesis.patch ??= {}
baseSpec.genesis.runtimeGenesis.patch.sudo = { key: ALICE_SS58 }
// Fund alith (0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac) so eth_accounts[0] has balance.
const ALITH_SS58 = '5HYRCKHYJN9z5xUtfFkyMj4JUhsAwWyvuU8vKB1FcnYTf9ZQ'
const balances = baseSpec.genesis.runtimeGenesis.patch.balances?.balances ??
[]
balances.push([ALITH_SS58, Number.MAX_SAFE_INTEGER])
baseSpec.genesis.runtimeGenesis.patch.balances = {
...baseSpec.genesis.runtimeGenesis.patch.balances,
balances,
}
await Deno.writeTextFile(basePath, JSON.stringify(baseSpec, null, 2))

// Step 2: Convert to raw format
Expand Down
56 changes: 33 additions & 23 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,29 +90,29 @@ function getEnvName(): string {

export const jsonRpcErrors: JsonRpcError[] = []

export type Env = Awaited<ReturnType<typeof getEnv>>
export async function getEnv() {
export function getRpcUrl() {
const port = Deno.env.get('RPC_PORT') ?? '8545'
const url = `http://localhost:${port}`
const name = getEnvName()
return `http://localhost:${port}`
}

const id = await (async (): Promise<number> => {
const resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_chainId',
id: 1,
}),
})
const { result } = (await resp.json()) as { result: Hex }
return hexToNumber(result)
})()
async function fetchChainId(url: string): Promise<number> {
const resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_chainId',
id: 1,
}),
})
const { result } = (await resp.json()) as { result: Hex }
return hexToNumber(result)
}

const chain = defineChain({
function createChain(id: number, name: string, url: string) {
return defineChain({
id,
name,
nativeCurrency: {
Expand All @@ -127,15 +127,27 @@ export async function getEnv() {
},
testnet: true,
})
}

const transport = http(url, {
function createTransport(url: string) {
return http(url, {
onFetchResponse: async (response) => {
const raw = await response.clone().json()
if (raw.error) {
jsonRpcErrors.push(raw.error as JsonRpcError)
}
},
})
}

export type Env = Awaited<ReturnType<typeof getEnv>>
export async function getEnv() {
const url = getRpcUrl()
const name = getEnvName()
const id = await fetchChainId(url)
const chain = createChain(id, name, url)
const transport = createTransport(url)
const publicClient = createPublicClient({ chain, transport })

const waitForTransactionReceiptExtension = (client: {
getTransactionReceipt: (args: {
Expand Down Expand Up @@ -252,8 +264,6 @@ export async function getEnv() {
}
}

const publicClient = createPublicClient({ chain, transport })

const debugClient = createClient({
chain,
transport,
Expand Down
12 changes: 12 additions & 0 deletions src/westend-tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Westend live runtime smoke tests — validates eth-rpc requests
// against the live Westend asset-hub runtime.
// Run with: deno task test:westend

import { cleanupTests, setupTests } from './test-setup.ts'

await setupTests()
await import('./westend.test.ts')

globalThis.addEventListener('unload', () => {
cleanupTests()
})
Loading
Loading