Skip to content

wallet: auto-reply over Tor when scanning an Invoice1 slatepack#21

Merged
ardocrat merged 1 commit into
GetGrin:masterfrom
jwinterm:feat/auto-tor-reply-on-invoice-scan
Jun 23, 2026
Merged

wallet: auto-reply over Tor when scanning an Invoice1 slatepack#21
ardocrat merged 1 commit into
GetGrin:masterfrom
jwinterm:feat/auto-tor-reply-on-invoice-scan

Conversation

@jwinterm

Copy link
Copy Markdown
Contributor

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 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 merchant's foreign-api
finalize_tx over Tor. The merchant's wallet finalizes and posts
on their side, so no local finalize/post is needed (cf. the
existing send_tor closure 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:

  1. send_tor() gains a finalize: bool parameter that
    switches the JSON-RPC body between receive_tx (existing
    behavior for Standard1 send) and finalize_tx (new path for
    the invoice flow). Same Tor SOCKS plumbing handles both.

  2. pay() gains a dest: Option<SlatepackAddress> parameter
    (mirroring receive()) and forwards it to
    create_slatepack_message. The on-disk response slatepack
    continues to be written, so paste-back is always available as
    a fallback.

  3. handle_task gains a sibling pay_tor closure to
    send_tor, used in the OpenMessage::Invoice1 arm when the
    parsed slatepack has a sender address and the wallet's Tor
    service is running. Unlike send_tor, pay_tor does not
    finalize/post locally — the merchant does that on their side.

  4. OpenMessage::Invoice1 arm passes dest.clone() to
    pay() and, when the wallet's Tor service is up + a sender
    address is present, awaits pay_tor() and returns. Otherwise
    falls through to the existing write-to-disk paste-back path
    unchanged.

Backward compatibility

  • Slatepacks without an embedded sender address (older issuers,
    or anything that explicitly omits the field) fall through to
    the existing on-disk paste-back path. No regression.
  • Tor send failures (no circuit, peer offline, onion unreachable)
    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.
  • No protocol change, no new dependencies, no new state to
    persist.

Privacy / security review

  • The sender's slatepack address is already embedded in the
    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.
  • Outbound Tor send to a slatepack-embedded onion is the same
    security surface as grin-wallet send -m tor -d <addr>. Same
    arti SOCKS5 proxy, same circuit semantics.
  • No change to keystore, no change to identity.

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 listen with a Tor
hidden 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_confirmations with
zero customer interaction beyond the initial scan.

The plugin-side counterpart picks up these payments by polling
the merchant wallet's tx_log for kernel_excess on the
invoice's recorded slate_id — an indicator that
finalize_tx already ran — and promotes the invoice through
its existing Broadcast → Confirmed → Settled state machine.
That work is independent of this PR and lives in the plugin,
but it's evidence the architecture works as designed.

Test plan

  • Builds for Android arm64-v8a (scripts/android.sh build v8,
    Cargo profile release-apk)
  • End-to-end Tor-finalize against a real merchant
    grin-wallet listen instance (mainnet)
  • Paste-back fallback still works when Tor service isn't up
    (no behavioral change for that path)
  • Standard1 + Send-task Tor flow unchanged (please verify in
    review — the only call-site change for that path is the
    false literal added to the existing send_tor invocation)

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.

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.
Comment thread src/wallet/wallet.rs
@ardocrat ardocrat merged commit 161faba into GetGrin:master Jun 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants