clboss/Boss/Mod/InvoicePayer.cpp
Ken Sedgwick 96dbec2d7d
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-05-22 10:52:08 -07:00

122 lines
3.4 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, expressed as a
* divisor of the invoice amount. 200 = 0.5% cap, preserving the
* historical pre-xpay `pay` non-MPP behaviour (xpay's own default
* would be 1%). Integer math: maxfee_msat = amount_msat / 200,
* avoiding the rounding drift a 0.005 floating-point multiplication
* would introduce on larger invoices. The only producer of
* Msg::PayInvoice is SwapManager paying a Boltz swap-out invoice,
* so tight fee discipline matters here.
*/
auto constexpr maxfee_divisor = std::uint64_t(200);
}
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,
* preserving the historical 0.5% cap via integer
* division. 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;
/* 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()
);
});
}
}}