clboss/Boss/Mod/InvoicePayer.cpp
Ken Sedgwick 2073303b60
InvoicePayer: migrate pay -> xpay for CLN v26.06 compat
The third v26.06 deprecation hitting CLBOSS.  pay was deprecated
in v26.06 with last supported version v27.03 (tighter than
getroute's v27.06 window) and is replaced by xpay, which has been
available since v24.11 -- so this is a straight migration with no
compat bridge required on any CLN version we care about.

The sole producer of Msg::PayInvoice is SwapManager paying a
Boltz swap-out invoice (Boss/Mod/SwapManager.cpp:541): when
CLBOSS does an off-chain to on-chain swap, Boltz issues a bolt11
invoice for the off-chain side and CLBOSS pays it; Boltz then
sends the equivalent on-chain.

- pay -> xpay
- request field bolt11 -> invstring
- replace maxfeepercent (a percentage) with maxfee (an absolute
  msat amount) computed from the decoded invoice amount; the
  fraction 0.005 preserves the legacy non-MPP 0.5% cap.

xpay's own default maxfee is 1% (5000msat floor), which would
have doubled our historical fee budget on swap-outs.  Because
this code path is exclusively swap-out invoice payment -- real
money on prod -- we set an explicit absolute maxfee instead of
accepting xpay's default.  Worst-case extra fee per swap was
bounded at 0.5% of the swap amount under the default; this
keeps it at the historical 0.5%.

- The MPP feature-bit detection (hex-parsing the bolt11 features
  string and checking bits 16/17) is removed.  Its sole purpose
  was to pick between two maxfeepercent values for pre-xpay pay,
  whose pre-v0.10 MPP fee-budget management was poor and required
  a generous 5% cap to succeed.  xpay's askrene-driven multi-path
  routing manages its own per-part fee budget, so we always pass
  a single absolute maxfee and let xpay split or not as needed.
- The Util::Str::HexParseFailure catch path is removed (no
  longer reachable -- we no longer parse hex features).
- The Util/Str.hpp include is dropped.
- The nonmpp_maxfeepercent and mpp_maxfeepercent constants are
  replaced by a single maxfee_fraction.

- The decode round-trip and validation that the input is a
  valid bolt11 invoice.  Boltz swap-out invoices are always
  bolt11, so this stays useful as a sanity check.  The decode
  is now also used to extract amount_msat for the maxfee
  computation; if the invoice is amountless (which Boltz never
  emits) the parser throws Jsmn::TypeError and the existing
  "Unexpected decode result" log path catches it.
- retry_for = 1000 (matches historical behavior).
- The RpcError / Jsmn::TypeError catching pattern; response
  parsing is unchanged (we still consume the response as
  Jsmn::Object _ without reading fields).
2026-08-04 11:01:42 -07:00

132 lines
3.9 KiB
C++

#include"Boss/Mod/InvoicePayer.hpp"
#include"Boss/Mod/Rpc.hpp"
#include"Boss/Msg/Init.hpp"
#include"Boss/Msg/PayInvoice.hpp"
#include"Boss/concurrent.hpp"
#include"Boss/log.hpp"
#include"Ev/Io.hpp"
#include"Ev/foreach.hpp"
#include"Jsmn/Object.hpp"
#include"Json/Out.hpp"
#include"Ln/Amount.hpp"
#include"S/Bus.hpp"
#include<memory>
namespace {
/* Maximum routing fee we are willing to pay: 1% of the invoice
* amount, floored at 5000 msat for small invoices -- the same
* budget xpay would apply if maxfee were omitted, made explicit
* here so the policy is visible at the call site and in review.
* History: the legacy `pay` call used maxfeepercent=5.0 for
* MPP-capable invoices (all Boltz swap-out invoices are), with
* realized fees observed under 1%; an earlier xpay-migration pass
* carried over the 0.5% non-MPP cap instead -- the branch that
* never applied to these invoices -- which is 2x tighter than
* xpay's own default and strands swap payments from modestly-
* connected nodes. A fee-capped failure on the sole PayInvoice
* producer (SwapManager swap-outs) just stalls inbound-liquidity
* acquisition, and SwapManager's amount-reduction retries cannot
* help against a proportional cap. Integer math to avoid float
* rounding drift on larger invoices.
*/
auto constexpr maxfee_divisor = std::uint64_t(100);
auto constexpr maxfee_floor_msat = std::uint64_t(5000);
}
namespace Boss { namespace Mod {
void InvoicePayer::start() {
bus.subscribe<Msg::Init
>([this](Msg::Init const& init) {
rpc = &init.rpc;
/* Pay pending invoices. */
auto f = [this](std::string invoice) {
return pay(std::move(invoice));
};
return Boss::concurrent(
Ev::foreach(f, std::move(pending_invoices))
);
});
bus.subscribe<Msg::PayInvoice
>([this](Msg::PayInvoice const& p) {
if (!rpc) {
/* Not yet ready, add to pending. */
pending_invoices.push_back(p.invoice);
return Ev::lift();
}
return Boss::concurrent(pay(p.invoice));
});
}
Ev::Io<void> InvoicePayer::pay(std::string n_invoice) {
auto inv = std::make_shared<std::string>(std::move(n_invoice));
return Ev::lift().then([this, inv]() {
return Boss::log( bus, Debug
, "InvoicePayer: Initiating: %s"
, inv->c_str()
);
}).then([this, inv]() {
auto parms = Json::Out()
.start_object()
.field("string", *inv)
.end_object()
;
return rpc->command("decode", std::move(parms));
}).then([this, inv](Jsmn::Object res) {
if (!res.has("type")
|| std::string(res["type"]) != "bolt11 invoice"
|| !res.has("valid")
|| !res["valid"].is_boolean()
|| !bool(res["valid"])
|| !res.has("amount_msat")
) {
throw Jsmn::TypeError();
}
/* Compute an absolute maxfee from the invoice amount
* (policy documented on maxfee_divisor above). xpay's
* MPP handling makes the legacy feature-bit-driven MPP
* branch unnecessary: askrene + xpay manage the fee
* budget across parts internally.
*/
auto amount = Ln::Amount::object(res["amount_msat"]);
auto maxfee_msat = amount.to_msat() / maxfee_divisor;
if (maxfee_msat < maxfee_floor_msat)
maxfee_msat = maxfee_floor_msat;
/* TODO: Get created_at and expiry, add them, then determine
* current time and subtract, to get retry_for.
*/
auto retry_for = 1000;
auto parms = Json::Out()
.start_object()
.field("invstring", *inv)
.field("retry_for", retry_for)
.field("maxfee", maxfee_msat)
.end_object()
;
return rpc->command("xpay", std::move(parms));
}).then([this, inv](Jsmn::Object _) {
return Boss::log( bus, Debug
, "InvoicePayer: Paid: %s"
, inv->c_str()
);
}).catching<RpcError>([this, inv](RpcError const& _) {
return Boss::log( bus, Debug
, "InvoicePayer: Failed to pay: %s"
, inv->c_str()
);
}).catching<Jsmn::TypeError>([this, inv](Jsmn::TypeError const& _) {
return Boss::log( bus, Error
, "InvoicePayer: "
"Unexpected decode result for invoice: %s"
, inv->c_str()
);
});
}
}}