Skip to content

Onchain LND, CLN, Phoenixd#40

Open
ntheile wants to merge 5 commits into
masterfrom
onchain-nodes
Open

Onchain LND, CLN, Phoenixd#40
ntheile wants to merge 5 commits into
masterfrom
onchain-nodes

Conversation

@ntheile
Copy link
Copy Markdown
Contributor

@ntheile ntheile commented Jun 4, 2026

Testing

  • LND
  • CLN
  • Phoenixd

For each test the different fee configs.

Summary by CodeRabbit

  • New Features

    • Added on-chain Bitcoin payment support to Phoenixd, Core Lightning (CLN), and LND with prepare-then-pay flows.
  • Documentation

    • Expanded on-chain guides: per-node fee/confirmation capabilities, fee-guardrail behavior, and safer LND macaroon guidance.
  • Tests

    • Added and expanded unit/integration and end-to-end tests (including gated real-send tests) covering on-chain prepare/pay flows.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds complete on-chain Bitcoin payment support to LND, CLN, and Phoenixd node implementations via a prepare-then-pay flow. Changes span TypeScript client logic, Rust backend APIs, N-API bindings, comprehensive test coverage, and documentation. Each provider's implementation includes fee preference resolution, guardrail enforcement, REST API integration, and PSBT/response parsing. The feature is accessible from both JavaScript and Rust with consistent error handling and validation.

Changes

On-chain payment implementations for LND, CLN, and Phoenixd

Layer / File(s) Summary
Type declarations and public API surface
bindings/lni_nodejs/index.d.ts, bindings/typescript/src/nodes/*.ts
Three node types gain prepareOnchainTransaction() and payOnchain() method declarations returning OnchainTransaction and PayOnchainResponse with optional PayOnchainOptions. Module imports updated to include on-chain types and guardrail constants.
TypeScript CLN on-chain implementation
bindings/typescript/src/nodes/cln.ts, bindings/typescript/src/__tests__/cln.test.ts, bindings/typescript/src/__tests__/integration/cln.real.test.ts
ClnNode implements OnchainPayments with methods to prepare transactions via CLN /v1/txprepare and pay via /v1/txsend. Includes PSBT fee parsing (compact-size, transaction inputs/outputs), fee validation, guardrail enforcement, state normalization, unit tests, and environment-gated real integration test.
TypeScript LND on-chain implementation
bindings/typescript/src/nodes/lnd.ts, bindings/typescript/src/__tests__/lnd.test.ts, bindings/typescript/src/__tests__/integration/lnd.real.test.ts
LndNode implements OnchainPayments with methods to estimate fees via LND /v1/transactions/fee and broadcast via /v1/transactions. Includes validation helpers, fee preference-to-LND-parameter translation (target_conf vs sat_per_vbyte), fee guardrail enforcement, GET-with-query helper, unit tests, and environment-gated integration test.
TypeScript Phoenixd on-chain implementation
bindings/typescript/src/nodes/phoenixd.ts, bindings/typescript/src/__tests__/phoenixd.test.ts, bindings/typescript/src/__tests__/integration/phoenixd.real.test.ts
PhoenixdNode implements OnchainPayments with methods to prepare and broadcast via Phoenixd /sendtoaddress. Includes helpers to validate amounts, map fee payer to Phoenixd constraints (sender-only), resolve explicit sats/vbyte requirements, enforce optional guardrails, build URL-encoded POSTs, unit tests, and environment-gated integration test.
Rust CLN backend API
crates/lni/cln/api.rs, crates/lni/cln/lib.rs
Introduces TxPrepareResponse, TxSendResponse, and CLN fee request structures. Implements fee validation/resolution, guardrail enforcement, and a PSBT parser (compact-size decoding, transaction input/output parsing, witness/non-witness UTXO handling) to extract fee_sats from CLN prepared transactions. Exports prepare_onchain_transaction(), pay_onchain(), and pay_onchain_with_options() and adds an E2E test.
Rust LND backend API
crates/lni/lnd/api.rs, crates/lni/lnd/lib.rs
Introduces EstimateFeeResponse, SendCoinsResponse, and LND fee request structures. Implements amount validation, fee preference translation (target_conf/sat_per_vbyte), guardrail enforcement, optional fee-rate parsing, and exports prepare_onchain_transaction(), pay_onchain(), and pay_onchain_with_options() with unit and E2E tests.
Rust Phoenixd backend API
crates/lni/phoenixd/api.rs, crates/lni/phoenixd/lib.rs
Adds PhoenixdOnchainFeeRequest struct. Implements amount/feerate validation, fee payer mapping, guardrail enforcement (with optional dangerous disable), txid format validation, and exported prepare/pay APIs plus an E2E test.
Rust node wrappers and N-API bindings
crates/lni/cln/lib.rs, crates/lni/lnd/lib.rs, crates/lni/phoenixd/lib.rs, bindings/lni_nodejs/src/cln.rs, bindings/lni_nodejs/src/lnd.rs, bindings/lni_nodejs/src/phoenixd.rs
ClnNode, LndNode, and PhoenixdNode each gain prepare_onchain_transaction(), pay_onchain(), and pay_onchain_with_options() that delegate to backend crate::*::api functions. N-API wrapper files expose thin async methods to JavaScript, forwarding parameters and mapping errors to napi::Error.
Unit, integration, and E2E tests
bindings/typescript/src/__tests__/*, crates/lni/*/lib.rs
Vitest unit tests (mocked) verify request payload construction, response parsing, and validation failures (invalid fees, guardrail breaches) without network calls. Integration tests (real endpoints) with environment-based gating prepare and conditionally broadcast transactions. Rust E2E tests read env vars, prepare quotes, and conditionally call pay_onchain().
README and integration test documentation
bindings/typescript/README.md, readme.md
Documents on-chain payment support across LND, CLN, and Phoenixd with per-provider fee/confirmation capability mapping, LND macaroon guidance (avoid admin.macaroon), expanded .env examples, and updated integration/broadcast test instructions.

