Skip to content

Integrate Open CryptoPay payment standard#1299

Open
reubenyap wants to merge 2 commits intocypherstack:stagingfrom
reubenyap:claude/integrate-open-cryptopay-8mI4H
Open

Integrate Open CryptoPay payment standard#1299
reubenyap wants to merge 2 commits intocypherstack:stagingfrom
reubenyap:claude/integrate-open-cryptopay-8mI4H

Conversation

@reubenyap
Copy link
Copy Markdown

@reubenyap reubenyap commented Apr 20, 2026

Summary

  • Adds Open CryptoPay support so users can pay at PoS systems like SPAR supermarkets in Switzerland by scanning a static QR code.
  • Detects Open CryptoPay QR codes (URLs with a ?lightning=LNURL... param), decodes the bech32 LNURL, fetches payment details from the provider (e.g. DFX Swiss), lets the user pick a method/asset compatible with their wallet's coin, then forwards to the standard SendView pre-filled with the payment address and amount.
  • After broadcast, notifies the provider with the txid (plus the raw signed hex when the wallet exposes it) via the provider's /tx/ endpoint so the merchant-side can settle without waiting for on-chain confirmation.
  • Adds a "Pay" shortcut to the wallet view overflow () menu that scans and validates an Open CryptoPay QR directly.

Spec: https://github.com/openCryptoPay/landingPage
Verified against DFX Swiss's implementation: https://github.com/DFXswiss/api

Files changed

New (all under lib/services/open_crypto_pay/ or lib/pages/open_crypto_pay/):

  • lib/services/open_crypto_pay/lnurl_utils.dart — bech32 LNURL (LUD-01) decode and Open CryptoPay URL detection. Scoped to this folder because Stack does not support Lightning generally — OCP is currently the sole consumer.
  • lib/services/open_crypto_pay/open_crypto_pay_api.dart — API client (payment details + transaction details + commit), Tor-aware, https-only, host-pinned.
  • lib/services/open_crypto_pay/models.dart — response data models and OpenCryptoPayCommit context.
  • lib/pages/open_crypto_pay/open_crypto_pay_view.dart — payment method/asset selection UI.
  • lib/pages/open_crypto_pay/open_crypto_pay_confirm_view.dart — summary + forward to SendView.

Modified:

  • lib/models/send_view_auto_fill_data.dart — adds optional openCryptoPayCommit field carried through to ConfirmTransactionView.
  • lib/pages/send_view/send_view.dart — intercept Open CryptoPay QR codes in _scanQr() before the standard URI parser; pass openCryptoPayCommit into ConfirmTransactionView.
  • lib/pages/send_view/confirm_transaction_view.dart — after a successful broadcast, best-effort notify the provider via OpenCryptoPayApi.commit(...) with the txid and raw hex.
  • lib/pages/wallet_view/wallet_view.dart — add "Pay" overflow menu button.
  • lib/route_generator.dart — register OpenCryptoPayView route.

Notes

  • Existing QR handling is preserved — LnurlUtils.isOpenCryptoPayUrl only returns true when the URL has a ?lightning=LNURL... query param, which standard crypto URIs (bitcoin:, monero:, etc.) never have.
  • Only assets matching the current wallet's coin ticker are shown as selectable payment options. Network selection is implicit in the method label shown next to each option.
  • Transaction submission. The spec defines two /tx/ flows: (a) EVM / Bitcoin / Firo — submit raw signed hex to the provider and let the provider broadcast; (b) Monero / Solana / Tron / Cardano / Zano — broadcast locally, then submit tx=<txid>. This integration always broadcasts locally via the standard SendView and then calls /tx/ with tx=<txid> (plus hex=<raw> when the wallet exposes a raw serialization). For the flow-(b) chains this matches the spec. For Bitcoin / EVM / Firo it diverges — the client pre-broadcasts instead of deferring to the provider.
  • Commit is best-effort: a failed /tx/ call is logged but does not unwind the send.

Security

The API client hardens the network path against a malicious QR / compromised provider response:

  • HTTPS-only. The decoded LNURL, the callback URL, and the derived /tx/ endpoint are all rejected unless the scheme is https and the URL has an authority. LUD-01 requires HTTPS; this also closes off MITM on plain http and SSRF into loopback.
  • Callback host pinning. The callback URL returned in the payment details response must share its host with the LNURL-decoded URL. Without this, a compromised provider response could redirect the txid + raw hex to an attacker-controlled host. Verified compatible with DFX Swiss (both LNURL and callback are served from api.dfx.swiss).
  • Connection timeout. All OCP HTTP calls carry a 15s connection timeout so a slow/hostile server can't hang the UI during TCP/TLS handshake. (Note: Dart's HttpClient.connectionTimeout covers connection setup only; a server that accepts and then stalls mid-body can still delay the request — improving this requires wrapping the HTTP layer.)

