feat: cashu wallet setup

feat: wallet send/receive pages
This commit is contained in:
Kieran 2024-01-04 16:10:40 +00:00
parent 1e08702072
commit 7bc00b4624
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
17 changed files with 348 additions and 143 deletions

View File

@ -1,47 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEApyUVkYJVwV7XgluUnllgCtrsdq1ctRICm5gQy8nd+aEdDQjA
CKPOWh5miLl/fAQVZGZy/JxavzXulwXo8238E6n6bmNB1Us2nuw7a0aW4iUSQ1Pt
P4ZhPpcrqeqMf+hp7iBW0nAHFy/aa2UR84d7tBmSk5J3NNrfBsZdUex/7FqF1EVv
mEzlc8kepU9lRXWFQDtZCllEZ1kY3SBJPm10h0g9saI8YIVRxUuNII5GHDYAE3hb
EmoY6fuSEoiXA8u0Yt9soBQxgxIhQVKSRPPoIPjGFOxsGHY6h8R9nx1kxhHKFRuV
nwsn0uWl/7yjhwyHanogJu73/WgelPcgP/hMDQIDAQABAoIBAAru+xU0oGVwzcoi
MXuWPxkWrwcoWfsiPXduIBMklleg+WSD4QPvqyzr9isVb0huf/O8W+M4WxtM7NmG
MnHSDP5ATThxV7obHGyS6WQgDvimEibDU66nHK9adim8RQqM6nkANo23dE9I+xGx
X9Y9U5M5ZQQwPYoAkzw/N5WHUerk+cSEYWYV8jDtO7wJhYOMu5qliPeuNOaWZ1W6
1uwr8A4ih69WwzugPuBSgBrPAW1c84zWIFN+njAugqPF5x8xp2uM3tUO9s5UlHJC
FWEuU40KcDT2utSUY+2HXSHbycF4KLKT5jAKSa4sPziLfo+YifrlN0Y3rhofUlZT
jCaeZ8ECgYEA5/xpk8aVhCEvv5iCghv0p/IHcjdXjx5+PCWh3Adx0fF91UvU5oqn
okdyYZDShZMuLDfJ0lG+OMKZd01JapnbTtiVNceVRMnraIdoWEM2/4bTXTSZGtdA
8gh/Kc/PMbPf5ppVWwqTCbUkPOSyGHyGc7+DQquq1w6yZu04A3x9vHECgYEAuHJk
uz8YKY5ZUR7CZ3y7YFuwq5Lcpl43AfiiCasjRch0P8yLrITc/6fORsXyy64XW9fC
h3YmXvEPaM03W2dxw2aQDvXEvXiEITzmILs7SE3UmZR9m7OMy7Jeqr3+JOc0ckZe
Rz5FfuMt1IvNB6lrpfHVtoVrpCOXpzHgC/k/x10CgYA6lU18GfwL/+107uiWPsUL
3FzxBPTBmau7OK2lSOP/ZoKmaJ39Eiq/GlfSN6ZSQRa55+S5jhcBcnMa45OUrgHp
6VvU1u/lDTC7luZM07yBzuR1dyDq3Ez0Uhz6zBXAsXHrZDIF6ae0HeBm2EH5WQkD
Fevp3DwqTvXSdDle+AMwoQKBgQCBSlaH1rNmNc0wCsK07f8ejUcrDZgz2mjurc1P
v7HK8bdjHUtvE/ciEguLGqiV06O2EmjesZg2Bv4JNYivPrTFBrjGc8qEEd10uw6J
NRVaGoyDV04w/UwdYRvwzZs/XP4reF4PzHvEdRSkH5cJ3t2BhiKLfby1YumkHlbx
rbbiVQKBgB02jyZUiB6pPTCP8vXZCJbZELgqNyS04ALhBBpdfGMcU1+0hRLJFBaE
tClJPGARFXl+MPkY032vmJZOuH3LrcTCm8DmMLzM/hT1EWawQ8BJkkwiIokE4lqc
Bi8CrkvuQs2cuCStK6C3Nkyr1lTkDge46trsb7KTcfHdtLsS7EPj
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDWzCCAkOgAwIBAgIJDji8iiceMvQlMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV
BAMTCWxvY2FsaG9zdDAeFw0yMzEwMTYwOTI0MThaFw0yMzExMTUxMDI0MThaMBQx
EjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAKclFZGCVcFe14JblJ5ZYAra7HatXLUSApuYEMvJ3fmhHQ0IwAijzloeZoi5
f3wEFWRmcvycWr817pcF6PNt/BOp+m5jQdVLNp7sO2tGluIlEkNT7T+GYT6XK6nq
jH/oae4gVtJwBxcv2mtlEfOHe7QZkpOSdzTa3wbGXVHsf+xahdRFb5hM5XPJHqVP
ZUV1hUA7WQpZRGdZGN0gST5tdIdIPbGiPGCFUcVLjSCORhw2ABN4WxJqGOn7khKI
lwPLtGLfbKAUMYMSIUFSkkTz6CD4xhTsbBh2OofEfZ8dZMYRyhUblZ8LJ9Llpf+8
o4cMh2p6ICbu9/1oHpT3ID/4TA0CAwEAAaOBrzCBrDAMBgNVHRMEBTADAQH/MAsG
A1UdDwQEAwIC9DAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwIGCCsGAQUF
BwMDBggrBgEFBQcDCDBcBgNVHREEVTBTgglsb2NhbGhvc3SCFWxvY2FsaG9zdC5s
b2NhbGRvbWFpboIGbHZoLm1lgggqLmx2aC5tZYIFWzo6MV2HBH8AAAGHEP6AAAAA
AAAAAAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggEBABY0rgWuzLYvVtvoVvWKS9cg
8rVhBRIFvpYO814ocN1iaxYQ9t9uLRsJXj0K+z1BHWf0zBiw4mB3dD9VpiKpuliL
4tRT+vATA96OYCd9G5k7DFQascAau40H3jxckh9rimIWa45FUSd7FIcddo1jeciv
gdAdiNUuHBen82O8KHJb+1PCBdA8RYeO5EGKfJM2yrOovu7dAFilf1ZPkXWgXnfG
nN6YfDDo9rAVDbvNXImrkwmGqEcN3Pq909IHiM/VETlU5lP4AbTNgrDa/aaZ+I+b
1MC1p87MvnibyXs+rTlK5+j8E6noNcD7tsHNd4ufkVCqr+pvSpuA3OvnXjbbm54=
-----END CERTIFICATE-----

