Repository navigation
Resolve XCHandles safely in every recipient flow - #932
Merged
Merged
Conversation
Adds a shared recipient pipeline around Sage::parse_address. Recipients written as @handle are verified through the resolver, a handle which resolves to a different address than it changed from or was last sent to is rejected until the new address is confirmed, and transactions which aren't submitted straight away are checked again before they are. Submitting records the address last sent to. Offers whose offered NFTs claim to be handle NFTs are checked against the registry and the pending-transfer endpoint, and can't be taken while a transfer is pending.
The host resolves a handle recipient when the approval is requested, so the approval shows the verified address and asks the user to acknowledge a handle which changed. The approved address is pinned for the send. Handles always require approval, even for apps that may auto submit.
Send (including bulk), NFT/DID/option transfers, NFT mint royalties and WalletConnect sends accept @handle recipients behind VITE_DISABLE_XCHANDLES. Handles are previewed as they're entered, a handle which changed asks the user to confirm the new address showing both addresses, and confirmations show each handle with the address it was resolved to. Offers with a handle NFT whose transfer is pending, or which can't be checked, can't be taken.
# Conflicts: # crates/sage/src/endpoints/offers.rs # src/bindings.ts
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 8d6a32c. Configure here.
Rigidity
marked this pull request as draft
October 26, 2026 04:58
- Only treat offered NFT names written as @handle as handle claims, and check them concurrently, capped at 8 per offer - Ignore surrounding whitespace in recipients, as the preview does - Preview handle recipients and show offer warnings even when XCHandles is hidden, since the backend still resolves them - Report the address a handle changed from in HandleRecord.changed_from, instead of each frontend deriving it - Explain that a handle which changed after a transaction was created needs the transaction to be created again - Add --confirm-handle to sage wallet send, and show recipient errors readably
HandleRecord gains resolved_confirmation_height and updated_height, the later of the slot's and the resolved singleton's confirmation heights. It's shown in the handle preview, the confirmation dialog, the Sage Apps send approval card and the CLI send preview.
Rigidity
marked this pull request as ready for review
October 26, 2026 18:08
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 #871. Phase 1 of #867.
Recipients can now be written as
@handleeverywhere Sage takes one. Handles go through one backend pipeline built on the verified resolver from #870, so RPC, WalletConnect and Sage Apps callers get the same checks as the UI. A handle which changed hands can't silently redirect a transaction.Pipeline (
crates/sage/src/utils/recipients.rs)resolve_recipientssits next toSage::parse_addressand is used by:send_xch,send_cat, their bulk versions,multi_sendand clawbackstransfer_nfts,transfer_didsandtransfer_optionsbulk_mint_nftsroyalty and target addressesmake_offer's receive addresscreate_transactionsend and mint actionshandles: each handle with the verified address it resolved to, and whether that came from the offline cache.HandleRecordgains:changed_from: the address a send has to confirm the new one against, if any. The frontends read this instead of deriving the rule themselves.updated_height: the later of the slot's and the resolved singleton's confirmation heights, so the latest height at which the address could have changed. The slot is also recreated by activity on neighboring handles, so this can be later than the handle's own last update.resolved_confirmation_height.Changed-hands protection
confirmed_handles: [{ handle, address }]. Confirming the current address acknowledges the change, and also pins it: if the handle resolves anywhere else by the time the request runs, it's rejected.auto_submitremembers its handle recipients, keyed by its coin spends.submitresolves them again right before submitting, throughsign_coin_spendsorsubmit_transaction, and refuses if any moved. The record is kept in memory, limited to 64 transactions, and cleared when switching wallets.Errors
recipienterror kind with aRecipientErrorper failed recipient:invalid,unsupported_network,not_registered,unverified,unavailableandchangedhandle,addressandprevious_addressrecipients.data.Offers with handle NFTs
view_offerreturnshandle_nftsfor offered NFTs whosedn(Classify NFT metadata updaters and support the XCHandles metadata format #911) is written as@handle. Names without the@aren't treated as claims, so an NFT that happens to be named like a handle doesn't block offers while the API is down.dnis only used to pick which handle to look up; the registry must list the NFT as the handle's owner.unavailable, which blocks taking the offer./handle/{handle}/pending-transferis checked. Statuses areverified,pending_transfer,not_associatedandunavailable.take_offerchecks again, and refuses if a transfer is pending or the check couldn't be done.Sage Apps
wallet.sendXchto a handle always asks the user for approval, even for apps allowed to auto-submit.prepare_approval). The card shows the verified address and the height it was last updated at, and requires a tick to continue if the handle changed.confirmedAddressand pinned.recipient_changedandrecipient_unresolved.UI
@handleand show a live preview: verified, cached or offline, changed, not registered or expired, and unavailable, along with the height the handle was last updated at. The preview callsresolve_handle, and the backend checks again regardless.chia_sendapproval shows the same preview.withHandleConfirmationwraps those requests. When the backend reports a changed handle, a dialog shows the old and new addresses and requires a tick before retrying with the new address confirmed. Declining is passed to dApps as a cancellation with the recipient list.VITE_DISABLE_XCHANDLESonly hides handles from placeholders and hints. The backend accepts handles regardless, so previews, the confirmation dialog's Handles section and offer warnings are always shown when a handle is involved.CLI
sage wallet sendaccepts@handle, shows what each handle resolves to and its last updated height, and takes--confirm-handle <ADDRESS>to confirm a changed handle.--confirm-handlecommand for a changed handle.Testing
sage-rpctests against a local mock XCHandles API, so they never reach the real one:changed_fromandupdated_heighton a summary handle, and the error for a change found at submission@, which isn't a claim and doesn't block taking the offerwithHandleConfirmation: confirm, decline, and errors which aren't handle changes.cargo clippy --workspace --all-features --all-targetsandcargo fmtare clean, andcargo test --workspacepasses.pnpm lint, prettier,tsc -band vitest pass. The approval app typechecks.Notes for reviewers
resolve_handlenow asks the API first and fetches the chain tip once it answers, instead of both at once. Peers aren't queried while the API is down, and the peer simulator, which can't serve block headers, no longer drops the connection in tests. The cost is one extra peer round-trip on a successful lookup..pocatalogs aren't updated, so the next release needspnpm extract.bindings.tsis regenerated after mergingmain.Note
High Risk
Changes payment recipient resolution, submission re-checks, and offer-taking guards—core wallet and fund-safety paths—with new failure modes that all API consumers must handle.
Overview
Adds XCHandles (
@handle) as first-class recipients across sends, transfers, offers, RPC, CLI, and Sage Apps, with one shared resolution pipeline so a handle that changed hands cannot redirect funds without explicit user confirmation.Backend: Recipients starting with
@are verified against the registry; transactions accept optionalconfirmed_handlesto acknowledge a new resolution. Builds without auto-submit remember handle recipients and re-resolve at submit; successful sends record last-sent addresses and acknowledge changes. Newrecipienterrors (JSON on RPC, including 409 when a handle changed) replace opaque failures for bulk sends. Offers gainhandle_nftschecks (owner + pending transfer) and blocktake_offerwhen unsafe.Apps & UI:
wallet.sendXchto a handle always requires approval with resolved address UI andconfirmedAddresspinning; bridge-approval shows change warnings. The main app wraps sends withwithHandleConfirmation, shows handle previews in confirmation flows, and adds offer warnings for handle NFTs.Other: CLI
--confirm-handle,TransactionSummary.handles, resolver now hits the API before fetching chain tip (#919), plus extensive RPC/integration tests with a mock XCHandles API.Reviewed by Cursor Bugbot for commit 548700c. Bugbot is set up for automated code reviews on this repo. Configure here.