Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/zerodev-integration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: ZeroDev Integration Test

on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:

jobs:
integration-test:
timeout-minutes: 15
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Install Node.js
uses: actions/setup-node@v3
with:
node-version: 22

- name: Set up Foundry
uses: foundry-rs/foundry-toolchain@v1

- name: Install pnpm
run: npm install -g [email protected]

- name: Install dependencies
run: pnpm install

- name: Start Anvil
working-directory: test/integration
run: |
docker compose up -d anvil
sleep 5
echo "✅ Anvil started"

- name: Deploy contracts to Anvil
working-directory: test/integration
run: ./setup-contracts.sh

- name: Start ultra-relay bundler
working-directory: test/integration
run: |
docker compose up -d ultra-relay
sleep 10
echo "✅ Ultra-relay started"

- name: Check ultra-relay health
run: |
curl -s -X POST http://localhost:4337 \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_supportedEntryPoints","params":[],"id":1}' | grep -q result || \
(echo "Ultra-relay health check failed" && docker compose -f test/integration/docker-compose.yml logs ultra-relay && exit 1)

- name: Run integration test
working-directory: test/integration
env:
BUNDLER_URL: http://localhost:4337
RPC_URL: http://localhost:8545
run: pnpm run test
timeout-minutes: 5

- name: Show logs on failure
if: failure()
working-directory: test/integration
run: |
echo "=== Docker Compose Services Status ==="
docker compose ps
echo "=== Ultra-Relay Bundler Logs ==="
docker compose logs ultra-relay
echo "=== Anvil Logs ==="
docker compose logs anvil

- name: Cleanup
if: always()
working-directory: test/integration
run: docker compose down -v
20 changes: 20 additions & 0 deletions integration-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"network-name": "local",
"log-environment": "production",
"enable-debug-endpoints": true,
"entrypoints": "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789,0x0000000071727De22E5E9d8BAf0edAc6f37da032",
"balance-override-enabled": "true",
"api-version": "v1,v2",
"min-balance": "0",
"rpc-url": "http://localhost:8545",
"utility-private-key": "0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97",
"executor-private-keys": "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6,0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356",
"max-block-range": 10000,
"safe-mode": false,
"log-level": "info",
"polling-interval": 100,
"mempool-max-parallel-ops": 10,
"mempool-max-queued-ops": 10,
"port": 4337,
"bundling-mode": "auto"
}
1 change: 1 addition & 0 deletions test/integration/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
35 changes: 35 additions & 0 deletions test/integration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Ultra-Relay Integration Tests

Tests ZeroDev SDK → ultra-relay-provider → ultra-relay → Anvil

## Architecture

```
ZeroDev SDK → Provider (3333) → ultra-relay (4337) → Anvil (8545)
```

**Design**: ultra-relay stays standard, provider handles ZeroDev transformations

## Run Test

```bash
cd test/integration
pnpm run docker:test
```

## Services

- `anvil:8545` - Local Ethereum chain
- `postgres:5432` - Provider database
- `redis:6379` - Provider cache
- `ultra-relay:4337` - Standard ERC-4337 bundler
- `ultra-relay-provider:3333` - ZeroDev middleware

## Manual

```bash
docker-compose up -d
./setup-contracts.sh
pnpm test
docker-compose down -v
```
32 changes: 32 additions & 0 deletions test/integration/copy-kernel-contracts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createPublicClient, http } from "viem"
import { mainnet, foundry } from "viem/chains"

const MAINNET_RPC = "https://eth.llamarpc.com"
const ANVIL_RPC = process.env.ANVIL_RPC || "http://localhost:8545"

const KERNEL_CONTRACTS = {
metaFactory: "0xd703aaE79538628d27099B8c4f621bE4CCd142d5",
factory: "0xaac5D4240AF87249B3f71BC8E4A2cae074A3E419",
kernel: "0xBAC849bB641841b44E965fB01A4Bf5F074f84b4D",
ecdsaValidator: "0x845ADb2C711129d4f3966735eD98a9F09fC4cE57"
}

