Repository navigation
Build the trusted XCHandles foundation and verified resolver - #919
Merged
Merged
Conversation
Constants are taken from the registry launcher solutions on chain and checked against them in tests. Custom networks only inherit a built-in deployment when their genesis challenge matches.
- Handle parsing and hashing, and a typed client for the listener API - Verify each lookup against the chain: rebuild the slot from the registry lineage, require it to be confirmed and unspent, and derive the resolved address from the resolved singleton's latest spend - Re-fetch when the slot was spent by neighbor activity - Store verified mappings per registry, with change tracking, offline fallback, and invalidation on rejection or reorg
Resolves a handle through the verified resolver and store, and exposes it over RPC and Tauri. Also adds a build-time flag for XCHandles UI.
- Only a rejected answer or a not-registered handle marks a stored mapping unverified; rate limits, unparseable responses, bad config and missing resolution data fall back to the cache instead - Check expiration only after the slot is confirmed on chain - Don't let handle cache invalidation abort coin state processing - Re-read the stored mapping in the same transaction as the write, so concurrent last-sent and acknowledgement writes aren't lost - Share the block header fetch between the timestamp helpers - Release the Sage lock while resolving, via a new opt-in for endpoints that manage the lock themselves (endpoints-unlocked.json)
Releasing the lock needed a macro feature and a one-off endpoint signature for a lookup that normally takes well under a second. Instead, cap the whole lookup at 15 seconds and the chain tip walk-back at 16 blocks. A timed-out lookup is unavailable and falls back to the cache.
ChildKind returned any clawback-shaped memos on a DID transfer, even when the DID's puzzle wasn't committed to that clawback, so sync recorded a clawback puzzle for an ordinary transfer. NFTs, CATs and options already filter on the owner puzzle hash.
- Ignore the API's melted flag. A melt is still detected on chain, when the last coin is spent without creating a successor. - Step past a spent eve coin to its child, which can be parsed from the eve spend. - Refuse to verify an unspent eve coin, since the launcher spend doesn't reveal the owner. This doesn't downgrade a stored mapping. - Apply cargo fmt to earlier commits.
Since chia-sdk-driver 0.37.1, Did::parse_child takes the p2 puzzle hash from the hint without checking it, leaving that to the caller. Without the check, anyone spending a DID could name an arbitrary owner, which sync recorded and the XCHandles verifier would have resolved to.
CoinStateUpdate is sent on ordinary blocks and only when subscribed coins change, so invalidating on every one of them marked fresh mappings stale without a reorg, and missed reorgs that didn't touch the wallet's coins. NewPeakWallet is sent for every peak, and its fork point is below the previous peak only when blocks were rolled back.
- Treat a handle as expired once the local clock reaches its expiration, since the peak timestamp comes from a peer which may be behind. - Keep the rejection reason if marking the mapping unverified fails. - Map lookups that may succeed if retried to an internal error rather than a bad request. - Share one HTTP client between lookups so API connections are reused. - Fetch the chain tip at the same time as the first API response.
# Conflicts: # clippy.toml # crates/sage-config/Cargo.toml
- main added 0006_clawback_v1, so the XCHandles migration becomes 0007. - Import crate items through a single crate group, and group the test module with the other modules. - Format the API fixtures with Prettier.
- Running out of hops while following the resolved singleton no longer counts as unavailable. The singleton is known to have moved, so falling back to a cached owner could return an outdated address. - A peer's reorg only invalidates mappings verified against blocks up to that peer's previous peak, so a lagging peer can't mark mappings verified at the real tip as stale.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 283da99. Configure here.
A resolved singleton that moved too often to follow, or that is now an unspent eve coin, can't be the owner stored from an earlier verification. Leaving the mapping verified let a later outage return the old address.
This was referenced Oct 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Closes #870. Phase 1 of #867.
Adds the trusted XCHandles foundation: per-network deployment config, a typed client for the listener API, on-chain verification of every lookup, a verified mapping store, and a
resolve_handleendpoint. Recipient integration (#871), address display (#912), broader endpoints (#913) and NFT metadata (#911) are separate.Config
chia-wallet-sdk'saction-layerfeature (XCHandles drivers from 0.37.1, no Git dependency).Network.xchandles(registry launcher ID, price singleton launcher ID, precommit payout puzzle hash, relative block height, API URL). Networks pick it up throughinherit.Verification (
crates/sage-wallet/src/xchandles/)For every lookup:
ChildKindparser), following later spends if the API is behind. A singleton in a clawback is rejected.A slot spent by neighbour activity triggers a re-fetch (up to 3 attempts) rather than a rejection. API lookups pin
?launcher_id=<registry>. A whole lookup is capped at 15 seconds, and a timeout counts as unavailable.Store
Migration
0006_xchandles.sqladds anxchandlestable keyed by(registry_launcher_id, handle). The policy is documented at the top ofcache.rs:cached, unless it has expired.CoinStateUpdate.fork_heightnow marks rows above the fork point stale. Failures there are logged and don't block coin processing.Endpoint
resolve_handletakes a handle (with or without@) and returns aHandleRecord: address,status(verifiedorchanged),previous_address,last_sent_address,cached, and the proof and height fields. It's wired into RPC/OpenAPI, the Tauri command list and themain-hostpermissions. Not-registered handles return NotFound.There's also a
VITE_DISABLE_XCHANDLESbuild flag infeatures.ts. Nothing uses it yet.Testing
yakuhito(mainnet) andcloudwallet(testnet11) were rebuilt from the fixtures and confirmed to exist, unspent, through coinset. The yakuhito NFT parsed from its real parent spend to the owner the API reports.Notes for reviewers
clippy.toml(doc-valid-idents = ["XCHandles", ".."])SyncCommand::SwitchNetworknow carries aBox<Network>bindings.tsis regenerated, which reverts a whitespace-only hand edit from Add coin_ids to send_xch, send_cat, cancel_offer and cancel_offers #895Note
High Risk
Changes payment-adjacent resolution (wrong address = lost funds), persists trust decisions in SQLite, and touches sync/DID parsing; verification relies on a single peer like the rest of the wallet.
Overview
Adds XCHandles support so wallets can resolve
@handlenames to addresses with on-chain verification, not API trust alone.resolve_handleis exposed through RPC/OpenAPI and Tauri. It fetches index data from the configured listener API, verifies registry slot lineage and coin state via peers, follows the resolved NFT/DID to the current owner puzzle hash, and returns a richHandleRecord(includingverifiedvschanged, optional cached fallback, and proof metadata). Lookups are bounded by a 15s timeout; unsupported or custom networks without a matching genesis/inherit chain reject resolution.Per-network
XchandlesConfig(mainnet/testnet11 built-ins, optional override) pins registry constants checked against on-chain launcher solutions. A newxchandlesSQLite table caches verified mappings with status (verified/unverified/stale), change tracking, offline 24h reuse, and reorg invalidation when peer peaks roll back.Sync hardening:
NewPeakWalletcan mark handle rows stale after forks; DID sync parsing now requires hint/clawback memos to match the actual puzzle hash.Also adds
VITE_DISABLE_XCHANDLES, sqlx query artifacts, and extensive unit/simulator tests (not wired into UI send flow yet).Reviewed by Cursor Bugbot for commit d2c57de. Bugbot is set up for automated code reviews on this repo. Configure here.