Skip to content

LNURL-pay resolution in max_sendable#23

Merged
amackillop merged 4 commits into
masterfrom
austin_mdk-865_resolve-lnurl
May 27, 2026
Merged

LNURL-pay resolution in max_sendable#23
amackillop merged 4 commits into
masterfrom
austin_mdk-865_resolve-lnurl

Conversation

@amackillop

Copy link
Copy Markdown
Contributor

What it does

  • Threads an HrnResolver through compute_estimate and resolves
    LNURL-pay destinations to a concrete BOLT11 invoice before pricing.
    Target amount is min(balance, max_value): the fee at the largest
    amount we could plausibly send.
  • Bumps ldk-node, bitcoin-payment-instructions, and reqwest. The
    reqwest 0.12 → 0.13 bump aligns mdkd's reqwest with the bpi rev's,
    which is what makes HTTPHrnResolver::with_client(...) accept
    MdkClient's shared reqwest::Client.
  • Stops leaking IP on /pay LN-Address sends. pay_any was
    building HTTPHrnResolver::new(), which constructs a fresh
    reqwest client that ignores socks_proxy. Both max_sendable and
    pay_any now reuse the shared client.

Error domain

MaxSendableError gains two variants for the API layer:

  • BelowLnurlMin { min_msat, balance_msat } — distinct from a dust
    balance. The destination has a hard floor we can't meet, and the
    UI should say so rather than show "0 sat sendable".
  • LnurlResolutionFailed(String) — transport or validation failure
    from the LNURL callback. Surfaced rather than buffer-falling-back
    so intermittent issues don't masquerade as a successful estimate.

Commits

Four atomic commits, each passes just check:

  1. Bump ldk-node, bitcoin-payment-instructions, reqwest
  2. Extract estimate_via_payment_params helper (pure refactor)
  3. Resolve LNURL-pay invoice in max_sendable
  4. Route pay_any LNURL fetches through the shared HTTP client

Out of scope

  • /getbalance accepting a dest= query param. Today the daemon
    calls max_sendable(None); the destination-aware path is
    reachable only from inside the library.
  • Caching resolved LNURL invoices. Every call with an LNURL dest
    hits the callback. Revisit if telemetry shows it matters.
  • Bumping the dev-dep ldk-node-lsp, which still pulls the older
    bpi rev transitively. Test-build dup only.

@amackillop amackillop force-pushed the austin_mdk-865_v1-max-withdrawable branch from e9ca454 to 2165ca5 Compare May 27, 2026 14:00
@amackillop amackillop changed the base branch from austin_mdk-865_v1-max-withdrawable to master May 27, 2026 15:11
Pull the build-route-params / find-route / fold-into-estimate tail
out of compute_estimate's FromRoute branch into a private helper.
No behaviour change.

The LNURL-pay branch added in the follow-up commit fetches a BOLT11
invoice asynchronously and then needs the exact same routing tail
to turn the resulting PaymentParameters into an estimate. Lifting
it here lets that branch reuse a single tested code path instead
of duplicating the find_route + estimate_from_route sequence.
Wire an HrnResolver through compute_estimate so the LNURL-pay arm
fetches a concrete BOLT11 invoice from the callback before pricing.
The estimate is driven by find_route over a real path now, not a
flat percentage buffer.

Target amount is min(balance, max_value): the fee at the largest
amount we could plausibly send. If balance falls below min_value
the destination is unreachable; surface that as
MaxSendableError::BelowLnurlMin { min_msat, balance_msat } so consumers
can render a recipient-specific message ("minimum send is N
sats") instead of conflating it with a dust balance. LNURL fetch
or validation failures surface as a new
MaxSendableError::LnurlResolutionFailed variant.

compute_estimate becomes async; MdkClient::max_sendable likewise.
The sole caller (/getbalance) is already inside an async fn, so
the change is a single .await.

The resolver is built with `HTTPHrnResolver::with_client(self.http_client.clone())`
rather than `::new()` so LNURL callbacks go through the configured
SOCKS proxy. `MdkClient` now holds the shared `reqwest::Client`
that was previously passed only to `MdkApiClient`; pay_any.rs
should be switched over in a follow-up so it stops leaking IP
on LN-Address payments too.

HRNs are already resolved by PaymentInstructions::parse. BIP 353
names land here as whatever reusable methods the recipient
publishes in their BIP 321 URI (most commonly a BOLT12 offer);
LN-Address names land as LNURL-pay. No extra branch needed.

Existing dispatch tests are converted to #[tokio::test]. The
LNURL fetch path itself is exercised manually via /getbalance
against a real LN-Address endpoint; producing a signed BOLT11 for
an arbitrary amount in a unit test would need its own keying
harness, which isn't worth the surface area.
`HTTPHrnResolver::new()` builds its own `reqwest::Client` and
ignores any proxy config, so LN-Address pays via /pay were
reaching the LNURL callback host directly even when
`socks_proxy` was set. /getbalance's max_sendable path was
fixed in the previous commit; this is the matching fix for
the actual send path.

Expose `MdkClient::http_client()` and pass that shared client
to `HTTPHrnResolver::with_client`. No new dependency; the
client was already built in `MdkClient::new()` and handed to
`MdkApiClient`.

Possible only because the reqwest 0.13 bump aligned mdkd with
bitcoin-payment-instructions; pre-bump the two crates pinned
incompatible reqwest majors and `with_client` rejected ours.
@amackillop amackillop force-pushed the austin_mdk-865_resolve-lnurl branch from 509e939 to e5f802f Compare May 27, 2026 15:35
@amackillop amackillop merged commit 7d0c1ff into master May 27, 2026
2 checks passed
@amackillop amackillop mentioned this pull request Jun 12, 2026
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