Interfaces
Local RPC v1
The private authenticated wallet API: loopback binding, bearer-token lifecycle, exact routes, schemas, pagination, readiness gates, transaction receipts, and stable errors.
This page documents the implemented alpha. Executable source, interoperability vectors, and tests remain authoritative if prose and code ever diverge.
Purpose and trust boundary
RPC v1 is the full node’s private control surface for the local wallet and operator tooling. It exposes account state, confirmed history, blocks, synchronization status, and transaction submission.
It is intentionally not the public explorer API:
| Property | Private RPC | Explorer API |
|---|---|---|
| Base routes | /v1/* |
/api/v1/* |
| Writes | transaction submission | none |
| Authentication | bearer token | none in alpha |
| Default bind | loopback only | loopback by default, explicitly publishable |
| Primary client | local wallet/operator | public website/explorer |
Never point a public web frontend at RPC. Never copy rpc.token into browser code or a static build.
Bind and ports
The server accepts only loopback host values:
127.0.0.1
::1
localhost
Any other configured bind is rejected at startup. Network defaults are:
| Network | RPC port |
|---|---|
| mainnet | 8444 |
| testnet | 18444 |
| devnet | 28444 |
Mainnet cannot initialize in the current alpha; its port is reserved.
Bearer token lifecycle
On first start, the node creates:
<data-directory>/rpc.token
The token is generated with the operating system CSPRNG and is at least 32 characters. Creation uses exclusive file semantics and requests mode 0600.
When loading an existing token on POSIX, the implementation verifies:
- the path itself is a regular file, not a symbolic link;
- open does not follow a symlink where supported;
- file identity does not change between metadata check and open;
- the owner is the current user;
- group and other permission bits are absent;
- file size does not exceed 4,096 bytes;
- content is valid UTF-8 with no embedded whitespace;
- only an optional final LF or CRLF is tolerated;
- the resulting token has at least 32 characters.
Failure is fatal rather than silently replacing suspicious material.
All routes except /health require:
Authorization: Bearer <token>
Comparison is constant-time. A missing, malformed, or incorrect header returns 401:
{ "error": "unauthorized" }
Safe command-line access
For local troubleshooting, avoid placing the token directly in shell history. One option is to read it into a task-specific variable:
LURA_RPC_TOKEN="$(tr -d '\r\n' < "$LURACOIN_DATA_DIR/rpc.token")"
curl -s \
-H "Authorization: Bearer $LURA_RPC_TOKEN" \
http://127.0.0.1:18444/v1/status
unset LURA_RPC_TOKEN
Protect terminal logs and process diagnostics. The token grants transaction-submission capability for that node.
Endpoint matrix
| Method | Route | Auth | Purpose |
|---|---|---|---|
GET |
/health |
no | minimal process/API probe |
GET |
/v1/status |
yes | node, chain, synchronization, and peers |
GET |
/v1/accounts/{address} |
yes | confirmed and pending-aware account state |
GET |
/v1/accounts/{address}/transactions |
yes | first-page pending plus indexed confirmed history |
GET |
/v1/blocks/{height} |
yes | one canonical block with all transactions |
POST |
/v1/transactions |
yes | admit and relay one complete signed transaction |
The maximum HTTP request body is 1,000,000 bytes, while the submission route imposes the much smaller canonical transaction length after decoding.
Monetary JSON rule
Every amount crossing JSON is a decimal string:
{
"balance": "5000000000",
"spendable_balance": "4999999000",
"value": "1000",
"fee": "0"
}
This preserves uint64 values in JavaScript. Clients must parse amounts with BigInt, decimal-string libraries, or checked arbitrary-precision integers—never binary floating point.
Nonces, heights, ports, counts, timestamps, and API versions are JSON numbers within their bounded domains.
GET /health
Unauthenticated response:
{
"ok": true,
"network": "testnet",
"api_version": 1
}
This proves the HTTP process is serving and identifies its configured network. It does not prove initialization, synchronization, Redis availability, peer connectivity, or readiness to submit.
GET /v1/status
Representative shape:
{
"network": "testnet",
"chain_id": 1,
"magic": "fbc0b6db",
"data_dir": "/home/alice/.luracoin/testnet",
"api_version": 1,
"initialized": true,
"height": 314,
"best_peer_height": 320,
"peer_count": 1,
"peers": [
{
"host": "203.0.113.10",
"port": 9999,
"inbound": false,
"height": 320,
"last_seen": 1788172000.25
}
],
"syncing": true,
"ready": false
}
initialized means genesis/canonical storage is present. An uninitialized chain reports height -1. best_peer_height is based on connected peers’ untrusted handshake advertisements.
ready means initialized, not actively syncing, and at least as high as every connected advertisement. It is a local transaction-submission gate, not consensus finality.
GET /v1/accounts/{address}
For a valid canonical address:
{
"network": "testnet",
"address": "L...34 characters total...",
"balance": "12500000000",
"spendable_balance": "11900000000",
"nonce": 7,
"next_nonce": 9,
"pending_outgoing_count": 1,
"mempool_available": true
}
Definitions:
balance: confirmed canonical balance;nonce: last confirmed outgoing nonce;spendable_balance: balance after simulating the readable contiguous pending outgoing sequence;next_nonce: nonce immediately after that pending sequence;pending_outgoing_count: number of transactions in that sequence;mempool_available: whether Redis could be read consistently.
An address with no confirmed record returns zero balance and nonce rather than 404.
When Redis is unavailable or malformed, confirmed values remain available but:
{
"spendable_balance": "<confirmed balance>",
"next_nonce": "<confirmed nonce + 1 as JSON number>",
"pending_outgoing_count": 0,
"mempool_available": false
}
Clients must disable sending in that state.
GET account transaction history
Route:
/v1/accounts/{address}/transactions?limit=50&cursor=<opaque>
limit defaults to 50 and is clamped into 1–200. A non-integer value returns invalid_limit.
Response:
{
"network": "testnet",
"address": "L...",
"mempool_available": true,
"transactions": [
{
"id": "64 lowercase hex characters",
"chain": 1,
"nonce": 8,
"fee": "0",
"value": "600000000",
"from_address": "L...",
"to_address": "L...",
"status": "pending",
"block_height": null,
"timestamp": null
}
],
"next_cursor": "opaque-or-null"
}
Pending transactions are volatile and appear only when no cursor is supplied—the first page. They are newest-first within the contiguous local pending view. Confirmed entries add status: "confirmed", block height, and block timestamp.
If pending items fill the page, the server may return the sentinel cursor latest; pass it back unchanged to begin at the newest confirmed index entry. All other cursor values are opaque. Do not decode or synthesize them.
Stale/mismatched internal index references are logged and omitted rather than returned as incorrect history.
GET /v1/blocks/{height}
The height must be a non-negative base-10 integer. The response is the block’s JSON representation plus network, with txns containing every transaction.
Representative fields include:
{
"network": "testnet",
"version": 1,
"id": "0000...",
"prev_block_hash": "0000...",
"height": 42,
"miner": "L...",
"timestamp": 1788172000,
"bits": "1dffffff",
"nonce": 12345,
"txns": []
}
Transaction value and fee remain strings. A nonexistent height returns 404 block_not_found.
POST /v1/transactions
Request:
POST /v1/transactions
Content-Type: application/json
Authorization: Bearer <token>
{ "raw": "<426 hexadecimal characters>" }
raw must decode to exactly 213 bytes. The node does not accept an unsigned body, field-by-field JSON, base64, or a transaction ID.
Processing order:
- parse JSON and hexadecimal;
- enforce exact decoded length;
- require node readiness when attached to a running P2P node;
- deserialize and perform mempool admission;
- attempt broadcast to connected peers;
- return the local acceptance receipt.
Successful status is 202 Accepted:
{
"network": "testnet",
"accepted": true,
"broadcast": true,
"relay_count": 2,
"transaction": {
"id": "...",
"chain": 1,
"nonce": 8,
"fee": "0",
"value": "600000000",
"from_address": "L...",
"to_address": "L...",
"status": "pending",
"block_height": null,
"timestamp": null
}
}
accepted: true means local Redis admission succeeded. It does not mean the transaction is mined or final. broadcast is exactly relay_count > 0; a zero count remains a valid local acceptance and the node retains the transaction for bounded replay.
If initial relay throws after admission, the API still reports acceptance with zero successful relays. A wallet must not re-sign automatically in response.
Error contract
| Status | Error | Meaning |
|---|---|---|
| 400 | invalid_address |
address syntax/checksum/version invalid |
| 400 | invalid_limit |
history limit is not an integer |
| 400 | invalid_cursor |
opaque history cursor is malformed |
| 400 | invalid_height |
block height is not a non-negative integer |
| 400 | invalid_transaction_encoding |
JSON/raw field/hex decoding failed |
| 400 | invalid_transaction_length |
decoded body is not 213 bytes; includes expected/actual |
| 400 | invalid_transaction |
deserialization or validation raised |
| 401 | unauthorized |
bearer missing or incorrect |
| 404 | block_not_found |
canonical height absent |
| 422 | transaction_rejected |
well-decoded transaction was not admitted |
| 503 | node_not_ready |
node is initializing or behind a connected advertisement |
| 503 | mempool_unavailable |
Redis failed during submission |
Clients should branch on error, not human prose. Transport failure remains distinct from a JSON rejection.
Wallet integration rules
The official Electron wallet keeps RPC access in its privileged main process. The renderer requests approved operations through a narrow IPC bridge and never receives the bearer token.
The browser-wallet gateway similarly adds the bearer server-side. It exposes only four allowlisted routes, enforces same-origin request metadata and a per-process HttpOnly cookie, and never serializes rpc.token into frontend assets.
A third-party client should follow the same principles:
- keep the token out of browser storage and renderer state;
- validate network and chain ID on every session;
- check
readyandmempool_availablebefore enabling send; - use confirmed nonce plus server-provided pending state;
- treat amounts as integer strings;
- present
202as pending, never confirmed; - paginate with opaque cursors;
- poll/reconcile by transaction ID and sender nonce.
Exposure prohibition
Loopback enforcement is a defense, not permission to forward the port publicly. A reverse proxy, SSH remote forward, container host mapping, or desktop webview misconfiguration can still move the trust boundary.
RPC has no public-user authorization model, account isolation, CSRF layer, rate limiting, or write quotas. Possession of its bearer token authorizes submission to that node. Keep it local.
Source anchors
Primary implementation files used for this chapter:
