Wallet API¶
Wallet endpoints expose the user's address, balance, and transaction history, and allow signing and submitting transfer transactions or downloading an encrypted backup of the keypair.
All endpoints below require a session cookie.
GET /api/wallet¶
Returns the current user's wallet info.
Response¶
{
"name": "alice",
"address": "34378b1ba5be9d0999acd60be3a8a1f1",
"balance": 99.998,
"public_key": "0a1b...4f78…",
"public_key_full": "0a1b...4f78... (full 2592-byte hex)",
"signature_scheme": "ML-DSA-87 (NIST FIPS 204)",
"transactions": [
{
"tx_id": "f5c4357...",
"direction": "out",
"counterparty": "39848b500426d8f1...",
"amount": 0.001,
"fee": 0.001,
"timestamp": 1780002999.123,
"block": 42,
"memo": "lunch"
}
]
}
| Field | Type | Notes |
|---|---|---|
name | string | The user's display name |
address | string (40 hex) | The user's wallet address |
balance | float | WAVE, confirmed (does not subtract pending outflows) |
public_key | string | Truncated ML-DSA-87 public key (UI display) |
public_key_full | string | Full hex of the public key |
signature_scheme | string | Always "ML-DSA-87 (NIST FIPS 204)" in v1 |
transactions | list | Last 25 in/out transactions involving this address, newest first |
Errors¶
| Status | Body | When |
|---|---|---|
| 401 | {"error":"no session"} | No session cookie present |
| 401 | {"error":"session invalid"} | Cookie present but name no longer in approved set |
POST /api/wallet/send¶
Sign and submit a WAVE transfer from the user's wallet.
Request¶
| Field | Required | Notes |
|---|---|---|
to | yes | Recipient address (hex, with or without 0x) |
amount | yes | WAVE, must be > 0 |
memo | no | Up to 200 chars; stored in tx.data.memo and visible to anyone |
Response¶
The transaction enters the mempool. Confirmation lands in the next block (~5 sec testnet, ~60 sec mainnet target).
Errors¶
| Status | Body | When |
|---|---|---|
| 400 | {"error":"invalid amount"} | Non-numeric or ≤ 0 |
| 400 | {"error":"recipient and positive amount required"} | Missing fields |
| 400 | {"error":"insufficient balance (X.XXX WAVE)"} | Balance too low for amount + 0.001 fee |
| 400 | {"error":"tx rejected by mempool"} | See mempool rules under Blocks |
| 401 | Standard session errors |
The fee is fixed at 0.001 WAVE (matching the contract gas fee).
Future extensions¶
Custom-fee transfers will land with the next mempool revision.
POST /api/wallet/export¶
Download an encrypted backup of the user's wallet. The backup is an AES-256-GCM ciphertext; the encryption key is derived from the user's passphrase via Argon2id.
Request¶
Response¶
{
"_format": "waveledger-wallet-backup/v1",
"_warning": "Keep this file safe. Anyone with this file + your passphrase can spend from your wallet.",
"address": "34378b1ba5be9d0999acd60be3a8a1f1",
"encrypted_data": "<hex ciphertext>",
"nonce": "<hex>",
"salt": "<hex>",
"created_at": "2026-05-28T20:00:00",
"last_accessed": "2026-05-28T21:00:00",
"version": "1.0",
"encryption_algorithm": "AES-256-GCM",
"kdf_algorithm": "Argon2id"
}
The wallet UI saves this as waveledger-wallet-<address-prefix>.json.
Why AES-256-GCM on a post-quantum chain?¶
AES-256-GCM is post-quantum safe per NIST SP 800-208 (Cat 5). Grover's algorithm halves symmetric security, so AES-256 yields 128-bit quantum security, which remains infeasible to break. ML-DSA and ML-KEM are the wrong tools for password-based file encryption (they target asymmetric signatures and key exchange).
Future extensions¶
Encrypted wallet handoff between two on-chain ML-KEM keys.
Errors¶
| Status | Body | When |
|---|---|---|
| 400 | {"error":"passphrase must be at least 8 chars"} | Self-explanatory |
| 400 | {"error":"Weak password: ..."} | Argon2id rejected as too weak |
| 401 | Standard session errors |
POST /api/wallet/import¶
Restore a wallet from an encrypted backup. The user must have already signed up (or been approved) under the target name.
Request¶
Response¶
This replaces the wallet that the admin's approval created for that name, allowing users to bring an existing wallet to a new chat account.
Errors¶
| Status | Body | When |
|---|---|---|
| 400 | {"error":"name, encrypted, passphrase all required"} | Missing field |
| 400 | {"error":"decrypt failed: ..."} | Wrong passphrase or corrupted file |
| 403 | {"error":"name is blocked"} | Admin blocked this name |
| 404 | {"error":"name not approved — submit a signup first"} | Sign up first, then import |