Skip to content

fmt#46

Merged
ntheile merged 2 commits into
masterfrom
fmt
Jul 8, 2026
Merged

fmt#46
ntheile merged 2 commits into
masterfrom
fmt

Conversation

@ntheile

@ntheile ntheile commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added repository-wide formatting checks and automatic formatting support before commits.
  • Documentation

    • Updated contributor docs with setup and usage guidance for formatting tools and hooks.
  • Chores

    • Standardized code and test formatting across Rust, TypeScript, and example projects.
    • Added ignore/config settings to keep generated and build artifacts out of formatting runs.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR is largely a repository-wide code formatting pass, introducing Prettier configuration, formatting shell scripts, and a pre-commit git hook, then reformatting existing Rust crates and Node.js/TypeScript bindings into consistent multi-line style. It also refactors the NWC crate's request handling and adds a lightning-address helper.

Changes

Formatting Tooling and Configuration

Layer / File(s) Summary
Prettier config and git hook
.prettierrc.json, .prettierignore, .githooks/pre-commit, scripts/install-hooks.sh
Adds Prettier settings, ignore patterns, a pre-commit hook invoking format-staged.sh, and a script to configure core.hooksPath.
Formatting scripts and docs
scripts/format*.sh, scripts/README.md
Adds scripts for full formatting, format-checking, staged-file formatting, and TypeScript formatting via Prettier, plus documentation updates.

Rust Crate and Node.js Binding Changes

