Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
- **The name status no longer contradicts the modal it opens.** The Owned Names table printed the auction phase while the modal printed the task, so a row reading "Closed" opened a modal headed "Won — Register Now". The table now shows the same summary the auctions list and the modal do, and defers to what is in flight when a transaction of yours is waiting for a block. The phase was nearly a constant down that column anyway: every name you own has a closed auction.
- **Reclaiming your own losing bids is no longer described as losing.** Outbidding yourself and winning leaves you owning the name and holding your own losing reveals — the ordinary outcome of bidding more than once. The wallet greeted it with a red "Lost — Redeem Now" on a name just registered. The state was right; only its description was written for the other way into it.
- **Broadcasting your own transaction no longer reads as losing the name.** hsd drops a coin from its unspent set as soon as a mempool transaction spends it, so the instant a register or transfer went out the wallet stopped finding the owner coin and concluded it did not own the name — which, on a closed auction still holding reveals, is the shape of a loss. Ownership now survives an unconfirmed spend of your own; acting on the name stays blocked until the block lands, which was the half that was already right.
- **Finalize now waits out the transfer lockup instead of failing.** Handshake refuses a finalize until the transfer has been locked for a set number of blocks — two days on mainnet — and the button went live the moment the transfer was mined. It now says how many blocks are left, and only unlocks when the next block could actually carry it.
- **A pending transfer is now the name's status, not a phase that never arrives.** Handshake has no TRANSFER state: a name being transferred stays CLOSED and signals the transfer in a separate field, so the status was derived from a string the node never sends and every transferring name read "Owned" — while the panel below it said "Transfer in progress". The status now reads what the node actually reports, and sits behind the renewal alarm but ahead of everything quiet.
- **Renew no longer cancels a transfer, and a second transfer is no longer offered.** Handshake's renew clears a pending transfer exactly as an update does, so "extend my registration" ended a transfer in flight without mentioning transfers; it is refused while one is pending, and says why. Transfer itself was offered on a name already being transferred, which the node rejects outright — a transfer coin can only become an update, renew, finalize or revoke.
- **A reclaimed lockup counts as spendable money again.** Redeeming a losing bid returns ordinary HNS, but the coin it lands on was classified with the name covenants, so the balance card did not show it as spendable and coin selection would not draw on it. You paid a fee to get it back and it stayed invisible. It follows Handshake's own rule now — a redeemed coin spends like any other output.
- **The confirm dialog counts every output a name action carries.** It reported the first one, which is right for an action with a single output and wrong for the two that matter: revealing a name you bid on more than once, and redeeming the bids that lost. A redeem of three reveals worth 28 HNS offered 12 on the dialog — the one figure you check before signing.
- **The paid-swap buttons are gone.** "Sell with payment" and "Buy with payment" offered a trade the code could not make. Finalizing a transfer spends the coin the TRANSFER created, and that coin stays at the seller's address — so only the seller can finalize, and the button labelled "Buy" could never be pressed by a buyer. Nor was anything atomic: every input is signed with a flag that forbids a counterparty from completing the transaction, so "pay and receive in one transaction" meant one wallet funding both halves of its own trade. Selling a name still works the ordinary way — agree a price, Transfer, Finalize — and an offer recorded before this change can still be claimed. What a real atomic swap needs is written down in `docs/specs/2026-09-21-paid-name-swaps.md`.
- **The name modal now shows the sections the current stage actually has.** Its advanced area was a flat catalogue of every verb the wallet has, filtered by one flag — "does this wallet own the name?" — and that flag is true during REVEAL for a wallet leading its own auction, because Handshake reports the highest revealer as the owner long before anyone has won. So a name you had only bid on offered DNS records, Ownership and Sign message, unfolded the section by itself without a click, and put a green "Owned by this wallet" badge above it. Each section is now live, still ahead (one muted line saying what unlocks it — "DNS records — after you register this name"), or not there at all. Register still lives in the records section, so a just-won name keeps its one button. Signing moved inside Ownership, where proving ownership belongs. The auction buttons say what they are: a manual fallback for when the guided panel has fallen out of step with the chain. While a transaction waits for a block nothing is offered at all, and the advanced toggle only appears when something behind it can actually be acted on — which removes the empty menu during OPENING and behind a node that cannot write, where every button repeated the reason already on the banner above.
- **Signing a message for a name now requires the name to be registered.** The signing key was resolved from whatever the name's owner record pointed at, with no check on what kind of coin that was — so during the reveal phase the wallet happily signed with its own reveal coin and returned a well-formed proof of ownership for a name nobody had won. Pasted into a verification flow it resolves as false, with nothing to explain why.
- **Editing DNS records can no longer cancel a transfer by accident.** Handshake accepts an UPDATE on a name that is mid-transfer, and that UPDATE *is* how a transfer is cancelled — so "Update" under DNS records was a way to lose a transfer in flight while saying nothing about transfers. It is refused while a transfer is pending, and says so; Cancel transfer remains, under the name that means it. Its sibling was wrong the other way: Cancel transfer was live on every registered name and built an update that changes nothing and costs a fee. It now requires a transfer to cancel.
Expand Down
88 changes: 88 additions & 0 deletions docs/specs/2026-09-21-paid-name-swaps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Paid name swaps

