From 94d0c0edba76c66079b7d56c77daa4a5d7d6be48 Mon Sep 17 00:00:00 2001 From: 2ro <17595647+2ro@users.noreply.github.com> Date: Thu, 2 Jul 2026 19:22:35 -0400 Subject: [PATCH] checkout: first-class grin1 / Slatepack payment method The hosted /pay page now shows the wallet's grin1 Slatepack address (with a QR and the exact amount) as a payment method alongside the Goblin/Nostr option. A payer sends the amount from any Grin wallet via the Slatepack or file method, pastes the S1 into the existing paste box, receives an S2, and finalizes to complete the payment. Reuses the existing offline receive_tx flow bound to the invoice token; the Nostr gift-wrap path, the invoice matcher, and the proof/confirm logic are unchanged. No Tor listener. The grin1 address is the wallet's stable index-0 address. --- README.md | 18 +++++- crates/gp-server/src/checkout.rs | 82 ++++++++++++++++++++++---- crates/gp-server/src/invoices.rs | 6 +- crates/gp-server/tests/checkout_e2e.rs | 4 ++ static/style.css | 5 ++ templates/pay.html | 35 ++++++----- templates/pay_result.html | 2 +- 7 files changed, 119 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 9c1a938..9d68e85 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,21 @@ carries the full merchant surface: invoice automatically. - **Hosted checkout:** a zero-JS `/pay/` page (server-rendered Askama + one CSS file + a server-generated QR SVG at ECC level H with an - optional GoblinPay-mark center logo), live status via ``, - and a manual slatepack fallback (paste S1 -> offline `receive_tx` -> copy the - S2 back) on every page. The same renderer serves embedded and hosted use. + optional GoblinPay-mark center logo) with live status via + ``. It offers two first-class ways to pay: + - **Goblin Wallet (Nostr):** scan the `nprofile` QR (or copy it) and the + payment auto-receives over Nostr. + - **Slatepack (`grin1`):** pay from any Grin wallet, no Nostr needed. The + page shows the wallet's stable index-0 Slatepack address (`grin1...`) plus + its QR and the exact amount to send. The payer sends that amount to the + address using their wallet's Slatepack/file method, pastes the resulting S1 + into the page (offline `receive_tx`), then copies the returned S2 back into + their wallet to finalize and broadcast it. There is no Tor listener; the + `grin1` address is stable and reused across invoices, and the existing + invoice matcher and on-chain confirmation handle the received payment like + any other. The Slatepack option only appears when a wallet is loaded. + + The same renderer serves embedded and hosted use. - **Per-user endpubs:** an admin assigns one receiving identity per user (a derived child keyed by `(user_id, epoch)`; only public keys and the rotation clock are stored, never private keys), with optional rolling rotation diff --git a/crates/gp-server/src/checkout.rs b/crates/gp-server/src/checkout.rs index ba691fe..d860038 100644 --- a/crates/gp-server/src/checkout.rs +++ b/crates/gp-server/src/checkout.rs @@ -1,13 +1,15 @@ //! The hosted, zero-JS checkout: the `/pay/` page (shared renderer for -//! embedded and hosted use), its live status, and the manual-slatepack -//! fallback. +//! embedded and hosted use), its live status, and the Slatepack receive flow. //! -//! The page shows the amount, a server-generated QR SVG of the recipient -//! `nprofile`, the `nprofile`/`npub` strings, live status via a -//! `` while open, and a ` +
+

Pay with Goblin Wallet

+
{{ info.qr_svg|safe }}
+

Scan with your Goblin Wallet, or copy the address below.

+ + +
-
- Can't scan? Pay manually with a slatepack - {% if wallet_available %} + {% if let Some(grin1) = info.slatepack_address %} +
+

Pay by Slatepack (grin1)

+

Pay from any Grin wallet, no Nostr needed. Send {{ info.amount_display }} to the address below.

+ {% if let Some(grin1_qr) = info.slatepack_qr_svg %}
{{ grin1_qr|safe }}
{% endif %} + +
    -
  1. In your wallet, send {{ info.amount_display }} using the manual / slatepack option.
  2. -
  3. Paste the generated S1 slatepack below and submit.
  4. -
  5. Copy the response slatepack we return, back into your wallet to finalize and post.
  6. +
  7. In your Grin wallet, send {{ info.amount_display }} to this address using the Slatepack / file method, then paste the Slatepack it produces below.
  8. +
  9. The pasted S1 Slatepack is received here and a response Slatepack is returned.
  10. +
  11. Paste that response back into your wallet to finalize and broadcast it, which completes the payment.
- +
- {% else %} -

Manual receive is unavailable on this instance.

- {% endif %} -
+ + {% endif %} {% endif %} {% if let Some(memo) = info.memo %}

{{ memo }}

{% endif %} diff --git a/templates/pay_result.html b/templates/pay_result.html index ff44e64..5fa9949 100644 --- a/templates/pay_result.html +++ b/templates/pay_result.html @@ -16,7 +16,7 @@
  1. Select all of the text above and copy it.
  2. -
  3. Paste it back into your wallet to finalize the transaction.
  4. +
  5. Paste it back into your wallet to finalize and broadcast the transaction; this completes the payment.
  6. Your wallet posts it to the chain; GoblinPay confirms it on receipt.
{% endif %}