Skip to content

Repository files navigation

pine-rpc

Pine Is Not an Emulator — a standalone Ethereum JSON-RPC node backed by a smoldot light client, for pallet-revive on Polkadot Asset Hub.

Pine boots a verifying light client in-process and translates Ethereum JSON-RPC calls (eth_*) into Substrate runtime API calls (ReviveApi_*, chainHead_v1_*). Run it as a daemon and point ethers / hardhat / foundry / MetaMask at it — no full archive node and no trust in a centralized RPC proxy required.

dApp / ethers / hardhat / cast / MetaMask
        │  eth_* JSON-RPC  (HTTP + WebSocket)
        ▼
   pine-rpc  ──►  PineProvider (EIP-1193)  ──►  smoldot light client  ──►  P2P

Pine is a light client, not a full node. It verifies Merkle proofs fetched over P2P; it has no historical archive. That makes some methods session-scoped or latest-only. Read SPEC.md for the exact per-method support matrix and limits — it is the source of truth.


Install

npm install -g pine-rpc
# or run without installing:
npx pine-rpc --chain paseo-asset-hub

Requires Node.js ≥ 18. Or skip Node entirely and use Docker:

docker run -p 8545:8545 ghcr.io/baronvonbonbon/pine-rpc --chain polkadot-asset-hub

Run the node

# Paseo Asset Hub testnet on http://127.0.0.1:8545 (+ ws://127.0.0.1:8545)
pine

# Polkadot Asset Hub mainnet on a custom port
pine --chain polkadot-asset-hub --port 9944

# Expose on the LAN (no auth — only behind a trusted network)
pine --host 0.0.0.0

First sync takes ~10–60 s (WASM init + P2P peer discovery + first finalized block). When ready you'll see:

  HTTP  http://127.0.0.1:8545
  WS    ws://127.0.0.1:8545
ready — press Ctrl+C to stop

GET /health returns 200 {"status":"ok"} once the light client is following finalized blocks (503 while starting) — use it for Docker HEALTHCHECK, systemd readiness, or load-balancer probes.

Point any Ethereum tool at it:

cast block-number --rpc-url http://127.0.0.1:8545
import { JsonRpcProvider } from "ethers";
const provider = new JsonRpcProvider("http://127.0.0.1:8545");
console.log(await provider.getBlockNumber());

CLI options