**Status: not implemented. The UI entry points were withdrawn on
2026-09-21; the backend commands remain, and an offer already recorded can
still be claimed.**

## 1. Summary

Selling a Handshake name for HNS should be one transaction: the buyer pays and
the name moves, or neither happens. The wallet shipped two buttons — "Sell with
payment" and "Buy with payment" — and a seller-side offer record, and the shape
they implement cannot do that. This spec says what was there, why it could not
work, and what a real implementation needs, so the next attempt starts from the
consensus rules rather than from the buttons.

## 2. Terms

- **Seller** — holds the name and its owner coin.
- **Buyer** — pays HNS and should end up holding the name.
- **TRANSFER coin** — the output a TRANSFER covenant creates. It carries the
recipient inside the covenant (`items[2]` = address version, `items[3]` =
address hash) and **stays at the seller's own address**: hsd requires
REGISTER → TRANSFER to keep the address (`rules.verifyCovenants`).
- **Atomic** — one transaction that either performs both halves or is invalid.

## 3. Why the withdrawn shape could not work

**W1 — Only the seller can finalize.** FINALIZE spends the TRANSFER coin, and
that coin sits at the seller's address, so the seller signs it. The wallet
agrees: `build_finalize_with_payment_draft` resolves the owner coin through
`owner_coin_and_state` and fails without it. So the button labelled "Buy with
payment" could only ever be pressed by the party selling — who has nobody to
pay.

**W2 — Nothing was atomic.** Every input in `noncustodial::actions` is signed
`sighash::ALL`. A counterparty cannot add inputs or outputs to a finished
transaction under that flag, so "finalize and pay in one transaction" means one
wallet funding both halves out of its own coins. There is no exchange.

**W3 — The claim verifies less than it says.** `claim_paid_transfer` documents
itself as checking "a P2WPKH output to the seller's address with value >=
price". `find_payment_output` works by exclusion instead: any output **not** at
the buyer's address, worth at least the price, counts. It cannot check the
seller's address because `paid_swap_offers` never records one. A payment to any
third party satisfies it. Not exploitable on its own — the seller supplies the
txid — but "verified" overstates the evidence.

## 4. What a real implementation needs

**R1 — Swap sighash flags.** The seller pre-signs the FINALIZE with a sighash
type that leaves room for the buyer to add their payment: this is what
Shakedex does (`SINGLEREVERSE` + `ANYONECANPAY`). `noncustodial::tx::sighash`
would need those variants, and the signer would need to be willing to produce
them — a wallet that signs `ANYONECANPAY` is signing something a stranger can
complete, which is a decision to make deliberately, not a flag to add quietly.

**R2 — An offer is a signed artefact, not a DB row.** What the seller publishes
must be the pre-signed input plus the price, so a buyer can verify and complete
it without trusting the seller's wallet. The current `paid_swap_offers` table
is local bookkeeping and cannot travel.

**R3 — The seller's payout address is part of the offer.** Without it no check
can answer "was I paid" (W3).

**R4 — The buyer's side is a fill, not a finalize.** The buyer takes the
seller's pre-signed transaction, adds funding inputs and the payment output,
and broadcasts. There is no separate "finalize with payment" command for them.

## 5. Explicitly not enforced

- Nothing stops a seller and buyer arranging payment off-chain and using a
plain Transfer + Finalize. That works today and is what the wallet supports.
- Removing the UI does not remove the backend commands
(`create_paid_swap_offer`, `claim_paid_transfer`,
`build_finalize_with_payment_draft`). They stay so an offer recorded before
this change can still be claimed through `PaidSwapClaim`, which renders only
when one exists.

## 6. Pointers

- `src/components/name-actions/OwnershipActions.tsx` — where the two buttons
and their forms were.
- `src/components/name-actions/PaidSwapClaim.tsx` — the claim panel, kept.
- `src-tauri/src/commands/paid_swaps.rs` — offer records and `find_payment_output`.
- `src-tauri/src/commands/names.rs::build_finalize_with_payment_draft` — W1.
- `src-tauri/src/noncustodial/actions.rs` — the `sighash::ALL` of W2.
- `name-modal-sections.test.tsx :: offers no way to start a paid swap` — pins
the withdrawal.
1 change: 1 addition & 0 deletions docs/specs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ Each spec has these sections, in this order:
| [2026-09-11 Remote-node connection & broadcast guard](./2026-09-11-remote-node-connection-and-broadcast-guard.md) | Implemented on `feat/spv-broadcast-guard-and-remote-node-onboarding` |
| [2026-09-14 Network-derived behaviour](./2026-09-14-network-derived-behaviour.md) | Implemented on `feat/batch-transfer` |
| [2026-09-20 Multiple bids per name](./2026-09-20-multiple-bids-per-name.md) | Implemented on `feat/batch-reveal-redeem-finalize-ui` |
| [2026-09-21 Paid name swaps](./2026-09-21-paid-name-swaps.md) | Not implemented — UI withdrawn, see spec |
Loading
Loading