chore: add example for wrapped invoices#529
Conversation
📝 WalkthroughWalkthroughTwo example files in the NWC client directory are modified. The first receives TypeScript type annotations on function parameters. The second introduces a new example script demonstrating hold-invoice workflow, including user input collection, invoice creation, notification listening, upstream payment, and settlement. Changes
Sequence DiagramsequenceDiagram
actor User
participant Script
participant NWCClient
participant WalletServer as Wallet Server<br/>(NWC)
User->>Script: Provides NWC URL, upstream invoice, extra amount
Script->>Script: Derives payment_hash from invoice
Script->>NWCClient: Initialize with NWC URL
Script->>WalletServer: Request hold_invoice creation
WalletServer->>Script: Return hold invoice
Script->>User: Display hold invoice
Script->>WalletServer: Subscribe to notifications
WalletServer->>Script: Emit hold_invoice_accepted notification
Script->>WalletServer: Pay upstream invoice
Script->>WalletServer: Settle hold invoice with preimage
Script->>User: Process complete, exit
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@examples/nwc/client/wrapped-invoice.ts`:
- Around line 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.
- Around line 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.
| const amount = | ||
| parseInt( | ||
| (await rl.question("Extra amount in sats (default 1 sat): ")) || "1", | ||
| ) * 1000; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the actual file to see the full context
head -80 examples/nwc/client/wrapped-invoice.tsRepository: 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 -A2Repository: getAlby/js-sdk
Length of output: 948
🏁 Script executed:
# Check package.json to understand `@getalby/lightning-tools` dependency
fd package.json | xargs catRepository: 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 fRepository: 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 -A3Repository: 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 10Repository: 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 -A2Repository: getAlby/js-sdk
Length of output: 384
🏁 Script executed:
# Look at hold-invoice.ts to compare the pattern
cat examples/nwc/client/hold-invoice.tsRepository: 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 -A2Repository: 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 -20Repository: 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 dRepository: 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 15Repository: 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 -100Repository: 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 -50Repository: 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 -20Repository: 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 theInvoiceinstance. (github.com)
Citations:
- 1: https://raw.githubusercontent.com/getAlby/js-lightning-tools/master/src/bolt11/Invoice.ts
- 2: https://github.com/getAlby/js-lightning-tools
🏁 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 2Repository: 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 -70Repository: 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/nullRepository: 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 5Repository: 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.
| const { preimage } = await client.payInvoice({ | ||
| invoice: upstreamInvoice, | ||
| }); | ||
|
|
||
| console.info("Paid upstream invoice", preimage); | ||
|
|
||
| await client.settleHoldInvoice({ preimage }); | ||
|
|
||
| process.exit(); |
There was a problem hiding this comment.
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.
Summary by CodeRabbit
New Features
Refactor