feat(lightning): deposits, withdrawals, and solvency behind a node interface

The node is an interface, so the code where money is actually at risk is
tested against a fake that can be made to fail, stall, or lie. Plugging
in Alby Hub is configuration, not new code.

Crediting a deposit is idempotent by payment hash: a node reporting the
same settlement twice must not mint money. Withdrawals debit before they
pay, because a payment that succeeds while the ledger write fails loses
money permanently, whereas the reverse is recoverable. Withdrawals above
a threshold wait for a human, which bounds what a stolen session token
can remove.

17 tests including concurrent settlement, concurrent double-spend,
concurrent processors, failed payment refunds, fee caps, and solvency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
drjones
2026-08-05 23:23:51 +00:00
parent 3bdb518f9c
commit f097721304
4 changed files with 1056 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
-- Lightning deposits and withdrawals.
--
-- Invoices key on payment_hash, which is what makes crediting idempotent: a
-- node that reports the same settlement twice cannot produce two credits.
CREATE TABLE lightning_invoices (
payment_hash TEXT PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id),
amount_msat BIGINT NOT NULL CHECK (amount_msat > 0),
bolt11 TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ,
-- Set exactly once, when the payment is credited to the ledger.
credited_at TIMESTAMPTZ
);
CREATE INDEX lightning_invoices_account_idx ON lightning_invoices (account_id, created_at DESC);
CREATE INDEX lightning_invoices_pending_idx ON lightning_invoices (created_at)
WHERE credited_at IS NULL;
CREATE TYPE withdrawal_status AS ENUM
('queued', 'needs_approval', 'sending', 'paid', 'failed', 'rejected');
CREATE TABLE lightning_withdrawals (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id),
bolt11 TEXT NOT NULL,
amount_msat BIGINT NOT NULL CHECK (amount_msat > 0),
status withdrawal_status NOT NULL,
payment_hash TEXT,
fee_msat BIGINT,
failure TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
resolved_at TIMESTAMPTZ
);
CREATE INDEX lightning_withdrawals_account_idx ON lightning_withdrawals (account_id, id DESC);
CREATE INDEX lightning_withdrawals_pending_idx ON lightning_withdrawals (id)
WHERE status IN ('queued', 'needs_approval', 'sending');