Node queries
The node speaks plain JSON-RPC 2.0 over HTTP POST. No handshake, no encryption, and the credential for the owner surface is created for you.
Two things decide every call you write:
- Which surface.
/v2/owneris node administration./v2/foreignis every chain read plus transaction submission. The split is listed in the node API. - The double envelope. A response has a JSON-RPC
result, and inside it anOkorErr. See the response envelope.
The owner credential is at ~/.epic/main/.api_secret (floo or user for other networks) and the
username is the literal string epic. Foreign calls take no credential. See
authentication.
Both clients honour NODE_URL and EPIC_NETWORK, so the local chain from
run a local network needs no edit. Run them from the examples directory:
NODE_URL=http://127.0.0.1:23413 EPIC_NETWORK=user python python/epic_node.py
A complete client
- CLI
- Python
Needs curl and jq.
#!/usr/bin/env bash
# Chain status and tip from a local Epic node, over JSON-RPC.
# Needs curl and jq. See https://devdocs.epiccash.com/examples/node-api
set -euo pipefail
NODE_URL="${NODE_URL:-http://127.0.0.1:3413}"
EPIC_NETWORK="${EPIC_NETWORK:-main}"
SECRET_FILE="${SECRET_FILE:-$HOME/.epic/$EPIC_NETWORK/.api_secret}"
command -v jq >/dev/null || { echo "this script needs jq" >&2; exit 1; }
# jq exits non-zero on the Err branch, so a pipeline fails loudly instead of
# printing an error object as if it were data.
epic_ok() {
jq -e 'if .error then error("jsonrpc: \(.error)")
elif .result.Err then error("epic: \(.result.Err)")
else (.result.Ok // .result) end'
}
# Owner surface: needs the secret when the node has one. Status, peers, chain admin.
owner_rpc() {
local -a cred=()
[ -r "$SECRET_FILE" ] && cred=(-u "epic:$(cat "$SECRET_FILE")")
curl -s "${cred[@]}" \
-H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":${2:-[]}}" \
"$NODE_URL/v2/owner" | epic_ok
}
# Foreign surface: no credential. Every chain read lives here.
foreign_rpc() {
curl -s \
-H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"$1\",\"params\":${2:-[]}}" \
"$NODE_URL/v2/foreign" | epic_ok
}
status=$(owner_rpc get_status)
echo "height: $(jq -r '.tip.height' <<<"$status")"
echo "sync state: $(jq -r '.sync_status' <<<"$status")"
echo "peers: $(jq -r '.connections' <<<"$status")"
echo "mempool: $(foreign_rpc get_pool_size)"
echo "stempool: $(foreign_rpc get_stempool_size)"
echo "version: $(foreign_rpc get_version | jq -r '.node_version')"
Needs requests.
"""Minimal Epic node JSON-RPC client.
Requires: requests. Run an `epic` node first.
See https://devdocs.epiccash.com/examples/node-api
Override the defaults for another network:
NODE_URL=http://127.0.0.1:23413 EPIC_NETWORK=user python epic_node.py
"""
from __future__ import annotations
import os
import time
from collections.abc import Iterator
from pathlib import Path
from typing import Any
import requests
NODE_URL = os.environ.get("NODE_URL", "http://127.0.0.1:3413")
NETWORK = os.environ.get("EPIC_NETWORK", "main")
# Only the owner surface needs this. Chain reads on /v2/foreign send no credential, so a
# missing file is not fatal.
_secret_file = Path.home() / ".epic" / NETWORK / ".api_secret"
AUTH = ("epic", _secret_file.read_text().strip()) if _secret_file.is_file() else None
def node_rpc(
method: str,
params: list[Any] | dict[str, Any] | None = None,
surface: str = "owner",
) -> Any:
"""Call a node JSON-RPC method.
`surface` is "owner" for status and peer management, "foreign" for chain reads
such as get_block and for push_transaction.
"""
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params if params is not None else [],
}
response = requests.post(f"{NODE_URL}/v2/{surface}", json=payload, auth=AUTH, timeout=30)
response.raise_for_status()
body = response.json()
if "error" in body:
raise RuntimeError(f"JSON-RPC error: {body['error']}")
result = body["result"]
if "Err" in result:
raise RuntimeError(f"Node returned an error: {result['Err']}")
return result["Ok"]
def get_block(height: int | None = None, block_hash: str | None = None) -> Any:
"""Fetch a block by height or by hash. Foreign surface, positional params."""
return node_rpc("get_block", [height, block_hash, None], surface="foreign")
def watch_blocks(poll_seconds: int = 10) -> Iterator[Any]:
"""Yield each new block as the chain advances.
The node API has no subscription mechanism. get_tip is on the foreign surface, so
polling it needs no credential.
"""
last = node_rpc("get_tip", [], surface="foreign")["height"]
while True:
time.sleep(poll_seconds)
height = node_rpc("get_tip", [], surface="foreign")["height"]
# Catch up rather than skipping, so a slow consumer misses nothing.
while last < height:
last += 1
yield get_block(height=last)
if __name__ == "__main__":
status = node_rpc("get_status")
print(f"height: {status['tip']['height']}")
print(f"sync state: {status['sync_status']}")
print(f"peers: {status['connections']}")
tip = node_rpc("get_tip", [], surface="foreign")
print(f"tip hash: {tip['last_block_pushed']}")
Chain status
get_status on the owner surface returns height, total difficulty, connected peers and sync state.
- CLI
- Python
curl -s -u "epic:$(cat ~/.epic/main/.api_secret)" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"get_status","params":[],"id":1}' \
http://127.0.0.1:3413/v2/owner | jq '.result.Ok'
On Windows PowerShell:
$secret = (Get-Content "$HOME\.epic\main\.api_secret" -Raw).Trim()
$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("epic:$secret"))
$body = '{"jsonrpc":"2.0","method":"get_status","params":[],"id":1}'
Invoke-RestMethod -Uri 'http://127.0.0.1:3413/v2/owner' -Method Post -Body $body `
-ContentType 'application/json' -Headers @{ Authorization = "Basic $auth" }
status = node_rpc("get_status")
print(status["tip"]["height"], status["sync_status"], status["connections"])
total_difficulty is an object, not a numberEpic runs several proof-of-work algorithms, so difficulty is tracked per algorithm. See proof of work.
{"total_difficulty": {"cuckaroo": 0, "cuckatoo": 1234, "randomx": 5678, "progpow": 9012}}
Fetch a block
get_block is on the foreign surface and takes positional params, [height, hash, commit].
Pass one and leave the others null.
- CLI
- Python
curl -s -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"get_block","params":[1000000,null,null],"id":1}' \
http://127.0.0.1:3413/v2/foreign | jq '.result.Ok.header | {height, timestamp, proof}'
block = get_block(height=1_000_000)
header = block["header"]
print(f"{header['height']} via {header['proof']} at {header['timestamp']}")
print(f"{len(block['kernels'])} kernels, {len(block['outputs'])} outputs")
Kernels appear in two shapes depending on the method. get_block returns a flattened form, while
get_kernel and get_last_n_kernels return the raw type with an externally tagged features
field.
Watch for new blocks
The node API has no subscription mechanism, so polling is the approach. At a
60-second target block time, every 10 to 15 seconds is ample. Poll
get_tip on the foreign surface, which needs no credential.
- CLI
- Python
#!/usr/bin/env bash
# Poll the tip and print each new block height. Needs curl and jq.
# The node also has a webhook mechanism if you would rather be pushed to;
# see [server.webhook_config] in epic-server.toml.
set -euo pipefail
NODE_URL="${NODE_URL:-http://127.0.0.1:3413}"
INTERVAL="${INTERVAL:-10}"
command -v jq >/dev/null || { echo "this script needs jq" >&2; exit 1; }
height() {
# tr -d '\r' because a Windows jq writes CRLF, which command substitution strips only the
# newline from. Without it the integer comparison below fails with "integer expression
# expected" and the loop never advances. Harmless on Linux and macOS.
curl -s -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"get_tip","params":[]}' \
"$NODE_URL/v2/foreign" | jq -e -r '.result.Ok.height' | tr -d '\r'
}
last=$(height)
echo "starting at height $last"
while true; do
sleep "$INTERVAL"
now=$(height)
while [ "$last" -lt "$now" ]; do
last=$((last + 1))
echo "new block: $last"
done
done
watch_blocks() in epic_node.py yields each new block as the tip advances:
for block in watch_blocks():
header = block["header"]
print(f"block {header['height']} via {header['proof']}")
For push rather than poll, see [server.webhook_config] in
epic-server.toml, which can POST on transaction, header and block events.
Mempool and peers
Mempool size and version are foreign-surface calls. Peer lists are owner-surface calls.
- CLI
- Python
rpc() { curl -s -H 'Content-Type: application/json' -d "$1" http://127.0.0.1:3413/v2/foreign; }
rpc '{"jsonrpc":"2.0","method":"get_pool_size","params":[],"id":1}' | jq '.result.Ok'
rpc '{"jsonrpc":"2.0","method":"get_stempool_size","params":[],"id":1}' | jq '.result.Ok'
curl -s -u "epic:$(cat ~/.epic/main/.api_secret)" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"get_connected_peers","params":[],"id":1}' \
http://127.0.0.1:3413/v2/owner | jq '.result.Ok | length'
print("mempool:", node_rpc("get_pool_size", [], surface="foreign"))
print("stempool:", node_rpc("get_stempool_size", [], surface="foreign"))
print("peers:", len(node_rpc("get_connected_peers", [], surface="owner")))
Every other method
Full JSON-RPC method list, by surface
/v2/owner, defined in api/src/owner_rpc.rs:29:
get_status validate_chain compact_chain
get_peers get_connected_peers ban_peer
unban_peer get_onion_addresses
/v2/foreign, defined in api/src/foreign_rpc.rs:37:
get_header get_block get_blocks
get_tip get_version get_kernel
get_last_n_kernels get_outputs get_unspent_outputs
get_pmmr_indices get_pool_size get_stempool_size
get_unconfirmed_transactions push_transaction
get_block_template finalize_block_template submit_block
Response types are defined in api/src/types.rs.