Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions examples/nwc/client/hold-invoice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import "websocket-polyfill"; // required in node.js
import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

import { NWCClient } from "@getalby/sdk/nwc";
import { Nip47Notification, NWCClient } from "@getalby/sdk/nwc";

const rl = readline.createInterface({ input, output });

Expand All @@ -21,7 +21,7 @@ const client = new NWCClient({
nostrWalletConnectUrl: nwcUrl,
});

const toHexString = (bytes) =>
const toHexString = (bytes: Uint8Array<ArrayBuffer>) =>
bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");

const preimageBytes = crypto.getRandomValues(new Uint8Array(32));
Expand All @@ -41,7 +41,7 @@ const response = await client.makeHoldInvoice({

console.info(response.invoice);

const onNotification = async (notification) => {
const onNotification = async (notification: Nip47Notification) => {
if (notification.notification.payment_hash !== paymentHash) {
console.info("Skipping unrelated notification", notification);
return;
Expand Down
80 changes: 80 additions & 0 deletions examples/nwc/client/wrapped-invoice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import "websocket-polyfill"; // required in node.js

import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

import { Nip47Notification, NWCClient } from "@getalby/sdk/nwc";
import { Invoice } from "@getalby/lightning-tools";

console.warn(
"Alby Hub WARNING: This currently only works with Alby Hub LND backend or self payments between sub-wallets\n",
);

const rl = readline.createInterface({ input, output });

const nwcUrl =
process.env.NWC_URL ||
(await rl.question("Nostr Wallet Connect URL (nostr+walletconnect://...): "));

const upstreamInvoice = await rl.question("Upstream Invoice: ");

const paymentHash = new Invoice({ pr: upstreamInvoice.trim() }).paymentHash;
console.info("Payment hash:", paymentHash);

const amount =
parseInt(
(await rl.question("Extra amount in sats (default 1 sat): ")) || "1",
) * 1000;
Comment on lines +24 to +27
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual file to see the full context
head -80 examples/nwc/client/wrapped-invoice.ts

Repository: getAlby/js-sdk

Length of output: 2268


🏁 Script executed:

# Search for Invoice class usage and imports
rg -n "Invoice" examples/nwc/client/wrapped-invoice.ts -B2 -A2

Repository: getAlby/js-sdk

Length of output: 948


🏁 Script executed:

# Check package.json to understand `@getalby/lightning-tools` dependency
fd package.json | xargs cat

Repository: getAlby/js-sdk

Length of output: 3766


🏁 Script executed:

# Look at hold-invoice.ts for comparison (mentioned in original comment)
fd "hold-invoice.ts" --type f

Repository: getAlby/js-sdk

Length of output: 93


🏁 Script executed:

# Search for any type definitions or imports from `@getalby/lightning-tools`
rg "@getalby/lightning-tools" --type=ts -A3

Repository: getAlby/js-sdk

Length of output: 2243


🏁 Script executed:

# Look for Invoice class definition or type in the SDK source
rg -n "class Invoice|interface Invoice" --type=ts -A 10

Repository: getAlby/js-sdk

Length of output: 40


🏁 Script executed:

# Check if there's any documentation or usage of Invoice.satoshi
rg "satoshi|\.amount" examples/nwc/client/ --type=ts -B2 -A2

Repository: getAlby/js-sdk

Length of output: 384


🏁 Script executed:

# Look at hold-invoice.ts to compare the pattern
cat examples/nwc/client/hold-invoice.ts

Repository: getAlby/js-sdk

Length of output: 2447


🏁 Script executed:

# Search for any comments or documentation about wrapped invoices
rg -i "wrapped.*invoice|wrap" --type=ts -B2 -A2

Repository: getAlby/js-sdk

Length of output: 1654


🏁 Script executed:

# Search for node_modules or type definitions for `@getalby/lightning-tools`
find . -name "lightning-tools" -o -name "*lightning-tools*" | head -20

Repository: getAlby/js-sdk

Length of output: 40


🏁 Script executed:

# Check if there's a dist/types directory with type definitions
find . -path "*/node_modules/@getalby/lightning-tools" -type d

Repository: getAlby/js-sdk

Length of output: 40


🏁 Script executed:

# Try to look at what properties Invoice instance uses in the code
rg "Invoice.*\{.*pr:" --type=ts -A 15

Repository: getAlby/js-sdk

Length of output: 3180


🏁 Script executed:

# Search for any .d.ts files or interfaces that might define Invoice
fd "\.d\.ts$" | xargs rg "Invoice" -A 10 2>/dev/null | head -100

Repository: getAlby/js-sdk

Length of output: 4501


🏁 Script executed:

# Check if there are any test files that use Invoice
fd "test|spec" --type f | xargs rg "Invoice" -A 5 -B 5 2>/dev/null | head -50

Repository: getAlby/js-sdk

Length of output: 40


🏁 Script executed:

# Search for any usage of Invoice properties beyond paymentHash
rg "Invoice.*\(" --type=ts -A 10 | grep -E "\.(paymentHash|satoshi|amount|msat)" | head -20

Repository: getAlby/js-sdk

Length of output: 602


🌐 Web query:

@getalby/lightning-tools Invoice class satoshi amount property documentation

💡 Result:

In @getalby/lightning-tools, the Invoice class exposes the invoice amount as a satoshi: number property (there isn’t an amount field on the class). It’s set when the constructor decodes the BOLT11 pr and assigns this.satoshi = decodedInvoice.satoshi. (raw.githubusercontent.com)

Usage-wise, the README shows both:

  • requesting an invoice by passing { satoshi: 1000 }, and
  • decoding an invoice and reading { paymentHash, satoshi, description, createdDate, expiryDate } from the Invoice instance. (github.com)

Citations:


🏁 Script executed:

# Let's check if there are any other usages or patterns with Invoice.satoshi in the codebase
rg "\.satoshi" --type=ts -B 2 -A 2

Repository: getAlby/js-sdk

Length of output: 2466


🏁 Script executed:

# Verify the wrapped invoice logic one more time - check what the intended behavior should be
cat -n examples/nwc/client/wrapped-invoice.ts | head -70

Repository: getAlby/js-sdk

Length of output: 2575


🏁 Script executed:

# Check if there's any documentation in README about wrapped invoices
fd "README|readme" | xargs grep -i "wrap" -B 2 -A 2 2>/dev/null

Repository: getAlby/js-sdk

Length of output: 42


🏁 Script executed:

# One final check - verify the exact property name and confirm satoshi vs amount
rg "Invoice" examples/lnclient/splitter.ts -A 5 -B 5

Repository: getAlby/js-sdk

Length of output: 40


Hold invoice amount must include the upstream invoice amount plus the extra fee.

The amount variable (lines 24-27) is calculated only from user input for the extra fee and passed directly to makeHoldInvoice (line 35). For a wrapped invoice to function correctly, the hold invoice must be created for upstream_amount + extra_amount so that paying it provides sufficient funds to cover the upstream payment. Currently, if the upstream invoice is 10,000 sats and the extra is 1 sat (default), the hold invoice will only be 1 sat—insufficient to pay upstream.

Extract the upstream amount and include it in the calculation:

Proposed fix
+const upstreamAmount = new Invoice({ pr: upstreamInvoice.trim() }).satoshi;
+
 const amount =
   parseInt(
     (await rl.question("Extra amount in sats (default 1 sat): ")) || "1",
-  ) * 1000;
+  ) * 1000 +
+  upstreamAmount * 1000;

Also applies to: lines 35-41

🤖 Prompt for AI Agents
In `@examples/nwc/client/wrapped-invoice.ts` around lines 24 - 27, The hold
invoice `amount` is currently set only from the user's extra-fee input and must
instead be upstream_amount + extra_amount; locate the user-facing `amount`
variable and where `makeHoldInvoice` is called, extract the upstream invoice
amount (e.g., `upstream.amount`, `upstreamAmount`, or the parsed msat/sat value
from the fetched upstream invoice) convert units consistently with the extra
fee, add it to the parsed extra fee, and pass that sum into `makeHoldInvoice` so
the hold invoice covers both the upstream payment and the extra fee.


rl.close();

const client = new NWCClient({
nostrWalletConnectUrl: nwcUrl,
});

const response = await client.makeHoldInvoice({
amount, // in millisats
description: "NWC HODL invoice example",
payment_hash: paymentHash,
// or set a 256-bit description hash:
//description_hash: "a40f2b27a4414044995b26b73eb5aa66688b5f18d6a8a2513827d9a116ad95f1",
});

console.info(response.invoice);

const onNotification = async (notification: Nip47Notification) => {
if (notification.notification.payment_hash !== paymentHash) {
console.info("Skipping unrelated notification", notification);
return;
}
console.info(
"HOLD invoice accepted! It can be settled or canceled before block " +
notification.notification.settle_deadline,
);

console.info("Paying upstream invoice", upstreamInvoice);

const { preimage } = await client.payInvoice({
invoice: upstreamInvoice,
});

console.info("Paid upstream invoice", preimage);

await client.settleHoldInvoice({ preimage });

process.exit();
Comment on lines +57 to +65
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing error handling: upstream payment failure leaves hold invoice in limbo.

If client.payInvoice fails (e.g., no route, insufficient balance), the hold invoice is never settled or canceled, locking the payer's funds until the settle deadline expires. Wrap the pay+settle in a try/catch and cancel the hold invoice on failure.

Proposed fix
-  const { preimage } = await client.payInvoice({
-    invoice: upstreamInvoice,
-  });
-
-  console.info("Paid upstream invoice", preimage);
-
-  await client.settleHoldInvoice({ preimage });
+  try {
+    const { preimage } = await client.payInvoice({
+      invoice: upstreamInvoice,
+    });
+
+    console.info("Paid upstream invoice", preimage);
+
+    await client.settleHoldInvoice({ preimage });
+  } catch (e) {
+    console.error("Failed to pay upstream invoice, canceling hold invoice", e);
+    await client.cancelHoldInvoice({ payment_hash: paymentHash });
+  }
🤖 Prompt for AI Agents
In `@examples/nwc/client/wrapped-invoice.ts` around lines 57 - 65, The pay+settle
sequence can leave the hold invoice unresolved if client.payInvoice throws; wrap
the call to client.payInvoice({... upstreamInvoice ...}) and the subsequent
client.settleHoldInvoice({ preimage }) in a try/catch, capture the thrown error,
and in the catch call client.cancelHoldInvoice({ preimage }) (or cancel using
the hold invoice id if no preimage) before rethrowing or exiting; ensure the
final process.exit() runs only after settle or cancel completes and log the
error while canceling to aid debugging.

};

const unsub = await client.subscribeNotifications(onNotification, [
"hold_invoice_accepted",
]);

console.info("Waiting for payment to be made...");
process.on("SIGINT", function () {
console.info("Caught interrupt signal");

unsub();
client.close();

process.exit();
});