Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
94b5836
fix(explorer): a seeded mainnet URL is not a choice the user made
DimazzzZ Sep 24, 2026
dd90548
fix(sync): an unreadable profile network is unknown, not mainnet
DimazzzZ Sep 24, 2026
ff0f1a7
fix(names): a failed lookup is not the same as nothing to reveal
DimazzzZ Sep 24, 2026
db2aa8d
fix(names): the rest of the context builder says when a lookup failed
DimazzzZ Sep 24, 2026
147aef7
fix(node): finish routing node config through the profile (ADR-001, S…
DimazzzZ Sep 24, 2026
9dd98b8
fix(bridge): the TS mirror stops making required fields optional
DimazzzZ Sep 24, 2026
14c4ba4
test: one lock per piece of global state, and stop excluding testable…
DimazzzZ Sep 24, 2026
4af5e58
refactor: delete the parts nothing calls
DimazzzZ Sep 24, 2026
dcb766d
docs: make the comments say what the code beside them does
DimazzzZ Sep 24, 2026
5ecea83
docs: stop the manual promising what the wallet does not do
DimazzzZ Sep 24, 2026
1bfbd04
docs(specs): write down the behaviour that shipped without a spec
DimazzzZ Sep 24, 2026
145d10e
refactor: one home per shared thing
DimazzzZ Sep 24, 2026
c33c393
refactor: complete fixtures, and one copy of what was written three t…
DimazzzZ Sep 24, 2026
91c8e94
refactor: one spelling per rule, and a guard where two are needed
DimazzzZ Sep 24, 2026
bdd74e1
fix(node): the directory a node writes into is not a thing to guess
DimazzzZ Sep 24, 2026
0456db7
refactor: thirteen positional arguments, six of them bool
DimazzzZ Sep 24, 2026
c7f7818
fix(read): an unreadable network picks no explorer, not mainnet's
DimazzzZ Sep 24, 2026
4798d9a
test(node): pin what "hsd failed to start" actually matches
DimazzzZ Sep 24, 2026
c60447e
docs(specs): record the node behaviour #56 shipped without one
DimazzzZ Sep 24, 2026
55fd143
refactor(names): name the shapes, share the sentence
DimazzzZ Sep 24, 2026
5855044
refactor(ui): a function that can answer should answer
DimazzzZ Sep 24, 2026
7c66925
fix(sync): a network this step cannot read is not a node to trust
DimazzzZ Sep 24, 2026
5fe7823
fix(rust): a DB failure in a user command is an error, and one owner …
DimazzzZ Sep 24, 2026
4e04db8
test: pin the foreign-chain sync refusal and the live-tip preference
DimazzzZ Sep 24, 2026
7cb3f8b
fix(ui): one explorer rule, one batch runner, and the amount the batc…
DimazzzZ Sep 24, 2026
227a372
docs: say what the code does, and point where it lives
DimazzzZ Sep 24, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ jobs:
- name: Lint secure window imports
run: pnpm lint:secure-imports

- name: Lint native title attributes
run: pnpm lint:native-title

- name: Format check
run: pnpm lint:format

Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,18 @@

### Added
- **Several bids on one name.** The wallet allowed a single bid per name per wallet; it now allows as many as you like, each with its own value and lockup, the way Namebase does. Every bid rotates to a fresh receive address, so each gets its own nonce, blind, commitment row and BID coin, and the send-side guards that refused a second one are gone. The name modal's header reads "Latest bid … · lockup … · N of yours", and the bids panel keeps every bid in one list with your own rows tinted, so which are yours is visible without counting.
- **Remote-node onboarding** — the first-run flow now opens with a "How do you want to connect?" step offering three choices: **Local full node** (default; start hsd on this device), **Remote node** (point at an existing hsd RPC, with a "Test connection" button that probes the node before you commit), and **SPV** (lightweight headers-only, read-only). Choosing a source persists `chain_source` + `node_mode` (plus `node_rpc_url` / API key for Remote node) up front so a new user reaches a working read+send wallet without waiting for a full local sync. Your recovery phrase never leaves the device — remote/SPV is a privacy/trust tradeoff, not custody. New Tauri command `check_node_connection` validates a candidate RPC (reachable / height / synced, and — once a wallet profile exists — whether the node's network matches the wallet's; a cross-network node, e.g. testnet-for-mainnet, is flagged with an amber warning under the "Connected" line and is not treated as a usable node) without persisting anything, honoring the existing plaintext-key / non-loopback transport guard. Settings' Chain source selector now offers the same four modes (local full / SPV / remote / explorer) and replaces the separate Node mode dropdown; Settings also gained the same "Test connection" affordance and an "Allow sending via remote node" toggle (`allow_remote_broadcast`, off by default) — the toggle appears both on the onboarding Remote step and in Settings.
- **Remote-node onboarding** — the first-run flow now opens with a "How do you want to connect?" step offering three choices: **Local full node** (default; start hsd on this device), **Remote node** (point at an existing hsd RPC, with a "Test connection" button that probes the node before you commit), and **SPV** (lightweight headers-only, read-only). Choosing a source persists `chain_source` + `node_mode` (plus `node_rpc_url` / API key for Remote node) up front so a new user reaches a working read+send wallet without waiting for a full local sync. Your recovery phrase never leaves the device — remote/SPV is a privacy/trust tradeoff, not custody. New Tauri command `check_node_connection` validates a candidate RPC (reachable / height / synced, and whether the node's network matches the wallet's — during onboarding, the network you picked on the previous step; a cross-network node, e.g. testnet-for-mainnet, is flagged with an amber warning under the "Connected" line and is not treated as a usable node) without persisting anything, honoring the existing plaintext-key / non-loopback transport guard. Settings' Chain source selector now offers the same four modes (local full / SPV / remote / explorer) and replaces the separate Node mode dropdown; Settings also gained the same "Test connection" affordance and an "Allow sending via remote node" toggle (`allow_remote_broadcast`, off by default) — the toggle appears both on the onboarding Remote step and in Settings.

### Fixed
- **The batch confirmation states the amount it is about to sign.** It showed the count and the fee and left out the one figure that changes with the batch: what the transaction moves, summed over every output except change. A batch reveal or redeem carries one output per bid, so that line is where you see how much is in play.
- **Redeeming no longer builds a transaction the node would reject when the wallet cannot read its own records.** The redeem builder keeps the winning reveal out of the transaction, because Handshake refuses to redeem the coin that owns the name; if the lookup that finds that coin failed, the builder carried on without the filter. It now reports the failure instead. In the same spirit, the watched-name alerts and the readiness check behind local reads no longer treat "could not read which network this wallet is on" as "nothing to compare", which let a node on another chain count as ready.
- **Name reads no longer fall back to the mainnet explorer when they cannot tell which chain you are on.** Looking up a name, its bids or its DNS records picked an explorer from a network that quietly answered "mainnet" whenever it could not be read, so a testnet or regtest wallet could be shown a mainnet name's auction phase, someone else's bids and someone else's records. Each of those reads now treats an unreadable network as unknown and serves nothing rather than another chain's data. Looking up bids also asks the profile it was given rather than whichever profile happens to be selected.
- **Re-syncing the chain asks which network it is about to move.** The one-click re-sync stops the node, moves the current chain data to a timestamped backup and starts a fresh sync. It worked out the directory and the network with two separate reads, either of which quietly answered "mainnet" when it could not tell — so with no wallet selected it would back up, and then re-sync over, a directory belonging to a network you are not on. It now resolves the network once and refuses when there is none, the way starting a node already did.
- **The manual and README no longer advertise paid name swaps.** The two buttons were withdrawn because the shape they implemented could not be atomic and could only be pressed by the party with nobody to pay, but the manual still walked you through using them, the README still promised that neither party could renege, and the QA checklist still looked for them. All three now say the feature is gone and what remains: an offer recorded earlier can still be claimed. The manual also had a note admitting its own batch-operations section was inaccurate; the section now says what the wallet does instead.
- **The guided panel no longer tells you a name allows one bid each.** Bidding a second time on a name has been allowed for a while, but a panel left over from the old rule still said otherwise, and it could only appear in a state the wallet stopped producing at the same time. The bid form now stays offered for the whole bidding window, however many bids you already hold. Settings also reads the node's sync verdict from the backend instead of working it out a second time, so the label and what reads actually do cannot disagree.
- **The About link in the sidebar has a name again for screen readers.** It is an icon with no text, so its old `title` was also its accessible name; converting the sidebar's hints to tooltips took the `title` away without putting an `aria-label` in its place, leaving the link announced as nothing. The keyboard-shortcuts button beside it already carried one.
- **Each wallet profile now really uses its own node.** Per-profile node configuration was stored and resolved, but several paths still asked the global settings: the node the app started took the global api-key, the background sync decided whether "the node" was caught up by asking the global one while every step it gated talked to the profile's, and a sync failure named the global URL in a message telling you to go fix it. Worse, a profile whose own node configuration would not resolve was quietly served the global node instead of being reported as misconfigured, so a profile pinned to a regtest node could be answered by a mainnet one. Each of those now resolves through the profile, and an unresolvable profile configuration is an error rather than somebody else's node. The automatic repair of a stale loopback port also stops rewriting a URL you set on the profile yourself.
- **A testnet or regtest wallet no longer reads from the mainnet explorer.** An early migration seeded `explorer_api_url` with the mainnet explorer, and a later change swapped that seed for an empty string so the wallet could pick an explorer per the profile's network. The swap only helped databases created after it: every installation that had already run the original kept the mainnet URL, and an explicit setting outranks the network default, so a testnet or regtest profile went on querying mainnet and getting confident answers about a chain it was not on. A new migration removes exactly the value the old one seeded, leaving an explorer URL you chose yourself alone, and the explorer factory now refuses the known mainnet explorer off mainnet in case a database reaches it before the migration has run.
- **Reveal is no longer offered on a name with nothing left to reveal.** A lockup stranded in an auction that lapsed is an unspent BID coin, and the Reveal button was gated on holding one of those anywhere in the wallet rather than in the auction being looked at. So on a fully revealed name the button stayed live for good and failed every time with "no unspent bid coin". It now reads the same set the reveal transaction is built from, so the button and the builder cannot disagree.
- **Registering no longer looks as though DNS records are required.** The Register step put a record editor in front of the user and said nothing about it. Records are optional — Handshake accepts an empty resource, and the wallet was already sending one when the editor was untouched — so the panel now says registering claims the name and records can follow with Update, and keeps the editor behind "Add DNS records now (optional)". Register also stopped appearing twice, live in both the guided step and the records section, and the manual auction actions now show only what the stage allows, each with a line saying what pressing it does — Redeem names the amount it reclaims.
- **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.
Expand Down
9 changes: 5 additions & 4 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ _Avoid_: Sync mode
The tuple `(node_rpc_url, node_rpc_api_key, chain_source)` that tells the wallet how to reach a node. Can be global (applies to all profiles) or per-profile (applies only to that profile). Per-profile overrides the global.
_Avoid_: Connection settings (too vague)

**Per-slot per-profile override**:
A per-profile override of a single slot's source that takes precedence over the global default for that slot. A profile resolves two independent slots — the read slot and the write (send) slot — and each carries its own override flag. A profile may override its read slot (e.g. read via explorer) while inheriting the write slot from global, or vice versa. Stored in `profile_settings` table. If an override is set and invalid (unreachable, mismatched network), it is a configuration error for that slot, not a fallback to global.
_Avoid_: Per-profile override (imprecise now that override is per-slot), profile-specific node, profile node setting
**Per-profile override**:
A per-profile choice of node configuration that takes precedence over the global default. Stored in the `profile_settings` table, one row per key. If an override is set and invalid (unreachable, mismatched network), it is a configuration error for that profile, not a fallback to global.
_Planned, not built_: splitting this per *slot*, so a profile could override its read slot (e.g. read via explorer) while inheriting the write slot from global. The table has no slot column and resolution returns one tuple; see step 8 of the [per-profile node spec](./docs/specs/2026-09-15-per-profile-node-banner-and-preflight.md). Until then "per-slot per-profile override" names something the wallet does not do.
_Avoid_: Profile-specific node, profile node setting

**Preflight**:
A check performed before an operation (sync, read, broadcast) to ensure the node is reachable and on the correct network for the active profile. Returns a status (ready, missing, misconfigured) and optionally a suggested fix.
Expand All @@ -41,5 +42,5 @@ The node's reported chain (from `getblockchaininfo`) does not match the profile'
_Avoid_: Chain mismatch (same thing, but "network" is the profile's term)

**Effective node config**:
The resolved sources for a profile after applying the resolution order per slot: per-slot per-profile override (if set) → global settings (if set) → built-in default. Resolution runs independently for the read slot and the write (send) slot, so the effective read source and effective write source can differ (e.g. read via explorer, write via a local node). Each resolved slot is a `(node_rpc_url, node_rpc_api_key, chain_source)` tuple; the write slot may resolve to none when no node is available to send through.
The resolved node configuration for a profile after applying the resolution order, evaluated independently per key: per-profile override (if set and non-empty) → global settings (if set) → built-in default. The result is one `(node_rpc_url, node_rpc_api_key, chain_source)` tuple, plus whether the profile's own override supplied it — which is what keeps the loopback-port realign off a URL the user chose. A profile that does not resolve is an error, never a fallback to global.
_Avoid_: Resolved config (same meaning, but "effective" emphasizes the resolution order)
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,15 @@ Built with Tauri v2, React + TypeScript, Rust, and SQLite.
- **Name watchlist** — track names you don't own for monitoring. Watchlist page
in the sidebar with add/remove, tags, CSV import/export, and "Add to
Watchlist" buttons in name modals.
- **Paid name swaps** — atomic buyer-seller name transfer with payment. The buyer
finalizes a TRANSFER and pays the seller in a single transaction
(finalizeWithPayment), so neither party can renege after the lockup expires.
- **Recover lost bids** — if you lose your local bid data (reinstall, seed-
restore, import from another wallet), Namehold can brute-force the bid value
from your seed and reveal it before the window closes. See
[`docs/RECOVER_LOST_BIDS.md`](docs/RECOVER_LOST_BIDS.md).

### Node-free reads
- Reads are **node-free by default** via the HNSFans explorer — no node required
just to view your wallet. When your local hsd is synced, the app automatically
just to view your wallet. Only mainnet has a built-in explorer; testnet and
regtest read node-free only with an explorer URL you configure. When your local hsd is synced, the app automatically
switches to **node-authoritative** reads (owned names, balances, bid history)
for faster, more reliable data. A local node is needed **only to send or
perform name actions**.
Expand Down Expand Up @@ -128,8 +126,8 @@ Built with Tauri v2, React + TypeScript, Rust, and SQLite.

## How it works

- **Reads are node-free.** Balances and names come from the explorer and are cached
locally per wallet. Links to transactions, names, and addresses open on Shakeshift.
- **Reads are node-free** on mainnet. Balances and names come from the explorer
and are cached locally per wallet; other networks need an explorer URL of yours. Links to transactions, names, and addresses open on Shakeshift.
- **Sending needs a node.** Broadcasting and coin/owner discovery use a local
**hsd** node over RPC. The app can start/stop hsd for you (Settings → Connections).
- **Secrets stay in a secure window.** Your mnemonic/passphrase is only ever typed
Expand Down
Loading
Loading