Web Wallet: Decentralized in the Browser

Web Wallet: Decentralized in the Browser

This week, ZooBC leaves the lab.

Up until now, everything has lived on nodes, terminals, and test networks. Powerful, but inaccessible. A blockchain only becomes real when people can use it, and that means a wallet. Not a service. Not an account. Just software running where the user already is.

The browser.

So this week, I build a web wallet that does not need a server at all. No backend. No sessions. No accounts. Just a browser, a static file, and a connection to any ZooBC node.

A Simple Design Philosophy

I start from a blunt assumption.

Every line of code that runs on a server is a point of centralization.

If the wallet depends on my infrastructure, then ZooBC is not truly decentralized. If my server goes down, nobody should lose access to their funds. That constraint shapes every decision.

The wallet must:

  • Run entirely in the browser
  • Never send private keys anywhere
  • Work with any ZooBC node
  • Keep functioning even if all my servers disappear

The result is deliberately simple: a single HTML file with embedded JavaScript. It can be opened directly from disk using file://, or served from any static host. No build step required. No installation. No trust.

Layered by Responsibility

Internally, the wallet SDK is organized into four clear layers. Each one does one job and nothing more.

┌─────────────────────────────────────────────────────────┐
│                    ZooBCWallet                          │
│  High-level API: accounts, transactions, storage        │
├─────────────────────────────────────────────────────────┤
│                   ZooBCNetwork                          │
│  Node communication: queries, broadcasts, discovery     │
├─────────────────────────────────────────────────────────┤
│                 ZooBCTransaction                        │
│  Transaction building: serialization, signing           │
├─────────────────────────────────────────────────────────┤
│                   ZooBCCrypto                           │
│  Cryptographic primitives: keys, addresses, hashing     │
└─────────────────────────────────────────────────────────┘

This mirrors the node architecture intentionally. The wallet is not a toy client. It is a peer that understands the protocol well enough to act independently.

Crypto Without Servers

ZooBCCrypto handles all cryptographic operations locally.

Key generation for Ed25519 and secp256k1.
Address encoding for ZooBC, Ethereum, and Bitcoin.
Signing and hashing.

It uses established libraries like tweetnacl and noble-secp256k1. No server calls. No remote entropy. No shortcuts. Everything happens in the browser process, under the user’s control.

Private keys never leave memory.

Transactions That Match the Node

ZooBCTransaction is responsible for building transactions exactly as the C++ node expects them.

Field order matters.
Endianness matters.
What gets hashed matters.

The bytes-to-sign hash computed in the browser must match exactly what the node computes on its side. If they differ by even one byte, signatures fail and funds get stuck.

This layer exists to remove ambiguity. The wallet does not “approximate” transactions. It speaks the protocol fluently.

Talking to the Network

ZooBCNetwork handles communication with nodes via their REST APIs.

It never assumes a single node is truthful. Instead, it queries multiple nodes and compares responses. If three nodes agree on an account balance, that is probably reality. If one disagrees, it is ignored.

Transactions are broadcast to multiple nodes for reliability. If one node is down or slow, the others carry the load.

Nodes are discovered dynamically. Failed nodes are deprioritized automatically.

Trust is statistical, not absolute.

The Wallet Layer

ZooBCWallet is the user-facing layer.

This is where accounts are created, mnemonics imported, balances checked, and transactions sent. It manages encrypted storage in localStorage and exposes a simple API for applications to build on.

Creating accounts of different types looks like this:

await ZooBCWallet.createAccount('ZBC', 'Main Account');
await ZooBCWallet.createAccount('ETH', 'Ethereum');
await ZooBCWallet.createAccount('BTC', 'Bitcoin P2WPKH');

One wallet.
Multiple chains.
One seed.

Features That Matter

Multi-address support is not a gimmick. It is a usability requirement. One mnemonic can manage ZooBC, Ethereum, and Bitcoin addresses, each using the correct cryptography and encoding.

Encrypted storage is mandatory. The mnemonic is encrypted using AES-GCM, with a key derived from the user’s password via PBKDF2 with 100,000 iterations. Without the password, localStorage contains nothing but encrypted noise.

The wallet also supports native ZooBC features, including escrowed transactions:

await ZooBCWallet.sendZBC({
    to: 'ZBC_RECIPIENT...',
    amount: 100,
    escrow: {
        approver: 'ZBC_ARBITRATOR...',
        commission: 0.5,
        timeout: 86400,
        instruction: 'Release when goods arrive'
    }
});

This is not layered on top. It is part of the protocol, and the wallet understands it directly.

A Realistic Threat Model

The security model is intentionally honest.

The browser is trusted. If the device is compromised, nothing can help.
Node APIs are untrusted. They can lie.
The network is untrusted. Man-in-the-middle attacks are possible without TLS.

Mitigations follow naturally:

  • All signing happens locally
  • Nodes never see private keys
  • Responses are cross-checked across nodes
  • Transaction hashes are verified client-side
  • HTTPS is recommended for node connections

This is not perfect security. It is realistic security.

The Mixed Content Reality

One practical issue surfaces quickly.

Modern browsers block HTTP requests from HTTPS pages. If the wallet is served over HTTPS but nodes expose HTTP APIs, the browser refuses to connect.

This is not theoretical. It breaks things immediately.

The current options are:

  • Serve the wallet over HTTP during development
  • Put nodes behind an HTTPS proxy
  • Use browser extensions that bypass mixed content checks

The proper solution is a gateway layer, which I will describe in the next post. That solves the problem cleanly for production deployments.

Ownership, Finally

Sitting in my lab late at night, opening the wallet directly from disk and sending a transaction without touching any server I control, something finally clicks.

This feels right.

Not because it is clever, but because it disappears. The wallet does not ask for permission. It does not need an account. It does not care if I am online tomorrow.

It just works.

And that is what a decentralized wallet should do.