Flag Default Description
-c, --chain <preset> paseo-asset-hub paseo-asset-hub, polkadot-asset-hub, kusama-asset-hub, westend-asset-hub, or custom
-p, --port <n> 8545 Listen port (HTTP + WS share it)
-H, --host <addr> 127.0.0.1 Bind address
--relay-chainspec <f> Relay chain spec: file path or https:// URL. Required for custom; with a preset it overrides the bundled spec
--para-chainspec <f> Parachain spec: file path or https:// URL. Required for custom; overrides presets
--para-id <n> 1000 Parachain id
--chain-id <n> preset value EVM chain id for eth_chainId/net_version. Required for custom (must equal the runtime's Revive::ChainId)
--log-window <n> 10000 eth_getLogs rolling window, in blocks
--max-blocks <n> 1024 Tracked finalized-block cache size
--timeout <ms> 30000 Connection timeout
--no-ws Disable WebSocket (HTTP only)
--no-cors Disable permissive CORS headers

Run pine --help for the full list.

Supported chains

Preset Chain ID Network
paseo-asset-hub 420420417 Paseo testnet Asset Hub
polkadot-asset-hub 420420416 Polkadot mainnet Asset Hub
kusama-asset-hub 420420418 Kusama Asset Hub
westend-asset-hub 420420419 Westend testnet Asset Hub
custom user-specified Requires --relay-chainspec + --para-chainspec + --chain-id

Chainspec flags accept a local file path or an https:// URL, and also work with presets to override the bundled spec — handy when a testnet migrates before the packaged spec catches up:

# Any pallet-revive chain, straight from hosted specs
pine --chain custom \
  --relay-chainspec https://example.com/my-relay.json \
  --para-chainspec  https://example.com/my-parachain.json \
  --chain-id 420420999

# Preset with a replacement relay spec
pine --chain paseo-asset-hub --relay-chainspec ./paseo-new.smol.json

JSON-RPC support

Supported (HTTP + WS): eth_blockNumber, eth_chainId, net_version, eth_gasPrice, eth_getBalance, eth_getStorageAt, eth_getTransactionCount, eth_call, eth_estimateGas, eth_sendRawTransaction, eth_getBlockByNumber / ByHash, eth_getBlockTransactionCountByNumber / ByHash, eth_getLogs, eth_getTransactionByHash, eth_getTransactionReceipt, web3_clientVersion, plus node-level eth_accounts, eth_syncing, net_listening, net_peerCount, web3_sha3, rpc_modules. Batch requests and standard JSON-RPC error envelopes are supported; ethers.js v6 works over both HTTP and WS.

Key limits (full detail and root causes in SPEC.md):

  • Light-client scope: no historical state — calls and queries execute against the latest finalized block; blocks, logs, and receipts are tracked in bounded in-memory windows from the moment Pine connects.
  • eth_getCode usually returns an error, not bytecode — current light-client peers do not serve the ReviveApi_get_code call proof. The error is bounded and descriptive; contract calls via eth_call work fine.
  • Explicit-number block lookups can return null even inside the tracked window — prefer "latest"/"finalized".
  • eth_getStorageAt requires a full 32-byte slot (ethers pads automatically); eth_estimateGas returns 0x0 for a plain value transfer.
  • eth_getTransactionByHash / eth_getTransactionReceipt are session-scoped: they cover transactions submitted through this Pine instance.

WebSocket subscriptions

The WS endpoint exposes eth_subscribe / eth_unsubscribe for newHeads and logs (address + topic filters); newPendingTransactions / syncing are rejected as unsupported. newHeads notifications arrive on each finalized block.

const ws = new WebSocket("ws://127.0.0.1:8545");
ws.send(JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_subscribe", params: ["newHeads"] }));

Use as a library

Pine is also importable. Embed the provider directly, or wrap it in the server:

import { PineProvider, JsonRpcServer } from "pine-rpc";

const provider = new PineProvider({ chain: "paseo-asset-hub" });
// or any pallet-revive chain:
//   new PineProvider({ chain: "custom", relayChainSpec, parachainChainSpec, chainId: 420420999 })
await provider.connect();

// (a) use the EIP-1193 provider in-process
const bal = await provider.request({ method: "eth_getBalance", params: ["0x…", "latest"] });

// (b) or expose it as a network node
const server = new JsonRpcServer(provider, { port: 8545 });
await server.listen();

Deploy as a service

  • Docker: docker run -p 8545:8545 ghcr.io/baronvonbonbon/pine-rpc — the image ships a HEALTHCHECK against /health and binds 0.0.0.0 inside the container. Pass any CLI flags after the image name.

  • systemd: a hardened unit file is provided at deploy/pine-rpc.service:

    sudo npm install -g pine-rpc
    sudo cp deploy/pine-rpc.service /etc/systemd/system/
    sudo systemctl enable --now pine-rpc

Pine has no built-in auth or rate limiting. Keep it on 127.0.0.1 (the default) or a trusted network; put a reverse proxy in front for anything public.

Develop

npm install
npm run build      # tsc → dist/
npm test           # vitest (unit + integration)
npm start          # run the built CLI

Known Paseo quirks handled

  • Paseo relay migration (2026-07-02): the old relay was retired and @polkadot-api/known-chains still points at it (dead bootnodes → smoldot stalls in peer discovery forever). Pine bundles the official replacement specs from paseo-network/paseo-chain-specs for the paseo-asset-hub preset.

  • eth_getTransactionReceipt returns null on the centralized Paseo eth-rpc proxy for confirmed txs — Pine rebuilds receipts from System.Events.

  • ReviveApi_balance returns EVM-scaled wei (not planck) — passed through directly.

  • chainHead_v1_follow may stall — the chain manager re-follows with backoff.

Full detail, including fundamental light-client limits, is in SPEC.md.

License

GPL-3.0-or-later. See LICENSE.

About

Standalone Ethereum JSON-RPC node backed by a smoldot light client, for pallet-revive on Polkadot Asset Hub. Run it and point ethers/hardhat/foundry/MetaMask at it.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages