From 8bb6871c930b6a3e2ca0aeec46a38d19da058680 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 20 Mar 2025 20:08:34 +0700 Subject: [PATCH 01/29] feat: add simpler LNClient class --- README.md | 516 +----------------- docs/ln.md | 21 + docs/nwc.md | 228 ++++++++ docs/oauth.md | 280 ++++++++++ examples/ln/paywall.js | 34 ++ examples/{ => oauth}/AlbyOauthCallback.jsx | 0 examples/{ => oauth}/boostagram.js | 2 +- examples/{ => oauth}/decode-invoice.js | 2 +- examples/{ => oauth}/invoices.js | 2 +- examples/{ => oauth}/keysends.js | 2 +- .../oauth2-public-callback_pkce_s256.mjs | 2 +- examples/{ => oauth}/send-to-ln-address.js | 2 +- examples/{ => oauth}/webhooks.js | 2 +- package.json | 4 +- src/LNClient.ts | 100 ++++ src/index.ts | 1 + yarn.lock | 2 +- 17 files changed, 691 insertions(+), 509 deletions(-) create mode 100644 docs/ln.md create mode 100644 docs/nwc.md create mode 100644 docs/oauth.md create mode 100644 examples/ln/paywall.js rename examples/{ => oauth}/AlbyOauthCallback.jsx (100%) rename examples/{ => oauth}/boostagram.js (97%) rename examples/{ => oauth}/decode-invoice.js (95%) rename examples/{ => oauth}/invoices.js (94%) rename examples/{ => oauth}/keysends.js (95%) rename examples/{ => oauth}/oauth2-public-callback_pkce_s256.mjs (97%) rename examples/{ => oauth}/send-to-ln-address.js (95%) rename examples/{ => oauth}/webhooks.js (95%) create mode 100644 src/LNClient.ts diff --git a/README.md b/README.md index dabb0fe..2ba1567 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ## Introduction -This JavaScript SDK for the Alby OAuth2 Wallet API and the Nostr Wallet Connect API. +This JavaScript SDK is for interacting with a bitcoin lightning wallet via Nostr Wallet Connect or the Alby Wallet API. ## Installing @@ -30,521 +30,39 @@ or for use without any build tools: **This library relies on a global fetch() function which will work in browsers and node v18.x or newer.** (In older versions you have to use a polyfill.) -## Content +## Lightning Network Client Documentation -- [Nostr Wallet Connect](#nostr-wallet-connect-documentation) -- [Alby OAuth API](#oauth-api-documentation) -- [Need help?](#need-help) +Quickly get started adding lightning payments to your app. -## Nostr Wallet Connect Documentation - -[Nostr Wallet Connect](https://nwc.dev) is an open protocol enabling applications to interact with bitcoin lightning wallets. It allows users to connect their existing wallets to your application allowing developers to easily integrate bitcoin lightning functionality. - -The Alby JS SDK allows you to easily integrate Nostr Wallet Connect into any JavaScript based application. - -There are two interfaces you can use to access NWC: - -- The `NWCClient` exposes the [NWC](https://nwc.dev/) interface directly, which is more powerful than the WebLN interface and is recommended if you plan to create an application outside of the web (e.g. native mobile/command line/server backend etc.). You can explore all the examples [here](./examples/nwc/client/). -- The `NostrWebLNProvider` exposes the [WebLN](https://webln.guide/) interface to execute lightning wallet functionality through Nostr Wallet Connect, such as sending payments, making invoices and getting the node balance. You can explore all the examples [here](./examples/nwc/). See also [Bitcoin Connect](https://github.com/getAlby/bitcoin-connect/) if you are developing a frontend web application. - -### NWCClient - -#### Initialization Options - -- `nostrWalletConnectUrl`: full Nostr Wallet Connect URL as defined by the [spec](https://github.com/getAlby/nips/blob/master/47.md) -- `relayUrl`: URL of the Nostr relay to be used (e.g. wss://nostr-relay.getalby.com) -- `walletPubkey`: pubkey of the Nostr Wallet Connect app -- `secret`: secret key to sign the request event (if not available window.nostr will be used) - -#### `static fromAuthorizationUrl()` - -Initialized a new `NWCClient` instance but generates a new random secret. The pubkey of that secret then needs to be authorized by the user (this can be initiated by redirecting the user to the `getAuthorizationUrl()` URL or calling `fromAuthorizationUrl()` to open an authorization popup. - -##### Example - -```js -const nwcClient = await nwc.NWCClient.fromAuthorizationUrl( - "https://my.albyhub.com/apps/new", - { - name: "My app name", - }, -); -``` - -The same options can be provided to getAuthorizationUrl() as fromAuthorizationUrl() - see [Manual Auth example](./examples/nwc/client/auth_manual.html) - -#### Quick start example - -```js -import { nwc } from "@getalby/sdk"; -const nwcClient = new nwc.NWCClient({ - nostrWalletConnectUrl: loadNWCUrl(), -}); // loadNWCUrl is some function to get the NWC URL from some (encrypted) storage - -// now you can send payments by passing in the invoice in an object -const response = await nwcClient.payInvoice({ invoice }); -``` - -See [the NWC client examples directory](./examples/nwc/client) for a full list of examples. - -### NostrWebLNProvider (aliased as NWC) Options - -- `nostrWalletConnectUrl`: full Nostr Wallet Connect URL as defined by the [spec](https://github.com/getAlby/nips/blob/master/47.md) -- `relayUrl`: URL of the Nostr relay to be used (e.g. wss://nostr-relay.getalby.com) -- `walletPubkey`: pubkey of the Nostr Wallet Connect app -- `secret`: secret key to sign the request event (if not available window.nostr will be used) -- `client`: initialize using an existing NWC client - -### Quick start example - -```js -import { webln } from "@getalby/sdk"; -const nwc = new webln.NostrWebLNProvider({ - nostrWalletConnectUrl: loadNWCUrl(), -}); // loadNWCUrl is some function to get the NWC URL from some (encrypted) storage -// or use the short version -const nwc = new webln.NWC({ nostrWalletConnectUrl: loadNWCUrl }); - -// connect to the relay -await nwc.enable(); - -// now you can send payments by passing in the invoice -const response = await nwc.sendPayment(invoice); -``` - -You can use NWC as a webln compatible object in your web app: - -```js -// you can set the window.webln object to use the universal API to send payments: -if (!window.webln) { - // prompt the user to connect to NWC - window.webln = new webln.NostrWebLNProvider({ - nostrWalletConnectUrl: loadNWCUrl, - }); - // now use any webln code -} -``` - -### NostrWebLNProvider Functions - -The goal of the Nostr Wallet Connect provider is to be API compatible with [webln](https://www.webln.guide/). Currently not all methods are supported - see the examples/nwc directory for a list of supported methods. - -#### sendPayment(invice: string) - -Takes a bolt11 invoice and calls the NWC `pay_invoice` function. -It returns a promise object that is resolved with an object with the preimage or is rejected with an error - -##### Example - -```js -import { webln } from "@getalby/sdk"; -const nwc = new webln.NostrWebLNProvider({ nostrWalletConnectUrl: loadNWCUrl }); -await nwc.enable(); -const response = await nwc.sendPayment(invoice); -console.log(response); -``` - -#### getNostrWalletConnectUrl() - -Returns the `nostr+walletconnect://` URL which includes all the connection information (`walletPubkey`, `relayUrl`, `secret`) -This can be used to get and persist the string for later use. - -#### fromAuthorizationUrl(url: string, {name: string}) - -Opens a new window prompt with at the provided authorization URL to ask the user to authorize the app connection. -The promise resolves when the connection is authorized and the popup sends a `nwc:success` message or rejects when the prompt is closed. -Pass a `name` to the NWC provider describing the application. +For example, to make a payment: ```js -import { webln } from "@getalby/sdk"; - -try { - const nwc = await webln.NostrWebLNProvider.fromAuthorizationUrl( - "https://my.albyhub.com/apps/new", - { - name: "My app name", - }, - ); -} catch (e) { - console.error(e); -} -await nwc.enable(); -let response; -try { - response = await nwc.sendPayment(invoice); - // if success then the response.preimage will be only - console.info(`payment successful, the preimage is ${response.preimage}`); -} catch (e) { - console.error(e.error || e); -} +await new LNClient(credentials).pay(invoice); ``` -#### React Native (Expo) - -Look at our [NWC React Native Expo Demo app](https://github.com/getAlby/nwc-react-native-expo) for how to use NWC in a React Native expo project. - -#### For Node.js - -To use this on Node.js you first must install `websocket-polyfill` and import it: +Or to request a payment to be received: ```js -import "websocket-polyfill"; -// or: require('websocket-polyfill'); +const request = await new LNClient(credentials).receive(USD(1.0)); +// give request.invoice to someone... +request.onPaid(giveAccess); ``` -if you get an `crypto is not defined` error, either upgrade to node.js 20 or above, or import it manually: +[Read more](./docs/ln.md) -```js -import * as crypto from 'crypto'; // or 'node:crypto' -globalThis.crypto = crypto as any; -//or: global.crypto = require('crypto'); -``` - -### Examples - -#### Defaults - -```js -import { webln } from "@getalby/sdk"; - -const nwc = new webln.NostrWebLNProvider(); // use defaults (connects to Alby's relay, will use window.nostr to sign the request) -await nwc.enable(); // connect to the relay -const response = await nwc.sendPayment(invoice); -console.log(response.preimage); - -nwc.close(); // close the websocket connection -``` - -#### Use a custom, user provided Nostr Wallet Connect URL - -```js -import { webln } from "@getalby/sdk"; - -const nwc = new webln.NostrWebLNProvider({ - nostrWalletConnectUrl: - "nostr+walletconnect://69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9?relay=wss://nostr.bitcoiner.social&secret=c60320b3ecb6c15557510d1518ef41194e9f9337c82621ddef3f979f668bfebd", -}); // use defaults -await nwc.enable(); // connect to the relay -const response = await nwc.sendPayment(invoice); -console.log(response.preimage); - -nwc.close(); // close the websocket connection -``` +### Quick Start -#### Generate a new NWC connect url using a locally-generated secret - -```js -// use the `fromAuthorizationUrl` helper which opens a popup to initiate the connection flow. -// the promise resolves once the NWC app returned. -const nwc = await webln.NostrWebLNProvider.fromAuthorizationUrl( - "https://my.albyhub.com/apps/new", - { - name: "My app name", - }, -); - -// ... enable and send a payment - -// if you want to get the connect url with the secret: -// const nostrWalletConnectUrl nwc.getNostrWalletConnectUrl(true) -``` - -The same options can be provided to getAuthorizationUrl() as fromAuthorizationUrl() - see [Manual Auth example](./examples/nwc/auth_manual.html) - -### Nostr Wallet Auth - -NWA is an alternative flow for lightning apps to easily initialize an NWC connection to mobile-first or self-custodial wallets, using a client-created secret. - -The app will generate an NWA URI which should be opened in the wallet, where the user can approve the connection. - -#### Generating an NWA URI - -See [NWA example](examples/nwc/client/nwa.js) - -### Accepting and creating a connection from an NWA URI - -See [NWA accept example](examples/nwc/client/nwa.js) for NWA URI parsing and handling. The implementation of actually creating the connection and showing a confirmation page to the user is wallet-specific. In the example, a connection will be created via the `create_connection` NWC command. - -## OAuth API Documentation - -Please have a look a the Alby OAuth2 Wallet API: - -[https://guides.getalby.com/alby-wallet-api/reference/getting-started](https://guides.getalby.com/alby-wallet-api/reference/getting-started) - -### Avalilable methods - -- accountBalance -- accountSummary -- signMessage -- accountInformation -- accountValue4Value -- invoices -- incomingInvoices -- outgoingInvoices -- getInvoice -- createInvoice -- decodeInvoice -- keysend -- sendPayment -- sendBoostagram -- sendBoostagramToAlbyAccount -- createWebhookEndpoint -- deleteWebhookEndpoint - -### Examples - -#### Full OAuth Authentication flow - -```js -const authClient = new auth.OAuth2User({ - client_id: process.env.CLIENT_ID, - client_secret: process.env.CLIENT_SECRET, - callback: "http://localhost:8080/callback", - scopes: [ - "invoices:read", - "account:read", - "balance:read", - "invoices:create", - "invoices:read", - "payments:send", - ], - token: { - access_token: undefined, - refresh_token: undefined, - expires_at: undefined, - }, // initialize with existing token -}); - -const authUrl = await authClient.generateAuthURL({ - code_challenge_method: "S256", - // authorizeUrl: "https://getalby.com/oauth" endpoint for authorization (replace with the appropriate URL based on the environment) -}); -// open auth URL -// `code` is passed as a query parameter when the user is redirected back after authorization -await authClient.requestAccessToken(code); - -// access the token response. You can store this securely for future client initializations -console.log(authClient.token); - -// initialize a client -const client = new Client(authClient); - -const result = await client.accountBalance(); -``` - -#### Initialize a client from existing token details - -```js -const token = loadTokenForUser(); // {access_token: string, refresh_token: string, expires_at: number} -const authClient = new auth.OAuth2User({ - client_id: process.env.CLIENT_ID, - callback: "http://localhost:8080/callback", - scopes: [ - "invoices:read", - "account:read", - "balance:read", - "invoices:create", - "invoices:read", - "payments:send", - ], - token: token, -}); - -const client = new Client(authClient); -// the authClient will automatically refresh the access token if expired using the refresh token -const result = await client.createInvoice({ amount: 1000 }); -``` - -#### Handling refresh token - -Access tokens do expire. If an access token is about to expire, this library will automatically use a refresh token to retrieve a fresh one. Utilising the _tokenRefreshed_ event is a simple approach to guarantee that you always save the most recent tokens. - -If token refresh fails, you can restart the OAuth Authentication flow or log the error by listening for the _tokenRefreshFailed_ event. - -(Note: To prevent losing access to the user's token, only initialize one instance of the client per token pair at a time) - -```js -const token = loadTokenForUser(); // {access_token: string, refresh_token: string, expires_at: number} -const authClient = new auth.OAuth2User({ - client_id: process.env.CLIENT_ID, - callback: "http://localhost:8080/callback", - scopes: [ - "invoices:read", - "account:read", - "balance:read", - "invoices:create", - "invoices:read", - "payments:send", - ], - token: token, -}); - -// listen to the tokenRefreshed event -authClient.on("tokenRefreshed", (tokens) => { - // store the tokens in database - console.log(tokens); -}); - -// Listen to the tokenRefreshFailed event -authClient.on("tokenRefreshFailed", (error) => { - // Handle the token refresh failure, for example, log the error or launch OAuth authentication flow - console.error("Token refresh failed:", error.message); -}); -``` - -#### Sending payments - -```js -const token = loadTokenForUser(); // {access_token: string, refresh_token: string, expires_at: number} -const authClient = new auth.OAuth2User({ - client_id: process.env.CLIENT_ID, - callback: "http://localhost:8080/callback", - scopes: [ - "invoices:read", - "account:read", - "balance:read", - "invoices:create", - "invoices:read", - "payments:send", - ], - token: token, -}); - -const client = new Client(authClient); -// the authClient will automatically refresh the access token if expired using the refresh token - -await client.sendPayment({ invoice: bolt11 }); - -await client.keysend({ - destination: nodekey, - amount: 10, - memo: memo, -}); -``` - -#### Send a boostagram - -refer also to the boostagram spec: https://github.com/lightning/blips/blob/master/blip-0010.md - -```js -const token = loadTokenForUser(); // {access_token: string, refresh_token: string, expires_at: number} -const authClient = new auth.OAuth2User({ - client_id: process.env.CLIENT_ID, - callback: "http://localhost:8080/callback", - scopes: ["payments:send"], - token: token, -}); - -const client = new Client(authClient); -// the authClient will automatically refresh the access token if expired using the refresh token - -// pass in an array if you want to send multiple boostagrams with one call -await client.sendBoostagram({ - recipient: { - address: - "030a58b8653d32b99200a2334cfe913e51dc7d155aa0116c176657a4f1722677a3", - customKey: "696969", - customValue: "bNVHj0WZ0aLPPAesnn9M", - }, - amount: 10, - // spec: https://github.com/lightning/blips/blob/master/blip-0010.md - boostagram: { - app_name: "Alby SDK Demo", - value_msat_total: 49960, // TOTAL Number of millisats for the payment (all splits together, before fees. The actual number someone entered in their player, for numerology purposes.) - value_msat: 2121, // Number of millisats for this split payment - url: "https://feeds.buzzsprout.com/xxx.rss", - podcast: "Podcast title", - action: "boost", - episode: "The episode title", - episode_guid: "Buzzsprout-xxx", - ts: 574, - name: "Podcaster - the recipient name", - sender_name: "Satoshi - the sender/listener name", - }, -}); - -// or manually through the keysend: - -// pass in an array if you want to do multiple keysend payments with one call -await client.keysend({ - destination: nodekey, - amount: 10, - customRecords: { - 7629169: JSON.stringify(boostagram), - 696969: "user", - }, -}); -``` - -#### Send multiple boostagrams - -You often want to send a boostagram for multiple splits. You can do this with one API call. Simply pass in an array of boostagrams. See example above. - -```js -const response = await client.sendBoostagram([ - boostagram1, - boostagram2, - boostagram3, -]); - -console.log(response.keysends); -``` - -`response.keysends` is an array of objects that either has an `error` key if a payment faild or the `keysend` key if everything succeeded. - -```json -{ - "keysends": [ - { - "keysend": { - "amount": 10, - "fee": 0, - "destination": "xx", - "payment_preimage": "xx", - "payment_hash": "xx" - } - }, - { - "keysend": { - "amount": 10, - "fee": 0, - "destination": "xxx", - "payment_preimage": "xxx", - "payment_hash": "xxx" - } - } - ] -} -``` - -#### Decoding an invoice - -For quick invoice decoding without an API request please see Alby's [Lightning Tools package](https://github.com/getAlby/js-lightning-tools#basic-invoice-decoding). - -For more invoice details you can use the Alby Wallet API: - -```js -const decodedInvoice = await client.decodeInvoice(paymentRequest); -const {payment_hash, amount, description, ...} = decodedInvoice; -``` - -## fetch() dependency - -This library relies on a global `fetch()` function which will only work in browsers and node v18.x or newer. In older versions you can manually install a global fetch option or polyfill if needed. +## Nostr Wallet Connect Documentation -For example: +[Nostr Wallet Connect](https://nwc.dev) is an open protocol enabling applications to interact with bitcoin lightning wallets. It allows users to connect their existing wallets to your application allowing developers to easily integrate bitcoin lightning functionality. -```js -import fetch from "cross-fetch"; // or "@inrupt/universal-fetch" -globalThis.fetch = fetch; +[Read more](./docs/nwc.md) -// or as a polyfill: -import "cross-fetch/polyfill"; -``` +## Alby Wallet API Documentation -## Full usage examples +The [Alby OAuth API](https://guides.getalby.com/alby-wallet-api/reference/getting-started) allows you to integrate bitcoin lightning functionality provided by the Alby Wallet into your applications, with the Alby Wallet API. Send & receive payments, create invoices, setup payment webhooks, access Podcasting 2.0 and more! -You can find examples in the [examples/](examples/) directory. +[Read more](./docs/oauth.md) ## Need help? diff --git a/docs/ln.md b/docs/ln.md new file mode 100644 index 0000000..17e1d73 --- /dev/null +++ b/docs/ln.md @@ -0,0 +1,21 @@ +# Lightning Network Client (LNClient) Documentation + +The LNClient is a high level wrapper around the [NWCClient](./nwc.md) which helps you easily get started interacting with the lightning network. + +For example, to make a payment: + +```js +await new LNClient(credentials).pay(invoice); +``` + +Or to request a payment to be received: + +```js +const request = await new LNClient(credentials).receive(USD(1.0)); +// give request.invoice to someone... +request.onPaid(giveAccess); +``` + +## Examples + +See [the LNClient examples directory](./examples/ln) for a full list of examples. diff --git a/docs/nwc.md b/docs/nwc.md new file mode 100644 index 0000000..6ac526d --- /dev/null +++ b/docs/nwc.md @@ -0,0 +1,228 @@ +# Nostr Wallet Connect Documentation + +[Nostr Wallet Connect](https://nwc.dev) is an open protocol enabling applications to interact with bitcoin lightning wallets. It allows users to connect their existing wallets to your application allowing developers to easily integrate bitcoin lightning functionality. + +The Alby JS SDK allows you to easily integrate Nostr Wallet Connect into any JavaScript based application. + +There are two interfaces you can use to access NWC: + +- The `NWCClient` exposes the [NWC](https://nwc.dev/) interface directly, which is more powerful than the WebLN interface and is recommended if you plan to create an application outside of the web (e.g. native mobile/command line/server backend etc.). You can explore all the examples [here](../examples/nwc/client/). +- The `NostrWebLNProvider` exposes the [WebLN](https://webln.guide/) interface to execute lightning wallet functionality through Nostr Wallet Connect, such as sending payments, making invoices and getting the node balance. You can explore all the examples [here](../examples/nwc/). See also [Bitcoin Connect](https://github.com/getAlby/bitcoin-connect/) if you are developing a frontend web application. + +## NWCClient + +### Initialization Options + +- `nostrWalletConnectUrl`: full Nostr Wallet Connect URL as defined by the [spec](https://github.com/getAlby/nips/blob/master/47.md) +- `relayUrl`: URL of the Nostr relay to be used (e.g. wss://nostr-relay.getalby.com) +- `walletPubkey`: pubkey of the Nostr Wallet Connect app +- `secret`: secret key to sign the request event (if not available window.nostr will be used) + +### NWCClient Quick start example + +```js +import { nwc } from "@getalby/sdk"; +const nwcClient = new nwc.NWCClient({ + nostrWalletConnectUrl: loadNWCUrl(), +}); // loadNWCUrl is some function to get the NWC URL from some (encrypted) storage + +// now you can send payments by passing in the invoice in an object +const response = await nwcClient.payInvoice({ invoice }); +``` + +### `static fromAuthorizationUrl()` + +Initialized a new `NWCClient` instance but generates a new random secret. The pubkey of that secret then needs to be authorized by the user (this can be initiated by redirecting the user to the `getAuthorizationUrl()` URL or calling `fromAuthorizationUrl()` to open an authorization popup. + +```js +const nwcClient = await nwc.NWCClient.fromAuthorizationUrl( + "https://my.albyhub.com/apps/new", + { + name: "My app name", + }, +); +``` + +The same options can be provided to getAuthorizationUrl() as fromAuthorizationUrl() - see [Manual Auth example](../examples/nwc/client/auth_manual.html) + +### Examples + +See [the NWC client examples directory](../examples/nwc/client) for a full list of examples. + +## NostrWebLNProvider (aliased as NWC) Options + +- `nostrWalletConnectUrl`: full Nostr Wallet Connect URL as defined by the [spec](https://github.com/getAlby/nips/blob/master/47.md) +- `relayUrl`: URL of the Nostr relay to be used (e.g. wss://nostr-relay.getalby.com) +- `walletPubkey`: pubkey of the Nostr Wallet Connect app +- `secret`: secret key to sign the request event (if not available window.nostr will be used) +- `client`: initialize using an existing NWC client + +### WebLN Quick start example + +```js +import { webln } from "@getalby/sdk"; +const nwc = new webln.NostrWebLNProvider({ + nostrWalletConnectUrl: loadNWCUrl(), +}); // loadNWCUrl is some function to get the NWC URL from some (encrypted) storage +// or use the short version +const nwc = new webln.NWC({ nostrWalletConnectUrl: loadNWCUrl }); + +// connect to the relay +await nwc.enable(); + +// now you can send payments by passing in the invoice +const response = await nwc.sendPayment(invoice); +``` + +You can use NWC as a webln compatible object in your web app: + +```js +// you can set the window.webln object to use the universal API to send payments: +if (!window.webln) { + // prompt the user to connect to NWC + window.webln = new webln.NostrWebLNProvider({ + nostrWalletConnectUrl: loadNWCUrl, + }); + // now use any webln code +} +``` + +## NostrWebLNProvider Functions + +The goal of the Nostr Wallet Connect provider is to be API compatible with [webln](https://www.webln.guide/). Currently not all methods are supported - see the examples/nwc directory for a list of supported methods. + +### sendPayment(invice: string) + +Takes a bolt11 invoice and calls the NWC `pay_invoice` function. +It returns a promise object that is resolved with an object with the preimage or is rejected with an error + +#### Payment Example + +```js +import { webln } from "@getalby/sdk"; +const nwc = new webln.NostrWebLNProvider({ nostrWalletConnectUrl: loadNWCUrl }); +await nwc.enable(); +const response = await nwc.sendPayment(invoice); +console.log(response); +``` + +#### getNostrWalletConnectUrl() + +Returns the `nostr+walletconnect://` URL which includes all the connection information (`walletPubkey`, `relayUrl`, `secret`) +This can be used to get and persist the string for later use. + +#### fromAuthorizationUrl(url: string, {name: string}) + +Opens a new window prompt with at the provided authorization URL to ask the user to authorize the app connection. +The promise resolves when the connection is authorized and the popup sends a `nwc:success` message or rejects when the prompt is closed. +Pass a `name` to the NWC provider describing the application. + +```js +import { webln } from "@getalby/sdk"; + +try { + const nwc = await webln.NostrWebLNProvider.fromAuthorizationUrl( + "https://my.albyhub.com/apps/new", + { + name: "My app name", + }, + ); +} catch (e) { + console.error(e); +} +await nwc.enable(); +let response; +try { + response = await nwc.sendPayment(invoice); + // if success then the response.preimage will be only + console.info(`payment successful, the preimage is ${response.preimage}`); +} catch (e) { + console.error(e.error || e); +} +``` + +#### React Native (Expo) + +Look at our [NWC React Native Expo Demo app](https://github.com/getAlby/nwc-react-native-expo) for how to use NWC in a React Native expo project. + +#### For Node.js + +To use this on Node.js you first must install `websocket-polyfill` and import it: + +```js +import "websocket-polyfill"; +// or: require('websocket-polyfill'); +``` + +if you get an `crypto is not defined` error, either upgrade to node.js 20 or above, or import it manually: + +```js +import * as crypto from 'crypto'; // or 'node:crypto' +globalThis.crypto = crypto as any; +//or: global.crypto = require('crypto'); +``` + +### Examples + +#### Defaults + +```js +import { webln } from "@getalby/sdk"; + +const nwc = new webln.NostrWebLNProvider(); // use defaults (connects to Alby's relay, will use window.nostr to sign the request) +await nwc.enable(); // connect to the relay +const response = await nwc.sendPayment(invoice); +console.log(response.preimage); + +nwc.close(); // close the websocket connection +``` + +#### Use a custom, user provided Nostr Wallet Connect URL + +```js +import { webln } from "@getalby/sdk"; + +const nwc = new webln.NostrWebLNProvider({ + nostrWalletConnectUrl: + "nostr+walletconnect://69effe7b49a6dd5cf525bd0905917a5005ffe480b58eeb8e861418cf3ae760d9?relay=wss://nostr.bitcoiner.social&secret=c60320b3ecb6c15557510d1518ef41194e9f9337c82621ddef3f979f668bfebd", +}); // use defaults +await nwc.enable(); // connect to the relay +const response = await nwc.sendPayment(invoice); +console.log(response.preimage); + +nwc.close(); // close the websocket connection +``` + +#### Generate a new NWC connect url using a locally-generated secret + +```js +// use the `fromAuthorizationUrl` helper which opens a popup to initiate the connection flow. +// the promise resolves once the NWC app returned. +const nwc = await webln.NostrWebLNProvider.fromAuthorizationUrl( + "https://my.albyhub.com/apps/new", + { + name: "My app name", + }, +); + +// ... enable and send a payment + +// if you want to get the connect url with the secret: +// const nostrWalletConnectUrl nwc.getNostrWalletConnectUrl(true) +``` + +The same options can be provided to getAuthorizationUrl() as fromAuthorizationUrl() - see [Manual Auth example](../examples/nwc/auth_manual.html) + +### Nostr Wallet Auth + +NWA is an alternative flow for lightning apps to easily initialize an NWC connection to mobile-first or self-custodial wallets, using a client-created secret. + +The app will generate an NWA URI which should be opened in the wallet, where the user can approve the connection. + +#### Generating an NWA URI + +See [NWA example](examples/nwc/client/nwa.js) + +### Accepting and creating a connection from an NWA URI + +See [NWA accept example](examples/nwc/client/nwa.js) for NWA URI parsing and handling. The implementation of actually creating the connection and showing a confirmation page to the user is wallet-specific. In the example, a connection will be created via the `create_connection` NWC command. diff --git a/docs/oauth.md b/docs/oauth.md new file mode 100644 index 0000000..3d3c3f0 --- /dev/null +++ b/docs/oauth.md @@ -0,0 +1,280 @@ +# OAuth API Documentation + +Please have a look a the Alby OAuth2 Wallet API: + +[https://guides.getalby.com/alby-wallet-api/reference/getting-started](https://guides.getalby.com/alby-wallet-api/reference/getting-started) + +## Avalilable methods + +- accountBalance +- accountSummary +- signMessage +- accountInformation +- accountValue4Value +- invoices +- incomingInvoices +- outgoingInvoices +- getInvoice +- createInvoice +- decodeInvoice +- keysend +- sendPayment +- sendBoostagram +- sendBoostagramToAlbyAccount +- createWebhookEndpoint +- deleteWebhookEndpoint + +### Examples + +#### Full OAuth Authentication flow + +```js +const authClient = new auth.OAuth2User({ + client_id: process.env.CLIENT_ID, + client_secret: process.env.CLIENT_SECRET, + callback: "http://localhost:8080/callback", + scopes: [ + "invoices:read", + "account:read", + "balance:read", + "invoices:create", + "invoices:read", + "payments:send", + ], + token: { + access_token: undefined, + refresh_token: undefined, + expires_at: undefined, + }, // initialize with existing token +}); + +const authUrl = await authClient.generateAuthURL({ + code_challenge_method: "S256", + // authorizeUrl: "https://getalby.com/oauth" endpoint for authorization (replace with the appropriate URL based on the environment) +}); +// open auth URL +// `code` is passed as a query parameter when the user is redirected back after authorization +await authClient.requestAccessToken(code); + +// access the token response. You can store this securely for future client initializations +console.log(authClient.token); + +// initialize a client +const client = new Client(authClient); + +const result = await client.accountBalance(); +``` + +#### Initialize a client from existing token details + +```js +const token = loadTokenForUser(); // {access_token: string, refresh_token: string, expires_at: number} +const authClient = new auth.OAuth2User({ + client_id: process.env.CLIENT_ID, + callback: "http://localhost:8080/callback", + scopes: [ + "invoices:read", + "account:read", + "balance:read", + "invoices:create", + "invoices:read", + "payments:send", + ], + token: token, +}); + +const client = new Client(authClient); +// the authClient will automatically refresh the access token if expired using the refresh token +const result = await client.createInvoice({ amount: 1000 }); +``` + +#### Handling refresh token + +Access tokens do expire. If an access token is about to expire, this library will automatically use a refresh token to retrieve a fresh one. Utilising the _tokenRefreshed_ event is a simple approach to guarantee that you always save the most recent tokens. + +If token refresh fails, you can restart the OAuth Authentication flow or log the error by listening for the _tokenRefreshFailed_ event. + +(Note: To prevent losing access to the user's token, only initialize one instance of the client per token pair at a time) + +```js +const token = loadTokenForUser(); // {access_token: string, refresh_token: string, expires_at: number} +const authClient = new auth.OAuth2User({ + client_id: process.env.CLIENT_ID, + callback: "http://localhost:8080/callback", + scopes: [ + "invoices:read", + "account:read", + "balance:read", + "invoices:create", + "invoices:read", + "payments:send", + ], + token: token, +}); + +// listen to the tokenRefreshed event +authClient.on("tokenRefreshed", (tokens) => { + // store the tokens in database + console.log(tokens); +}); + +// Listen to the tokenRefreshFailed event +authClient.on("tokenRefreshFailed", (error) => { + // Handle the token refresh failure, for example, log the error or launch OAuth authentication flow + console.error("Token refresh failed:", error.message); +}); +``` + +#### Sending payments + +```js +const token = loadTokenForUser(); // {access_token: string, refresh_token: string, expires_at: number} +const authClient = new auth.OAuth2User({ + client_id: process.env.CLIENT_ID, + callback: "http://localhost:8080/callback", + scopes: [ + "invoices:read", + "account:read", + "balance:read", + "invoices:create", + "invoices:read", + "payments:send", + ], + token: token, +}); + +const client = new Client(authClient); +// the authClient will automatically refresh the access token if expired using the refresh token + +await client.sendPayment({ invoice: bolt11 }); + +await client.keysend({ + destination: nodekey, + amount: 10, + memo: memo, +}); +``` + +#### Send a boostagram + +refer also to the boostagram spec: [BLIP-10](https://github.com/lightning/blips/blob/master/blip-0010.md) + +```js +const token = loadTokenForUser(); // {access_token: string, refresh_token: string, expires_at: number} +const authClient = new auth.OAuth2User({ + client_id: process.env.CLIENT_ID, + callback: "http://localhost:8080/callback", + scopes: ["payments:send"], + token: token, +}); + +const client = new Client(authClient); +// the authClient will automatically refresh the access token if expired using the refresh token + +// pass in an array if you want to send multiple boostagrams with one call +await client.sendBoostagram({ + recipient: { + address: + "030a58b8653d32b99200a2334cfe913e51dc7d155aa0116c176657a4f1722677a3", + customKey: "696969", + customValue: "bNVHj0WZ0aLPPAesnn9M", + }, + amount: 10, + // spec: https://github.com/lightning/blips/blob/master/blip-0010.md + boostagram: { + app_name: "Alby SDK Demo", + value_msat_total: 49960, // TOTAL Number of millisats for the payment (all splits together, before fees. The actual number someone entered in their player, for numerology purposes.) + value_msat: 2121, // Number of millisats for this split payment + url: "https://feeds.buzzsprout.com/xxx.rss", + podcast: "Podcast title", + action: "boost", + episode: "The episode title", + episode_guid: "Buzzsprout-xxx", + ts: 574, + name: "Podcaster - the recipient name", + sender_name: "Satoshi - the sender/listener name", + }, +}); + +// or manually through the keysend: + +// pass in an array if you want to do multiple keysend payments with one call +await client.keysend({ + destination: nodekey, + amount: 10, + customRecords: { + 7629169: JSON.stringify(boostagram), + 696969: "user", + }, +}); +``` + +#### Send multiple boostagrams + +You often want to send a boostagram for multiple splits. You can do this with one API call. Simply pass in an array of boostagrams. See example above. + +```js +const response = await client.sendBoostagram([ + boostagram1, + boostagram2, + boostagram3, +]); + +console.log(response.keysends); +``` + +`response.keysends` is an array of objects that either has an `error` key if a payment faild or the `keysend` key if everything succeeded. + +```json +{ + "keysends": [ + { + "keysend": { + "amount": 10, + "fee": 0, + "destination": "xx", + "payment_preimage": "xx", + "payment_hash": "xx" + } + }, + { + "keysend": { + "amount": 10, + "fee": 0, + "destination": "xxx", + "payment_preimage": "xxx", + "payment_hash": "xxx" + } + } + ] +} +``` + +#### Decoding an invoice + +For quick invoice decoding without an API request please see Alby's [Lightning Tools package](https://github.com/getAlby/js-lightning-tools#basic-invoice-decoding). + +For more invoice details you can use the Alby Wallet API: + +```js +const decodedInvoice = await client.decodeInvoice(paymentRequest); +const {payment_hash, amount, description, ...} = decodedInvoice; +``` + +## fetch() dependency + +This library relies on a global `fetch()` function which will only work in browsers and node v18.x or newer. In older versions you can manually install a global fetch option or polyfill if needed. + +For example: + +```js +import fetch from "cross-fetch"; // or "@inrupt/universal-fetch" +globalThis.fetch = fetch; + +// or as a polyfill: +import "cross-fetch/polyfill"; +``` + +## Full usage examples + +You can find examples in the [examples/](../examples/oauth/) directory. diff --git a/examples/ln/paywall.js b/examples/ln/paywall.js new file mode 100644 index 0000000..7e82ed1 --- /dev/null +++ b/examples/ln/paywall.js @@ -0,0 +1,34 @@ +import "websocket-polyfill"; // required in node.js +import qrcode from "qrcode-terminal"; + +import * as readline from "node:readline/promises"; +import { stdin as input, stdout as output } from "node:process"; + +import { LNClient, USD } from "../../dist/index.module.js"; + +const rl = readline.createInterface({ input, output }); + +const nwcUrl = + process.env.NWC_URL || + (await rl.question("Nostr Wallet Connect URL (nostr+walletconnect://...): ")); +rl.close(); + +const client = new LNClient(nwcUrl); +const request = await client.receive(USD(1.0)); + +qrcode.generate(request.invoice, { small: true }); +console.info("Waiting for payment..."); + +const unsub = await request.onPaid(() => { + console.info("received payment!"); + client.close(); +}); + +process.on("SIGINT", function () { + console.info("Caught interrupt signal"); + + unsub(); + client.close(); + + process.exit(); +}); diff --git a/examples/AlbyOauthCallback.jsx b/examples/oauth/AlbyOauthCallback.jsx similarity index 100% rename from examples/AlbyOauthCallback.jsx rename to examples/oauth/AlbyOauthCallback.jsx diff --git a/examples/boostagram.js b/examples/oauth/boostagram.js similarity index 97% rename from examples/boostagram.js rename to examples/oauth/boostagram.js index 1ce3712..1b2746d 100644 --- a/examples/boostagram.js +++ b/examples/oauth/boostagram.js @@ -1,7 +1,7 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../dist/index.module.js"; +import { auth, Client } from "../../dist/index.module.js"; const rl = readline.createInterface({ input, output }); diff --git a/examples/decode-invoice.js b/examples/oauth/decode-invoice.js similarity index 95% rename from examples/decode-invoice.js rename to examples/oauth/decode-invoice.js index 2109d51..0f94571 100644 --- a/examples/decode-invoice.js +++ b/examples/oauth/decode-invoice.js @@ -1,6 +1,6 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../dist/index.module.js"; +import { auth, Client } from "../../dist/index.module.js"; const rl = readline.createInterface({ input, output }); diff --git a/examples/invoices.js b/examples/oauth/invoices.js similarity index 94% rename from examples/invoices.js rename to examples/oauth/invoices.js index 0e4c25c..43e36f1 100644 --- a/examples/invoices.js +++ b/examples/oauth/invoices.js @@ -1,7 +1,7 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../dist/index.module.js"; +import { auth, Client } from "../../dist/index.module.js"; const rl = readline.createInterface({ input, output }); diff --git a/examples/keysends.js b/examples/oauth/keysends.js similarity index 95% rename from examples/keysends.js rename to examples/oauth/keysends.js index fd76078..29fe9d6 100644 --- a/examples/keysends.js +++ b/examples/oauth/keysends.js @@ -1,7 +1,7 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../dist/index.module.js"; +import { auth, Client } from "../../dist/index.module.js"; const rl = readline.createInterface({ input, output }); diff --git a/examples/oauth2-public-callback_pkce_s256.mjs b/examples/oauth/oauth2-public-callback_pkce_s256.mjs similarity index 97% rename from examples/oauth2-public-callback_pkce_s256.mjs rename to examples/oauth/oauth2-public-callback_pkce_s256.mjs index f91c941..5a58b8a 100644 --- a/examples/oauth2-public-callback_pkce_s256.mjs +++ b/examples/oauth/oauth2-public-callback_pkce_s256.mjs @@ -1,4 +1,4 @@ -import { auth, Client } from "../dist/index.module.js"; +import { auth, Client } from "../../dist/index.module.js"; import express from "express"; if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) { diff --git a/examples/send-to-ln-address.js b/examples/oauth/send-to-ln-address.js similarity index 95% rename from examples/send-to-ln-address.js rename to examples/oauth/send-to-ln-address.js index 307b46e..30660db 100644 --- a/examples/send-to-ln-address.js +++ b/examples/oauth/send-to-ln-address.js @@ -1,7 +1,7 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../dist/index.module.js"; +import { auth, Client } from "../../dist/index.module.js"; import { LightningAddress } from "alby-tools"; const rl = readline.createInterface({ input, output }); diff --git a/examples/webhooks.js b/examples/oauth/webhooks.js similarity index 95% rename from examples/webhooks.js rename to examples/oauth/webhooks.js index a2cf29a..54495fa 100644 --- a/examples/webhooks.js +++ b/examples/oauth/webhooks.js @@ -1,7 +1,7 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../dist/index.module.js"; +import { auth, Client } from "../../dist/index.module.js"; const rl = readline.createInterface({ input, output }); diff --git a/package.json b/package.json index e66fc05..4b0de72 100644 --- a/package.json +++ b/package.json @@ -38,12 +38,12 @@ "prepare": "husky" }, "dependencies": { - "nostr-tools": "2.9.4" + "nostr-tools": "2.9.4", + "@getalby/lightning-tools": "^5.1.2" }, "devDependencies": { "@commitlint/cli": "^19.4.1", "@commitlint/config-conventional": "^19.4.1", - "@getalby/lightning-tools": "^5.0.1", "@types/jest": "^29.5.5", "@types/node": "^22.13.4", "@typescript-eslint/eslint-plugin": "^7.0.0", diff --git a/src/LNClient.ts b/src/LNClient.ts new file mode 100644 index 0000000..1c74649 --- /dev/null +++ b/src/LNClient.ts @@ -0,0 +1,100 @@ +import { fiat } from "@getalby/lightning-tools"; +import { + NewNWCClientOptions, + Nip47MakeInvoiceRequest, + Nip47Notification, + Nip47PayInvoiceRequest, + Nip47Transaction, + NWCClient, +} from "./NWCClient"; + +type LNClientCredentials = string | NWCClient | NewNWCClientOptions; +type FiatAmount = { amount: number; currency: string }; + +/** + * An amount in satoshis, or an amount in a fiat currency + */ +type Amount = number | FiatAmount; + +// Most popular fiat currencies +export const USD = (amount: number) => + ({ amount, currency: "USD" }) satisfies FiatAmount; +export const EUR = (amount: number) => + ({ amount, currency: "EUR" }) satisfies FiatAmount; +export const JPY = (amount: number) => + ({ amount, currency: "JPY" }) satisfies FiatAmount; +export const GBP = (amount: number) => + ({ amount, currency: "GBP" }) satisfies FiatAmount; +export const CHF = (amount: number) => + ({ amount, currency: "CHF" }) satisfies FiatAmount; + +export class LNClient { + readonly nwcClient: NWCClient; + + constructor(credentials: LNClientCredentials) { + if (typeof credentials === "string") { + this.nwcClient = new NWCClient({ + nostrWalletConnectUrl: credentials, + }); + } else if (credentials instanceof NWCClient) { + this.nwcClient = credentials; + } else { + this.nwcClient = new NWCClient(credentials); + } + } + + pay(invoice: string, args?: Omit) { + return this.nwcClient.payInvoice({ + invoice, + ...(args || {}), + }); + } + + async receive( + amount: Amount, + args?: Omit, + ) { + let parsedAmount = 0; + if (typeof amount === "number") { + parsedAmount = amount; + } else { + parsedAmount = await fiat.getSatoshiValue({ + amount: amount.amount, + currency: amount.currency, + }); + } + + const transaction = await this.nwcClient.makeInvoice({ + amount: parsedAmount * 1000 /* as millisats */, + ...(args || {}), + }); + + return { + transaction, + invoice: transaction.invoice, + onPaid: async (callback: (receivedPayment: Nip47Transaction) => void) => { + let unsubscribeFunc = () => {}; + const onNotification = (notification: Nip47Notification) => { + if ( + notification.notification.payment_hash === transaction.payment_hash + ) { + unsubscribeFunc(); + callback(notification.notification); + } + }; + + unsubscribeFunc = await this.nwcClient.subscribeNotifications( + onNotification, + ["payment_received"], + ); + return unsubscribeFunc; + }, + }; + } + + close() { + this.nwcClient.close(); + } + + // TODO: proxy everything from NWCClient +} diff --git a/src/index.ts b/src/index.ts index b46e97b..271841a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,3 +4,4 @@ export * as webln from "./webln"; export { Client } from "./client"; export * as nwc from "./NWCClient"; export * as nwa from "./NWAClient"; +export * from "./LNClient"; diff --git a/yarn.lock b/yarn.lock index 41a330e..43f7760 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1401,7 +1401,7 @@ resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== -"@getalby/lightning-tools@^5.0.1": +"@getalby/lightning-tools@^5.1.2": version "5.1.2" resolved "https://registry.yarnpkg.com/@getalby/lightning-tools/-/lightning-tools-5.1.2.tgz#8a018e98d5c13097dd98d93192cf5e4e455f4c20" integrity sha512-BwGm8eGbPh59BVa1gI5yJMantBl/Fdps6X4p1ZACnmxz9vDINX8/3aFoOnDlF7yyA2boXWCsReVQSr26Q2yjiQ== From cc427c24debc988a5660fee83b3a500181403af0 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 20 Mar 2025 21:15:48 +0700 Subject: [PATCH 02/29] chore: make sat units clear, add pay to ln address, fix args order --- README.md | 3 +- docs/ln.md | 5 ++-- examples/ln/pay_ln_address.js | 18 ++++++++++++ src/LNClient.ts | 55 +++++++++++++++++++++++++---------- 4 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 examples/ln/pay_ln_address.js diff --git a/README.md b/README.md index 2ba1567..58d62dd 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ Quickly get started adding lightning payments to your app. For example, to make a payment: ```js -await new LNClient(credentials).pay(invoice); +await new LNClient(credentials).pay("lnbc..."); // pay a lightning invoice +await new LNClient(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address ``` Or to request a payment to be received: diff --git a/docs/ln.md b/docs/ln.md index 17e1d73..085a1c2 100644 --- a/docs/ln.md +++ b/docs/ln.md @@ -1,11 +1,12 @@ # Lightning Network Client (LNClient) Documentation -The LNClient is a high level wrapper around the [NWCClient](./nwc.md) which helps you easily get started interacting with the lightning network. +The LNClient helps you easily get started interacting with the lightning network. It is a high level wrapper around the [NWCClient](./nwc.md) which is compatible with many different lightning wallets. For example, to make a payment: ```js -await new LNClient(credentials).pay(invoice); +await new LNClient(credentials).pay("lnbc..."); // pay a lightning invoice +await new LNClient(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address ``` Or to request a payment to be received: diff --git a/examples/ln/pay_ln_address.js b/examples/ln/pay_ln_address.js new file mode 100644 index 0000000..38452cc --- /dev/null +++ b/examples/ln/pay_ln_address.js @@ -0,0 +1,18 @@ +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 { LNClient, USD } from "../../dist/index.module.js"; + +const rl = readline.createInterface({ input, output }); + +const nwcUrl = + process.env.NWC_URL || + (await rl.question("Nostr Wallet Connect URL (nostr+walletconnect://...): ")); +rl.close(); + +const client = new LNClient(nwcUrl); +const response = await client.pay("rolznz@getalby.com", USD(1.0)); +console.info("Paid successfully", response); +client.close(); diff --git a/src/LNClient.ts b/src/LNClient.ts index 1c74649..9ab48ff 100644 --- a/src/LNClient.ts +++ b/src/LNClient.ts @@ -1,4 +1,4 @@ -import { fiat } from "@getalby/lightning-tools"; +import { fiat, LightningAddress } from "@getalby/lightning-tools"; import { NewNWCClientOptions, Nip47MakeInvoiceRequest, @@ -14,7 +14,7 @@ type FiatAmount = { amount: number; currency: string }; /** * An amount in satoshis, or an amount in a fiat currency */ -type Amount = number | FiatAmount; +type Amount = { satoshi: number } | FiatAmount; // Most popular fiat currencies export const USD = (amount: number) => @@ -43,10 +43,31 @@ export class LNClient { } } - pay(invoice: string, args?: Omit) { + async pay( + recipient: string, + amount?: Amount, + args?: Omit, + ) { + let invoice = recipient; + const parsedAmount = amount ? await parseAmount(amount) : undefined; + if (invoice.indexOf("@") > -1) { + if (!parsedAmount) { + throw new Error( + "Amount must be provided when paying to a lightning address", + ); + } + const ln = new LightningAddress(recipient); + await ln.fetch(); + const invoiceObj = await ln.requestInvoice({ + satoshi: parsedAmount.satoshi, + }); + invoice = invoiceObj.paymentRequest; + } + return this.nwcClient.payInvoice({ - invoice, ...(args || {}), + invoice, + amount: parsedAmount?.millisat, }); } @@ -54,19 +75,10 @@ export class LNClient { amount: Amount, args?: Omit, ) { - let parsedAmount = 0; - if (typeof amount === "number") { - parsedAmount = amount; - } else { - parsedAmount = await fiat.getSatoshiValue({ - amount: amount.amount, - currency: amount.currency, - }); - } - + const parsedAmount = await parseAmount(amount); const transaction = await this.nwcClient.makeInvoice({ - amount: parsedAmount * 1000 /* as millisats */, ...(args || {}), + amount: parsedAmount.millisat, }); return { @@ -98,3 +110,16 @@ export class LNClient { // TODO: proxy everything from NWCClient } + +async function parseAmount(amount: Amount) { + let parsedAmount = 0; + if ("satoshi" in amount) { + parsedAmount = amount.satoshi; + } else { + parsedAmount = await fiat.getSatoshiValue({ + amount: amount.amount, + currency: amount.currency, + }); + } + return { satoshi: parsedAmount, millisat: parsedAmount * 1000 }; +} From 8b5f10b0249d3d543d3a091d5ac577f3393c4374 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 28 Mar 2025 12:43:19 +0700 Subject: [PATCH 03/29] chore: add LN alias --- README.md | 9 +++++---- docs/ln.md | 11 ++++++----- examples/ln/pay_ln_address.js | 4 ++-- examples/ln/paywall.js | 4 ++-- src/LNClient.ts | 2 ++ 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 58d62dd..f4d1ba0 100644 --- a/README.md +++ b/README.md @@ -30,21 +30,22 @@ or for use without any build tools: **This library relies on a global fetch() function which will work in browsers and node v18.x or newer.** (In older versions you have to use a polyfill.) -## Lightning Network Client Documentation +## Lightning Network Client (LN) Documentation Quickly get started adding lightning payments to your app. For example, to make a payment: ```js -await new LNClient(credentials).pay("lnbc..."); // pay a lightning invoice -await new LNClient(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address +import { LN, USD } from "@getalby/sdk"; +await new LN(credentials).pay("lnbc..."); // pay a lightning invoice +await new LN(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address ``` Or to request a payment to be received: ```js -const request = await new LNClient(credentials).receive(USD(1.0)); +const request = await new LN(credentials).receive(USD(1.0)); // give request.invoice to someone... request.onPaid(giveAccess); ``` diff --git a/docs/ln.md b/docs/ln.md index 085a1c2..c2b06cd 100644 --- a/docs/ln.md +++ b/docs/ln.md @@ -1,18 +1,19 @@ -# Lightning Network Client (LNClient) Documentation +# Lightning Network Client (LN) Documentation -The LNClient helps you easily get started interacting with the lightning network. It is a high level wrapper around the [NWCClient](./nwc.md) which is compatible with many different lightning wallets. +The LN class helps you easily get started interacting with the lightning network. It is a high level wrapper around the [NWCClient](./nwc.md) which is compatible with many different lightning wallets. For example, to make a payment: ```js -await new LNClient(credentials).pay("lnbc..."); // pay a lightning invoice -await new LNClient(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address +import { LN, USD } from "@getalby/sdk"; +await new LN(credentials).pay("lnbc..."); // pay a lightning invoice +await new LN(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address ``` Or to request a payment to be received: ```js -const request = await new LNClient(credentials).receive(USD(1.0)); +const request = await new LN(credentials).receive(USD(1.0)); // give request.invoice to someone... request.onPaid(giveAccess); ``` diff --git a/examples/ln/pay_ln_address.js b/examples/ln/pay_ln_address.js index 38452cc..19d4485 100644 --- a/examples/ln/pay_ln_address.js +++ b/examples/ln/pay_ln_address.js @@ -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 { LNClient, USD } from "../../dist/index.module.js"; +import { LN, USD } from "../../dist/index.module.js"; const rl = readline.createInterface({ input, output }); @@ -12,7 +12,7 @@ const nwcUrl = (await rl.question("Nostr Wallet Connect URL (nostr+walletconnect://...): ")); rl.close(); -const client = new LNClient(nwcUrl); +const client = new LN(nwcUrl); const response = await client.pay("rolznz@getalby.com", USD(1.0)); console.info("Paid successfully", response); client.close(); diff --git a/examples/ln/paywall.js b/examples/ln/paywall.js index 7e82ed1..0acfc18 100644 --- a/examples/ln/paywall.js +++ b/examples/ln/paywall.js @@ -4,7 +4,7 @@ import qrcode from "qrcode-terminal"; import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { LNClient, USD } from "../../dist/index.module.js"; +import { LN, USD } from "../../dist/index.module.js"; const rl = readline.createInterface({ input, output }); @@ -13,7 +13,7 @@ const nwcUrl = (await rl.question("Nostr Wallet Connect URL (nostr+walletconnect://...): ")); rl.close(); -const client = new LNClient(nwcUrl); +const client = new LN(nwcUrl); const request = await client.receive(USD(1.0)); qrcode.generate(request.invoice, { small: true }); diff --git a/src/LNClient.ts b/src/LNClient.ts index 9ab48ff..3d04f3a 100644 --- a/src/LNClient.ts +++ b/src/LNClient.ts @@ -28,6 +28,8 @@ export const GBP = (amount: number) => export const CHF = (amount: number) => ({ amount, currency: "CHF" }) satisfies FiatAmount; +export type LN = LNClient; + export class LNClient { readonly nwcClient: NWCClient; From 11a9f8690a34a57b7ac5a467465e9257ea2ff1e7 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 1 Apr 2025 17:09:36 +0700 Subject: [PATCH 04/29] docs: add wallet service documentation, fix example relay urls --- README.md | 4 ++- docs/nwc-wallet-service.md | 52 ++++++++++++++++++++++++++++++++++++++ docs/nwc.md | 4 +-- 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 docs/nwc-wallet-service.md diff --git a/README.md b/README.md index f4d1ba0..efc3c09 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,9 @@ request.onPaid(giveAccess); [Nostr Wallet Connect](https://nwc.dev) is an open protocol enabling applications to interact with bitcoin lightning wallets. It allows users to connect their existing wallets to your application allowing developers to easily integrate bitcoin lightning functionality. -[Read more](./docs/nwc.md) +For apps, see [NWC client documentation](./docs/nwc.md) + +For wallet services, see [NWC wallet service documentation](./docs/nwc-wallet-service.md) ## Alby Wallet API Documentation diff --git a/docs/nwc-wallet-service.md b/docs/nwc-wallet-service.md new file mode 100644 index 0000000..03576f0 --- /dev/null +++ b/docs/nwc-wallet-service.md @@ -0,0 +1,52 @@ +# Nostr Wallet Connect - Wallet Service Documentation + +[Nostr Wallet Connect](https://nwc.dev) is an open protocol enabling applications to interact with bitcoin lightning wallets. It allows users to connect apps they use to your wallet service, allowing app developers to easily integrate bitcoin lightning functionality. + +The Alby JS SDK allows you to easily integrate Nostr Wallet Connect into any JavaScript based lightning wallet to allow client applications to easily connect and seamlessly interact with the wallet. + +## NWCWalletService + +### Initialization Options + +- `relayUrl`: URL of the Nostr relay to be used (e.g. wss://relay.getalby.com/v1) + +### NWCWalletService quick start example + +See [the full example](examples/nwc/wallet-service/example.js) + +```js +import { nwc } from "@getalby/sdk"; + +const walletService = new nwc.NWCWalletService({ + relayUrl: "wss://relay.getalby.com/v1", +}); + +// now for each client/app connection you can publish a NIP-47 info event and subscribe to requests + +await walletService.publishWalletServiceInfoEvent( + walletServiceSecretKey, + ["get_info"], // NIP-47 methods supported by your wallet service + [], +); + +// each client connection will have a unique keypair + +const keypair = new nwc.NWCWalletServiceKeyPair( + walletServiceSecretKey, + clientPubkey, +); + +const unsub = await walletService.subscribe(keypair, { + getInfo: () => { + return Promise.resolve({ + result: { + methods: ["get_info"], + alias: "Alby Hub", + //... add other fields here + }, + error: undefined, + }); + }, + // ... handle other NIP-47 methods here +}); +``` diff --git a/docs/nwc.md b/docs/nwc.md index 6ac526d..ecdf7b6 100644 --- a/docs/nwc.md +++ b/docs/nwc.md @@ -14,7 +14,7 @@ There are two interfaces you can use to access NWC: ### Initialization Options - `nostrWalletConnectUrl`: full Nostr Wallet Connect URL as defined by the [spec](https://github.com/getAlby/nips/blob/master/47.md) -- `relayUrl`: URL of the Nostr relay to be used (e.g. wss://nostr-relay.getalby.com) +- `relayUrl`: URL of the Nostr relay to be used (e.g. wss://relay.getalby.com/v1) - `walletPubkey`: pubkey of the Nostr Wallet Connect app - `secret`: secret key to sign the request event (if not available window.nostr will be used) @@ -52,7 +52,7 @@ See [the NWC client examples directory](../examples/nwc/client) for a full list ## NostrWebLNProvider (aliased as NWC) Options - `nostrWalletConnectUrl`: full Nostr Wallet Connect URL as defined by the [spec](https://github.com/getAlby/nips/blob/master/47.md) -- `relayUrl`: URL of the Nostr relay to be used (e.g. wss://nostr-relay.getalby.com) +- `relayUrl`: URL of the Nostr relay to be used (e.g. wss://relay.getalby.com/v1) - `walletPubkey`: pubkey of the Nostr Wallet Connect app - `secret`: secret key to sign the request event (if not available window.nostr will be used) - `client`: initialize using an existing NWC client From 7be89cc67c0a32673e25804cb3da27ff9957b8ab Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 1 Apr 2025 20:31:36 +0700 Subject: [PATCH 05/29] chore: move oauth client code into folder --- examples/oauth/boostagram.js | 3 ++- examples/oauth/decode-invoice.js | 3 ++- examples/oauth/invoices.js | 3 ++- examples/oauth/keysends.js | 3 ++- .../oauth/oauth2-public-callback_pkce_s256.mjs | 3 ++- examples/oauth/send-to-ln-address.js | 3 ++- examples/oauth/webhooks.js | 3 ++- package.json | 2 +- src/index.ts | 7 ++----- src/{ => lnclient}/LNClient.ts | 4 ++-- src/nwc/NWCClient.ts | 2 +- src/nwc/index.ts | 1 + src/nwc/types.ts | 13 +++++++++++++ src/{ => oauth}/AlbyResponseError.test.ts | 0 src/{ => oauth}/AlbyResponseError.ts | 3 +++ src/{ => oauth}/OAuth2Bearer.ts | 0 src/{ => oauth}/OAuth2User.ts | 3 ++- src/{ => oauth}/auth.ts | 0 src/{ => oauth}/client.ts | 0 src/{ => oauth}/eventEmitter/EventEmitter.ts | 0 src/{ => oauth}/helpers.ts | 0 src/oauth/index.ts | 3 +++ src/{ => oauth}/request.ts | 4 ++++ src/{ => oauth}/types.ts | 14 -------------- src/oauth/utils.ts | 14 ++++++++++++++ src/utils.ts | 15 --------------- src/webln/NostrWeblnProvider.ts | 2 +- src/webln/OauthWeblnProvider.ts | 4 ++-- 28 files changed, 63 insertions(+), 49 deletions(-) rename src/{ => lnclient}/LNClient.ts (97%) rename src/{ => oauth}/AlbyResponseError.test.ts (100%) rename src/{ => oauth}/AlbyResponseError.ts (76%) rename src/{ => oauth}/OAuth2Bearer.ts (100%) rename src/{ => oauth}/OAuth2User.ts (98%) rename src/{ => oauth}/auth.ts (100%) rename src/{ => oauth}/client.ts (100%) rename src/{ => oauth}/eventEmitter/EventEmitter.ts (100%) rename src/{ => oauth}/helpers.ts (100%) create mode 100644 src/oauth/index.ts rename src/{ => oauth}/request.ts (90%) rename src/{ => oauth}/types.ts (94%) create mode 100644 src/oauth/utils.ts diff --git a/examples/oauth/boostagram.js b/examples/oauth/boostagram.js index 1b2746d..9d646c1 100644 --- a/examples/oauth/boostagram.js +++ b/examples/oauth/boostagram.js @@ -1,7 +1,8 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../../dist/index.module.js"; +import { oauth } from "../../dist/index.module.js"; +const { auth, Client } = oauth; const rl = readline.createInterface({ input, output }); diff --git a/examples/oauth/decode-invoice.js b/examples/oauth/decode-invoice.js index 0f94571..abee6ec 100644 --- a/examples/oauth/decode-invoice.js +++ b/examples/oauth/decode-invoice.js @@ -1,6 +1,7 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../../dist/index.module.js"; +import { oauth } from "../../dist/index.module.js"; +const { auth, Client } = oauth; const rl = readline.createInterface({ input, output }); diff --git a/examples/oauth/invoices.js b/examples/oauth/invoices.js index 43e36f1..0b0c455 100644 --- a/examples/oauth/invoices.js +++ b/examples/oauth/invoices.js @@ -1,7 +1,8 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../../dist/index.module.js"; +import { oauth } from "../../dist/index.module.js"; +const { auth, Client } = oauth; const rl = readline.createInterface({ input, output }); diff --git a/examples/oauth/keysends.js b/examples/oauth/keysends.js index 29fe9d6..a176ba4 100644 --- a/examples/oauth/keysends.js +++ b/examples/oauth/keysends.js @@ -1,7 +1,8 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../../dist/index.module.js"; +import { oauth } from "../../dist/index.module.js"; +const { auth, Client } = oauth; const rl = readline.createInterface({ input, output }); diff --git a/examples/oauth/oauth2-public-callback_pkce_s256.mjs b/examples/oauth/oauth2-public-callback_pkce_s256.mjs index 5a58b8a..68d00f1 100644 --- a/examples/oauth/oauth2-public-callback_pkce_s256.mjs +++ b/examples/oauth/oauth2-public-callback_pkce_s256.mjs @@ -1,4 +1,5 @@ -import { auth, Client } from "../../dist/index.module.js"; +import { oauth } from "../../dist/index.module.js"; +const { auth, Client } = oauth; import express from "express"; if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) { diff --git a/examples/oauth/send-to-ln-address.js b/examples/oauth/send-to-ln-address.js index 30660db..5ccb1ee 100644 --- a/examples/oauth/send-to-ln-address.js +++ b/examples/oauth/send-to-ln-address.js @@ -1,7 +1,8 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../../dist/index.module.js"; +import { oauth } from "../../dist/index.module.js"; +const { auth, Client } = oauth; import { LightningAddress } from "alby-tools"; const rl = readline.createInterface({ input, output }); diff --git a/examples/oauth/webhooks.js b/examples/oauth/webhooks.js index 54495fa..8740d47 100644 --- a/examples/oauth/webhooks.js +++ b/examples/oauth/webhooks.js @@ -1,7 +1,8 @@ import * as readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; -import { auth, Client } from "../../dist/index.module.js"; +import { oauth } from "../../dist/index.module.js"; +const { auth, Client } = oauth; const rl = readline.createInterface({ input, output }); diff --git a/package.json b/package.json index 4b0de72..e871e53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@getalby/sdk", - "version": "4.1.1", + "version": "5.0.0", "description": "The SDK to integrate with Nostr Wallet Connect and the Alby API", "repository": "https://github.com/getAlby/js-sdk.git", "bugs": "https://github.com/getAlby/js-sdk/issues", diff --git a/src/index.ts b/src/index.ts index 6d0fbc9..c51383f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,4 @@ -export * as auth from "./auth"; -export * as types from "./types"; +export * as oauth from "./oauth"; export * as webln from "./webln"; -export { Client } from "./client"; -export * from "./LNClient"; -export * as nwa from "./nwc/NWAClient"; +export * from "./lnclient/LNClient"; export * as nwc from "./nwc"; diff --git a/src/LNClient.ts b/src/lnclient/LNClient.ts similarity index 97% rename from src/LNClient.ts rename to src/lnclient/LNClient.ts index d097d51..636c028 100644 --- a/src/LNClient.ts +++ b/src/lnclient/LNClient.ts @@ -4,8 +4,8 @@ import { Nip47Notification, Nip47PayInvoiceRequest, Nip47Transaction, -} from "./nwc/types"; -import { NewNWCClientOptions, NWCClient } from "./nwc/NWCClient"; +} from "../nwc/types"; +import { NewNWCClientOptions, NWCClient } from "../nwc/NWCClient"; type LNClientCredentials = string | NWCClient | NewNWCClientOptions; type FiatAmount = { amount: number; currency: string }; diff --git a/src/nwc/NWCClient.ts b/src/nwc/NWCClient.ts index 12d3827..828b590 100644 --- a/src/nwc/NWCClient.ts +++ b/src/nwc/NWCClient.ts @@ -10,7 +10,6 @@ import { EventTemplate, Relay, } from "nostr-tools"; -import { NWCAuthorizationUrlOptions } from "../types"; import { hexToBytes, bytesToHex } from "@noble/hashes/utils"; import { Subscription } from "nostr-tools/lib/types/abstract-relay"; import { @@ -50,6 +49,7 @@ import { Nip47UnsupportedEncryptionError, Nip47WalletError, Nip47MultiMethod, + NWCAuthorizationUrlOptions, } from "./types"; export interface NWCOptions { diff --git a/src/nwc/index.ts b/src/nwc/index.ts index 959daf2..2906c57 100644 --- a/src/nwc/index.ts +++ b/src/nwc/index.ts @@ -1,4 +1,5 @@ export * from "./types"; export * from "./NWCClient"; +export * from "./NWAClient"; export * from "./NWCWalletService"; export * from "./NWCWalletServiceRequestHandler"; diff --git a/src/nwc/types.ts b/src/nwc/types.ts index 53e33b5..d7c73b7 100644 --- a/src/nwc/types.ts +++ b/src/nwc/types.ts @@ -1,5 +1,18 @@ export type Nip47EncryptionType = "nip04" | "nip44_v2"; +export type NWCAuthorizationUrlOptions = { + name?: string; + icon?: string; + requestMethods?: Nip47Method[]; + notificationTypes?: Nip47NotificationType[]; + returnTo?: string; + expiresAt?: Date; + maxAmount?: number; + budgetRenewal?: "never" | "daily" | "weekly" | "monthly" | "yearly"; + isolated?: boolean; + metadata?: unknown; +}; + export class Nip47Error extends Error { code: string; constructor(message: string, code: string) { diff --git a/src/AlbyResponseError.test.ts b/src/oauth/AlbyResponseError.test.ts similarity index 100% rename from src/AlbyResponseError.test.ts rename to src/oauth/AlbyResponseError.test.ts diff --git a/src/AlbyResponseError.ts b/src/oauth/AlbyResponseError.ts similarity index 76% rename from src/AlbyResponseError.ts rename to src/oauth/AlbyResponseError.ts index 43ff59b..c82c71b 100644 --- a/src/AlbyResponseError.ts +++ b/src/oauth/AlbyResponseError.ts @@ -1,12 +1,15 @@ export class AlbyResponseError extends Error { status: number; statusText: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any headers: Record; + // eslint-disable-next-line @typescript-eslint/no-explicit-any error: any; // todo: typeable? constructor( status: number, statusText: string, headers: Headers, + // eslint-disable-next-line @typescript-eslint/no-explicit-any error: any, ) { let message = status.toString(); diff --git a/src/OAuth2Bearer.ts b/src/oauth/OAuth2Bearer.ts similarity index 100% rename from src/OAuth2Bearer.ts rename to src/oauth/OAuth2Bearer.ts diff --git a/src/OAuth2User.ts b/src/oauth/OAuth2User.ts similarity index 98% rename from src/OAuth2User.ts rename to src/oauth/OAuth2User.ts index 9457234..2fa6932 100644 --- a/src/OAuth2User.ts +++ b/src/oauth/OAuth2User.ts @@ -9,7 +9,8 @@ import { OAuthClient, Token, } from "./types"; -import { basicAuthHeader, buildQueryString, toHexString } from "./utils"; +import { basicAuthHeader, buildQueryString } from "./utils"; +import { toHexString } from "../utils"; const AUTHORIZE_URL = "https://getalby.com/oauth"; diff --git a/src/auth.ts b/src/oauth/auth.ts similarity index 100% rename from src/auth.ts rename to src/oauth/auth.ts diff --git a/src/client.ts b/src/oauth/client.ts similarity index 100% rename from src/client.ts rename to src/oauth/client.ts diff --git a/src/eventEmitter/EventEmitter.ts b/src/oauth/eventEmitter/EventEmitter.ts similarity index 100% rename from src/eventEmitter/EventEmitter.ts rename to src/oauth/eventEmitter/EventEmitter.ts diff --git a/src/helpers.ts b/src/oauth/helpers.ts similarity index 100% rename from src/helpers.ts rename to src/oauth/helpers.ts diff --git a/src/oauth/index.ts b/src/oauth/index.ts new file mode 100644 index 0000000..41d58ad --- /dev/null +++ b/src/oauth/index.ts @@ -0,0 +1,3 @@ +export * as auth from "./auth"; +export * as types from "./types"; +export { Client } from "./client"; diff --git a/src/request.ts b/src/oauth/request.ts similarity index 90% rename from src/request.ts rename to src/oauth/request.ts index 5683466..dadaede 100644 --- a/src/request.ts +++ b/src/oauth/request.ts @@ -6,8 +6,10 @@ const BASE_URL = "https://api.getalby.com"; export interface RequestOptions extends Omit { auth?: AuthClient; endpoint: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any params?: Record; user_agent?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any request_body?: Record; method?: string; max_retries?: number; @@ -84,7 +86,9 @@ export async function request({ return response; } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export async function rest(args: RequestOptions): Promise { const response = await request(args); + // eslint-disable-next-line @typescript-eslint/no-explicit-any return response.json() as any; } diff --git a/src/types.ts b/src/oauth/types.ts similarity index 94% rename from src/types.ts rename to src/oauth/types.ts index b18290a..935f323 100644 --- a/src/types.ts +++ b/src/oauth/types.ts @@ -1,5 +1,4 @@ import { AlbyResponseError } from "./AlbyResponseError"; -import { Nip47Method, Nip47NotificationType } from "./nwc/types"; import { RequestOptions } from "./request"; export type SuccessStatus = 200 | 201; @@ -230,19 +229,6 @@ export type Invoice = { }; } & Record; -export type NWCAuthorizationUrlOptions = { - name?: string; - icon?: string; - requestMethods?: Nip47Method[]; - notificationTypes?: Nip47NotificationType[]; - returnTo?: string; - expiresAt?: Date; - maxAmount?: number; - budgetRenewal?: "never" | "daily" | "weekly" | "monthly" | "yearly"; - isolated?: boolean; - metadata?: unknown; -}; - export type SendPaymentResponse = { amount: number; description: string; diff --git a/src/oauth/utils.ts b/src/oauth/utils.ts new file mode 100644 index 0000000..314fc19 --- /dev/null +++ b/src/oauth/utils.ts @@ -0,0 +1,14 @@ +// https://stackoverflow.com/a/62969380 + fix to remove empty entries (.filter(entry => entry)) +export function buildQueryString(query: Record): string { + return Object.entries(query) + .map(([key, value]) => (key && value ? `${key}=${value}` : "")) + .filter((entry) => entry) + .join("&"); +} + +export function basicAuthHeader( + client_id: string, + client_secret: string | undefined, +) { + return `Basic ${btoa(`${client_id}:${client_secret}`)}`; +} diff --git a/src/utils.ts b/src/utils.ts index eb1245a..da46af0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,18 +1,3 @@ -// https://stackoverflow.com/a/62969380 + fix to remove empty entries (.filter(entry => entry)) -export function buildQueryString(query: Record): string { - return Object.entries(query) - .map(([key, value]) => (key && value ? `${key}=${value}` : "")) - .filter((entry) => entry) - .join("&"); -} - -export function basicAuthHeader( - client_id: string, - client_secret: string | undefined, -) { - return `Basic ${btoa(`${client_id}:${client_secret}`)}`; -} - // from https://stackoverflow.com/a/50868276 export const toHexString = (bytes: Uint8Array) => bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), ""); diff --git a/src/webln/NostrWeblnProvider.ts b/src/webln/NostrWeblnProvider.ts index f608960..0c90fca 100644 --- a/src/webln/NostrWeblnProvider.ts +++ b/src/webln/NostrWeblnProvider.ts @@ -19,9 +19,9 @@ import { Nip47Method, Nip47PayKeysendRequest, Nip47Transaction, + NWCAuthorizationUrlOptions, } from "../nwc/types"; import { toHexString } from "../utils"; -import { NWCAuthorizationUrlOptions } from "../types"; // TODO: review fields (replace with camelCase) // TODO: consider move to webln-types package diff --git a/src/webln/OauthWeblnProvider.ts b/src/webln/OauthWeblnProvider.ts index 74318af..779dd84 100644 --- a/src/webln/OauthWeblnProvider.ts +++ b/src/webln/OauthWeblnProvider.ts @@ -1,5 +1,5 @@ -import { Client } from "../client"; -import { OAuthClient, KeysendRequestParams } from "../types"; +import { Client } from "../oauth/client"; +import { OAuthClient, KeysendRequestParams } from "../oauth/types"; interface RequestInvoiceArgs { amount: string | number; From 1bf7f06119a7c6030a24d7d4f5bb718b29cd50e9 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 1 Apr 2025 20:44:58 +0700 Subject: [PATCH 06/29] docs: clean up main README, add WebLN section --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index efc3c09..0227d51 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,6 @@ or for use without any build tools: ``` -### NodeJS - -**This library relies on a global fetch() function which will work in browsers and node v18.x or newer.** (In older versions you have to use a polyfill.) - ## Lightning Network Client (LN) Documentation Quickly get started adding lightning payments to your app. @@ -52,13 +48,13 @@ request.onPaid(giveAccess); [Read more](./docs/ln.md) -### Quick Start +For more flexibility you can access the underlying NWC wallet directly. Continue to read the Nostr Wallet Connect documentation below. ## Nostr Wallet Connect Documentation [Nostr Wallet Connect](https://nwc.dev) is an open protocol enabling applications to interact with bitcoin lightning wallets. It allows users to connect their existing wallets to your application allowing developers to easily integrate bitcoin lightning functionality. -For apps, see [NWC client documentation](./docs/nwc.md) +For apps, see [NWC client and NWA client documentation](./docs/nwc.md) For wallet services, see [NWC wallet service documentation](./docs/nwc-wallet-service.md) @@ -68,6 +64,15 @@ The [Alby OAuth API](https://guides.getalby.com/alby-wallet-api/reference/gettin [Read more](./docs/oauth.md) +### NodeJS + +**This library relies on a global fetch() function which will work in browsers and node v18.x or newer.** (In older versions you have to use a polyfill.) + +## WebLN Documentation + +The JS SDK also has some implementations for [WebLN](https://webln.guide). +See the [NostrWebLNProvider documentation](./docs/nwc.md) and [OAuthWebLNProvider documentation](./docs/oauth.md). + ## Need help? We are happy to help, please contact us or create an issue. From 7d070fd08705edac03c8c0510a29b8635cf61ca4 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Tue, 1 Apr 2025 21:41:21 +0700 Subject: [PATCH 07/29] feat: extract invoice receiving object into class and add fallback polling, fix exports --- src/index.ts | 2 +- src/lnclient/LNClient.ts | 34 +++--------------- src/lnclient/ReceiveInvoice.ts | 66 ++++++++++++++++++++++++++++++++++ src/lnclient/index.ts | 2 ++ src/oauth/index.ts | 1 + 5 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 src/lnclient/ReceiveInvoice.ts create mode 100644 src/lnclient/index.ts diff --git a/src/index.ts b/src/index.ts index c51383f..cdfb584 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ export * as oauth from "./oauth"; export * as webln from "./webln"; -export * from "./lnclient/LNClient"; export * as nwc from "./nwc"; +export * from "./lnclient"; diff --git a/src/lnclient/LNClient.ts b/src/lnclient/LNClient.ts index 636c028..6b6cedb 100644 --- a/src/lnclient/LNClient.ts +++ b/src/lnclient/LNClient.ts @@ -1,11 +1,7 @@ import { fiat, LightningAddress } from "@getalby/lightning-tools"; -import { - Nip47MakeInvoiceRequest, - Nip47Notification, - Nip47PayInvoiceRequest, - Nip47Transaction, -} from "../nwc/types"; +import { Nip47MakeInvoiceRequest, Nip47PayInvoiceRequest } from "../nwc/types"; import { NewNWCClientOptions, NWCClient } from "../nwc/NWCClient"; +import { ReceiveInvoice } from "./ReceiveInvoice"; type LNClientCredentials = string | NWCClient | NewNWCClientOptions; type FiatAmount = { amount: number; currency: string }; @@ -27,8 +23,6 @@ export const GBP = (amount: number) => export const CHF = (amount: number) => ({ amount, currency: "CHF" }) satisfies FiatAmount; -export type LN = LNClient; - export class LNClient { readonly nwcClient: NWCClient; @@ -82,27 +76,7 @@ export class LNClient { amount: parsedAmount.millisat, }); - return { - transaction, - invoice: transaction.invoice, - onPaid: async (callback: (receivedPayment: Nip47Transaction) => void) => { - let unsubscribeFunc = () => {}; - const onNotification = (notification: Nip47Notification) => { - if ( - notification.notification.payment_hash === transaction.payment_hash - ) { - unsubscribeFunc(); - callback(notification.notification); - } - }; - - unsubscribeFunc = await this.nwcClient.subscribeNotifications( - onNotification, - ["payment_received"], - ); - return unsubscribeFunc; - }, - }; + return new ReceiveInvoice(this.nwcClient, transaction); } close() { @@ -112,6 +86,8 @@ export class LNClient { // TODO: proxy everything from NWCClient } +export { LNClient as LN }; + async function parseAmount(amount: Amount) { let parsedAmount = 0; if ("satoshi" in amount) { diff --git a/src/lnclient/ReceiveInvoice.ts b/src/lnclient/ReceiveInvoice.ts new file mode 100644 index 0000000..965ca1e --- /dev/null +++ b/src/lnclient/ReceiveInvoice.ts @@ -0,0 +1,66 @@ +import { Nip47Notification, Nip47Transaction, NWCClient } from "../nwc"; + +export class ReceiveInvoice { + transaction: Nip47Transaction; + private _nwcClient: NWCClient; + + constructor(nwcClient: NWCClient, transaction: Nip47Transaction) { + this.transaction = transaction; + this._nwcClient = nwcClient; + } + + get invoice(): string { + return this.transaction.invoice; + } + + async onPaid(callback: (receivedPayment: Nip47Transaction) => void) { + // TODO: is there a better way than calling getInfo here? + const info = await this._nwcClient.getInfo(); + + if (!info.notifications?.includes("payment_received")) { + console.warn( + "current connection does not support notifications, falling back to polling", + ); + return this._onPaidPollingFallback(callback); + } + + let unsubscribeFunc = () => {}; + const onNotification = (notification: Nip47Notification) => { + if ( + notification.notification.payment_hash === this.transaction.payment_hash + ) { + unsubscribeFunc(); + callback(notification.notification); + } + }; + + unsubscribeFunc = await this._nwcClient.subscribeNotifications( + onNotification, + ["payment_received"], + ); + return unsubscribeFunc; + } + + private async _onPaidPollingFallback( + callback: (receivedPayment: Nip47Transaction) => void, + ) { + let subscribed = true; + const unsubscribeFunc = () => { + subscribed = false; + }; + while (subscribed) { + const transaction = await this._nwcClient.lookupInvoice({ + payment_hash: this.transaction.payment_hash, + }); + if (transaction.settled_at && transaction.preimage) { + callback(transaction); + subscribed = false; + break; + } + // sleep for 3 seconds per lookup attempt + await new Promise((resolve) => setTimeout(resolve, 3000)); + } + + return unsubscribeFunc; + } +} diff --git a/src/lnclient/index.ts b/src/lnclient/index.ts new file mode 100644 index 0000000..41bf92d --- /dev/null +++ b/src/lnclient/index.ts @@ -0,0 +1,2 @@ +export * from "./LNClient"; +export * from "./ReceiveInvoice"; diff --git a/src/oauth/index.ts b/src/oauth/index.ts index 41d58ad..65ad59e 100644 --- a/src/oauth/index.ts +++ b/src/oauth/index.ts @@ -1,3 +1,4 @@ export * as auth from "./auth"; export * as types from "./types"; +export * as utils from "./utils"; export { Client } from "./client"; From 317bae7256aa1584404d6815aa52ea05cbf39011 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 2 Apr 2025 12:16:41 +0700 Subject: [PATCH 08/29] chore: move fiat logic to new FiatAmount class --- README.md | 2 + docs/ln.md | 2 +- examples/{ln => lnclient}/pay_ln_address.js | 2 +- examples/{ln => lnclient}/paywall.js | 0 src/lnclient/Amount.ts | 18 ++++++++ src/lnclient/FiatAmount.ts | 27 ++++++++++++ src/lnclient/LNClient.ts | 46 ++++----------------- src/lnclient/index.ts | 2 + 8 files changed, 60 insertions(+), 39 deletions(-) rename examples/{ln => lnclient}/pay_ln_address.js (88%) rename examples/{ln => lnclient}/paywall.js (100%) create mode 100644 src/lnclient/Amount.ts create mode 100644 src/lnclient/FiatAmount.ts diff --git a/README.md b/README.md index 0227d51..83817c8 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ ## Introduction +Build zero-custody bitcoin payments into apps with a few lines of code. + This JavaScript SDK is for interacting with a bitcoin lightning wallet via Nostr Wallet Connect or the Alby Wallet API. ## Installing diff --git a/docs/ln.md b/docs/ln.md index c2b06cd..2c941bb 100644 --- a/docs/ln.md +++ b/docs/ln.md @@ -20,4 +20,4 @@ request.onPaid(giveAccess); ## Examples -See [the LNClient examples directory](./examples/ln) for a full list of examples. +See [the LNClient examples directory](./examples/lnclient) for a full list of examples. diff --git a/examples/ln/pay_ln_address.js b/examples/lnclient/pay_ln_address.js similarity index 88% rename from examples/ln/pay_ln_address.js rename to examples/lnclient/pay_ln_address.js index 19d4485..bf8628e 100644 --- a/examples/ln/pay_ln_address.js +++ b/examples/lnclient/pay_ln_address.js @@ -13,6 +13,6 @@ const nwcUrl = rl.close(); const client = new LN(nwcUrl); -const response = await client.pay("rolznz@getalby.com", USD(1.0)); +const response = await client.pay("hello@getalby.com", USD(1.0)); console.info("Paid successfully", response); client.close(); diff --git a/examples/ln/paywall.js b/examples/lnclient/paywall.js similarity index 100% rename from examples/ln/paywall.js rename to examples/lnclient/paywall.js diff --git a/src/lnclient/Amount.ts b/src/lnclient/Amount.ts new file mode 100644 index 0000000..0a0aa24 --- /dev/null +++ b/src/lnclient/Amount.ts @@ -0,0 +1,18 @@ +// TODO: move to lightning tools +/** + * An amount in satoshis + */ +export interface Amount { + satoshi: { satoshi: number } | Promise<{ satoshi: number }>; +} + +export async function resolveAmount( + amount: Amount, +): Promise<{ satoshi: number; millisat: number }> { + const satoshi = await Promise.resolve(amount.satoshi); + + return { + satoshi: satoshi.satoshi, + millisat: satoshi.satoshi * 1000, + }; +} diff --git a/src/lnclient/FiatAmount.ts b/src/lnclient/FiatAmount.ts new file mode 100644 index 0000000..b88dfd5 --- /dev/null +++ b/src/lnclient/FiatAmount.ts @@ -0,0 +1,27 @@ +import { fiat } from "@getalby/lightning-tools"; +import { Amount } from "./Amount"; + +// TODO: move this to Lightning Tools +export class FiatAmount implements Amount { + satoshi: Promise<{ satoshi: number }>; + constructor(amount: number, currency: string) { + this.satoshi = this._fetchSatoshi(amount, currency); + } + private async _fetchSatoshi( + amount: number, + currency: string, + ): Promise<{ satoshi: number }> { + const satoshi = await fiat.getSatoshiValue({ + amount, + currency, + }); + return { satoshi }; + } +} + +// Most popular fiat currencies +export const USD = (amount: number) => new FiatAmount(amount, "USD"); +export const EUR = (amount: number) => new FiatAmount(amount, "EUR"); +export const JPY = (amount: number) => new FiatAmount(amount, "JPY"); +export const GBP = (amount: number) => new FiatAmount(amount, "GBP"); +export const CHF = (amount: number) => new FiatAmount(amount, "CHF"); diff --git a/src/lnclient/LNClient.ts b/src/lnclient/LNClient.ts index 6b6cedb..499324d 100644 --- a/src/lnclient/LNClient.ts +++ b/src/lnclient/LNClient.ts @@ -1,27 +1,10 @@ -import { fiat, LightningAddress } from "@getalby/lightning-tools"; +import { LightningAddress } from "@getalby/lightning-tools"; import { Nip47MakeInvoiceRequest, Nip47PayInvoiceRequest } from "../nwc/types"; import { NewNWCClientOptions, NWCClient } from "../nwc/NWCClient"; import { ReceiveInvoice } from "./ReceiveInvoice"; +import { Amount, resolveAmount } from "./Amount"; type LNClientCredentials = string | NWCClient | NewNWCClientOptions; -type FiatAmount = { amount: number; currency: string }; - -/** - * An amount in satoshis, or an amount in a fiat currency - */ -type Amount = { satoshi: number } | FiatAmount; - -// Most popular fiat currencies -export const USD = (amount: number) => - ({ amount, currency: "USD" }) satisfies FiatAmount; -export const EUR = (amount: number) => - ({ amount, currency: "EUR" }) satisfies FiatAmount; -export const JPY = (amount: number) => - ({ amount, currency: "JPY" }) satisfies FiatAmount; -export const GBP = (amount: number) => - ({ amount, currency: "GBP" }) satisfies FiatAmount; -export const CHF = (amount: number) => - ({ amount, currency: "CHF" }) satisfies FiatAmount; export class LNClient { readonly nwcClient: NWCClient; @@ -44,7 +27,7 @@ export class LNClient { args?: Omit, ) { let invoice = recipient; - const parsedAmount = amount ? await parseAmount(amount) : undefined; + const parsedAmount = amount ? await resolveAmount(amount) : undefined; if (invoice.indexOf("@") > -1) { if (!parsedAmount) { throw new Error( @@ -59,18 +42,22 @@ export class LNClient { invoice = invoiceObj.paymentRequest; } - return this.nwcClient.payInvoice({ + const result = await this.nwcClient.payInvoice({ ...(args || {}), invoice, amount: parsedAmount?.millisat, }); + return { + ...result, + invoice, + }; } async receive( amount: Amount, args?: Omit, ) { - const parsedAmount = await parseAmount(amount); + const parsedAmount = await resolveAmount(amount); const transaction = await this.nwcClient.makeInvoice({ ...(args || {}), amount: parsedAmount.millisat, @@ -82,21 +69,6 @@ export class LNClient { close() { this.nwcClient.close(); } - - // TODO: proxy everything from NWCClient } export { LNClient as LN }; - -async function parseAmount(amount: Amount) { - let parsedAmount = 0; - if ("satoshi" in amount) { - parsedAmount = amount.satoshi; - } else { - parsedAmount = await fiat.getSatoshiValue({ - amount: amount.amount, - currency: amount.currency, - }); - } - return { satoshi: parsedAmount, millisat: parsedAmount * 1000 }; -} diff --git a/src/lnclient/index.ts b/src/lnclient/index.ts index 41bf92d..4c3d8b3 100644 --- a/src/lnclient/index.ts +++ b/src/lnclient/index.ts @@ -1,2 +1,4 @@ export * from "./LNClient"; export * from "./ReceiveInvoice"; +export * from "./Amount"; +export * from "./FiatAmount"; From 33640c40ca626045c2dc2da348ecfacb6d47aab5 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 2 Apr 2025 12:19:22 +0700 Subject: [PATCH 09/29] docs: rename --- README.md | 2 +- docs/{ln.md => lnclient.md} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename docs/{ln.md => lnclient.md} (100%) diff --git a/README.md b/README.md index 83817c8..9692827 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ const request = await new LN(credentials).receive(USD(1.0)); request.onPaid(giveAccess); ``` -[Read more](./docs/ln.md) +[Read more](./docs/lnclient.md) For more flexibility you can access the underlying NWC wallet directly. Continue to read the Nostr Wallet Connect documentation below. diff --git a/docs/ln.md b/docs/lnclient.md similarity index 100% rename from docs/ln.md rename to docs/lnclient.md From cf5f2c19a83faa9ce458fdaa81af52e418980a13 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 2 Apr 2025 12:36:06 +0700 Subject: [PATCH 10/29] fix: amount type --- docs/lnclient.md | 1 + examples/lnclient/pay_ln_address.js | 2 +- src/lnclient/Amount.test.ts | 16 ++++++++++++++++ src/lnclient/Amount.ts | 14 +++++++++----- src/lnclient/FiatAmount.test.ts | 10 ++++++++++ src/lnclient/FiatAmount.ts | 16 ++++------------ 6 files changed, 41 insertions(+), 18 deletions(-) create mode 100644 src/lnclient/Amount.test.ts create mode 100644 src/lnclient/FiatAmount.test.ts diff --git a/docs/lnclient.md b/docs/lnclient.md index 2c941bb..74e30d6 100644 --- a/docs/lnclient.md +++ b/docs/lnclient.md @@ -8,6 +8,7 @@ For example, to make a payment: import { LN, USD } from "@getalby/sdk"; await new LN(credentials).pay("lnbc..."); // pay a lightning invoice await new LN(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address +await new LN(credentials).pay("hello@getalby.com", { satoshi: 21 }); // or pay 21 sats to a lightning address ``` Or to request a payment to be received: diff --git a/examples/lnclient/pay_ln_address.js b/examples/lnclient/pay_ln_address.js index bf8628e..19d4485 100644 --- a/examples/lnclient/pay_ln_address.js +++ b/examples/lnclient/pay_ln_address.js @@ -13,6 +13,6 @@ const nwcUrl = rl.close(); const client = new LN(nwcUrl); -const response = await client.pay("hello@getalby.com", USD(1.0)); +const response = await client.pay("rolznz@getalby.com", USD(1.0)); console.info("Paid successfully", response); client.close(); diff --git a/src/lnclient/Amount.test.ts b/src/lnclient/Amount.test.ts new file mode 100644 index 0000000..157bda8 --- /dev/null +++ b/src/lnclient/Amount.test.ts @@ -0,0 +1,16 @@ +import { resolveAmount } from "./Amount"; + +describe("Amount", () => { + test("resolveAmount", async () => { + const resolved = await resolveAmount({ satoshi: 10 }); + expect(resolved.satoshi).toBe(10); + expect(resolved.millisat).toBe(10_000); + }); + test("resolveAmount async", async () => { + const resolved = await resolveAmount({ + satoshi: new Promise((resolve) => setTimeout(() => resolve(10), 300)), + }); + expect(resolved.satoshi).toBe(10); + expect(resolved.millisat).toBe(10_000); + }); +}); diff --git a/src/lnclient/Amount.ts b/src/lnclient/Amount.ts index 0a0aa24..096536a 100644 --- a/src/lnclient/Amount.ts +++ b/src/lnclient/Amount.ts @@ -2,17 +2,21 @@ /** * An amount in satoshis */ -export interface Amount { - satoshi: { satoshi: number } | Promise<{ satoshi: number }>; -} +export type Amount = { satoshi: number } | { satoshi: Promise }; export async function resolveAmount( amount: Amount, ): Promise<{ satoshi: number; millisat: number }> { + if (typeof amount === "number") { + return { + satoshi: amount, + millisat: amount * 1000, + }; + } const satoshi = await Promise.resolve(amount.satoshi); return { - satoshi: satoshi.satoshi, - millisat: satoshi.satoshi * 1000, + satoshi: satoshi, + millisat: satoshi * 1000, }; } diff --git a/src/lnclient/FiatAmount.test.ts b/src/lnclient/FiatAmount.test.ts new file mode 100644 index 0000000..1686562 --- /dev/null +++ b/src/lnclient/FiatAmount.test.ts @@ -0,0 +1,10 @@ +import { resolveAmount } from "./Amount"; +import { USD } from "./FiatAmount"; + +describe("FiatAmount", () => { + test("interoperable with Amount", async () => { + const fiatAmount = USD(1); + const resolved = await resolveAmount(fiatAmount); + expect(resolved.satoshi).toBeGreaterThan(0); + }); +}); diff --git a/src/lnclient/FiatAmount.ts b/src/lnclient/FiatAmount.ts index b88dfd5..ff2ebdb 100644 --- a/src/lnclient/FiatAmount.ts +++ b/src/lnclient/FiatAmount.ts @@ -1,21 +1,13 @@ import { fiat } from "@getalby/lightning-tools"; -import { Amount } from "./Amount"; -// TODO: move this to Lightning Tools -export class FiatAmount implements Amount { - satoshi: Promise<{ satoshi: number }>; +// TODO: move to Lightning Tools +export class FiatAmount { + satoshi: Promise; constructor(amount: number, currency: string) { - this.satoshi = this._fetchSatoshi(amount, currency); - } - private async _fetchSatoshi( - amount: number, - currency: string, - ): Promise<{ satoshi: number }> { - const satoshi = await fiat.getSatoshiValue({ + this.satoshi = fiat.getSatoshiValue({ amount, currency, }); - return { satoshi }; } } From 7ec91d1afc85c1056297b653afcb523ca8b24514 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 2 Apr 2025 12:39:18 +0700 Subject: [PATCH 11/29] chore: return invoice objects from lnclient send and receive methods --- src/lnclient/LNClient.ts | 4 ++-- src/lnclient/ReceiveInvoice.ts | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/lnclient/LNClient.ts b/src/lnclient/LNClient.ts index 499324d..d134eaa 100644 --- a/src/lnclient/LNClient.ts +++ b/src/lnclient/LNClient.ts @@ -1,4 +1,4 @@ -import { LightningAddress } from "@getalby/lightning-tools"; +import { Invoice, LightningAddress } from "@getalby/lightning-tools"; import { Nip47MakeInvoiceRequest, Nip47PayInvoiceRequest } from "../nwc/types"; import { NewNWCClientOptions, NWCClient } from "../nwc/NWCClient"; import { ReceiveInvoice } from "./ReceiveInvoice"; @@ -49,7 +49,7 @@ export class LNClient { }); return { ...result, - invoice, + invoice: new Invoice({ pr: invoice }), }; } diff --git a/src/lnclient/ReceiveInvoice.ts b/src/lnclient/ReceiveInvoice.ts index 965ca1e..7959ad1 100644 --- a/src/lnclient/ReceiveInvoice.ts +++ b/src/lnclient/ReceiveInvoice.ts @@ -1,18 +1,17 @@ +import { Invoice } from "@getalby/lightning-tools"; import { Nip47Notification, Nip47Transaction, NWCClient } from "../nwc"; export class ReceiveInvoice { - transaction: Nip47Transaction; + readonly transaction: Nip47Transaction; + readonly invoice: Invoice; private _nwcClient: NWCClient; constructor(nwcClient: NWCClient, transaction: Nip47Transaction) { this.transaction = transaction; + this.invoice = new Invoice({ pr: transaction.invoice }); this._nwcClient = nwcClient; } - get invoice(): string { - return this.transaction.invoice; - } - async onPaid(callback: (receivedPayment: Nip47Transaction) => void) { // TODO: is there a better way than calling getInfo here? const info = await this._nwcClient.getInfo(); From 665c5b3bac5daa10b51277958477e4daab1d5ffc Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 2 Apr 2025 12:48:46 +0700 Subject: [PATCH 12/29] chore: add sats helper --- docs/lnclient.md | 5 +++-- src/lnclient/Amount.test.ts | 6 +++++- src/lnclient/Amount.ts | 4 ++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/lnclient.md b/docs/lnclient.md index 74e30d6..03a8b39 100644 --- a/docs/lnclient.md +++ b/docs/lnclient.md @@ -5,10 +5,11 @@ The LN class helps you easily get started interacting with the lightning network For example, to make a payment: ```js -import { LN, USD } from "@getalby/sdk"; +import { LN, USD, SATS } from "@getalby/sdk"; await new LN(credentials).pay("lnbc..."); // pay a lightning invoice +await new LN(credentials).pay("hello@getalby.com", SATS(21)); // or pay 21 sats to a lightning address await new LN(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address -await new LN(credentials).pay("hello@getalby.com", { satoshi: 21 }); // or pay 21 sats to a lightning address +await new LN(credentials).pay("hello@getalby.com", new FiatAmount(1, "THB")); // or pay an amount in any currency to a lightning address ``` Or to request a payment to be received: diff --git a/src/lnclient/Amount.test.ts b/src/lnclient/Amount.test.ts index 157bda8..f12b890 100644 --- a/src/lnclient/Amount.test.ts +++ b/src/lnclient/Amount.test.ts @@ -1,6 +1,10 @@ -import { resolveAmount } from "./Amount"; +import { resolveAmount, SATS } from "./Amount"; describe("Amount", () => { + test("SATS", async () => { + const amount = SATS(10); + expect(amount.satoshi).toBe(10); + }); test("resolveAmount", async () => { const resolved = await resolveAmount({ satoshi: 10 }); expect(resolved.satoshi).toBe(10); diff --git a/src/lnclient/Amount.ts b/src/lnclient/Amount.ts index 096536a..3913b4c 100644 --- a/src/lnclient/Amount.ts +++ b/src/lnclient/Amount.ts @@ -4,6 +4,10 @@ */ export type Amount = { satoshi: number } | { satoshi: Promise }; +export const SATS: (amount: number) => Amount = (amount) => ({ + satoshi: amount, +}); + export async function resolveAmount( amount: Amount, ): Promise<{ satoshi: number; millisat: number }> { From e7a49391fc5bcd4ce6971453af4c37d98a40ecd3 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Wed, 2 Apr 2025 13:10:17 +0700 Subject: [PATCH 13/29] docs: add basic LNClient documentation --- src/lnclient/LNClient.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/lnclient/LNClient.ts b/src/lnclient/LNClient.ts index d134eaa..dbfda98 100644 --- a/src/lnclient/LNClient.ts +++ b/src/lnclient/LNClient.ts @@ -6,9 +6,16 @@ import { Amount, resolveAmount } from "./Amount"; type LNClientCredentials = string | NWCClient | NewNWCClientOptions; +/** + * A simple lightning network client to interact with your lightning wallet + */ export class LNClient { readonly nwcClient: NWCClient; + /** + * Create a new LNClient + * @param credentials credentials to connect to a NWC-based wallet. Learn more at https://nwc.dev + */ constructor(credentials: LNClientCredentials) { if (typeof credentials === "string") { this.nwcClient = new NWCClient({ @@ -21,6 +28,13 @@ export class LNClient { } } + /** + * Make a payment + * @param recipient a BOLT-11 invoice or lightning address + * @param amount the amount to pay, only required if paying to a lightning address or the amount is not specified in the BOLT 11 invoice. + * @param args additional options, e.g. to store metadata on the payment + * @returns the receipt of the payment, and details of the paid invoice. + */ async pay( recipient: string, amount?: Amount, @@ -53,6 +67,12 @@ export class LNClient { }; } + /** + * Receive a payment + * @param amount the amount requested, either in sats (e.g. {satoshi: 21}) or fiat (e.g. new FiatAmount(21, "USD")). + * @param args additional options, e.g. to set a description on the payment request, or store metadata for the received payment + * @returns the invoice to be paid, along with methods to easily listen for a payment and act upon it. + */ async receive( amount: Amount, args?: Omit, From 8149fdf0547dc1d308fe01b532d173778aa51b1f Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 4 Apr 2025 12:26:35 +0700 Subject: [PATCH 14/29] docs: add links to get credentials --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9692827..087d7b3 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ await new LN(credentials).pay("lnbc..."); // pay a lightning invoice await new LN(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address ``` +> The easiest way to provide credentials is with an [NWC connection secret](https://nwc.dev). Get one in minutes by connecting to [Alby Hub](https://albyhub.com/), [coinos](https://coinos.io/apps/new), [Primal](https://primal.net/downloads), [lnwallet.app](https://lnwallet.app/), [Yakihonne](https://yakihonne.com/), [or other NWC-enabled wallets](https://github.com/getAlby/awesome-nwc?tab=readme-ov-file#nwc-wallets). + Or to request a payment to be received: ```js From e7fca5a1e0367c5518c91a197a1b130dd10f2e1d Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 4 Apr 2025 22:09:00 +0700 Subject: [PATCH 15/29] chore: add docs github action --- .github/workflows/docs.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..f360092 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,35 @@ +name: "typedoc" + +on: + push: + branches: [feat/lnclient, master] + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + # https://github.com/actions/checkout + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + # Generate your TypeDoc documentation + - run: npx typedoc --out docs/generated + # https://github.com/actions/upload-pages-artifact + - uses: actions/upload-pages-artifact@v3 + with: + path: ./docs/generated # This should be your TypeDoc "out" path. + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + # https://github.com/actions/deploy-pages + uses: actions/deploy-pages@v4 From a2beb96bf7c8cffa51776bd60172d64ed801ce75 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 4 Apr 2025 22:12:53 +0700 Subject: [PATCH 16/29] fix: typedoc entrypoint --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f360092..cbdf5f0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 # Generate your TypeDoc documentation - - run: npx typedoc --out docs/generated + - run: npx typedoc src/index.ts --out docs/generated # https://github.com/actions/upload-pages-artifact - uses: actions/upload-pages-artifact@v3 with: From 3234991275b9611a7e5636df1304aff7538395ef Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 4 Apr 2025 22:15:15 +0700 Subject: [PATCH 17/29] fix: run yarn install before generating docs --- .github/workflows/docs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cbdf5f0..f1175d3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,6 +16,10 @@ jobs: # https://github.com/actions/checkout - uses: actions/checkout@v4 - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: "yarn" + - run: yarn install --frozen-lockfile # Generate your TypeDoc documentation - run: npx typedoc src/index.ts --out docs/generated # https://github.com/actions/upload-pages-artifact From a8b0e106b7ad7ff7c860b53c22060ebd73ce4268 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 4 Apr 2025 23:17:11 +0700 Subject: [PATCH 18/29] chore: fix typedoc warnings --- src/lnclient/LNClient.ts | 4 ++-- src/nwc/NWCClient.ts | 6 +++--- src/nwc/NWCWalletService.ts | 2 +- src/nwc/types.ts | 4 ++-- src/webln/NostrWeblnProvider.ts | 4 ++-- src/webln/OauthWeblnProvider.ts | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lnclient/LNClient.ts b/src/lnclient/LNClient.ts index dbfda98..e8837f8 100644 --- a/src/lnclient/LNClient.ts +++ b/src/lnclient/LNClient.ts @@ -4,7 +4,7 @@ import { NewNWCClientOptions, NWCClient } from "../nwc/NWCClient"; import { ReceiveInvoice } from "./ReceiveInvoice"; import { Amount, resolveAmount } from "./Amount"; -type LNClientCredentials = string | NWCClient | NewNWCClientOptions; +export type LNClientCredentials = string | NWCClient | NewNWCClientOptions; /** * A simple lightning network client to interact with your lightning wallet @@ -14,7 +14,7 @@ export class LNClient { /** * Create a new LNClient - * @param credentials credentials to connect to a NWC-based wallet. Learn more at https://nwc.dev + * @param credentials credentials to connect to a NWC-based wallet. This can be a NWC connection string e.g. nostr+walletconnect://... or an existing NWC Client. Learn more at https://nwc.dev */ constructor(credentials: LNClientCredentials) { if (typeof credentials === "string") { diff --git a/src/nwc/NWCClient.ts b/src/nwc/NWCClient.ts index 828b590..c9858ab 100644 --- a/src/nwc/NWCClient.ts +++ b/src/nwc/NWCClient.ts @@ -272,9 +272,9 @@ export class NWCClient { /** * create a new client-initiated NWC connection via HTTP deeplink * - * @authorizationBasePath the deeplink path e.g. https://my.albyhub.com/apps/new - * @options configure the created app (e.g. the name, budget, expiration) - * @secret optionally pass a secret, otherwise one will be generated. + * @param authorizationBasePath the deeplink path e.g. https://my.albyhub.com/apps/new + * @param options configure the created app (e.g. the name, budget, expiration) + * @param secret optionally pass a secret, otherwise one will be generated. */ static fromAuthorizationUrl( authorizationBasePath: string, diff --git a/src/nwc/NWCWalletService.ts b/src/nwc/NWCWalletService.ts index dce654e..8a3ea62 100644 --- a/src/nwc/NWCWalletService.ts +++ b/src/nwc/NWCWalletService.ts @@ -29,7 +29,7 @@ import { NWCWalletServiceResponsePromise, } from "./NWCWalletServiceRequestHandler"; -type NewNWCWalletServiceOptions = { +export type NewNWCWalletServiceOptions = { relayUrl: string; }; diff --git a/src/nwc/types.ts b/src/nwc/types.ts index d7c73b7..2af353c 100644 --- a/src/nwc/types.ts +++ b/src/nwc/types.ts @@ -37,11 +37,11 @@ export class Nip47ResponseValidationError extends Nip47Error {} export class Nip47UnexpectedResponseError extends Nip47Error {} export class Nip47UnsupportedEncryptionError extends Nip47Error {} -type WithDTag = { +export type WithDTag = { dTag: string; }; -type WithOptionalId = { +export type WithOptionalId = { id?: string; }; diff --git a/src/webln/NostrWeblnProvider.ts b/src/webln/NostrWeblnProvider.ts index 0c90fca..829e58d 100644 --- a/src/webln/NostrWeblnProvider.ts +++ b/src/webln/NostrWeblnProvider.ts @@ -56,7 +56,7 @@ export type MultiKeysendResponse = { type NostrWebLNOptions = NWCOptions; -type Nip07Provider = { +export type Nip07Provider = { getPublicKey(): Promise; signEvent(event: UnsignedEvent): Promise; }; @@ -77,7 +77,7 @@ const nip47ToWeblnRequestMap: Record< sign_message: "signMessage", }; -type NewNostrWeblnProviderOptions = NewNWCClientOptions & { +export type NewNostrWeblnProviderOptions = NewNWCClientOptions & { client?: NWCClient; }; diff --git a/src/webln/OauthWeblnProvider.ts b/src/webln/OauthWeblnProvider.ts index 779dd84..b2b3197 100644 --- a/src/webln/OauthWeblnProvider.ts +++ b/src/webln/OauthWeblnProvider.ts @@ -1,7 +1,7 @@ import { Client } from "../oauth/client"; import { OAuthClient, KeysendRequestParams } from "../oauth/types"; -interface RequestInvoiceArgs { +export interface RequestInvoiceArgs { amount: string | number; defaultMemo?: string; } From fb5d6ab783a27ffa75cdeb2d33ebca4d174bc603 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Fri, 4 Apr 2025 23:17:20 +0700 Subject: [PATCH 19/29] docs: link to typedoc documentation --- README.md | 8 ++++++-- docs/lnclient.md | 2 ++ docs/nwc-wallet-service.md | 2 ++ docs/nwc.md | 10 +++++++++- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 087d7b3..d53b545 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ or for use without any build tools: Quickly get started adding lightning payments to your app. +> The easiest way to provide credentials is with an [NWC connection secret](https://nwc.dev). Get one in minutes by connecting to [Alby Hub](https://albyhub.com/), [coinos](https://coinos.io/apps/new), [Primal](https://primal.net/downloads), [lnwallet.app](https://lnwallet.app/), [Yakihonne](https://yakihonne.com/), [or other NWC-enabled wallets](https://github.com/getAlby/awesome-nwc?tab=readme-ov-file#nwc-wallets). + For example, to make a payment: ```js @@ -40,8 +42,6 @@ await new LN(credentials).pay("lnbc..."); // pay a lightning invoice await new LN(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address ``` -> The easiest way to provide credentials is with an [NWC connection secret](https://nwc.dev). Get one in minutes by connecting to [Alby Hub](https://albyhub.com/), [coinos](https://coinos.io/apps/new), [Primal](https://primal.net/downloads), [lnwallet.app](https://lnwallet.app/), [Yakihonne](https://yakihonne.com/), [or other NWC-enabled wallets](https://github.com/getAlby/awesome-nwc?tab=readme-ov-file#nwc-wallets). - Or to request a payment to be received: ```js @@ -77,6 +77,10 @@ The [Alby OAuth API](https://guides.getalby.com/alby-wallet-api/reference/gettin The JS SDK also has some implementations for [WebLN](https://webln.guide). See the [NostrWebLNProvider documentation](./docs/nwc.md) and [OAuthWebLNProvider documentation](./docs/oauth.md). +## More Documentation + +Read the [auto-generated documentation](https://getalby.github.io/js-sdk/modules.html) + ## Need help? We are happy to help, please contact us or create an issue. diff --git a/docs/lnclient.md b/docs/lnclient.md index 03a8b39..7bb6c1f 100644 --- a/docs/lnclient.md +++ b/docs/lnclient.md @@ -2,6 +2,8 @@ The LN class helps you easily get started interacting with the lightning network. It is a high level wrapper around the [NWCClient](./nwc.md) which is compatible with many different lightning wallets. +See [LNClient class documentation](https://getalby.github.io/js-sdk/classes/LNClient.html) + For example, to make a payment: ```js diff --git a/docs/nwc-wallet-service.md b/docs/nwc-wallet-service.md index 03576f0..fb15fb2 100644 --- a/docs/nwc-wallet-service.md +++ b/docs/nwc-wallet-service.md @@ -4,6 +4,8 @@ The Alby JS SDK allows you to easily integrate Nostr Wallet Connect into any JavaScript based lightning wallet to allow client applications to easily connect and seamlessly interact with the wallet. +> See [NWCWalletService class documentation](https://getalby.github.io/js-sdk/classes/nwc.NWCWalletService.html) + ## NWCWalletService ### Initialization Options diff --git a/docs/nwc.md b/docs/nwc.md index ecdf7b6..3870620 100644 --- a/docs/nwc.md +++ b/docs/nwc.md @@ -9,6 +9,8 @@ There are two interfaces you can use to access NWC: - The `NWCClient` exposes the [NWC](https://nwc.dev/) interface directly, which is more powerful than the WebLN interface and is recommended if you plan to create an application outside of the web (e.g. native mobile/command line/server backend etc.). You can explore all the examples [here](../examples/nwc/client/). - The `NostrWebLNProvider` exposes the [WebLN](https://webln.guide/) interface to execute lightning wallet functionality through Nostr Wallet Connect, such as sending payments, making invoices and getting the node balance. You can explore all the examples [here](../examples/nwc/). See also [Bitcoin Connect](https://github.com/getAlby/bitcoin-connect/) if you are developing a frontend web application. +> See [NWCClient class documentation](https://getalby.github.io/js-sdk/classes/nwc.NWCClient.html) + ## NWCClient ### Initialization Options @@ -49,7 +51,11 @@ The same options can be provided to getAuthorizationUrl() as fromAuthorizationUr See [the NWC client examples directory](../examples/nwc/client) for a full list of examples. -## NostrWebLNProvider (aliased as NWC) Options +## NostrWebLNProvider + +> See [NostrWebLNProvider class documentation](https://getalby.github.io/js-sdk/classes/webln.NostrWebLNProvider.html) + +### Initialization Options - `nostrWalletConnectUrl`: full Nostr Wallet Connect URL as defined by the [spec](https://github.com/getAlby/nips/blob/master/47.md) - `relayUrl`: URL of the Nostr relay to be used (e.g. wss://relay.getalby.com/v1) @@ -219,6 +225,8 @@ NWA is an alternative flow for lightning apps to easily initialize an NWC connec The app will generate an NWA URI which should be opened in the wallet, where the user can approve the connection. +> See [NWAClient class documentation](https://getalby.github.io/js-sdk/classes/nwc.NWAClient.html) + #### Generating an NWA URI See [NWA example](examples/nwc/client/nwa.js) From 7eb7a42d0bf0197273a1190a17c2256db223b8f1 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sun, 6 Apr 2025 13:08:48 +0200 Subject: [PATCH 20/29] docs: add splitter This example shows how to receive a payment and split it to other recipients --- examples/lnclient/splitter.js | 60 +++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 examples/lnclient/splitter.js diff --git a/examples/lnclient/splitter.js b/examples/lnclient/splitter.js new file mode 100644 index 0000000..c5377db --- /dev/null +++ b/examples/lnclient/splitter.js @@ -0,0 +1,60 @@ +import "websocket-polyfill"; // required in node.js +import qrcode from "qrcode-terminal"; + +import * as readline from "node:readline/promises"; +import { stdin as input, stdout as output } from "node:process"; + +import { LN, USD } from "../../dist/index.module.js"; + +const rl = readline.createInterface({ input, output }); + +const nwcUrl = + process.env.NWC_URL || + (await rl.question("Nostr Wallet Connect URL (nostr+walletconnect://...): ")); +rl.close(); + +const amount = USD(1.0); +const recipients = ["rolznz@getalby.com", "hello@getalby.com"]; +const forwardPercentage = 50; + +const client = new LN(nwcUrl); + +// request an lightning invoice +const request = await client.receive(amount, { description: "prism payment" }); + +// prompt the user to pay the invoice +qrcode.generate(request.invoice.paymentRequest, { small: true }); +console.info(request.invoice.paymentRequest); +console.info("Waiting for payment..."); + +// once the invoice got paid by the user run this callback +const unsub = await request.onPaid(async () => { + // we take the sats amount from theinvocie and calculate the amount we want to forward + const satsReceived = request.invoice.satoshi; + const satsToForward = Math.floor( + (satsReceived * forwardPercentage) / 100 / recipients.length, + ); + console.info( + `Received ${satsReceived} sats! Forwarding ${satsToForward} to the ${recipients.join(", ")}`, + ); + + // iterate over all recipients and pay them the amount + await Promise.all( + recipients.map(async (r) => { + const response = await client.pay(r, satsToForward, "splitter"); + console.info( + `Forwarded ${satsToForward} sats to ${r} (preimage: ${response.preimage})`, + ); + }), + ); + client.close(); +}); + +process.on("SIGINT", function () { + console.info("Caught interrupt signal"); + + unsub(); + client.close(); + + process.exit(); +}); From 3edeb2cd3de0202f6016b0184a3a39ca132b1792 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sun, 6 Apr 2025 13:09:49 +0200 Subject: [PATCH 21/29] docs: more logs in examples --- examples/lnclient/pay_ln_address.js | 3 ++- examples/lnclient/paywall.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/lnclient/pay_ln_address.js b/examples/lnclient/pay_ln_address.js index 19d4485..c182a2b 100644 --- a/examples/lnclient/pay_ln_address.js +++ b/examples/lnclient/pay_ln_address.js @@ -13,6 +13,7 @@ const nwcUrl = rl.close(); const client = new LN(nwcUrl); -const response = await client.pay("rolznz@getalby.com", USD(1.0)); +console.log("Paying $1"); +const response = await client.pay("rolznz@getalby.com", USD(1000)); console.info("Paid successfully", response); client.close(); diff --git a/examples/lnclient/paywall.js b/examples/lnclient/paywall.js index 0acfc18..a94ec60 100644 --- a/examples/lnclient/paywall.js +++ b/examples/lnclient/paywall.js @@ -16,7 +16,8 @@ rl.close(); const client = new LN(nwcUrl); const request = await client.receive(USD(1.0)); -qrcode.generate(request.invoice, { small: true }); +qrcode.generate(request.invoice.paymentRequest, { small: true }); +console.info(request.invoice.paymentRequest); console.info("Waiting for payment..."); const unsub = await request.onPaid(() => { From e7614bb5019e5c7387a6b3254bc145aa16f35013 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sun, 6 Apr 2025 13:28:39 +0200 Subject: [PATCH 22/29] fix: undo my test amount --- examples/lnclient/pay_ln_address.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/lnclient/pay_ln_address.js b/examples/lnclient/pay_ln_address.js index c182a2b..8dda60e 100644 --- a/examples/lnclient/pay_ln_address.js +++ b/examples/lnclient/pay_ln_address.js @@ -14,6 +14,6 @@ rl.close(); const client = new LN(nwcUrl); console.log("Paying $1"); -const response = await client.pay("rolznz@getalby.com", USD(1000)); +const response = await client.pay("rolznz@getalby.com", USD(1.0)); console.info("Paid successfully", response); client.close(); From 5b7a4aa2a5cb85eeb975334b1f5247bba4935297 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Sun, 6 Apr 2025 14:56:11 +0200 Subject: [PATCH 23/29] Update examples/lnclient/splitter.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- examples/lnclient/splitter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/lnclient/splitter.js b/examples/lnclient/splitter.js index c5377db..6bb3110 100644 --- a/examples/lnclient/splitter.js +++ b/examples/lnclient/splitter.js @@ -29,7 +29,7 @@ console.info("Waiting for payment..."); // once the invoice got paid by the user run this callback const unsub = await request.onPaid(async () => { - // we take the sats amount from theinvocie and calculate the amount we want to forward + // we take the sats amount from the invoice and calculate the amount we want to forward const satsReceived = request.invoice.satoshi; const satsToForward = Math.floor( (satsReceived * forwardPercentage) / 100 / recipients.length, From d82169f6ee38e3ff281f2cb24c2a1aea58455219 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:45:12 +0700 Subject: [PATCH 24/29] docs: make credentials more clear Co-authored-by: Michael Bumann --- docs/lnclient.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/lnclient.md b/docs/lnclient.md index 7bb6c1f..a4783ab 100644 --- a/docs/lnclient.md +++ b/docs/lnclient.md @@ -8,6 +8,7 @@ For example, to make a payment: ```js import { LN, USD, SATS } from "@getalby/sdk"; +const credentials = "nostr+walletconnect://..."; // the NWC connection credentials await new LN(credentials).pay("lnbc..."); // pay a lightning invoice await new LN(credentials).pay("hello@getalby.com", SATS(21)); // or pay 21 sats to a lightning address await new LN(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address From 688e9b7c50b4f9e3494d11d9f917ad70bdb8c817 Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:47:49 +0700 Subject: [PATCH 25/29] docs: make credentials more clear --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d53b545..a14000b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ For example, to make a payment: ```js import { LN, USD } from "@getalby/sdk"; +const credentials = "nostr+walletconnect://..."; // the NWC connection credentials await new LN(credentials).pay("lnbc..."); // pay a lightning invoice await new LN(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address ``` From 6e390493c7a8c8dee99a061b6812eab14f47065e Mon Sep 17 00:00:00 2001 From: Roland <33993199+rolznz@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:57:20 +0700 Subject: [PATCH 26/29] feat: allow comment and payerdata in LNClient pay method (#356) * feat: allow comment and payerdata in LNClient pay method * fix: apply transaction metadata to all related transaction method types * chore: update example and lnclient docs to pay ln address with extra metadata --- docs/lnclient.md | 3 +++ examples/lnclient/pay_ln_address.js | 6 ++++-- src/lnclient/LNClient.ts | 2 ++ src/nwc/types.ts | 21 +++++++++++++++++---- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/lnclient.md b/docs/lnclient.md index a4783ab..e234b8a 100644 --- a/docs/lnclient.md +++ b/docs/lnclient.md @@ -13,6 +13,9 @@ await new LN(credentials).pay("lnbc..."); // pay a lightning invoice await new LN(credentials).pay("hello@getalby.com", SATS(21)); // or pay 21 sats to a lightning address await new LN(credentials).pay("hello@getalby.com", USD(1)); // or pay $1 USD to a lightning address await new LN(credentials).pay("hello@getalby.com", new FiatAmount(1, "THB")); // or pay an amount in any currency to a lightning address +await new LN(credentials).pay("hello@getalby.com", USD(1), { + metadata: { comment: "Example comment", payer_data: { name: "Bob" } }, +}); // set a comment for the payment you are making, and that the payment was made by Bob ``` Or to request a payment to be received: diff --git a/examples/lnclient/pay_ln_address.js b/examples/lnclient/pay_ln_address.js index 8dda60e..18e8ddd 100644 --- a/examples/lnclient/pay_ln_address.js +++ b/examples/lnclient/pay_ln_address.js @@ -13,7 +13,9 @@ const nwcUrl = rl.close(); const client = new LN(nwcUrl); -console.log("Paying $1"); -const response = await client.pay("rolznz@getalby.com", USD(1.0)); +console.info("Paying $1"); +const response = await client.pay("hello@getalby.com", USD(1.0), { + metadata: { comment: "Payment from JS SDK", payer_data: { name: "Bob" } }, +}); console.info("Paid successfully", response); client.close(); diff --git a/src/lnclient/LNClient.ts b/src/lnclient/LNClient.ts index e8837f8..c71da04 100644 --- a/src/lnclient/LNClient.ts +++ b/src/lnclient/LNClient.ts @@ -52,6 +52,8 @@ export class LNClient { await ln.fetch(); const invoiceObj = await ln.requestInvoice({ satoshi: parsedAmount.satoshi, + comment: args?.metadata?.comment, + payerdata: args?.metadata?.payer_data, }); invoice = invoiceObj.paymentRequest; } diff --git a/src/nwc/types.ts b/src/nwc/types.ts index 2af353c..70949d0 100644 --- a/src/nwc/types.ts +++ b/src/nwc/types.ts @@ -157,8 +157,21 @@ export type Nip47Transaction = { settled_at: number; created_at: number; expires_at: number; - metadata?: Record; -}; + metadata?: Nip47TransactionMetadata; +}; + +export type Nip47TransactionMetadata = { + comment?: string; // LUD-12 + payer_data?: { + email?: string; + name?: string; + pubkey?: string; + }; // LUD-18 + nostr?: { + pubkey: string; + tags: string[][]; + }; // NIP-57 +} & Record; export type Nip47NotificationType = Nip47Notification["notification_type"]; @@ -174,7 +187,7 @@ export type Nip47Notification = export type Nip47PayInvoiceRequest = { invoice: string; - metadata?: unknown; + metadata?: Nip47TransactionMetadata; amount?: number; // msats }; @@ -190,7 +203,7 @@ export type Nip47MakeInvoiceRequest = { description?: string; description_hash?: string; expiry?: number; // in seconds - metadata?: unknown; // TODO: update to also include known keys (payerData, nostr, comment) + metadata?: Nip47TransactionMetadata; }; export type Nip47LookupInvoiceRequest = { From 03c3e90ac427e0613aa0b83059558ac6f5bd5f86 Mon Sep 17 00:00:00 2001 From: Michael Bumann Date: Wed, 9 Apr 2025 19:19:26 +0200 Subject: [PATCH 27/29] docs: add some comments to the examples (#355) * docs: add some comments to the examples * docs: comments * feat: add lnclient receive timeout for ReceiveInvoice class (#357) * chore: remove unnecessary comment * chore: remove unnecessary comment --------- Co-authored-by: Roland <33993199+rolznz@users.noreply.github.com> --- docs/lnclient.md | 7 +- examples/lnclient/pay_ln_address.js | 2 +- examples/lnclient/paywall.js | 22 +++-- examples/lnclient/splitter.js | 52 ++++++----- src/lnclient/ReceiveInvoice.ts | 137 +++++++++++++++++++++------- 5 files changed, 150 insertions(+), 70 deletions(-) diff --git a/docs/lnclient.md b/docs/lnclient.md index e234b8a..a298213 100644 --- a/docs/lnclient.md +++ b/docs/lnclient.md @@ -22,8 +22,11 @@ Or to request a payment to be received: ```js const request = await new LN(credentials).receive(USD(1.0)); -// give request.invoice to someone... -request.onPaid(giveAccess); + +// give request.invoice to someone, then act upon it: +request + .onPaid(giveAccess) // listen for incoming payment and then fire the given method + .onTimeout(60, showTimeout); // if they didn't pay within 60 seconds, do something else ``` ## Examples diff --git a/examples/lnclient/pay_ln_address.js b/examples/lnclient/pay_ln_address.js index 18e8ddd..277edf9 100644 --- a/examples/lnclient/pay_ln_address.js +++ b/examples/lnclient/pay_ln_address.js @@ -18,4 +18,4 @@ const response = await client.pay("hello@getalby.com", USD(1.0), { metadata: { comment: "Payment from JS SDK", payer_data: { name: "Bob" } }, }); console.info("Paid successfully", response); -client.close(); +client.close(); // when done and no longer needed close the wallet connection diff --git a/examples/lnclient/paywall.js b/examples/lnclient/paywall.js index a94ec60..51bc7e3 100644 --- a/examples/lnclient/paywall.js +++ b/examples/lnclient/paywall.js @@ -14,22 +14,26 @@ const nwcUrl = rl.close(); const client = new LN(nwcUrl); -const request = await client.receive(USD(1.0)); +// request a lightning invoice that we show the user to pay +const request = await client.receive(USD(1.0), { description: "best content" }); qrcode.generate(request.invoice.paymentRequest, { small: true }); console.info(request.invoice.paymentRequest); +console.info("Please pay the above invoice within 60 seconds."); console.info("Waiting for payment..."); -const unsub = await request.onPaid(() => { - console.info("received payment!"); - client.close(); -}); +// once the invoice got paid by the user run this callback +request + .onPaid(() => { + console.info("received payment!"); + client.close(); // when done and no longer needed close the wallet connection + }) + .onTimeout(60, () => { + console.info("didn't receive payment in time."); + client.close(); // when done and no longer needed close the wallet connection + }); process.on("SIGINT", function () { console.info("Caught interrupt signal"); - - unsub(); - client.close(); - process.exit(); }); diff --git a/examples/lnclient/splitter.js b/examples/lnclient/splitter.js index 6bb3110..2c33cbf 100644 --- a/examples/lnclient/splitter.js +++ b/examples/lnclient/splitter.js @@ -25,36 +25,38 @@ const request = await client.receive(amount, { description: "prism payment" }); // prompt the user to pay the invoice qrcode.generate(request.invoice.paymentRequest, { small: true }); console.info(request.invoice.paymentRequest); +console.info("Please pay the above invoice within 60 seconds."); console.info("Waiting for payment..."); // once the invoice got paid by the user run this callback -const unsub = await request.onPaid(async () => { - // we take the sats amount from the invoice and calculate the amount we want to forward - const satsReceived = request.invoice.satoshi; - const satsToForward = Math.floor( - (satsReceived * forwardPercentage) / 100 / recipients.length, - ); - console.info( - `Received ${satsReceived} sats! Forwarding ${satsToForward} to the ${recipients.join(", ")}`, - ); - - // iterate over all recipients and pay them the amount - await Promise.all( - recipients.map(async (r) => { - const response = await client.pay(r, satsToForward, "splitter"); - console.info( - `Forwarded ${satsToForward} sats to ${r} (preimage: ${response.preimage})`, - ); - }), - ); - client.close(); -}); +request + .onPaid(async () => { + // we take the sats amount from theinvocie and calculate the amount we want to forward + const satsReceived = request.invoice.satoshi; + const satsToForward = Math.floor( + (satsReceived * forwardPercentage) / 100 / recipients.length, + ); + console.info( + `Received ${satsReceived} sats! Forwarding ${satsToForward} to the ${recipients.join(", ")}`, + ); + + // iterate over all recipients and pay them the amount + await Promise.all( + recipients.map(async (r) => { + const response = await client.pay(r, satsToForward, "splitter"); + console.info( + `Forwarded ${satsToForward} sats to ${r} (preimage: ${response.preimage})`, + ); + }), + ); + client.close(); // when done and no longer needed close the wallet connection + }) + .onTimeout(60, () => { + console.info("didn't receive payment in time."); + client.close(); // when done and no longer needed close the wallet connection + }); process.on("SIGINT", function () { console.info("Caught interrupt signal"); - - unsub(); - client.close(); - process.exit(); }); diff --git a/src/lnclient/ReceiveInvoice.ts b/src/lnclient/ReceiveInvoice.ts index 7959ad1..1b33d8b 100644 --- a/src/lnclient/ReceiveInvoice.ts +++ b/src/lnclient/ReceiveInvoice.ts @@ -1,10 +1,17 @@ import { Invoice } from "@getalby/lightning-tools"; import { Nip47Notification, Nip47Transaction, NWCClient } from "../nwc"; +/** + * A lightning invoice to be received by your wallet, along with utility functions, + * such as checking if the invoice was paid and acting upon it. + */ export class ReceiveInvoice { readonly transaction: Nip47Transaction; readonly invoice: Invoice; private _nwcClient: NWCClient; + private _unsubscribeFunc?: () => void; + private _timeoutFunc?: () => void; + private _timeoutId?: number | NodeJS.Timeout; constructor(nwcClient: NWCClient, transaction: Nip47Transaction) { this.transaction = transaction; @@ -12,53 +19,117 @@ export class ReceiveInvoice { this._nwcClient = nwcClient; } - async onPaid(callback: (receivedPayment: Nip47Transaction) => void) { - // TODO: is there a better way than calling getInfo here? - const info = await this._nwcClient.getInfo(); + /** + * Setup an action once the invoice has been paid. + * + * @param callback this method will be fired once we register the invoice was paid, with information of the received payment. + * @returns the current instance for method chaining e.g. add optional timeout + */ + onPaid( + callback: (receivedPayment: Nip47Transaction) => void, + ): ReceiveInvoice { + (async () => { + let supportsNotifications; + try { + // TODO: is there a better way than calling getInfo here? + const info = await this._nwcClient.getInfo(); + supportsNotifications = + info.notifications?.includes("payment_received"); + } catch (error) { + console.error("failed to fetch info, falling back to polling"); + } + + const callbackWrapper = (receivedPayment: Nip47Transaction) => { + this._unsubscribeFunc?.(); + callback(receivedPayment); + }; - if (!info.notifications?.includes("payment_received")) { - console.warn( - "current connection does not support notifications, falling back to polling", - ); - return this._onPaidPollingFallback(callback); - } + const unsubscribeWrapper = (unsubscribe: () => void) => { + return () => { + // cancel the timeout method and + this._timeoutFunc = undefined; + clearTimeout(this._timeoutId); + unsubscribe(); + }; + }; - let unsubscribeFunc = () => {}; - const onNotification = (notification: Nip47Notification) => { - if ( - notification.notification.payment_hash === this.transaction.payment_hash - ) { - unsubscribeFunc(); - callback(notification.notification); + if (!supportsNotifications) { + console.warn( + "current connection does not support notifications, falling back to polling", + ); + this._unsubscribeFunc = unsubscribeWrapper( + this._onPaidPollingFallback(callbackWrapper), + ); + } else { + const onNotification = (notification: Nip47Notification) => { + if ( + notification.notification.payment_hash === + this.transaction.payment_hash + ) { + callbackWrapper(notification.notification); + } + }; + + this._unsubscribeFunc = unsubscribeWrapper( + await this._nwcClient.subscribeNotifications(onNotification, [ + "payment_received", + ]), + ); } + })(); + + return this; + } + + /** + * Setup an action that happens if the invoice is not paid after a certain amount of time. + * + * @param seconds the number of seconds to wait for a payment + * @param callback this method will be called once the timeout is elapsed. + * @returns the current instance for method + */ + onTimeout(seconds: number, callback: () => void): ReceiveInvoice { + this._timeoutFunc = () => { + this._unsubscribeFunc?.(); + callback(); }; + this._timeoutId = setTimeout(() => { + this._timeoutFunc?.(); + }, seconds * 1000); - unsubscribeFunc = await this._nwcClient.subscribeNotifications( - onNotification, - ["payment_received"], - ); - return unsubscribeFunc; + return this; + } + + /** + * Manually unsubscribe if you no longer expect the user to pay. + * + * This is only needed if no payment was received and no timeout was configured. + */ + unsubscribe() { + this._unsubscribeFunc?.(); } - private async _onPaidPollingFallback( + private _onPaidPollingFallback( callback: (receivedPayment: Nip47Transaction) => void, ) { let subscribed = true; const unsubscribeFunc = () => { subscribed = false; }; - while (subscribed) { - const transaction = await this._nwcClient.lookupInvoice({ - payment_hash: this.transaction.payment_hash, - }); - if (transaction.settled_at && transaction.preimage) { - callback(transaction); - subscribed = false; - break; + (async () => { + while (subscribed) { + const transaction = await this._nwcClient.lookupInvoice({ + payment_hash: this.transaction.payment_hash, + }); + if (transaction.settled_at && transaction.preimage) { + callback(transaction); + subscribed = false; + break; + } + // sleep for 3 seconds per lookup attempt + await new Promise((resolve) => setTimeout(resolve, 3000)); } - // sleep for 3 seconds per lookup attempt - await new Promise((resolve) => setTimeout(resolve, 3000)); - } + })(); return unsubscribeFunc; } From f5bf0c40b9a369873d821f5bd6fc77f9cf29fb86 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 10 Apr 2025 14:21:11 +0700 Subject: [PATCH 28/29] docs: set specific websocket polyfill version --- README.md | 11 +++++++++++ docs/lnclient.md | 17 +++++++++++++++++ docs/nwc.md | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a14000b..8b4474d 100644 --- a/README.md +++ b/README.md @@ -71,8 +71,19 @@ The [Alby OAuth API](https://guides.getalby.com/alby-wallet-api/reference/gettin ### NodeJS +#### Fetch + **This library relies on a global fetch() function which will work in browsers and node v18.x or newer.** (In older versions you have to use a polyfill.) +#### Websocket polyfill + +To use this on Node.js you first must install `websocket-polyfill@0.0.3` and import it: + +```js +import "websocket-polyfill"; +// or: require('websocket-polyfill'); +``` + ## WebLN Documentation The JS SDK also has some implementations for [WebLN](https://webln.guide). diff --git a/docs/lnclient.md b/docs/lnclient.md index a298213..d66f1e3 100644 --- a/docs/lnclient.md +++ b/docs/lnclient.md @@ -32,3 +32,20 @@ request ## Examples See [the LNClient examples directory](./examples/lnclient) for a full list of examples. + +## For Node.js + +To use this on Node.js you first must install `websocket-polyfill@0.0.3` and import it: + +```js +import "websocket-polyfill"; +// or: require('websocket-polyfill'); +``` + +if you get an `crypto is not defined` error, either upgrade to node.js 20 or above, or import it manually: + +```js +import * as crypto from 'crypto'; // or 'node:crypto' +globalThis.crypto = crypto as any; +//or: global.crypto = require('crypto'); +``` diff --git a/docs/nwc.md b/docs/nwc.md index 3870620..2d806ef 100644 --- a/docs/nwc.md +++ b/docs/nwc.md @@ -153,7 +153,7 @@ Look at our [NWC React Native Expo Demo app](https://github.com/getAlby/nwc-reac #### For Node.js -To use this on Node.js you first must install `websocket-polyfill` and import it: +To use this on Node.js you first must install `websocket-polyfill@0.0.3` and import it: ```js import "websocket-polyfill"; From ced5f59050c2491cc9c418f987c1ba2301bf79a9 Mon Sep 17 00:00:00 2001 From: Roland Bewick Date: Thu, 10 Apr 2025 14:25:40 +0700 Subject: [PATCH 29/29] docs: improve NWA documentation --- docs/nwc.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/nwc.md b/docs/nwc.md index 2d806ef..13c6560 100644 --- a/docs/nwc.md +++ b/docs/nwc.md @@ -227,10 +227,27 @@ The app will generate an NWA URI which should be opened in the wallet, where the > See [NWAClient class documentation](https://getalby.github.io/js-sdk/classes/nwc.NWAClient.html) -#### Generating an NWA URI +#### Generating an NWA URI (For Client apps) -See [NWA example](examples/nwc/client/nwa.js) +```js +import { nwa } from "@getalby/sdk"; +const connectionUri = new nwa.NWAClient({ + relayUrl, + requestMethods: ["get_info"], +}).connectionUri; + +// then allow the user to copy it / display it as a QR code to the user +``` + +See full [NWA example](../examples/nwc/client/nwa.js) -### Accepting and creating a connection from an NWA URI +### Accepting and creating a connection from an NWA URI (For Wallet services) + +```js +import { nwa } from "@getalby/sdk"; +const nwaOptions = nwa.NWAClient.parseWalletAuthUrl(nwaUrl); + +// then use `nwaOptions` to display a confirmation page to the user and create a connection. +``` -See [NWA accept example](examples/nwc/client/nwa.js) for NWA URI parsing and handling. The implementation of actually creating the connection and showing a confirmation page to the user is wallet-specific. In the example, a connection will be created via the `create_connection` NWC command. +See full [NWA accept example](../examples/nwc/client/nwa-accept.js) for NWA URI parsing and handling. The implementation of actually creating the connection and showing a confirmation page to the user is wallet-specific. In the example, a connection will be created via the `create_connection` NWC command.