Destination-aware max-sendable#41
Conversation
Sets up the destination-aware max-sendable work from mdkd PR #22. The new ldk-node rev exposes Node::find_route so the estimator can walk a real route and subtract its fees instead of falling back to a static buffer.
The destination-aware path coming next needs a knob for "how much above the cheapest route's fees should we reserve so retries can take a costlier path". Park it on an internal config struct now so the napi layer and the estimator share defaults. sum_outbound_balance and subtract_fee_buffer are split out of compute_estimate ahead of their second caller. The route-based strategy in the next commit reuses sum_outbound_balance to size the find_route amount and keeps subtract_fee_buffer as the no-destination arm of the strategy match. Splitting now keeps that diff to pure additions.
Reshape compute_estimate to accept Option<&PaymentInstructions>
and a find_route closure so the next commit can expose a
destination param on getMaxSendable. JS behaviour is unchanged:
the napi accessor still passes None, hitting the same buffer
path as before.
The destination dispatch lives in compute_estimate via a small
EstimationStrategy enum, with pick_strategy peeling apart the
ConfigurableAmount methods iterator (the 0.6 fork's
PossiblyResolvedPaymentMethod). FixedAmount destinations return
Err(FixedAmount { amount_msat }) so the caller does not have to
re-extract the amount; on-chain-only returns NoLightningMethod;
find_route bubbles up as RoutingFailure(String).
estimate_from_route prices a route at
route.total_fees * route_retry_fee_multiplier_bps / 10_000.
total_fees is computed from a find_route call sized to the full
balance, so the reported budget is slightly conservative
(actual fees on the smaller sent amount will be lower). The
multiplier reserves headroom for LDK to retry along a costlier
path if the chosen route fails at send time.
BOLT12 offers, LNURL-pay, and HRN destinations fall back to the
buffer until the upstream invoice/HRN resolution work lands.
BOLT11 round-trips through bech32 because bitcoin-payment-
instructions pulls upstream rust-lightning while ldk-node pulls
the moneydevkit fork: wire-compatible, distinct Rust types. A
re-parse failure falls back to Buffer rather than panicking.
Add an optional `destination` arg to `MdkNode#getMaxSendable` so JS
callers can ask "how much can I send to this specific payee right
now" and get a fee budget grounded in a real `find_route` call
rather than the 1% buffer fallback.
When supplied, the destination is parsed through the same
HTTPHrnResolver + current-thread runtime path used by `pay`, then
threaded into compute_estimate. The four MaxSendableError variants
map onto the napi surface as:
- NoUsableChannel -> Ok(None)
Preserves the existing null contract so UIs do not have to
re-handle the "node still booting" case.
- FixedAmount -> InvalidArg
Carries the dictated amount in the message so callers can
surface it without re-parsing.
- NoLightningMethod -> InvalidArg
- RoutingFailure -> GenericFailure
- parse failure -> InvalidArg
The regenerated index.d.ts/index.js carry the new optional arg,
the routeRetryFeeMultiplierBps field added two commits back, and
a pile of unrelated whitespace churn from napi-rs's formatter
disagreeing with whatever generated the previously committed
files. Folding the formatter noise into this commit keeps the
diff against the next regen empty.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc19fbe3a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| RouteParameters::from_payment_params_and_value(payment_params, balance_msat); | ||
| let route = find_route(route_params).map_err(MaxSendableError::RoutingFailure)?; |
There was a problem hiding this comment.
Route against spendable amount, not full outbound balance
compute_estimate asks find_route for balance_msat as the payment value and only subtracts routing fees afterward, so the route search is performed for an amount that already consumes the entire outbound liquidity. When any non-zero routing fee is required (the common case), there are channels where paying balance_msat is impossible but paying balance_msat - fees is valid; this path currently returns RoutingFailure instead of a max-sendable estimate for those destinations.
Useful? React with 👍 / 👎.
Ports mdkd#22 into
lightning-js.
MdkNode#getMaxSendablenow accepts an optionaldestinationand, when supplied, computes the fee budget from a realNode::find_routeagainst that payee instead of the static 1% buffer.JS surface
nullstill means "no usable LSP channel" (transient case).and routing failures throw napi errors instead of returning
null.routeRetryFeeMultiplierBpsonMaxSendableConfig(default 11_000 = 1.1x) scales the reported fee budget so retries
along a costlier path still fit.
Scope
invoice/HRN resolution moves into the estimator.
InvalidArg(the payee dictated theamount, nothing to estimate).