mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-12 09:28:09 +00:00
plans: names store on StoreService
This commit is contained in:
1 parent
e2347ea55c
commit
bd510a1896
1 file changed
+169
-209
@@ -1,247 +1,207 @@
|
||||
# Name codes in the badge service and webapp
|
||||
# SimpleX names store: purchase and registration on the web
|
||||
|
||||
## Context
|
||||
|
||||
SimpleX names (`<label>.simplex`, an ENS fork on Ethereum) are registered in the app with a code, as planned on `origin/ab/names-actions-api:plans/2026-09-28-name-registration-core-cli.md` (#7530). That plan says codes come from "the web store" and `//issue`, into the badge service's code table with a kind column, and that the badge service is renamed ShopService. No web store exists. This work builds it: the badge service and its webapp also sell **name codes**, with a Python mock to drive the webapp. Redemption (commit/reveal on chain, `BSCRedeemNameCode`) stays with #7530.
|
||||
SimpleX names (`<label>.simplex`, an ENS fork on Ethereum) get their own store at **simplex.domains**. The buyer searches a name. Then:
|
||||
|
||||
Facts from research that shape the design:
|
||||
- A name code is an entitlement: "names of N+ letters for Y years", format `SN6-2Y-4K2P7-TQ9M1-ZX3RB-8HJ5W` (#7530 §6). It cannot be bound to a name: registration needs the owner's wallet address, which only the app has. The availability check is therefore advice, and the final screen says so.
|
||||
- Contract minimum term is 730 days, so 2 years minimum. `.simplex` is not deployed yet (only `.testing` on mainnet).
|
||||
- Availability comes from the resolver (`simplexmq: scripts/resolver/service/snrc-resolve.py`), `GET /v2/resolve/[<keccak256(label) hex>].<tld>` → `registration.type` = `available | registered | reserved`. It binds loopback with no CORS, so the service proxies it.
|
||||
- Name rules (#7530 `validLabel`, simplexmq `nameLabelP`): `a-z 0-9 -`, lowercase-folded, no leading/trailing hyphen, no `--` at positions 3–4, 6–63 characters.
|
||||
- The approved canvas (5f) has the store learn only the length. **Decided otherwise by the owner:** the page sends the name to our service for the check. The service hashes it before asking the resolver and never logs it.
|
||||
- if it is free, the page shows its price, the buyer pays by card, Bitcoin or Monero, and the page shows the registration steps the app shows (Committing, Waiting, Registering);
|
||||
- once registered, the buyer gets a claim code that moves the name into the app.
|
||||
|
||||
## Decisions (owner, 2026-10-09)
|
||||
Badges keep their own store at **badges.simplex.chat**. To the user these are two stores, each with a secondary link to the other.
|
||||
|
||||
Both stores run on the badge service, renamed **StoreService**, and on one webapp codebase. Redemption in the app (core and UI) is #7530's: `origin/ab/names-actions-api:plans/2026-09-28-name-registration-core-cli.md`. That plan also specifies the service's chain client, which this work shares.
|
||||
|
||||
Facts that shape the design:
|
||||
|
||||
- Registering needs an owner address. Only the app's wallet has one, so the service registers each web-bought name to **its own registrar address** and holds it until the buyer claims it with the app. The name is held for real from the moment it is registered, so it cannot be taken while unclaimed.
|
||||
- The contract's term is fixed here at **2 years**, the contract minimum (730 days). It runs from the web registration, not from the claim.
|
||||
- Availability and records come from the resolver (`simplexmq: scripts/resolver`): `GET /v2/resolve/[<keccak256(label)>].<tld>` → `available | registered{nameRecord} | reserved`. A registered name's `nameRecord` lists its SimpleX contact and channel links. The service opens those with its own chat core to show their display name and picture.
|
||||
- Name rules (#7530 `validLabel`): `a-z 0-9 -`, folded to lower case, no leading or trailing hyphen, no `--` at positions 3–4, 6–63 characters.
|
||||
- `.simplex` is not deployed; `.testing` is live on mainnet.
|
||||
|
||||
## Decisions (owner, 2026-10-09 and 2026-10-10)
|
||||
|
||||
| Topic | Decision |
|
||||
|---|---|
|
||||
| Name screen | Type name; page validates locally; service checks availability via resolver |
|
||||
| Term | 2 years minimum, "+"/"−" stepper, max 10, price linear in years |
|
||||
| Price | $50/year for 8+ letters; 7 letters $100/year; 6 letters $200/year |
|
||||
| App shape | One app, one build; landing offers Badge or Name; payment/history screens shared |
|
||||
| Code format | Adopt #7530 §6; implement the kind in core `Badges/Code.hs`, mirror in `web/src/codes.ts` |
|
||||
| Final screen | Code, Copy, QR, and an "Open in SimpleX" link (format proposed below, for the app team) |
|
||||
| Service scope | Web issuance, operator `//issue name` and group `/issue name`, ShopService rename |
|
||||
| Storage | Same code table with `code_kind`; `badge_type`/`months` become nullable |
|
||||
| Mockups | Static HTML on the real `styles.css`, rendered to PNG with Playwright |
|
||||
| Stores | Names at simplex.domains, badges at badges.simplex.chat; separate to the user, with secondary cross-links |
|
||||
| Code | One webapp codebase with two shells (names, badges) sharing payment, card and history screens; one service |
|
||||
| Service | The badge service is renamed **StoreService** (`apps/simplex-store-service`, `Simplex.Chat.StoreService`) |
|
||||
| Search | The buyer types a name; the service answers availability and the price for that name |
|
||||
| Taken | A taken name shows what uses it: address and/or channel, with display name, picture, link and deep link |
|
||||
| Reserved | Shown as reserved, with a link to contact the SimpleX team |
|
||||
| Price | Internal and changeable, possibly per name. The page never shows a price list, only the price of the name searched |
|
||||
| Term | 2 years, fixed. No term screen |
|
||||
| Registration | After payment the service registers the name to its registrar address. The page shows Committing → Waiting 60s → Registering, as in the app (canvas 9) |
|
||||
| Ownership | The service holds the name until it is claimed: a claim code, and Open in SimpleX, move it to the app's wallet |
|
||||
| Secondary | "Buy a code for any name of N+ letters", for registering later in the app (#7530's `SN<len>-2Y-…` code) |
|
||||
| Code format | #7530 §6: `SN6-2Y-4K2P7-TQ9M1-ZX3RB-8HJ5W`, in core `Badges/Code.hs`, mirrored in `web/src/codes.ts`. A claim code is the same format, bound to its name in the service |
|
||||
| Storage | Name codes in the code table with `code_kind`; registrations in their own table |
|
||||
| Mockups | The board and screens generated from the real webapp modules (`plans/names-codes/mockups/`) |
|
||||
|
||||
## The names store, screen by screen
|
||||
|
||||
1. **Search** (the store's first screen): a name field with `.simplex`, validated locally as typed (length, characters). Search asks `GET /api/name/:label`, and the answer appears on the same screen:
|
||||
- **available:** "Available for $X for 2 years" with Register enabled;
|
||||
- **taken:** "Used by" and one card per address or channel, each with picture, display name, Open (deep link) and the link;
|
||||
- **reserved:** "Reserved", with "Contact the SimpleX team";
|
||||
- **check failed or rate limited:** shown as such.
|
||||
|
||||
Secondary links: "Buy a code for any name of 6+ letters" and "Badges".
|
||||
2. **Checkout:** summary (name, 2 years, total), Pay with Card / Bitcoin / Monero, and Pay.
|
||||
3. **Payment:** the badge payment screens, unchanged.
|
||||
4. **Registering:** the three steps, each ticked as it completes, with the transaction links. A reload resumes them; they are kept by the service, not the page.
|
||||
5. **Registered:** "dynamis.simplex is yours until 10 Oct 2028". The claim code, Copy, Open in SimpleX and its QR. A warning that the name points nowhere until it is claimed, and that this browser keeps the only copy.
|
||||
|
||||
**Outcomes off the path:**
|
||||
- **Taken between search and registration:** the order keeps its value. The page offers to search another name to register with the same order (price at most what was paid), or to keep it as a code for any name of that length.
|
||||
- **Registration failed after paying:** "Paid, not registered". Retrying charges nothing.
|
||||
- **Payment endings:** the badge screens (partial, expired, cancelled, card failures).
|
||||
|
||||
**The secondary flow:** choose the shortest length (6, 7, 8+) with its price, then checkout and payment as above. The result is an unbound `SN<len>-2Y` code for the app (#7530's redemption).
|
||||
|
||||
## Stages
|
||||
|
||||
Each stage is its own set of atomic commits; stage 0 is reviewed before any code.
|
||||
Each stage is its own set of atomic commits. Stage 0 is reviewed before any code.
|
||||
|
||||
### 0. Design: flow board, spec and mockups (review gate)
|
||||
### 0. Design: board, spec and mockups (review gate)
|
||||
|
||||
Deliverables, all under `plans/names-codes/`:
|
||||
Redo `plans/names-codes/`: the board `names-flow.svg`, `screens/*.jpg`, the spec `2026-10-09-name-codes.md` and the generator `mockups/`, for the store above. The board's sections:
|
||||
|
||||
1. **`names-flow.svg`**: one large board that shows every screen and outcome and how each screen leads to the next. It follows `plans/badges-codes/badges-flow-mvp.svg`:
|
||||
- a dark title bar: "SimpleX names: every screen, on the web and where it meets the app"
|
||||
- one tree per surface, read left to right
|
||||
- each screen drawn in its frame (a browser with URL bar on desktop, a phone frame on phone), with an ID tag and a caption: a bold "N1. Title" and a short paragraph saying why the screen exists and what it reads
|
||||
- arrows labelled with the action or outcome ("Check availability", "Continue", "Pay with Monero", "it confirms", "429")
|
||||
- arrow colours: blue for the normal path, orange for a variant or failure, grey for a platform difference, dashed for anything crossing between the site and the app
|
||||
- a footer of design notes
|
||||
2. **`screens/*.jpg`**: each screen of the board on its own, rendered with Playwright Chromium: desktop at 1024 wide, phone at 390, dark only for N0, N1b, N3 and N4 to check the palette. JPEG at quality 85, since the page's background wash makes PNGs four times larger.
|
||||
3. **`mockups/screens.js` and `mockups/mockups.css`**: every screen, built in the browser on the webapp's compiled modules. Shared screens call the real `screens.js`, and name screens are prototypes for `nameScreens.ts`. The new rules in `mockups.css` (name field, tiers, stepper) move into `styles.css` in stage 4.
|
||||
4. **`2026-10-09-name-codes.md`**: the spec, following `plans/badges-codes/2026-08-27-badge-codes.md`. It covers the flow, the API, the codes, the schema and the copy, and embeds `screens/*.jpg` per section and the board at the top.
|
||||
- the names store start to finish;
|
||||
- what a search can find;
|
||||
- registration steps and their outcomes;
|
||||
- the secondary code flow;
|
||||
- payment endings (shared);
|
||||
- where it meets the app (claim; grey, #7530);
|
||||
- the badges store at badges.simplex.chat, its landing and cross-link only;
|
||||
- phone width;
|
||||
- the operator.
|
||||
|
||||
**Screen inventory**: the board holds every screen below. "Shared" means the badge screen is reused, drawn with name content.
|
||||
The generator stays as built; only `screens.js`, `mockups.css` and `layout.mjs` change.
|
||||
|
||||
*On the web, buying start to finish* (blue path):
|
||||
### 1. Rename to StoreService (pure refactor)
|
||||
|
||||
| ID | Screen | Leads to |
|
||||
|---|---|---|
|
||||
| N0 | Landing: hero, "Get a badge" and "Get a SimpleX name" | B2 badge wizard; N1 |
|
||||
| N1 | Name: empty field with `.simplex` suffix, rules line "6+ letters, a–z, 0–9, hyphens" | as typed: N1a |
|
||||
| N1a | Typing: live tier and price ("7 letters · $100/year"); Check availability enabled | Check availability → N1b / N1c / N1d / N1e |
|
||||
| N1b | Available: green "dynamis.simplex is available"; Continue enabled | Continue → N2 |
|
||||
| N2 | Years: "− 2 years +" with minus disabled at 2, the total, a per-year line | + / − redraw it; Continue → N3 |
|
||||
| N3 | Checkout: summary rows (Name, Covers "names of 7+ letters", Term, Total), Pay with Card / Bitcoin / Monero | Pay → N5 (crypto) or N8 (card) |
|
||||
| N5 | Send N BTC/XMR (shared B5): QR, amount, address, reference, rate countdown | it confirms → N6 → N4 |
|
||||
| N6 | Payment received, waiting to confirm (shared B5b) | settled → N4 |
|
||||
| N4 | Code: tick, "Paid. Here is your name code.", code frame `SN7-2Y-…`, Copy code, "Open in SimpleX", QR, "Register it in the app", warning "Not reserved until you register it. This code covers any name of 7+ letters for 2 years." | Open in SimpleX → A1 (dashed) |
|
||||
As #7530 §5, with StoreService for ShopService:
|
||||
|
||||
*On the web, the name screen's other outcomes* (orange):
|
||||
- A JSON test first pins every command, response and error tag.
|
||||
- `Simplex.Chat.Badges.Service` → `Simplex.Chat.StoreService`, with constructor prefixes `BSC`/`BSP`/`BSE` → `STC`/`STP`/`STE` (`dropPrefix` updated in the same commit). `ChatConfig.badgeServiceAddress` → `storeServiceAddress`.
|
||||
- `apps/simplex-badge-service` → `apps/simplex-store-service`, `BadgeService.*` → `StoreService.*`, executable `simplex-store-service`, scripts, tests and hspec labels.
|
||||
- Persisted names stay: the `sx_badge_service_` prefix, the migrations table and the database.
|
||||
- Coordinate with #7530 so the rename lands once.
|
||||
|
||||
| ID | Screen |
|
||||
|---|---|
|
||||
| N1c | Too short: "6 letters or more" under the field; Check disabled |
|
||||
| N1d | Not allowed: characters or hyphen rules ("only a–z, 0–9 and single inner hyphens") |
|
||||
| N1e | Taken: "dynamis.simplex is registered"; Continue disabled; field kept for another try |
|
||||
| N1f | Reserved: "This name is reserved" |
|
||||
| N1g | Check unavailable (resolver down or service 503): "Could not check now. Try again." |
|
||||
| N1h | Too many checks (429): "Try again in N seconds", Check disabled for that long |
|
||||
### 2. Core: name code kind, label rule, Registration encoding
|
||||
|
||||
*On the web, the years screen's edges:*
|
||||
- N2a: max 10 years, plus disabled
|
||||
- N2b: 6-letter tier at 3 years, showing the ×4 price
|
||||
- `src/Simplex/Chat/Badges/Code.hs` gains `NameCode` (`parseNameCode`, `randomNameCode`, `nameCodeParams`, `nameCodeHash`, `formatNameCode`, `nameCodeText`).
|
||||
- The grammar is `SN` + minLength digit (6–8) + years + `Y` + 20 body characters, with a Luhn mod-32 check over the parameters and the payload.
|
||||
- The canonical form is hashed with SHA-256.
|
||||
- `src/Simplex/Chat/Names.hs`: `validNameLabel`, `nameTier`.
|
||||
- The `Registration` ABI encoding and commitment, as one module both core and service use (#7530 D17). This is shared with #7530: build it once, where it lands first.
|
||||
- Tests with vectors shared with `web/test`.
|
||||
|
||||
*On the web, refused at checkout* (orange; shared badge screens with name summary):
|
||||
### 3. Service: search, prices, checkout
|
||||
|
||||
| ID | Screen |
|
||||
|---|---|
|
||||
| N3a | Method temporarily unavailable (503 `provider_unavailable`), shared B4b |
|
||||
| N3b | Prices changed (`catalog_changed`), shared B4c, back to N1 |
|
||||
| N3c | Too many attempts (429), shared B4d |
|
||||
| N3d | That did not go through (network / 5xx / `bad_request`), shared invoice failure |
|
||||
| N3e | An order already waiting (open order line; card awaiting confirmation blocks a second order) |
|
||||
- **`[names]`** in the ini: `resolver_url`, `tld` (`testing` until `.simplex` is deployed), and the internal price rule.
|
||||
- **`GET /api/name/:label`:**
|
||||
- validates and keccak-hashes the label, then asks the resolver;
|
||||
- for a registered name, opens each `simplexContact`/`simplexChannel` link with the chat core (as `APIConnectPlan` does) for display name and picture, and adds a deep link;
|
||||
- answers `available{price}`, `registered{entities}` or `reserved`;
|
||||
- uses the read rate limit and never logs the label;
|
||||
- caches link profiles briefly, so one search costs at most one fetch per link.
|
||||
- **Prices:** `Catalog.namePrice :: Text -> Either Refusal CurrencyAmount`. It starts from a per-length table seeded insert-only (`name_prices`), with per-name overrides possible. The page shows what the service answers. Checkout reprices and refuses a changed price with `catalog_changed`.
|
||||
- **Checkout:**
|
||||
- `POST /api/invoice` with `kind: "name"` (label, the price shown, method, and the hash of a claim code made in the browser), or `kind: "nameCode"` (minLength, price, method, codeHash).
|
||||
- A name invoice writes a `name_registrations` row (label, price, state `awaiting_payment`) and an unpaid claim code row bound to it.
|
||||
- Settlement moves the registration to `paid`, which the registration worker picks up (stage 4).
|
||||
- **Migration `20261010_names`** (SQLite and Postgres):
|
||||
- `badge_codes` gains `code_kind`, `min_length` and `years`, and `badge_type`/`months` become nullable. SQLite needs a table rebuild with `PRAGMA defer_foreign_keys = ON` and an up/down test with referencing rows.
|
||||
- `badge_code_invoices` gains `name_price_id`.
|
||||
- New tables `name_prices` and `name_registrations`: invoice, label, code, secret, commitment, phase, tx hashes, block numbers, `expires_at`, claimed owner, timestamps.
|
||||
- **`GET /api/invoice/:id`** adds `registration {phase, label, commitTx, registerTx, expiresAt, error}` for a name invoice. The long poll also wakes on phase changes.
|
||||
- **Operator:** `//issue name <6|7|8> [paid|unpaid|free]`, and group `/issue name` / `/bulk name`, for unbound length codes. `//revoke` takes either kind.
|
||||
|
||||
*On the web, the other ways a payment ends* (orange, shared):
|
||||
### 4. Service: registration on chain and claiming
|
||||
|
||||
| ID | Screen |
|
||||
|---|---|
|
||||
| N5a | Part of the amount arrived |
|
||||
| N5b | Cancel invoice → canceled |
|
||||
| N7a | Expired, nothing received |
|
||||
| N7b | Expired, part paid ("quote the reference") |
|
||||
| N8 | Card form (Stripe) |
|
||||
| N8a | Card form did not load |
|
||||
| N8b | Card submitted, waiting to confirm |
|
||||
| N8c | Taking longer than expected |
|
||||
| N4a | Code not saved in this browser |
|
||||
| N4b | Paid, but the code is not on this device |
|
||||
| N9 | Unknown order link |
|
||||
Shares #7530 §10's chain client: JSON-RPC to the resolver host's reth node, a dry run of every transaction, serial nonces, EIP-1559 fees, receipts with replacement, and 3 confirmations. Signing comes from simplexmq (#7530 D20, stacked on `ab/eth-crypto` #1843). `[chain]` holds the RPC URL, chain id, contract addresses, registrar key and confirmation depth.
|
||||
|
||||
*On the web, history:* N10 "Your codes" with name rows (paid with code and the chosen name, waiting for payment, expired) beside a badge row.
|
||||
- **Registration worker**, one per paid registration, resumable after a restart:
|
||||
1. `paid` → commit `makeCommitment(Registration{label, owner = registrar, duration = 2y, secret, …})` → `committing`
|
||||
2. → `committed` once mined → `waiting` until `minCommitmentAge`
|
||||
3. → `registering`: send the register call
|
||||
4. → `registered` at receipt + 3 blocks, recording `expires_at`
|
||||
|
||||
Each phase is published to the page's long poll.
|
||||
- **Failure handling:**
|
||||
- A dry run reverting with `NameNotAvailable` → `taken`. The order offers another name of at most its price, or conversion to an unbound length code.
|
||||
- Other failures → `failed`, retried; still failing → "Paid, not registered", where a retry charges nothing.
|
||||
- **Claiming:** a new service command `claimName {code, owner, nameLinks}`, at the next version.
|
||||
- It checks that the code is paid, bound and unspent and that the name is held.
|
||||
- It sets the resolver records the links ask for while the service still owns the name, then transfers the token and the registry node to `owner`, and marks the code spent.
|
||||
- The core and app side are #7530's.
|
||||
- **Funding:** the deployed `.testing` controller's `register` is payable in ETH, so the registrar key holds ETH. #7530's `registerWithCredit` replaces this when the `.simplex` contracts deploy. This is an open question in the spec.
|
||||
|
||||
*On the web, on a phone* (390 px): N0, N1b, N1e, N2, N3, N5, N4. These show what stacks (QR first), what stays side by side (payment methods) and the stepper at phone width.
|
||||
### 5. Webapp (`web/`)
|
||||
|
||||
*Where it meets the app* (dashed; drawn grey as "not built here", from canvas 5g):
|
||||
- A1: the app's register screen opened by the link, with code and name filled in.
|
||||
- A1a: the name was taken between the check and redemption; the app keeps the code for another name.
|
||||
- **Two sites from one build:** `build.js` emits `dist/badges/` and `dist/names/`. Each is a complete site root (`index.html`, `sw.js`, `assets/<hash>/`), so each host has its own shell, service worker scope and precache, and Caddy needs no rewrites.
|
||||
- **New modules:**
|
||||
- `names.ts` (pure): `validateLabel`, the search result types;
|
||||
- `nameScreens.ts`: search, checkout, steps, registered, taken-meanwhile, secondary code flow;
|
||||
- `namesMain.ts`: the names shell's wiring, reusing `flow.ts`, `order.ts`, `api.ts` and `store.ts`.
|
||||
- **Extended modules:**
|
||||
- `codes.ts` (name codes);
|
||||
- `api.ts` (`checkName`, name and name-code invoices, registration in the invoice view, `invalid_name`);
|
||||
- `domain.ts`/`store.ts` (`kind`, `label`, `minLength`, `years`);
|
||||
- `order.ts` (registration phases select the steps, registered and taken screens);
|
||||
- `main.ts` (the badges shell adds the "SimpleX names" cross-link).
|
||||
- **Styles:** the name field, entity cards and step list, from `mockups.css`, into `styles.css`.
|
||||
- **The open-in-app link:** `openInAppUrl(code, label)` → `simplex:/name#code=…&label=…`. This is a proposal for the app team to confirm.
|
||||
- **Tests:** node:test in the existing style, plus the build-hash tripwire for both shells.
|
||||
|
||||
*For the operator:*
|
||||
- OP1: `//issue name 7 3` in `--run-cli`, printing `Code: SN7-3Y-…`.
|
||||
- OP2: group `/issue name 6 years 2` and `/bulk name 8 count 3`, with the service's replies.
|
||||
### 6. Deployment (`scripts/badge-service/`, renamed `scripts/store-service/` in stage 1)
|
||||
|
||||
**How the board is built** (owner, 2026-10-09):
|
||||
- `mockups/board.mjs` (committed) reads one layout table, `mockups/layout.mjs`: per frame its tag, screen, kind, section, position and caption, plus its incoming arrow with label and colour.
|
||||
- It renders each frame with Playwright (`screens/*.jpg`) and writes `names-flow.svg`: vector title bar, section headings, frames, arrows, labels and captions, with each screen embedded as a base64 JPEG.
|
||||
- Editing a mockup or the layout and running `node plans/names-codes/mockups/board.mjs` regenerates both the screens and the board.
|
||||
- Playwright is not added to `web/package.json`. The script states its one-off install, as `web/README.md` does for its rendered review.
|
||||
Production is the split deployment in `scripts/badge-service/`:
|
||||
- Caddy serves the folder the service exports at boot (`webapp_export_dir`, bind-mounted to `./web`) and proxies `/api` and `/webhooks` to the service.
|
||||
- The service runs with `+client_postgres`, `network_mode: host`, and the ini mounted read-only.
|
||||
|
||||
### 1. Rename to ShopService (pure refactor)
|
||||
Changes:
|
||||
- **Dockerfile:** the web stage builds both sites; the runtime image carries `/srv/web/badges` and `/srv/web/names`.
|
||||
- **Export:** `exportWebapp` copies both sites and injects the Stripe publishable key into both shells.
|
||||
- **Caddy, per host:**
|
||||
- `badges.simplex.chat` → `web/badges`;
|
||||
- `simplex.domains` → `web/names`;
|
||||
- both proxy `/api/*` to the service; `/webhooks/*` stays on one host;
|
||||
- a deployment note gives the per-host blocks and CSP (Stripe, `frame-ancestors`), since the Caddyfile is not in this repository.
|
||||
- **All-in-one mode** (`serve_webapp = on`, development): the service picks the site by `Host` against `[listener] names_hosts` (default `simplex.domains`). Every other host gets badges.
|
||||
- **Chain access:** with host networking, the resolver (`127.0.0.1:8000`) and reth (`127.0.0.1:8545`) are reached on loopback when they share the host, or over a private network otherwise. The registrar key lives in the mounted ini, which `Dockerfile.dockerignore` already keeps out of the image.
|
||||
- **Persisted names stay** (database `badge_service`, the `sx_badge_service_` prefix). The compose service, image, binary and ini path move to the new name with the rename.
|
||||
|
||||
As #7530 §5:
|
||||
- Add a JSON test first that pins every `BadgeService*` command/response/error tag, so the rename provably leaves the wire unchanged.
|
||||
- Rename `Simplex.Chat.Badges.Service` → `Simplex.Chat.ShopService`; `BadgeService{Request,Command,Response,Version,ErrorCode}` → `ShopService…`.
|
||||
- Constructor prefixes `BSC/BSP/BSE` → `SHC/SHP/SHE`, updating each `dropPrefix` in the same commit. `ChatConfig.badgeServiceAddress` → `shopServiceAddress`.
|
||||
- `apps/simplex-badge-service` → `apps/simplex-shop-service`, `BadgeService.*` → `ShopService.*`, executable `simplex-shop-service`, `scripts/badge-service`, test modules and hspec labels.
|
||||
- Persisted names stay: the `sx_badge_service_` table prefix and migrations table.
|
||||
- Badge-domain modules keep their names (`Simplex.Chat.Badges*`, `Store.Badges`).
|
||||
- Risk: `ab/names-actions-api` plans the same rename; coordinate so only one lands.
|
||||
### 7. Mock (`web/mock/server.py`)
|
||||
|
||||
### 2. Core: the name code kind and label rule
|
||||
|
||||
- `src/Simplex/Chat/Badges/Code.hs` grows a `NameCode` beside `BadgeCode`, sharing the alphabet, `charValue` and `checkValue`. API:
|
||||
- `parseNameCode :: Text -> Maybe NameCode`
|
||||
- `randomNameCode :: TVar ChaChaDRG -> Int -> Int -> IO NameCode`
|
||||
- `nameCodeParams :: NameCode -> (Int, Int)`
|
||||
- `nameCodeHash`, `formatNameCode`, `nameCodeText`
|
||||
- Grammar (on the alphanumeric-filtered, upper-cased text): `SN` + one digit minLength (6–8) + 1–2 digits years (2–10) + `Y` + 20 body characters. The last body character is the Luhn mod-32 check over the values of the parameter characters and the 19 payload characters, so `SN6`→`SN8` fails.
|
||||
- Canonical: `SN62Y` + 20 characters; hashed with SHA-256, like badges.
|
||||
- Display: `SN6-2Y-XXXXX-XXXXX-XXXXX-XXXXX`.
|
||||
- `src/Simplex/Chat/Names.hs`: `validNameLabel :: Text -> Either LabelError Text` (fold case, rules above) and `nameTier :: Int -> Int` (len ≥ 8 → 8, else len). #7530 reuses both.
|
||||
- Tests in `tests/BadgeTests.hs`: round trips, the check over the parameters, folding, rejected grammar. Fixed vectors are shared verbatim with `web/test/codes.test.ts`.
|
||||
|
||||
### 3. Service (`apps/simplex-shop-service`)
|
||||
|
||||
**Config `[names]`** (`Config.hs`, ini example, README):
|
||||
- `resolver_url` (default `http://127.0.0.1:8000`), `tld` (default `simplex`; `testing` until deployment).
|
||||
- Absent: no name sales, and `/api/name/*` answers `provider_unavailable`.
|
||||
|
||||
**Migration `20261009_name_codes`**, SQLite and Postgres:
|
||||
- `@badge_codes`:
|
||||
- add `code_kind TEXT NOT NULL DEFAULT 'badge'`, `min_length INTEGER`, `years INTEGER`
|
||||
- make `badge_type`/`months` nullable
|
||||
- add a CHECK that the badge columns are set exactly for `badge` and the name columns exactly for `name`
|
||||
- `@badge_code_invoices`: `price_id` nullable, add `name_price_id TEXT REFERENCES @name_prices`, CHECK exactly one of the two is set.
|
||||
- New `@name_prices(price_id PK, min_length, year_price, currency, status, created_at)`, seeded insert-only like `seedCatalog` (`Store/Invoices.hs`): `name_8`/5000, `name_7`/10000, `name_6`/20000 cents.
|
||||
- Postgres: `ALTER … DROP NOT NULL` and `ADD CONSTRAINT`.
|
||||
- SQLite: table rebuilds following `src/Simplex/Chat/Store/SQLite/Migrations/M20230118_recreate_smp_servers.hs`, with `PRAGMA defer_foreign_keys = ON`, because `badge_purchases` and `badge_code_invoices` reference `badge_codes`. A migration test runs up/down with referencing rows present; this is the riskiest step.
|
||||
|
||||
**Catalog** (`Catalog.hs`):
|
||||
- `NamePrice` and `priceName :: [NamePrice] -> priceId -> years -> Either CatalogRefusal PricedName`.
|
||||
- Years outside 2–10 → `bad_request`; unknown or disabled price → `catalog_changed`; reuses `offerTotal`'s amount cap.
|
||||
- `defaultNameCatalog` mirrors `web/src/names.ts` and the mock.
|
||||
|
||||
**Store** (`Store.hs`, `Store/Invoices.hs`):
|
||||
- `InvoiceRow`'s `irBadgeType`/`irMonths` become `irItem :: CodeItem = CIBadge BadgeType Word8 | CIName Word8 Word8` (minLength, years). Decoding is driven by `code_kind`.
|
||||
- `getBadgeCode` filters `code_kind = 'badge'`. A name row must never decode as a badge; the SN prefix already keeps the hashes apart.
|
||||
- `insertBadgeCode` and the invoice inserts write the kind.
|
||||
- `revokeCode` works for both kinds.
|
||||
- The row's `min_length`/`years` are authoritative at redemption (#7530), so a client that bought an 8+ code with `SN6` text gains nothing.
|
||||
|
||||
**Web API** (`Web/Server.hs`):
|
||||
- `GET /api/name/:label`:
|
||||
- Validate with `validNameLabel` → 400 `invalid_name`.
|
||||
- Compute keccak256 (crypton `Keccak_256`), then `GET {resolver_url}/v2/resolve/[hex].{tld}` with a 3 s timeout.
|
||||
- 200 `{status: available|registered|reserved, minLength, yearPrice, currency}`.
|
||||
- Resolver failure → 503 `provider_unavailable`.
|
||||
- Read rate limit. The label is never logged.
|
||||
- `POST /api/invoice` accepts `{kind:"name", priceId, years, method, codeHash}` beside the unchanged badge body.
|
||||
- The code row is written unpaid with kind `name` and the price's `min_length` and `years`.
|
||||
- The response and `GET /api/invoice/:id` carry `kind:"name", minLength, years` instead of `badgeType`/`months`.
|
||||
- Settlement (`Orders.hs`), poller, webhooks and cancel stay unchanged: they work on invoice ids and code hashes.
|
||||
- New wire error `invalid_name`, added to `WIRE_ERROR_CODES` and the README table.
|
||||
|
||||
**Operator** (`Service.hs`, `Group/Command.hs`):
|
||||
- `//issue name <6|7|8> [<years 2-10>] [paid|unpaid|free]`.
|
||||
- Group commands `/issue name <len> [years <Y>]` and `/bulk name <len> [years <Y>] count <B>`, single use.
|
||||
- Badge parsing is unchanged; `groupCommands` params text updated.
|
||||
|
||||
### 4. Webapp (`web/`)
|
||||
|
||||
- `src/names.ts` (pure):
|
||||
- `validateLabel` (mirror of `validNameLabel`), `tierOf`
|
||||
- `NAME_CATALOG` (price ids, year prices, `MIN_YEARS = 2`, `MAX_YEARS = 10`), `nameTotal`
|
||||
- `src/codes.ts`: `generateNameCode(minLength, years)`, name `normalise`/`display`/`canonical`; `hash` shared.
|
||||
- `src/api.ts`:
|
||||
- `checkName(label)`
|
||||
- `createInvoice` takes a badge or name request
|
||||
- response parsers accept the `kind:"name"` shape
|
||||
- `invalid_name` added to the error codes
|
||||
- `src/domain.ts`/`store.ts`: `OrderRecord` gains optional `kind: "name"`, `minLength`, `years`, `label`. The label is stored only locally, for display; a record without `kind` is a badge.
|
||||
- `src/flow.ts`: `Selection` becomes badge | name. Checkout generates the matching code kind; retry on `code_conflict` stays the same.
|
||||
- `src/order.ts`: `codeIssued` carries the kind and parameters; history titles such as "dynamis.simplex · 6+ letters · 2 years".
|
||||
- `src/nameScreens.ts` (new, keeps `screens.ts` from growing): `nameEntry`, `nameYears`, `nameSummary`, `nameCodeIssued`. Built from exported helpers in `screens.ts` (`el`, `button`, panel, back button, `summaryRows`, `copyControl`, `qrFigure`, `investPanel`).
|
||||
- `src/main.ts`:
|
||||
- The wizard's steps become per-product lists (badge: tier/months/checkout; name: name/years/checkout) on the same track/rail. Routes are `#/name`, `#/name/years`, `#/name/checkout`.
|
||||
- The landing gets the two entries.
|
||||
- Panel builders call `nameScreens`.
|
||||
- `src/embed.ts`: name hashes added to `ROUTE_HASHES`.
|
||||
- `public/styles.css`: the stepper and name-field rules from the mockups.
|
||||
- `public/sw.js`: `PRECACHE` gains the new modules. `npm run build` rewrites the hash, and the regenerated `index.html`/`sw.js` are committed.
|
||||
- Open-in-app link, one function `openInAppUrl(code, label)` → `simplex:/name#code=<display code>&label=<label>`. **Proposal only**: the app does not handle it yet and needs the app team's confirmation.
|
||||
- Tests (node:test, existing style): `names.test.ts`, name-code vectors, api parsers, flow checkout for names, screens structure, main-level name wizard (the existing `main-*.test.ts` patterns), sw precache tripwire.
|
||||
|
||||
### 5. Mock (`web/mock/server.py`)
|
||||
|
||||
- `GET /api/name/<label>`: same validation. `registered` for a fixed set (e.g. `simplex`, `dynamis`); `reserved` for e.g. `support1`; `POST /control/name/<label>/<status>` overrides one, to exercise races and the resolver-down state.
|
||||
- `POST /api/invoice` with `kind:"name"`: mirrors the name catalog; invoices behave as today, including all `/control/*` payment controls.
|
||||
- README "Running the mock" gains the name endpoints.
|
||||
|
||||
## Critical files
|
||||
|
||||
- Core: `src/Simplex/Chat/Badges/Code.hs`, `src/Simplex/Chat/Names.hs`, `src/Simplex/Chat/Badges/Service.hs` (→ `ShopService.hs`), `tests/BadgeTests.hs`
|
||||
- Service: `Config.hs`, `Catalog.hs`, `Store.hs`, `Store/Invoices.hs`, `Store/{SQLite,Postgres}/Migrations.hs`, `Web/Server.hs`, `Service.hs`, `Group/Command.hs`, `README.md`, `badge_service.ini.example`; tests under `tests/Bots/BadgeService/` (→ ShopService), with a fake resolver beside the fake Greenfield in `WebTests.hs`
|
||||
- Web: `src/{names,nameScreens}.ts` (new), `src/{codes,api,domain,store,flow,order,main,embed,screens}.ts`, `public/{styles.css,sw.js,index.html}`, `mock/server.py`, `README.md`
|
||||
- `GET /api/name/<label>` with fixed available, taken (with entities and images) and reserved names, and per-name prices.
|
||||
- Name and name-code invoices.
|
||||
- `POST /control/registration/<id>/<phase>` to step a registration through committing, waiting, registering and registered, or into taken or failed. It also exposes the existing payment controls.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Name redemption and the chain client (#7530).
|
||||
- In-app purchase.
|
||||
- The app's handling of the open-in-app link.
|
||||
- Renewals.
|
||||
- Fixing the stale "development stand-in" docs found earlier; a separate change.
|
||||
- The app and core side of claiming and redemption (#7530).
|
||||
- Funding the registrar wallet (operations).
|
||||
- Renewals, transfers between users, in-app purchase.
|
||||
|
||||
## Verification
|
||||
|
||||
- Haskell: `cabal build exe:simplex-shop-service` and the test filter for the service, SQLite and Postgres builds. Covers name code vectors, `validNameLabel`, the migration up/down with referencing rows, catalog pricing, `/api/name` against a fake resolver (available/registered/reserved/down/invalid), and name invoice → settle → code row paid with kind, min_length and years. Badge tests unchanged, wire tags pinned.
|
||||
- Web: `npm run build` (hash committed) and `npm test`, all green.
|
||||
- End to end on the mock with Playwright Chromium:
|
||||
1. landing → name → years (stepper to 3) → checkout → BTC → partial → confirming → settle → N4 code
|
||||
2. card path
|
||||
3. taken, reserved and resolver-down names
|
||||
4. history row
|
||||
- Screenshots in light, dark and phone, compared against the stage-0 mockups.
|
||||
- Kotlin compile is not needed: wire tags are unchanged.
|
||||
- **Haskell:** `cabal build exe:simplex-store-service` and the service and core test filters, on SQLite and Postgres. Covers:
|
||||
- code vectors, `validNameLabel`, the migration up/down;
|
||||
- `/api/name` against a fake resolver and fake link profiles;
|
||||
- the registration worker against a fake JSON-RPC node (commit, wait, register, a revert to taken, a restart mid-phase);
|
||||
- `claimName` (records, then transfer);
|
||||
- badges unchanged, with wire tags pinned.
|
||||
- **Web:** `npm run build` (both sites, hash committed) and `npm test`.
|
||||
- **Docker:** `docker compose build` in `scripts/store-service/`, and a run against local Postgres that exports both sites.
|
||||
- **End to end on the mock with Playwright:**
|
||||
1. search → available → Bitcoin payment → steps → registered
|
||||
2. taken with entity cards, reserved, invalid
|
||||
3. taken during registration → another name
|
||||
4. the secondary code flow
|
||||
5. card
|
||||
6. both sites' cross-links
|
||||
|
||||
Light, dark and phone, compared against the stage-0 board.
|
||||
Reference in new issue
Block a user