Wallet applications

Wallet Docs

Complete guide for third-party BitKuruş wallet clients: Ed25519 keys, canonical JSON, UTXO planning, peer quorum, merge-then-pay, activity API, and backups.

00

Overview

A wallet client talks to any BitKuruş node over HTTPS. It generates keys locally, reads UTXOs, signs transactions, and submits them. The node validates signatures, enforces spend-once rules, asks peers for quorum approval, then commits and replicates.

You do not run a federation node unless you also validate or replicate. Correct signing alone is not enough — implement UTXO planning and post-merge peer sync.
01

1. Key and address model

Generate an Ed25519 keypair on the user device. The 64-character lowercase hex public key is the wallet address (sender on every transaction). Prove ownership by signing the transaction payload — the server never sees the private key.

  • Address: exactly 64 lowercase hex chars (32-byte Ed25519 public key).
  • Signature: exactly 128 lowercase hex chars (64-byte detached Ed25519 signature).
  • Ownership: sender must own every input token (owner on the active UTXO).
  • Optional: POST /api/wallet/register with public_key + label (metadata only).
// Client-side only
public_key  = bytesToHex(ed25519.publicKey)   // 64 chars
private_key = encryptWithUserPassword(...)    // never POST

signature = sign(canonical_json(payload_without_signature), private_key)
02

2. Balance and token reads

Each row in tokens[] is one spendable UTXO. balance is the decimal-string sum of all active outputs for the address.

GET /api/wallet/{public_key}

200 OK
{
  "result": "ok",
  "data": {
    "public_key": "aabbcc…(64 hex)",
    "balance": "10.000000000000000000",
    "is_commission_wallet": false,
    "validator_commission_rate_ppm": 200,
    "tokens": [
      {
        "token_id": "wallet-active-a",
        "value": "4.250000000000000000",
        "version": 1,
        "status": "active",
        "origin": "transfer_in",
        "origin_tx_id": "wallet-tx-…",
        "created_at": "2026-05-20T12:00:00.000000Z"
      }
    ]
  }
}

origin examples: transfer_in, merge, split, apple_tree, faucet, validator_commission. Only status: active tokens can be spent. Refresh after every committed transaction.

03

3. Transaction field reference

Field Required Description
tx_idYesClient-generated unique id (e.g. wallet-tx-abc123).
typeYestransfer, split, or merge.
senderYes64-hex public key; must own all inputs.
nonceYesUnique per sender (e.g. tx_id + "-nonce").
inputs[]Yes{ token_id, version } — version from wallet API.
outputs[]Yes{ token_id, value, owner } — new ids you invent.
signatureYes128-hex Ed25519 over canonical JSON without this field.

Type shape rules

Type Inputs Outputs When to use
transfer11Exact payment, one UTXO covers amount.
split12+One UTXO, pay receiver + change back to sender.
merge2+1Combine several UTXOs into one (often to sender).

Value conservation: sum(input values) must equal sum(output values). All value fields are decimal strings (max 18 fractional digits), never JSON numbers.

04

4. Canonical JSON and signing

Serialize the signing payload (all fields except signature) with these rules — byte-identical on client and server:

  1. Object keys sorted ascending (UTF-8 byte order).
  2. Arrays keep submission order (inputs, outputs).
  3. No \uXXXX escapes — raw UTF-8 in strings.
  4. Forward slashes not escaped (JSON_UNESCAPED_SLASHES).
  5. Money amounts as decimal strings, e.g. "1.000000000000000000".
// Payload to sign (no signature field)
{
  "inputs": [{ "token_id": "tok-a", "version": 1 }],
  "nonce": "wallet-tx-abc-nonce",
  "outputs": [{ "owner": "bb…", "token_id": "out-1", "value": "5.000000000000000000" }],
  "sender": "aa…(64 hex)",
  "tx_id": "wallet-tx-abc",
  "type": "transfer"
}

signature = Ed25519_sign(private_key, UTF8(canonical_json(payload_above)))

Full specification and test vectors: Run a node → Identity & canonical JSON. Reference: canonicalJson() in public/bitkurus/wallet.js and App\Services\Crypto\CanonicalJson on the server.

05

5. Submit signed transaction

POST the complete signed object. The node runs local validation, peer pre-validation, then queues commit.

POST /api/tx/submit
Content-Type: application/json

{ … full transaction including signature … }
HTTP result Meaning
202acceptedQueued — poll GET /api/tx/{tx_id}
409rejectedValidation failed — read data.error, data.reason
403Observer node (read-only)
06

6. Worked examples

Split — pay 3 BK$ from a 10 BK$ token

One input, two outputs: receiver gets payment, sender gets change. Sum = 10.

{
  "tx_id": "wallet-tx-pay1",
  "type": "split",
  "sender": "<your 64-hex>",
  "nonce": "wallet-tx-pay1-nonce",
  "inputs": [{ "token_id": "tok-10", "version": 2 }],
  "outputs": [
    { "token_id": "out-recv", "value": "3.000000000000000000", "owner": "<receiver>" },
    { "token_id": "out-change", "value": "7.000000000000000000", "owner": "<your 64-hex>" }
  ],
  "signature": "<128-hex>"
}

Merge — combine two UTXOs

Burn both inputs, mint one output to sender (often before a split payment).

