Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 28 additions & 0 deletions examples/nwc/client/pay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

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

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

const nwcUrl =
process.env.NWC_URL ||
(await rl.question("Nostr Wallet Connect URL (nostr+walletconnect://...): "));
const payment = await rl.question(
"BIP-321 payment URI (bitcoin:?lightning=lnbc...): ",
);
rl.close();

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

const response = await client.pay({
payment,
// amount: 1000, // in millisats, required if the selected payment instruction has no amount
// payer_note: "a message from the payer",
});

console.info(response);

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

Resolve the no-console lint errors in both examples. ESLint reports no-console errors for the required CLI output.

  • examples/nwc/client/pay.ts#L26-L26: add a targeted lint suppression with a CLI-output reason, or use the project-approved output mechanism.
  • examples/nwc/client/receive.ts#L27-L27: apply the same approved output handling.
🧰 Tools
🪛 ESLint

[error] 26-26: Unexpected console statement.

(no-console)

📍 Affects 2 files
  • examples/nwc/client/pay.ts#L26-L26 (this comment)
  • examples/nwc/client/receive.ts#L27-L27
🤖 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 `@examples/nwc/client/pay.ts` at line 26, Resolve the no-console lint errors
for the required CLI output by applying the project-approved output mechanism,
or adding targeted suppressions with a CLI-output reason, at
examples/nwc/client/pay.ts lines 26-26 and examples/nwc/client/receive.ts lines
27-27; apply the same handling to both console.info calls.

Source: Linters/SAST tools


client.close();
29 changes: 29 additions & 0 deletions examples/nwc/client/receive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";

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

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

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

const amount =
parseInt((await rl.question("Amount in sats (default 1 sat): ")) || "1") *
1000;
Comment on lines +12 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

According to the MDN JavaScript reference, how do parseInt("1abc"), parseInt("abc"), and JSON.stringify({ amount: NaN }) behave?

💡 Result:

According to the MDN JavaScript reference, these operations behave as follows: parseInt("1abc") The function returns the integer 1 [1][2]. When parseInt encounters a character that is not a valid numeral in the specified radix (or decimal, if no radix is provided), it ignores that character and all succeeding characters, returning the integer value parsed up to that point [1][3]. parseInt("abc") The function returns NaN [1][2]. If the first character of the string cannot be converted to a number in the specified radix, parseInt returns NaN [1][3]. JSON.stringify({ amount: NaN }) The result is the string '{"amount":null}' [4][5]. During JSON serialization, the value NaN is considered null and is converted to null [4][6]. In the context of an object property, the NaN value is thus represented as null within the resulting JSON string [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(receive\.ts|.*nwc.*)$' | head -80

printf '%s\n' '--- receive.ts ---'
cat -n examples/nwc/client/receive.ts

printf '%s\n' '--- NWCClient request serialization and receive definitions ---'
rg -n -C 5 'JSON\.stringify|receive|amount' examples packages src 2>/dev/null | head -240

Repository: getAlby/js-sdk

Length of output: 17524


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- NWCClient outline ---'
ast-grep outline src/nwc/NWCClient.ts

printf '%s\n' '--- receive and request implementation ---'
rg -n -C 12 'async receive|receive\(|JSON\.stringify|params' src/nwc/NWCClient.ts src/nwc/types.ts

printf '%s\n' '--- receive request types and wallet-service handling ---'
rg -n -C 10 'Nip47Receive|ReceiveRequest|receive.*amount|amount.*receive|variable amount|variable-amount' src/nwc docs examples

Repository: getAlby/js-sdk

Length of output: 20678


🏁 Script executed:

#!/bin/bash
set -eu

node - <<'JS'
const inputs = ["", "1", "1abc", "abc", "0", " 2 ", "9007199254740"];
for (const raw of inputs) {
  const sats = parseInt(raw || "1");
  const amount = sats * 1000;
  const command = { method: "receive", params: { amount, description: "NWC Client example" } };
  console.log(JSON.stringify({
    raw,
    sats,
    amount,
    serializedParams: JSON.stringify(command.params),
    proposedValidation: (() => {
      const value = raw.trim() || "1";
      if (!/^[1-9]\d*$/.test(value)) return "invalid";
      const millisats = Number(value) * 1000;
      return Number.isSafeInteger(millisats) ? millisats : "too large";
    })(),
  }));
}
JS

Repository: getAlby/js-sdk

Length of output: 1203


Reject invalid satoshi input before creating the request.

parseInt accepts "1abc" as 1. Non-numeric input becomes NaN, which JSON serialization converts to null. The NWC receive type defines amount: null as a variable-amount request. Validate a positive safe integer before converting sats to millisats.

🤖 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 `@examples/nwc/client/receive.ts` around lines 12 - 14, Validate the parsed
amount in the receive flow before constructing the NWC request: require a
positive safe integer representing sats and reject malformed or non-numeric
input such as “1abc” or NaN. Only after validation should the value be converted
to millisats and assigned to the request’s amount, preserving null exclusively
for intentional variable-amount requests.


rl.close();

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

const response = await client.receive({
amount, // in millisats; omit for a variable amount (if supported by the wallet)
description: "NWC Client example",
});

console.info(response);

client.close();
48 changes: 48 additions & 0 deletions src/nwc/NWCClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ import {
Nip47SettleHoldInvoiceResponse,
Nip47CancelHoldInvoiceRequest,
Nip47CancelHoldInvoiceResponse,
Nip47Bip321PayRequest,
Nip47Bip321PayResponse,
Nip47Bip321ReceiveRequest,
Nip47Bip321ReceiveResponse,
Nip47NetworkError,
} from "./types";
import { ReconnectingPool } from "./ReconnectingPool";
Expand Down Expand Up @@ -539,6 +543,50 @@ export class NWCClient {
}
}