Sequence Diagram(s)

sequenceDiagram
  participant Client as JavaScript Client
  participant TSNode as TypeScript Node (CLN/LND/Phoenixd)
  participant NAPI as N-API Binding
  participant RustLib as Rust Node Lib
  participant RustAPI as Rust Backend API
  participant Provider as Lightning Provider REST

  Client->>TSNode: prepareOnchainTransaction(params)
  TSNode->>TSNode: validate amount & fee preference
  TSNode->>NAPI: call napi.prepare_onchain_transaction
  NAPI->>RustLib: forward call
  RustLib->>RustAPI: prepare_onchain_transaction(config, params)
  RustAPI->>Provider: REST /txprepare or /fee or build send request
  Provider-->>RustAPI: quote / PSBT / response
  RustAPI->>RustAPI: parse fee, apply guardrails
  RustAPI-->>RustLib: OnchainTransaction
  RustLib-->>NAPI: return
  NAPI-->>TSNode: OnchainTransaction
  TSNode-->>Client: OnchainTransaction

  Client->>TSNode: payOnchain(transaction, options)
  TSNode->>TSNode: re-validate, enforce guardrails
  TSNode->>NAPI: call napi.pay_onchain
  NAPI->>RustLib: forward call
  RustLib->>RustAPI: pay_onchain_with_options(config, transaction, options)
  RustAPI->>Provider: REST /txsend or /transactions or /sendtoaddress
  Provider-->>RustAPI: txid / confirmation
  RustAPI->>RustAPI: normalize PayOnchainResponse (pending/failed)
  RustAPI-->>RustLib: PayOnchainResponse
  RustLib-->>NAPI: return
  NAPI-->>TSNode: PayOnchainResponse
  TSNode-->>Client: PayOnchainResponse
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