Test plan

  • Scan an Open CryptoPay QR code (e.g. from a DFX Swiss test payment link) from the send view — should route to the Open CryptoPay selection screen
  • Tap the "Pay" button in the wallet overflow () menu and scan a QR — same flow
  • Scan a non-Open-CryptoPay QR (plain bitcoin: URI) — should be handled by the existing flow, not intercepted
  • Select a payment method, confirm, proceed to send — amount and address should be pre-filled correctly
  • Wait for a quote to expire before selecting a method — warning flushbar shown and details re-fetched; user must re-select
  • Scan an Ethereum-based Open CryptoPay payment — @chainId suffix should be stripped from the address
  • Complete a send end-to-end — confirm the /tx/ commit fires (check logs) and the merchant-side reflects the payment
  • Try with Tor enabled — requests should go through the proxy
  • Scan a QR pointing at an endpoint with no pending payment — should show the provider's "no pending payment" message
  • Scan a QR whose LNURL decodes to an http:// URL (hand-crafted test) — request should be rejected with an https error

@reubenyap reubenyap force-pushed the claude/integrate-open-cryptopay-8mI4H branch from ddbf4ba to 17c6dae Compare April 20, 2026 18:27
Adds support for the Open CryptoPay payment standard so users can pay at
PoS systems like SPAR supermarkets in Switzerland by scanning a static
QR code.

Detects Open CryptoPay QR codes (URLs with a ?lightning=LNURL... param),
decodes the bech32 LNURL, fetches payment details from the provider
(e.g. DFX Swiss), lets the user pick a method/asset compatible with their
wallet's coin, then forwards to the standard SendView pre-filled with the
payment address and amount. After broadcast, the provider's /tx/ endpoint
is notified with the tx ID and raw hex (best-effort) so the merchant can
settle. A "Pay" shortcut is also added to the wallet view overflow menu
for scanning an Open CryptoPay QR directly.

Spec: https://github.com/openCryptoPay/landingPage
Verified against DFX Swiss: https://github.com/DFXswiss/api

New:
- lib/utilities/lnurl_utils.dart — bech32 LNURL (LUD-01) decode
- lib/services/open_crypto_pay/open_crypto_pay_api.dart — API client, Tor-aware
- lib/services/open_crypto_pay/models.dart — response models
- lib/pages/open_crypto_pay/open_crypto_pay_view.dart — method/asset picker
- lib/pages/open_crypto_pay/open_crypto_pay_confirm_view.dart — summary + forward to SendView

Modified:
- lib/pages/send_view/send_view.dart — intercept QR before standard parser
- lib/pages/send_view/confirm_transaction_view.dart — notify provider post-broadcast
- lib/pages/wallet_view/wallet_view.dart — add "Pay" overflow menu button
- lib/route_generator.dart — register OpenCryptoPayView route
- lib/models/send_view_auto_fill_data.dart — carry OpenCryptoPay commit context

Notes:
- Existing QR handling is preserved — isOpenCryptoPayUrl only returns true
  when the URL has a ?lightning=LNURL... query param, which standard crypto
  URIs (bitcoin:, monero:, etc.) never have.
- Only assets matching the current wallet's coin ticker are offered as
  selectable payment options.
- After the client broadcasts the tx, we POST to the provider's /tx/
  endpoint with tx ID (and raw hex where the wallet impl exposes it). This
  matches the spec's merchant-notification contract uniformly across
  Bitcoin/Monero/Ethereum/Solana; a notify failure is logged but does not
  unwind the already-broadcast transaction.
@reubenyap reubenyap force-pushed the claude/integrate-open-cryptopay-8mI4H branch from 17c6dae to ba0134e Compare April 20, 2026 19:01
- Move LNURL helper into open_crypto_pay/ since Stack does not support
  Lightning generally; OCP is its only consumer.
- Reject non-https decoded LNURLs and callbacks (LUD-01 mandates HTTPS;
  also closes loopback SSRF and MITM via plain http).
- Pin the callback host to the LNURL-decoded host so a malicious
  provider response cannot redirect the txid + raw hex elsewhere.
- Apply a 15s connection timeout to the OCP HTTP calls.
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.

1 participant