/**
* Pays one Lightning payment instruction from a BIP-321 URI (NWC-321).
*
* The wallet service selects and pays a single supported instruction
* (e.g. a BOLT-11 invoice or BOLT-12 offer) from the URI.
*
* @see https://github.com/nostr-wallet-connect/nwc/blob/main/321.md
*/
async pay(request: Nip47Bip321PayRequest): Promise<Nip47Bip321PayResponse> {
try {
const result = await this.executeNip47Request<Nip47Bip321PayResponse>(
"pay",
request,
(result) => !!result.state,
);
Comment on lines +554 to +560

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

Validate complete NWC-321 responses at the client boundary. Both validators accept partial or invalid payloads from decrypted JSON and return them as fully typed responses.

  • src/nwc/NWCClient.ts#L554-L560: validate required pay fields and accepted enum values, not only truthy state.
  • src/nwc/NWCClient.ts#L574-L582: validate that bip321 is a valid non-empty BIP-321 URI before returning the receive response.
📍 Affects 1 file
  • src/nwc/NWCClient.ts#L554-L560 (this comment)
  • src/nwc/NWCClient.ts#L574-L582
🤖 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 `@src/nwc/NWCClient.ts` around lines 554 - 560, Strengthen response validation
in NWCClient.pay (src/nwc/NWCClient.ts:554-560) to require every mandatory pay
response field and verify each enum-valued field against its accepted values,
rather than checking only truthy state. Also update the receive response
validation in NWCClient.receive (src/nwc/NWCClient.ts:574-582) to require bip321
to be a valid, non-empty BIP-321 URI before returning it.

return result;
} catch (error) {
console.error("Failed to request pay", error);
throw error;
}
}

/**
* Creates a BIP-321 URI containing one or more wallet-selected Lightning
* receive instructions that can be given to a payer (NWC-321).
*
* @see https://github.com/nostr-wallet-connect/nwc/blob/main/321.md
*/
async receive(
request: Nip47Bip321ReceiveRequest,
): Promise<Nip47Bip321ReceiveResponse> {
try {
const result = await this.executeNip47Request<Nip47Bip321ReceiveResponse>(
"receive",
request,
(result) => !!result.bip321,
);
return result;
} catch (error) {
console.error("Failed to request receive", error);
throw error;
}
}

