diff --git a/README.md b/README.md index 3bcc11e..b3c9061 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,19 @@ yarn add @getalby/lightning-tools or for use without any build tools: -``` -// lightning-tools now available at window.lightningTools - +```html + ``` **This library relies on a global `fetch()` function which will work in [browsers](https://caniuse.com/?search=fetch) and node v18 or newer.** (In older versions you have to use a polyfill.) @@ -179,7 +189,7 @@ import { fetchWithL402 } from "@getalby/lightning-tools"; await fetchWithL402( "https://lsat-weather-api.getalby.repl.co/kigali", {}, - { store: window.localStorage }, + { store: window.localStorage } ) .then((res) => res.json()) .then(console.log); @@ -198,7 +208,7 @@ const nwc = new webln.NostrWebLNProvider({ await fetchWithL402( "https://lsat-weather-api.getalby.repl.co/kigali", {}, - { webln: nwc }, + { webln: nwc } ) .then((res) => res.json()) .then(console.log); @@ -211,7 +221,7 @@ import { l402 } from "@getalby/lightning-tools"; await l402.fetchWithL402( "https://lsat-weather-api.getalby.repl.co/kigali", {}, - { store: new l402.storage.NoStorage() }, + { store: new l402.storage.NoStorage() } ); ``` @@ -262,13 +272,25 @@ This library uses a [proxy](https://github.com/getAlby/lightning-address-details You can disable the proxy by explicitly setting the proxy to false when initializing a lightning address: +```js +const lightningAddress = new LightningAddress("hello@getalby.com", { + proxy: false, +}); ``` -const lightningAddress = new LightningAddress("hello@getalby.com", {proxy: false}); + +## crypto dependency + +If you get an `crypto is not defined` in NodeJS error you have to import it first: + +```js +import * as crypto from 'crypto'; // or 'node:crypto' +globalThis.crypto = crypto as any; +//or: global.crypto = require('crypto'); ``` ## fetch() dependency -This library relies on a global fetch object which will work in browsers and node v18.x or newer. In old version yoi can manually install a global fetch option or polyfill if needed. +This library relies on a global fetch object which will work in browsers and node v18.x or newer. In old version you can manually install a global fetch option or polyfill if needed. For example: diff --git a/examples/request-invoice.js b/examples/request-invoice.js new file mode 100644 index 0000000..7d2f60f --- /dev/null +++ b/examples/request-invoice.js @@ -0,0 +1,13 @@ +import * as crypto from "crypto"; // or 'node:crypto' +global.crypto = crypto; +import { LightningAddress } from "@getalby/lightning-tools"; + +const ln = new LightningAddress("hello@getalby.com"); + +await ln.fetch(); +// request an invoice for 1000 satoshis +// this returns a new `Invoice` class that can also be used to validate the payment +const invoice = await ln.requestInvoice({ satoshi: 1000 }); + +console.log(invoice.paymentRequest); // print the payment request +console.log(invoice.paymentHash); // print the payment hash diff --git a/package.json b/package.json index 97fa43d..363fd88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@getalby/lightning-tools", - "version": "4.2.1", + "version": "5.0.0", "description": "Collection of helpful building blocks and tools to develop Bitcoin Lightning web apps", "type": "module", "source": "src/index.ts", @@ -36,21 +36,16 @@ "tsc:compile": "tsc --noEmit", "format": "prettier --check '**/*.(md|json)' 'src/**/*.(js|ts)' 'examples/**/*.js'", "format:fix": "prettier --loglevel silent --write '**/*.(md|json)' 'src/**/*.(js|ts)' 'examples/**/*.js'", - "build:browser": "cp src/window.js dist && browserify dist/window.js > dist/index.browser.js", "test": "jest", "clean": "rm -rf dist", - "build": "microbundle && yarn build:browser", + "build": "microbundle --no-sourcemap", "dev": "microbundle watch", "prepare": "husky install" }, - "dependencies": { - "crypto-js": "^4.1.1", - "light-bolt11-decoder": "^3.0.0" - }, + "dependencies": {}, "devDependencies": { "@commitlint/cli": "^17.7.1", "@commitlint/config-conventional": "^17.7.0", - "@types/crypto-js": "^4.1.1", "@types/jest": "^29.4.0", "@types/node": "^20.8.2", "@typescript-eslint/eslint-plugin": "^6.4.0", @@ -62,6 +57,7 @@ "husky": "^8.0.3", "jest": "^29.5.0", "jest-fetch-mock": "^3.0.3", + "light-bolt11-decoder": "^3.0.0", "lint-staged": "^14.0.0", "microbundle": "^0.15.1", "nostr-tools": "^1.17.0", diff --git a/setupJest.ts b/setupJest.ts index 79e0bb2..1d5b38c 100644 --- a/setupJest.ts +++ b/setupJest.ts @@ -1,3 +1,3 @@ -import jestFetchMock from 'jest-fetch-mock'; +import jestFetchMock from "jest-fetch-mock"; -jestFetchMock.enableMocks(); \ No newline at end of file +jestFetchMock.enableMocks(); diff --git a/src/invoice.test.ts b/src/invoice.test.ts index b20d9a8..adbc249 100644 --- a/src/invoice.test.ts +++ b/src/invoice.test.ts @@ -27,4 +27,15 @@ describe("Invoice", () => { const decodedInvoice = new Invoice({ pr: paymentRequestWithMemo }); expect(decodedInvoice.description).toBe("Test memo"); }); + + test("validate preimage", async () => { + const decodedInvoice = new Invoice({ + pr: "lnbc120n1p3ecwp5pp5z8n0tzytydn57x6q0kqgfearewkx6kdh90svrkrc64azwy9jpnfqdq4f35kw6r5wdshgueqw35hqcqzpgxqyz5vqsp535pwwk083jvpnf87nl3mr4ext8q5f576s57cds72nvu7fpr037nq9qyyssqtq40wszjzs0vpaka2uckjf4xs2fu24f4vp9eev8r230m6epcp2kxdg8xztlw89p2kzkdpadujuflv6f8avgw3jhnvcxjkegdtydd95sp8hwns5", + }); + expect( + await decodedInvoice.validatePreimage( + "dedbef581d83342848d99c02519053f01856add237f94437bc9bbec7bd6f6e55", + ), + ).toBe(true); + }); }); diff --git a/src/invoice.ts b/src/invoice.ts index 8ad900f..ae8e4dc 100644 --- a/src/invoice.ts +++ b/src/invoice.ts @@ -1,7 +1,7 @@ import { decodeInvoice } from "./utils/invoice"; -import Hex from "crypto-js/enc-hex.js"; -import sha256 from "crypto-js/sha256.js"; import { InvoiceArgs } from "./types"; +import { sha256 } from "./utils/sha256"; +import { fromHexString } from "./utils/hex"; export default class Invoice { paymentRequest: string; @@ -44,11 +44,11 @@ export default class Invoice { } } - validatePreimage(preimage: string): boolean { + async validatePreimage(preimage: string): Promise { if (!preimage || !this.paymentHash) return false; try { - const preimageHash = sha256(Hex.parse(preimage)).toString(Hex); + const preimageHash = await sha256(fromHexString(preimage)); return this.paymentHash === preimageHash; } catch { return false; diff --git a/src/lightning-address.ts b/src/lightning-address.ts index 3d019c4..3409af7 100644 --- a/src/lightning-address.ts +++ b/src/lightning-address.ts @@ -75,7 +75,7 @@ export default class LightningAddress { ); const json = await result.json(); - this.parseResponse(json.lnurlp, json.keysend, json.nostr); + await this.parseResponse(json.lnurlp, json.keysend, json.nostr); } async fetchWithoutProxy() { @@ -99,7 +99,7 @@ export default class LightningAddress { nostrData = await nostrResult.json(); } - this.parseResponse(lnurlData, keysendData, nostrData); + await this.parseResponse(lnurlData, keysendData, nostrData); } lnurlpUrl() { @@ -249,13 +249,13 @@ export default class LightningAddress { return response; } - private parseResponse( + private async parseResponse( lnurlpData: LnUrlRawData | undefined, keysendData: KeySendRawData | undefined, nostrData: NostrResponse | undefined, ) { if (lnurlpData) { - this.lnurlpData = parseLnUrlPayResponse(lnurlpData); + this.lnurlpData = await parseLnUrlPayResponse(lnurlpData); } if (keysendData) { this.keysendData = parseKeysendResponse(keysendData); diff --git a/src/utils/hex.ts b/src/utils/hex.ts new file mode 100644 index 0000000..2151dd1 --- /dev/null +++ b/src/utils/hex.ts @@ -0,0 +1,5 @@ +// from https://stackoverflow.com/a/50868276 +export const fromHexString = (hexString: string) => + Uint8Array.from( + hexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)), + ); diff --git a/src/utils/lnurl.test.ts b/src/utils/lnurl.test.ts index ca94013..632a2eb 100644 --- a/src/utils/lnurl.test.ts +++ b/src/utils/lnurl.test.ts @@ -27,7 +27,7 @@ describe("isUrl", () => { }); describe("parseLnUrlPayResponse", () => { - test("min/max must be in millisats", () => { + test("min/max must be in millisats", async () => { const response = { status: "OK", tag: "payRequest", @@ -42,7 +42,7 @@ describe("parseLnUrlPayResponse", () => { "79f00d3f5a19ec806189fcab03c1be4ff81d18ee4f653c88fac41fe03570f432", allowsNostr: true, }; - const parsed = parseLnUrlPayResponse(response); + const parsed = await parseLnUrlPayResponse(response); expect(parsed.min).toBe(1000); expect(parsed.max).toBe(11000000000); }); diff --git a/src/utils/lnurl.ts b/src/utils/lnurl.ts index 47e5b91..0fcc2f2 100644 --- a/src/utils/lnurl.ts +++ b/src/utils/lnurl.ts @@ -1,11 +1,9 @@ -import Hex from "crypto-js/enc-hex.js"; -import sha256 from "crypto-js/sha256.js"; - import type { LUD18ServicePayerData, LnUrlPayResponse, LnUrlRawData, } from "../types"; +import { sha256 } from "./sha256"; const URL_REGEX = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/; @@ -30,7 +28,9 @@ export const isValidAmount = ({ const TAG_PAY_REQUEST = "payRequest"; // From: https://github.com/dolcalmi/lnurl-pay/blob/main/src/request-pay-service-params.ts -export const parseLnUrlPayResponse = (data: LnUrlRawData): LnUrlPayResponse => { +export const parseLnUrlPayResponse = async ( + data: LnUrlRawData, +): Promise => { if (data.tag !== TAG_PAY_REQUEST) throw new Error("Invalid pay service params"); @@ -45,10 +45,10 @@ export const parseLnUrlPayResponse = (data: LnUrlRawData): LnUrlPayResponse => { let metadataHash: string; try { metadata = JSON.parse(data.metadata + ""); - metadataHash = sha256(data.metadata + "").toString(Hex); + metadataHash = await sha256(data.metadata + ""); } catch { metadata = []; - metadataHash = sha256("[]").toString(Hex); + metadataHash = await sha256("[]"); } let image = ""; diff --git a/src/utils/nostr.ts b/src/utils/nostr.ts index f806b60..97cf0e7 100644 --- a/src/utils/nostr.ts +++ b/src/utils/nostr.ts @@ -1,6 +1,5 @@ -import Hex from "crypto-js/enc-hex.js"; -import sha256 from "crypto-js/sha256.js"; import { Event, NostrResponse, ZapArgs, ZapOptions } from "../types"; +import { sha256 } from "./sha256"; export async function generateZapEvent( { satoshi, comment, p, e, relays }: ZapArgs, @@ -32,7 +31,7 @@ export async function generateZapEvent( content: comment ?? "", }; - nostrEvent.id = getEventHash(nostrEvent); + nostrEvent.id = await getEventHash(nostrEvent); return await nostr.signEvent(nostrEvent); } @@ -69,8 +68,8 @@ export function serializeEvent(evt: Event): string { ]); } -export function getEventHash(event: Event): string { - return sha256(serializeEvent(event)).toString(Hex); +export function getEventHash(event: Event): Promise { + return sha256(serializeEvent(event)); } export function parseNostrResponse( diff --git a/src/utils/sha256.ts b/src/utils/sha256.ts new file mode 100644 index 0000000..83c9753 --- /dev/null +++ b/src/utils/sha256.ts @@ -0,0 +1,18 @@ +// from https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest +export async function sha256(message: string | Uint8Array) { + // encode as UTF-8 + const msgBuffer = + typeof message === "string" ? new TextEncoder().encode(message) : message; + + // hash the message + const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); + + // convert ArrayBuffer to Array + const hashArray = Array.from(new Uint8Array(hashBuffer)); + + // convert bytes to hex string + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + return hashHex; +} diff --git a/src/window.js b/src/window.js deleted file mode 100644 index 915186d..0000000 --- a/src/window.js +++ /dev/null @@ -1,3 +0,0 @@ -// assign alby-tools exports to global window object (for index.browser.js) -// @ts-ignore this file is created at build time -window["lightningTools"] = require("./index.cjs"); diff --git a/tsconfig.json b/tsconfig.json index 576273a..0bd1179 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,13 +6,14 @@ "skipLibCheck": true, "checkJs": true, "allowJs": true, - "declarationMap": true, + "declarationMap": false, "declaration": true, "allowSyntheticDefaultImports": true, "target": "es2020", "rootDir": "./src", + "module": "ESNext", "moduleResolution": "node", - "sourceMap": true, + "sourceMap": false, "noImplicitAny": false }, "include": ["src/**/*"], diff --git a/yarn.lock b/yarn.lock index 0d94405..1dcf19d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1899,11 +1899,6 @@ dependencies: "@babel/types" "^7.3.0" -"@types/crypto-js@^4.1.1": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d" - integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA== - "@types/estree@*": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" @@ -3066,11 +3061,6 @@ crypto-browserify@^3.0.0: randombytes "^2.0.0" randomfill "^1.0.3" -crypto-js@^4.1.1: - version "4.2.0" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" - integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== - css-declaration-sorter@^6.3.0: version "6.3.1" resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz#be5e1d71b7a992433fb1c542c7a1b835e45682ec"