Запросы к узлу
Узел использует обычный JSON-RPC 2.0 поверх HTTP POST. Без рукопожатия, без шифрования; учётные данные для административной поверхности создаются автоматически.
Каждый вызов определяют два параметра:
- Какой интерфейс.
/v2/owner— администрирование узла./v2/foreign— все операции чтения цепочки плюс отправка транзакций. Разделение описано в Node API. - Двойной конверт. Ответ содержит JSON-RPC
result, а внутри него —OkилиErr. См. конверт ответа.
Учётные данные владельца находятся в ~/.epic/main/.api_secret (floo или user для других сетей), имя пользователя —
литеральная строка epic. Вызовы Foreign API не требуют учётных данных. См. аутентификация.
Оба клиента принимают NODE_URL и EPIC_NETWORK, поэтому локальная цепочка из запустить локальную сеть не
требует правок. Запускайте их из каталога examples:
NODE_URL=http://127.0.0.1:23413 EPIC_NETWORK=user python python/epic_node.py
Полный клиент
- CLI
- Python
Требуется curl и 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')"
Требуется 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']}")
Статус цепочки
get_status на административной поверхности возвращает высоту блока, суммарную сложность, подключённые пиры
и состояние синхронизации.
- 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'
В 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 использует несколько алгоритмов доказательства работы, поэтому сложность отслеживается отдельно для каждого алгоритма. См. доказательство работы.
{"total_difficulty": {"cuckaroo": 0, "cuckatoo": 1234, "randomx": 5678, "progpow": 9012}}
Получить блок
get_block находится на foreign-поверхности и принимает позиционные параметры, [height, hash, commit]. Передайте один и
оставьте остальные 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) представлены в двух формах в зависимости от метода. get_block возвращает плоскую форму,
тогда как get_kernel и get_last_n_kernels возвращают исходный тип с полем features с внешней тегировкой.
Отслеживать новые блоки
Node API не имеет механизма подписки, поэтому используется опрос. При целевом времени блока 60
секунд достаточно опрашивать каждые 10–15 секунд. Опрашивайте get_tip на foreign-поверхности — учётные
данные не требуются.
- 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() в epic_node.py выдаёт каждый новый блок по мере продвижения вершины цепочки:
for block in watch_blocks():
header = block["header"]
print(f"block {header['height']} via {header['proof']}")
Для получения событий по модели push вместо опроса см. [server.webhook_config] в epic-server.toml — этот параметр
позволяет отправлять POST-запросы при событиях транзакции, заголовка и блока.
Мемпул и пиры
Размер мемпула и версия — вызовы foreign-поверхности. Списки пиров — вызовы административной поверхности.
- 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")))
Все остальные методы
Полный список методов JSON-RPC по интерфейсам
/v2/owner, определено в 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, определено в 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
Типы ответов определены в api/src/types.rs.