Layer / File(s) Summary
NWC lightning-address helper
crates/lni/nwc/api.rs
Adds lightning_address_from_nwc_uri, reworks get_info/pay_invoice/lookup_invoice/list_transactions to use execute_nwc_request consistently.
Core crate reformatting
crates/lni/lib.rs, crates/lni/permissions.rs, crates/lni/utils.rs, crates/lni/database.rs
Reformats macro-generated methods, mnemonic/HTTP helpers, permission normalization, and BOLT11/12 decoding.
Per-backend Rust reformatting
crates/lni/{blink,cln,lnd,phoenixd,speed,spark,nwc}/*.rs
Reformats method bodies, tests, and struct comments without changing logic.
Node.js napi bindings reformatting
bindings/lni_nodejs/src/*.rs
Reformats N-API wrapper implementations across all backend nodes.

TypeScript Bindings Reformatting

Layer / File(s) Summary
typescript-arkade reformatting
bindings/typescript-arkade/**
Reformats ArkadeBoltzNode implementation, types, README, and tests.
typescript-spark example app
bindings/typescript-spark/examples/spark-expo-go/App.tsx
Reformats Drizzle cache queries, invoice decoding, and UI/style definitions.
typescript-spark build/shims
bindings/typescript-spark/scripts/**
Reformats build vendor script and shims.
typescript-spark core implementation
bindings/typescript-spark/src/nodes/spark.ts, spark-runtime.ts
Reformats Lightning operations, Frost cryptography, and runtime helpers.
typescript-spark tests
bindings/typescript-spark/src/__tests__/**, tsconfig.build.json
Reformats unit/integration tests.
bindings/typescript core library
decode.ts, errors.ts, factory.ts, internal/*.ts, lnurl.ts, types.ts, README.md
Reformats core decoding, error, HTTP, and permission logic.
bindings/typescript node backends
src/nodes/{blink,cln,lnd,nwc,phoenixd,speed,strike}.ts
Reformats per-provider request/response handling.
bindings/typescript tests
src/__tests__/**
Reformats unit and integration tests.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NwcNode
  participant lightning_address_from_nwc_uri
  participant execute_nwc_request
  participant NWCRelay

  NwcNode->>lightning_address_from_nwc_uri: parse nwc_uri for lud16
  lightning_address_from_nwc_uri-->>NwcNode: validated Lightning Address
  NwcNode->>execute_nwc_request: get_info / get_balance
  execute_nwc_request->>NWCRelay: NIP-47 request
  NWCRelay-->>execute_nwc_request: response or timeout
  execute_nwc_request-->>NwcNode: NodeInfo / ApiError
Loading

Possibly related PRs

  • lightning-node-interface/lni#11: Main PR's NWC changes to crates/lni/nwc/api.rs (lightning-address helper, request rewiring) directly build on the NWC integration this PR added.
  • lightning-node-interface/lni#37: Main PR's reformatting of get_config() secret-redaction expressions across bindings/lni_nodejs/src/*.rs overlaps directly with this PR's redaction work.
  • lightning-node-interface/lni#42: Main PR's Rust-side NWC/LNURL changes underpin the TypeScript NWC wrapper and error handling this PR introduced.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is too vague to describe the actual changes; it only says "fmt". Use a concise title that names the main change, such as "Add formatting scripts and Prettier configuration".
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fmt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/README.md (1)

131-132: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the maintainer-specific path from the notes.

This bakes one checkout path into the docs and will look wrong for everyone else. The earlier repository-root note is enough.

💡 Suggested fix
-Run from `/Users/nick/code/lni` or another checkout root.
+Run from the repository root or any checkout root.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/README.md` around lines 131 - 132, Remove the maintainer-specific
checkout path from the notes in the README and keep the generic repository-root
guidance only. Update the text near the runtime/version note so it no longer
references the hardcoded path, and preserve the Node 20.19+/22.12+
recommendation as-is.
🧹 Nitpick comments (1)
scripts/format-staged.sh (1)

51-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

cargo fmt --all formats the entire workspace, not just staged files.

If a non-staged Rust file has formatting issues, cargo fmt --all will modify it, but lines 58–62 only re-stage files from rust_files. This leaves unstaged formatting changes in the working tree, which could be confusing for the developer.

This is a common tradeoff since cargo fmt doesn't support per-file formatting with --all. Consider documenting this behavior in the script or README so developers know to expect possible unstaged changes after commit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/format-staged.sh` at line 51, `cargo fmt --all` in `format-staged.sh`
formats the whole workspace, which can leave unstaged Rust changes behind after
only `rust_files` are re-staged. Update the script behavior or accompanying
developer docs/README to explicitly document that `cargo fmt --all
--manifest-path "$ROOT/Cargo.toml"` may modify non-staged files, and that only
formatted staged Rust files are re-added by the script; reference
`format-staged.sh` and the `rust_files` re-stage logic so it’s easy to find.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bindings/lni_nodejs/src/lib.rs`:
- Around line 77-90: The HTTP client setup in the request-building logic should
not automatically disable TLS verification when socks5_proxy is present. Update
the client construction in the helper around reqwest::Client::builder and
reqwest::Proxy::all so that danger_accept_invalid_certs(true) is removed from
the default proxy path, and only allow invalid certs behind an explicit opt-in
flag or configuration. Keep the existing proxy fallback behavior intact while
ensuring the default client created by the function still validates
certificates.

In `@bindings/lni_nodejs/src/spark.rs`:
- Around line 287-309: Clamp the polling values in the invoice polling loop
before converting them to durations: in `OnInvoiceEventParams` handling inside
`spark.rs`, ensure `polling_delay_sec` and `max_polling_sec` are validated to
non-negative bounds before the `as u64` casts used by the `while
start_time.elapsed()` condition and `tokio::time::sleep`. Update the
`lookup_invoice` polling logic so negative JS inputs cannot wrap into huge
`Duration` values, keeping the behavior bounded and predictable.

In `@bindings/typescript/src/__tests__/integration/blink.real.test.ts`:
- Around line 24-137: Remove the sensitive console logging from the Blink real
integration tests so CI never prints full invoice, payment, or transaction
payloads. In the test cases that use makeNode, createInvoice,
prepareOnchainTransaction, and payOnchain, delete the console.log calls that
dump the returned objects or replace them with non-sensitive status messages
only. Keep the assertions and flow intact, but ensure no full
invoice/payment/preimage or on-chain transaction details are emitted.

In `@bindings/typescript/src/__tests__/integration/cln.real.test.ts`:
- Around line 14-59: The test in the createInvoice + lookupInvoice +
listTransactions case is logging the full invoice payload, which can leak
sensitive data into CI output. Remove the console.log from the CLN integration
test and keep the existing assertions on invoice.invoice, invoice.paymentHash,
lookupInvoice, and listTransactions in place without exposing the invoice
object.

In `@bindings/typescript/src/__tests__/integration/lnd.real.test.ts`:
- Around line 14-60: Remove the invoice logging from the LND integration test by
deleting the console output in the createInvoice + lookupInvoice +
listTransactions case; update the test around node.createInvoice so it no longer
prints the full invoice payload to CI logs, while keeping the existing
assertions on invoice.invoice and invoice.paymentHash intact.

In `@bindings/typescript/src/__tests__/integration/phoenixd.real.test.ts`:
- Around line 27-52: Remove the console logging of the full invoice object in
the phoenixd integration test. In the createInvoice + lookupInvoice +
listTransactions case, delete the console.log call that prints the invoice
payload and keep the assertions/use of invoice.invoice and invoice.paymentHash
only. This change should be made in the test body around makeNode() and
createInvoice() so CI logs do not expose sensitive invoice data.

In `@bindings/typescript/src/__tests__/integration/speed.real.test.ts`:
- Around line 22-40: Remove the invoice logging from the speed test to avoid
exposing sensitive payment data in CI logs. Update the test in the getInfo +
createInvoice + listTransactions block by deleting the console.log tied to the
invoice variable, while keeping the existing assertions and flow unchanged.

In `@bindings/typescript/src/__tests__/nwc.test.ts`:
- Around line 215-225: The payInvoice test no longer asserts the wrapped
NwcError payload, so it only checks the thrown type and weakens NIP-47 error
coverage. Update the test around makeNode().payInvoice and NwcError to keep the
stronger assertion by verifying the preserved error details from the rejected
promise, not just the instance type, alongside the existing LniError rewrap
case.

In `@crates/lni/lib.rs`:
- Around line 305-316: The SOCKS5 client setup in the reqwest builder is
disabling TLS verification globally via the client_builder path, which should
not be the default. Remove the danger_accept_invalid_certs(true) usage from the
proxy/client construction and keep certificate validation enabled in the normal
flow. If special handling for self-signed endpoints is needed, gate it behind an
explicit opt-in in the same client-building logic around
reqwest::Client::builder(), reqwest::Proxy::all, and the build fallback path.

In `@crates/lni/nwc/api.rs`:
- Around line 97-131: Extract the duplicated pubkey parsing in NodeInfo
construction into a dedicated helper, and use it in both the get_info success
path and the Err fallback. Update the logic in the api.rs NWC handling code to
normalize all NWC URI scheme variants the same way as
lightning_address_from_nwc_uri does, so pubkey extraction works for
nostr+walletconnect://, nostrwalletconnect://, nostrwalletconnect:, and
nostr+walletconnect:. Keep the existing NodeInfo and config.nwc_uri call sites,
but route them through the new helper so the fallback no longer silently returns
an empty pubkey.

---

Outside diff comments:
In `@scripts/README.md`:
- Around line 131-132: Remove the maintainer-specific checkout path from the
notes in the README and keep the generic repository-root guidance only. Update
the text near the runtime/version note so it no longer references the hardcoded
path, and preserve the Node 20.19+/22.12+ recommendation as-is.

---

Nitpick comments:
In `@scripts/format-staged.sh`:
- Line 51: `cargo fmt --all` in `format-staged.sh` formats the whole workspace,
which can leave unstaged Rust changes behind after only `rust_files` are
re-staged. Update the script behavior or accompanying developer docs/README to
explicitly document that `cargo fmt --all --manifest-path "$ROOT/Cargo.toml"`
may modify non-staged files, and that only formatted staged Rust files are
re-added by the script; reference `format-staged.sh` and the `rust_files`
re-stage logic so it’s easy to find.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 355663b8-af64-4e8b-b35a-8efee565172c

📥 Commits

Reviewing files that changed from the base of the PR and between 9dae6a8 and 02ceb0d.

📒 Files selected for processing (93)
  • .githooks/pre-commit
  • .prettierignore
  • .prettierrc.json
  • bindings/lni_nodejs/src/blink.rs
  • bindings/lni_nodejs/src/cln.rs
  • bindings/lni_nodejs/src/lib.rs
  • bindings/lni_nodejs/src/lnd.rs
  • bindings/lni_nodejs/src/lnurl.rs
  • bindings/lni_nodejs/src/nwc.rs
  • bindings/lni_nodejs/src/phoenixd.rs
  • bindings/lni_nodejs/src/spark.rs
  • bindings/lni_nodejs/src/speed.rs
  • bindings/typescript-arkade/README.md
  • bindings/typescript-arkade/src/__tests__/arkade-boltz-node.test.ts
  • bindings/typescript-arkade/src/__tests__/integration/arkade-boltz.real.test.ts
  • bindings/typescript-arkade/src/__tests__/integration/helpers.ts
  • bindings/typescript-arkade/src/nodes/arkade-boltz.ts
  • bindings/typescript-arkade/src/types.ts
  • bindings/typescript-arkade/tsconfig.build.json
  • bindings/typescript-spark/README.md
  • bindings/typescript-spark/examples/spark-expo-go/App.tsx
  • bindings/typescript-spark/scripts/build-spark-vendor.mjs
  • bindings/typescript-spark/scripts/spark-shims/bare-crypto.js
  • bindings/typescript-spark/scripts/spark-shims/bare-fetch.js
  • bindings/typescript-spark/scripts/spark-shims/node-events.cjs
  • bindings/typescript-spark/scripts/spark-shims/vitest.js
  • bindings/typescript-spark/src/__tests__/integration/helpers.ts
  • bindings/typescript-spark/src/__tests__/integration/spark.real.test.ts
  • bindings/typescript-spark/src/__tests__/spark-node.test.ts
  • bindings/typescript-spark/src/__tests__/spark-runtime.test.ts
  • bindings/typescript-spark/src/nodes/spark.ts
  • bindings/typescript-spark/src/spark-runtime.ts
  • bindings/typescript-spark/tsconfig.build.json
  • bindings/typescript/README.md
  • bindings/typescript/src/__tests__/blink.test.ts
  • bindings/typescript/src/__tests__/decode.test.ts
  • bindings/typescript/src/__tests__/expo-polyfills.test.ts
  • bindings/typescript/src/__tests__/integration/blink.real.test.ts
  • bindings/typescript/src/__tests__/integration/cln.real.test.ts
  • bindings/typescript/src/__tests__/integration/helpers.ts
  • bindings/typescript/src/__tests__/integration/lnd.real.test.ts
  • bindings/typescript/src/__tests__/integration/lnurl.real.test.ts
  • bindings/typescript/src/__tests__/integration/nwc.real.test.ts
  • bindings/typescript/src/__tests__/integration/phoenixd.real.test.ts
  • bindings/typescript/src/__tests__/integration/speed.real.test.ts
  • bindings/typescript/src/__tests__/integration/strike.real.test.ts
  • bindings/typescript/src/__tests__/lnurl.test.ts
  • bindings/typescript/src/__tests__/nwc.test.ts
  • bindings/typescript/src/__tests__/permissions.test.ts
  • bindings/typescript/src/__tests__/provider-error-normalization.test.ts
  • bindings/typescript/src/__tests__/strike.test.ts
  • bindings/typescript/src/decode.ts
  • bindings/typescript/src/errors.ts
  • bindings/typescript/src/factory.ts
  • bindings/typescript/src/internal/encoding.ts
  • bindings/typescript/src/internal/error-normalization.ts
  • bindings/typescript/src/internal/http.ts
  • bindings/typescript/src/internal/permissions.ts
  • bindings/typescript/src/internal/sha256.ts
  • bindings/typescript/src/lnurl.ts
  • bindings/typescript/src/nodes/blink.ts
  • bindings/typescript/src/nodes/cln.ts
  • bindings/typescript/src/nodes/lnd.ts
  • bindings/typescript/src/nodes/nwc.ts
  • bindings/typescript/src/nodes/phoenixd.ts
  • bindings/typescript/src/nodes/speed.ts
  • bindings/typescript/src/nodes/strike.ts
  • bindings/typescript/src/types.ts
  • crates/lni/blink/lib.rs
  • crates/lni/blink/types.rs
  • crates/lni/cln/lib.rs
  • crates/lni/cln/types.rs
  • crates/lni/database.rs
  • crates/lni/lib.rs
  • crates/lni/lnd/lib.rs
  • crates/lni/lnd/types.rs
  • crates/lni/nwc/api.rs
  • crates/lni/nwc/lib.rs
  • crates/lni/nwc/types.rs
  • crates/lni/permissions.rs
  • crates/lni/phoenixd/lib.rs
  • crates/lni/phoenixd/types.rs
  • crates/lni/spark/api.rs
  • crates/lni/spark/lib.rs
  • crates/lni/speed/lib.rs
  • crates/lni/speed/types.rs
  • crates/lni/utils.rs
  • scripts/README.md
  • scripts/format-check.sh
  • scripts/format-staged.sh
  • scripts/format-typescript.sh
  • scripts/format.sh
  • scripts/install-hooks.sh
💤 Files with no reviewable changes (1)
  • crates/lni/lnd/types.rs

Comment on lines +77 to 90
// Create HTTP client with optional SOCKS5 proxy
let client = if let Some(proxy_url) = socks5_proxy {
// Ignore certificate errors when using SOCKS5 proxy
let client_builder = reqwest::Client::builder().danger_accept_invalid_certs(true);

match reqwest::Proxy::all(&proxy_url) {
Ok(proxy) => {
match client_builder.proxy(proxy).build() {
Ok(client) => client,
Err(_) => reqwest::Client::new(), // Fallback to default client on error
}
} else {
reqwest::Client::builder().build().unwrap_or_else(|_| reqwest::Client::new())
};

// Create request with optional header
let mut request = client.get(&url);

if let (Some(key), Some(value)) = (header_key, header_value) {
request = request.header(&key, &value);
}
Err(_) => reqwest::Client::new(), // Fallback to default client on error
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "danger_accept_invalid_certs|say_after_with_tokio" bindings/lni_nodejs/src crates/lni -g '*.rs'

Repository: lightning-node-interface/lni

Length of output: 1886


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- bindings/lni_nodejs/src/lib.rs ---'
sed -n '1,160p' bindings/lni_nodejs/src/lib.rs

echo
echo '--- crates/lni/lib.rs (say_after_with_tokio) ---'
sed -n '280,340p' crates/lni/lib.rs

echo
echo '--- crates/lni/lnd/api.rs ---'
sed -n '1,120p' crates/lni/lnd/api.rs

echo
echo '--- crates/lni/strike/api.rs ---'
sed -n '1,110p' crates/lni/strike/api.rs

Repository: lightning-node-interface/lni

Length of output: 14923


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- occurrences of proxy/self-signed/local testing guidance ---'
rg -n "self-signed|certificate|invalid cert|proxy|local testing|danger_accept_invalid_certs|accept_invalid_certs" crates/lni bindings/lni_nodejs -g '*.rs' -g '*.md' -g '*.toml'

echo
echo '--- client construction around bindings/lni_nodejs/src/lib.rs ---'
sed -n '60,110p' bindings/lni_nodejs/src/lib.rs

Repository: lightning-node-interface/lni

Length of output: 20391


Don't disable TLS validation just because a SOCKS5 proxy is set.

danger_accept_invalid_certs(true) makes every HTTPS request through this helper trust invalid certificates, so a proxy can MITM arbitrary URLs. Make this an explicit opt-in instead of enabling it automatically.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 79-79: Dangerously accepting invalid TLS
Context: reqwest::Client::builder().danger_accept_invalid_certs(true)
Note: [CWE-295]: Improper Certificate

(reqwest-accept-invalid-rust)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/lni_nodejs/src/lib.rs` around lines 77 - 90, The HTTP client setup
in the request-building logic should not automatically disable TLS verification
when socks5_proxy is present. Update the client construction in the helper
around reqwest::Client::builder and reqwest::Proxy::all so that
danger_accept_invalid_certs(true) is removed from the default proxy path, and
only allow invalid certs behind an explicit opt-in flag or configuration. Keep
the existing proxy fallback behavior intact while ensuring the default client
created by the function still validates certificates.

Source: Linters/SAST tools

Comment on lines +287 to +309
while start_time.elapsed().as_secs() < params.max_polling_sec as u64 {
let lookup_params = lni::LookupInvoiceParams {
payment_hash: params.payment_hash.clone(),
search: params.search.clone(),
};

match node.lookup_invoice(lookup_params).await {
Ok(transaction) if transaction.settled_at > 0 => {
let _ = callback("success".to_string(), Some(transaction));
return;
}
Ok(transaction) => {
let _ = callback("pending".to_string(), Some(transaction));
}
Err(_) => {
let _ = callback("pending".to_string(), None);
}
}

let _ = callback("failure".to_string(), None);
});
tokio::time::sleep(tokio::time::Duration::from_secs(
params.polling_delay_sec as u64,
))
.await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "OnInvoiceEventParams|max_polling_sec|polling_delay_sec" bindings/lni_nodejs/src crates/lni -g '*.rs' -g '*.ts'

Repository: lightning-node-interface/lni

Length of output: 7367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the Node.js binding and the shared params type.
sed -n '1,220p' bindings/lni_nodejs/src/spark.rs
printf '\n--- types ---\n'
sed -n '640,690p' crates/lni/types.rs
printf '\n--- node spark binding ---\n'
sed -n '240,340p' bindings/lni_nodejs/src/spark.rs

Repository: lightning-node-interface/lni

Length of output: 9779


Clamp polling_delay_sec and max_polling_sec before casting to u64. OnInvoiceEventParams accepts signed values from the JS binding, so a negative input wraps to a huge duration and can block this sync polling loop for far longer than intended.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/lni_nodejs/src/spark.rs` around lines 287 - 309, Clamp the polling
values in the invoice polling loop before converting them to durations: in
`OnInvoiceEventParams` handling inside `spark.rs`, ensure `polling_delay_sec`
and `max_polling_sec` are validated to non-negative bounds before the `as u64`
casts used by the `while start_time.elapsed()` condition and
`tokio::time::sleep`. Update the `lookup_invoice` polling logic so negative JS
inputs cannot wrap into huge `Duration` values, keeping the behavior bounded and
predictable.

Comment on lines +24 to +137
enabled &&
process.env.BLINK_RUN_ONCHAIN_SEND === 'true' &&
process.env.BLINK_ONCHAIN_SEND_CONFIRM === ONCHAIN_SEND_CONFIRMATION &&
hasEnv('BLINK_ONCHAIN_TEST_ADDRESS', 'BLINK_ONCHAIN_AMOUNT_SATS') &&
Number.isSafeInteger(onchainSendAmountSats) &&
onchainSendAmountSats > 0;

const makeNode = () =>
new BlinkNode({
apiKey: process.env.BLINK_API_KEY!,
baseUrl: nonEmpty(process.env.BLINK_BASE_URL),
});

itIf(enabled)('getInfo + createInvoice + listTransactions', async () => {
const node = makeNode();
const info = await node.getInfo();
expect(typeof info.alias).toBe('string');
itIf(enabled)(
'getInfo + createInvoice + listTransactions',
async () => {
const node = makeNode();
const info = await node.getInfo();
expect(typeof info.alias).toBe('string');

const invoice = await node.createInvoice({
amountMsats: 5_000,
description: testInvoiceLabel('blink'),
});
console.log('Blink Invoice:', invoice);
expect(invoice.invoice.length).toBeGreaterThan(0);
const invoice = await node.createInvoice({
amountMsats: 5_000,
description: testInvoiceLabel('blink'),
});
console.log('Blink Invoice:', invoice);
expect(invoice.invoice.length).toBeGreaterThan(0);

const txs = await node.listTransactions({ from: 0, limit: 25 });
expect(Array.isArray(txs)).toBe(true);
},
timeout
);

itIf(enabled)(
'lookupInvoice (best effort from env or recent tx)',
async () => {
await runOrSkipKnownError(async () => {
const node = makeNode();
const txs = await node.listTransactions({ from: 0, limit: 50 });
const candidateHash = txs.find((tx) => tx.paymentHash.length > 0)?.paymentHash;
const hashes = uniqueValues([process.env.BLINK_TEST_PAYMENT_HASH, candidateHash]);

if (!hashes.length) {
return;
}

const txs = await node.listTransactions({ from: 0, limit: 25 });
expect(Array.isArray(txs)).toBe(true);
}, timeout);
let lastError: unknown;
for (const paymentHash of hashes) {
try {
const tx = await node.lookupInvoice({ paymentHash });
expect(tx.paymentHash.length).toBeGreaterThan(0);
return;
} catch (error) {
lastError = error;
}
}

itIf(enabled)('lookupInvoice (best effort from env or recent tx)', async () => {
await runOrSkipKnownError(async () => {
if (lastError) {
throw lastError;
}
}, ['transaction not found', 'http 404']);
},
timeout
);

itIf(enabled && hasEnv('BLINK_ONCHAIN_TEST_ADDRESS'))(
'prepareOnchainTransaction + optionally payOnchain',
async () => {
const node = makeNode();
const txs = await node.listTransactions({ from: 0, limit: 50 });
const candidateHash = txs.find((tx) => tx.paymentHash.length > 0)?.paymentHash;
const hashes = uniqueValues([process.env.BLINK_TEST_PAYMENT_HASH, candidateHash]);

if (!hashes.length) {
let transaction: Awaited<ReturnType<BlinkNode['prepareOnchainTransaction']>> | undefined;

await runOrSkipKnownError(async () => {
transaction = await node.prepareOnchainTransaction({
address: process.env.BLINK_ONCHAIN_TEST_ADDRESS!,
amountSats: quoteOnlyAmountSats,
fee: { type: 'speed', speed: runOnchainSend ? 'fast' : 'normal' },
feePayer: 'sender',
description: testInvoiceLabel(
runOnchainSend ? 'blink onchain e2e' : 'blink onchain quote'
),
});

expect(transaction.address).toBe(process.env.BLINK_ONCHAIN_TEST_ADDRESS);
expect(transaction.amountSats).toBe(quoteOnlyAmountSats);
expect(transaction.feePayer).toBe('sender');
expect(transaction.feeSats).toBeGreaterThanOrEqual(0);
}, ['insufficient balance', 'invalid address', 'amount']);

console.log('Prepared Blink on-chain transaction:', transaction);
if (!transaction) {
return;
}

let lastError: unknown;
for (const paymentHash of hashes) {
try {
const tx = await node.lookupInvoice({ paymentHash });
expect(tx.paymentHash.length).toBeGreaterThan(0);
return;
} catch (error) {
lastError = error;
}
if (!runOnchainSend) {
console.log(
'Prepared Blink on-chain quote; skipping broadcast without explicit confirmation'
);
return;
}

if (lastError) {
throw lastError;
}
}, ['transaction not found', 'http 404']);
}, timeout);

itIf(enabled && hasEnv('BLINK_ONCHAIN_TEST_ADDRESS'))('prepareOnchainTransaction + optionally payOnchain', async () => {
const node = makeNode();
let transaction: Awaited<ReturnType<BlinkNode['prepareOnchainTransaction']>> | undefined;

await runOrSkipKnownError(async () => {
transaction = await node.prepareOnchainTransaction({
address: process.env.BLINK_ONCHAIN_TEST_ADDRESS!,
amountSats: quoteOnlyAmountSats,
fee: { type: 'speed', speed: runOnchainSend ? 'fast' : 'normal' },
feePayer: 'sender',
description: testInvoiceLabel(runOnchainSend ? 'blink onchain e2e' : 'blink onchain quote'),
// Blink can quote high miner/provider fees for small test sends; keep the absolute guardrail
// default, but allow this real test up to 52% before broadcasting.
const payment = await node.payOnchain(transaction, {
feeGuardrail: {
maxFeePercent: 52,
},
});
console.log('Blink on-chain payment result:', payment);

expect(transaction.address).toBe(process.env.BLINK_ONCHAIN_TEST_ADDRESS);
expect(transaction.amountSats).toBe(quoteOnlyAmountSats);
expect(transaction.feePayer).toBe('sender');
expect(transaction.feeSats).toBeGreaterThanOrEqual(0);
}, ['insufficient balance', 'invalid address', 'amount']);

console.log('Prepared Blink on-chain transaction:', transaction);
if (!transaction) {
return;
}

if (!runOnchainSend) {
console.log('Prepared Blink on-chain quote; skipping broadcast without explicit confirmation');
return;
}

// Blink can quote high miner/provider fees for small test sends; keep the absolute guardrail
// default, but allow this real test up to 52% before broadcasting.
const payment = await node.payOnchain(transaction, {
feeGuardrail: {
maxFeePercent: 52,
},
});
console.log('Blink on-chain payment result:', payment);

expect(['pending', 'completed']).toContain(payment.state);
expect(payment.address).toBe(process.env.BLINK_ONCHAIN_TEST_ADDRESS);
}, timeout);
expect(['pending', 'completed']).toContain(payment.state);
expect(payment.address).toBe(process.env.BLINK_ONCHAIN_TEST_ADDRESS);
},
timeout
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove sensitive logging from this test.

These console.log(...) calls print full invoice/payment payloads, and the on-chain path also emits transaction details into CI logs. As per coding guidelines, tests/logging must not expose full invoices or preimages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/typescript/src/__tests__/integration/blink.real.test.ts` around
lines 24 - 137, Remove the sensitive console logging from the Blink real
integration tests so CI never prints full invoice, payment, or transaction
payloads. In the test cases that use makeNode, createInvoice,
prepareOnchainTransaction, and payOnchain, delete the console.log calls that
dump the returned objects or replace them with non-sensitive status messages
only. Keep the assertions and flow intact, but ensure no full
invoice/payment/preimage or on-chain transaction details are emitted.

Source: Coding guidelines

Comment on lines +14 to +59
itIf(enabled)(
'getInfo',
async () => {
const node = makeNode();
const info = await node.getInfo();
expect(typeof info.pubkey).toBe('string');
expect(info.pubkey.length).toBeGreaterThan(0);
},
timeout
);

itIf(enabled)('createInvoice + lookupInvoice + listTransactions', async () => {
const node = makeNode();
const invoice = await node.createInvoice({
amountMsats: 2_000,
description: testInvoiceLabel('cln'),
});
console.log('CLN Invoice:', invoice);
expect(invoice.invoice.length).toBeGreaterThan(0);
expect(invoice.paymentHash.length).toBeGreaterThan(0);
itIf(enabled)(
'createInvoice + lookupInvoice + listTransactions',
async () => {
const node = makeNode();
const invoice = await node.createInvoice({
amountMsats: 2_000,
description: testInvoiceLabel('cln'),
});
console.log('CLN Invoice:', invoice);
expect(invoice.invoice.length).toBeGreaterThan(0);
expect(invoice.paymentHash.length).toBeGreaterThan(0);

const lookedUp = await node.lookupInvoice({ paymentHash: invoice.paymentHash });
expect(lookedUp.paymentHash).toBe(invoice.paymentHash);
const lookedUp = await node.lookupInvoice({ paymentHash: invoice.paymentHash });
expect(lookedUp.paymentHash).toBe(invoice.paymentHash);

const txs = await node.listTransactions({ from: 0, limit: 25, paymentHash: invoice.paymentHash });
expect(Array.isArray(txs)).toBe(true);
expect(txs.some((tx) => tx.paymentHash === invoice.paymentHash)).toBe(true);
}, timeout);
const txs = await node.listTransactions({
from: 0,
limit: 25,
paymentHash: invoice.paymentHash,
});
expect(Array.isArray(txs)).toBe(true);
expect(txs.some((tx) => tx.paymentHash === invoice.paymentHash)).toBe(true);
},
timeout
);

itIf(enabled && hasEnv('CLN_TEST_PAYMENT_REQUEST'))('decode', async () => {
const node = makeNode();
const decoded = await node.decode(process.env.CLN_TEST_PAYMENT_REQUEST!);
expect(decoded.length).toBeGreaterThan(0);
}, timeout);
itIf(enabled && hasEnv('CLN_TEST_PAYMENT_REQUEST'))(
'decode',
async () => {
const node = makeNode();
const decoded = await node.decode(process.env.CLN_TEST_PAYMENT_REQUEST!);
expect(decoded.length).toBeGreaterThan(0);
},
timeout
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the invoice log from this test.

console.log('CLN Invoice:', invoice) can leak the full invoice payload into CI logs. As per coding guidelines, tests/logging must not expose full invoices or preimages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/typescript/src/__tests__/integration/cln.real.test.ts` around lines
14 - 59, The test in the createInvoice + lookupInvoice + listTransactions case
is logging the full invoice payload, which can leak sensitive data into CI
output. Remove the console.log from the CLN integration test and keep the
existing assertions on invoice.invoice, invoice.paymentHash, lookupInvoice, and
listTransactions in place without exposing the invoice object.

Source: Coding guidelines

Comment on lines +14 to +60
itIf(enabled)(
'getInfo',
async () => {
await runOrSkipKnownError(async () => {
const node = makeNode();
const info = await node.getInfo();
expect(typeof info.pubkey).toBe('string');
expect(info.pubkey.length).toBeGreaterThan(0);
}, ['permission denied']);
},
timeout
);

itIf(enabled)('createInvoice + lookupInvoice + listTransactions', async () => {
const node = makeNode();
const invoice = await node.createInvoice({
amountMsats: 3_000,
description: testInvoiceLabel('lnd'),
});
console.log('LND Invoice:', invoice);
expect(invoice.invoice.length).toBeGreaterThan(0);
expect(invoice.paymentHash.length).toBeGreaterThan(0);
itIf(enabled)(
'createInvoice + lookupInvoice + listTransactions',
async () => {
const node = makeNode();
const invoice = await node.createInvoice({
amountMsats: 3_000,
description: testInvoiceLabel('lnd'),
});
console.log('LND Invoice:', invoice);
expect(invoice.invoice.length).toBeGreaterThan(0);
expect(invoice.paymentHash.length).toBeGreaterThan(0);

const lookedUp = await node.lookupInvoice({ paymentHash: invoice.paymentHash });
expect(lookedUp.paymentHash.length).toBeGreaterThan(0);
const lookedUp = await node.lookupInvoice({ paymentHash: invoice.paymentHash });
expect(lookedUp.paymentHash.length).toBeGreaterThan(0);

const txs = await node.listTransactions({ from: 0, limit: 25, paymentHash: invoice.paymentHash });
expect(Array.isArray(txs)).toBe(true);
}, timeout);
const txs = await node.listTransactions({
from: 0,
limit: 25,
paymentHash: invoice.paymentHash,
});
expect(Array.isArray(txs)).toBe(true);
},
timeout
);

itIf(enabled && hasEnv('LND_TEST_PAYMENT_REQUEST'))('decode', async () => {
const node = makeNode();
const decoded = await node.decode(process.env.LND_TEST_PAYMENT_REQUEST!);
expect(decoded.length).toBeGreaterThan(0);
}, timeout);
itIf(enabled && hasEnv('LND_TEST_PAYMENT_REQUEST'))(
'decode',
async () => {
const node = makeNode();
const decoded = await node.decode(process.env.LND_TEST_PAYMENT_REQUEST!);
expect(decoded.length).toBeGreaterThan(0);
},
timeout
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the invoice log from this test.

console.log('LND Invoice:', invoice) can leak the full invoice payload into CI logs. As per coding guidelines, tests/logging must not expose full invoices or preimages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/typescript/src/__tests__/integration/lnd.real.test.ts` around lines
14 - 60, Remove the invoice logging from the LND integration test by deleting
the console output in the createInvoice + lookupInvoice + listTransactions case;
update the test around node.createInvoice so it no longer prints the full
invoice payload to CI logs, while keeping the existing assertions on
invoice.invoice and invoice.paymentHash intact.

Source: Coding guidelines

Comment on lines +27 to +52
itIf(enabled)(
'createInvoice + lookupInvoice + listTransactions',
async () => {
await runOrSkipKnownError(async () => {
const node = makeNode();
const invoice = await node.createInvoice({
amountMsats: 2_000,
description: testInvoiceLabel('phoenixd'),
});
console.log('Phoenixd Invoice:', invoice);
expect(invoice.invoice.length).toBeGreaterThan(0);
expect(invoice.paymentHash.length).toBeGreaterThan(0);

const lookedUp = await node.lookupInvoice({ paymentHash: invoice.paymentHash });
expect(lookedUp.paymentHash).toBe(invoice.paymentHash);
const lookedUp = await node.lookupInvoice({ paymentHash: invoice.paymentHash });
expect(lookedUp.paymentHash).toBe(invoice.paymentHash);

const txs = await node.listTransactions({ from: 0, limit: 25, paymentHash: invoice.paymentHash });
expect(Array.isArray(txs)).toBe(true);
}, ['fetch failed', 'econnrefused', 'enotfound', 'timed out']);
}, timeout);
const txs = await node.listTransactions({
from: 0,
limit: 25,
paymentHash: invoice.paymentHash,
});
expect(Array.isArray(txs)).toBe(true);
}, ['fetch failed', 'econnrefused', 'enotfound', 'timed out']);
},
timeout
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the invoice log from this test.

console.log('Phoenixd Invoice:', invoice) can leak the full invoice payload into CI logs. As per coding guidelines, tests/logging must not expose full invoices or preimages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/typescript/src/__tests__/integration/phoenixd.real.test.ts` around
lines 27 - 52, Remove the console logging of the full invoice object in the
phoenixd integration test. In the createInvoice + lookupInvoice +
listTransactions case, delete the console.log call that prints the invoice
payload and keep the assertions/use of invoice.invoice and invoice.paymentHash
only. This change should be made in the test body around makeNode() and
createInvoice() so CI logs do not expose sensitive invoice data.

Source: Coding guidelines

Comment on lines +22 to +40
itIf(enabled)(
'getInfo + createInvoice + listTransactions',
async () => {
const node = makeNode();
const info = await node.getInfo();
expect(typeof info.alias).toBe('string');

const invoice = await node.createInvoice({
amountMsats: 5_000,
description: testInvoiceLabel('speed'),
});
console.log('Speed Invoice:', invoice);
expect(invoice.invoice.length).toBeGreaterThan(0);
const invoice = await node.createInvoice({
amountMsats: 5_000,
description: testInvoiceLabel('speed'),
});
console.log('Speed Invoice:', invoice);
expect(invoice.invoice.length).toBeGreaterThan(0);

const txs = await node.listTransactions({ from: 0, limit: 25 });
expect(Array.isArray(txs)).toBe(true);
}, timeout);
const txs = await node.listTransactions({ from: 0, limit: 25 });
expect(Array.isArray(txs)).toBe(true);
},
timeout
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the invoice log from this test.

console.log('Speed Invoice:', invoice) can leak the full invoice payload into CI logs. As per coding guidelines, tests/logging must not expose full invoices or preimages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/typescript/src/__tests__/integration/speed.real.test.ts` around
lines 22 - 40, Remove the invoice logging from the speed test to avoid exposing
sensitive payment data in CI logs. Update the test in the getInfo +
createInvoice + listTransactions block by deleting the console.log tied to the
invoice variable, while keeping the existing assertions and flow unchanged.

Source: Coding guidelines

Comment on lines +215 to +225
await expect(makeNode().payInvoice({ invoice: BOLT11_INVOICE })).rejects.toBeInstanceOf(
NwcError
);
});

it('preserves LniError status and body when rewrapping request failures', async () => {
nwcMocks.payInvoice.mockRejectedValue(
new LniError('Http', 'gateway timeout', {
status: 504,
body: '{"error":"timeout"}',
}),
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the stronger error assertion.

This hunk now only checks the thrown type, so it no longer verifies the preserved NwcError payload. That weakens coverage for NIP-47 error wrapping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/typescript/src/__tests__/nwc.test.ts` around lines 215 - 225, The
payInvoice test no longer asserts the wrapped NwcError payload, so it only
checks the thrown type and weakens NIP-47 error coverage. Update the test around
makeNode().payInvoice and NwcError to keep the stronger assertion by verifying
the preserved error details from the rejected promise, not just the instance
type, alongside the existing LniError rewrap case.

Comment thread crates/lni/lib.rs
Comment on lines 305 to 316
// Ignore certificate errors when using SOCKS5 proxy
let client_builder = reqwest::Client::builder().danger_accept_invalid_certs(true);

match reqwest::Proxy::all(&proxy_url) {
Ok(proxy) => {
match client_builder.proxy(proxy).build() {
Ok(client) => client,
Err(_) => reqwest::Client::new() // Fallback to default client on error
Err(_) => reqwest::Client::new(), // Fallback to default client on error
}
}
Err(_) => reqwest::Client::new() // Fallback to default client on error
Err(_) => reqwest::Client::new(), // Fallback to default client on error
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the file and the relevant lines
git ls-files crates/lni/lib.rs
wc -l crates/lni/lib.rs
sed -n '260,340p' crates/lni/lib.rs

# Search for related reqwest client configuration in the repo
rg -n "danger_accept_invalid_certs|Proxy::all|Client::builder\(" crates -S

Repository: lightning-node-interface/lni

Length of output: 5475


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show surrounding function context and call sites
ast-grep outline crates/lni/lib.rs --view expanded
rg -n "say_after_with_tokio|proxy_url|socks5" crates/lni/lib.rs -n -S

Repository: lightning-node-interface/lni

Length of output: 2513


Don't disable certificate validation on the SOCKS5 path.

danger_accept_invalid_certs(true) disables TLS verification for every HTTPS request made with this client, which exposes requests to proxy/MITM interception. Keep verification on by default and make any relaxation an explicit opt-in for self-signed endpoints.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 305-305: Dangerously accepting invalid TLS
Context: reqwest::Client::builder().danger_accept_invalid_certs(true)
Note: [CWE-295]: Improper Certificate

(reqwest-accept-invalid-rust)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lni/lib.rs` around lines 305 - 316, The SOCKS5 client setup in the
reqwest builder is disabling TLS verification globally via the client_builder
path, which should not be the default. Remove the
danger_accept_invalid_certs(true) usage from the proxy/client construction and
keep certificate validation enabled in the normal flow. If special handling for
self-signed endpoints is needed, gate it behind an explicit opt-in in the same
client-building logic around reqwest::Client::builder(), reqwest::Proxy::all,
and the build fallback path.

Source: Linters/SAST tools

Comment thread crates/lni/nwc/api.rs
Comment on lines +97 to +131
Ok(nwc_info) => {
Ok(NodeInfo {
alias: nwc_info.alias.unwrap_or_else(|| "NWC Node".to_string()),
color: nwc_info.color.unwrap_or_default(),
pubkey: nwc_info.pubkey.map(|pk| pk.to_string()).unwrap_or_else(|| {
// If no pubkey in get_info, try to extract from URI
config
.nwc_uri
.split("?")
.next()
.and_then(|part| part.strip_prefix("nostr+walletconnect://"))
.unwrap_or_default()
.to_string()
}),
network: nwc_info.network.unwrap_or_else(|| "mainnet".to_string()),
block_height: nwc_info.block_height.unwrap_or(0) as i64,
block_hash: nwc_info.block_hash.unwrap_or_default(),
send_balance_msat: balance as i64,
receive_balance_msat: 0, // NWC doesn't provide separate receive balance
fee_credit_balance_msat: 0,
unsettled_send_balance_msat: 0,
unsettled_receive_balance_msat: 0,
pending_open_send_balance: 0,
pending_open_receive_balance: 0,
})
}
Err(_) => {
// Fallback: extract pubkey from NWC URI if get_info is not available
let pubkey = config
.nwc_uri
.split("?")
.next()
.and_then(|part| part.strip_prefix("nostr+walletconnect://"))
.unwrap_or_default()
.to_string();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Extract pubkey-from-URI logic into a helper and handle all NWC scheme variants.

The pubkey extraction logic is duplicated between the Ok branch (lines 101–110) and the Err fallback (lines 125–131). Both inline the same split("?")strip_prefix("nostr+walletconnect://") chain, but this only handles one of the four NWC URI scheme variants. If the URI uses nostrwalletconnect://, nostrwalletconnect:, or nostr+walletconnect:, the pubkey will silently default to an empty string, producing an incorrect NodeInfo.

The new lightning_address_from_nwc_uri helper (lines 166–171) already normalizes all four variants — the same approach should be applied here.

♻️ Proposed refactor: extract a `pubkey_from_nwc_uri` helper
+/// Extracts the pubkey from an NWC URI string, handling all known scheme variants.
+fn pubkey_from_nwc_uri(nwc_uri: &str) -> String {
+    nwc_uri
+        .split("?")
+        .next()
+        .and_then(|part| {
+            part.strip_prefix("nostr+walletconnect://")
+                .or_else(|| part.strip_prefix("nostrwalletconnect://"))
+                .or_else(|| part.strip_prefix("nostr+walletconnect:"))
+                .or_else(|| part.strip_prefix("nostrwalletconnect:"))
+        })
+        .unwrap_or_default()
+        .to_string()
+}
+
 pub async fn get_info(config: NwcConfig) -> Result<NodeInfo, ApiError> {
     // ...
     match info_result {
         Ok(nwc_info) => {
             Ok(NodeInfo {
                 // ...
-                pubkey: nwc_info.pubkey.map(|pk| pk.to_string()).unwrap_or_else(|| {
-                    // If no pubkey in get_info, try to extract from URI
-                    config
-                        .nwc_uri
-                        .split("?")
-                        .next()
-                        .and_then(|part| part.strip_prefix("nostr+walletconnect://"))
-                        .unwrap_or_default()
-                        .to_string()
-                }),
+                pubkey: nwc_info
+                    .pubkey
+                    .map(|pk| pk.to_string())
+                    .unwrap_or_else(|| pubkey_from_nwc_uri(&config.nwc_uri)),
                 // ...
             })
         }
         Err(_) => {
-            let pubkey = config
-                .nwc_uri
-                .split("?")
-                .next()
-                .and_then(|part| part.strip_prefix("nostr+walletconnect://"))
-                .unwrap_or_default()
-                .to_string();
+            let pubkey = pubkey_from_nwc_uri(&config.nwc_uri);
             // ...
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Ok(nwc_info) => {
Ok(NodeInfo {
alias: nwc_info.alias.unwrap_or_else(|| "NWC Node".to_string()),
color: nwc_info.color.unwrap_or_default(),
pubkey: nwc_info.pubkey.map(|pk| pk.to_string()).unwrap_or_else(|| {
// If no pubkey in get_info, try to extract from URI
config
.nwc_uri
.split("?")
.next()
.and_then(|part| part.strip_prefix("nostr+walletconnect://"))
.unwrap_or_default()
.to_string()
}),
network: nwc_info.network.unwrap_or_else(|| "mainnet".to_string()),
block_height: nwc_info.block_height.unwrap_or(0) as i64,
block_hash: nwc_info.block_hash.unwrap_or_default(),
send_balance_msat: balance as i64,
receive_balance_msat: 0, // NWC doesn't provide separate receive balance
fee_credit_balance_msat: 0,
unsettled_send_balance_msat: 0,
unsettled_receive_balance_msat: 0,
pending_open_send_balance: 0,
pending_open_receive_balance: 0,
})
}
Err(_) => {
// Fallback: extract pubkey from NWC URI if get_info is not available
let pubkey = config
.nwc_uri
.split("?")
.next()
.and_then(|part| part.strip_prefix("nostr+walletconnect://"))
.unwrap_or_default()
.to_string();
/// Extracts the pubkey from an NWC URI string, handling all known scheme variants.
fn pubkey_from_nwc_uri(nwc_uri: &str) -> String {
nwc_uri
.split("?")
.next()
.and_then(|part| {
part.strip_prefix("nostr+walletconnect://")
.or_else(|| part.strip_prefix("nostrwalletconnect://"))
.or_else(|| part.strip_prefix("nostr+walletconnect:"))
.or_else(|| part.strip_prefix("nostrwalletconnect:"))
})
.unwrap_or_default()
.to_string()
}
Ok(nwc_info) => {
Ok(NodeInfo {
alias: nwc_info.alias.unwrap_or_else(|| "NWC Node".to_string()),
color: nwc_info.color.unwrap_or_default(),
pubkey: nwc_info
.pubkey
.map(|pk| pk.to_string())
.unwrap_or_else(|| pubkey_from_nwc_uri(&config.nwc_uri)),
network: nwc_info.network.unwrap_or_else(|| "mainnet".to_string()),
block_height: nwc_info.block_height.unwrap_or(0) as i64,
block_hash: nwc_info.block_hash.unwrap_or_default(),
send_balance_msat: balance as i64,
receive_balance_msat: 0, // NWC doesn't provide separate receive balance
fee_credit_balance_msat: 0,
unsettled_send_balance_msat: 0,
unsettled_receive_balance_msat: 0,
pending_open_send_balance: 0,
pending_open_receive_balance: 0,
})
}
Err(_) => {
// Fallback: extract pubkey from NWC URI if get_info is not available
let pubkey = pubkey_from_nwc_uri(&config.nwc_uri);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lni/nwc/api.rs` around lines 97 - 131, Extract the duplicated pubkey
parsing in NodeInfo construction into a dedicated helper, and use it in both the
get_info success path and the Err fallback. Update the logic in the api.rs NWC
handling code to normalize all NWC URI scheme variants the same way as
lightning_address_from_nwc_uri does, so pubkey extraction works for
nostr+walletconnect://, nostrwalletconnect://, nostrwalletconnect:, and
nostr+walletconnect:. Keep the existing NodeInfo and config.nwc_uri call sites,
but route them through the new helper so the fallback no longer silently returns
an empty pubkey.

@ntheile
ntheile merged commit ac5b268 into master Jul 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant