Skip to content

Commit 3be49b5

Browse files
committed
feat: add nodeId probe target support
1 parent 27cca1d commit 3be49b5

2 files changed

Lines changed: 158 additions & 26 deletions

File tree

test/helpers/probe.ts

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import path from 'node:path';
44

55
import { getAppId } from './constants';
66

7-
export type ProbeTargetType = 'lightningAddress' | 'lnurlCallback';
7+
export type ProbeTargetType = 'lightningAddress' | 'lnurlCallback' | 'nodeId';
88

99
export type ProbeTarget = {
1010
name: string;
@@ -14,11 +14,13 @@ export type ProbeTarget = {
1414
amountsMsat?: number[];
1515
address?: string;
1616
url?: string;
17+
nodeId?: string;
1718
};
1819

1920
export type ProbeResult = {
2021
targetName: string;
2122
targetType: ProbeTargetType;
23+
probeMode: 'invoice' | 'keysend';
2224
amountMsat: number;
2325
amountSats: number;
2426
required: boolean;
@@ -28,6 +30,7 @@ export type ProbeResult = {
2830
success: boolean;
2931
durationMs: number;
3032
bolt11?: string;
33+
nodeId?: string;
3134
rawProviderResult?: string;
3235
error?: string;
3336
};
@@ -129,9 +132,17 @@ export async function fetchBolt11ForProbe(
129132
return response.pr;
130133
}
131134

132-
export function runProbeCommand(target: ProbeTarget, amountMsat: number, bolt11: string): string {
135+
export function probeModeForTargetType(type: ProbeTargetType): 'invoice' | 'keysend' {
136+
return type === 'nodeId' ? 'keysend' : 'invoice';
137+
}
138+
139+
export function runProbeInvoiceCommand(
140+
target: ProbeTarget,
141+
amountMsat: number,
142+
bolt11: string
143+
): string {
133144
const amountSats = amountMsat / 1000;
134-
const method = process.env.PROBE_CONTENT_METHOD ?? 'probeInvoice';
145+
const method = process.env.PROBE_INVOICE_METHOD ?? 'probeInvoice';
135146
const timeoutSeconds =
136147
parsePositiveIntEnv('PROBE_TIMEOUT_SECONDS') ?? DEFAULT_PROBE_TIMEOUT_SECONDS;
137148
const payload = {
@@ -141,21 +152,29 @@ export function runProbeCommand(target: ProbeTarget, amountMsat: number, bolt11:
141152
amountSats,
142153
timeoutSeconds,
143154
};
144-
const command = [
145-
'content',
146-
'call',
147-
'--uri',
148-
shellQuote(`content://${getAppId()}.devtools`),
149-
'--method',
150-
shellQuote(method),
151-
'--arg',
152-
shellQuote(JSON.stringify(payload)),
153-
].join(' ');
154155

155-
return execFileSync('adb', ['shell', command], {
156-
encoding: 'utf8',
157-
timeout: (timeoutSeconds + 10) * 1000,
158-
});
156+
return runDevToolsCommand(method, payload, timeoutSeconds);
157+
}
158+
159+
export function runProbeNodeCommand(target: ProbeTarget, amountMsat: number): string {
160+
const nodeId = target.nodeId;
161+
if (!nodeId) {
162+
throw new Error(`Probe target '${target.name}' is missing nodeId`);
163+
}
164+
165+
const amountSats = amountMsat / 1000;
166+
const method = process.env.PROBE_NODE_METHOD ?? 'probeNode';
167+
const timeoutSeconds =
168+
parsePositiveIntEnv('PROBE_TIMEOUT_SECONDS') ?? DEFAULT_PROBE_TIMEOUT_SECONDS;
169+
const payload = {
170+
targetName: target.name,
171+
nodeId,
172+
amountMsat,
173+
amountSats,
174+
timeoutSeconds,
175+
};
176+
177+
return runDevToolsCommand(method, payload, timeoutSeconds);
159178
}
160179

161180
export function parseProbeCommandSuccess(raw: string): boolean {
@@ -356,17 +375,18 @@ export function renderProbeReport(
356375
`Required failures: ${failedRequired.length}`,
357376
`Readiness at probe start: ${readiness ? summarizeProbeReadiness(readiness) : 'not captured'}`,
358377
'',
359-
'| Target | Amount sats | Required | Invoice | Probe | Retries | Duration ms | Failure |',
360-
'| --- | ---: | --- | --- | --- | ---: | ---: | --- |',
378+
'| Target | Type | Amount sats | Required | Fetch | Probe | Retries | Duration ms | Failure |',
379+
'| --- | --- | ---: | --- | --- | --- | ---: | ---: | --- |',
361380
];
362381

363382
for (const result of results) {
364383
lines.push(
365384
`| ${[
366385
result.targetName,
386+
result.probeMode,
367387
result.amountSats.toString(),
368388
result.required ? 'yes' : 'no',
369-
result.invoiceFetched ? 'ok' : 'failed',
389+
formatFetchCell(result),
370390
result.success ? '✅' : '❌',
371391
result.retries.toString(),
372392
result.durationMs.toString(),
@@ -387,15 +407,22 @@ function parseProbeTarget(value: unknown): ProbeTarget {
387407
if (!target.name || typeof target.name !== 'string') {
388408
throw new Error('Each probe target must define a string name');
389409
}
390-
if (target.type !== 'lightningAddress' && target.type !== 'lnurlCallback') {
391-
throw new Error(`Probe target '${target.name}' has unsupported type '${target.type}'`);
410+
if (
411+
target.type !== 'lightningAddress' &&
412+
target.type !== 'lnurlCallback' &&
413+
target.type !== 'nodeId'
414+
) {
415+
throw new Error(`Probe target '${target.name}' has unsupported type '${String(target.type)}'`);
392416
}
393417
if (target.type === 'lightningAddress' && !target.address) {
394418
throw new Error(`Probe target '${target.name}' must define address`);
395419
}
396420
if (target.type === 'lnurlCallback' && !target.url) {
397421
throw new Error(`Probe target '${target.name}' must define url`);
398422
}
423+
if (target.type === 'nodeId' && !target.nodeId) {
424+
throw new Error(`Probe target '${target.name}' must define nodeId`);
425+
}
399426

400427
return {
401428
name: target.name,
@@ -405,6 +432,7 @@ function parseProbeTarget(value: unknown): ProbeTarget {
405432
amountsMsat: target.amountsMsat,
406433
address: target.address,
407434
url: target.url,
435+
nodeId: target.nodeId,
408436
};
409437
}
410438

@@ -518,6 +546,33 @@ function formatFailureCell(error: string): string {
518546
return `\`${sanitized}\``;
519547
}
520548

549+
function formatFetchCell(result: ProbeResult): string {
550+
if (result.probeMode === 'keysend') return 'n/a';
551+
return result.invoiceFetched ? 'ok' : 'failed';
552+
}
553+
554+
function runDevToolsCommand(
555+
method: string,
556+
payload: Record<string, unknown>,
557+
timeoutSeconds: number
558+
): string {
559+
const command = [
560+
'content',
561+
'call',
562+
'--uri',
563+
shellQuote(`content://${getAppId()}.devtools`),
564+
'--method',
565+
shellQuote(method),
566+
'--arg',
567+
shellQuote(JSON.stringify(payload)),
568+
].join(' ');
569+
570+
return execFileSync('adb', ['shell', command], {
571+
encoding: 'utf8',
572+
timeout: (timeoutSeconds + 10) * 1000,
573+
});
574+
}
575+
521576
function shellQuote(value: string): string {
522577
return `'${value.replace(/'/g, "'\\''")}'`;
523578
}

test/specs/mainnet/probe.e2e.ts

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import {
55
fetchBolt11ForProbe,
66
parseNonNegativeIntEnv,
77
parseProbeCommandSuccess,
8+
probeModeForTargetType,
89
resolveProbeTargets,
9-
runProbeCommand,
10+
runProbeInvoiceCommand,
11+
runProbeNodeCommand,
1012
summarizeProbeCommandFailure,
1113
waitForProbeReadiness,
1214
writeProbeArtifacts,
@@ -40,12 +42,13 @@ function resolveProbeRetryDelayMs(): number {
4042
return parseNonNegativeIntEnv('PROBE_RETRY_DELAY_MS') ?? DEFAULT_PROBE_RETRY_DELAY_MS;
4143
}
4244

43-
async function runProbe(target: ProbeTarget, amountMsat: number): Promise<ProbeResult> {
45+
async function runInvoiceProbe(target: ProbeTarget, amountMsat: number): Promise<ProbeResult> {
4446
const startedAt = Date.now();
4547
const amountSats = amountMsat / 1000;
4648
const baseResult = {
4749
targetName: target.name,
4850
targetType: target.type,
51+
probeMode: probeModeForTargetType(target.type),
4952
amountMsat,
5053
amountSats,
5154
required: target.required ?? true,
@@ -79,7 +82,7 @@ async function runProbe(target: ProbeTarget, amountMsat: number): Promise<ProbeR
7982
maxRetries + 1
8083
}...`
8184
);
82-
const rawProviderResult = runProbeCommand(target, amountMsat, bolt11);
85+
const rawProviderResult = runProbeInvoiceCommand(target, amountMsat, bolt11);
8386
lastRawProviderResult = rawProviderResult;
8487
const success = parseProbeCommandSuccess(rawProviderResult);
8588

@@ -118,6 +121,80 @@ async function runProbe(target: ProbeTarget, amountMsat: number): Promise<ProbeR
118121
};
119122
}
120123

124+
async function runNodeProbe(target: ProbeTarget, amountMsat: number): Promise<ProbeResult> {
125+
const startedAt = Date.now();
126+
const amountSats = amountMsat / 1000;
127+
const nodeId = target.nodeId;
128+
if (!nodeId) {
129+
throw new Error(`Probe target '${target.name}' is missing nodeId`);
130+
}
131+
132+
const baseResult = {
133+
targetName: target.name,
134+
targetType: target.type,
135+
probeMode: probeModeForTargetType(target.type),
136+
amountMsat,
137+
amountSats,
138+
required: target.required ?? true,
139+
attempt: Number.parseInt(process.env.ATTEMPT ?? '1', 10),
140+
nodeId,
141+
invoiceFetched: false,
142+
};
143+
144+
const maxRetries = resolveProbeRetries();
145+
const retryDelayMs = resolveProbeRetryDelayMs();
146+
let lastRawProviderResult = '';
147+
let lastError = 'Probe command returned a failed result';
148+
149+
for (let retry = 0; retry <= maxRetries; retry++) {
150+
try {
151+
console.info(
152+
`→ [Probe] Keysend probing '${target.name}' (${amountSats} sats), attempt ${retry + 1}/${
153+
maxRetries + 1
154+
}...`
155+
);
156+
const rawProviderResult = runProbeNodeCommand(target, amountMsat);
157+
lastRawProviderResult = rawProviderResult;
158+
const success = parseProbeCommandSuccess(rawProviderResult);
159+
160+
if (success) {
161+
return {
162+
...baseResult,
163+
retries: retry,
164+
success: true,
165+
durationMs: Date.now() - startedAt,
166+
rawProviderResult,
167+
};
168+
}
169+
170+
lastError = summarizeProbeCommandFailure(rawProviderResult);
171+
} catch (error) {
172+
lastError = error instanceof Error ? error.message : String(error);
173+
}
174+
175+
if (retry < maxRetries && retryDelayMs > 0) {
176+
console.info(`→ [Probe] Retrying '${target.name}' in ${retryDelayMs / 1000}s...`);
177+
await sleep(retryDelayMs);
178+
}
179+
}
180+
181+
return {
182+
...baseResult,
183+
retries: maxRetries,
184+
success: false,
185+
durationMs: Date.now() - startedAt,
186+
rawProviderResult: lastRawProviderResult,
187+
error: lastError,
188+
};
189+
}
190+
191+
async function runProbe(target: ProbeTarget, amountMsat: number): Promise<ProbeResult> {
192+
if (target.type === 'nodeId') {
193+
return runNodeProbe(target, amountMsat);
194+
}
195+
return runInvoiceProbe(target, amountMsat);
196+
}
197+
121198
describe('@probe_mainnet - Lightning probe smoke', () => {
122199
let probeSeed: string;
123200
let targets: ProbeTarget[];
@@ -152,7 +229,7 @@ describe('@probe_mainnet - Lightning probe smoke', () => {
152229
const result = await runProbe(target, amountMsat);
153230
results.push(result);
154231
console.info(
155-
`→ [Probe] ${result.targetName} ${result.amountSats} sats: ${
232+
`→ [Probe] ${result.targetName} ${result.amountSats} sats (${result.probeMode}): ${
156233
result.success ? '✅ success' : `❌ failed (${result.error ?? 'unknown'})`
157234
}`
158235
);

0 commit comments

Comments
 (0)