Skip to content

feat(shakedex): Finalize & sign the price, and follow the listing to a mined FINALIZE - #87

Open
DimazzzZ wants to merge 17 commits into
feat/shakedex-sell-T2from
feat/shakedex-sell-T3
Open

DimazzzZ wants to merge 17 commits into
feat/shakedex-sell-T2from
feat/shakedex-sell-T3

Conversation

@DimazzzZ

@DimazzzZ DimazzzZ commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Stacked on #86; merge after it.

Fourth PR of the Shakedex selling stack (spec docs/specs/2026-10-05-shakedex-name-sales.md, ticket T3): Finalize & sign — the FINALIZE of a locked name into its lock and the price signed in one unlock — the listing file, the daily reminder, and the listing's states from the lockup to a mined FINALIZE, followed from the chain. A real shakedex CLI buys a Namehold listing on a live node. The UI comes in T7.

What changes

  • Finalize & sign (R18, R19, R20, R31). shakedex_finalize_and_sign re-checks everything on the profile's node before anything is signed: the listing belongs to the active profile and is ReadyToFinalize; no Cancel transfer of it can still land; the lock TRANSFER is the name's owner and the lockup has passed (from hsd's info.transfer, judged at the next block); R31 with no lockup left; the confirmed TRANSFER's covenant (from hsd's coin, not our copy) commits to the lock address re-derived from the seed, and the re-derived key equals the stored one. The price is parsed exactly (refusing 0, dust, above the money supply, more than 6 decimals). A secure prompt (R20) lists the FINALIZE fee, the price, when it becomes valid, the payment and lock addresses and that each signature lets anyone buy until a cancel is mined. Only after it is confirmed, in one hold of the signer, the FINALIZE is signed and the Buy Now step is signed over the FINALIZE's lock output (its txid, index and value) with lock time = node median time − 512 s. The signed draft, the Finalizing state, the step, the listing file and expiresAt (median time + 365 days) are written in one transaction. The command sends nothing; the existing broadcast sends the FINALIZE and re-checks R31 first.
  • Listing file (R23). Written as Shakedex v2 JSON with the CLI's field set, read back by our own strict parser before it is stored; refuses a market fee. shakedex_export_listing_file returns the stored file byte for byte, only once the FINALIZE is mined.
  • States from the chain (R19). Read-only jobs in the app and the daemon: Locking → ReadyToFinalize when the lockup passes (and back on a reorg); Finalizing → Listed when the FINALIZE's lock output is a mined coin at our lock address, back to Finalizing on a reorg; a dead FINALIZE returns the listing to ReadyToFinalize once the lock TRANSFER is a coin again. A FINALIZE into our lock from another device becomes a Restored lock, never Aborted; when something else spent the lock TRANSFER, the listing settles as Aborted, Expired or Restored by what hsd reports. An owner coin that hsd answers 404 for (spent in the mempool) gives no verdict.
  • Reminder (R19). listing_ready: once a day while a listing is ready to Finalize & sign, or its FINALIZE is signed but not sent.
  • Storage. Migration 035 (lock_finalize_draft_id); guarded state transitions; deleting an unsent FINALIZE draft returns its own listing to ReadyToFinalize.

Checks

  • Unit: cargo fmt --check, cargo clippy --locked --all-targets -D warnings, cargo test --locked 3120 passed / 0 failed, pnpm test, prettier on tracked files — all pass.
  • Live (regtest, local only): full live_ suite with HNS_IT_SHAKEDEX=1 and the second node without --index-tx — 107 passed, 0 failed, 0 skipped (chain 0 → 11994). New: shakedex_namehold_buy_now_is_bought_by_the_cli (lock → lockup → Finalize & sign → FINALIZE mined → Listed → export → the shakedex CLI fills it; hsd shows the name's TRANSFER committing to the CLI wallet's address and our payment address paid exactly the price), shakedex_lock_finalize_pays_its_fee_rate_on_vsize, a reorg of the FINALIZE's block (Listed → Finalizing → Listed), and a FINALIZE from a second copy of the wallet adopted as a Restored lock.
  • Mutations (each made, the named test failed, file restored, git diff clean), among them live ones: steps signed over the lock TRANSFER instead of the FINALIZE output (the CLI refuses the file), the price paid to the cancel address (payment 0 instead of the price), ready one block early, the lock witness under-sized (hsd's rate below the asked rate); and unit ones for every gate of Finalize & sign, the price parser, the listing-file writer, each state transition and its SQL guard, the reminder, the R31 re-check at send, the export gate and the profile filter.
  • Code review (standards and spec axes) done; findings fixed in this branch.

Not checked here: the settle path for a Finalizing listing whose FINALIZE died while something else spent the lock TRANSFER is covered by unit tests against hsd-shaped replies only, not live; a confirmed FINALIZE that is then purchased (Sold) is T4; cancel and lower price are T5; publishing is T6; the UI is T7.

…clear the cancel link when a lock is adopted
…R31)

prepare_lock_finalize runs every gate of Finalize & sign and builds the FINALIZE into the lock without writing or asking anything: the Shakedex profile and node gates (no experimental flag, it acts on an existing listing), the unlocked signer before any read, a ReadyToFinalize Buy Now listing of the active profile, no live Cancel transfer linked to the listing (both spend the same TRANSFER coin), exactly one price, the name owned by an unrevoked (lock transfer, 0) on the node, that coin mined and unspent, the lockup over at tip + 1, R31 with the remaining lockup, and R18 against the covenant hsd reports for the coin, compared with the lock address of the key re-derived after the reads. Every node field read is matched on its own and is "could not check" when missing.

The two signer blocks of shakedex_build_lock_draft become authorize_signer and derive_listing_key, shared by both commands.

Gates: cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test --locked (3083 + 3 passed); 22 mutations, each caught.
…ck TRANSFER that is not the owner as no verdict
… and a reason for each ended listing

Finalize & sign and the sync job now ask whether the lock TRANSFER owns the name through one helper (`sell::lock_transfer_owns_name`) and take the lockup's start from the same fact, hsd's `info.transfer` (`sell::transfer_height`), the field hsd's FINALIZE rule reads; Finalize & sign no longer reads the TRANSFER coin's height. Finalize & sign and the listing file export refuse an Aborted, Expired, Cancelled, Sold or Restored listing with that state's own reason, and the export is allowed only in the named `ListingState::EXPORT_ALLOWED` states.

Shared pieces: the profile network lives in `db::queries::profile_network` (re-exported as `active_profile::profile_network_from_conn`), the draft-status predicates are two named rules (`draft_alive`, `draft_may_still_land`), the selling commands take the signer through one helper, and the jobs' listing sets are named consts with a test that they are disjoint and hold no Restored listing.
…NSFER to something else

A Finalizing listing whose FINALIZE draft was failed, dropped or deleted, and whose lock TRANSFER had been spent by something other than that FINALIZE (an older Cancel transfer mined later, another device's FINALIZE into our lock, a REVOKE), stayed Finalizing for ever: its lock coin and its lock TRANSFER were both hsd's 404 and no job read it again.

The finalize job now settles it with the same code as the before-lock job (`settle_left_lock`): `info: null` makes it Expired, the owner coin a FINALIZE at our lock address a Restored lock with that outpoint, the owner coin readable elsewhere or the name revoked Aborted, and the owner coin's 404 is no verdict. The Aborted, Expired and Restored writes accept a Finalizing listing only while its FINALIZE draft is dead, and drop that FINALIZE's outpoint, steps and file, so a reorg that relocks the listing does not carry them. The jobs' listing sets stay disjoint.
…t, and judge R31 again when it is

A listing whose FINALIZE was signed but never sent stopped getting its daily reminder the moment Finalize & sign ran, though nothing was locked in. The `listing_ready` reminder now also lists a Finalizing listing whose FINALIZE draft is `draft` or `signed`, and reads "Ready to finalize" / "{name}: the transfer lockup is over — Finalize & sign to lock it in and set its price".

Sending a `shakedex_lock_finalize` draft runs R31 again from the node's tip with no lockup left, as Finalize & sign judged it (`draft_ctx::recheck_lock_expiry` with `LockSend::Finalize`); a refusal sends nothing and keeps the signed draft and the Finalizing listing. The lock TRANSFER keeps its own recheck, which discards its unsent draft. A shell-level test pins that the disabled setting silences the listing reminder.
…'s txid

Finalize & sign refuses a listing whose stored lock public key is not the re-derived one even when the TRANSFER commits to the derived lock (dropping that half of the check fails the test). The end-to-end Finalize & sign test now decodes the signed FINALIZE and checks that its txid is the listing's lock txid and the listing file's lockingTxHash.
…from another device on a live node

shakedex_listing_follows_reorg_of_its_lock_finalize invalidates the block holding a Listed listing's FINALIZE: while hsd has it nowhere the listing stays Listed, handed back to the node it is a mempool coin and the listing Finalizing, mined again Listed, each step read from hsd first. shakedex_finalize_from_another_device_is_a_restored_lock Finalize & signs from a copy of the wallet's database (another device with the same recovery phrase) and mines the FINALIZE; the first wallet's ReadyToFinalize listing becomes a Restored lock with the outpoint hsd reports as the name's owner. The CLI test no longer compares the file it wrote for the CLI with itself.
…read, and type the listing state for the UI

SECURITY.md section 5 and spec R20 now say that Finalize & sign makes one 0x84 signature before its prompt, the R18 self-check's over a template with a zero outpoint, verified and dropped, and that the median time for the lock time is read just before the prompt, not after; R19 and the buy_now_lock_time doc say the same. SECURITY.md also lists the export refusals for a listing that is over. ShakedexListingSummary.state is the union of the 13 camelCase ListingState spellings, pinned against Rust by shakedex_sell_tests::ts_listing_state_union_lists_every_state; spec R19 names the two new live tests. The 035 migration comment names the lock output's index instead of output 0.
@github-actions github-actions Bot added domain: wallet Balances, send/receive, accounts, HD derivation, coin selection. domain: names Auction lifecycle, batch ops, watchlist, paid swaps, lost-bid recovery. domain: sync Background sync daemon, auto-sync, cross-process DB locks. domain: security Secure window, encrypted vault, signing engine, secret handling. domain: infra CI/CD, build, sidecar packaging, updater, DB migrations, deps. labels Oct 9, 2026
@DimazzzZ DimazzzZ self-assigned this Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain: infra CI/CD, build, sidecar packaging, updater, DB migrations, deps. domain: names Auction lifecycle, batch ops, watchlist, paid swaps, lost-bid recovery. domain: security Secure window, encrypted vault, signing engine, secret handling. domain: sync Background sync daemon, auto-sync, cross-process DB locks. domain: wallet Balances, send/receive, accounts, HD derivation, coin selection.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant