feat: probe payment paths before LDK Node payments to improve routing

This commit is contained in:
Evan Kaloudis
2026-04-14 17:23:59 -04:00
parent 87f0ff4716
commit 4ea6ca123c
6 changed files with 185 additions and 1 deletions
@@ -1297,6 +1297,48 @@ class LdkNodeModule(reactContext: ReactApplicationContext) : ReactContextBaseJav
}
}
// Probe Methods
@ReactMethod
fun sendBolt11Probes(invoice: String, maxTotalRoutingFeeMsat: Double, maxPathCount: Double, promise: Promise) {
val routeParams = buildRouteParameters(maxTotalRoutingFeeMsat, maxPathCount)
moduleScope.launch {
try {
val node = this@LdkNodeModule.node ?: throw Exception("Node not initialized")
val bolt11 = node.bolt11Payment()
val invoiceObj = Bolt11Invoice.fromStr(invoice)
bolt11.sendProbes(invoiceObj, routeParams)
withContext(Dispatchers.Main) {
promise.resolve(null)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
promise.reject("error", errorMessage(e))
}
}
}
}
@ReactMethod
fun sendBolt11ProbesUsingAmount(invoice: String, amountMsat: Double, maxTotalRoutingFeeMsat: Double, maxPathCount: Double, promise: Promise) {
val routeParams = buildRouteParameters(maxTotalRoutingFeeMsat, maxPathCount)
moduleScope.launch {
try {
val node = this@LdkNodeModule.node ?: throw Exception("Node not initialized")
val bolt11 = node.bolt11Payment()
val invoiceObj = Bolt11Invoice.fromStr(invoice)
bolt11.sendProbesUsingAmount(invoiceObj, amountMsat.toLong().toULong(), routeParams)
withContext(Dispatchers.Main) {
promise.resolve(null)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
promise.reject("error", errorMessage(e))
}
}
}
}
// Spontaneous Payment Methods
@ReactMethod
+4
View File
@@ -65,6 +65,10 @@ RCT_EXTERN_METHOD(receiveVariableAmountBolt11:(NSString *)invoiceDescription exp
RCT_EXTERN_METHOD(sendBolt11:(NSString *)invoice maxTotalRoutingFeeMsat:(double)maxTotalRoutingFeeMsat maxPathCount:(double)maxPathCount resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(sendBolt11UsingAmount:(NSString *)invoice amountMsat:(double)amountMsat maxTotalRoutingFeeMsat:(double)maxTotalRoutingFeeMsat maxPathCount:(double)maxPathCount resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
// Probe Methods
RCT_EXTERN_METHOD(sendBolt11Probes:(NSString *)invoice maxTotalRoutingFeeMsat:(double)maxTotalRoutingFeeMsat maxPathCount:(double)maxPathCount resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(sendBolt11ProbesUsingAmount:(NSString *)invoice amountMsat:(double)amountMsat maxTotalRoutingFeeMsat:(double)maxTotalRoutingFeeMsat maxPathCount:(double)maxPathCount resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
// Spontaneous Payment Methods
RCT_EXTERN_METHOD(sendSpontaneousPayment:(NSString *)nodeId amountMsat:(double)amountMsat maxTotalRoutingFeeMsat:(double)maxTotalRoutingFeeMsat maxPathCount:(double)maxPathCount resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
+44
View File
@@ -1157,6 +1157,50 @@ class LdkNodeModule: RCTEventEmitter {
}
}
// MARK: - Probe Methods
@objc(sendBolt11Probes:maxTotalRoutingFeeMsat:maxPathCount:resolver:rejecter:)
func sendBolt11Probes(_ invoice: String, maxTotalRoutingFeeMsat: Double, maxPathCount: Double, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
guard let node = self.getNode() else {
reject("error", "Node not initialized", nil)
return
}
let routeParams = buildRouteParameters(maxTotalRoutingFeeMsat: maxTotalRoutingFeeMsat, maxPathCount: maxPathCount)
DispatchQueue.global(qos: .userInitiated).async {
do {
let bolt11 = node.bolt11Payment()
let bolt11Invoice = try Bolt11Invoice.fromStr(invoiceStr: invoice)
try bolt11.sendProbes(invoice: bolt11Invoice, routeParameters: routeParams)
resolve(nil)
} catch {
reject("error", self.errorMessage(error), error)
}
}
}
@objc(sendBolt11ProbesUsingAmount:amountMsat:maxTotalRoutingFeeMsat:maxPathCount:resolver:rejecter:)
func sendBolt11ProbesUsingAmount(_ invoice: String, amountMsat: Double, maxTotalRoutingFeeMsat: Double, maxPathCount: Double, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {
guard let node = self.getNode() else {
reject("error", "Node not initialized", nil)
return
}
let routeParams = buildRouteParameters(maxTotalRoutingFeeMsat: maxTotalRoutingFeeMsat, maxPathCount: maxPathCount)
DispatchQueue.global(qos: .userInitiated).async {
do {
let bolt11 = node.bolt11Payment()
let bolt11Invoice = try Bolt11Invoice.fromStr(invoiceStr: invoice)
try bolt11.sendProbesUsingAmount(invoice: bolt11Invoice, amountMsat: UInt64(amountMsat), routeParameters: routeParams)
resolve(nil)
} catch {
reject("error", self.errorMessage(error), error)
}
}
}
// MARK: - Spontaneous Payment Methods
@objc(sendSpontaneousPayment:amountMsat:maxTotalRoutingFeeMsat:maxPathCount:resolver:rejecter:)
+13
View File
@@ -565,6 +565,19 @@ export interface ILdkNodeModule {
maxPathCount: number
): Promise<string>;
// Probe Methods
sendBolt11Probes(
invoice: string,
maxTotalRoutingFeeMsat: number,
maxPathCount: number
): Promise<void>;
sendBolt11ProbesUsingAmount(
invoice: string,
amountMsat: number,
maxTotalRoutingFeeMsat: number,
maxPathCount: number
): Promise<void>;
// Spontaneous Payment Methods
sendSpontaneousPayment(
nodeId: string,
+53 -1
View File
@@ -464,6 +464,45 @@ const sendBolt11UsingAmount = async ({
return result.paymentId;
};
// ============================================================================
// Probe Functions
// ============================================================================
const sendBolt11Probes = async ({
invoice,
maxTotalRoutingFeeMsat,
maxPathCount
}: {
invoice: string;
maxTotalRoutingFeeMsat?: number;
maxPathCount?: number;
}): Promise<void> => {
await LdkNodeModule.sendBolt11Probes(
invoice,
maxTotalRoutingFeeMsat ?? -1,
maxPathCount ?? -1
);
};
const sendBolt11ProbesUsingAmount = async ({
invoice,
amountMsat,
maxTotalRoutingFeeMsat,
maxPathCount
}: {
invoice: string;
amountMsat: number;
maxTotalRoutingFeeMsat?: number;
maxPathCount?: number;
}): Promise<void> => {
await LdkNodeModule.sendBolt11ProbesUsingAmount(
invoice,
amountMsat,
maxTotalRoutingFeeMsat ?? -1,
maxPathCount ?? -1
);
};
// ============================================================================
// Spontaneous Payment Functions
// ============================================================================
@@ -1062,6 +1101,17 @@ export interface ILdkNodeInjections {
maxTotalRoutingFeeMsat?: number;
maxPathCount?: number;
}) => Promise<string>;
sendBolt11Probes: (params: {
invoice: string;
maxTotalRoutingFeeMsat?: number;
maxPathCount?: number;
}) => Promise<void>;
sendBolt11ProbesUsingAmount: (params: {
invoice: string;
amountMsat: number;
maxTotalRoutingFeeMsat?: number;
maxPathCount?: number;
}) => Promise<void>;
};
spontaneous: {
sendSpontaneousPayment: (params: {
@@ -1237,7 +1287,9 @@ const LdkNodeInjection: ILdkNodeInjections = {
receiveBolt11,
receiveVariableAmountBolt11,
sendBolt11,
sendBolt11UsingAmount
sendBolt11UsingAmount,
sendBolt11Probes,
sendBolt11ProbesUsingAmount
},
spontaneous: {
sendSpontaneousPayment
+29
View File
@@ -14,6 +14,7 @@ import LSPStore from './LSPStore';
import BackendUtils from '../utils/BackendUtils';
import { localeString } from '../utils/LocaleUtils';
import { errorToUserFriendly } from '../utils/ErrorUtils';
import LdkNodeInjection from '../ldknode/LdkNodeInjection';
import ChannelsStore from './ChannelsStore';
import NodeInfoStore from './NodeInfoStore';
import WithdrawalRequest from '../models/WithdrawalRequest';
@@ -70,6 +71,14 @@ export default class InvoicesStore {
this.pay_req.getRequestAmount
);
}
// Send probes for LDK Node to warm up the scorer
// while the user reviews the payment request
if (
this.pay_req &&
this.settingsStore.implementation === 'ldk-node'
) {
this.sendLdkProbes();
}
}
);
}
@@ -760,4 +769,24 @@ export default class InvoicesStore {
})
.catch(() => this.getRoutesError());
};
/**
* Send probes along the payment path for LDK Node.
* This warms up the probabilistic scorer so the actual
* payment has better routing information. Fire-and-forget.
* Only probes fixed-amount invoices variable-amount
* invoices can't be probed without knowing the amount.
*/
private sendLdkProbes = async () => {
try {
const amount = this.pay_req?.getRequestAmount;
if (!amount || Number(amount) <= 0) return;
await LdkNodeInjection.bolt11.sendBolt11Probes({
invoice: this.paymentRequest
});
} catch {
// Probe failures are expected and non-fatal
}
};
}