async function main() {
const mainnetClient = createPublicClient({ transport: http(MAINNET_RPC), chain: mainnet })
const anvilClient = createPublicClient({ transport: http(ANVIL_RPC), chain: foundry })

for (const [name, address] of Object.entries(KERNEL_CONTRACTS)) {
const bytecode = await mainnetClient.getBytecode({ address: address as `0x${string}` })
if (!bytecode) throw new Error(`No bytecode for ${name}`)

await anvilClient.request({ method: 'anvil_setCode' as any, params: [address, bytecode] })

const deployed = await anvilClient.getBytecode({ address: address as `0x${string}` })
if (deployed !== bytecode) throw new Error(`Verification failed: ${name}`)
}
}

main().catch((error) => {
console.error("Failed to copy Kernel contracts:", error.message)
process.exit(1)
})
64 changes: 64 additions & 0 deletions test/integration/deploy-entrypoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { http, type Address, createPublicClient, createWalletClient, type PublicClient } from "viem"
import { mnemonicToAccount } from "viem/accounts"
import { foundry } from "viem/chains"
import {
ENTRY_POINT_SIMULATIONS_CREATECALL,
ENTRY_POINT_V06_CREATECALL,
ENTRY_POINT_V07_CREATECALL,
SIMPLE_ACCOUNT_FACTORY_V06_CREATECALL,
SIMPLE_ACCOUNT_FACTORY_V07_CREATECALL
} from "../e2e/deploy-contracts/constants.js"

const DETERMINISTIC_DEPLOYER = "0x4e59b44847b379578588920ca78fbf26c0b4956c"
const DEPLOYMENT_TIMEOUT = 30000

const deployWithTimeout = <T>(promise: Promise<T>, timeoutMs: number, errorMsg: string): Promise<T> =>
Promise.race([promise, new Promise<T>((_, reject) => setTimeout(() => reject(new Error(errorMsg)), timeoutMs))])

const verifyDeployed = async (addresses: Address[], client: PublicClient) => {
for (const address of addresses) {
const bytecode = await client.getBytecode({ address })
if (!bytecode || bytecode === "0x") throw new Error(`Contract ${address} not deployed`)
}
}

async function setupContracts({ anvilRpc }: { anvilRpc: string }) {
const walletClient = createWalletClient({
account: mnemonicToAccount("test test test test test test test test test test test junk"),
chain: foundry,
transport: http(anvilRpc)
})

const client = createPublicClient({ transport: http(anvilRpc) })

const deployments = [
{ name: "EntryPoint v0.7", data: ENTRY_POINT_V07_CREATECALL },
{ name: "SimpleAccountFactory v0.7", data: SIMPLE_ACCOUNT_FACTORY_V07_CREATECALL },
{ name: "EntryPointSimulations", data: ENTRY_POINT_SIMULATIONS_CREATECALL },
{ name: "EntryPoint v0.6", data: ENTRY_POINT_V06_CREATECALL },
{ name: "SimpleAccountFactory v0.6", data: SIMPLE_ACCOUNT_FACTORY_V06_CREATECALL }
]

let nonce = await client.getTransactionCount({ address: walletClient.account.address })

for (const deployment of deployments) {
const hash = await deployWithTimeout(
walletClient.sendTransaction({ to: DETERMINISTIC_DEPLOYER, data: deployment.data, gas: 15_000_000n, nonce: nonce++ }),
DEPLOYMENT_TIMEOUT,
`Timeout deploying ${deployment.name}`
)
await deployWithTimeout(client.waitForTransactionReceipt({ hash }), DEPLOYMENT_TIMEOUT, `Timeout waiting for ${deployment.name}`)
}

await verifyDeployed(
["0x0000000071727De22E5E9d8BAf0edAc6f37da032", "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"],
client
)
}

setupContracts({ anvilRpc: process.env.ANVIL_RPC || process.env.RPC_URL || "http://localhost:8545" })
.then(() => process.exit(0))
.catch((error) => {
console.error("Deploy failed:", error.message)
process.exit(1)
})
91 changes: 91 additions & 0 deletions test/integration/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: ultra_test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test -d ultra_test"]
interval: 5s
timeout: 5s
retries: 5

redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5

