pocket
Build and contribute

How this code is written

The conventions a contributor needs, and the one rule about comments that explains why this codebase reads the way it does.

Comments carry evidence

The rule: a comment explains why, not what, and it carries the evidence. The measurement, the upstream file and line, or the observed failure.

// Measured ledger close time.
//
// Not nominal 5s: measured over 199 consecutive ledgers from Horizon on
// 2026-07-31 as 5.0101s on testnet and 5.5678s on mainnet. The two differ by
// 11%, which is why this is a per-network table and not a single constant.
export const SECONDS_PER_LEDGER = { testnet: 5.01, mainnet: 5.57 } as const;

That is why files here carry long headers. A constant with a number in it is a decision, and a decision without its evidence is a decision the next person will undo.

Do not strip these when editing nearby code. Most of them record something that already went wrong once.

Comments in src/core/ use sentence case. Comments in popup/ui/ use lowercase. Match the file you are in.

Prose conventions

FormattingPrettier: 100 columns, double quotes, semicolons, trailing commas
Em dashesnever, anywhere: code comments, commit messages, documentation, prose
Commitsone line, lowercase, plain, no conventional-commit prefixes, no emoji, no body

What the interface is allowed to say

Three rules run through every screen, and they are the ones most likely to be broken by an ordinary-looking change.

Never render a value the wallet was not told. A shimmer means not read yet, a number means read, and a sentence means something failed. A zero standing in for an unread balance is the defect this rule exists to prevent, and it has a name in the codebase: a zero would be a lie.

Never state a fact twice. A sentence said on two screens will drift, so shared sentences live in one module and are read from there. The same applies to formatting a dollar figure, deciding whether an asset is held, and choosing which private pocket a screen is talking about.

Never offer a control that cannot succeed. If a build has no archive configured, the rebuild control is absent rather than present-and-refusing. A user told "your balances cannot be rebuilt" and handed a button labelled "Rebuild from history" can only find out which is true by pressing it.

Where each rule is enforced

A check in the interface is a check that anything calling the worker can go around. So anything that decides whether money moves lives in the worker, and the interface may repeat it to disable a control early.

Adding to the message contract

A new operation touches four places, in this order:

core/messages.ts

Add the request to the union and its payload to the response map. Amounts are decimal strings.

core/dispatch.ts

Add a case, and validate every field with the shape helpers. The union erases at runtime and nothing downstream re-checks.

Decide whether it is user activity. Ask whether it postpones the idle lock, and answer no for anything the interface polls.

core/controller.ts

Anything that builds against a sequence number, signs, submits or writes openings goes through the exclusive queue.

Anything that spends checks the balance first, and again once simulation has produced the real fee.

The error name

If the operation can fail in a way the user can act on, give the error a name and add it to the allowlist. An unnamed error reaches the user as a generic connection message, which is wrong for anything a retry cannot fix.

A test holds every allowlisted name, so removing one goes red.

Adding a test tier

A new directory under tests/ needs adding to vitest.suites.config.ts, or nothing will ever run it and the gate will stay green.

A test not named in a config is a file, not a test.

Adding a dependency

Check the registry for the current version, and read the current documentation rather than writing from memory of an API.

Two dependencies are pinned as protocol constraints rather than preferences, and bumping either is a migration: the proving toolchain, and the OpenZeppelin contract crates. What cannot be changed.

Where internal files go

Notes, plans and scratch material go in resources/, which is not part of the distribution. Nothing there ships and nothing in the product reads it.

The one exception is resources/deployment-<network>.json, which is a machine-consumed record: the release gates, the infrastructure check and the browser test harness all read it, so it is the authority on what is deployed.

The lint rule that is about safety

no-console: error, outside tests

Amounts, openings and blinding factors must never reach a log. There is no telemetry in the product, and this keeps the local case closed too. What may leave the worker.

On this page