diff --git a/examples/nwc/client/pay.ts b/examples/nwc/client/pay.ts new file mode 100644 index 0000000..4c4fced --- /dev/null +++ b/examples/nwc/client/pay.ts @@ -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); + +client.close(); diff --git a/examples/nwc/client/receive.ts b/examples/nwc/client/receive.ts new file mode 100644 index 0000000..f66082b --- /dev/null +++ b/examples/nwc/client/receive.ts @@ -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; + +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(); diff --git a/src/nwc/NWCClient.ts b/src/nwc/NWCClient.ts index d3c6835..e38f159 100644 --- a/src/nwc/NWCClient.ts +++ b/src/nwc/NWCClient.ts @@ -53,6 +53,10 @@ import { Nip47SettleHoldInvoiceResponse, Nip47CancelHoldInvoiceRequest, Nip47CancelHoldInvoiceResponse, + Nip47Bip321PayRequest, + Nip47Bip321PayResponse, + Nip47Bip321ReceiveRequest, + Nip47Bip321ReceiveResponse, Nip47NetworkError, } from "./types"; import { ReconnectingPool } from "./ReconnectingPool"; @@ -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 { + try { + const result = await this.executeNip47Request( + "pay", + request, + (result) => !!result.state, + ); + 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 { + try { + const result = await this.executeNip47Request( + "receive", + request, + (result) => !!result.bip321, + ); + return result; + } catch (error) { + console.error("Failed to request receive", error); + throw error; + } + } + async signMessage( request: Nip47SignMessageRequest, ): Promise { diff --git a/src/nwc/types.ts b/src/nwc/types.ts index 34b221d..3d75acd 100644 --- a/src/nwc/types.ts +++ b/src/nwc/types.ts @@ -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"; @@ -98,6 +100,9 @@ export type Nip47GetBalanceResponse = { balance: number; // msats }; +// TODO: rename to Nip47PayInvoiceResponse (with a Nip47PayKeysendResponse alias) +// and deprecate this name - "pay" now refers to the NWC-321 pay method +// (see Nip47Bip321PayResponse) export type Nip47PayResponse = { preimage: string; fees_paid: number; @@ -212,6 +217,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; diff --git a/src/webln/NostrWeblnProvider.ts b/src/webln/NostrWeblnProvider.ts index 2248ed1..9fa7630 100644 --- a/src/webln/NostrWeblnProvider.ts +++ b/src/webln/NostrWeblnProvider.ts @@ -69,6 +69,8 @@ const nip47ToWeblnRequestMap: Record< | "make_hold_invoice" | "settle_hold_invoice" | "cancel_hold_invoice" + | "pay" + | "receive" >, WebLNMethod > = {