Protocol documentationSource snapshot: 0.1.0 alphaNetwork: testnet / devnet

Start here

Wallet operations

The complete operational and architectural manual for wallet creation, restoration, local encryption, locking, node connectivity, receiving, signing, sending, and history reconciliation.

Testnet-only wallet alphaReviewed August 31, 2026
Specification status

This page documents the implemented alpha. Executable source, interoperability vectors, and tests remain authoritative if prose and code ever diverge.

Product boundary

Luracoin Wallet is a local, non-custodial testnet client. It creates and restores one BIP39/BIP32 root key, encrypts the resulting wallet record on the device, reads an authenticated full node on the same machine, signs the canonical transaction locally, and asks that node to admit the signed bytes.

The wallet does not:

  • start, stop, update, or manage the Python node;
  • connect directly to P2P peers;
  • connect to a remote RPC or public explorer for balances;
  • hold multiple derived accounts;
  • provide hardware-wallet, multisig, watch-only, or secure-enclave isolation;
  • make testnet assets valuable or a confirmation final;
  • enable mainnet.

The read-only demo is a separate product mode. Its sample balances, contacts, and transactions never become network state and cannot be spent.

Two execution modes

Electron desktop

Electron is the preferred local development mode. The process boundary is deliberate:

React renderer
    │ narrow contextBridge methods

Electron preload (sandboxed bridge)
    │ validated IPC channels

Electron main process
    │ reads rpc.token + loopback HTTP

Python RPC v1

The browser window enables context isolation and sandboxing, disables Node integration, webviews, drag-and-drop navigation, arbitrary navigation, external windows, and runtime permissions. IPC requests are accepted only from the trusted top-level renderer URL.

The main process reads the token with no-follow semantics, verifies it is a small regular file owned by the current user, and rejects group/other permissions on POSIX. It accepts only HTTP URLs whose hostname is localhost, 127.0.0.1, or ::1, rejects redirects, uses an eight-second timeout, and caps responses at two million bytes.

Local browser gateway