{
  "tx_id": "wallet-merge-tx-1",
  "type": "merge",
  "sender": "<your 64-hex>",
  "nonce": "wallet-merge-tx-1-nonce",
  "inputs": [
    { "token_id": "tok-a", "version": 1 },
    { "token_id": "tok-b", "version": 1 }
  ],
  "outputs": [
    { "token_id": "wallet-merged-xyz", "value": "15.000000000000000000", "owner": "<your 64-hex>" }
  ],
  "signature": "<128-hex>"
}

Merge then split (two submits)

When multiple inputs and change are needed: (1) merge → wait committed + peer quorum → (2) split with merged token’s new version.

07

7. Accepted vs committed

202 accepted is not final success. Poll transaction status before spending the same inputs again or chaining a second tx.

GET /api/tx/{tx_id}

{
  "result": "ok",
  "data": {
    "tx_id": "wallet-tx-…",
    "status": "pending | committed | rejected",
    "type": "split",
    "reason": null
  }
}

UI flow: show pending → on committed refresh wallet → on rejected show reason and stop.

08

8. UTXO planning algorithm

Reference implementation planWalletTransfer() in public/bitkurus/wallet.js:

if (one input && exact amount)     → transfer
if (one input && change needed)    → split
if (N inputs && exact total)     → merge (outputs = [receiver] or [sender])
if (N inputs && change needed)   → merge_then_split (two transactions)

Input selection: greedy largest-first until sum ≥ amount (see chooseInputs()). Generate fresh token_id for every new output.

09

9. Peer validation and quorum

Before commit, the node broadcasts transaction_validation to peers. Each peer validates against its own DB. You need enough validated responses (config min_peer_validations, typically floor(N/2)+1).

409 Peer validation failed
{
  "result": "rejected",
  "data": {
    "error": "Peer validation failed.",
    "peer_id": "node2",
    "reason": "Input token does not exist."
  }
}

409 Insufficient peer approvals
{
  "data": {
    "error": "Insufficient peer approvals.",
    "required": 2,
    "validated": 0
  }
}
GET /api/cluster/token/{token_id}?expect=active

{
  "data": {
    "token_id": "wallet-merged-xyz",
    "confirmed_count": 3,
    "node_count": 3,
    "quorum": 2
  }
}

Wait until confirmed_count ≥ quorum after merge before payment submit. Retry 1.5–3s on peer errors (reference client: up to 4 retries).

10

10. Merge then pay (step by step)

  1. POST /api/tx/submit signed merge.
  2. Poll GET /api/tx/{merge_tx_id} until status = committed.
  3. GET /api/token/{merged_token_id} until status = active.
  4. GET /api/cluster/token/{merged_token_id}?expect=active until quorum.
  5. GET /api/wallet/{sender} — read merged token’s new version.
  6. POST /api/tx/submit signed split or transfer to receiver (+ change).

Skipping steps 3–4 is the main cause of large-transfer failures right after merge.

11

11. Activity feed (optional UI)

Human-readable history without parsing the full ledger.

GET /api/wallet/{public_key}/activity?limit=30

{
  "data": {
    "activities": [
      {
        "at": "2026-05-20T…",
        "kind": "send | receive | merge | split | tree_claim | commission",
        "amount": "3.000000000000000000",
        "tx_id": "…",
        "counterparty": "64-hex or null",
        "origin": "apple_tree"
      }
    ]
  }
}
12

12. Common errors

Peer validation failed
Peer DB missing input or stale version. Typical after merge — wait for cluster quorum, refresh wallet, retry. Show peer_id + reason.
Insufficient peer approvals
Too few validators online. Display required vs validated; retry later.
Input version mismatch
Token already spent elsewhere. Refresh GET /api/wallet and replan inputs.
Input token is locked
Pending tx holds lock. Wait for that tx to commit or reject.
Transfer / split / merge shape errors
Wrong input/output counts for type (e.g. transfer must be 1→1). See section 3 tables.
Invalid signature
Canonical JSON or decimal formatting mismatch. Compare with Run a node vectors.
Value conservation failed
Sum of inputs ≠ sum of outputs. Fix amounts before submit.
13

13. Backup and restore

Store encrypted private key + public key. Password never inside the file. After restore, fetch balance from network — chain state is not in the backup.

{
  "version": 1,
  "public_key": "64-hex",
  "ciphertext": "…"
}
14

14. Integration checklist

  • Ed25519 keygen; private key never leaves device
  • Canonical JSON + decimal string amounts tested against server
  • Correct type shape (transfer 1→1, split 1→2+, merge N→1)
  • UTXO planner including merge_then_split
  • Poll tx until committed before next spend
  • After merge: cluster quorum before payment
  • Retry peer validation with refreshed state
  • Show peer_id, reason, required/validated to user
  • Optional activity API for history UI

API endpoints

GET /api/wallet/{public_key}
Balance, tokens, commission flags
POST /api/wallet/register
Register address label (optional)
POST /api/tx/submit
Submit signed transaction
GET /api/tx/{tx_id}
Transaction status
GET /api/token/{token_id}
Single token status
GET /api/cluster/token/{token_id}
Federation presence / quorum
GET /api/wallet/{pubkey}/activity
Recent movements
GET /api/network
Peers, commission rate
GET /api/ledger/export
Deterministic ledger snapshot

Security notes

  • Private keys on device only
  • Canonical JSON signing
  • Strong password on backups
  • Refresh wallet after every commit
  • Post-merge peer quorum before payment
  • Expose peer errors; retry with backoff