"I'm a rabbit in the code wood,
Pasting fees where functions stood,
Prepare, then pay — three nodes aligned,
PSBTs parsed, guardrails defined,
Hop! Bitcoin hops off good." 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Onchain LND, CLN, Phoenixd' directly refers to the main change in the changeset—adding on-chain payment support to three Lightning node implementations (LND, CLN, and Phoenixd).
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 onchain-nodes

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 and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0193ebd01

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const feeRequest = resolveLndFeeRequest(fee);
const estimate = await this.getJsonWithQuery<LndEstimateFeeResponse>('/v1/transactions/fee', {
[`AddrToAmount[${params.address}]`]: amountSats,
...feeRequest,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Do not quote LND sats/vbyte fees via EstimateFee

When callers prepare an LND on-chain send with fee: { type: 'satsPerVbyte', ... }, this spread adds sat_per_vbyte to /v1/transactions/fee. I checked the LND EstimateFee REST docs: the request only supports target_conf, min_confs, and spend_unconfirmed query fields; sat_per_vbyte is only a response field and is supported later by SendCoins. As a result this mode either gets rejected by LND or returns a fee quote that is not based on the manual feerate, while payOnchain later broadcasts with that manual feerate, so the guardrail can approve a transaction using the wrong fee estimate.

Useful? React with 👍 / 👎.

Comment thread crates/lni/lnd/api.rs Outdated
Comment on lines +805 to +806
if let Some(sat_per_vbyte) = fee_request.sat_per_vbyte {
query.push(("sat_per_vbyte".to_string(), sat_per_vbyte.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.

P1 Badge Do not quote LND sats/vbyte fees via EstimateFee

When Rust callers prepare an LND on-chain send with OnchainFeePreferenceType::SatsPerVbyte, this adds sat_per_vbyte to /v1/transactions/fee. I checked the LND EstimateFee REST docs: the request query fields are target_conf, min_confs, and spend_unconfirmed; sat_per_vbyte is only returned by EstimateFee and accepted by SendCoins. This means the manual-fee prepare path either fails or records a fee quote unrelated to the fee rate that pay_onchain later broadcasts with, so the fee guardrail can be evaluated against the wrong fee.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (2)
crates/lni/cln/api.rs (1)

162-204: 💤 Low value

Inconsistent guardrail logic between CLN and LND implementations.

CLN uses min(max_fee_sats, max_fee_by_percent) as a single threshold (Line 195), meaning the fee must satisfy both constraints simultaneously. LND (lines 223-236 in lnd/api.rs) checks each constraint independently with separate error messages.

This leads to different user experiences:

  • CLN: 1000 sat fee with 500 sat max and 10% max on 50000 sats → fails at 500 sat threshold
  • LND: Same scenario → fails with "exceeds max_fee_sats 500" message

Consider aligning the guardrail semantics across providers for consistent behavior.

Also applies to: 193-200

🤖 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/cln/api.rs` around lines 162 - 204, The CLN guardrail in
assert_onchain_fee_guardrail currently combines max_fee_sats and
max_fee_by_percent with min(...) which enforces both as a single threshold;
change it to check each constraint independently like the LND implementation by
computing max_fee_by_percent as now but then comparing fee_sats separately
against max_fee_sats and against max_fee_by_percent and returning distinct
ApiError::InvalidInput messages for each violation (e.g., "exceeds max_fee_sats
{max_fee_sats}" and "exceeds max_fee_percent {max_fee_by_percent}") so behavior
and user-facing errors match lnd/api.rs.
crates/lni/lnd/api.rs (1)

187-196: 💤 Low value

Redundant fallback chain for guardrail defaults.

The fallback guardrail.max_fee_sats.or(default_guardrail.max_fee_sats).unwrap_or(CONSTANT) is overly complex since default_guardrail is Default::default() which should already contain the constant values.

♻️ Simplified version
-    let default_guardrail = crate::types::OnchainFeeGuardrail::default();
     let guardrail = options.fee_guardrail.unwrap_or_default();
     let max_fee_sats = guardrail
         .max_fee_sats
-        .or(default_guardrail.max_fee_sats)
         .unwrap_or(crate::types::DEFAULT_ONCHAIN_MAX_FEE_SATS);
     let max_fee_percent = guardrail
         .max_fee_percent
-        .or(default_guardrail.max_fee_percent)
         .unwrap_or(crate::types::DEFAULT_ONCHAIN_MAX_FEE_PERCENT);
🤖 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/lnd/api.rs` around lines 187 - 196, The current fallback chain is
redundant: you create default_guardrail then call
guardrail.max_fee_sats.or(default_guardrail.max_fee_sats).unwrap_or(...).
Simplify by relying on options.fee_guardrail.unwrap_or_default() (the Default
provides the constants) and then use straightforward unwrap_or on the fields;
e.g., compute max_fee_sats from
guardrail.max_fee_sats.unwrap_or(crate::types::DEFAULT_ONCHAIN_MAX_FEE_SATS) and
max_fee_percent from
guardrail.max_fee_percent.unwrap_or(crate::types::DEFAULT_ONCHAIN_MAX_FEE_PERCENT),
removing the extra default_guardrail.or(...) logic.
🤖 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/phoenixd.rs`:
- Around line 82-95: The defaulting in pay_onchain currently calls
options.unwrap_or_default(), which leaves Phoenixd-prepared transactions (that
omit fee_sats) rejected by backend guardrails; update the defaulting logic in
the pay_onchain function so that when options is None you construct a
PayOnchainOptions with dangerously_disable_fee_guardrail = true (instead of the
plain Default), then pass that to lni::phoenixd::api::pay_onchain_with_options;
reference the pay_onchain function, PayOnchainOptions type, and the call to
lni::phoenixd::api::pay_onchain_with_options to locate where to change the
default.

In `@bindings/typescript/src/__tests__/cln.test.ts`:
- Around line 82-94: The fetch mock in cln.test.ts is too permissive and should
assert the CLN prepare endpoint and call count; inside the fetchMock
implementation (the vi.fn named fetchMock) check that the received request's URL
pathname equals '/v1/txprepare' and that init?.method (or the request method) is
'POST', and after calling prepareOnchainTransaction() add
expect(fetchMock).toHaveBeenCalledTimes(1); update the assertions in the test
surrounding prepareOnchainTransaction and fetchMock to ensure the RPC path, HTTP
method, and a single network call are verified.

In `@bindings/typescript/src/__tests__/integration/cln.real.test.ts`:
- Around line 71-89: The test currently only calls clnPost('/v1/txdiscard', {
txid: transaction.id }) after assertions, so a failed assertion leaves the
prepared CLN transaction reserved; wrap the discard call in a finally block that
always runs after prepareOnchainTransaction — store the result of
node.prepareOnchainTransaction(...) in the variable transaction, then in finally
check transaction.id and call clnPost('/v1/txdiscard', { txid: transaction.id })
if present; ensure the existing shouldBroadcastOnchain flow still returns early
when appropriate and that node.payOnchain is only invoked when broadcasting.

In `@bindings/typescript/src/__tests__/integration/lnd.real.test.ts`:
- Around line 71-85: Remove the full-object console logs that expose sensitive
runtime fields in the LND on-chain test flow: replace or delete the console.log
calls that print the entire transaction and payment objects (see variables
transaction and payment and the call node.payOnchain) so tests only emit
non-sensitive scalar fields or nothing; if logging is needed, log a minimal
whitelist such as transaction.address, transaction.amountSats, and a sanitized
payment.status or payment.id instead of the whole objects, and ensure the
early-return branch that checks runOnchainSend keeps the minimal logging policy.

In `@crates/lni/phoenixd/api.rs`:
- Around line 425-433: The guardrail check in pay_onchain_with_options calls
assert_onchain_fee_guardrail but prepare_onchain_transaction leaves
transaction.fee.fee_sats as None, causing the guardrail to always error unless
dangerously_disable_fee_guardrail is set; update pay_onchain_with_options (or
assert_onchain_fee_guardrail) to skip enforcement when transaction.fee.fee_sats
is None (i.e., treat unknown fees as "cannot enforce" and allow processing), or
alternatively make prepare_onchain_transaction populate fee_sats before calling
assert_onchain_fee_guardrail; reference prepare_onchain_transaction,
pay_onchain_with_options, assert_onchain_fee_guardrail, transaction.fee.fee_sats
and dangerously_disable_fee_guardrail to locate and change the logic so unknown
fees don't cause a hard guardrail failure.
- Around line 89-103: The sats_per_vbyte value in the
OnchainFeePreferenceType::SatsPerVbyte branch is missing a finite check and can
be NaN/Inf before casting; update the validation for sats_per_vbyte (used to
build PhoenixdOnchainFeeRequest with feerate_sat_byte) to reject non-finite
values by checking !sats_per_vbyte.is_finite() || sats_per_vbyte <= 0.0 and
return the same ApiError::InvalidInput when that condition is true so only
finite, positive rates are ceil()-cast to i64.

In `@crates/lni/phoenixd/lib.rs`:
- Around line 103-108: The forwarded pay_onchain convenience method
(pay_onchain) blindly calls crate::phoenixd::api::pay_onchain with default
guardrail options but prepare_onchain_transaction returns transactions with
fee_sats = None, so phoenixd's api rejects them unless guardrails are disabled;
update pay_onchain to detect a prepared OnchainTransaction with fee_sats == None
and either (A) call crate::phoenixd::api::pay_onchain with the guardrail option
explicitly disabled or (B) return a clear error instructing the caller to
re-quote/prepare with a fee; locate pay_onchain and prepare_onchain_transaction
symbols to implement the check and choose one of these two behaviors so the
prepare->pay flow no longer always fails.

---

Nitpick comments:
In `@crates/lni/cln/api.rs`:
- Around line 162-204: The CLN guardrail in assert_onchain_fee_guardrail
currently combines max_fee_sats and max_fee_by_percent with min(...) which
enforces both as a single threshold; change it to check each constraint
independently like the LND implementation by computing max_fee_by_percent as now
but then comparing fee_sats separately against max_fee_sats and against
max_fee_by_percent and returning distinct ApiError::InvalidInput messages for
each violation (e.g., "exceeds max_fee_sats {max_fee_sats}" and "exceeds
max_fee_percent {max_fee_by_percent}") so behavior and user-facing errors match
lnd/api.rs.

In `@crates/lni/lnd/api.rs`:
- Around line 187-196: The current fallback chain is redundant: you create
default_guardrail then call
guardrail.max_fee_sats.or(default_guardrail.max_fee_sats).unwrap_or(...).
Simplify by relying on options.fee_guardrail.unwrap_or_default() (the Default
provides the constants) and then use straightforward unwrap_or on the fields;
e.g., compute max_fee_sats from
guardrail.max_fee_sats.unwrap_or(crate::types::DEFAULT_ONCHAIN_MAX_FEE_SATS) and
max_fee_percent from
guardrail.max_fee_percent.unwrap_or(crate::types::DEFAULT_ONCHAIN_MAX_FEE_PERCENT),
removing the extra default_guardrail.or(...) logic.
🪄 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: c7b75170-1f18-40b0-811f-e4da4bac1538

📥 Commits

Reviewing files that changed from the base of the PR and between 14aaece and c0193eb.

📒 Files selected for processing (21)
  • bindings/lni_nodejs/index.d.ts
  • bindings/lni_nodejs/src/cln.rs
  • bindings/lni_nodejs/src/lnd.rs
  • bindings/lni_nodejs/src/phoenixd.rs
  • bindings/typescript/README.md
  • bindings/typescript/src/__tests__/cln.test.ts
  • bindings/typescript/src/__tests__/integration/cln.real.test.ts
  • bindings/typescript/src/__tests__/integration/lnd.real.test.ts
  • bindings/typescript/src/__tests__/integration/phoenixd.real.test.ts
  • bindings/typescript/src/__tests__/lnd.test.ts
  • bindings/typescript/src/__tests__/phoenixd.test.ts
  • bindings/typescript/src/nodes/cln.ts
  • bindings/typescript/src/nodes/lnd.ts
  • bindings/typescript/src/nodes/phoenixd.ts
  • crates/lni/cln/api.rs
  • crates/lni/cln/lib.rs
  • crates/lni/lnd/api.rs
  • crates/lni/lnd/lib.rs
  • crates/lni/phoenixd/api.rs
  • crates/lni/phoenixd/lib.rs
  • readme.md

Comment on lines +82 to +95
#[napi]
pub async fn pay_onchain(
&self,
transaction: OnchainTransaction,
options: Option<PayOnchainOptions>,
) -> Result<lni::types::PayOnchainResponse> {
lni::phoenixd::api::pay_onchain_with_options(
self.inner.clone(),
transaction,
options.unwrap_or_default(),
)
.await
.map_err(|e| napi::Error::from_reason(e.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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

The JS pay_onchain convenience path will fail for Phoenixd by default.

This wrapper defaults PayOnchainOptions, but Phoenixd prepared transactions intentionally omit fee_sats, so the backend guardrail rejects the payment unless dangerously_disable_fee_guardrail is set. That means the new JS-visible pay_onchain(transaction) API does not work for the normal prepare→pay flow.

Suggested fix
   pub async fn pay_onchain(
     &self,
     transaction: OnchainTransaction,
     options: Option<PayOnchainOptions>,
   ) -> Result<lni::types::PayOnchainResponse> {
-    lni::phoenixd::api::pay_onchain_with_options(
-      self.inner.clone(),
-      transaction,
-      options.unwrap_or_default(),
-    )
+    let options = options.unwrap_or_default();
+    if transaction.fee_sats.is_none() && !options.dangerously_disable_fee_guardrail {
+      return Err(napi::Error::from_reason(
+        "Phoenixd prepared on-chain transactions do not include feeSats; pass { dangerouslyDisableFeeGuardrail: true } explicitly or use an API that returns a fee quote.".to_string(),
+      ));
+    }
+
+    lni::phoenixd::api::pay_onchain_with_options(
+      self.inner.clone(),
+      transaction,
+      options,
+    )
     .await
     .map_err(|e| napi::Error::from_reason(e.to_string()))
   }
📝 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
#[napi]
pub async fn pay_onchain(
&self,
transaction: OnchainTransaction,
options: Option<PayOnchainOptions>,
) -> Result<lni::types::PayOnchainResponse> {
lni::phoenixd::api::pay_onchain_with_options(
self.inner.clone(),
transaction,
options.unwrap_or_default(),
)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn pay_onchain(
&self,
transaction: OnchainTransaction,
options: Option<PayOnchainOptions>,
) -> Result<lni::types::PayOnchainResponse> {
let options = options.unwrap_or_default();
if transaction.fee_sats.is_none() && !options.dangerously_disable_fee_guardrail {
return Err(napi::Error::from_reason(
"Phoenixd prepared on-chain transactions do not include feeSats; pass { dangerouslyDisableFeeGuardrail: true } explicitly or use an API that returns a fee quote.".to_string(),
));
}
lni::phoenixd::api::pay_onchain_with_options(
self.inner.clone(),
transaction,
options,
)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
🤖 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/phoenixd.rs` around lines 82 - 95, The defaulting in
pay_onchain currently calls options.unwrap_or_default(), which leaves
Phoenixd-prepared transactions (that omit fee_sats) rejected by backend
guardrails; update the defaulting logic in the pay_onchain function so that when
options is None you construct a PayOnchainOptions with
dangerously_disable_fee_guardrail = true (instead of the plain Default), then
pass that to lni::phoenixd::api::pay_onchain_with_options; reference the
pay_onchain function, PayOnchainOptions type, and the call to
lni::phoenixd::api::pay_onchain_with_options to locate where to change the
default.

Comment on lines +82 to +94
const fetchMock = vi.fn<FetchLike>(async (_input, init) => {
const body = init?.body ? JSON.parse(String(init.body)) : undefined;
expect(body).toEqual({
outputs: [{ bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh: '10000sat' }],
feerate: 'normal',
});

return jsonResponse({
txid: 'txid-1',
unsigned_tx: '02000000',
psbt: testPsbtWithFee(1_000),
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert the CLN prepare endpoint and request count.

This mock returns success for any fetch call, so prepareOnchainTransaction() would still pass if it hit the wrong RPC path or made extra network calls. Please assert url.pathname === '/v1/txprepare', the HTTP method, and expect(fetchMock).toHaveBeenCalledTimes(1) here.

🤖 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__/cln.test.ts` around lines 82 - 94, The
fetch mock in cln.test.ts is too permissive and should assert the CLN prepare
endpoint and call count; inside the fetchMock implementation (the vi.fn named
fetchMock) check that the received request's URL pathname equals '/v1/txprepare'
and that init?.method (or the request method) is 'POST', and after calling
prepareOnchainTransaction() add expect(fetchMock).toHaveBeenCalledTimes(1);
update the assertions in the test surrounding prepareOnchainTransaction and
fetchMock to ensure the RPC path, HTTP method, and a single network call are
verified.

Comment on lines +71 to +89
const transaction = await node.prepareOnchainTransaction({
address: process.env.CLN_ONCHAIN_TEST_ADDRESS!,
amountSats,
fee: { type: 'speed', speed: 'normal' },
description: testInvoiceLabel('cln onchain'),
});

expect(transaction.id?.length).toBeGreaterThan(0);
expect(transaction.amountSats).toBe(amountSats);
expect(transaction.feeSats).toBeGreaterThanOrEqual(0);

if (!shouldBroadcastOnchain) {
await clnPost('/v1/txdiscard', { txid: transaction.id });
return;
}

const payment = await node.payOnchain(transaction);
expect(payment.txid?.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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Always discard the prepared CLN transaction in a finally block.

Right now txdiscard only runs after the assertions pass. If one of those assertions fails, this test leaves the prepared CLN transaction behind, which can reserve wallet state and poison later runs. Move the discard into finally once transaction.id exists.

Suggested fix
-    const transaction = await node.prepareOnchainTransaction({
-      address: process.env.CLN_ONCHAIN_TEST_ADDRESS!,
-      amountSats,
-      fee: { type: 'speed', speed: 'normal' },
-      description: testInvoiceLabel('cln onchain'),
-    });
-
-    expect(transaction.id?.length).toBeGreaterThan(0);
-    expect(transaction.amountSats).toBe(amountSats);
-    expect(transaction.feeSats).toBeGreaterThanOrEqual(0);
-
-    if (!shouldBroadcastOnchain) {
-      await clnPost('/v1/txdiscard', { txid: transaction.id });
-      return;
-    }
-
-    const payment = await node.payOnchain(transaction);
-    expect(payment.txid?.length).toBeGreaterThan(0);
+    let transaction:
+      | Awaited<ReturnType<ClnNode['prepareOnchainTransaction']>>
+      | undefined;
+
+    try {
+      transaction = await node.prepareOnchainTransaction({
+        address: process.env.CLN_ONCHAIN_TEST_ADDRESS!,
+        amountSats,
+        fee: { type: 'speed', speed: 'normal' },
+        description: testInvoiceLabel('cln onchain'),
+      });
+
+      expect(transaction.id?.length).toBeGreaterThan(0);
+      expect(transaction.amountSats).toBe(amountSats);
+      expect(transaction.feeSats).toBeGreaterThanOrEqual(0);
+
+      if (!shouldBroadcastOnchain) {
+        return;
+      }
+
+      const payment = await node.payOnchain(transaction);
+      expect(payment.txid?.length).toBeGreaterThan(0);
+    } finally {
+      if (!shouldBroadcastOnchain && transaction?.id) {
+        await clnPost('/v1/txdiscard', { txid: transaction.id });
+      }
+    }
🤖 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
71 - 89, The test currently only calls clnPost('/v1/txdiscard', { txid:
transaction.id }) after assertions, so a failed assertion leaves the prepared
CLN transaction reserved; wrap the discard call in a finally block that always
runs after prepareOnchainTransaction — store the result of
node.prepareOnchainTransaction(...) in the variable transaction, then in finally
check transaction.id and call clnPost('/v1/txdiscard', { txid: transaction.id })
if present; ensure the existing shouldBroadcastOnchain flow still returns early
when appropriate and that node.payOnchain is only invoked when broadcasting.

Comment on lines +71 to +85
console.log('Prepared LND on-chain transaction:', transaction);

expect(transaction.address).toBe(process.env.LND_ONCHAIN_TEST_ADDRESS);
expect(transaction.amountSats).toBe(quoteOnlyAmountSats);
expect(transaction.feePayer).toBe('sender');
expect(transaction.feeSats).toBeGreaterThanOrEqual(0);

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

const payment = await node.payOnchain(transaction);

console.log('LND on-chain payment:', payment);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the full-object logs from the real LND on-chain flow.

These console.log calls dump live transaction/payment objects into test output. That exposes runtime payment metadata and any backend-specific raw fields carried on these objects. Please remove them or log only narrowly selected non-sensitive fields.

As per coding guidelines, "Do not print or expose sensitive runtime values in tests and logging, including: API keys, macaroons, runes, passwords, NWC URIs, 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
71 - 85, Remove the full-object console logs that expose sensitive runtime
fields in the LND on-chain test flow: replace or delete the console.log calls
that print the entire transaction and payment objects (see variables transaction
and payment and the call node.payOnchain) so tests only emit non-sensitive
scalar fields or nothing; if logging is needed, log a minimal whitelist such as
transaction.address, transaction.amountSats, and a sanitized payment.status or
payment.id instead of the whole objects, and ensure the early-return branch that
checks runOnchainSend keeps the minimal logging policy.

Comment on lines +89 to +103
OnchainFeePreferenceType::SatsPerVbyte => {
let sats_per_vbyte = fee.sats_per_vbyte.ok_or_else(|| {
ApiError::InvalidInput(
"Phoenixd sats_per_vbyte fee preference requires a fee rate".to_string(),
)
})?;
if sats_per_vbyte <= 0.0 {
return Err(ApiError::InvalidInput(
"Phoenixd sats_per_vbyte fee preference requires a positive fee rate".to_string(),
));
}

Ok(PhoenixdOnchainFeeRequest {
feerate_sat_byte: sats_per_vbyte.ceil() as i64,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing is_finite() check for sats_per_vbyte.

The LND implementation (line 152) checks !sats_per_vbyte.is_finite() || sats_per_vbyte <= 0.0, but Phoenixd only checks <= 0.0. This allows NaN or Infinity to pass validation and be cast to i64, resulting in undefined behavior.

🐛 Proposed fix
             if sats_per_vbyte <= 0.0 {
+            if !sats_per_vbyte.is_finite() || sats_per_vbyte <= 0.0 {
                 return Err(ApiError::InvalidInput(
                     "Phoenixd sats_per_vbyte fee preference requires a positive fee rate".to_string(),
                 ));
             }
🤖 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/phoenixd/api.rs` around lines 89 - 103, The sats_per_vbyte value
in the OnchainFeePreferenceType::SatsPerVbyte branch is missing a finite check
and can be NaN/Inf before casting; update the validation for sats_per_vbyte
(used to build PhoenixdOnchainFeeRequest with feerate_sat_byte) to reject
non-finite values by checking !sats_per_vbyte.is_finite() || sats_per_vbyte <=
0.0 and return the same ApiError::InvalidInput when that condition is true so
only finite, positive rates are ceil()-cast to i64.

Comment on lines +425 to +433
pub async fn pay_onchain_with_options(
config: PhoenixdConfig,
transaction: OnchainTransaction,
options: PayOnchainOptions,
) -> Result<PayOnchainResponse, ApiError> {
assert_valid_onchain_amount(transaction.amount_sats)?;
let _fee_payer = resolve_phoenixd_fee_payer(Some(transaction.fee_payer.clone()))?;
let fee_request = resolve_phoenixd_fee_request(&transaction.fee)?;
assert_onchain_fee_guardrail(&transaction, options)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Phoenixd on-chain payments always fail guardrail check unless explicitly disabled.

prepare_onchain_transaction sets fee_sats: None (line 397), but assert_onchain_fee_guardrail (lines 149-153) requires fee_sats to be Some or returns an error. This means Phoenixd users must always pass dangerously_disable_fee_guardrail: true to use pay_onchain, defeating the safety purpose of the guardrail.

Consider either:

  1. Skipping guardrail enforcement when fee_sats is None (since Phoenixd doesn't support fee estimation)
  2. Documenting this behavior clearly for Phoenixd users
  3. Having Phoenixd estimate fees differently
♻️ Option 1: Skip guardrail when fee unknown
 fn assert_onchain_fee_guardrail(
     transaction: &OnchainTransaction,
     options: PayOnchainOptions,
 ) -> Result<(), ApiError> {
     if options.dangerously_disable_fee_guardrail {
         return Ok(());
     }
+
+    // If fee_sats is unknown, we cannot enforce guardrails - caller must explicitly disable
+    let fee_sats = match transaction.fee_sats {
+        Some(fee) => fee,
+        None => return Ok(()), // Cannot enforce without fee information
+    };
🤖 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/phoenixd/api.rs` around lines 425 - 433, The guardrail check in
pay_onchain_with_options calls assert_onchain_fee_guardrail but
prepare_onchain_transaction leaves transaction.fee.fee_sats as None, causing the
guardrail to always error unless dangerously_disable_fee_guardrail is set;
update pay_onchain_with_options (or assert_onchain_fee_guardrail) to skip
enforcement when transaction.fee.fee_sats is None (i.e., treat unknown fees as
"cannot enforce" and allow processing), or alternatively make
prepare_onchain_transaction populate fee_sats before calling
assert_onchain_fee_guardrail; reference prepare_onchain_transaction,
pay_onchain_with_options, assert_onchain_fee_guardrail, transaction.fee.fee_sats
and dangerously_disable_fee_guardrail to locate and change the logic so unknown
fees don't cause a hard guardrail failure.

Comment on lines +103 to +108
pub async fn pay_onchain(
&self,
transaction: OnchainTransaction,
) -> Result<PayOnchainResponse, ApiError> {
crate::phoenixd::api::pay_onchain(self.config.clone(), transaction).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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

pay_onchain is broken for prepared Phoenixd transactions.

The new convenience method forwards default guardrail options, but this backend's own prepare flow returns fee_sats = None (Lines 266-270), and crates/lni/phoenixd/api.rs:134-176 rejects payment when fee_sats is unknown unless the caller explicitly disables the guardrail. As added, prepare_onchain_transaction(...) followed by pay_onchain(...) will fail every time for Phoenixd.

Suggested direction
-    pub async fn pay_onchain(
-        &self,
-        transaction: OnchainTransaction,
-    ) -> Result<PayOnchainResponse, ApiError> {
-        crate::phoenixd::api::pay_onchain(self.config.clone(), transaction).await
-    }
+    pub async fn pay_onchain(
+        &self,
+        _transaction: OnchainTransaction,
+    ) -> Result<PayOnchainResponse, ApiError> {
+        Err(ApiError::InvalidInput(
+            "Phoenixd prepared on-chain transactions do not include fee_sats; use pay_onchain_with_options(...) explicitly until prepare_onchain_transaction returns a fee quote.".to_string(),
+        ))
+    }

Longer term, the better fix is to either quote fee_sats during prepare or avoid exposing this convenience API for Phoenixd.

📝 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
pub async fn pay_onchain(
&self,
transaction: OnchainTransaction,
) -> Result<PayOnchainResponse, ApiError> {
crate::phoenixd::api::pay_onchain(self.config.clone(), transaction).await
}
pub async fn pay_onchain(
&self,
_transaction: OnchainTransaction,
) -> Result<PayOnchainResponse, ApiError> {
Err(ApiError::InvalidInput(
"Phoenixd prepared on-chain transactions do not include fee_sats; use pay_onchain_with_options(...) explicitly until prepare_onchain_transaction returns a fee quote.".to_string(),
))
}
🤖 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/phoenixd/lib.rs` around lines 103 - 108, The forwarded pay_onchain
convenience method (pay_onchain) blindly calls crate::phoenixd::api::pay_onchain
with default guardrail options but prepare_onchain_transaction returns
transactions with fee_sats = None, so phoenixd's api rejects them unless
guardrails are disabled; update pay_onchain to detect a prepared
OnchainTransaction with fee_sats == None and either (A) call
crate::phoenixd::api::pay_onchain with the guardrail option explicitly disabled
or (B) return a clear error instructing the caller to re-quote/prepare with a
fee; locate pay_onchain and prepare_onchain_transaction symbols to implement the
check and choose one of these two behaviors so the prepare->pay flow no longer
always fails.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
bindings/typescript/src/__tests__/lnd.test.ts (2)

172-189: ⚡ Quick win

Add test coverage for guardrail disabled case.

The test name mentions "unless the fee guardrail is disabled" but only validates the blocking behavior. To complete the test matrix, add a complementary test that passes { feeGuardrail: false } to payOnchain and verifies that manual sat/vbyte execution succeeds even without fee estimates.

🧪 Suggested test case to add
it('allows manual sat/vbyte execution when the fee guardrail is disabled', async () => {
  const fetchMock = vi.fn<FetchLike>(async (input, init) => {
    const url = new URL(String(input));
    const body = init?.body ? JSON.parse(String(init.body)) : undefined;

    if (url.pathname === '/v1/transactions') {
      expect(body).toMatchObject({
        addr: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
        amount: 10_000,
        sat_per_vbyte: '5',
      });
      return jsonResponse({ txid: 'txid-manual' });
    }

    return new Response('not found', { status: 404 });
  });
  const node = new LndNode(
    { url: 'https://lnd.test', macaroon: '00' },
    { fetch: fetchMock },
  );

  const payment = await node.payOnchain(
    {
      address: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
      amountSats: 10_000,
      recipientAmountSats: 10_000,
      feePayer: 'sender',
      fee: { type: 'satsPerVbyte', satsPerVbyte: 5 },
    },
    { feeGuardrail: false }, // Disable guardrail
  );

  expect(payment).toMatchObject({
    txid: 'txid-manual',
    state: 'pending',
  });
  expect(fetchMock).toHaveBeenCalledOnce();
});
🤖 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__/lnd.test.ts` around lines 172 - 189, Add a
complementary test that verifies payOnchain accepts manual sats-per-vbyte when
the fee guardrail is disabled: create a fetchMock that responds to POST
/v1/transactions with a successful txid, instantiate LndNode, call
node.payOnchain with the same payload used in the failing test but pass the
options { feeGuardrail: false }, and assert the returned payment has txid and
state 'pending' and that fetchMock was called; reference the LndNode class and
its payOnchain method and the existing fetchMock pattern to locate where to add
this test.

74-78: ⚡ Quick win

Consider explicitly asserting that raw.estimate is absent.

The test validates that raw.sendRequest and raw.label are present, but doesn't verify that raw.estimate is undefined. According to the upstream contract (context snippet 2), the manual sat/vbyte path returns early and never populates the estimate field.

✨ Suggested assertion for completeness
   expect(transaction.feeSats).toBeUndefined();
   expect(transaction.totalAmountSats).toBeUndefined();
+  expect(transaction.raw?.estimate).toBeUndefined();
   expect(fetchMock).not.toHaveBeenCalled();
🤖 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__/lnd.test.ts` around lines 74 - 78, The test
currently asserts that raw.sendRequest and raw.label are present but misses
asserting that raw.estimate is absent; update the test in
bindings/typescript/src/__tests__/lnd.test.ts to explicitly assert that
raw.estimate is undefined (e.g., using expect(raw.estimate).toBeUndefined())
after the existing assertions so the manual sat_per_vbyte code path's contract
(which should not populate estimate) is enforced; locate the assertion block
where the response object "raw" is inspected and add the explicit absence check
for raw.estimate.
crates/lni/nwc/lib.rs (1)

167-194: ⚡ Quick win

Consider using the existing lazy_static NODE for consistency.

This test creates a new NwcNode instance and manually loads NWC_URI with graceful skip logic, while all other tests in this file use the shared lazy_static NODE (lines 150-156). This creates inconsistency and duplicates the node setup logic.

Additionally, the graceful skip doesn't reliably work: if any other test runs first, the lazy_static initialization will panic if NWC_URI is not set (line 148), so this test would never get the chance to skip gracefully.

For consistency and to reduce duplication, consider using the existing NODE:

♻️ Refactor to use shared lazy_static NODE
 #[tokio::test]
 async fn test_get_permissions() {
-    dotenv().ok();
-    let nwc_uri = match env::var("NWC_URI") {
-        Ok(nwc_uri) => nwc_uri,
-        Err(_) => {
-            eprintln!("Skipping NWC permissions test because NWC_URI is not set");
-            return;
-        }
-    };
-    let node = NwcNode::new(NwcConfig {
-        nwc_uri,
-        ..Default::default()
-    });
-
-    match node.get_permissions().await {
+    match NODE.get_permissions().await {
         Ok(permissions) => {
             dbg!(&permissions);
             assert!(
                 !permissions.is_empty(),
                 "NWC permissions should include at least one supported method"
             );
         }
         Err(e) => {
             panic!("Failed to get NWC permissions: {:?}", e);
         }
     }
 }

Alternatively, if the graceful skip behavior is important, consider restructuring all tests to use the same optional pattern.

🤖 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/lib.rs` around lines 167 - 194, The test creates a new NwcNode
and re-implements NWC_URI loading/skipping; instead use the shared lazy_static
NODE used by the other tests: remove the dotenv/env block and NwcNode::new call
in test_get_permissions and instead call NODE.get_permissions().await (or
NODE.as_ref()/clone() as appropriate to your NODE type), then assert on the
returned permissions or panic on Err as before; this keeps setup consistent with
other tests and avoids duplicating NWC_URI handling.
🤖 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.

Nitpick comments:
In `@bindings/typescript/src/__tests__/lnd.test.ts`:
- Around line 172-189: Add a complementary test that verifies payOnchain accepts
manual sats-per-vbyte when the fee guardrail is disabled: create a fetchMock
that responds to POST /v1/transactions with a successful txid, instantiate
LndNode, call node.payOnchain with the same payload used in the failing test but
pass the options { feeGuardrail: false }, and assert the returned payment has
txid and state 'pending' and that fetchMock was called; reference the LndNode
class and its payOnchain method and the existing fetchMock pattern to locate
where to add this test.
- Around line 74-78: The test currently asserts that raw.sendRequest and
raw.label are present but misses asserting that raw.estimate is absent; update
the test in bindings/typescript/src/__tests__/lnd.test.ts to explicitly assert
that raw.estimate is undefined (e.g., using
expect(raw.estimate).toBeUndefined()) after the existing assertions so the
manual sat_per_vbyte code path's contract (which should not populate estimate)
is enforced; locate the assertion block where the response object "raw" is
inspected and add the explicit absence check for raw.estimate.

In `@crates/lni/nwc/lib.rs`:
- Around line 167-194: The test creates a new NwcNode and re-implements NWC_URI
loading/skipping; instead use the shared lazy_static NODE used by the other
tests: remove the dotenv/env block and NwcNode::new call in test_get_permissions
and instead call NODE.get_permissions().await (or NODE.as_ref()/clone() as
appropriate to your NODE type), then assert on the returned permissions or panic
on Err as before; this keeps setup consistent with other tests and avoids
duplicating NWC_URI handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91bf3350-ef06-46f5-b545-27d2a1a62906

📥 Commits

Reviewing files that changed from the base of the PR and between c0193eb and b9fe04d.

📒 Files selected for processing (6)
  • bindings/typescript/README.md
  • bindings/typescript/src/__tests__/lnd.test.ts
  • bindings/typescript/src/nodes/lnd.ts
  • crates/lni/lnd/api.rs
  • crates/lni/nwc/lib.rs
  • readme.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • crates/lni/lnd/api.rs
  • bindings/typescript/src/nodes/lnd.ts

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