When Electron preload is absent, the renderer installs a browser bridge only on an HTTP(S) loopback origin. Every network request is a relative, same-origin /wallet-api/* call. There is no remote URL setting.

The Python gateway retains rpc.token server-side and exposes exactly four private-RPC mappings:

Browser route Private RPC route
GET /wallet-api/status GET /v1/status
GET /wallet-api/accounts/{address} GET /v1/accounts/{address}
GET /wallet-api/accounts/{address}/transactions Same /v1 path; only limit and cursor pass through.
POST /wallet-api/transactions POST /v1/transactions

The first SPA response installs a random, per-process HttpOnly, SameSite=Strict cookie. Gateway requests require that cookie and Sec-Fetch-Site: same-origin; POST also requires an exact same-origin Origin. The gateway validates the local Host, enables no CORS, refuses redirects and non-JSON upstream responses, caps the web body at 16 KiB, the RPC response at 2 MB, each static file at 16 MiB, and the upstream hop at five seconds.

Wallet derivation

The current wallet has one root key and one address. Creation uses:

  1. 256 bits from the operating system CSPRNG.
  2. A 24-word English BIP39 mnemonic.
  3. BIP39 seed derivation with the passphrase LURA by default.
  4. BIP32 master-key derivation with HMAC-SHA512 key Bitcoin seed.
  5. The root secp256k1 private key and compressed public key—no child path is applied.
  6. HASH160(compressed_public_key).
  7. Version byte 0x30 plus that 20-byte hash.
  8. Base58Check encoding to a 34-character Luracoin address.

A custom BIP39 passphrase creates a completely different wallet. It is not the local encryption password and cannot be inferred later. Restoration must use the exact same mnemonic and passphrase.

See Addresses & keys for the byte-level derivation and Python/JavaScript compatibility contract.

Create a wallet

The implemented onboarding flow is intentionally staged:

  1. Set a local password. It must contain at least 12 characters, match its confirmation, satisfy the basic strength calculation, and accompany an explicit recovery acknowledgment.
  2. Generate the phrase. The 24 words remain concealed until the user reveals them and hide again after 60 seconds.
  3. Record it offline. The current screen does not offer a convenient copy action by design.
  4. Verify the backup. Enter words 3, 11, 17, and 22.
  5. Choose the network. Testnet is the only enabled choice; mainnet is displayed as unavailable.
  6. Encrypt and save. The full secret wallet enters the encrypted vault; the live React state retains only the address and public key after authentication.

Do not photograph, paste, or commit the mnemonic. The phrase plus passphrase is the durable recovery path; the device password only protects one local vault.

Restore a wallet

Restoration accepts 24 normalized English BIP39 words and an optional custom passphrase. It derives the wallet before saving and asks the user to compare the resulting address with a known address. This catches a wrong mnemonic or passphrase before the new local vault is committed.

Restoration does not import Python’s plaintext wallet.dat JSON directly. Recover through the mnemonic and passphrase. A CLI-generated wallet is compatible because Python and JavaScript share the same root derivation and address format.

Encrypted vault format

The vault lives under local-storage key luracoin.wallet.v1 in the Electron or browser profile. The record schema is:

{
  "version": 1,
  "kdf": "PBKDF2-SHA256",
  "iterations": 250000,
  "cipher": "AES-256-GCM",
  "salt": "<16 random bytes, base64>",
  "iv": "<12 random bytes, base64>",
  "ciphertext": "<authenticated ciphertext, base64>",
  "address": "<public address>",
  "publicKey": "<compressed public key>",
  "network": "testnet"
}

The ciphertext contains the wallet object, options, and creation timestamp. Web Crypto derives a non-exportable AES key from the password and random salt. AES-GCM authenticates the ciphertext and metadata implicit in the algorithm invocation; an incorrect password or modified ciphertext fails decryption.

Record validation accepts iteration counts from 100,000 through 5,000,000 to support controlled migration, but new records use 250,000. The current design does not use Keychain, Secret Service, DPAPI, a hardware enclave, or a dedicated signing process. Encryption-at-rest is meaningful hardening, not a claim that a compromised desktop session cannot extract keys.

Unlocking and locking

Successful unlock decrypts the record and immediately reduces live application state to the public address and public key. A send operation decrypts the vault again for that single signing flow.

The wallet locks when:

  • the user selects Lock wallet;
  • the inactivity timer expires—supported preferences are never, 1, 5, or 15 minutes;
  • Electron reports system suspend or session lock;
  • the local browser page becomes hidden or is left, according to the browser-mode security lifecycle;
  • a demo session is explicitly locked, which also exits demo mode.

Deep-linked payment requests received while locked are queued and revealed only after valid authentication.

Connect to the node

The safe testnet defaults are:

RPC URL:   http://127.0.0.1:18444
Token:     ~/.luracoin/testnet/rpc.token
Timeout:   8 seconds
Max body:  2,000,000 bytes

If the node uses --data-dir, export the exact same path before launching Electron:

LURACOIN_DATA_DIR=/path/to/node-data npm run electron:dev

LURACOIN_TESTNET_RPC_URL may change the loopback port, but the client rejects HTTPS, non-loopback hosts, paths inherited from the setting, queries, and fragments. Never place the bearer in a VITE_* variable; Vite publishes those variables into renderer assets.

The wallet refreshes status, account, and history every 15 seconds. Sending remains disabled unless the node is initialized, caught up with its best connected peer, and able to read the Redis mempool.

Receive funds

The Receive screen renders the one address, a QR code, and an optional payment request. Luracoin URI syntax is:

luracoin:<ADDRESS>?amount=1.5&label=Coffee

The parser accepts either a canonical address or the luracoin: scheme, validates Base58Check, parses up to eight LURA decimal places without floating point, and truncates labels to 120 characters. On desktop the operating system protocol handler forwards links to the existing single application instance. In browser mode the link is encoded in the URL fragment, which is not sent to the server.

Always verify a receiving address through a second channel when the environment may be compromised. A checksum detects accidental corruption, not clipboard malware or a malicious renderer.

Send flow

The alpha intentionally enforces a narrow sequence:

  1. Fetch confirmed balance and nonce, plus spendable_balance and next_nonce after contiguous outgoing mempool entries.
  2. Reject sending if the node is unavailable, syncing, or reports the mempool unavailable.
  3. Reject a second local outgoing send while one is still pending.
  4. Parse the requested amount as integer lurashis. The alpha uses fee = 0.
  5. Check value + fee <= spendable_balance.
  6. Ask for the local password and decrypt the vault.
  7. Verify that the decrypted wallet address equals the currently displayed wallet.
  8. Serialize the canonical 85 unsigned bytes using testnet chain ID 1 and next_nonce.
  9. Verify that the private key’s compressed public key derives the sender address.
  10. Sign SHA256(unsigned_bytes) with deterministic secp256k1 ECDSA and low-S normalization.
  11. Build the 128-byte unlocking field as raw uncompressed public-key coordinates x || y, followed by compact signature r || s.
  12. Submit the resulting 213 bytes as 426 lowercase hex characters.
  13. Validate the node receipt field by field against the locally signed transaction.
  14. Show pending success only if the node responds accepted: true with a matching transaction.

The note is UI-only, truncated to 120 characters, and is not committed to the transaction or persistently stored in this alpha.

accepted means local mempool admission. broadcast is true only if at least one peer write succeeded, and relay_count is that successful write count. Neither field means mined, confirmed, irreversible, or economically final.

Transaction history

The wallet asks for up to 100 records per page, follows at most five pages, and retains at most 500 recent movements. Confirmed history uses an opaque cursor from RocksDB. Pending outgoing entries appear only on the first RPC page.

The wallet reconciles its local send receipts against node history:

  • a matching confirmed transaction replaces the local pending record;
  • a transaction still observed by the node remains pending;
  • if Redis is unavailable, the wallet does not declare missing transactions failed;
  • if next_nonce has moved beyond a receipt nonce, the receipt remains because another accepted state transition may have consumed the slot;
  • otherwise a vanished pending item becomes failed with an explicit local explanation.

Incoming mempool transactions are not included in the account RPC. They appear after a block confirms them. Confirmations are calculated as observed depth: tip_height - block_height + 1. Because the node cannot reorganize, this count must not be described as finality.

Read-only demo

Demo mode can be selected from onboarding or enabled with ?demo=true during development. It uses hard-coded data, a public sample address, and disabled sending. The UI labels the mode and its values as sample data. Demo contacts and transaction states are product fixtures, not outputs from the node, explorer, or blockchain.

Never reuse demo data in protocol conformance tests or represent it as live testnet activity.

Delete the wallet

Deletion requires the exact confirmation text DELETE and successful password decryption. It removes the wallet vault and preferences from profile storage, clears in-memory account/history state, and returns to onboarding.

Deletion does not erase the blockchain, node data, browser profile free space, operating-system backups, or the mnemonic written elsewhere. If the recovery phrase still exists, the same wallet can be restored.

Troubleshooting

“No local node token was found”

Start the matching node first and make Electron resolve the same LURACOIN_HOME or exact LURACOIN_DATA_DIR.

“The local node is unavailable”

Confirm the RPC listener and port. A custom URL must remain loopback HTTP. Check whether another process owns the port.

“The local node is still catching up”

Wait until ready is true or investigate the node’s peers. The wallet correctly pauses signing while a connected peer advertises a higher height.

“The node mempool is unavailable”

Start Redis and confirm the node’s Redis host, port, and DB. Confirmed balances can still be displayed, but spending is unsafe without a reliable contiguous nonce view.

“The node rejected the transaction”

Refresh account state. Common causes are stale nonce, insufficient spendable balance, invalid recipient, wrong network, an occupied sender/nonce slot, or a conflicting transaction already confirmed.

Restored address does not match

Stop. Check spelling and order of all 24 words and the exact custom BIP39 passphrase. Do not save a mismatched vault and assume the balance will appear later.

Security checklist

  • Keep the mnemonic and any custom BIP39 passphrase offline.
  • Use the wallet only with disposable testnet data.
  • Keep rpc.token readable only by the current user.
  • Never configure a remote RPC, publish the browser gateway, or proxy it publicly.
  • Treat clipboard content and QR payloads as untrusted until validated and visually confirmed.
  • Lock before leaving the device; do not rely solely on inactivity timeout.
  • Verify the network badge, local node readiness, recipient, amount, and receipt.
  • Remember that encryption at rest does not protect a fully compromised running desktop.
  • Do not interpret confirmations as finality while fork choice and reorganizations remain unimplemented.

Source anchors

Primary implementation files used for this chapter: