Skip to content

LNURL-pay resolution in max_sendable#23

Open
amackillop wants to merge 4 commits into
austin_mdk-865_v1-max-withdrawablefrom
austin_mdk-865_resolve-lnurl
Open

LNURL-pay resolution in max_sendable#23
amackillop wants to merge 4 commits into
austin_mdk-865_v1-max-withdrawablefrom
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.

ldk-node: 5616db4e -> 6d2502bd. The new rev brings in the
bitcoin-payment-instructions bump transitively, so the direct
pin moves to bdcef061 to match and dedupe (verify with
`cargo tree -d | grep bitcoin-payment-instructions`).

bitcoin-payment-instructions on bdcef061 pins reqwest 0.13,
so mdkd's reqwest is bumped 0.12 -> 0.13 to share the type
across the crate boundary. The `rustls-tls` feature was
renamed to `rustls` in 0.13.

Sharing the same reqwest version is what unlocks passing
MdkClient's shared `reqwest::Client` into
`HTTPHrnResolver::with_client(...)`. Before the bump, the
two crates pinned incompatible reqwest majors and the
resolver had to be built with `::new()`, which bypassed
the SOCKS proxy. See the follow-up commits that route
LNURL fetches through the shared client.

The dev-dep ldk-node-lsp (rev e5fcce06) still pulls the
older bpi rev (6796e87) transitively; that duplication
only affects test builds and is left for a separate bump.
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.
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