View File

@ -2,10 +2,10 @@
"name": "@snort/app",
"version": "0.1.24",
"dependencies": {
"@cashu/cashu-ts": "^0.6.1",
"@cashu/cashu-ts": "0.6.1",
"@lightninglabs/lnc-web": "^0.2.3-alpha",
"@noble/curves": "^1.0.0",
"@noble/hashes": "^1.2.0",
"@noble/hashes": "^1.3.3",
"@scure/base": "^1.1.1",
"@scure/bip32": "^1.3.0",
"@scure/bip39": "^1.1.1",

View File

@ -21,9 +21,9 @@
<symbol id="bookmark" viewBox="0 0 12 14" fill="none">
<path d="M1.3335 4.2C1.3335 3.0799 1.3335 2.51984 1.55148 2.09202C1.74323 1.71569 2.04919 1.40973 2.42552 1.21799C2.85334 1 3.41339 1 4.5335 1H7.46683C8.58693 1 9.14699 1 9.57481 1.21799C9.95114 1.40973 10.2571 1.71569 10.4488 2.09202C10.6668 2.51984 10.6668 3.0799 10.6668 4.2V13L6.00016 10.3333L1.3335 13V4.2Z" stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</symbol>
<svg id="check" viewBox="0 0 24 25" fill="none">
<symbol id="check" viewBox="0 0 24 25" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.7071 5.79289C21.0976 6.18342 21.0976 6.81658 20.7071 7.20711L9.70711 18.2071C9.31658 18.5976 8.68342 18.5976 8.29289 18.2071L3.29289 13.2071C2.90237 12.8166 2.90237 12.1834 3.29289 11.7929C3.68342 11.4024 4.31658 11.4024 4.70711 11.7929L9 16.0858L19.2929 5.79289C19.6834 5.40237 20.3166 5.40237 20.7071 5.79289Z" fill="currentColor"/>
</svg>
</symbol>
<symbol id="chevronDown" viewBox="0 0 24 24" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.29289 8.29289C5.68342 7.90237 6.31658 7.90237 6.70711 8.29289L12 13.5858L17.2929 8.29289C17.6834 7.90237 18.3166 7.90237 18.7071 8.29289C19.0976 8.68342 19.0976 9.31658 18.7071 9.70711L12.7071 15.7071C12.3166 16.0976 11.6834 16.0976 11.2929 15.7071L5.29289 9.70711C4.90237 9.31658 4.90237 8.68342 5.29289 8.29289Z" fill="currentColor" />
</symbol>

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -1,16 +0,0 @@
.wallet-history-item {
}
.wallet-history-item time {
font-size: small;
color: var(--font-tertiary-color);
line-height: 1.5em;
}
.pending {
color: var(--font-tertiary-color);
}
.wallet-buttons > button {
margin: 10px;
}

View File

@ -10,7 +10,7 @@ import { useNavigate } from "react-router-dom";
const ConnectCashu = () => {
const navigate = useNavigate();
const { formatMessage } = useIntl();
const [mintUrl, setMintUrl] = useState<string>();
const [mintUrl, setMintUrl] = useState<string>("https://8333.space:3338");
const [error, setError] = useState<string>();
async function tryConnect(config: string) {
@ -20,7 +20,12 @@ const ConnectCashu = () => {
}
const { CashuWallet } = await import("@/Wallet/Cashu");
const connection = new CashuWallet(config);
const connection = new CashuWallet({
url: config,
keys: {},
proofs: [],
keysets: []
}, () => { });
await connection.login();
const info = await connection.getInfo();
const newWallet = {
@ -28,7 +33,7 @@ const ConnectCashu = () => {
kind: WalletKind.Cashu,
active: true,
info,
data: mintUrl,
data: JSON.stringify(connection.getConfig()),
} as WalletConfig;
Wallets.add(newWallet);
navigate("/settings/wallet");

View File

@ -1,5 +1,3 @@
import "./WalletPage.css";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
@ -91,10 +89,16 @@ export default function WalletPage(props: { showHistory: boolean }) {
function walletList() {
if (walletState.configs.length === 0) {
return (
<button onClick={() => navigate("/settings/wallet")}>
<FormattedMessage defaultMessage="Connect Wallet" id="cg1VJ2" />
</button>
return (<div className="flex flex-col gap-4">
<div>
<button onClick={() => navigate("/settings/wallet")}>
<FormattedMessage defaultMessage="Connect Wallet" id="cg1VJ2" />
</button>
</div>
<small>
<FormattedMessage defaultMessage="Connect a wallet to send instant payments" id="Yf3DwC" />
</small>
</div>
);
}
return (
@ -125,6 +129,9 @@ export default function WalletPage(props: { showHistory: boolean }) {
<h3>
<FormattedMessage defaultMessage="Payments" id="pukxg/" description="Wallet transation history" />
</h3>
{history === undefined && <small>
<FormattedMessage defaultMessage="Your sent and received payments will show up here." id="i5gBFz" />
</small>}
{history?.map(a => {
const dirClassname = {
"text-[--success]": a.direction === "in",
@ -206,9 +213,11 @@ export default function WalletPage(props: { showHistory: boolean }) {
}
function walletInfo() {
if (!wallet) return;
return (
<>
<div className="flex flex-col items-center px-6 py-4 bg-[--gray-superdark] rounded-2xl gap-1">
<div className="flex flex-col items-center px-6 py-4 bg-[--gray-ultradark] rounded-2xl gap-1">
{walletBalance()}
<div className="text-secondary">
<FormattedMessage
@ -221,6 +230,16 @@ export default function WalletPage(props: { showHistory: boolean }) {
}}
/>
</div>
<div className="flex gap-2">
{wallet?.canCreateInvoice() && <AsyncButton className="secondary" onClick={() => navigate("/wallet/receive")}>
<FormattedMessage defaultMessage="Receive" id="ULXFfP" />
<Icon name="arrow-up-right" className="rotate-180" />
</AsyncButton>}
{wallet?.canPayInvoice() && <AsyncButton onClick={() => navigate("/wallet/send")}>
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
<Icon name="arrow-up-right" />
</AsyncButton>}
</div>
</div>
{walletHistory()}
</>

View File

@ -0,0 +1,55 @@
import AsyncButton from "@/Components/Button/AsyncButton";
import Copy from "@/Components/Copy/Copy";
import QrCode from "@/Components/QrCode";
import { useWallet } from "@/Wallet";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
export function WalletReceivePage() {
const wallets = useWallet();
const { formatMessage } = useIntl();
const [invoice, setInvoice] = useState("");
const [error, setError] = useState("");
const [amount, setAmount] = useState(0);
const [comment, setComment] = useState("");
return <div className="p flex flex-col gap-4">
<div className="text-2xl font-bold">
<FormattedMessage defaultMessage="Receive" id="ULXFfP" />
</div>
<p>
<FormattedMessage defaultMessage="Receiving to <b>{wallet}</b>" id="PXQ0z0" values={{
b: b => <b>&quot;{b}&quot;</b>,
wallet: wallets.config?.info.alias
}} />
</p>
<input type="text" placeholder={formatMessage({ defaultMessage: "Comment", id: 'LgbKvU' })} value={comment} onChange={e => setComment(e.target.value)} />
<div className="flex flex-col">
<small>
<FormattedMessage defaultMessage="Amount in sats" id="djLctd" />
</small>
<input type="number" value={amount} onChange={e => setAmount(Number(e.target.value))} />
</div>
<AsyncButton onClick={async () => {
try {
if (wallets.wallet) {
const inv = await wallets.wallet.createInvoice({
amount: amount,
memo: comment,
expiry: 600
});
setInvoice(inv.pr);
}
} catch (e) {
setError((e as Error).message);
}
}}>
<FormattedMessage defaultMessage="Generate Invoice" id="ipHVx5" />
</AsyncButton>
{error && <b className="warning">{error}</b>}
{invoice && <div className="flex flex-col gap-2 items-center">
<QrCode data={invoice} link={`lightning:${invoice}`} />
<Copy text={invoice} />
</div>}
</div>
}

View File

@ -0,0 +1,75 @@
import AsyncButton from "@/Components/Button/AsyncButton";
import Icon from "@/Components/Icons/Icon";
import { formatShort } from "@/Utils/Number";
import { WalletInvoice, useWallet } from "@/Wallet"
import { LNURL } from "@snort/shared";
import { useEffect, useState } from "react";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
export function WalletSendPage() {
const wallets = useWallet();
const { formatMessage } = useIntl();
const [invoice, setInvoice] = useState("");
const [error, setError] = useState("");
const [lnurl, isLnurl] = useState(true);
const [amount, setAmount] = useState(0);
const [comment, setComment] = useState("");
const [result, setResult] = useState<WalletInvoice>();
useEffect(() => {
isLnurl(!invoice.startsWith("lnbc"))
}, [invoice]);
return <div className="p flex flex-col gap-4">
<div className="text-2xl font-bold">
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
</div>
<p>
<FormattedMessage defaultMessage="Sending from <b>{wallet}</b>" id="Xnimz0" values={{
b: b => <b>&quot;{b}&quot;</b>,
wallet: wallets.config?.info.alias
}} />
</p>
<input type="text" placeholder={formatMessage({ defaultMessage: "Invoice / Lightning Address", id: 'EHqHsu' })} value={invoice} onChange={e => setInvoice(e.target.value)} />
{lnurl && <>
<input type="text" placeholder={formatMessage({ defaultMessage: "Comment", id: 'LgbKvU' })} value={comment} onChange={e => setComment(e.target.value)} />
<div className="flex flex-col">
<small>
<FormattedMessage defaultMessage="Amount in sats" id="djLctd" />
</small>
<input type="number" value={amount} onChange={e => setAmount(Number(e.target.value))} />
</div>
</>}
<AsyncButton onClick={async () => {
try {
if (wallets.wallet) {
if (!isLnurl) {
const res = await wallets.wallet.payInvoice(invoice);
setResult(res);
} else {
const lnurl = new LNURL(invoice);
await lnurl.load();
const pr = await lnurl.getInvoice(amount, comment);
if (pr.pr) {
const res = await wallets.wallet.payInvoice(pr.pr);
setResult(res);
}
}
}
} catch (e) {
setError((e as Error).message);
}
}}>
<FormattedMessage defaultMessage="Pay" id="lD3+8a" />
</AsyncButton>
{error && <b className="warning">{error}</b>}
{result && <div className="flex gap-2">
<Icon name="check" className="success" />
<FormattedMessage defaultMessage="Paid {amount} sats, fee {fee} sats" id="aRex7h" values={{
amount: <FormattedNumber value={result.amount / 1000} />,
fee: <FormattedNumber value={result.fees / 1000} />
}} />
</div>}
</div>
}

View File

@ -15,7 +15,7 @@ export default class AlbyWallet implements LNWallet {
#token: OAuthToken;
constructor(
token: OAuthToken,
readonly onChange: () => void,
readonly onChange: (data?: object) => void,
) {
this.#token = token;
}
@ -23,15 +23,26 @@ export default class AlbyWallet implements LNWallet {
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");

View File

@ -1,36 +1,67 @@
import { LNWallet, Sats, WalletError, WalletErrorCode, WalletInfo, WalletInvoice } from "@/Wallet";
import { CashuMint, CashuWallet as TheCashuWallet, Proof } from "@cashu/cashu-ts";
import { InvoiceRequest, LNWallet, WalletInfo, WalletInvoice } from "@/Wallet";
import { CashuMint, Proof } from "@cashu/cashu-ts";
export type CashuWalletConfig = {
url: string;
keys: Record<string, string>;
keysets: Array<string>;
proofs: Array<Proof>;
};
export class CashuWallet implements LNWallet {
#mint: string;
#wallet?: TheCashuWallet;
#wallet: CashuWalletConfig;
#mint: CashuMint;
constructor(mint: string) {
this.#mint = mint;
constructor(
wallet: CashuWalletConfig,
readonly onChange: (data?: object) => void,
) {
this.#wallet = wallet;
this.#mint = new CashuMint(this.#wallet.url);
}
canAutoLogin(): boolean {
getConfig() {
return { ...this.#wallet };
}
canGetInvoices() {
return false;
}
canGetBalance() {
return true;
}
isReady(): boolean {
return this.#wallet !== undefined;
canAutoLogin() {
return true;
}
async getInfo(): Promise<WalletInfo> {
if (!this.#wallet) {
throw new WalletError(WalletErrorCode.GeneralError, "Wallet not initialized");
}
isReady() {
return true;
}
canCreateInvoice() {
return true;
}
canPayInvoice() {
return true;
}
async getInfo() {
return {
nodePubKey: "asdd",
alias: "Cashu mint: " + this.#mint,
alias: "Cashu mint: " + this.#wallet.url,
} as WalletInfo;
}
async login(): Promise<boolean> {
const m = new CashuMint(this.#mint);
const keys = await m.getKeys();
this.#wallet = new TheCashuWallet(keys, m);
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;
}
@ -38,25 +69,36 @@ export class CashuWallet implements LNWallet {
return Promise.resolve(true);
}
getBalance(): Promise<Sats> {
throw new Error("Method not implemented.");
async getBalance() {
return this.#wallet.proofs.reduce((acc, v) => (acc += v.amount), 0);
}
createInvoice(): Promise<WalletInvoice> {
throw new Error("Method not implemented.");
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([]);
}
}
export interface NutStashBackup {
proofs: Array<Proof>;
mints: [
{
mintURL: string;
},
];
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

@ -44,6 +44,14 @@ export class LNCWallet implements LNWallet {
return true;
}
canCreateInvoice() {
return true;
}
canPayInvoice() {
return true;
}
isReady(): boolean {
return this.#lnc.isReady;
}

View File

@ -25,7 +25,7 @@ export default class LNDHubWallet implements LNWallet {
constructor(
url: string,
readonly changed: () => void,
readonly changed: (data?: object) => void,
) {
if (url.startsWith("lndhub://")) {
const regex = /^lndhub:\/\/([\S-]+):([\S-]+)@(.*)$/i;
@ -58,6 +58,14 @@ export default class LNDHubWallet implements LNWallet {
return true;
}
canCreateInvoice() {
return true;
}
canPayInvoice() {
return true;
}
close(): Promise<boolean> {
return Promise.resolve(true);
}

View File

@ -83,7 +83,7 @@ export class NostrConnectWallet implements LNWallet {
constructor(
cfg: string,
readonly changed: () => void,
readonly changed: (data?: object) => void,
) {
this.#config = NostrConnectWallet.parseConfigUrl(cfg);
this.#commandQueue = new Map();
@ -106,6 +106,22 @@ export class NostrConnectWallet implements LNWallet {
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;
@ -247,14 +263,6 @@ export class NostrConnectWallet implements LNWallet {
}
}
canGetInvoices() {
return this.#supported_methods.includes("list_transactions");
}
canGetBalance() {
return this.#supported_methods.includes("get_balance");
}
async #onReply(sub: string, e: NostrEvent) {
if (sub === "info") {
const pending = this.#commandQueue.get("info");

View File

@ -44,6 +44,22 @@ export class WebLNWallet implements LNWallet {
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;
}
@ -76,6 +92,7 @@ export class WebLNWallet implements LNWallet {
}
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;
@ -116,6 +133,7 @@ export class WebLNWallet implements LNWallet {
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;
@ -128,12 +146,4 @@ export class WebLNWallet implements LNWallet {
getInvoices(): Promise<WalletInvoice[]> {
return Promise.resolve([]);
}
canGetInvoices() {
return false;
}
canGetBalance() {
return window.webln?.getBalance !== undefined;
}
}

View File

@ -116,6 +116,8 @@ export interface LNWallet {
canAutoLogin: () => boolean;
canGetInvoices: () => boolean;
canGetBalance: () => boolean;
canCreateInvoice: () => boolean;
canPayInvoice: () => boolean;
}
export interface WalletConfig {
@ -237,16 +239,31 @@ export class WalletStore extends ExternalStore<WalletStoreSnapshot> {
return new WebLNWallet();
}
case WalletKind.LNDHub: {
return new LNDHubWallet(unwrap(cfg.data), () => this.notifyChange());
return new LNDHubWallet(unwrap(cfg.data), d => this.#onWalletChange(cfg, d));
}
case WalletKind.NWC: {
return new NostrConnectWallet(unwrap(cfg.data), () => this.notifyChange());
return new NostrConnectWallet(unwrap(cfg.data), d => this.#onWalletChange(cfg, d));
}
case WalletKind.Alby: {
return new AlbyWallet(JSON.parse(unwrap(cfg.data)), () => this.notifyChange());
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.notifyChange();
}
}
export const Wallets = new WalletStore();

View File

@ -38,9 +38,11 @@ import { OnboardingRoutes } from "@/Pages/onboarding";
import { setupWebLNWalletConfig } from "@/Wallet/WebLN";
import { Wallets } from "@/Wallet";
import NetworkGraph from "@/Pages/NetworkGraph";
import WalletPage from "./Pages/WalletPage";
import WalletPage from "./Pages/wallet";
import { hasWasm, wasmInit, WasmPath } from "@/Utils/wasm";
import { System } from "@/system";
import { WalletSendPage } from "./Pages/wallet/send";
import { WalletReceivePage } from "./Pages/wallet/receive";
declare global {
interface Window {
@ -146,6 +148,14 @@ const mainRoutes = [
</div>
),
},
{
path: "/wallet/send",
element: <WalletSendPage />
},
{
path: "/wallet/receive",
element: <WalletReceivePage />
},
...OnboardingRoutes,
...SettingsRoutes,
] as Array<RouteObject>;

View File

@ -1421,7 +1421,7 @@ __metadata:
languageName: node
linkType: hard
"@cashu/cashu-ts@npm:^0.6.1":
"@cashu/cashu-ts@npm:0.6.1":
version: 0.6.1
resolution: "@cashu/cashu-ts@npm:0.6.1"
dependencies:
@ -2589,14 +2589,14 @@ __metadata:
languageName: node
linkType: hard
"@noble/hashes@npm:1.3.2, @noble/hashes@npm:^1.2.0, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.2":
"@noble/hashes@npm:1.3.2, @noble/hashes@npm:^1.3.2, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.2":
version: 1.3.2
resolution: "@noble/hashes@npm:1.3.2"
checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474
languageName: node
linkType: hard
"@noble/hashes@npm:~1.3.1":
"@noble/hashes@npm:^1.3.3, @noble/hashes@npm:~1.3.1":
version: 1.3.3
resolution: "@noble/hashes@npm:1.3.3"
checksum: 8a6496d1c0c64797339bc694ad06cdfaa0f9e56cd0c3f68ae3666cfb153a791a55deb0af9c653c7ed2db64d537aa3e3054629740d2f2338bb1dcb7ab60cd205b
@ -2923,11 +2923,11 @@ __metadata:
version: 0.0.0-use.local
resolution: "@snort/app@workspace:packages/app"
dependencies:
"@cashu/cashu-ts": ^0.6.1
"@cashu/cashu-ts": 0.6.1
"@formatjs/cli": ^6.1.3
"@lightninglabs/lnc-web": ^0.2.3-alpha
"@noble/curves": ^1.0.0
"@noble/hashes": ^1.2.0
"@noble/hashes": ^1.3.3
"@scure/base": ^1.1.1
"@scure/bip32": ^1.3.0
"@scure/bip39": ^1.1.1
@ -4446,13 +4446,13 @@ __metadata:
linkType: hard
"axios@npm:^1.2.1":
version: 1.6.2
resolution: "axios@npm:1.6.2"
version: 1.6.4
resolution: "axios@npm:1.6.4"
dependencies:
follow-redirects: ^1.15.0
follow-redirects: ^1.15.4
form-data: ^4.0.0
proxy-from-env: ^1.1.0
checksum: 4a7429e2b784be0f2902ca2680964391eae7236faa3967715f30ea45464b98ae3f1c6f631303b13dfe721b17126b01f486c7644b9ef276bfc63112db9fd379f8
checksum: 48d8af8488ac7402fae312437c0189b3b609a472fca2f7fc796129c804d98520589b6317096eba8509711d49f855a3f620b6a24ff23acd73ac26433d0383b8f9
languageName: node
linkType: hard
@ -6273,13 +6273,13 @@ __metadata:
languageName: node
linkType: hard
"follow-redirects@npm:^1.15.0":
version: 1.15.3
resolution: "follow-redirects@npm:1.15.3"
"follow-redirects@npm:^1.15.4":
version: 1.15.4
resolution: "follow-redirects@npm:1.15.4"
peerDependenciesMeta:
debug:
optional: true
checksum: 584da22ec5420c837bd096559ebfb8fe69d82512d5585004e36a3b4a6ef6d5905780e0c74508c7b72f907d1fa2b7bd339e613859e9c304d0dc96af2027fd0231
checksum: e178d1deff8b23d5d24ec3f7a94cde6e47d74d0dc649c35fc9857041267c12ec5d44650a0c5597ef83056ada9ea6ca0c30e7c4f97dbf07d035086be9e6a5b7b6
languageName: node
linkType: hard