complete basic wallet
This commit is contained in:
@ -1,6 +1,25 @@
|
||||
import { InvoiceRequest, LNWallet, Login, WalletError, WalletInfo, WalletInvoice, WalletInvoiceState } from "Wallet";
|
||||
|
||||
import LNC from "@lightninglabs/lnc-web";
|
||||
import { unwrap } from "Util";
|
||||
import {
|
||||
InvoiceRequest,
|
||||
LNWallet,
|
||||
Login,
|
||||
prToWalletInvoice,
|
||||
WalletError,
|
||||
WalletErrorCode,
|
||||
WalletInfo,
|
||||
WalletInvoice,
|
||||
WalletInvoiceState,
|
||||
} from "Wallet";
|
||||
|
||||
enum Payment_PaymentStatus {
|
||||
UNKNOWN = "UNKNOWN",
|
||||
IN_FLIGHT = "IN_FLIGHT",
|
||||
SUCCEEDED = "SUCCEEDED",
|
||||
FAILED = "FAILED",
|
||||
UNRECOGNIZED = "UNRECOGNIZED",
|
||||
}
|
||||
|
||||
export class LNCWallet implements LNWallet {
|
||||
#lnc: LNC;
|
||||
|
||||
@ -11,23 +30,32 @@ export class LNCWallet implements LNWallet {
|
||||
});
|
||||
}
|
||||
|
||||
static async Initialize(pairingPhrase: string, password: string) {
|
||||
const lnc = new LNCWallet(pairingPhrase, password);
|
||||
isReady(): boolean {
|
||||
return this.#lnc.isReady;
|
||||
}
|
||||
|
||||
static async Initialize(pairingPhrase: string) {
|
||||
const lnc = new LNCWallet(pairingPhrase);
|
||||
await lnc.login();
|
||||
return lnc;
|
||||
}
|
||||
|
||||
static async Connect(password: string) {
|
||||
const lnc = new LNCWallet(undefined, password);
|
||||
await lnc.login();
|
||||
return lnc;
|
||||
static Empty() {
|
||||
return new LNCWallet();
|
||||
}
|
||||
|
||||
setPassword(pw: string) {
|
||||
if (this.#lnc.credentials.password && pw !== this.#lnc.credentials.password) {
|
||||
throw new WalletError(WalletErrorCode.GeneralError, "Password is already set, cannot update password");
|
||||
}
|
||||
this.#lnc.credentials.password = pw;
|
||||
}
|
||||
|
||||
createAccount(): Promise<WalletError | Login> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
|
||||
async getInfo(): Promise<WalletInfo | WalletError> {
|
||||
async getInfo(): Promise<WalletInfo> {
|
||||
const nodeInfo = await this.#lnc.lnd.lightning.getInfo();
|
||||
return {
|
||||
nodePubKey: nodeInfo.identityPubkey,
|
||||
@ -35,14 +63,18 @@ export class LNCWallet implements LNWallet {
|
||||
} as WalletInfo;
|
||||
}
|
||||
|
||||
close(): Promise<boolean | WalletError> {
|
||||
close(): Promise<boolean> {
|
||||
if (this.#lnc.isConnected) {
|
||||
this.#lnc.disconnect();
|
||||
}
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
async login(): Promise<boolean | WalletError> {
|
||||
async login(password?: string): Promise<boolean> {
|
||||
if (password) {
|
||||
this.setPassword(password);
|
||||
this.#lnc.run();
|
||||
}
|
||||
await this.#lnc.connect();
|
||||
while (!this.#lnc.isConnected) {
|
||||
await new Promise(resolve => {
|
||||
@ -52,32 +84,68 @@ export class LNCWallet implements LNWallet {
|
||||
return true;
|
||||
}
|
||||
|
||||
async getBalance(): Promise<number | WalletError> {
|
||||
async getBalance(): Promise<number> {
|
||||
const rsp = await this.#lnc.lnd.lightning.channelBalance();
|
||||
console.debug(rsp);
|
||||
return parseInt(rsp.localBalance?.sat ?? "0");
|
||||
}
|
||||
|
||||
createInvoice(req: InvoiceRequest): Promise<WalletInvoice | WalletError> {
|
||||
throw new Error("Not implemented");
|
||||
async createInvoice(req: InvoiceRequest): Promise<WalletInvoice> {
|
||||
const rsp = await this.#lnc.lnd.lightning.addInvoice({
|
||||
memo: req.memo,
|
||||
value: req.amount.toString(),
|
||||
expiry: req.expiry?.toString(),
|
||||
});
|
||||
return unwrap(prToWalletInvoice(rsp.paymentRequest));
|
||||
}
|
||||
|
||||
payInvoice(pr: string): Promise<WalletInvoice | WalletError> {
|
||||
throw new Error("Not implemented");
|
||||
async payInvoice(pr: string): Promise<WalletInvoice> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#lnc.lnd.router.sendPaymentV2(
|
||||
{
|
||||
paymentRequest: pr,
|
||||
timeoutSeconds: 60,
|
||||
feeLimitSat: "100",
|
||||
},
|
||||
msg => {
|
||||
console.debug(msg);
|
||||
if (msg.status === Payment_PaymentStatus.SUCCEEDED) {
|
||||
resolve({
|
||||
preimage: msg.paymentPreimage,
|
||||
state: WalletInvoiceState.Paid,
|
||||
timestamp: parseInt(msg.creationTimeNs) / 1e9,
|
||||
} as WalletInvoice);
|
||||
}
|
||||
},
|
||||
err => {
|
||||
console.debug(err);
|
||||
reject(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async getInvoices(): Promise<WalletInvoice[] | WalletError> {
|
||||
async getInvoices(): Promise<WalletInvoice[]> {
|
||||
const invoices = await this.#lnc.lnd.lightning.listPayments({
|
||||
includeIncomplete: true,
|
||||
maxPayments: "10",
|
||||
reversed: true,
|
||||
});
|
||||
|
||||
return invoices.payments.map(a => {
|
||||
const parsedInvoice = prToWalletInvoice(a.paymentRequest);
|
||||
return {
|
||||
amount: parseInt(a.valueSat),
|
||||
state: a.status === "SUCCEEDED" ? WalletInvoiceState.Paid : WalletInvoiceState.Pending,
|
||||
timestamp: parseInt(a.creationTimeNs) / 1e9,
|
||||
...parsedInvoice,
|
||||
state: (() => {
|
||||
switch (a.status) {
|
||||
case Payment_PaymentStatus.SUCCEEDED:
|
||||
return;
|
||||
case Payment_PaymentStatus.FAILED:
|
||||
return WalletInvoiceState.Failed;
|
||||
default:
|
||||
return WalletInvoiceState.Pending;
|
||||
}
|
||||
})(),
|
||||
preimage: a.paymentPreimage,
|
||||
} as WalletInvoice;
|
||||
});
|
||||
}
|
||||
|
@ -2,9 +2,10 @@ import { EventPublisher } from "Feed/EventPublisher";
|
||||
import {
|
||||
InvoiceRequest,
|
||||
LNWallet,
|
||||
prToWalletInvoice,
|
||||
Sats,
|
||||
UnknownWalletError,
|
||||
WalletError,
|
||||
WalletErrorCode,
|
||||
WalletInfo,
|
||||
WalletInvoice,
|
||||
WalletInvoiceState,
|
||||
@ -47,12 +48,12 @@ export default class LNDHubWallet implements LNWallet {
|
||||
}
|
||||
}
|
||||
|
||||
close(): Promise<boolean | WalletError> {
|
||||
throw new Error("Not implemented");
|
||||
isReady(): boolean {
|
||||
return this.auth !== undefined;
|
||||
}
|
||||
|
||||
async createAccount() {
|
||||
return Promise.resolve(UnknownWalletError);
|
||||
close(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
async getInfo() {
|
||||
@ -66,19 +67,12 @@ export default class LNDHubWallet implements LNWallet {
|
||||
login: this.user,
|
||||
password: this.password,
|
||||
});
|
||||
|
||||
if ("error" in rsp) {
|
||||
return rsp as WalletError;
|
||||
}
|
||||
this.auth = rsp as AuthResponse;
|
||||
return true;
|
||||
}
|
||||
|
||||
async getBalance(): Promise<Sats | WalletError> {
|
||||
async getBalance(): Promise<Sats> {
|
||||
const rsp = await this.getJson<GetBalanceResponse>("GET", "/balance");
|
||||
if ("error" in rsp) {
|
||||
return rsp as WalletError;
|
||||
}
|
||||
const bal = Math.floor((rsp as GetBalanceResponse).BTC.AvailableBalance);
|
||||
return bal as Sats;
|
||||
}
|
||||
@ -88,9 +82,6 @@ export default class LNDHubWallet implements LNWallet {
|
||||
amt: req.amount,
|
||||
memo: req.memo,
|
||||
});
|
||||
if ("error" in rsp) {
|
||||
return rsp as WalletError;
|
||||
}
|
||||
|
||||
const pRsp = rsp as UserInvoicesResponse;
|
||||
return {
|
||||
@ -107,10 +98,6 @@ export default class LNDHubWallet implements LNWallet {
|
||||
invoice: pr,
|
||||
});
|
||||
|
||||
if ("error" in rsp) {
|
||||
return rsp as WalletError;
|
||||
}
|
||||
|
||||
const pRsp = rsp as PayInvoiceResponse;
|
||||
return {
|
||||
pr: pr,
|
||||
@ -119,24 +106,23 @@ export default class LNDHubWallet implements LNWallet {
|
||||
} as WalletInvoice;
|
||||
}
|
||||
|
||||
async getInvoices(): Promise<WalletInvoice[] | WalletError> {
|
||||
async getInvoices(): Promise<WalletInvoice[]> {
|
||||
const rsp = await this.getJson<UserInvoicesResponse[]>("GET", "/getuserinvoices");
|
||||
if ("error" in rsp) {
|
||||
return rsp as WalletError;
|
||||
}
|
||||
return (rsp as UserInvoicesResponse[]).map(a => {
|
||||
const decodedInvoice = prToWalletInvoice(a.payment_request);
|
||||
if (!decodedInvoice) {
|
||||
throw new WalletError(WalletErrorCode.InvalidInvoice, "Failed to parse invoice");
|
||||
}
|
||||
return {
|
||||
memo: a.description,
|
||||
amount: Math.floor(a.amt),
|
||||
timestamp: a.timestamp,
|
||||
state: a.ispaid ? WalletInvoiceState.Paid : WalletInvoiceState.Pending,
|
||||
pr: a.payment_request,
|
||||
...decodedInvoice,
|
||||
state: a.ispaid ? WalletInvoiceState.Paid : decodedInvoice.state,
|
||||
paymentHash: a.payment_hash,
|
||||
memo: a.description,
|
||||
} as WalletInvoice;
|
||||
});
|
||||
}
|
||||
|
||||
private async getJson<T>(method: "GET" | "POST", path: string, body?: any): Promise<T | WalletError> {
|
||||
private async getJson<T>(method: "GET" | "POST", path: string, body?: unknown): Promise<T> {
|
||||
let auth = `Bearer ${this.auth?.access_token}`;
|
||||
if (this.type === "snort") {
|
||||
const ev = await this.publisher?.generic(`${new URL(this.url).pathname}${path}`, 30_000);
|
||||
@ -152,7 +138,8 @@ export default class LNDHubWallet implements LNWallet {
|
||||
});
|
||||
const json = await rsp.json();
|
||||
if ("error" in json) {
|
||||
return json as WalletError;
|
||||
const err = json as ErrorResponse;
|
||||
throw new WalletError(err.code, err.message);
|
||||
}
|
||||
return json as T;
|
||||
}
|
||||
@ -188,3 +175,8 @@ interface PayInvoiceResponse {
|
||||
payment_preimage: string;
|
||||
payment_route?: { total_amt: number; total_fees: number };
|
||||
}
|
||||
|
||||
interface ErrorResponse {
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
||||
|
181
packages/app/src/Wallet/WebLN.ts
Normal file
181
packages/app/src/Wallet/WebLN.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import {
|
||||
InvoiceRequest,
|
||||
LNWallet,
|
||||
prToWalletInvoice,
|
||||
Sats,
|
||||
WalletConfig,
|
||||
WalletError,
|
||||
WalletErrorCode,
|
||||
WalletInfo,
|
||||
WalletInvoice,
|
||||
WalletInvoiceState,
|
||||
WalletKind,
|
||||
WalletStore,
|
||||
} from "Wallet";
|
||||
import { delay } from "Util";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a wallet config for WebLN if detected
|
||||
*/
|
||||
export function setupWebLNWalletConfig(store: WalletStore) {
|
||||
const wallets = store.list();
|
||||
if (window.webln && !wallets.some(a => a.kind === WalletKind.WebLN)) {
|
||||
const newConfig = {
|
||||
id: "webln",
|
||||
kind: WalletKind.WebLN,
|
||||
active: wallets.length === 0,
|
||||
info: {
|
||||
alias: "WebLN",
|
||||
},
|
||||
} as WalletConfig;
|
||||
store.add(newConfig);
|
||||
}
|
||||
}
|
||||
|
||||
export class WebLNWallet implements LNWallet {
|
||||
isReady(): boolean {
|
||||
if (window.webln) {
|
||||
return true;
|
||||
}
|
||||
throw new WalletError(WalletErrorCode.GeneralError, "WebLN not available");
|
||||
}
|
||||
|
||||
async getInfo(): Promise<WalletInfo> {
|
||||
await this.login();
|
||||
if (this.isReady()) {
|
||||
const rsp = await barrierWebLn(async () => await window.webln?.getInfo());
|
||||
if (rsp) {
|
||||
return {
|
||||
nodePubKey: rsp.node.pubkey,
|
||||
alias: rsp.node.alias,
|
||||
} as WalletInfo;
|
||||
} else {
|
||||
throw new WalletError(WalletErrorCode.GeneralError, "Could not load wallet info");
|
||||
}
|
||||
}
|
||||
throw new WalletError(WalletErrorCode.GeneralError, "WebLN not available");
|
||||
}
|
||||
|
||||
async login(): Promise<boolean> {
|
||||
if (window.webln && !window.webln.enabled) {
|
||||
await window.webln.enable();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
close(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
getBalance(): Promise<Sats> {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
async createInvoice(req: InvoiceRequest): Promise<WalletInvoice> {
|
||||
await this.login();
|
||||
if (this.isReady()) {
|
||||
const rsp = await barrierWebLn(
|
||||
async () =>
|
||||
await window.webln?.makeInvoice({
|
||||
amount: req.amount,
|
||||
defaultMemo: req.memo,
|
||||
})
|
||||
);
|
||||
if (rsp) {
|
||||
const invoice = prToWalletInvoice(rsp.paymentRequest);
|
||||
if (!invoice) {
|
||||
throw new WalletError(WalletErrorCode.InvalidInvoice, "Could not parse invoice");
|
||||
}
|
||||
return invoice;
|
||||
}
|
||||
}
|
||||
throw new WalletError(WalletErrorCode.GeneralError, "WebLN not available");
|
||||
}
|
||||
|
||||
async payInvoice(pr: string): Promise<WalletInvoice> {
|
||||
await this.login();
|
||||
if (this.isReady()) {
|
||||
const invoice = prToWalletInvoice(pr);
|
||||
if (!invoice) {
|
||||
throw new WalletError(WalletErrorCode.InvalidInvoice, "Could not parse invoice");
|
||||
}
|
||||
const rsp = await barrierWebLn(async () => await window.webln?.sendPayment(pr));
|
||||
if (rsp) {
|
||||
invoice.state = WalletInvoiceState.Paid;
|
||||
invoice.preimage = rsp.preimage;
|
||||
return invoice;
|
||||
} else {
|
||||
invoice.state = WalletInvoiceState.Failed;
|
||||
return invoice;
|
||||
}
|
||||
}
|
||||
throw new WalletError(WalletErrorCode.GeneralError, "WebLN not available");
|
||||
}
|
||||
|
||||
getInvoices(): Promise<WalletInvoice[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
@ -1,3 +1,16 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
import { decodeInvoice, unwrap } from "Util";
|
||||
import { LNCWallet } from "./LNCWallet";
|
||||
import LNDHubWallet from "./LNDHub";
|
||||
import { setupWebLNWalletConfig, WebLNWallet } from "./WebLN";
|
||||
|
||||
export enum WalletKind {
|
||||
LNDHub = 1,
|
||||
LNC = 2,
|
||||
WebLN = 3,
|
||||
}
|
||||
|
||||
export enum WalletErrorCode {
|
||||
BadAuth = 1,
|
||||
NotEnoughBalance = 2,
|
||||
@ -8,9 +21,13 @@ export enum WalletErrorCode {
|
||||
NodeFailure = 7,
|
||||
}
|
||||
|
||||
export interface WalletError {
|
||||
export class WalletError extends Error {
|
||||
code: WalletErrorCode;
|
||||
message: string;
|
||||
|
||||
constructor(c: WalletErrorCode, msg: string) {
|
||||
super(msg);
|
||||
this.code = c;
|
||||
}
|
||||
}
|
||||
|
||||
export const UnknownWalletError = {
|
||||
@ -39,7 +56,7 @@ export interface Login {
|
||||
}
|
||||
|
||||
export interface InvoiceRequest {
|
||||
amount: number;
|
||||
amount: Sats;
|
||||
memo?: string;
|
||||
expiry?: number;
|
||||
}
|
||||
@ -48,31 +65,199 @@ export enum WalletInvoiceState {
|
||||
Pending = 0,
|
||||
Paid = 1,
|
||||
Expired = 2,
|
||||
Failed = 3,
|
||||
}
|
||||
|
||||
export interface WalletInvoice {
|
||||
pr: string;
|
||||
paymentHash: string;
|
||||
memo: string;
|
||||
amount: number;
|
||||
amount: MilliSats;
|
||||
fees: number;
|
||||
timestamp: number;
|
||||
preimage?: string;
|
||||
state: WalletInvoiceState;
|
||||
}
|
||||
|
||||
export function prToWalletInvoice(pr: string) {
|
||||
const parsedInvoice = decodeInvoice(pr);
|
||||
if (parsedInvoice) {
|
||||
return {
|
||||
amount: parsedInvoice.amount ?? 0,
|
||||
memo: parsedInvoice.description,
|
||||
paymentHash: parsedInvoice.paymentHash,
|
||||
timestamp: parsedInvoice.timestamp,
|
||||
state: parsedInvoice.expired ? WalletInvoiceState.Expired : WalletInvoiceState.Pending,
|
||||
pr,
|
||||
} as WalletInvoice;
|
||||
}
|
||||
}
|
||||
|
||||
export type Sats = number;
|
||||
export type MilliSats = number;
|
||||
|
||||
export interface LNWallet {
|
||||
createAccount: () => Promise<Login | WalletError>;
|
||||
getInfo: () => Promise<WalletInfo | WalletError>;
|
||||
login: () => Promise<boolean | WalletError>;
|
||||
close: () => Promise<boolean | WalletError>;
|
||||
getBalance: () => Promise<Sats | WalletError>;
|
||||
createInvoice: (req: InvoiceRequest) => Promise<WalletInvoice | WalletError>;
|
||||
payInvoice: (pr: string) => Promise<WalletInvoice | WalletError>;
|
||||
getInvoices: () => Promise<WalletInvoice[] | WalletError>;
|
||||
isReady(): boolean;
|
||||
getInfo: () => Promise<WalletInfo>;
|
||||
login: (password?: string) => Promise<boolean>;
|
||||
close: () => Promise<boolean>;
|
||||
getBalance: () => Promise<Sats>;
|
||||
createInvoice: (req: InvoiceRequest) => Promise<WalletInvoice>;
|
||||
payInvoice: (pr: string) => Promise<WalletInvoice>;
|
||||
getInvoices: () => Promise<WalletInvoice[]>;
|
||||
}
|
||||
|
||||
export function useWallet(): LNWallet | undefined {
|
||||
return undefined;
|
||||
export interface WalletConfig {
|
||||
id: string;
|
||||
kind: WalletKind;
|
||||
active: boolean;
|
||||
info: WalletInfo;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface WalletStoreSnapshot {
|
||||
configs: Array<WalletConfig>;
|
||||
config?: WalletConfig;
|
||||
wallet?: LNWallet;
|
||||
}
|
||||
|
||||
type WalletStateHook = (state: WalletStoreSnapshot) => void;
|
||||
|
||||
export class WalletStore {
|
||||
#configs: Array<WalletConfig>;
|
||||
#instance: Map<string, LNWallet>;
|
||||
|
||||
#hooks: Array<WalletStateHook>;
|
||||
#snapshot: Readonly<WalletStoreSnapshot>;
|
||||
|
||||
constructor() {
|
||||
this.#configs = [];
|
||||
this.#instance = new Map();
|
||||
this.#hooks = [];
|
||||
this.#snapshot = Object.freeze({
|
||||
configs: [],
|
||||
});
|
||||
this.load(false);
|
||||
setupWebLNWalletConfig(this);
|
||||
this.snapshotState();
|
||||
}
|
||||
|
||||
hook(fn: WalletStateHook) {
|
||||
this.#hooks.push(fn);
|
||||
return () => {
|
||||
const idx = this.#hooks.findIndex(a => a === fn);
|
||||
this.#hooks = this.#hooks.splice(idx, 1);
|
||||
};
|
||||
}
|
||||
|
||||
list() {
|
||||
return Object.freeze([...this.#configs]);
|
||||
}
|
||||
|
||||
get() {
|
||||
const activeConfig = this.#configs.find(a => a.active);
|
||||
if (!activeConfig) {
|
||||
if (this.#configs.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
throw new Error("No active wallet config");
|
||||
}
|
||||
if (this.#instance.has(activeConfig.id)) {
|
||||
return unwrap(this.#instance.get(activeConfig.id));
|
||||
} else {
|
||||
const w = this.#activateWallet(activeConfig);
|
||||
if (w) {
|
||||
this.#instance.set(activeConfig.id, w);
|
||||
return w;
|
||||
} else {
|
||||
throw new Error("Unable to activate wallet config");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add(cfg: WalletConfig) {
|
||||
this.#configs.push(cfg);
|
||||
this.save();
|
||||
}
|
||||
|
||||
remove(id: string) {
|
||||
const idx = this.#configs.findIndex(a => a.id === id);
|
||||
if (idx === -1) {
|
||||
throw new Error("Wallet not found");
|
||||
}
|
||||
const [removed] = this.#configs.splice(idx, 1);
|
||||
if (removed.active && this.#configs.length > 0) {
|
||||
this.#configs[0].active = true;
|
||||
}
|
||||
this.save();
|
||||
}
|
||||
|
||||
switch(id: string) {
|
||||
this.#configs.forEach(a => (a.active = a.id === id));
|
||||
this.save();
|
||||
}
|
||||
|
||||
save() {
|
||||
const json = JSON.stringify(this.#configs);
|
||||
window.localStorage.setItem("wallet-config", json);
|
||||
this.snapshotState();
|
||||
}
|
||||
|
||||
load(snapshot = true) {
|
||||
const cfg = window.localStorage.getItem("wallet-config");
|
||||
if (cfg) {
|
||||
this.#configs = JSON.parse(cfg);
|
||||
}
|
||||
if (snapshot) {
|
||||
this.snapshotState();
|
||||
}
|
||||
}
|
||||
|
||||
free() {
|
||||
this.#instance.forEach(w => w.close());
|
||||
}
|
||||
|
||||
getSnapshot() {
|
||||
return this.#snapshot;
|
||||
}
|
||||
|
||||
snapshotState() {
|
||||
const newState = {
|
||||
configs: [...this.#configs],
|
||||
config: this.#configs.find(a => a.active),
|
||||
wallet: this.get(),
|
||||
} as WalletStoreSnapshot;
|
||||
this.#snapshot = Object.freeze(newState);
|
||||
for (const hook of this.#hooks) {
|
||||
console.debug(this.#snapshot);
|
||||
hook(this.#snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
#activateWallet(cfg: WalletConfig): LNWallet | undefined {
|
||||
switch (cfg.kind) {
|
||||
case WalletKind.LNC: {
|
||||
const w = LNCWallet.Empty();
|
||||
return w;
|
||||
}
|
||||
case WalletKind.WebLN: {
|
||||
return new WebLNWallet();
|
||||
}
|
||||
case WalletKind.LNDHub: {
|
||||
return new LNDHubWallet(unwrap(cfg.data));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const Wallets = new WalletStore();
|
||||
window.document.addEventListener("close", () => {
|
||||
Wallets.free();
|
||||
});
|
||||
|
||||
export function useWallet() {
|
||||
return useSyncExternalStore<WalletStoreSnapshot>(
|
||||
h => Wallets.hook(h),
|
||||
() => Wallets.getSnapshot()
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user