async signMessage(
request: Nip47SignMessageRequest,
): Promise<Nip47SignMessageResponse> {
Expand Down
53 changes: 52 additions & 1 deletion src/nwc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export type Nip47SingleMethod =
| "create_connection"
| "make_hold_invoice"
| "settle_hold_invoice"
| "cancel_hold_invoice";
| "cancel_hold_invoice"
| "pay"
| "receive";

export type Nip47MultiMethod = "multi_pay_invoice" | "multi_pay_keysend";

Expand Down Expand Up @@ -212,6 +214,55 @@ export type Nip47PayKeysendRequest = {
tlv_records?: { type: number; value: string }[];
};

/**
* Request to pay a Lightning payment instruction from a BIP-321 URI (NWC-321).
* @see https://github.com/nostr-wallet-connect/nwc/blob/main/321.md
*/
export type Nip47Bip321PayRequest = {
payment: string; // BIP-321 URI e.g. "bitcoin:?lightning=lnbc..."
amount?: number; // msats, required if the selected payment instruction has no amount
payer_note?: string;
metadata?: Nip47TransactionMetadata;
};

/**
* Response of the NWC-321 `pay` method.
* @see https://github.com/nostr-wallet-connect/nwc/blob/main/321.md
*/
export type Nip47Bip321PayResponse = {
transaction_id: string; // wallet-scoped transaction identifier
state: "pending" | "settled" | "failed";
instruction_type: "bolt11" | "bolt12";
amount: number; // paid amount in msats
fees_paid: number; // paid fees in msats
payment_hash?: string;
preimage?: string;
payer_proof?: string; // BOLT-12 payer proof
txid?: string; // on-chain transaction identifier
failure_reason?: string; // present if state is "failed"
created_at: number;
settled_at?: number;
};

/**
* Request to create a BIP-321 URI for receiving a payment (NWC-321).
* @see https://github.com/nostr-wallet-connect/nwc/blob/main/321.md
*/
export type Nip47Bip321ReceiveRequest = {
amount?: number | null; // msats; omit or use null for a variable amount
description?: string;
metadata?: Nip47TransactionMetadata;
};

/**
* Response of the NWC-321 `receive` method.
* @see https://github.com/nostr-wallet-connect/nwc/blob/main/321.md
*/
export type Nip47Bip321ReceiveResponse = {
bip321: string; // BIP-321 URI e.g. "bitcoin:?lightning=lnbc..."
transaction_id?: string; // wallet-scoped transaction identifier
};

export type Nip47MakeInvoiceRequest = {
amount: number; //msat
description?: string;
Expand Down
2 changes: 2 additions & 0 deletions src/webln/NostrWeblnProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ const nip47ToWeblnRequestMap: Record<
| "make_hold_invoice"
| "settle_hold_invoice"
| "cancel_hold_invoice"
| "pay"
| "receive"
Comment on lines +72 to +73

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 | 🟡 Minor | ⚡ Quick win

Filter methods that have no WebLN mapping.

When a wallet advertises pay or receive, getInfo() maps the method at src/webln/NostrWeblnProvider.ts Line 160 and returns undefined in methods. Omit unmapped NIP-47 methods before returning the WebLN response.

Proposed fix
-        methods: nip47Result.methods.map(
-          (key) =>
-            nip47ToWeblnRequestMap[key as keyof typeof nip47ToWeblnRequestMap],
-        ),
+        methods: nip47Result.methods.flatMap((key) => {
+          const method =
+            nip47ToWeblnRequestMap[
+              key as keyof typeof nip47ToWeblnRequestMap
+            ];
+          return method === undefined ? [] : [method];
+        }),
🤖 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 `@src/webln/NostrWeblnProvider.ts` around lines 72 - 73, Update getInfo() to
filter out NIP-47 methods such as “pay” and “receive” when their WebLN mapping
is undefined, so the returned methods array contains only mapped capabilities.

>,
WebLNMethod
> = {
Expand Down
Loading