OpenReceive v0.3.2
An overpaid swap deposit is a refund, not a support ticket
A payer who sent more than deposit_amount landed on attention — terminal,
no form, "This payment needs support review." The provider does not treat it
that way: lightning-swap opens refund_required on MORE and takes a full
choice=REFUND for it, exactly as it does for LESS and EXPIRED. The
client was the only thing calling it an incident, and the payer's money sat
behind a sentence with no next step.
MORE / OVER / OVERPAID now map to refund_required in both engines, so
the deposit comes back through the same two-step form as every other
emergency. The refund is the WHOLE deposit — the payout is a fixed-amount
bolt11, so there is nothing to exchange a surplus into and choice=EXCHANGE
is not a path this client takes. The order stays unpaid and the payer can pay
again afterwards.
refund_reason gains overpaid and overpaid_and_late beside the three that
were there, with payer-facing copy for both. LIMIT, which the provider pairs
with LESS/MORE when a deposit falls outside the pair's limits, names no
reason of its own — it says nothing the payer can act on beyond the amount.
attention still holds what genuinely needs a human: choice=EXCHANGE, a
provider status we do not recognize, and the states OpenReceive itself sets.
Attempts already sitting in attention heal on their next status read, which
is the next time the payer opens the checkout.
The attention screen says what to do
Both renderers printed the same sentence twice — once as the heading detail,
once as the warning banner underneath it — and then hid the ids behind a
"Payment details" caret. The banner now carries the next step, the amounts
sent and required sit under it, and the deposit transaction and provider order
render open: on a screen whose whole remaining job is quoting an id to a
human, a disclosure triangle is the wrong shape. The demo shops stop rendering
a payable QR under an attention status for the same reason.
A payer can get back to a deposit that is already in flight
A stablecoin deposit has no account behind it and sends no email. The order's
reference is the whole of the payer's claim on it, and POST /checkouts/prepare
answers with the amount and the pay-in catalog and NO attempts — so a checkout
rebuilt from a reference alone opened on the payment-method grid. That is the
wrong screen for someone who was told to bookmark a refund.
resumePaymentHash (attribute resume-payment-hash) is a create-mode prop on
the React, element, Vue, Svelte and Angular checkouts: pass the payment hash
your application stored beside the order and the deposit — or its refund form —
comes back as the payer left it. A hash the server will not serve is
ignored, because a stale note in host storage is not a reason to put an error
in front of a payer who can still start a fresh payment; they land on the method
grid exactly as they would have.
The hash and not the asset, deliberately: POST /swaps/status addresses one
attempt and applies no reuse test, so it still answers for a deposit that
stopped being payable hours ago, while re-selecting the asset re-serves the
committed attempt only until it expires and then mints a second address.
Four additive names on @openreceive/browser (and /headless) for hosts
driving their own UI: requestSwapStatus reads one attempt by hash and rejects
with a status-carrying error; resumeSwapAttempt is the forgiving wrapper the
renderers share, folding the attempt into a prepared snapshot and returning the
snapshot unchanged on any failure; currentCheckoutUrl is the one definition of
"its URL" that the two refund screens copy; formatMethodNetworkDetail joins
network labels for a method tile. SwapDisplayModel gains resumable, the
boolean refundReturnLabel was already chosen from, so a UI can render the copy
button that sentence names without string-matching the label.
The guides say less
Thirteen guides were rewritten against what the packages actually export, and
the set shrank by roughly 1,800 lines while gaining a page. swap-refunds.md
is new and is the one that was missing: when refund_required happens, which
refund_reason means what, and — the part every stack got wrong — that a refund
form is only a promise if the payer can reach it again after closing the tab.
headless-checkout.md, storage.md, security.md and checkout-ux.md lost
the most; three internal notes (architecture.md, checkout-design.md,
scope-lock.md) now hold the reasoning that was scattered through them.
Every demo stack runs with no wallet, and every order has a URL
The checkout lives at /checkout/:reference on all four Buy a Button stacks,
which serve the SPA there, put the reference in the address bar the moment the
order exists, and take a pasted one back in from the catalog. The two custom-UI
stacks reopen the attempt with resumePaymentHash; the two that mount the
packaged checkout let the payer re-pick their coin.
DEMO_WALLET=testkit now boots the Rails stack too —
examples/buttons/server/rails/lib/button_shop/testkit/ is a port of
packages/js/testkit down to the fixtures, so one Playwright suite drives all
four with the same assertions. It fakes the wallet, the swap provider and the
price feed, and nothing else: the engine, the hooks, the migrations and Postgres
are the production paths. The /__testkit route is declared unconditionally and
404s unconditionally without the variable, which its own test asserts in an
environment that does not set it, and which npm run check:demo-containers
enforces against every compose file.
All four stacks — node-express, static-html-small-api, nextjs-fullstack
and rails — were rebuilt against the 0.3.2 packages, and the Playwright suite
runs against node-express. Of the gems, only openreceive-server changed;
openreceive and openreceive-rails are byte-identical to 0.3.1 and ship to
keep the one-version-for-everything rule.
Two release gates that were lying are fixed. release:prepare now refreshes the
Rails demo's Gemfile.lock alongside package-lock.json — the v0.3.1 tag's CI
died at bundle install with exit 16 because the three VERSION constants moved
and a lockfile pinning them by path did not, and local test:ci never sees it
because nothing here sets frozen mode. And Release Dry Run, which had never
passed on either tag it ever ran for, now builds the packages before smoking
them: @openreceive/angular resolves @openreceive/elements/wrapper-shared to
a .d.ts that exists only once elements is built, and a fresh checkout with no
build step cannot. ci.yml hid it by running npm test — and so pretest:js —
first.
The live wallet smoke (npm run test:live) was NOT run for this release: it
needs a funded NWC connection.
This is a PATCH bump: nothing left the public API. resumePaymentHash and the
four new browser exports are additive, and the swap-status mapping change moves
attempts out of a terminal state into a recoverable one.