pine-rpc
v0.3.0
Published
Pine Is Not an Emulator — a standalone Ethereum JSON-RPC node backed by a smoldot light client, for pallet-revive on Polkadot Asset Hub. Run it as a daemon and point ethers / hardhat / foundry / MetaMask at it.
Downloads
147
Maintainers
Readme
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 ──► P2PPine 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.mdfor 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-hubRequires Node.js ≥ 18. Or skip Node entirely and use Docker:
docker run -p 8545:8545 ghcr.io/baronvonbonbon/pine-rpc --chain polkadot-asset-hubRun 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.0First 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 stopGET /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:8545import { 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.jsonJSON-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_getCodeusually returns an error, not bytecode — current light-client peers do not serve theReviveApi_get_codecall proof. The error is bounded and descriptive; contract calls viaeth_callwork fine.- Explicit-number block lookups can return
nulleven inside the tracked window — prefer"latest"/"finalized". eth_getStorageAtrequires a full 32-byte slot (ethers pads automatically);eth_estimateGasreturns0x0for a plain value transfer.eth_getTransactionByHash/eth_getTransactionReceiptare 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 aHEALTHCHECKagainst/healthand binds0.0.0.0inside 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 CLIKnown Paseo quirks handled
Paseo relay migration (2026-07-02): the old relay was retired and
@polkadot-api/known-chainsstill points at it (dead bootnodes → smoldot stalls in peer discovery forever). Pine bundles the official replacement specs from paseo-network/paseo-chain-specs for thepaseo-asset-hubpreset.eth_getTransactionReceiptreturnsnullon the centralized Paseo eth-rpc proxy for confirmed txs — Pine rebuilds receipts fromSystem.Events.ReviveApi_balancereturns EVM-scaled wei (not planck) — passed through directly.chainHead_v1_followmay 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.
