Skip to content

Commit 2a58e38

Browse files
authored
Merge pull request #191 from synonymdev/feat/route-fee
feat(probe): include route fee in reports
2 parents 1d0149b + d3a101d commit 2a58e38

2 files changed

Lines changed: 77 additions & 35 deletions

File tree

test/helpers/probe.ts

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type ProbeResult = {
2929
invoiceFetched: boolean;
3030
success: boolean;
3131
durationMs: number;
32+
routeFeeMsat?: number;
3233
bolt11?: string;
3334
nodeId?: string;
3435
rawProviderResult?: string;
@@ -49,6 +50,12 @@ type LnurlInvoiceResponse = {
4950
reason?: string;
5051
};
5152

53+
export type ProbeCommandResult = {
54+
success: boolean;
55+
durationMs?: number;
56+
routeFeeMsat?: number;
57+
};
58+
5259
const DEFAULT_PROBE_TIMEOUT_SECONDS = 90;
5360
const DEFAULT_PROBE_FETCH_RETRIES = 2;
5461
const DEFAULT_PROBE_FETCH_RETRY_DELAY_MS = 1_000;
@@ -234,24 +241,56 @@ export function runProbeNodeCommand(target: ProbeTarget, amountMsat: number): st
234241
return runDevToolsCommand(method, payload, timeoutSeconds);
235242
}
236243

