refactor: extract wallet system

This commit is contained in:
2024-04-11 13:26:50 +01:00
parent d1095847d8
commit 8137317bfe
33 changed files with 5252 additions and 4916 deletions

View File

@ -2,9 +2,7 @@
"name": "@snort/app",
"version": "0.2.0",
"dependencies": {
"@cashu/cashu-ts": "0.6.1",
"@here/maps-api-for-javascript": "^1.50.0",
"@lightninglabs/lnc-web": "^0.2.8-alpha",
"@noble/curves": "^1.0.0",
"@noble/hashes": "^1.3.3",
"@scure/base": "^1.1.1",
@ -15,6 +13,7 @@
"@snort/system-react": "workspace:*",
"@snort/system-wasm": "workspace:*",
"@snort/system-web": "workspace:*",
"@snort/wallet": "workspace:*",
"@snort/worker-relay": "workspace:*",
"@szhsin/react-menu": "^3.3.1",
"@uidotdev/usehooks": "^2.4.1",
@ -119,7 +118,7 @@
"tailwindcss": "^3.3.3",
"tinybench": "^2.5.1",
"typescript": "^5.2.2",
"vite": "^5.1.5",
"vite": "^5.2.8",
"vite-plugin-pwa": "^0.19.2",
"vite-plugin-version-mark": "^0.0.10",
"vitest": "^0.34.6"

View File

@ -1,5 +1,6 @@
import { LNURL } from "@snort/shared";
import { NostrEvent } from "@snort/system";
import { WalletInvoiceState } from "@snort/wallet";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { UserCache } from "@/Cache";
@ -10,7 +11,6 @@ import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import { dedupe, findTag, getDisplayName, hexToBech32 } from "@/Utils";
import { useWallet } from "@/Wallet";
import { WalletInvoiceState } from "@/Wallet";
export default function PubkeyList({ ev, className }: { ev: NostrEvent; className?: string }) {
const wallet = useWallet();

View File

@ -1,10 +1,10 @@
import { LNWallet } from "@snort/wallet";
import React, { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import Copy from "@/Components/Copy/Copy";
import QrCode from "@/Components/QrCode";
import { ZapTargetResult } from "@/Utils/Zapper";
import { LNWallet } from "@/Wallet";
export function ZapModalInvoice(props: {
invoice: Array<ZapTargetResult>;

View File

@ -8,6 +8,7 @@ import BlueWallet from "@/Components/Icons/BlueWallet";
import Icon from "@/Components/Icons/Icon";
import NostrIcon from "@/Components/Icons/Nostrich";
import { getAlbyOAuth } from "@/Pages/settings/wallet/utils";
import CashuIcon from "@/Components/Icons/Cashu";
const WalletRow = (props: {
logo: ReactNode;
@ -70,12 +71,12 @@ const WalletSettings = () => {
url="/settings/wallet/lndhub"
desc={<FormattedMessage defaultMessage="Generic LNDHub wallet (BTCPayServer / Alby / LNBits)" id="0MndVW" />}
/>
{/*<WalletRow
<WalletRow
logo={<CashuIcon size={64} />}
name="Cashu"
url="/settings/wallet/cashu"
desc={<FormattedMessage defaultMessage="Cashu mint wallet" id="3natuV" />}
/>*/}
/>
{CONFIG.alby && (
<WalletRow
logo={<AlbyIcon size={64} />}

View File

@ -1,11 +1,11 @@
import { AlbyWallet, WalletKind } from "@snort/wallet";
import { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { v4 as uuid } from "uuid";
import PageSpinner from "@/Components/PageSpinner";
import { getAlbyOAuth } from "@/Pages/settings/wallet/utils";
import { WalletConfig, WalletKind, Wallets } from "@/Wallet";
import AlbyWallet from "@/Wallet/AlbyWallet";
import { WalletConfig, Wallets } from "@/Wallet";
export default function AlbyOAuth() {
const navigate = useNavigate();
@ -16,7 +16,7 @@ export default function AlbyOAuth() {
async function setupWallet(token: string) {
try {
const auth = await alby.getToken(token);
const connection = new AlbyWallet(auth, () => {});
const connection = new AlbyWallet(auth);
const info = await connection.getInfo();
const newWallet = {

View File

@ -1,3 +1,4 @@
import { CashuWallet, WalletKind } from "@snort/wallet";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
@ -5,7 +6,7 @@ import { v4 as uuid } from "uuid";
import AsyncButton from "@/Components/Button/AsyncButton";
import { unwrap } from "@/Utils";
import { WalletConfig, WalletKind, Wallets } from "@/Wallet";
import { WalletConfig, Wallets } from "@/Wallet";
const ConnectCashu = () => {
const navigate = useNavigate();
@ -19,7 +20,6 @@ const ConnectCashu = () => {
throw new Error("Mint URL is required");
}
const { CashuWallet } = await import("@/Wallet/Cashu");
const connection = new CashuWallet(
{
url: config,
@ -27,7 +27,6 @@ const ConnectCashu = () => {
proofs: [],
keysets: [],
},
() => {},
);
await connection.login();
const info = await connection.getInfo();

View File

@ -1,3 +1,4 @@
import { LNCWallet, LNWallet, WalletInfo, WalletKind } from "@snort/wallet";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
@ -5,7 +6,7 @@ import { v4 as uuid } from "uuid";
import AsyncButton from "@/Components/Button/AsyncButton";
import { unwrap } from "@/Utils";
import { LNWallet, WalletInfo, WalletKind, Wallets } from "@/Wallet";
import { Wallets } from "@/Wallet";
const ConnectLNC = () => {
const { formatMessage } = useIntl();
@ -18,7 +19,6 @@ const ConnectLNC = () => {
async function tryConnect(cfg: string) {
try {
const { LNCWallet } = await import("@/Wallet/LNCWallet");
const lnc = await LNCWallet.Initialize(cfg);
const info = await lnc.getInfo();

View File

@ -1,3 +1,4 @@
import { LNDHubWallet, WalletKind } from "@snort/wallet";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
@ -5,8 +6,7 @@ import { v4 as uuid } from "uuid";
import AsyncButton from "@/Components/Button/AsyncButton";
import { unwrap } from "@/Utils";
import { WalletConfig, WalletKind, Wallets } from "@/Wallet";
import LNDHubWallet from "@/Wallet/LNDHub";
import { WalletConfig, Wallets } from "@/Wallet";
const ConnectLNDHub = () => {
const navigate = useNavigate();
@ -16,7 +16,7 @@ const ConnectLNDHub = () => {
async function tryConnect(config: string) {
try {
const connection = new LNDHubWallet(config, () => {});
const connection = new LNDHubWallet(config);
await connection.login();
const info = await connection.getInfo();

View File

@ -1,3 +1,4 @@
import { NostrConnectWallet, WalletKind } from "@snort/wallet";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Link, useNavigate } from "react-router-dom";
@ -5,8 +6,7 @@ import { v4 as uuid } from "uuid";
import AsyncButton from "@/Components/Button/AsyncButton";
import { unwrap } from "@/Utils";
import { WalletConfig, WalletKind, Wallets } from "@/Wallet";
import { NostrConnectWallet } from "@/Wallet/NostrWalletConnect";
import { WalletConfig, Wallets } from "@/Wallet";
const ConnectNostrWallet = () => {
const navigate = useNavigate();
@ -16,7 +16,7 @@ const ConnectNostrWallet = () => {
async function tryConnect(config: string) {
try {
const connection = new NostrConnectWallet(config, () => {});
const connection = new NostrConnectWallet(config);
await connection.login();
const info = await connection.getInfo();

View File

@ -59,7 +59,7 @@ export function getAlbyOAuth() {
const data = await req.json();
if (req.ok) {
return { ...data, created_at: unixNow() } as OAuthToken;
return { ...data, created_at: unixNow(), clientId, clientSecret } as OAuthToken;
} else {
throw new Error(data.error_description as string);
}
@ -74,4 +74,6 @@ export interface OAuthToken {
refresh_token: string;
scope: string;
token_type: string;
clientId: string;
clientSecret: string;
}

View File

@ -1,4 +1,5 @@
/* eslint-disable max-lines */
import { LNWallet,Sats, WalletInvoice } from "@snort/wallet";
import classNames from "classnames";
import { useEffect, useState } from "react";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
@ -10,7 +11,7 @@ import NoteTime from "@/Components/Event/Note/NoteTime";
import Icon from "@/Components/Icons/Icon";
import { useRates } from "@/Hooks/useRates";
import { unwrap } from "@/Utils";
import { LNWallet, Sats, useWallet, WalletInvoice, Wallets } from "@/Wallet";
import { useWallet, Wallets } from "@/Wallet";
export default function WalletPage(props: { showHistory: boolean }) {
const navigate = useNavigate();

View File

@ -1,11 +1,12 @@
import { ExternalStore, LNURL, unixNow } from "@snort/shared";
import { LNWallet, WalletInvoiceState } from "@snort/wallet";
import debug from "debug";
import { UserCache } from "@/Cache";
import { Toastore } from "@/Components/Toaster/Toaster";
import { SnortPubKey } from "@/Utils/Const";
import { bech32ToHex, getDisplayName, trackEvent } from "@/Utils/index";
import { LNWallet, WalletInvoiceState, Wallets } from "@/Wallet";
import { Wallets } from "@/Wallet";
export enum ZapPoolRecipientType {
Generic = 0,

View File

@ -1,8 +1,8 @@
import { isHex, LNURL } from "@snort/shared";
import { EventPublisher, NostrEvent, NostrLink, SystemInterface } from "@snort/system";
import { LNWallet, WalletInvoiceState } from "@snort/wallet";
import { generateRandomKey } from "@/Utils/Login";
import { LNWallet, WalletInvoiceState } from "@/Wallet";
export interface ZapTarget {
type: "lnurl" | "pubkey";

View File

@ -1,193 +0,0 @@
import { base64 } from "@scure/base";
import { unixNow, unwrap } from "@snort/shared";
import { OAuthToken } from "@/Pages/settings/wallet/utils";
import {
InvoiceRequest,
LNWallet,
prToWalletInvoice,
WalletError,
WalletErrorCode,
WalletInfo,
WalletInvoice,
WalletInvoiceState,
} from ".";
export default class AlbyWallet implements LNWallet {
#token: OAuthToken;
constructor(
token: OAuthToken,
readonly onChange: (data?: object) => void,
) {
this.#token = token;
}
isReady() {
return true;
}
canAutoLogin() {
return true;
}
canGetInvoices() {
return this.#token.scope.includes("invoices:read");
}
canGetBalance() {
return this.#token.scope.includes("balance:read");
}
canCreateInvoice() {
return true;
}
canPayInvoice() {
return true;
}
async getInfo() {
const me = await this.#fetch<GetUserResponse>("/user/me");
return { alias: me.lightning_address } as WalletInfo;
}
async login() {
return true;
}
close() {
return Promise.resolve(true);
}
async getBalance() {
await this.#refreshToken();
const bal = await this.#fetch<GetBalanceResponse>("/balance");
return bal.balance;
}
async createInvoice(req: InvoiceRequest) {
const inv = await this.#fetch<CreateInvoiceResponse>("/invoices", "POST", {
amount: req.amount,
memo: req.memo,
});
return unwrap(prToWalletInvoice(inv.payment_request));
}
async payInvoice(pr: string) {
const pay = await this.#fetch<PayInvoiceResponse>("/payments/bolt11", "POST", {
invoice: pr,
});
return {
...prToWalletInvoice(pay.payment_request),
fees: pay.fee,
preimage: pay.payment_preimage,
state: WalletInvoiceState.Paid,
direction: "out",
} as WalletInvoice;
}
async getInvoices() {
const invoices = await this.#fetch<Array<GetInvoiceResponse>>("/invoices?page=1&items=20");
return invoices.map(a => {
return {
...prToWalletInvoice(a.payment_request),
memo: a.comment,
preimage: a.preimage,
state: a.settled ? WalletInvoiceState.Paid : WalletInvoiceState.Pending,
direction: a.type === "incoming" ? "in" : "out",
} as WalletInvoice;
});
}
async #fetch<T>(path: string, method: "GET" | "POST" = "GET", body?: object) {
const req = await fetch(`https://api.getalby.com${path}`, {
method: method,
body: body ? JSON.stringify(body) : undefined,
headers: {
accept: "application/json",
authorization: `Bearer ${this.#token.access_token}`,
...(body ? { "content-type": "application/json" } : {}),
},
});
const json = await req.text();
if (req.ok) {
return JSON.parse(json) as T;
} else {
if (json.length > 0) {
throw new WalletError(WalletErrorCode.GeneralError, JSON.parse(json).message as string);
} else {
throw new WalletError(WalletErrorCode.GeneralError, `Error: ${json} (${req.status})`);
}
}
}
async #refreshToken() {
if (this.#token.created_at + this.#token.expires_in < unixNow()) {
const params = new URLSearchParams();
params.set("refresh_token", this.#token.refresh_token);
params.set("grant_type", "refresh_token");
const req = await fetch("https://api.getalby.com/oauth/token", {
method: "POST",
body: params,
headers: {
accept: "application/json",
"content-type": "application/x-www-form-urlencoded",
authorization: `Basic ${base64.encode(
new TextEncoder().encode(`${CONFIG.alby?.clientId}:${CONFIG.alby?.clientSecret}`),
)}`,
},
});
const json = await req.json();
if (req.ok) {
this.#token = {
...(json as OAuthToken),
created_at: unixNow(),
};
this.onChange(this.#token);
}
}
}
}
interface GetBalanceResponse {
balance: number;
currency: string;
unit: string;
}
interface CreateInvoiceResponse {
expires_at: string;
payment_hash: string;
payment_request: string;
}
interface PayInvoiceResponse {
amount: number;
description?: string;
destination: string;
fee: number;
payment_hash: string;
payment_preimage: string;
payment_request: string;
}
interface GetInvoiceResponse {
amount: number;
comment?: string;
created_at: string;
creation_date: number;
currency: string;
expires_at: string;
preimage: string;
payment_request: string;
settled: boolean;
settled_at: string;
type: "incoming" | "outgoing";
}
interface GetUserResponse {
lightning_address: string;
}

View File

@ -1,105 +0,0 @@
import { CashuMint, Proof } from "@cashu/cashu-ts";
import { InvoiceRequest, LNWallet, WalletInfo, WalletInvoice } from "@/Wallet";
export type CashuWalletConfig = {
url: string;
keys: Record<string, string>;
keysets: Array<string>;
proofs: Array<Proof>;
};
export class CashuWallet implements LNWallet {
#wallet: CashuWalletConfig;
#mint: CashuMint;
constructor(
wallet: CashuWalletConfig,
readonly onChange: (data?: object) => void,
) {
this.#wallet = wallet;
this.#mint = new CashuMint(this.#wallet.url);
}
getConfig() {
return { ...this.#wallet };
}
canGetInvoices() {
return false;
}
canGetBalance() {
return true;
}
canAutoLogin() {
return true;
}
isReady() {
return true;
}
canCreateInvoice() {
return true;
}
canPayInvoice() {
return true;
}
async getInfo() {
return {
alias: "Cashu mint: " + this.#wallet.url,
} as WalletInfo;
}
async login(): Promise<boolean> {
if (this.#wallet.keysets.length === 0) {
const keys = await this.#mint.getKeys();
this.#wallet.keys = keys;
this.#wallet.keysets = [""];
this.onChange(this.#wallet);
}
await this.#checkProofs();
return true;
}
close(): Promise<boolean> {
return Promise.resolve(true);
}
async getBalance() {
return this.#wallet.proofs.reduce((acc, v) => (acc += v.amount), 0);
}
async createInvoice(req: InvoiceRequest) {
const rsp = await this.#mint.requestMint(req.amount);
return {
pr: rsp.pr,
} as WalletInvoice;
}
payInvoice(): Promise<WalletInvoice> {
throw new Error("Method not implemented.");
}
getInvoices(): Promise<WalletInvoice[]> {
return Promise.resolve([]);
}
async #checkProofs() {
if (this.#wallet.proofs.length == 0) return;
const checks = await this.#mint.check({
proofs: this.#wallet.proofs.map(a => ({ secret: a.secret })),
});
const filteredProofs = this.#wallet.proofs.filter((_, i) => checks.spendable[i]);
this.#wallet.proofs = filteredProofs;
if (filteredProofs.length !== checks.spendable.length) {
this.onChange(this.#wallet);
}
}
}

View File

@ -1,179 +0,0 @@
import LNC from "@lightninglabs/lnc-web";
import debug from "debug";
import { unwrap } from "@/Utils";
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;
readonly #log = debug("LNC");
private constructor(pairingPhrase?: string, password?: string) {
this.#lnc = new LNC({
pairingPhrase,
password,
});
}
canAutoLogin() {
return false;
}
canGetInvoices() {
return true;
}
canGetBalance() {
return true;
}
canCreateInvoice() {
return true;
}
canPayInvoice() {
return true;
}
isReady(): boolean {
return this.#lnc.isReady;
}
static async Initialize(pairingPhrase: string) {
const lnc = new LNCWallet(pairingPhrase);
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> {
const nodeInfo = await this.#lnc.lnd.lightning.getInfo();
return {
nodePubKey: nodeInfo.identityPubkey,
alias: nodeInfo.alias,
} as WalletInfo;
}
close(): Promise<boolean> {
if (this.#lnc.isConnected) {
this.#lnc.disconnect();
}
return Promise.resolve(true);
}
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 => {
setTimeout(resolve, 100);
});
}
return true;
}
async getBalance(): Promise<number> {
const rsp = await this.#lnc.lnd.lightning.channelBalance();
this.#log(rsp);
return parseInt(rsp.localBalance?.sat ?? "0");
}
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));
}
async payInvoice(pr: string): Promise<WalletInvoice> {
return new Promise((resolve, reject) => {
this.#lnc.lnd.router.sendPaymentV2(
{
paymentRequest: pr,
timeoutSeconds: 60,
feeLimitSat: "100",
},
msg => {
this.#log(msg);
if (msg.status === Payment_PaymentStatus.SUCCEEDED) {
resolve({
preimage: msg.paymentPreimage,
state: WalletInvoiceState.Paid,
timestamp: parseInt(msg.creationTimeNs) / 1e9,
} as WalletInvoice);
}
},
err => {
this.#log(err);
reject(err);
},
);
});
}
async getInvoices(): Promise<WalletInvoice[]> {
const invoices = await this.#lnc.lnd.lightning.listPayments({
maxPayments: "10",
reversed: true,
});
this.#log(invoices);
return invoices.payments.map(a => {
const parsedInvoice = prToWalletInvoice(a.paymentRequest);
if (!parsedInvoice) {
throw new WalletError(WalletErrorCode.InvalidInvoice, `Could not parse ${a.paymentRequest}`);
}
return {
...parsedInvoice,
state: (() => {
switch (a.status) {
case Payment_PaymentStatus.SUCCEEDED:
return WalletInvoiceState.Paid;
case Payment_PaymentStatus.FAILED:
return WalletInvoiceState.Failed;
default:
return WalletInvoiceState.Pending;
}
})(),
preimage: a.paymentPreimage,
} as WalletInvoice;
});
}
}

View File

@ -1,209 +0,0 @@
import { throwIfOffline } from "@snort/shared";
import {
InvoiceRequest,
LNWallet,
prToWalletInvoice,
Sats,
WalletError,
WalletErrorCode,
WalletInfo,
WalletInvoice,
WalletInvoiceState,
} from "@/Wallet";
const defaultHeaders = {
Accept: "application/json",
"Content-Type": "application/json",
};
export default class LNDHubWallet implements LNWallet {
type: "lndhub";
url: URL;
user: string;
password: string;
auth?: AuthResponse;
constructor(
url: string,
readonly changed: (data?: object) => void,
) {
if (url.startsWith("lndhub://")) {
const regex = /^lndhub:\/\/([\S-]+):([\S-]+)@(.*)$/i;
const parsedUrl = url.match(regex);
if (!parsedUrl || parsedUrl.length !== 4) {
throw new Error("Invalid LNDHUB config");
}
this.url = new URL(parsedUrl[3]);
this.user = parsedUrl[1];
this.password = parsedUrl[2];
this.type = "lndhub";
} else {
throw new Error("Invalid config");
}
}
isReady(): boolean {
return this.auth !== undefined;
}
canAutoLogin() {
return true;
}
canGetInvoices() {
return true;
}
canGetBalance() {
return true;
}
canCreateInvoice() {
return true;
}
canPayInvoice() {
return true;
}
close(): Promise<boolean> {
return Promise.resolve(true);
}
async getInfo() {
await this.login();
return await this.getJson<WalletInfo>("GET", "/getinfo");
}
async login() {
if (this.auth) return true;
const rsp = await this.getJson<AuthResponse>("POST", "/auth?type=auth", {
login: this.user,
password: this.password,
});
this.auth = rsp as AuthResponse;
this.changed();
return true;
}
async getBalance(): Promise<Sats> {
await this.login();
const rsp = await this.getJson<GetBalanceResponse>("GET", "/balance");
const bal = Math.floor((rsp as GetBalanceResponse).BTC.AvailableBalance);
return bal as Sats;
}
async createInvoice(req: InvoiceRequest) {
await this.login();
const rsp = await this.getJson<UserInvoicesResponse>("POST", "/addinvoice", {
amt: req.amount,
memo: req.memo,
});
const pRsp = rsp as UserInvoicesResponse;
return {
pr: pRsp.payment_request,
memo: req.memo,
amount: req.amount,
paymentHash: pRsp.payment_hash,
timestamp: pRsp.timestamp,
} as WalletInvoice;
}
async payInvoice(pr: string) {
await this.login();
const rsp = await this.getJson<PayInvoiceResponse>("POST", "/payinvoice", {
invoice: pr,
});
const pRsp = rsp as PayInvoiceResponse;
return {
pr: pr,
paymentHash: pRsp.payment_hash,
preimage: pRsp.payment_preimage,
state: pRsp.payment_error
? WalletInvoiceState.Failed
: pRsp.payment_preimage
? WalletInvoiceState.Paid
: WalletInvoiceState.Pending,
} as WalletInvoice;
}
async getInvoices(): Promise<WalletInvoice[]> {
await this.login();
const rsp = await this.getJson<UserInvoicesResponse[]>("GET", "/getuserinvoices");
return (rsp as UserInvoicesResponse[])
.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1))
.slice(0, 50)
.map(a => {
const decodedInvoice = prToWalletInvoice(a.payment_request);
if (!decodedInvoice) {
throw new WalletError(WalletErrorCode.InvalidInvoice, "Failed to parse invoice");
}
return {
...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?: unknown): Promise<T> {
throwIfOffline();
const auth = `Bearer ${this.auth?.access_token}`;
const url = `${this.url.pathname === "/" ? this.url.toString().slice(0, -1) : this.url.toString()}${path}`;
const rsp = await fetch(url, {
method: method,
body: body ? JSON.stringify(body) : undefined,
headers: {
...defaultHeaders,
Authorization: auth,
},
});
const json = await rsp.json();
if ("code" in json && !rsp.ok) {
const err = json as ErrorResponse;
throw new WalletError(err.code, err.message);
}
return json as T;
}
}
interface AuthResponse {
refresh_token?: string;
access_token?: string;
token_type?: string;
}
interface GetBalanceResponse {
BTC: {
AvailableBalance: number;
};
}
interface UserInvoicesResponse {
amt: number;
description: string;
ispaid: boolean;
type: string;
timestamp: number;
pay_req: string;
payment_hash: string;
payment_request: string;
r_hash: string;
}
interface PayInvoiceResponse {
payment_error?: string;
payment_hash: string;
payment_preimage: string;
payment_route?: { total_amt: number; total_fees: number };
}
interface ErrorResponse {
code: number;
message: string;
}

View File

@ -1,349 +0,0 @@
/* eslint-disable max-lines */
import { dedupe } from "@snort/shared";
import { Connection, EventBuilder, EventKind, NostrEvent, PrivateKeySigner } from "@snort/system";
import debug from "debug";
import {
InvoiceRequest,
LNWallet,
WalletError,
WalletErrorCode,
WalletInfo,
WalletInvoice,
WalletInvoiceState,
} from "@/Wallet";
interface WalletConnectConfig {
relayUrl: string;
walletPubkey: string;
secret: string;
}
interface QueueObj {
resolve: (o: string) => void;
reject: (e: Error) => void;
}
interface WalletConnectResponse<T> {
result_type?: string;
result?: T;
error?: {
code:
| "RATE_LIMITED"
| "NOT_IMPLEMENTED"
| "INSUFFICIENT_BALANCE"
| "QUOTA_EXCEEDED"
| "RESTRICTED"
| "UNAUTHORIZED"
| "INTERNAL"
| "OTHER";
message: string;
};
}
interface GetInfoResponse {
alias?: string;
color?: string;
pubkey?: string;
network?: string;
block_height?: number;
block_hash?: string;
methods?: Array<string>;
}
interface ListTransactionsResponse {
transactions: Array<{
type: "incoming" | "outgoing";
invoice: string;
description?: string;
description_hash?: string;
preimage?: string;
payment_hash?: string;
amount: number;
feed_paid: number;
settled_at?: number;
created_at: number;
expires_at: number;
metadata?: object;
}>;
}
interface MakeInvoiceResponse {
invoice: string;
payment_hash: string;
}
const DefaultSupported = ["get_info", "pay_invoice"];
export class NostrConnectWallet implements LNWallet {
#log = debug("NWC");
#config: WalletConnectConfig;
#conn?: Connection;
#commandQueue: Map<string, QueueObj>;
#info?: WalletInfo;
#supported_methods: Array<string> = DefaultSupported;
constructor(
cfg: string,
readonly changed: (data?: object) => void,
) {
this.#config = NostrConnectWallet.parseConfigUrl(cfg);
this.#commandQueue = new Map();
}
static parseConfigUrl(url: string) {
const uri = new URL(url.replace("nostrwalletconnect://", "http://").replace("nostr+walletconnect://", "http://"));
return {
relayUrl: uri.searchParams.get("relay"),
walletPubkey: uri.host,
secret: uri.searchParams.get("secret"),
} as WalletConnectConfig;
}
canAutoLogin(): boolean {
return true;
}
isReady(): boolean {
return this.#conn !== undefined;
}
canGetInvoices() {
return this.#supported_methods.includes("list_transactions");
}
canGetBalance() {
return this.#supported_methods.includes("get_balance");
}
canCreateInvoice() {
return this.#supported_methods.includes("make_invoice");
}
canPayInvoice() {
return this.#supported_methods.includes("pay_invoice");
}
async getInfo() {
await this.login();
if (this.#info) return this.#info;
const rsp = await this.#rpc<WalletConnectResponse<GetInfoResponse>>("get_info", {});
if (!rsp.error) {
this.#supported_methods = dedupe(["get_info", ...(rsp.result?.methods ?? DefaultSupported)]);
this.#log("Supported methods: %o", this.#supported_methods);
const info = {
nodePubKey: rsp.result?.pubkey,
alias: rsp.result?.alias,
blockHeight: rsp.result?.block_height,
blockHash: rsp.result?.block_hash,
chains: rsp.result?.network ? [rsp.result.network] : undefined,
} as WalletInfo;
this.#info = info;
return info;
} else if (rsp.error.code === "NOT_IMPLEMENTED") {
// legacy get_info uses event kind 13_194
return await new Promise<WalletInfo>((resolve, reject) => {
this.#commandQueue.set("info", {
resolve: (o: string) => {
this.#supported_methods = dedupe(["get_info", ...o.split(",")]);
this.#log("Supported methods: %o", this.#supported_methods);
const info = {
alias: "NWC",
} as WalletInfo;
this.#info = info;
resolve(info);
},
reject,
});
this.#conn?.queueReq(["REQ", "info", { kinds: [13194], limit: 1 }], () => {
// ignored
});
});
} else {
throw new WalletError(WalletErrorCode.GeneralError, rsp.error.message);
}
}
async login() {
if (this.#conn) return true;
await new Promise<void>(resolve => {
this.#conn = new Connection(this.#config.relayUrl, { read: true, write: true });
this.#conn.on("connected", () => resolve());
this.#conn.on("auth", async (c, r, cb) => {
const eb = new EventBuilder();
eb.kind(EventKind.Auth).tag(["relay", r]).tag(["challenge", c]);
const ev = await eb.buildAndSign(this.#config.secret);
cb(ev);
});
this.#conn.on("event", (s, e) => {
this.#onReply(s, e);
});
this.#conn.connect();
});
await this.getInfo();
this.changed();
return true;
}
async close() {
this.#conn?.close();
return true;
}
async getBalance() {
await this.login();
const rsp = await this.#rpc<WalletConnectResponse<{ balance: number }>>("get_balance", {});
if (!rsp.error) {
return (rsp.result?.balance ?? 0) / 1000;
} else {
throw new WalletError(WalletErrorCode.GeneralError, rsp.error.message);
}
}
async createInvoice(req: InvoiceRequest) {
await this.login();
const rsp = await this.#rpc<WalletConnectResponse<MakeInvoiceResponse>>("make_invoice", {
amount: req.amount * 1000,
description: req.memo,
expiry: req.expiry,
});
if (!rsp.error) {
return {
pr: rsp.result?.invoice,
paymentHash: rsp.result?.payment_hash,
memo: req.memo,
amount: req.amount * 1000,
state: WalletInvoiceState.Pending,
} as WalletInvoice;
} else {
throw new WalletError(WalletErrorCode.GeneralError, rsp.error.message);
}
}
async payInvoice(pr: string) {
await this.login();
const rsp = await this.#rpc<WalletConnectResponse<WalletInvoice>>("pay_invoice", {
invoice: pr,
});
if (!rsp.error) {
return {
...rsp.result,
pr,
state: WalletInvoiceState.Paid,
} as WalletInvoice;
} else {
throw new WalletError(WalletErrorCode.GeneralError, rsp.error.message);
}
}
async getInvoices() {
await this.login();
const rsp = await this.#rpc<WalletConnectResponse<ListTransactionsResponse>>("list_transactions", {
limit: 50,
});
if (!rsp.error) {
return (
rsp.result?.transactions.map(
a =>
({
pr: a.invoice,
paymentHash: a.payment_hash,
memo: a.description,
amount: a.amount,
fees: a.feed_paid,
timestamp: typeof a.created_at === "string" ? new Date(a.created_at).getTime() / 1000 : a.created_at,
preimage: a.preimage,
state: WalletInvoiceState.Paid,
direction: a.type === "incoming" ? "in" : "out",
}) as WalletInvoice,
) ?? []
);
} else {
throw new WalletError(WalletErrorCode.GeneralError, rsp.error.message);
}
}
async #onReply(sub: string, e: NostrEvent) {
if (sub === "info") {
const pending = this.#commandQueue.get("info");
if (!pending) {
throw new WalletError(WalletErrorCode.GeneralError, "No pending info command found");
}
pending.resolve(e.content);
this.#commandQueue.delete("info");
return;
}
if (e.kind !== 23195) {
throw new WalletError(WalletErrorCode.GeneralError, "Unknown event kind");
}
const replyTo = e.tags.find(a => a[0] === "e");
if (!replyTo) {
throw new WalletError(WalletErrorCode.GeneralError, "Missing e-tag in command response");
}
const pending = this.#commandQueue.get(replyTo[1]);
if (!pending) {
throw new WalletError(WalletErrorCode.GeneralError, "No pending command found");
}
pending.resolve(e.content);
this.#commandQueue.delete(replyTo[1]);
this.#conn?.closeReq(sub);
}
async #rpc<T>(method: string, params: Record<string, string | number | undefined>) {
if (!this.#conn) throw new WalletError(WalletErrorCode.GeneralError, "Not implemented");
this.#log("> %o", { method, params });
if (!this.#supported_methods.includes(method)) {
const ret = {
error: {
code: "NOT_IMPLEMENTED",
message: `get_info claims the method "${method}" is not supported`,
},
} as T;
this.#log("< %o", ret);
return ret;
}
const payload = JSON.stringify({
method,
params,
});
const signer = new PrivateKeySigner(this.#config.secret);
const eb = new EventBuilder();
eb.kind(23194 as EventKind)
.content(await signer.nip4Encrypt(payload, this.#config.walletPubkey))
.tag(["p", this.#config.walletPubkey]);
const evCommand = await eb.buildAndSign(this.#config.secret);
this.#conn.queueReq(
[
"REQ",
evCommand.id.slice(0, 12),
{
kinds: [23195 as EventKind],
authors: [this.#config.walletPubkey],
["#e"]: [evCommand.id],
},
],
() => {
// ignored
},
);
await this.#conn.sendEventAsync(evCommand);
return await new Promise<T>((resolve, reject) => {
this.#commandQueue.set(evCommand.id, {
resolve: async (o: string) => {
const reply = JSON.parse(await signer.nip4Decrypt(o, this.#config.walletPubkey));
this.#log("< %o", reply);
resolve(reply);
},
reject,
});
});
}
}

View File

@ -1,150 +0,0 @@
import { barrierQueue, processWorkQueue, unwrap, WorkQueueItem } from "@snort/shared";
import {
InvoiceRequest,
LNWallet,
prToWalletInvoice,
Sats,
WalletConfig,
WalletError,
WalletErrorCode,
WalletInfo,
WalletInvoice,
WalletInvoiceState,
WalletKind,
WalletStore,
} from "@/Wallet";
const WebLNQueue: Array<WorkQueueItem> = [];
processWorkQueue(WebLNQueue);
/**
* Adds a wallet config for WebLN if detected
*/
export function setupWebLNWalletConfig(store: WalletStore) {
const wallets = store.list();
const existing = wallets.find(a => a.kind === WalletKind.WebLN);
if (window.webln && !existing) {
const newConfig = {
id: "webln",
kind: WalletKind.WebLN,
active: wallets.length === 0,
info: {
alias: "WebLN",
},
} as WalletConfig;
store.add(newConfig);
} else if (existing) {
store.remove(existing.id);
}
}
export class WebLNWallet implements LNWallet {
isReady(): boolean {
return window.webln !== undefined && window.webln !== null;
}
canCreateInvoice() {
return true;
}
canPayInvoice() {
return true;
}
canGetInvoices() {
return false;
}
canGetBalance() {
return window.webln?.getBalance !== undefined;
}
canAutoLogin(): boolean {
return true;
}
async getInfo(): Promise<WalletInfo> {
await this.login();
if (this.isReady()) {
const rsp = await barrierQueue(WebLNQueue, 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) {
await window.webln.enable();
}
return true;
}
close(): Promise<boolean> {
return Promise.resolve(true);
}
async getBalance(): Promise<Sats> {
await this.login();
if (window.webln?.getBalance) {
const rsp = await barrierQueue(WebLNQueue, async () => await unwrap(window.webln?.getBalance).call(window.webln));
return rsp.balance;
}
return 0;
}
async createInvoice(req: InvoiceRequest): Promise<WalletInvoice> {
await this.login();
if (this.isReady()) {
const rsp = await barrierQueue(
WebLNQueue,
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 barrierQueue(WebLNQueue, async () => await window.webln?.sendPayment(pr));
if (rsp) {
invoice.state = WalletInvoiceState.Paid;
invoice.preimage = rsp.preimage;
invoice.fees = "route" in rsp ? (rsp.route as { total_fees: number }).total_fees : 0;
return invoice;
} else {
invoice.state = WalletInvoiceState.Failed;
return invoice;
}
}
throw new WalletError(WalletErrorCode.GeneralError, "WebLN not available");
}
getInvoices(): Promise<WalletInvoice[]> {
return Promise.resolve([]);
}
}

View File

@ -1,288 +1,179 @@
import { decodeInvoice, ExternalStore } from "@snort/shared";
import { ExternalStore, unwrap } from "@snort/shared";
import { LNWallet, loadWallet, WalletInfo, WalletKind } from "@snort/wallet";
import { useEffect, useSyncExternalStore } from "react";
import { unwrap } from "@/Utils";
import AlbyWallet from "./AlbyWallet";
import LNDHubWallet from "./LNDHub";
import { NostrConnectWallet } from "./NostrWalletConnect";
import { WebLNWallet } from "./WebLN";
export enum WalletKind {
LNDHub = 1,
LNC = 2,
WebLN = 3,
NWC = 4,
Cashu = 5,
Alby = 6,
}
export enum WalletErrorCode {
BadAuth = 1,
NotEnoughBalance = 2,
BadPartner = 3,
InvalidInvoice = 4,
RouteNotFound = 5,
GeneralError = 6,
NodeFailure = 7,
}
export class WalletError extends Error {
code: WalletErrorCode;
constructor(c: WalletErrorCode, msg: string) {
super(msg);
this.code = c;
}
}
export const UnknownWalletError = {
code: WalletErrorCode.GeneralError,
message: "Unknown error",
} as WalletError;
export interface WalletInfo {
fee: number;
nodePubKey: string;
alias: string;
pendingChannels: number;
activeChannels: number;
peers: number;
blockHeight: number;
blockHash: string;
synced: boolean;
chains: string[];
version: string;
}
export interface Login {
service: string;
save: () => Promise<void>;
load: () => Promise<void>;
}
export interface InvoiceRequest {
amount: Sats;
memo?: string;
expiry?: number;
}
export enum WalletInvoiceState {
Pending = 0,
Paid = 1,
Expired = 2,
Failed = 3,
}
export interface WalletInvoice {
pr: string;
paymentHash: string;
memo: string;
amount: MilliSats;
fees: number;
timestamp: number;
preimage?: string;
state: WalletInvoiceState;
direction: "in" | "out";
}
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 ?? 0,
state: parsedInvoice.expired ? WalletInvoiceState.Expired : WalletInvoiceState.Pending,
pr,
direction: "in",
} as WalletInvoice;
}
}
export type Sats = number;
export type MilliSats = number;
export interface LNWallet {
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[]>;
canAutoLogin: () => boolean;
canGetInvoices: () => boolean;
canGetBalance: () => boolean;
canCreateInvoice: () => boolean;
canPayInvoice: () => boolean;
}
export interface WalletConfig {
id: string;
kind: WalletKind;
active: boolean;
info: WalletInfo;
data?: string;
id: string;
kind: WalletKind;
active: boolean;
info: WalletInfo;
/**
* Opaque string for wallet config
*/
data?: string;
}
export interface WalletStoreSnapshot {
configs: Array<WalletConfig>;
config?: WalletConfig;
wallet?: LNWallet;
configs: Array<WalletConfig>;
config?: WalletConfig;
wallet?: LNWallet;
}
export class WalletStore extends ExternalStore<WalletStoreSnapshot> {
#configs: Array<WalletConfig>;
#instance: Map<string, LNWallet>;
#configs: Array<WalletConfig>;
#instance: Map<string, LNWallet>;
constructor() {
super();
this.#configs = [];
this.#instance = new Map();
this.load(false);
this.notifyChange();
}
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");
constructor() {
super();
this.#configs = [];
this.#instance = new Map();
this.load(false);
this.notifyChange();
}
if (this.#instance.has(activeConfig.id)) {
return unwrap(this.#instance.get(activeConfig.id));
} else {
const w = this.#activateWallet(activeConfig);
if (w) {
if ("then" in w) {
w.then(async wx => {
this.#instance.set(activeConfig.id, wx);
this.notifyChange();
});
return undefined;
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 {
this.#instance.set(activeConfig.id, w);
this.notifyChange();
const w = this.#activateWallet(activeConfig);
if (w) {
if ("then" in w) {
w.then(async wx => {
this.#instance.set(activeConfig.id, wx);
this.notifyChange();
});
return undefined;
} else {
this.#instance.set(activeConfig.id, w);
this.notifyChange();
}
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.notifyChange();
}
load(snapshot = true) {
const cfg = window.localStorage.getItem("wallet-config");
if (cfg) {
this.#configs = JSON.parse(cfg);
}
if (snapshot) {
this.notifyChange();
}
}
free() {
this.#instance.forEach(w => w.close());
}
takeSnapshot(): WalletStoreSnapshot {
return {
configs: [...this.#configs],
config: this.#configs.find(a => a.active),
wallet: this.get(),
} as WalletStoreSnapshot;
}
#activateWallet(cfg: WalletConfig): LNWallet | Promise<LNWallet> | undefined {
const w = loadWallet(cfg.kind, cfg.data);
if (w) {
w.on("change", d => this.#onWalletChange(cfg, d));
}
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");
#onWalletChange(cfg: WalletConfig, data?: string) {
if (data) {
const activeConfig = this.#configs.find(a => a.id === cfg.id);
if (activeConfig) {
activeConfig.data = data;
}
this.save();
} else {
this.notifyChange();
}
}
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.notifyChange();
}
load(snapshot = true) {
const cfg = window.localStorage.getItem("wallet-config");
if (cfg) {
this.#configs = JSON.parse(cfg);
}
if (snapshot) {
this.notifyChange();
}
}
free() {
this.#instance.forEach(w => w.close());
}
takeSnapshot(): WalletStoreSnapshot {
return {
configs: [...this.#configs],
config: this.#configs.find(a => a.active),
wallet: this.get(),
} as WalletStoreSnapshot;
}
#activateWallet(cfg: WalletConfig): LNWallet | Promise<LNWallet> | undefined {
switch (cfg.kind) {
case WalletKind.LNC: {
return import("./LNCWallet").then(({ LNCWallet }) => LNCWallet.Empty());
}
case WalletKind.WebLN: {
return new WebLNWallet();
}
case WalletKind.LNDHub: {
return new LNDHubWallet(unwrap(cfg.data), d => this.#onWalletChange(cfg, d));
}
case WalletKind.NWC: {
return new NostrConnectWallet(unwrap(cfg.data), d => this.#onWalletChange(cfg, d));
}
case WalletKind.Alby: {
return new AlbyWallet(JSON.parse(unwrap(cfg.data)), d => this.#onWalletChange(cfg, d));
}
case WalletKind.Cashu: {
return import("./Cashu").then(
({ CashuWallet }) => new CashuWallet(JSON.parse(unwrap(cfg.data)), d => this.#onWalletChange(cfg, d)),
);
}
}
}
#onWalletChange(cfg: WalletConfig, data?: object) {
if (data) {
const activeConfig = this.#configs.find(a => a.id === cfg.id);
if (activeConfig) {
activeConfig.data = JSON.stringify(data);
}
this.save();
} else {
this.notifyChange();
}
}
}
export const Wallets = new WalletStore();
window.document.addEventListener("close", () => {
Wallets.free();
Wallets.free();
});
export function useWallet() {
const wallet = useSyncExternalStore<WalletStoreSnapshot>(
h => Wallets.hook(h),
() => Wallets.snapshot(),
);
useEffect(() => {
if (wallet.wallet?.isReady() === false && wallet.wallet.canAutoLogin()) {
wallet.wallet.login().catch(console.error);
}
}, [wallet]);
return wallet;
const wallet = useSyncExternalStore<WalletStoreSnapshot>(
h => Wallets.hook(h),
() => Wallets.snapshot(),
);
useEffect(() => {
if (wallet.wallet?.isReady() === false && wallet.wallet.canAutoLogin()) {
wallet.wallet.login().catch(console.error);
}
}, [wallet]);
return wallet;
}
/**
* Adds a wallet config for WebLN if detected
*/
export function setupWebLNWalletConfig(store: WalletStore) {
const wallets = store.list();
const existing = wallets.find(a => a.kind === WalletKind.WebLN);
if (window.webln && !existing) {
const newConfig = {
id: "webln",
kind: WalletKind.WebLN,
active: wallets.length === 0,
info: {
alias: "WebLN",
},
} as WalletConfig;
store.add(newConfig);
} else if (existing) {
store.remove(existing.id);
}
}

View File

@ -42,7 +42,7 @@ import { storeRefCode, unwrap } from "@/Utils";
import { LoginStore } from "@/Utils/Login";
import { hasWasm, wasmInit, WasmPath } from "@/Utils/wasm";
import { Wallets } from "@/Wallet";
import { setupWebLNWalletConfig } from "@/Wallet/WebLN";
import { setupWebLNWalletConfig } from "@/Wallet";
async function initSite() {
storeRefCode();