bug: lndhub payment state

refactor: use webln package
This commit is contained in:
2023-05-18 11:17:32 +01:00
parent 7ab8eff33a
commit c52eb38833
11 changed files with 117 additions and 120 deletions

View File

@ -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))

View File

@ -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<unknown>;
resolve(v: unknown): void;
reject(e: unknown): void;
}
const Nip7QueueDelay = 200;
const Nip7Queue: Array<Nip7QueueItem> = [];
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 <T>(then: () => Promise<T>): Promise<T> => {
return new Promise<T>((resolve, reject) => {
Nip7Queue.push({
next: then,
resolve,
reject,
});
});
};
const Nip7Queue: Array<WorkQueueItem> = [];
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) {

View File

@ -10,6 +10,10 @@ export class CashuWallet implements LNWallet {
this.#mint = mint;
}
canAutoLogin(): boolean {
return true;
}
isReady(): boolean {
return this.#wallet !== undefined;
}

View File

@ -30,6 +30,10 @@ export class LNCWallet implements LNWallet {
});
}
canAutoLogin(): boolean {
return false;
}
isReady(): boolean {
return this.#lnc.isReady;
}

View File

@ -43,6 +43,10 @@ export default class LNDHubWallet implements LNWallet {
return this.auth !== undefined;
}
canAutoLogin(): boolean {
return true;
}
close(): Promise<boolean> {
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;
}

View File

@ -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<WalletInfo>((resolve, reject) => {

View File

@ -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 <T>(then: () => Promise<T>): Promise<T> => {
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<GetInfoResponse>;
enable(): Promise<void>;
makeInvoice(args: RequestInvoiceArgs): Promise<RequestInvoiceResponse>;
signMessage(message: string): Promise<SignMessageResponse>;
verifyMessage(signature: string, message: string): Promise<void>;
sendPayment: (pr: string) => Promise<SendPaymentResponse>;
}
declare global {
interface Window {
webln?: WebLN;
}
}
const WebLNQueue: Array<WorkQueueItem> = [];
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<WalletInfo> {
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<boolean> {
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<WalletInvoice> {
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;

View File

@ -100,6 +100,7 @@ export type MilliSats = number;
export interface LNWallet {
isReady(): boolean;
canAutoLogin(): boolean;
getInfo: () => Promise<WalletInfo>;
login: (password?: string) => Promise<boolean>;
close: () => Promise<boolean>;
@ -140,8 +141,8 @@ export class WalletStore {
configs: [],
});
this.load(false);
setupWebLNWalletConfig(this);
this.snapshotState();
setupWebLNWalletConfig(this);
}
hook(fn: WalletStateHook) {

View File

@ -0,0 +1,30 @@
export interface WorkQueueItem {
next: () => Promise<unknown>;
resolve(v: unknown): void;
reject(e: unknown): void;
}
export async function processWorkQueue(queue?: Array<WorkQueueItem>, 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 <T>(queue: Array<WorkQueueItem>, then: () => Promise<T>): Promise<T> => {
return new Promise<T>((resolve, reject) => {
queue.push({
next: then,
resolve,
reject,
});
});
};