From 873ae118d7532ca6bed15fa3fba0e6b827fa8d05 Mon Sep 17 00:00:00 2001 From: shum Date: Wed, 21 Oct 2026 20:51:20 +0000 Subject: [PATCH] plans: names API refusal flag and held codes --- plans/names-codes/2026-10-21-store-api.md | 15 ++++++++++----- .../names-codes/2026-10-21-store-service-tasks.md | 10 +++++----- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/plans/names-codes/2026-10-21-store-api.md b/plans/names-codes/2026-10-21-store-api.md index 9ddcaefb05..c05d06946b 100644 --- a/plans/names-codes/2026-10-21-store-api.md +++ b/plans/names-codes/2026-10-21-store-api.md @@ -264,6 +264,8 @@ Says what a prepaid code covers, without spending it. - `409 code_used` (spent, including by another order after the sweep released it), `409 code_reserved` (another registration holds it), `409 code_expired`, `409 payment_pending` (unpaid); - `409 code_not_covering`: the name is shorter than the code's `minLength`. +A code still reserved by its order stays good for that order: a later revocation or expiry does not stop that order's Try again or `/name`. `code_invalid` and `code_expired` apply only to a code that has to be reserved again after its release. + A code order keeps its code reserved while `failed`, for Try again. A daily sweep releases it once the registration has been `failed` for 30 days; a later Try again or `/name` reserves it again if it is still unused, else answers the tag above. A `taken` code order releases its code at once. ## 5. Orders @@ -360,7 +362,7 @@ A cancel that lost to settlement answers `200` with `status: paid`; the page re- ### The registration view ```json -{"id": "t2Jk8RmQ4vXw9NcP3yLb6g", "version": 7, "moves": 0, "phase": "committed", "labelHash": "0x…", "label": null, "owner": "0x…", +{"id": "t2Jk8RmQ4vXw9NcP3yLb6g", "version": 7, "moves": 0, "linksRefused": false, "phase": "committed", "labelHash": "0x…", "label": null, "owner": "0x…", "commitment": "0x…", "sealed": "…", "resolver": "0x…", "sponsor": "0x…", "chainId": 1, "tld": "simplex", "years": 2, "duration": 69292800, "price": 10000, "codeLength": null, "commitTx": "0x…", "registerTx": null, "revealAfter": 1792828800, "revealBefore": 1792915140, "intentNonce": 0, "unread": [], "expiresAt": null, "error": null} @@ -377,6 +379,7 @@ A cancel that lost to settlement answers `200` with `status: paid`; the page re- - `intentNonce`: `SimplexSponsor.nonces(owner)`, read only in `committed`, null otherwise. - `unread`: the fields left null because their chain read failed (`intentNonce`, `codeLength`). The view itself does not fail. The page reads the view again without `seenVersion` (a failed read changes no version), after 5 seconds, then 15, then every 30 seconds while `unread` is not empty. - `moves`: how many times `/name` has moved this order; at most `max_order_moves` (3). +- `linksRefused`: true once a reveal was refused for its links (section 7), so every browser, not only the one that revealed, offers `/name` with corrected links; false again after a successful `/name`. - `expiresAt`: the name's `nameExpires`, once `registered`. - `error`: null, or one sentence for the buyer. It never holds the RPC URL or a raw revert. @@ -475,8 +478,8 @@ Finds a name code's orders. Used by Check your code (the name code's verdict and - At most `max_order_moves` (3) successful moves per order over its life, counted on success; past that, `409 {"error": "move_limit", "registration": {…the current view}}`. The order stays as it is (`failed`, `taken`, or `committed` with its links refused); the page shows its state with a line to contact SimpleX support and the order's reference, and support settles it by hand, refunding a paid order. - The order checks run on the new fields: formats, `invalid_name`, existing live orders for the new commitment or this owner and the new hash, other than this registration (`409 order_exists`), the duration for the registration's `years`, `name_taken`, `name_check_unavailable`. - A paid order: the new name's chain price for the registration's `years` must be at most its `price`, else `409 {"error": "price_too_high", "price": }`. A cheaper name refunds nothing, which the page says before the buyer chooses. -- A code order: the code is checked again and reserved again under the per-code serialization, as `/retry` does: still reserved by this order, or released and still unused, it is (re)reserved; spent elsewhere after the sweep released it is `409 code_used`; held by another order `409 code_reserved`; revoked `400 code_invalid`; expired `409 code_expired`. The new name must be covered by it (`409 code_not_covering`). -- `200` the registration view: the new hash, length, duration, commitment and sealed, `price` the new name's (a paid order), `moves` one more, phase `queued`, with the label, intent, transactions, reveal times and the recorded link refusal cleared. An old commitment already on chain is left to lapse. +- A code order: the code is checked again and reserved again under the per-code serialization, as `/retry` does: still reserved by this order, it is kept, even if revoked or expired since (as `/retry` does); released and still unused, it is reserved again; spent elsewhere after the sweep released it is `409 code_used`; held by another order `409 code_reserved`; released and then revoked `400 code_invalid`, or expired `409 code_expired`. The new name must be covered by it (`409 code_not_covering`). +- `200` the registration view: the new hash, length, duration, commitment and sealed, `price` the new name's (a paid order), `moves` one more, phase `queued`, with the label, intent, transactions, reveal times and the recorded link refusal cleared (`linksRefused` false). An old commitment already on chain is left to lapse. ### `POST /api/registration/:id/code {"codeHash": "…", "codeLength": 8, "timestamp": 1792828800, "signature": "0x…"}` (a code for a taken order) @@ -575,6 +578,8 @@ Each line: the difference, the choice here, and what changes in the mock (`webap 31. **`unread` pacing.** Page: re-read without `seenVersion` after 5, 15, then every 30 seconds. 32. **Overdue orders.** Chosen: a new order from the same wallet and name closes an old order whose invoice is past its expiry time, in its own transaction; settlement queues only a live order; a payment for a closed order is a late payment. Mock: expire the old invoice in `createInvoice` and `registerWithCode` before the existing-order check, and settle only a live order. 33. **`/code` repeats** answer the issued code whatever `codeLength` now. Mock: check the repeat first in `takenCode`. +34. **`linksRefused`** in the registration view, set when the reveal records a links refusal and cleared by a successful `/name`. Mock: in the view. Page: offer `/name` with corrected links from the view, not only from its own record. +35. **A held code on `/name`.** A code still reserved by its order is accepted on `/name` even if revoked or expired since, as on `/retry`. Mock: in `retarget`. ## Appendix B. Error tags @@ -583,14 +588,14 @@ Each line: the difference, the choice here, and what changes in the mock (`webap | `bad_request` | 400 | | any | | `invalid_name` | 400 | | search, orders, `/name` | | `invalid_link` | 400 | | preview, reveal, records | -| `code_invalid` | 400 | | code check, register, `/name`, retry | +| `code_invalid` | 400 | | code check, register, `/name` (only for a released code), retry | | `not_found` | 404 | | unknown ids, `find` with no orders | | `method_not_allowed` | 405 | `Allow` header | any | | `order_exists` | 409 | | orders, register, `/name` | | `name_taken` | 409 | | orders, register, `/name` | | `catalog_changed` | 409 | `price` | `/api/invoice` | | `price_too_high` | 409 | `price` | `/name` | -| `code_used`, `code_reserved`, `code_expired`, `payment_pending`, `code_not_covering` | 409 | | register, `/name` (a code order), retry | +| `code_used`, `code_reserved`, `code_expired`, `payment_pending`, `code_not_covering` | 409 | | register, `/name` (a code order; `code_expired` only for a released code), retry | | `move_limit` | 409 | `registration` (the current view) | `/name` | | `registration_changed` | 409 | `registration` (the current view) | reveal, `/name`, `/code`, `/retry`, cancel | | `code_length_changed` | 409 | `codeLength` | `/code` | diff --git a/plans/names-codes/2026-10-21-store-service-tasks.md b/plans/names-codes/2026-10-21-store-service-tasks.md index 2c11244652..0171c6d46c 100644 --- a/plans/names-codes/2026-10-21-store-service-tasks.md +++ b/plans/names-codes/2026-10-21-store-service-tasks.md @@ -97,7 +97,7 @@ This is the implementation plan for the names backend in StoreService: stages 2, #### T4. Wiring: configuration, HTTP helpers, phases, the names router - **Goal:** what every later task builds on, without an import cycle. Nothing answers differently until later tasks add routes. -- **Files:** `StoreService/Config.hs`; the ini example; new `StoreService/Web/Http.hs`; `StoreService/Web/Server.hs`; `StoreService/Service.hs`; new `StoreService/Names/Env.hs`; new `StoreService/Names/Phases.hs`; new `StoreService/Names/Turns.hs`; new `StoreService/Web/Names.hs`; `simplex-chat.cabal` (its own new modules, and the branch's `Names.Chain` and `Names.Registration` in the executable); `tests/Test.hs`; `tests/Bots/StoreService/ConfigTests.hs`. +- **Files:** `StoreService/Config.hs`; `apps/simplex-store-service/store_service.ini.example`; new `StoreService/Web/Http.hs`; `StoreService/Web/Server.hs`; `StoreService/Service.hs`; new `StoreService/Names/Env.hs`; new `StoreService/Names/Phases.hs`; new `StoreService/Names/Turns.hs`; new `StoreService/Web/Names.hs`; `simplex-chat.cabal` (its own new modules, and the branch's `Names.Chain` and `Names.Registration` in the executable); `tests/Test.hs`; `tests/Bots/StoreService/ConfigTests.hs`. - **Do:** - **The cycle:** move out of `Web/Server.hs` into `Web/Http.hs` everything a handler needs: `Respond`, `Limit`, `takeToken`, `limited`, `clientKey` and the bucket map, `readLimit`, `createLimit`, the new limits (`search` 30, `preview` 20, `code` 10, `find` 5, `records` 10 per minute), `maxBuckets` (now 32768), `jsonResponse`, `jsonError`, the error responses, `readBoundedBody`, `maxBodyBytes`, `nameBodyBytes` (16384), and `only` (today local to `webApp`'s `where`), so a known names path with another method answers `405 method_not_allowed` with `Allow`. The rate-limit state moves with them into a `HttpEnv` that `WebEnv` holds. `Web/Server.hs` and `Web/Names/*` import `Web/Http.hs`; `Web/Server.hs` imports `Web.Names.namesApp`, and no `Web/Names/*` module imports `Web/Server.hs`. Handlers that the invoice side shares (`invoiceView`, `createAtProvider`) move into `Web/Http.hs` or a module of their own when T11 needs them. - `[names]`: `tld`, `years` (2; startup refuses below 2), `term_start` (`2027-01-01T00:00:00Z`), `term_slack_seconds` (3600), `reveal_margin_seconds` (900), `max_order_moves` (3). `[chain]`: `rpc_url`, `chain_id`, `controller`, `sponsor`, `base_registrar`, `resolver`, `service_key`, `confirmations` (3), `gas_limit_cap` (8000000). `[listener]`: `names_static_dir`, `names_export_dir`, `names_hosts`. All in `knownSettings`. The names side is off when `[names]` is absent. @@ -198,7 +198,7 @@ This is the implementation plan for the names backend in StoreService: stages 2, #### T11. Paid orders, the registration view and the invoice's name side - **Goal:** `POST /api/invoice` for names, the views it answers with, settlement, cancel and cleanup. -- **Files:** new `StoreService/Names/Orders.hs` (the order checks shared with T12 and T15: formats, the order signature, existing orders, the timestamp, the duration, the chain, the price; one at a time per label hash); new `StoreService/Names/View.hs` (the registration view of the contract's section 6, with `version`, `codeLength` from T6 and `unread`); new `StoreService/Web/Names/Orders.hs`; `StoreService/Web/Server.hs` (`createInvoiceHandler` dispatches on `kind`; the name invoice view); `StoreService/Web/Http.hs` (the shared invoice view helpers); `StoreService/Store/Invoices.hs` (every invoice query gains the name side; provider-ref lookups across both tables, refusing a ref found in both); `StoreService/Orders.hs` (settling a name invoice queues its registration in the same transaction, only while open, then publishes; a late payment of a closed one alerts and leaves it `unpaid`); `StoreService/Poller.hs` (the 30-day cleanup with a provider read first; the overdue sweep for names); `StoreService/Providers/BTCPay.hs` (`InvoiceReceivedPayment`, `InvoicePaymentSettled` as read hints); `StoreService/Names/Store.hs` if a query is missing; new `tests/Bots/StoreService/NamesOrdersTests.hs`. +- **Files:** new `StoreService/Names/Orders.hs` (the order checks shared with T12 and T15: formats, the order signature, existing orders, the timestamp, the duration, the chain, the price; one at a time per label hash); new `StoreService/Names/View.hs` (the registration view of the contract's section 6, with `version`, `moves`, `linksRefused` from `links_refused_at`, `codeLength` from T6 and `unread`); new `StoreService/Web/Names/Orders.hs`; `StoreService/Web/Server.hs` (`createInvoiceHandler` dispatches on `kind`; the name invoice view); `StoreService/Web/Http.hs` (the shared invoice view helpers); `StoreService/Store/Invoices.hs` (every invoice query gains the name side; provider-ref lookups across both tables, refusing a ref found in both); `StoreService/Orders.hs` (settling a name invoice queues its registration in the same transaction, only while open, then publishes; a late payment of a closed one alerts and leaves it `unpaid`); `StoreService/Poller.hs` (the 30-day cleanup with a provider read first; the overdue sweep for names); `StoreService/Providers/BTCPay.hs` (`InvoiceReceivedPayment`, `InvoicePaymentSettled` as read hints); `StoreService/Names/Store.hs` if a query is missing; new `tests/Bots/StoreService/NamesOrdersTests.hs`. - **Do:** the contract's section 5 for `/api/invoice` and cancel, in its order of checks; publish after every commit. - Orders run under `withLabelTurn` (T4's `Turns`). - **`live`:** every transition that changes it is in this task, in the same transaction as the change: settlement (past `unpaid`, live), cancel (`cancelOpenInvoice`), the poller's overdue sweep (`expireOverdue`) and its expiry of an invoice it reads, a late payment of a closed invoice (stays not live), the 30-day cleanup (deleted), and phase changes to `taken` or `code_issued` (not live). @@ -229,7 +229,7 @@ This is the implementation plan for the names backend in StoreService: stages 2, - **Goal:** `POST /api/registration/:id/reveal`. - **Files:** new `StoreService/Names/Reveal.hs`; `StoreService/Web/Names/Registration.hs` (owned in wave 6); new `tests/Bots/StoreService/NamesRevealTests.hs`. - **Do:** the contract's section 7, in its order of checks, under `withLabelTurn`: the commitment rebuilt locally with `makeCommitment` from the sent links and the stored duration (check 5); `verifyRegisterIntent` plus T3's low-`s` parse and the sponsor nonce from the chain (check 6); only then the links through T7a's `linkKindOf` (check 7), recording `links_refused_at` on `invalid_link` as a compare-and-set write that bumps `version` and publishes; the accepted reveal's write as a compare-and-set on the `version` read, a lost race answering `409 registration_changed` with the current view; bump `version` and publish after the commit. Every request takes one read token, whatever its outcome. -- **Tests:** with and without links; an identical repeat in a later phase; a different repeat refused; a high-`s` intent refused; a wrong signer, a stale nonce, a past deadline, a label not matching the hash, a second link for one key (`invalid_link`), a link one byte over the bound (`invalid_link`), a wrong duration (commitment mismatch); a reveal with bad links that do not rebuild the commitment, or with a wrong signer, refused `bad_request` without recording a refusal; a refusal recorded only for the committed links with the owner's signature, waking a held poll; a reveal with two records at the bound passing the body cap; `provider_unavailable` on the nonce read; `invalid_link` recording the refusal; a reveal racing a `/name` (with a stubbed check pause), exactly one taking effect and the other answered `registration_changed` or `order_exists`/`bad_request`; a held poll woken by the label being set. +- **Tests:** with and without links; an identical repeat in a later phase; a different repeat refused; a high-`s` intent refused; a wrong signer, a stale nonce, a past deadline, a label not matching the hash, a second link for one key (`invalid_link`), a link one byte over the bound (`invalid_link`), a wrong duration (commitment mismatch); a reveal with bad links that do not rebuild the commitment, or with a wrong signer, refused `bad_request` without recording a refusal; a refusal recorded only for the committed links with the owner's signature, waking a held poll, and the view read by another browser showing `linksRefused: true`; a reveal with two records at the bound passing the body cap; `provider_unavailable` on the nonce read; `invalid_link` recording the refusal; a reveal racing a `/name` (with a stubbed check pause), exactly one taking effect and the other answered `registration_changed` or `order_exists`/`bad_request`; a held poll woken by the label being set. - **Depends on:** T2, T3, T6, T7a, T13. #### T15. Owner-signed requests: find, another name, a code instead @@ -239,9 +239,9 @@ This is the implementation plan for the names backend in StoreService: stages 2, - **Do:** the contract's section 8, each write a compare-and-set on `version`, then publish: - Every lost compare-and-set answers `409 registration_changed` with the current view. - `find` with `labelHash` and `sealed`, the 50 newest. - - `/name`: under `withLabelTurns` (the old and the new label hash, in hash order); allowed from `taken` (paid), `failed`, and `committed` only with `links_refused_at` set; a body equal to the current fields answers the view unchanged; at most `max_order_moves` successful moves, counted on success, then `409 move_limit` with the current view (the page shows a support line and the reference); the duration in its message; the order checks for the new fields except this registration (`order_exists`); `price_too_high`; for a code order the code checked and reserved again through T9 under its per-code turn (`code_used`, `code_reserved`, `code_invalid`, `code_expired`, `code_not_covering`); the reveal, transactions and `links_refused_at` cleared. + - `/name`: under `withLabelTurns` (the old and the new label hash, in hash order); allowed from `taken` (paid), `failed`, and `committed` only with `links_refused_at` set; a body equal to the current fields answers the view unchanged; at most `max_order_moves` successful moves, counted on success, then `409 move_limit` with the current view (the page shows a support line and the reference); the duration in its message; the order checks for the new fields except this registration (`order_exists`); `price_too_high`; for a code order the code checked and reserved again through T9 under its per-code turn (`code_used`, `code_reserved`, `code_invalid`, `code_expired`, `code_not_covering`); the reveal, transactions and `links_refused_at` cleared (`linksRefused` false); a code still reserved by this order kept even if revoked or expired since, as `/retry` does. - `/code`: an identical repeat first (the issued code's hash) answers `200 {}` whatever `codeLength` the request carries; otherwise `codeLength` in the body and the signed message, `minLength` from `codeLengthFor`, `409 code_length_changed` with the current value when they differ; `code_unavailable` when none; the order's years; `code_conflict`; `503` on a failed read. -- **Tests:** each with a wrong signer and a stale timestamp refused; `find` newest first with retargeted and `unpaid` entries, capped at 50, `not_found`; `/name` from `registered` and from a `committed` order with no refused reveal refused, from `failed` and from `committed` after a refused reveal accepted (the same name with other links too), a repeat of the current fields answered unchanged with no move counted, the fourth move `move_limit` with the current view, meeting another live order `order_exists`, a dearer name `price_too_high` with its price, a cheaper one accepted with the new `price`, a code order to a shorter name `code_not_covering`, a code order after the 30-day sweep re-reserving an unused code and answering `code_used` for one spent elsewhere, `sealed` over 11095 refused; `/name` A→B running concurrently with another order's B→A without deadlock; `/code` repeat answered 200 even after a price change that would move the length, its `minLength` for 6, 7 and 12 letters at the launch prices (6, 7 and 8), `code_length_changed` after a price change, `code_unavailable`, `503` on a failed read, the view's `codeLength` kept in `code_issued`; a held poll woken by `/name` and by `/code`. +- **Tests:** each with a wrong signer and a stale timestamp refused; `find` newest first with retargeted and `unpaid` entries, capped at 50, `not_found`; `/name` from `registered` and from a `committed` order with no refused reveal refused, from `failed` and from `committed` after a refused reveal accepted (the same name with other links too), a repeat of the current fields answered unchanged with no move counted, the fourth move `move_limit` with the current view, meeting another live order `order_exists`, a dearer name `price_too_high` with its price, a cheaper one accepted with the new `price`, a code order to a shorter name `code_not_covering`, a code order after the 30-day sweep re-reserving an unused code and answering `code_used` for one spent elsewhere, a code order whose held code was revoked or expired since accepted, `linksRefused` false after a move, `sealed` over 11095 refused; `/name` A→B running concurrently with another order's B→A without deadlock; `/code` repeat answered 200 even after a price change that would move the length, its `minLength` for 6, 7 and 12 letters at the launch prices (6, 7 and 8), `code_length_changed` after a price change, `code_unavailable`, `503` on a failed read, the view's `codeLength` kept in `code_issued`; a held poll woken by `/name` and by `/code`. - **Depends on:** T3, T6, T9, T12, T13. ### Stage 4: registration on chain