Skip to content

feat: add LNURLw withdraw support (Boltcard / LUD-17)#456

Open
FrankChinedu wants to merge 1 commit into
fedimint:masterfrom
FrankChinedu:feat/lnurlw_boltcard_support
Open

feat: add LNURLw withdraw support (Boltcard / LUD-17)#456
FrankChinedu wants to merge 1 commit into
fedimint:masterfrom
FrankChinedu:feat/lnurlw_boltcard_support

Conversation

@FrankChinedu

@FrankChinedu FrankChinedu commented May 20, 2026

Copy link
Copy Markdown
Contributor

Implements issue #331. Boltcards use the lnurlw:// URI scheme (LUD-17) to pull Lightning payments into the user's wallet via the LNURL Withdraw protocol (LUD-03).

Changes:

  • Register lnurlw:// scheme in AndroidManifest.xml and macos/Info.plist
  • Parse lnurlw:// deep links in DeepLinkHandler (lnurlw:// → https://)
  • Add fetch_lnurl_withdraw + execute_lnurl_withdraw Rust functions
  • Add LnurlWithdrawScreen with loading/details/executing/waiting states
  • Route DeepLinkType.lnurlWithdraw in app.dart
  • Add 8 i18n strings (English + Spanish)
  • Add scripts/test-lnurlw.py for end-to-end testing (macOS + Android)
  • Add docs/contributor-guide.md

Manual Testing Instructions

scripts/test-lnurlw/ is a self-contained Rust binary that acts as a mock Boltcard server. It replaces the earlier Python script and does the same job: spins up a local HTTP server, fires a lnurlw:// deep link to the running app, and validates that the app sends back the correct k1 token and a valid bolt11 invoice.

Prerequisites

Rust toolchain installed (cargo)
App built and running on device/emulator
Run on macOS (simulator or physical device via USB)

cd scripts/test-lnurlw
cargo run
Run on Android emulator

cd scripts/test-lnurlw
cargo run -- --android
What to expect

The terminal prints the mock server details and fires the deep link
The app opens to the Boltcard Withdraw screen showing "Boltcard test withdraw", amount range 1–100 sats
Select a gateway, enter or accept the amount, tap Withdraw
The terminal prints ✓ k1 matched then ✓ bolt11 invoice received
The app transitions to "Waiting for payment…" — at this point the mock server has accepted the invoice (it does not actually pay it, so the app will time out after 5 minutes and show a "Withdraw failed" toast, which is expected behaviour in the test)
The terminal exits with ✓ Test passed — LNURLw withdraw flow completed successfully.
Testing with a real Boltcard

Tap the card while the app is open and select a federation when prompted. The full flow completes including actual payment.

This covers both paths (mock + real card) and sets the right expectation that the mock server validates the protocol but does not actually pay the invoice

@FrankChinedu FrankChinedu force-pushed the feat/lnurlw_boltcard_support branch 2 times, most recently from 142f045 to b8b25d6 Compare May 20, 2026 19:04
Comment thread scripts/test-lnurlw.py Outdated
Comment thread macos/Runner/Info.plist
Comment thread rust/ecashapp/src/multimint.rs Outdated
@FrankChinedu FrankChinedu force-pushed the feat/lnurlw_boltcard_support branch from b8b25d6 to 308c780 Compare May 27, 2026 12:58
@m1sterc001guy

Copy link
Copy Markdown
Collaborator

Can we not rely on the emulator for the test? We're not going to be able to add that to CI which means we'd need to run it manually, which means it will likely break in the future. We shouldn't be relying on dependencies outside of nix too.

Is there a more self-contained way (and ideally automated) that we can test LNURLw without a boltcard?

@FrankChinedu

Copy link
Copy Markdown
Contributor Author

Hi @m1sterc001guy. The constraint is that full end-to-end testing requires a Fedimint federation, which we don't have wired up for CI. The HTTP protocol parsing is already covered by Rust unit tests. What's missing is test coverage for the deep link URL conversion (lnurlw:// → https://), which we can add to the existing Flutter test suite with no new infrastructure. The invoice creation and payment completion path can't be tested without devimint, which would be a separate piece of work. i'll update that and remove the script for manual testing.

@FrankChinedu FrankChinedu force-pushed the feat/lnurlw_boltcard_support branch 2 times, most recently from fefb489 to 59d29e7 Compare June 1, 2026 17:40
Implements the full LNURLw withdraw flow (LUD-03 / LUD-17) so users can
receive funds by tapping a Boltcard NFC card.

1. NFC tap fires lnurlw://host/path?k1=TOKEN deep link
2. App converts lnurlw:// → https:// and GETs the endpoint
3. Server responds with callback URL, k1, min/max withdrawable, description
4. App creates a Lightning invoice for the chosen amount
5. App GETs callback?k1=TOKEN&pr=<invoice>
6. Server pays the invoice — funds land in the wallet

Register the lnurlw scheme in the intent-filter so the OS routes Boltcard
deep links to this app.

- Add DeepLinkType.lnurlWithdraw enum value
- Parse lnurlw:// URIs: convert to https:// for clearnet hosts, http://
  for localhost / 127.0.0.1 / 10.0.2.2 / .onion (enables the local test
  harness without TLS)

Route DeepLinkType.lnurlWithdraw directly to LnurlWithdrawScreen, skipping
the Rust parse pipeline (type is already known from the scheme).

- LnurlWithdrawParams struct (#[frb]) with callback, k1, min/max msats,
  defaultDescription
- fetch_lnurl_withdraw: HTTP GET + JSON parse; pure read, no side effects
- parse_lnurl_withdraw_response: private helper (testable without network);
  checks status=ERROR, validates tag=withdrawRequest, extracts all fields
- execute_lnurl_withdraw: creates Lightning invoice via existing
  multimint.receive(), GETs the callback URL, returns OperationId
- build_lnurlw_callback_url: appends k1 and pr with correct ? / & separator
- 7 unit tests covering happy path, optional fields, error responses,
  wrong tag, missing fields, and callback URL building

4-state machine: loading → showingDetails → executing → waitingForPayment
- loading: fetches LNURLw params and gateway list in parallel
- showingDetails: fixed amounts show a static label; variable amounts show
  a TextField (digits only, sats unit) validated against [min, max] with
  inline error and disabled Withdraw button while out of range; gateway
  picker row shown when multiple gateways available
- executing: spinner while invoice is created and callback is called
- waitingForPayment: spinner + Cancel button; awaitReceive with 5-minute
  timeout; navigates to Success screen on payment, shows error toast on
  timeout or failure, always pops to dashboard in finally block

New keys: lnurlWithdrawTitle, lnurlWithdrawConfirm, lnurlWithdrawRequesting,
lnurlWithdrawWaiting, lnurlWithdrawFailed, lnurlWithdrawCallbackError,
lnurlWithdrawFixedAmount, lnurlWithdrawAmountRange

Standalone Rust binary (no Python dependency):
- Binds to a random port, generates a random k1 from /dev/urandom
- Fires lnurlw:// deep link via open (macOS) or adb (--android flag)
- Serves LNURLw JSON on GET /lnurlw
- Validates callback: k1 match + bolt11 prefix check + percent-decode
- Returns {status:OK} or {status:ERROR} accordingly
- Exits 0 on success, 1 on failure or 60s timeout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@FrankChinedu FrankChinedu force-pushed the feat/lnurlw_boltcard_support branch from 59d29e7 to 9f2312a Compare June 5, 2026 10:25
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