wallet: auto-reply over Tor when scanning an Invoice1 slatepack#21
Merged
ardocrat merged 1 commit intoJun 23, 2026
Merged
Conversation
Mirrors the existing Standard1 + Send-task flow for the invoice direction. When a customer scans an Invoice1 slatepack QR with an embedded sender address (now standard for slatepacks produced by grin-wallet's issue_invoice_tx), the patched pay() forwards the sender address through create_slatepack_message, and the OpenMessage handler — if the wallet's Tor service is running or starting — pushes the signed Invoice2 to the sender's foreign-api finalize_tx over Tor. The merchant's wallet finalizes + posts on their side, so no local finalize/post is needed (cf. the existing send_tor closure for Standard1 which does need that). Backward-compatible: if the slatepack has no embedded sender address (older clients) or the Tor service isn't up, the existing write-slatepack-to-disk-for-paste-back fallback runs unchanged. No protocol change, no new dependencies, no new failure modes — the response slatepack file is always written before the Tor send is attempted, so a Tor failure mid-flight is recoverable. Closes the mobile-UX gap that required the customer to manually copy the response slatepack from the wallet and paste it back to the merchant's web/storefront interface. With this patch and a foreign-api listener on the merchant side, scanning a Grin invoice QR is now a single tap: scan → confirm → done. send_tor() gains a `finalize: bool` parameter that selects between the existing receive_tx body (for Standard1 sends) and a new finalize_tx body (for the invoice-flow case). The same Tor SOCKS plumbing handles both. Real-world validation: end-to-end working today on a production BTCPay deployment (Such Software's btcpayserver-plugin-grin v1.3.5) — invoice QR scan with a patched build settles a merchant invoice in ~1 confirmation window with zero customer interaction beyond the scan.
ardocrat
reviewed
Jun 22, 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.
Summary
Mirrors the existing Standard1 + Send-task flow for the invoice
direction. When a customer scans an Invoice1 slatepack QR with an
embedded sender address, the patched
pay()forwards the senderaddress through
create_slatepack_message, and theOpenMessagehandler — if the wallet's Tor service is running or starting —
pushes the signed Invoice2 to the merchant's foreign-api
finalize_txover Tor. The merchant's wallet finalizes and postson their side, so no local finalize/post is needed (cf. the
existing
send_torclosure for Standard1, which does need that).Closes the mobile-UX gap where a customer scanning a Grin invoice
QR had to manually copy the response slatepack from the wallet
and paste it back to the merchant's web/storefront. With this
patch, scanning an invoice QR is now a single tap: scan →
confirm → done.
What the patch touches
Single file,
src/wallet/wallet.rs:send_tor()gains afinalize: boolparameter thatswitches the JSON-RPC body between
receive_tx(existingbehavior for Standard1 send) and
finalize_tx(new path forthe invoice flow). Same Tor SOCKS plumbing handles both.
pay()gains adest: Option<SlatepackAddress>parameter(mirroring
receive()) and forwards it tocreate_slatepack_message. The on-disk response slatepackcontinues to be written, so paste-back is always available as
a fallback.
handle_taskgains a siblingpay_torclosure tosend_tor, used in theOpenMessage::Invoice1arm when theparsed slatepack has a sender address and the wallet's Tor
service is running. Unlike
send_tor,pay_tordoes notfinalize/post locally — the merchant does that on their side.
OpenMessage::Invoice1arm passesdest.clone()topay()and, when the wallet's Tor service is up + a senderaddress is present, awaits
pay_tor()and returns. Otherwisefalls through to the existing write-to-disk paste-back path
unchanged.
Backward compatibility
or anything that explicitly omits the field) fall through to
the existing on-disk paste-back path. No regression.
are logged and surface via
on_tx_error+on_task_result.The on-disk response slatepack was already written before the
Tor send was attempted, so the customer can always recover by
pasting it back manually.
persist.
Privacy / security review
slatepack by the issuer (it's how merchants identify which
customer signed the response in any flow). Auto-using it for
the reply doesn't reveal anything the merchant didn't already
receive.
security surface as
grin-wallet send -m tor -d <addr>. Samearti SOCKS5 proxy, same circuit semantics.
Real-world validation
End-to-end working today on a production BTCPay deployment using
the Such-Software btcpayserver-grin-plugin v1.3.5 (released
2026-06-22). The merchant runs
grin-wallet listenwith a Torhidden service exposed; a patched Grim build on Android scans the
invoice QR, signs it, posts the Invoice2 over Tor, and the
merchant's BTCPay invoice settles after
min_confirmationswithzero customer interaction beyond the initial scan.
The plugin-side counterpart picks up these payments by polling
the merchant wallet's
tx_logforkernel_excesson theinvoice's recorded
slate_id— an indicator thatfinalize_txalready ran — and promotes the invoice throughits existing
Broadcast → Confirmed → Settledstate machine.That work is independent of this PR and lives in the plugin,
but it's evidence the architecture works as designed.
Test plan
scripts/android.sh build v8,Cargo profile
release-apk)grin-wallet listeninstance (mainnet)(no behavioral change for that path)
review — the only call-site change for that path is the
falseliteral added to the existingsend_torinvocation)Happy to iterate on the approach or the naming — this is the
minimum-diff version, but I'm open to factoring it differently if
maintainers prefer.