From c52eb38833d4681fa797ebbc9c527dc74087ca89 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 18 May 2023 11:17:32 +0100 Subject: [PATCH] bug: lndhub payment state refactor: use webln package --- packages/app/package.json | 1 + packages/app/src/Pages/WalletPage.tsx | 4 +- packages/app/src/System/EventPublisher.ts | 49 ++------- packages/app/src/Wallet/Cashu.ts | 4 + packages/app/src/Wallet/LNCWallet.ts | 4 + packages/app/src/Wallet/LNDHub.ts | 11 ++- packages/app/src/Wallet/NostrWalletConnect.ts | 6 +- packages/app/src/Wallet/WebLN.ts | 99 +++++-------------- packages/app/src/Wallet/index.ts | 3 +- packages/app/src/WorkQueue.ts | 30 ++++++ yarn.lock | 26 +++++ 11 files changed, 117 insertions(+), 120 deletions(-) create mode 100644 packages/app/src/WorkQueue.ts diff --git a/packages/app/package.json b/packages/app/package.json index 305768eb..c05e3459 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -31,6 +31,7 @@ "react-textarea-autosize": "^8.4.0", "react-twitter-embed": "^4.0.4", "use-long-press": "^2.0.3", + "webln": "^0.3.2", "workbox-background-sync": "^6.4.2", "workbox-broadcast-update": "^6.4.2", "workbox-cacheable-response": "^6.4.2", diff --git a/packages/app/src/Pages/WalletPage.tsx b/packages/app/src/Pages/WalletPage.tsx index 17167562..e0861694 100644 --- a/packages/app/src/Pages/WalletPage.tsx +++ b/packages/app/src/Pages/WalletPage.tsx @@ -5,7 +5,7 @@ import { RouteObject, useNavigate } from "react-router-dom"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; import NoteTime from "Element/NoteTime"; -import { WalletInvoice, Sats, WalletInfo, WalletInvoiceState, useWallet, LNWallet, Wallets, WalletKind } from "Wallet"; +import { WalletInvoice, Sats, WalletInfo, WalletInvoiceState, useWallet, LNWallet, Wallets } from "Wallet"; import AsyncButton from "Element/AsyncButton"; import { unwrap } from "Util"; import { WebLNWallet } from "Wallet/WebLN"; @@ -50,7 +50,7 @@ export default function WalletPage() { if (wallet) { if (wallet.isReady()) { loadWallet(wallet).catch(console.warn); - } else if (walletState.config?.kind !== WalletKind.LNC) { + } else if (wallet.canAutoLogin()) { wallet .login() .then(async () => await loadWallet(wallet)) diff --git a/packages/app/src/System/EventPublisher.ts b/packages/app/src/System/EventPublisher.ts index 70fa3a0b..ac58686b 100644 --- a/packages/app/src/System/EventPublisher.ts +++ b/packages/app/src/System/EventPublisher.ts @@ -17,41 +17,10 @@ import { System } from "System"; import { unwrap } from "Util"; import { EventBuilder } from "./EventBuilder"; import { EventExt } from "./EventExt"; +import { barrierQueue, processWorkQueue, WorkQueueItem } from "WorkQueue"; -interface Nip7QueueItem { - next: () => Promise; - resolve(v: unknown): void; - reject(e: unknown): void; -} - -const Nip7QueueDelay = 200; -const Nip7Queue: Array = []; -async function processQueue() { - while (Nip7Queue.length > 0) { - const v = Nip7Queue.shift(); - if (v) { - try { - const ret = await v.next(); - v.resolve(ret); - } catch (e) { - v.reject(e); - } - } - } - setTimeout(processQueue, Nip7QueueDelay); -} -processQueue(); - -export const barrierNip07 = async (then: () => Promise): Promise => { - return new Promise((resolve, reject) => { - Nip7Queue.push({ - next: then, - resolve, - reject, - }); - }); -}; - +const Nip7Queue: Array = []; +processWorkQueue(Nip7Queue); export type EventBuilderHook = (ev: EventBuilder) => EventBuilder; export class EventPublisher { @@ -78,12 +47,12 @@ export class EventPublisher { async #sign(eb: EventBuilder) { if (this.#hasNip07 && !this.#privateKey) { - const nip7PubKey = await barrierNip07(() => unwrap(window.nostr).getPublicKey()); + const nip7PubKey = await barrierQueue(Nip7Queue, () => unwrap(window.nostr).getPublicKey()); if (nip7PubKey !== this.#pubKey) { throw new Error("Can't sign event, NIP-07 pubkey does not match"); } const ev = eb.build(); - return await barrierNip07(() => unwrap(window.nostr).signEvent(ev)); + return await barrierQueue(Nip7Queue, () => unwrap(window.nostr).signEvent(ev)); } else if (this.#privateKey) { return await eb.buildAndSign(this.#privateKey); } else { @@ -93,11 +62,13 @@ export class EventPublisher { async nip4Encrypt(content: string, key: HexKey) { if (this.#hasNip07 && !this.#privateKey) { - const nip7PubKey = await barrierNip07(() => unwrap(window.nostr).getPublicKey()); + const nip7PubKey = await barrierQueue(Nip7Queue, () => unwrap(window.nostr).getPublicKey()); if (nip7PubKey !== this.#pubKey) { throw new Error("Can't encrypt content, NIP-07 pubkey does not match"); } - return await barrierNip07(() => unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content)); + return await barrierQueue(Nip7Queue, () => + unwrap(window.nostr?.nip04?.encrypt).call(window.nostr?.nip04, key, content) + ); } else if (this.#privateKey) { return await EventExt.encryptData(content, key, this.#privateKey); } else { @@ -107,7 +78,7 @@ export class EventPublisher { async nip4Decrypt(content: string, otherKey: HexKey) { if (this.#hasNip07 && !this.#privateKey && window.nostr?.nip04?.decrypt) { - return await barrierNip07(() => + return await barrierQueue(Nip7Queue, () => unwrap(window.nostr?.nip04?.decrypt).call(window.nostr?.nip04, otherKey, content) ); } else if (this.#privateKey) { diff --git a/packages/app/src/Wallet/Cashu.ts b/packages/app/src/Wallet/Cashu.ts index e00c9e49..7bbf87e0 100644 --- a/packages/app/src/Wallet/Cashu.ts +++ b/packages/app/src/Wallet/Cashu.ts @@ -10,6 +10,10 @@ export class CashuWallet implements LNWallet { this.#mint = mint; } + canAutoLogin(): boolean { + return true; + } + isReady(): boolean { return this.#wallet !== undefined; } diff --git a/packages/app/src/Wallet/LNCWallet.ts b/packages/app/src/Wallet/LNCWallet.ts index da5c3ac2..fe8d8e37 100644 --- a/packages/app/src/Wallet/LNCWallet.ts +++ b/packages/app/src/Wallet/LNCWallet.ts @@ -30,6 +30,10 @@ export class LNCWallet implements LNWallet { }); } + canAutoLogin(): boolean { + return false; + } + isReady(): boolean { return this.#lnc.isReady; } diff --git a/packages/app/src/Wallet/LNDHub.ts b/packages/app/src/Wallet/LNDHub.ts index 07653744..83cb6519 100644 --- a/packages/app/src/Wallet/LNDHub.ts +++ b/packages/app/src/Wallet/LNDHub.ts @@ -43,6 +43,10 @@ export default class LNDHubWallet implements LNWallet { return this.auth !== undefined; } + canAutoLogin(): boolean { + return true; + } + close(): Promise { return Promise.resolve(true); } @@ -91,7 +95,12 @@ export default class LNDHubWallet implements LNWallet { return { pr: pr, paymentHash: pRsp.payment_hash, - state: pRsp.payment_error === undefined ? WalletInvoiceState.Paid : WalletInvoiceState.Pending, + preimage: pRsp.payment_preimage, + state: pRsp.payment_error + ? WalletInvoiceState.Failed + : pRsp.payment_preimage + ? WalletInvoiceState.Paid + : WalletInvoiceState.Pending, } as WalletInvoice; } diff --git a/packages/app/src/Wallet/NostrWalletConnect.ts b/packages/app/src/Wallet/NostrWalletConnect.ts index 34bf621b..1fd6d70d 100644 --- a/packages/app/src/Wallet/NostrWalletConnect.ts +++ b/packages/app/src/Wallet/NostrWalletConnect.ts @@ -50,10 +50,14 @@ export class NostrConnectWallet implements LNWallet { } as WalletConnectConfig; } - isReady(): boolean { + canAutoLogin(): boolean { return true; } + isReady(): boolean { + return this.#conn !== undefined; + } + async getInfo() { await this.login(); return await new Promise((resolve, reject) => { diff --git a/packages/app/src/Wallet/WebLN.ts b/packages/app/src/Wallet/WebLN.ts index 342ab301..a9770cca 100644 --- a/packages/app/src/Wallet/WebLN.ts +++ b/packages/app/src/Wallet/WebLN.ts @@ -1,3 +1,4 @@ +import { requestProvider, WebLNProvider } from "webln"; import { InvoiceRequest, LNWallet, @@ -12,77 +13,19 @@ import { WalletKind, WalletStore, } from "Wallet"; -import { delay } from "Util"; +import { unwrap } from "Util"; +import { barrierQueue, processWorkQueue, WorkQueueItem } from "WorkQueue"; -let isWebLnBusy = false; -export const barrierWebLn = async (then: () => Promise): Promise => { - while (isWebLnBusy) { - await delay(10); - } - isWebLnBusy = true; - try { - return await then(); - } finally { - isWebLnBusy = false; - } -}; - -interface SendPaymentResponse { - paymentHash?: string; - preimage: string; - route?: { - total_amt: number; - total_fees: number; - }; -} - -interface RequestInvoiceArgs { - amount?: string | number; - defaultAmount?: string | number; - minimumAmount?: string | number; - maximumAmount?: string | number; - defaultMemo?: string; -} - -interface RequestInvoiceResponse { - paymentRequest: string; -} - -interface GetInfoResponse { - node: { - alias: string; - pubkey: string; - color?: string; - }; -} - -interface SignMessageResponse { - message: string; - signature: string; -} - -interface WebLN { - enabled: boolean; - getInfo(): Promise; - enable(): Promise; - makeInvoice(args: RequestInvoiceArgs): Promise; - signMessage(message: string): Promise; - verifyMessage(signature: string, message: string): Promise; - sendPayment: (pr: string) => Promise; -} - -declare global { - interface Window { - webln?: WebLN; - } -} +const WebLNQueue: Array = []; +processWorkQueue(WebLNQueue); /** * Adds a wallet config for WebLN if detected */ -export function setupWebLNWalletConfig(store: WalletStore) { +export async function setupWebLNWalletConfig(store: WalletStore) { const wallets = store.list(); - if (window.webln && !wallets.some(a => a.kind === WalletKind.WebLN)) { + const provider = await requestProvider(); + if (provider && !wallets.some(a => a.kind === WalletKind.WebLN)) { const newConfig = { id: "webln", kind: WalletKind.WebLN, @@ -96,17 +39,20 @@ export function setupWebLNWalletConfig(store: WalletStore) { } export class WebLNWallet implements LNWallet { + #provider?: WebLNProvider; + isReady(): boolean { - if (window.webln) { - return true; - } - return false; + return this.#provider !== undefined; + } + + canAutoLogin(): boolean { + return true; } async getInfo(): Promise { await this.login(); - if (this.isReady()) { - const rsp = await barrierWebLn(async () => await window.webln?.getInfo()); + if (this.isReady() && this.#provider) { + const rsp = await barrierQueue(WebLNQueue, async () => await unwrap(this.#provider).getInfo()); if (rsp) { return { nodePubKey: rsp.node.pubkey, @@ -120,8 +66,8 @@ export class WebLNWallet implements LNWallet { } async login(): Promise { - if (window.webln && !window.webln.enabled) { - await window.webln.enable(); + if (this.#provider === undefined) { + this.#provider = await requestProvider(); } return true; } @@ -137,9 +83,10 @@ export class WebLNWallet implements LNWallet { async createInvoice(req: InvoiceRequest): Promise { await this.login(); if (this.isReady()) { - const rsp = await barrierWebLn( + const rsp = await barrierQueue( + WebLNQueue, async () => - await window.webln?.makeInvoice({ + await unwrap(this.#provider).makeInvoice({ amount: req.amount, defaultMemo: req.memo, }) @@ -162,7 +109,7 @@ export class WebLNWallet implements LNWallet { if (!invoice) { throw new WalletError(WalletErrorCode.InvalidInvoice, "Could not parse invoice"); } - const rsp = await barrierWebLn(async () => await window.webln?.sendPayment(pr)); + const rsp = await barrierQueue(WebLNQueue, async () => await unwrap(this.#provider).sendPayment(pr)); if (rsp) { invoice.state = WalletInvoiceState.Paid; invoice.preimage = rsp.preimage; diff --git a/packages/app/src/Wallet/index.ts b/packages/app/src/Wallet/index.ts index ec59fd56..f3078715 100644 --- a/packages/app/src/Wallet/index.ts +++ b/packages/app/src/Wallet/index.ts @@ -100,6 +100,7 @@ export type MilliSats = number; export interface LNWallet { isReady(): boolean; + canAutoLogin(): boolean; getInfo: () => Promise; login: (password?: string) => Promise; close: () => Promise; @@ -140,8 +141,8 @@ export class WalletStore { configs: [], }); this.load(false); - setupWebLNWalletConfig(this); this.snapshotState(); + setupWebLNWalletConfig(this); } hook(fn: WalletStateHook) { diff --git a/packages/app/src/WorkQueue.ts b/packages/app/src/WorkQueue.ts new file mode 100644 index 00000000..37df22e0 --- /dev/null +++ b/packages/app/src/WorkQueue.ts @@ -0,0 +1,30 @@ +export interface WorkQueueItem { + next: () => Promise; + resolve(v: unknown): void; + reject(e: unknown): void; +} + +export async function processWorkQueue(queue?: Array, queueDelay = 200) { + while (queue && queue.length > 0) { + const v = queue.shift(); + if (v) { + try { + const ret = await v.next(); + v.resolve(ret); + } catch (e) { + v.reject(e); + } + } + } + setTimeout(() => processWorkQueue(queue, queueDelay), queueDelay); +} + +export const barrierQueue = async (queue: Array, then: () => Promise): Promise => { + return new Promise((resolve, reject) => { + queue.push({ + next: then, + resolve, + reject, + }); + }); +}; diff --git a/yarn.lock b/yarn.lock index f1287aa0..665c731d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2119,6 +2119,13 @@ resolved "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz" integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== +"@types/chrome@^0.0.74": + version "0.0.74" + resolved "https://registry.yarnpkg.com/@types/chrome/-/chrome-0.0.74.tgz#f69827c48fcf7fecc90c96089807661749a5a5e3" + integrity sha512-hzosS5CkQcIKCgxcsV2AzbJ36KNxG/Db2YEN/erEu7Boprg+KpMDLBQqKFmSo+JkQMGqRcicUyqCowJpuT+C6A== + dependencies: + "@types/filesystem" "*" + "@types/connect-history-api-fallback@^1.3.5": version "1.3.5" resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz" @@ -2206,6 +2213,18 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/filesystem@*": + version "0.0.32" + resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.32.tgz#307df7cc084a2293c3c1a31151b178063e0a8edf" + integrity sha512-Yuf4jR5YYMR2DVgwuCiP11s0xuVRyPKmz8vo6HBY3CGdeMj8af93CFZX+T82+VD1+UqHOxTq31lO7MI7lepBtQ== + dependencies: + "@types/filewriter" "*" + +"@types/filewriter@*": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.29.tgz#a48795ecadf957f6c0d10e0c34af86c098fa5bee" + integrity sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ== + "@types/hoist-non-react-statics@^3.3.1": version "3.3.1" resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" @@ -9153,6 +9172,13 @@ webidl-conversions@^4.0.2: resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz" integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== +webln@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/webln/-/webln-0.3.2.tgz#bbadf52916666b6059e3661ef5ab73a76b7cd0f4" + integrity sha512-YYT83aOCLup2AmqvJdKtdeBTaZpjC6/JDMe8o6x1kbTYWwiwrtWHyO//PAsPixF3jwFsAkj5DmiceB6w/QSe7Q== + dependencies: + "@types/chrome" "^0.0.74" + webpack-bundle-analyzer@^4.8.0: version "4.8.0" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.8.0.tgz#951b8aaf491f665d2ae325d8b84da229157b1d04"