diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..f1175d32 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,39 @@ +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 + 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 + - 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 diff --git a/README.md b/README.md index dabb0fe1..8b4474db 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ ## Introduction -This JavaScript SDK for the Alby OAuth2 Wallet API and the Nostr Wallet Connect API. +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 @@ -26,525 +28,70 @@ 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.) - -## Content - -- [Nostr Wallet Connect](#nostr-wallet-connect-documentation) -- [Alby OAuth API](#oauth-api-documentation) -- [Need help?](#need-help) - -## 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. +## Lightning Network Client (LN) Documentation -### NostrWebLNProvider (aliased as NWC) Options +Quickly get started adding lightning payments to your app. -- `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 +> 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). -### Quick start example +For example, to make a payment: ```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); +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 ``` -You can use NWC as a webln compatible object in your web app: +Or to request a payment to be received: ```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 -} +const request = await new LN(credentials).receive(USD(1.0)); +// give request.invoice to someone... +request.onPaid(giveAccess); ``` -### NostrWebLNProvider Functions +[Read more](./docs/lnclient.md) -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. +For more flexibility you can access the underlying NWC wallet directly. Continue to read the Nostr Wallet Connect documentation below. -#### 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 +## Nostr Wallet Connect Documentation -##### 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 { webln } from "@getalby/sdk"; -const nwc = new webln.NostrWebLNProvider({ nostrWalletConnectUrl: loadNWCUrl }); -await nwc.enable(); -const response = await nwc.sendPayment(invoice); -console.log(response); -``` +For apps, see [NWC client and NWA client documentation](./docs/nwc.md) -#### getNostrWalletConnectUrl() +For wallet services, see [NWC wallet service documentation](./docs/nwc-wallet-service.md) -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. +## Alby Wallet API Documentation -#### fromAuthorizationUrl(url: string, {name: string}) +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! -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. +[Read more](./docs/oauth.md) -```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); -} -``` +### NodeJS -#### React Native (Expo) +#### Fetch -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. +**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.) -#### For Node.js +#### Websocket polyfill -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"; // 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. - -## 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); -``` +## WebLN Documentation -`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"; -``` +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). -## Full usage examples +## More Documentation -You can find examples in the [examples/](examples/) directory. +Read the [auto-generated documentation](https://getalby.github.io/js-sdk/modules.html) ## Need help? diff --git a/docs/lnclient.md b/docs/lnclient.md new file mode 100644 index 00000000..d66f1e35 --- /dev/null +++ b/docs/lnclient.md @@ -0,0 +1,51 @@ +# Lightning Network Client (LN) Documentation + +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 +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 +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: + +```js +const request = await new LN(credentials).receive(USD(1.0)); + +// 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 + +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-wallet-service.md b/docs/nwc-wallet-service.md new file mode 100644 index 00000000..fb15fb26 --- /dev/null +++ b/docs/nwc-wallet-service.md @@ -0,0 +1,54 @@ +# 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. + +> See [NWCWalletService class documentation](https://getalby.github.io/js-sdk/classes/nwc.NWCWalletService.html) + +## 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 new file mode 100644 index 00000000..13c6560e --- /dev/null +++ b/docs/nwc.md @@ -0,0 +1,253 @@ +# 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. + +> See [NWCClient class documentation](https://getalby.github.io/js-sdk/classes/nwc.NWCClient.html) + +## 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://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) + +### 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 + +> 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) +- `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@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'); +``` + +### 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. + +> See [NWAClient class documentation](https://getalby.github.io/js-sdk/classes/nwc.NWAClient.html) + +#### Generating an NWA URI (For Client apps) + +```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 (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 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. diff --git a/docs/oauth.md b/docs/oauth.md new file mode 100644 index 00000000..3d3c3f0f --- /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/lnclient/pay_ln_address.js b/examples/lnclient/pay_ln_address.js new file mode 100644 index 00000000..277edf94 --- /dev/null +++ b/examples/lnclient/pay_ln_address.js @@ -0,0 +1,21 @@ +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 { 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 client = new LN(nwcUrl); +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(); // when done and no longer needed close the wallet connection diff --git a/examples/lnclient/paywall.js b/examples/lnclient/paywall.js new file mode 100644 index 00000000..51bc7e38 --- /dev/null +++ b/examples/lnclient/paywall.js @@ -0,0 +1,39 @@ +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 client = new LN(nwcUrl); +// 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..."); + +// 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"); + process.exit(); +}); diff --git a/examples/lnclient/splitter.js b/examples/lnclient/splitter.js new file mode 100644 index 00000000..2c33cbf7 --- /dev/null +++ b/examples/lnclient/splitter.js @@ -0,0 +1,62 @@ +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("Please pay the above invoice within 60 seconds."); +console.info("Waiting for payment..."); + +// once the invoice got paid by the user run this callback +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"); + 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 96% rename from examples/boostagram.js rename to examples/oauth/boostagram.js index 1ce3712f..9d646c11 100644 --- a/examples/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/decode-invoice.js b/examples/oauth/decode-invoice.js similarity index 94% rename from examples/decode-invoice.js rename to examples/oauth/decode-invoice.js index 2109d518..abee6ec7 100644 --- a/examples/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/invoices.js b/examples/oauth/invoices.js similarity index 92% rename from examples/invoices.js rename to examples/oauth/invoices.js index 0e4c25c3..0b0c4558 100644 --- a/examples/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/keysends.js b/examples/oauth/keysends.js similarity index 94% rename from examples/keysends.js rename to examples/oauth/keysends.js index fd760780..a176ba4f 100644 --- a/examples/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/oauth2-public-callback_pkce_s256.mjs b/examples/oauth/oauth2-public-callback_pkce_s256.mjs similarity index 96% rename from examples/oauth2-public-callback_pkce_s256.mjs rename to examples/oauth/oauth2-public-callback_pkce_s256.mjs index f91c941d..68d00f14 100644 --- a/examples/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/send-to-ln-address.js b/examples/oauth/send-to-ln-address.js similarity index 93% rename from examples/send-to-ln-address.js rename to examples/oauth/send-to-ln-address.js index 307b46e3..5ccb1ee4 100644 --- a/examples/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/webhooks.js b/examples/oauth/webhooks.js similarity index 93% rename from examples/webhooks.js rename to examples/oauth/webhooks.js index a2cf29a5..8740d47c 100644 --- a/examples/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 e66fc05b..e871e537 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", @@ -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/index.ts b/src/index.ts index 77a94b22..cdfb584f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +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 * as nwa from "./nwc/NWAClient"; export * as nwc from "./nwc"; +export * from "./lnclient"; diff --git a/src/lnclient/Amount.test.ts b/src/lnclient/Amount.test.ts new file mode 100644 index 00000000..f12b8907 --- /dev/null +++ b/src/lnclient/Amount.test.ts @@ -0,0 +1,20 @@ +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); + 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 new file mode 100644 index 00000000..3913b4c2 --- /dev/null +++ b/src/lnclient/Amount.ts @@ -0,0 +1,26 @@ +// TODO: move to lightning tools +/** + * An amount in satoshis + */ +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 }> { + if (typeof amount === "number") { + return { + satoshi: amount, + millisat: amount * 1000, + }; + } + const satoshi = await Promise.resolve(amount.satoshi); + + return { + satoshi: satoshi, + millisat: satoshi * 1000, + }; +} diff --git a/src/lnclient/FiatAmount.test.ts b/src/lnclient/FiatAmount.test.ts new file mode 100644 index 00000000..16865628 --- /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 new file mode 100644 index 00000000..ff2ebdbe --- /dev/null +++ b/src/lnclient/FiatAmount.ts @@ -0,0 +1,19 @@ +import { fiat } from "@getalby/lightning-tools"; + +// TODO: move to Lightning Tools +export class FiatAmount { + satoshi: Promise; + constructor(amount: number, currency: string) { + this.satoshi = fiat.getSatoshiValue({ + amount, + currency, + }); + } +} + +// 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 new file mode 100644 index 00000000..c71da04f --- /dev/null +++ b/src/lnclient/LNClient.ts @@ -0,0 +1,96 @@ +import { Invoice, 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"; + +export 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. 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") { + this.nwcClient = new NWCClient({ + nostrWalletConnectUrl: credentials, + }); + } else if (credentials instanceof NWCClient) { + this.nwcClient = credentials; + } else { + this.nwcClient = new NWCClient(credentials); + } + } + + /** + * 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, + args?: Omit, + ) { + let invoice = recipient; + const parsedAmount = amount ? await resolveAmount(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, + comment: args?.metadata?.comment, + payerdata: args?.metadata?.payer_data, + }); + invoice = invoiceObj.paymentRequest; + } + + const result = await this.nwcClient.payInvoice({ + ...(args || {}), + invoice, + amount: parsedAmount?.millisat, + }); + return { + ...result, + invoice: new Invoice({ pr: invoice }), + }; + } + + /** + * 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, + ) { + const parsedAmount = await resolveAmount(amount); + const transaction = await this.nwcClient.makeInvoice({ + ...(args || {}), + amount: parsedAmount.millisat, + }); + + return new ReceiveInvoice(this.nwcClient, transaction); + } + + close() { + this.nwcClient.close(); + } +} + +export { LNClient as LN }; diff --git a/src/lnclient/ReceiveInvoice.ts b/src/lnclient/ReceiveInvoice.ts new file mode 100644 index 00000000..1b33d8b8 --- /dev/null +++ b/src/lnclient/ReceiveInvoice.ts @@ -0,0 +1,136 @@ +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; + this.invoice = new Invoice({ pr: transaction.invoice }); + this._nwcClient = nwcClient; + } + + /** + * 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); + }; + + const unsubscribeWrapper = (unsubscribe: () => void) => { + return () => { + // cancel the timeout method and + this._timeoutFunc = undefined; + clearTimeout(this._timeoutId); + unsubscribe(); + }; + }; + + 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); + + 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 _onPaidPollingFallback( + callback: (receivedPayment: Nip47Transaction) => void, + ) { + let subscribed = true; + const unsubscribeFunc = () => { + subscribed = false; + }; + (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)); + } + })(); + + return unsubscribeFunc; + } +} diff --git a/src/lnclient/index.ts b/src/lnclient/index.ts new file mode 100644 index 00000000..4c3d8b3c --- /dev/null +++ b/src/lnclient/index.ts @@ -0,0 +1,4 @@ +export * from "./LNClient"; +export * from "./ReceiveInvoice"; +export * from "./Amount"; +export * from "./FiatAmount"; diff --git a/src/nwc/NWCClient.ts b/src/nwc/NWCClient.ts index 12d38278..c9858ab2 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 { @@ -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 dce654ef..8a3ea624 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/index.ts b/src/nwc/index.ts index 959daf22..2906c574 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 53e33b57..70949d04 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) { @@ -24,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; }; @@ -144,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"]; @@ -161,7 +187,7 @@ export type Nip47Notification = export type Nip47PayInvoiceRequest = { invoice: string; - metadata?: unknown; + metadata?: Nip47TransactionMetadata; amount?: number; // msats }; @@ -177,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 = { 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 43ff59b1..c82c71b8 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 94572343..2fa69327 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 00000000..65ad59e0 --- /dev/null +++ b/src/oauth/index.ts @@ -0,0 +1,4 @@ +export * as auth from "./auth"; +export * as types from "./types"; +export * as utils from "./utils"; +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 56834665..dadaede0 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 b18290ae..935f323e 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 00000000..314fc194 --- /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 eb1245a5..da46af09 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 f6089608..829e58d3 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 @@ -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 74318af2..b2b31977 100644 --- a/src/webln/OauthWeblnProvider.ts +++ b/src/webln/OauthWeblnProvider.ts @@ -1,7 +1,7 @@ -import { Client } from "../client"; -import { OAuthClient, KeysendRequestParams } from "../types"; +import { Client } from "../oauth/client"; +import { OAuthClient, KeysendRequestParams } from "../oauth/types"; -interface RequestInvoiceArgs { +export interface RequestInvoiceArgs { amount: string | number; defaultMemo?: string; } diff --git a/yarn.lock b/yarn.lock index 0ab913f9..582aa96f 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==