anvil:
image: ghcr.io/foundry-rs/foundry:latest
environment:
ANVIL_IP_ADDR: "0.0.0.0"
command: anvil --port 8545 --code-size-limit 100000000
ports:
- "8545:8545"

ultra-relay:
build:
context: ../..
dockerfile: Dockerfile
ports:
- "4337:4337"
depends_on:
- anvil
volumes:
- ./start-bundler.sh:/start-bundler.sh
entrypoint: ["/bin/sh", "/start-bundler.sh"]
restart: "no"
healthcheck:
test: ["CMD-SHELL", "curl -s -X POST -H 'Content-Type: application/json' --data '{\"jsonrpc\":\"2.0\",\"method\":\"eth_supportedEntryPoints\",\"params\":[],\"id\":1}' http://localhost:4337 | grep -q result"]
interval: 5s
timeout: 5s
retries: 10
start_period: 15s

ultra-relay-provider:
image: oven/bun:latest
working_dir: /app
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
ultra-relay:
condition: service_healthy
ports:
- "3333:3333"
volumes:
- ../../../zerodev-stack/ultra-relay-provider:/app
environment:
PORT: "3333"
NODE_ENV: test
DATABASE_URL: postgresql://test:test@postgres:5432/ultra_test
REDIS_URL: redis://redis:6379
ULTRA_RELAY_URL: http://ultra-relay:4337
STRIPE_API_KEY: sk_test_mock
LOGTAIL_SOURCE_TOKEN: mock_token
LOG_LEVEL: info
command: >
sh -c "
apt-get update && apt-get install -y postgresql-client curl &&
bun install &&
cat docker/init-db.sql | PGPASSWORD=test psql -h postgres -U test -d ultra_test &&
(cat docker/seed-data.sql | PGPASSWORD=test psql -h postgres -U test -d ultra_test || true) &&
echo \"INSERT INTO projects (id, user_id, chain_id) VALUES ('550e8400-e29b-41d4-a716-446655440000', '550e8400-e29b-41d4-a716-446655440000', 31337) ON CONFLICT DO NOTHING;\" | PGPASSWORD=test psql -h postgres -U test -d ultra_test &&
echo \"INSERT INTO policies (project_id, policy_name, description, strategy, policy_group, chain_id, enabled) VALUES ('550e8400-e29b-41d4-a716-446655440000', 'Allow All Test Transactions', 'Sponsor all transactions for integration testing', 'pay_for_user', 'project', 31337, true) ON CONFLICT DO NOTHING;\" | PGPASSWORD=test psql -h postgres -U test -d ultra_test &&
bun run src/index.ts
"
healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3333/health || exit 1"]
interval: 5s
timeout: 5s
retries: 10
start_period: 15s
23 changes: 23 additions & 0 deletions test/integration/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "ultra-relay-zerodev-integration",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"test": "tsx zerodev.test.ts",
"deploy:entrypoints": "tsx deploy-entrypoints.ts",
"copy:kernel": "tsx copy-kernel-contracts.ts",
"docker:test": "./run-provider-test.sh"
},
"dependencies": {
"@zerodev/ecdsa-validator": "^5.4.6",
"@zerodev/sdk": "^5.4.6",
"permissionless": "^0.2.1",
"tslib": "^2.8.1",
"viem": "^2.9.5"
},
"devDependencies": {
"tsx": "^4.19.2",
"typescript": "^5.3.3"
}
}
18 changes: 18 additions & 0 deletions test/integration/run-provider-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash
set -e

docker-compose down -v 2>/dev/null || true
docker-compose up -d anvil postgres redis
sleep 10

./setup-contracts.sh

docker-compose up -d ultra-relay
sleep 10

docker-compose up -d ultra-relay-provider
sleep 15

pnpm test

docker-compose down -v
7 changes: 7 additions & 0 deletions test/integration/setup-contracts.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash
set -e

until curl -s http://localhost:8545 > /dev/null 2>&1; do sleep 1; done

ANVIL_RPC=http://localhost:8545 pnpm run deploy:entrypoints
ANVIL_RPC=http://localhost:8545 pnpm run copy:kernel
Loading
Loading