237-
export function parseProbeCommandSuccess(raw: string): boolean {
244+
function parseDevResultPayload(raw: string): Record<string, unknown> | null {
238245
const result = extractContentCallResult(raw);
239-
if (!result) return false;
246+
if (!result) return null;
240247

241248
let parsed: unknown;
242249
try {
243250
parsed = JSON.parse(result);
244251
} catch {
245-
return false;
252+
return null;
246253
}
247-
if (typeof parsed !== 'object' || parsed === null) return false;
254+
if (typeof parsed !== 'object' || parsed === null) return null;
255+
256+
return parsed as Record<string, unknown>;
257+
}
258+
259+
function parseDevResultSuccess(payload: Record<string, unknown>): boolean {
260+
if ('success' in payload) return payload.success === true;
261+
262+
const type = payload.type;
263+
return (
264+
typeof type === 'string' && (type === 'Success' || type.endsWith('.ProbeSuccess'))
265+
);
266+
}
267+
268+
export function parseProbeCommandResult(raw: string): ProbeCommandResult | null {
269+
const payload = parseDevResultPayload(raw);
270+
if (!payload) return null;
271+
272+
return {
273+
success: parseDevResultSuccess(payload),
274+
durationMs: parseOptionalNumber(payload.durationMs),
275+
routeFeeMsat: parseOptionalNumber(payload.routeFeeMsat),
276+
};
277+
}
248278

249-
if ('success' in parsed) return parsed.success === true;
250-
if ('type' in parsed && typeof parsed.type === 'string') {
251-
return parsed.type === 'Success' || parsed.type.endsWith('.ProbeSuccess');
279+
export function parseProbeCommandSuccess(raw: string): boolean {
280+
const payload = parseDevResultPayload(raw);
281+
return payload ? parseDevResultSuccess(payload) : false;
282+
}
283+
284+
function parseOptionalNumber(value: unknown): number | undefined {
285+
if (typeof value === 'number' && Number.isFinite(value)) {
286+
return value;
287+
}
288+
if (typeof value === 'string') {
289+
const parsedValue = Number.parseInt(value, 10);
290+
return Number.isFinite(parsedValue) ? parsedValue : undefined;
252291
}
253292

254-
return false;
293+
return undefined;
255294
}
256295

257296
export function summarizeProbeCommandFailure(raw: string): string {
@@ -299,10 +338,11 @@ export async function resetPathfindingScores({
299338
console.info(`→ [${logPrefix}] Resetting pathfinding scores (timeout ${timeoutSeconds}s)...`);
300339
const fallbackFloorS = getDeviceEpochSeconds();
301340
const raw = runDevToolsCommand(method, {}, timeoutSeconds);
302-
if (!parseProbeCommandSuccess(raw)) {
341+
const payload = parseDevResultPayload(raw);
342+
if (!payload || !parseDevResultSuccess(payload)) {
303343
throw new Error(`Pathfinding scores reset failed: ${summarizeProbeCommandFailure(raw)}`);
304344
}
305-
const deviceResetAtS = parseResetTimestamp(raw);
345+
const deviceResetAtS = parseResetTimestamp(payload);
306346
if (deviceResetAtS === null) {
307347
console.warn(
308348
`→ [${logPrefix}] Reset result has no timestamp (old app build?); using pre-reset device time as scores sync floor`
@@ -313,20 +353,8 @@ export async function resetPathfindingScores({
313353
return resetFloorS;
314354
}
315355

316-
function parseResetTimestamp(raw: string): number | null {
317-
const result = extractContentCallResult(raw);
318-
if (!result) return null;
319-
320-
let parsed: unknown;
321-
try {
322-
parsed = JSON.parse(result);
323-
} catch {
324-
return null;
325-
}
326-
if (typeof parsed !== 'object' || parsed === null) return null;
327-
if (!('timestamp' in parsed)) return null;
328-
329-
const timestamp = parsed.timestamp;
356+
function parseResetTimestamp(payload: Record<string, unknown>): number | null {
357+
const timestamp = payload.timestamp;
330358
return typeof timestamp === 'number' && Number.isFinite(timestamp) && timestamp > 0
331359
? timestamp
332360
: null;
@@ -557,8 +585,8 @@ export function renderProbeReport(
557585
`Scores reset: ${scoresResetForReport()}`,
558586
`Readiness at probe start: ${readiness ? summarizeProbeReadiness(readiness) : 'not captured'}`,
559587
'',
560-
'| Target | Type | Amount sats | Required | Fetch | Probe | Retries | Duration ms | Failure |',
561-
'| --- | --- | ---: | --- | --- | --- | ---: | ---: | --- |',
588+
'| Target | Type | Amount sats | Required | Fetch | Probe | Retries | Duration ms | Route fee msat | Failure |',
589+
'| --- | --- | ---: | --- | --- | --- | ---: | ---: | ---: | --- |',
562590
];
563591

564592
for (const result of results) {
@@ -572,6 +600,7 @@ export function renderProbeReport(
572600
result.success ? '✅' : '❌',
573601
result.retries.toString(),
574602
result.durationMs.toString(),
603+
formatRouteFeeCell(result),
575604
result.success ? '' : formatFailureCell(result.error ?? ''),
576605
].join(' | ')} |`
577606
);
@@ -684,6 +713,9 @@ async function fetchJsonOnce<T>(url: string): Promise<T> {
684713
if (!response.ok) {
685714
throw new Error(`HTTP ${response.status} for ${url}${formatResponseBody(text)}`);
686715
}
716+
if (text.trim().length === 0) {
717+
throw new Error(`HTTP ${response.status} for ${url} returned an empty response body`);
718+
}
687719

688720
return JSON.parse(text) as T;
689721
}
@@ -759,6 +791,10 @@ function formatFetchCell(result: ProbeResult): string {
759791
return result.invoiceFetched ? 'ok' : 'failed';
760792
}
761793

794+
function formatRouteFeeCell(result: ProbeResult): string {
795+
return result.routeFeeMsat === undefined ? '' : result.routeFeeMsat.toString();
796+
}
797+
762798
function runDevToolsCommand(
763799
method: string,
764800
payload: Record<string, unknown>,

test/specs/mainnet/probe.e2e.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
buildProbeQueue,
55
fetchBolt11ForProbe,
66
parseNonNegativeIntEnv,
7-
parseProbeCommandSuccess,
7+
parseProbeCommandResult,
88
probeModeForTargetType,
99
resolveProbeAmountProfile,
1010
resetPathfindingScores,
@@ -88,15 +88,16 @@ async function runInvoiceProbe(target: ProbeTarget, amountMsat: number): Promise
8888
);
8989
const rawProviderResult = runProbeInvoiceCommand(target, amountMsat, bolt11);
9090
lastRawProviderResult = rawProviderResult;
91-
const success = parseProbeCommandSuccess(rawProviderResult);
91+
const providerResult = parseProbeCommandResult(rawProviderResult);
9292

93-
if (success) {
93+
if (providerResult?.success) {
9494
return {
9595
...baseResult,
9696
retries: retry,
9797
invoiceFetched: true,
9898
success: true,
99-
durationMs: Date.now() - startedAt,
99+
durationMs: providerResult.durationMs ?? Date.now() - startedAt,
100+
routeFeeMsat: providerResult.routeFeeMsat,
100101
bolt11,
101102
rawProviderResult,
102103
};
@@ -113,12 +114,14 @@ async function runInvoiceProbe(target: ProbeTarget, amountMsat: number): Promise
113114
}
114115
}
115116

117+
const providerResult = parseProbeCommandResult(lastRawProviderResult);
116118
return {
117119
...baseResult,
118120
retries: maxRetries,
119121
invoiceFetched: true,
120122
success: false,
121-
durationMs: Date.now() - startedAt,
123+
durationMs: providerResult?.durationMs ?? Date.now() - startedAt,
124+
routeFeeMsat: providerResult?.routeFeeMsat,
122125
bolt11,
123126
rawProviderResult: lastRawProviderResult,
124127
error: lastError,
@@ -159,14 +162,15 @@ async function runNodeProbe(target: ProbeTarget, amountMsat: number): Promise<Pr
159162
);
160163
const rawProviderResult = runProbeNodeCommand(target, amountMsat);
161164
lastRawProviderResult = rawProviderResult;
162-
const success = parseProbeCommandSuccess(rawProviderResult);
165+
const providerResult = parseProbeCommandResult(rawProviderResult);
163166

164-
if (success) {
167+
if (providerResult?.success) {
165168
return {
166169
...baseResult,
167170
retries: retry,
168171
success: true,
169-
durationMs: Date.now() - startedAt,
172+
durationMs: providerResult.durationMs ?? Date.now() - startedAt,
173+
routeFeeMsat: providerResult.routeFeeMsat,
170174
rawProviderResult,
171175
};
172176
}
@@ -182,11 +186,13 @@ async function runNodeProbe(target: ProbeTarget, amountMsat: number): Promise<Pr
182186
}
183187
}
184188

189+
const providerResult = parseProbeCommandResult(lastRawProviderResult);
185190
return {
186191
...baseResult,
187192
retries: maxRetries,
188193
success: false,
189-
durationMs: Date.now() - startedAt,
194+
durationMs: providerResult?.durationMs ?? Date.now() - startedAt,
195+
routeFeeMsat: providerResult?.routeFeeMsat,
190196
rawProviderResult: lastRawProviderResult,
191197
error: lastError,
192198
};

0 commit comments

Comments
 (0)