plans: names credit only confirmed late money

This commit is contained in:
shum committed 2026-10-22 09:55:01 +00:00
1 parent 52548b8865
commit 0244e247d7
1 file changed
+5 -5
+5 -5
View File
@@ -51,9 +51,9 @@ The service serializes the requests that create or change an order:
The database backs this with unique constraints: one live registration per commitment, one live registration per owner and label hash, one registration per reserved code, one holder per balance. A registration is live while it can still lead to a registration of its name: `unpaid` with an open invoice, `queued`, `committed`, revealing or registering, and `failed` (retry is allowed). A `registered` order is not live, so the same wallet can order the name again after it lapses; a `taken` order is not live. `live` is a stored flag, and every transition that changes it does so in the same transaction.
In one transaction, rows are locked in this order: the invoice, then the registration, then a prepaid code or a balance. Rows a transaction inserts are new and wait on nobody, so `/spend` may insert its invoice and order after closing a holder (which releases that holder's balance row) and before writing the balance again.
In one transaction, rows are locked in this order: the invoice, then the registration, then a prepaid code or a balance; when one transaction releases one balance and credits another (a difference order's invoice closing with money in it), the released balance is locked first. Rows a transaction inserts are new and wait on nobody, so `/spend` may insert its invoice and order after closing a holder (which releases that holder's balance row) and before writing the balance again.
**An overdue order is closed by the next order.** When a new order from the same wallet and name meets a live order whose open invoice has passed its expiry time with no money received (the poller may not have marked it yet; a funded invoice is never closed this way, as the sweep never expires one), the new order closes it inside its own transaction, under the label hash's turn: the old invoice becomes `expired`, its registration loses `live`, and only then is the new order inserted. The provider's cancel of the old invoice is sent afterwards, best effort, as the poller would. Settlement queues a registration only while it is still live; a payment that arrives for a closed order is a late payment (it stays `unpaid`, and what arrived is credited to that order's balance, section 8; owner, 2026-10-22), never a second live order.
**An overdue order is closed by the next order.** When a new order from the same wallet and name meets a live order whose open invoice has passed its expiry time with no money received (the poller may not have marked it yet; a funded invoice is never closed this way, as the sweep never expires one), the new order closes it inside its own transaction, under the label hash's turn: the old invoice becomes `expired`, its registration loses `live`, and only then is the new order inserted. The provider's cancel of the old invoice is sent afterwards, best effort, as the poller would. Settlement queues a registration only while it is still live; a payment that arrives for a closed order is a late payment (it stays `unpaid`, and its confirmed money is credited to that order's balance, section 8; owner, 2026-10-22), never a second live order.
### Formats
@@ -377,7 +377,7 @@ A cancel that lost to settlement answers `200` with `status: paid`; the page re-
- `years`, `duration`: the term paid for; `duration` rebuilds the commitment on another browser.
- `price`: what the order is worth in cents: the price paid, any part from a balance included, or after `/name` the new name's price, which caps a further move. 0 for a code. A paid order that ends `taken` keeps it as its balance.
- `fromBalance`: the cents paid from another order's balance when the order was made (`/spend`, section 8); 0 when none. A move does not change it, so after a move to a cheaper name it can exceed `price`.
- `balance`: this order's balance (section 8), null when it has none: `{"amount": 10000, "held": false, "heldUntil": null, "refunded": 0}`. `amount`: the cents left to spend. `held`: an order's open invoice holds the balance; `heldUntil`: that invoice's `expiresAt`, else null. A holder whose invoice is past its expiry time with no money received reads as none (`held` false), as its invoice reads `expired`. `refunded`: the cents support refunded from it. The holder is not named: whoever holds this order's link gets no other order's id from it. The page knows the holder from its own records (the order it made by `/spend`), or finds it with `find`.
- `balance`: this order's balance (section 8), null when it has none: `{"amount": 10000, "held": false, "heldUntil": null, "refunded": 0}`. `amount`: the cents left to spend. `held`: an order's open invoice holds the balance; `heldUntil`: that invoice's `expiresAt`, else null. A holder whose invoice is past its expiry time with no money received reads as none (`held` false), as its invoice reads `expired`. `refunded`: the cents support refunded from it, over all its refunds. The holder is not named: whoever holds this order's link gets no other order's id from it. The page knows the holder from its own records (the order it made by `/spend`), or finds it with `find`.
- `commitTx`, `registerTx`: among the hashes sent since they were last cleared, the one mined with status 1, else the latest sent. Null when none, or when it was judged mined from chain state with no hash known.
- `revealAfter`, `revealBefore`: the commit's block time plus the controller's `minCommitmentAge` and `maxCommitmentAge`.
- `intentNonce`: `SimplexSponsor.nonces(owner)`, read only in `committed`, null otherwise.
@@ -491,10 +491,10 @@ Finds a name code's orders. Used by Check your code (the name code's verdict and
A paid order whose name someone registered first keeps what it is worth as a balance, which buys another name (owner, 2026-10-22). There is no code for a taken order.
- **What it is:** US cents, held per order and owned by that order's owner code. Balances are never merged, across orders or wallets: one order's balance pays for one new order at a time. A balance never expires.
- **Credited:** with the order's `price`, in the write that makes a paid order `taken` (paid by invoice, from a balance, or both), so a `taken` order with a payment always has a balance. A move to a cheaper name adds the difference (`/name`, above). A late payment (section 1), on any name invoice closed before its money arrived (a difference invoice included), adds what arrived to the closed order's own balance, in the settlement's transaction (owner, 2026-10-22): the cents the provider reports received, at the invoice's rate, each new figure adding only its increase over what that invoice already credited, so a repeated or growing report never credits twice. A payment the provider reports with no cents figure credits nothing and alerts the operator. A code order gets none: codes carry no money, and its code is released (section 4).
- **Credited:** with the order's `price`, in the write that makes a paid order `taken` (paid by invoice, from a balance, or both), so a `taken` order with a payment always has a balance. A move to a cheaper name adds the difference (`/name`, above). A late payment (section 1) adds its money to the closed order's own balance, in the settlement's (or the cancel's) own transaction (owner, 2026-10-22). It covers money that arrives on any name invoice after it closed (a difference invoice included), and money received while the invoice was open that it still holds when the provider closes it or a cancel meets it. Only confirmed money is credited: BTCPay's payments with status `Settled`, at the invoice's rate, and Stripe's succeeded amount. The confirmed figure is stored apart from the received one and only rises; a rise in it is a new payment even when the totals are unchanged, and each rise credits only its increase over what that invoice already credited, so a repeated or growing report never credits twice and an unconfirmed payment that is replaced or dropped never credits. Money seen but not yet confirmed (a mempool payment, Monero confirming with a zero figure) credits nothing yet and is logged as pending; the operator is alerted only for a closed invoice that still holds money with nothing credited once the poller's read window (`settleWindow`, about 72 hours) has passed. A code order gets none: codes carry no money, and its code is released (section 4).
- **Spent:** only by `/spend` (below), signed by the owner, for a new order of the same owner. When the balance covers the new name's price, the price is spent at once and the rest stays as balance. When it does not, all of it pays, and an invoice is made for the difference.
- **Held:** while that invoice is open, the new order holds the balance: no other spend and no refund takes it. Settlement queues the new order and spends what it held, in one transaction. Whatever closes the invoice unpaid (a cancel, its expiry, the poller's sweep, a next order closing it as overdue, section 1) releases the hold in the transaction that closes the order. A payment that arrives after that is a late payment (section 1): it is credited to the closed order's own balance, and the released balance is not touched. A hold by an order whose invoice is past its expiry time with no money received reads as free (`held` false), and the next `/spend` of that balance closes that order first, in its own transaction, as section 1 closes an overdue order (below). A refund does not: it waits for the poller's sweep to close the holder.
- **Refunded:** only by support, when the buyer writes with the order's reference (its invoice id, or its registration id for an order without an invoice). The operator command `//refund <reference> <cents>` (task plan, T26) sets the balance to 0 and records the cents; support then pays them back by hand, by the methods that paid them, which the command lists back through every balance the money came from. A held balance is never refunded, overdue holder or not: the command names the holder and its invoice's expiry, and support runs it again once the poller has closed the holder. There is no refund endpoint, no automatic refund, and no refund address given to the service.
- **Refunded:** only by support, when the buyer writes with the order's reference (its invoice id, or its registration id for an order without an invoice). The operator command `//refund <reference> <cents>` (task plan, T26) sets the balance to 0 and adds the cents to `refunded`, which counts every refund of the balance (one can be credited again later, by a late payment or a move); support then pays them back by hand, by the methods that paid them, which the command lists back through every balance the money came from. A held balance is never refunded, overdue holder or not: the command names the holder and its invoice's expiry, and support runs it again once the poller has closed the holder. There is no refund endpoint, no automatic refund, and no refund address given to the service.
- **Concurrency:** each hold, spending and refund is one conditional write on the balance row (its `amount` as read, and no holder, or this holder), in the balance's turn (section 1). A writer outside the turn (settlement, the poller, a second process running the operator command) can only make the write refuse, never spend a balance twice. Every write is in the transaction of the order change it belongs to, so a restart finds both or neither.
- **Closing a holder** of another name: `/spend` holds the new name's turn, not the holder's. The close is the compare-and-set on the holder's `version` and the conditional expiry of its unfunded invoice (section 1), so it is safe without that turn. A holder no longer live when the transaction runs (cancelled, swept, or closed by its own name's next order meanwhile) is skipped, and the hold write decides; a holder still live that cannot be closed (its money arrived meanwhile) answers `409 balance_changed` with the current balance.
- **Seen in** the view's `balance` (section 6). A balance change leaves the order's `version` unchanged: the page reads the view again, without `seenVersion`, to see it.