feat: new wallet design

This commit is contained in:
2024-01-02 18:11:44 +00:00
parent 0d10122394
commit bc7ec4d77f
16 changed files with 252 additions and 130 deletions

View File

@ -408,7 +408,7 @@ export function NoteCreator() {
<div className="flex flex-col g8"> <div className="flex flex-col g8">
{[...(note.zapSplits ?? [])].map((v, i, arr) => ( {[...(note.zapSplits ?? [])].map((v, i, arr) => (
<div className="flex items-center g8"> <div className="flex items-center g8">
<div className="flex flex-col f-4 g4"> <div className="flex flex-col flex-4 g4">
<h4> <h4>
<FormattedMessage defaultMessage="Recipient" id="8Rkoyb" /> <FormattedMessage defaultMessage="Recipient" id="8Rkoyb" />
</h4> </h4>
@ -423,7 +423,7 @@ export function NoteCreator() {
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address", id: "WvGmZT" })} placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address", id: "WvGmZT" })}
/> />
</div> </div>
<div className="flex flex-col f-1 g4"> <div className="flex flex-col flex-1 g4">
<h4> <h4>
<FormattedMessage defaultMessage="Weight" id="zCb8fX" /> <FormattedMessage defaultMessage="Weight" id="zCb8fX" />
</h4> </h4>

View File

@ -0,0 +1,34 @@
import { bech32ToHex } from "@snort/shared";
import { EventKind, ReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react";
// Snort backend publishes rates
const SnortPubkey = "npub1sn0rtcjcf543gj4wsg7fa59s700d5ztys5ctj0g69g2x6802npjqhjjtws";
export function useRates(symbol: string, leaveOpen = true) {
const sub = useMemo(() => {
const rb = new RequestBuilder(`rates:${symbol}`);
rb.withOptions({
leaveOpen,
});
rb.withFilter()
.kinds([1009 as EventKind])
.authors([bech32ToHex(SnortPubkey)])
.tag("d", [symbol])
.limit(1);
return rb;
}, [symbol]);
const data = useRequestBuilder(ReplaceableNoteStore, sub);
const tag = data?.data?.tags.find(a => a[0] === "d" && a[1] === symbol);
if (!tag) return undefined;
return {
time: data.data?.created_at,
ask: Number(tag[2]),
bid: Number(tag[3]),
low: Number(tag[4]),
hight: Number(tag[5]),
};
}

View File

@ -71,7 +71,7 @@ export function SnortDeckLayout() {
id="IOu4Xh" id="IOu4Xh"
values={{ values={{
app: CONFIG.appNameCapitalized, app: CONFIG.appNameCapitalized,
tier: mapPlanName(CONFIG.deckSubKind), tier: mapPlanName(CONFIG.deckSubKind ?? -1),
}} }}
/> />
</div> </div>
@ -122,7 +122,11 @@ export function SnortDeckLayout() {
)} )}
{deckState.article && ( {deckState.article && (
<> <>
<Modal onClose={() => setDeckState({})} className="long-form" onClick={() => setDeckState({})}> <Modal
id="deck-article"
onClose={() => setDeckState({})}
className="long-form"
onClick={() => setDeckState({})}>
<div onClick={e => e.stopPropagation()}> <div onClick={e => e.stopPropagation()}>
<LongFormText ev={deckState.article} isPreview={false} related={[]} /> <LongFormText ev={deckState.article} isPreview={false} related={[]} />
</div> </div>
@ -140,11 +144,11 @@ function NotesCol() {
return ( return (
<div> <div>
<div className="deck-col-header flex"> <div className="deck-col-header flex">
<div className="flex f-1 g8"> <div className="flex flex-1 g8">
<Icon name="rows-01" size={24} /> <Icon name="rows-01" size={24} />
<FormattedMessage defaultMessage="Notes" id="7+Domh" /> <FormattedMessage defaultMessage="Notes" id="7+Domh" />
</div> </div>
<div className="f-1"> <div className="flex-1">
<RootTabs base="/deck" /> <RootTabs base="/deck" />
</div> </div>
</div> </div>

View File

@ -1,18 +1,21 @@
import { LogoHeader } from "./LogoHeader"; import { LogoHeader } from "./LogoHeader";
import { useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import Icon from "@/Icons/Icon"; import Icon from "@/Icons/Icon";
import { ProfileLink } from "../../Element/User/ProfileLink"; import { ProfileLink } from "../../Element/User/ProfileLink";
import Avatar from "../../Element/User/Avatar"; import Avatar from "../../Element/User/Avatar";
import useLogin from "../../Hooks/useLogin"; import useLogin from "../../Hooks/useLogin";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import { NoteCreatorButton } from "../../Element/Event/Create/NoteCreatorButton"; import { NoteCreatorButton } from "../../Element/Event/Create/NoteCreatorButton";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import classNames from "classnames"; import classNames from "classnames";
import { getCurrentSubscription } from "@/Subscription"; import { getCurrentSubscription } from "@/Subscription";
import { HasNotificationsMarker } from "@/Pages/Layout/HasNotificationsMarker"; import { HasNotificationsMarker } from "@/Pages/Layout/HasNotificationsMarker";
import NavLink from "@/Element/Button/NavLink"; import NavLink from "@/Element/Button/NavLink";
import { subscribeToNotifications } from "@/Notifications"; import { subscribeToNotifications } from "@/Notifications";
import useEventPublisher from "@/Hooks/useEventPublisher"; import useEventPublisher from "@/Hooks/useEventPublisher";
import { Sats, useWallet } from "@/Wallet";
import { useEffect, useState } from "react";
import { useRates } from "@/Hooks/useRates";
const MENU_ITEMS = [ const MENU_ITEMS = [
{ {
@ -72,6 +75,44 @@ const getNavLinkClass = (isActive: boolean, narrow: boolean) => {
}); });
}; };
const WalletBalance = () => {
const [balance, setBalance] = useState<Sats>();
const wallet = useWallet();
const rates = useRates("BTCUSD");
useEffect(() => {
setBalance(undefined);
if (wallet.wallet && wallet.wallet.canGetBalance()) {
wallet.wallet.getBalance().then(setBalance);
}
}, [wallet]);
return (
<div className="w-max flex flex-col max-xl:hidden">
<div className="grow flex items-center justify-between">
<div className="flex gap-1 items-center">
<Icon name="sats" size={24} />
<FormattedNumber value={balance ?? 0} />
</div>
<Link to="/wallet">
<Icon name="dots" className="text-secondary" />
</Link>
</div>
<div className="text-secondary text-sm">
<FormattedMessage
defaultMessage="~{amount}"
id="3QwfJR"
values={{
amount: (
<FormattedNumber style="currency" currency="USD" value={(rates?.ask ?? 0) * (balance ?? 0) * 1e-8} />
),
}}
/>
</div>
</div>
);
};
export default function NavSidebar({ narrow = false }) { export default function NavSidebar({ narrow = false }) {
const { publicKey, subscriptions, readonly } = useLogin(s => ({ const { publicKey, subscriptions, readonly } = useLogin(s => ({
publicKey: s.publicKey, publicKey: s.publicKey,
@ -106,6 +147,7 @@ export default function NavSidebar({ narrow = false }) {
{ "xl:items-start": !narrow, "xl:gap-2": !narrow }, { "xl:items-start": !narrow, "xl:gap-2": !narrow },
"gap-1 flex flex-col items-center text-lg font-bold", "gap-1 flex flex-col items-center text-lg font-bold",
)}> )}>
<WalletBalance narrow={narrow} />
{MENU_ITEMS.filter(a => { {MENU_ITEMS.filter(a => {
if ((CONFIG.hideFromNavbar ?? []).includes(a.link)) { if ((CONFIG.hideFromNavbar ?? []).includes(a.link)) {
return false; return false;
@ -122,7 +164,7 @@ export default function NavSidebar({ narrow = false }) {
return ""; return "";
} }
const onClick = () => { const onClick = () => {
if (item.label === "Notifications") { if (item.label === "Notifications" && publisher) {
subscribeToNotifications(publisher); subscribeToNotifications(publisher);
} }
}; };

View File

@ -5,31 +5,35 @@ import { useNavigate } from "react-router-dom";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import NoteTime from "@/Element/Event/NoteTime"; import NoteTime from "@/Element/Event/NoteTime";
import { WalletInvoice, Sats, WalletInfo, WalletInvoiceState, useWallet, LNWallet, Wallets } from "@/Wallet"; import { WalletInvoice, Sats, useWallet, LNWallet, Wallets } from "@/Wallet";
import AsyncButton from "@/Element/Button/AsyncButton"; import AsyncButton from "@/Element/Button/AsyncButton";
import { unwrap } from "@/SnortUtils"; import { unwrap } from "@/SnortUtils";
import Icon from "@/Icons/Icon"; import Icon from "@/Icons/Icon";
import { useRates } from "@/Hooks/useRates";
import { AsyncIcon } from "@/Element/Button/AsyncIcon";
import classNames from "classnames";
export default function WalletPage() { export default function WalletPage(props: { showHistory: boolean }) {
const navigate = useNavigate(); const navigate = useNavigate();
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const [info, setInfo] = useState<WalletInfo>();
const [balance, setBalance] = useState<Sats>(); const [balance, setBalance] = useState<Sats>();
const [history, setHistory] = useState<WalletInvoice[]>(); const [history, setHistory] = useState<WalletInvoice[]>();
const [walletPassword, setWalletPassword] = useState<string>(); const [walletPassword, setWalletPassword] = useState<string>();
const [error, setError] = useState<string>(); const [error, setError] = useState<string>();
const walletState = useWallet(); const walletState = useWallet();
const wallet = walletState.wallet; const wallet = walletState.wallet;
const rates = useRates("BTCUSD");
async function loadWallet(wallet: LNWallet) { async function loadWallet(wallet: LNWallet) {
try { try {
const i = await wallet.getInfo(); setError(undefined);
setInfo(i); setBalance(0);
setHistory(undefined);
if (wallet.canGetBalance()) { if (wallet.canGetBalance()) {
const b = await wallet.getBalance(); const b = await wallet.getBalance();
setBalance(b as Sats); setBalance(b as Sats);
} }
if (wallet.canGetInvoices()) { if (wallet.canGetInvoices() && (props.showHistory ?? true)) {
const h = await wallet.getInvoices(); const h = await wallet.getInvoices();
setHistory((h as WalletInvoice[]).sort((a, b) => b.timestamp - a.timestamp)); setHistory((h as WalletInvoice[]).sort((a, b) => b.timestamp - a.timestamp));
} }
@ -43,29 +47,11 @@ export default function WalletPage() {
} }
useEffect(() => { useEffect(() => {
if (wallet) { if (wallet && wallet.isReady()) {
if (wallet.isReady()) { loadWallet(wallet).catch(console.warn);
loadWallet(wallet).catch(console.warn);
} else if (wallet.canAutoLogin()) {
wallet
.login()
.then(async () => await loadWallet(wallet))
.catch(console.warn);
}
} }
}, [wallet]); }, [wallet]);
function stateIcon(s: WalletInvoiceState) {
switch (s) {
case WalletInvoiceState.Pending:
return <Icon name="clock" size={15} />;
case WalletInvoiceState.Paid:
return <Icon name="check" size={15} />;
case WalletInvoiceState.Expired:
return <Icon name="close" size={15} />;
}
}
async function loginWallet(pw: string) { async function loginWallet(pw: string) {
if (wallet) { if (wallet) {
await wallet.login(pw); await wallet.login(pw);
@ -112,11 +98,11 @@ export default function WalletPage() {
); );
} }
return ( return (
<div className="flex w-max"> <div className="flex items-center">
<h4 className="f-1"> <h4 className="grow">
<FormattedMessage defaultMessage="Select Wallet" id="G1BGCg" /> <FormattedMessage defaultMessage="Select Wallet" id="G1BGCg" />
</h4> </h4>
<div className="f-1"> <div>
<select className="w-max" onChange={e => Wallets.switch(e.target.value)} value={walletState.config?.id}> <select className="w-max" onChange={e => Wallets.switch(e.target.value)} value={walletState.config?.id}>
{Wallets.list().map(a => { {Wallets.list().map(a => {
return <option value={a.id}>{a.info.alias}</option>; return <option value={a.id}>{a.info.alias}</option>;
@ -128,79 +114,108 @@ export default function WalletPage() {
} }
function walletHistory() { function walletHistory() {
if (!wallet?.canGetInvoices()) return; if (!wallet?.canGetInvoices() || !(props.showHistory ?? true)) return;
return ( return (
<> <div className="flex flex-col gap-1">
<h3> <h3>
<FormattedMessage defaultMessage="History" id="d6CyG5" description="Wallet transation history" /> <FormattedMessage defaultMessage="Payments" id="pukxg/" description="Wallet transation history" />
</h3> </h3>
{history?.map(a => ( {history?.map(a => {
<div className="card flex wallet-history-item" key={a.timestamp}> const dirClassname = {
<div className="grow"> "text-[--success]": a.direction === "in",
<NoteTime from={a.timestamp * 1000} fallback={formatMessage({ defaultMessage: "now", id: "kaaf1E" })} /> "text-[--error]": a.direction === "out",
<div>{(a.memo ?? "").length === 0 ? <>&nbsp;</> : a.memo}</div> };
</div> return (
<div <div className="flex gap-4 p-2 hover:bg-[--gray-superdark] rounded-xl items-center" key={a.timestamp}>
className={`flex gap-2 items-center ${(() => {
switch (a.state) {
case WalletInvoiceState.Paid:
return "success";
case WalletInvoiceState.Expired:
return "expired";
case WalletInvoiceState.Failed:
return "failed";
default:
return "pending";
}
})()}`}>
<div>{stateIcon(a.state)}</div>
<div> <div>
<FormattedMessage <div className="rounded-full aspect-square p-2 bg-[--gray-dark]">
defaultMessage="{amount} sats" <Icon
id="vrTOHJ" name="arrow-up-right"
values={{ className={classNames(dirClassname, {
amount: <FormattedNumber value={a.amount / 1e3} />, "rotate-180": a.direction === "in",
}} })}
/> />
</div>
</div>
<div className="grow flex justify-between">
<div className="flex flex-col gap-1">
<div>{a.memo?.length === 0 ? CONFIG.appNameCapitalized : a.memo}</div>
<div className="text-secondary text-sm">
<NoteTime
from={a.timestamp * 1000}
fallback={formatMessage({ defaultMessage: "now", id: "kaaf1E" })}
/>
</div>
</div>
<div className="flex flex-col gap-1 text-right">
<div className={classNames(dirClassname)}>
<FormattedMessage
defaultMessage="{sign} {amount} sats"
id="tj6kdX"
values={{
sign: a.direction === "in" ? "+" : "-",
amount: <FormattedNumber value={a.amount / 1e3} />,
}}
/>
</div>
<div className="text-secondary text-sm">
<FormattedMessage
defaultMessage="~{amount}"
id="3QwfJR"
values={{
amount: (
<FormattedNumber
style="currency"
currency="USD"
value={(rates?.ask ?? 0) * a.amount * 1e-11}
/>
),
}}
/>
</div>
</div>
</div> </div>
</div> </div>
</div> );
))} })}
</> </div>
); );
} }
function walletBalance() { function walletBalance() {
if (!wallet?.canGetBalance()) return; if (!wallet?.canGetBalance()) return;
return ( return (
<small> <div className="flex items-center gap-2">
<FormattedMessage <FormattedMessage
defaultMessage="Balance: {amount} sats" defaultMessage="<big>{amount}</big> <small>sats</small>"
id="VN0+Fz" id="E5ZIPD"
values={{ values={{
big: c => <span className="text-3xl font-bold">{c}</span>,
small: c => <span className="text-secondary">{c}</span>,
amount: <FormattedNumber value={balance ?? 0} />, amount: <FormattedNumber value={balance ?? 0} />,
}} }}
/> />
</small> <AsyncIcon size={20} className="text-secondary cursor-pointer" iconName="closedeye" />
</div>
); );
} }
function walletInfo() { function walletInfo() {
if (!wallet?.isReady()) return;
return ( return (
<> <>
<div className="p br b flex justify-between"> <div className="flex flex-col items-center px-6 py-4 bg-[--gray-superdark] rounded-2xl gap-1">
<div> {walletBalance()}
<div>{info?.alias}</div> <div className="text-secondary">
{walletBalance()} <FormattedMessage
</div> defaultMessage="~{amount}"
<div> id="3QwfJR"
{walletState.config?.id && ( values={{
<AsyncButton onClick={() => Wallets.remove(unwrap(walletState.config?.id))}> amount: (
<FormattedMessage defaultMessage="Remove" id="G/yZLu" /> <FormattedNumber style="currency" currency="USD" value={(rates?.ask ?? 0) * (balance ?? 0) * 1e-8} />
</AsyncButton> ),
)} }}
/>
</div> </div>
</div> </div>
{walletHistory()} {walletHistory()}
@ -209,7 +224,7 @@ export default function WalletPage() {
} }
return ( return (
<div className="main-content p"> <div className="main-content">
{walletList()} {walletList()}
{error && <b className="warning">{error}</b>} {error && <b className="warning">{error}</b>}
{unlockWallet()} {unlockWallet()}

View File

@ -22,7 +22,7 @@ export default function AccountsPage() {
about: false, about: false,
}} }}
actions={ actions={
<div className="f-1"> <div className="flex-1">
<button className="mb10" onClick={() => LoginStore.switchAccount(a.id)}> <button className="mb10" onClick={() => LoginStore.switchAccount(a.id)}>
<FormattedMessage defaultMessage="Switch" id="n1Whvj" /> <FormattedMessage defaultMessage="Switch" id="n1Whvj" />
</button> </button>

View File

@ -16,7 +16,7 @@ const WalletSettings = () => {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<> <>
<WalletPage /> <WalletPage showHistory={false} />
<h3> <h3>
<FormattedMessage defaultMessage="Connect Wallet" id="cg1VJ2" /> <FormattedMessage defaultMessage="Connect Wallet" id="cg1VJ2" />
</h3> </h3>

View File

@ -47,7 +47,7 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
{mapPlanName(sub.type)} {mapPlanName(sub.type)}
</div> </div>
<div className="flex"> <div className="flex">
<p className="f-1"> <p className="flex-1">
<FormattedMessage defaultMessage="Created" id="ORGv1Q" /> <FormattedMessage defaultMessage="Created" id="ORGv1Q" />
:&nbsp; :&nbsp;
<time dateTime={created.toISOString()}> <time dateTime={created.toISOString()}>
@ -55,7 +55,7 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
</time> </time>
</p> </p>
{daysToExpire >= 1 && ( {daysToExpire >= 1 && (
<p className="f-1"> <p className="flex-1">
<FormattedMessage defaultMessage="Expires" id="xhQMeQ" /> <FormattedMessage defaultMessage="Expires" id="xhQMeQ" />
:&nbsp; :&nbsp;
<time dateTime={expires.toISOString()}> <time dateTime={expires.toISOString()}>
@ -70,7 +70,7 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
</p> </p>
)} )}
{daysToExpire >= 0 && daysToExpire < 1 && ( {daysToExpire >= 0 && daysToExpire < 1 && (
<p className="f-1"> <p className="flex-1">
<FormattedMessage defaultMessage="Expires" id="xhQMeQ" /> <FormattedMessage defaultMessage="Expires" id="xhQMeQ" />
:&nbsp; :&nbsp;
<time dateTime={expires.toISOString()}> <time dateTime={expires.toISOString()}>
@ -85,12 +85,12 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
</p> </p>
)} )}
{isExpired && ( {isExpired && (
<p className="f-1 error"> <p className="flex-1 error">
<FormattedMessage defaultMessage="Expired" id="RahCRH" /> <FormattedMessage defaultMessage="Expired" id="RahCRH" />
</p> </p>
)} )}
{isNew && ( {isNew && (
<p className="f-1"> <p className="flex-1">
<FormattedMessage defaultMessage="Unpaid" id="6uMqL1" /> <FormattedMessage defaultMessage="Unpaid" id="6uMqL1" />
</p> </p>
)} )}

View File

@ -23,7 +23,10 @@ export default class LNDHubWallet implements LNWallet {
password: string; password: string;
auth?: AuthResponse; auth?: AuthResponse;
constructor(url: string) { constructor(
url: string,
readonly changed: () => void,
) {
if (url.startsWith("lndhub://")) { if (url.startsWith("lndhub://")) {
const regex = /^lndhub:\/\/([\S-]+):([\S-]+)@(.*)$/i; const regex = /^lndhub:\/\/([\S-]+):([\S-]+)@(.*)$/i;
const parsedUrl = url.match(regex); const parsedUrl = url.match(regex);
@ -60,25 +63,31 @@ export default class LNDHubWallet implements LNWallet {
} }
async getInfo() { async getInfo() {
await this.login();
return await this.getJson<WalletInfo>("GET", "/getinfo"); return await this.getJson<WalletInfo>("GET", "/getinfo");
} }
async login() { async login() {
if (this.auth) return true;
const rsp = await this.getJson<AuthResponse>("POST", "/auth?type=auth", { const rsp = await this.getJson<AuthResponse>("POST", "/auth?type=auth", {
login: this.user, login: this.user,
password: this.password, password: this.password,
}); });
this.auth = rsp as AuthResponse; this.auth = rsp as AuthResponse;
this.changed();
return true; return true;
} }
async getBalance(): Promise<Sats> { async getBalance(): Promise<Sats> {
await this.login();
const rsp = await this.getJson<GetBalanceResponse>("GET", "/balance"); const rsp = await this.getJson<GetBalanceResponse>("GET", "/balance");
const bal = Math.floor((rsp as GetBalanceResponse).BTC.AvailableBalance); const bal = Math.floor((rsp as GetBalanceResponse).BTC.AvailableBalance);
return bal as Sats; return bal as Sats;
} }
async createInvoice(req: InvoiceRequest) { async createInvoice(req: InvoiceRequest) {
await this.login();
const rsp = await this.getJson<UserInvoicesResponse>("POST", "/addinvoice", { const rsp = await this.getJson<UserInvoicesResponse>("POST", "/addinvoice", {
amt: req.amount, amt: req.amount,
memo: req.memo, memo: req.memo,
@ -95,6 +104,7 @@ export default class LNDHubWallet implements LNWallet {
} }
async payInvoice(pr: string) { async payInvoice(pr: string) {
await this.login();
const rsp = await this.getJson<PayInvoiceResponse>("POST", "/payinvoice", { const rsp = await this.getJson<PayInvoiceResponse>("POST", "/payinvoice", {
invoice: pr, invoice: pr,
}); });
@ -113,6 +123,7 @@ export default class LNDHubWallet implements LNWallet {
} }
async getInvoices(): Promise<WalletInvoice[]> { async getInvoices(): Promise<WalletInvoice[]> {
await this.login();
const rsp = await this.getJson<UserInvoicesResponse[]>("GET", "/getuserinvoices"); const rsp = await this.getJson<UserInvoicesResponse[]>("GET", "/getuserinvoices");
return (rsp as UserInvoicesResponse[]) return (rsp as UserInvoicesResponse[])
.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1)) .sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1))

View File

@ -81,7 +81,10 @@ export class NostrConnectWallet implements LNWallet {
#info?: WalletInfo; #info?: WalletInfo;
#supported_methods: Array<string> = DefaultSupported; #supported_methods: Array<string> = DefaultSupported;
constructor(cfg: string) { constructor(
cfg: string,
readonly changed: () => void,
) {
this.#config = NostrConnectWallet.parseConfigUrl(cfg); this.#config = NostrConnectWallet.parseConfigUrl(cfg);
this.#commandQueue = new Map(); this.#commandQueue = new Map();
} }
@ -147,9 +150,9 @@ export class NostrConnectWallet implements LNWallet {
async login() { async login() {
if (this.#conn) return true; if (this.#conn) return true;
return await new Promise<boolean>(resolve => { await new Promise<void>(resolve => {
this.#conn = new Connection(this.#config.relayUrl, { read: true, write: true }); this.#conn = new Connection(this.#config.relayUrl, { read: true, write: true });
this.#conn.on("connected", () => resolve(true)); this.#conn.on("connected", () => resolve());
this.#conn.on("auth", async (c, r, cb) => { this.#conn.on("auth", async (c, r, cb) => {
const eb = new EventBuilder(); const eb = new EventBuilder();
eb.kind(EventKind.Auth).tag(["relay", r]).tag(["challenge", c]); eb.kind(EventKind.Auth).tag(["relay", r]).tag(["challenge", c]);
@ -161,6 +164,9 @@ export class NostrConnectWallet implements LNWallet {
}); });
this.#conn.Connect(); this.#conn.Connect();
}); });
await this.getInfo();
this.changed();
return true;
} }
async close() { async close() {
@ -232,6 +238,7 @@ export class NostrConnectWallet implements LNWallet {
timestamp: typeof a.created_at === "string" ? new Date(a.created_at).getTime() / 1000 : a.created_at, timestamp: typeof a.created_at === "string" ? new Date(a.created_at).getTime() / 1000 : a.created_at,
preimage: a.preimage, preimage: a.preimage,
state: WalletInvoiceState.Paid, state: WalletInvoiceState.Paid,
direction: a.type === "incoming" ? "in" : "out",
}) as WalletInvoice, }) as WalletInvoice,
) ?? [] ) ?? []
); );

View File

@ -80,6 +80,7 @@ export interface WalletInvoice {
timestamp: number; timestamp: number;
preimage?: string; preimage?: string;
state: WalletInvoiceState; state: WalletInvoiceState;
direction: "in" | "out";
} }
export function prToWalletInvoice(pr: string) { export function prToWalletInvoice(pr: string) {
@ -92,6 +93,7 @@ export function prToWalletInvoice(pr: string) {
timestamp: parsedInvoice.timestamp ?? 0, timestamp: parsedInvoice.timestamp ?? 0,
state: parsedInvoice.expired ? WalletInvoiceState.Expired : WalletInvoiceState.Pending, state: parsedInvoice.expired ? WalletInvoiceState.Expired : WalletInvoiceState.Pending,
pr, pr,
direction: "in",
} as WalletInvoice; } as WalletInvoice;
} }
} }
@ -163,6 +165,9 @@ export class WalletStore extends ExternalStore<WalletStoreSnapshot> {
this.notifyChange(); this.notifyChange();
}); });
return undefined; return undefined;
} else {
this.#instance.set(activeConfig.id, w);
this.notifyChange();
} }
return w; return w;
} else { } else {
@ -230,10 +235,10 @@ export class WalletStore extends ExternalStore<WalletStoreSnapshot> {
return new WebLNWallet(); return new WebLNWallet();
} }
case WalletKind.LNDHub: { case WalletKind.LNDHub: {
return new LNDHubWallet(unwrap(cfg.data)); return new LNDHubWallet(unwrap(cfg.data), () => this.notifyChange());
} }
case WalletKind.NWC: { case WalletKind.NWC: {
return new NostrConnectWallet(unwrap(cfg.data)); return new NostrConnectWallet(unwrap(cfg.data), () => this.notifyChange());
} }
} }
} }

View File

@ -454,5 +454,12 @@
<symbol id="medical-cross" viewBox="0 0 24 24" fill="none"> <symbol id="medical-cross" viewBox="0 0 24 24" fill="none">
<path d="M15 4.6C15 4.03995 15 3.75992 14.891 3.54601C14.7951 3.35785 14.6422 3.20487 14.454 3.10899C14.2401 3 13.9601 3 13.4 3H10.6C10.0399 3 9.75992 3 9.54601 3.10899C9.35785 3.20487 9.20487 3.35785 9.10899 3.54601C9 3.75992 9 4.03995 9 4.6V7.4C9 7.96005 9 8.24008 8.89101 8.45399C8.79513 8.64215 8.64215 8.79513 8.45399 8.89101C8.24008 9 7.96005 9 7.4 9H4.6C4.03995 9 3.75992 9 3.54601 9.10899C3.35785 9.20487 3.20487 9.35785 3.10899 9.54601C3 9.75992 3 10.0399 3 10.6V13.4C3 13.9601 3 14.2401 3.10899 14.454C3.20487 14.6422 3.35785 14.7951 3.54601 14.891C3.75992 15 4.03995 15 4.6 15H7.4C7.96005 15 8.24008 15 8.45399 15.109C8.64215 15.2049 8.79513 15.3578 8.89101 15.546C9 15.7599 9 16.0399 9 16.6V19.4C9 19.9601 9 20.2401 9.10899 20.454C9.20487 20.6422 9.35785 20.7951 9.54601 20.891C9.75992 21 10.0399 21 10.6 21H13.4C13.9601 21 14.2401 21 14.454 20.891C14.6422 20.7951 14.7951 20.6422 14.891 20.454C15 20.2401 15 19.9601 15 19.4V16.6C15 16.0399 15 15.7599 15.109 15.546C15.2049 15.3578 15.3578 15.2049 15.546 15.109C15.7599 15 16.0399 15 16.6 15H19.4C19.9601 15 20.2401 15 20.454 14.891C20.6422 14.7951 20.7951 14.6422 20.891 14.454C21 14.2401 21 13.9601 21 13.4V10.6C21 10.0399 21 9.75992 20.891 9.54601C20.7951 9.35785 20.6422 9.20487 20.454 9.10899C20.2401 9 19.9601 9 19.4 9L16.6 9C16.0399 9 15.7599 9 15.546 8.89101C15.3578 8.79513 15.2049 8.64215 15.109 8.45399C15 8.24008 15 7.96005 15 7.4V4.6Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M15 4.6C15 4.03995 15 3.75992 14.891 3.54601C14.7951 3.35785 14.6422 3.20487 14.454 3.10899C14.2401 3 13.9601 3 13.4 3H10.6C10.0399 3 9.75992 3 9.54601 3.10899C9.35785 3.20487 9.20487 3.35785 9.10899 3.54601C9 3.75992 9 4.03995 9 4.6V7.4C9 7.96005 9 8.24008 8.89101 8.45399C8.79513 8.64215 8.64215 8.79513 8.45399 8.89101C8.24008 9 7.96005 9 7.4 9H4.6C4.03995 9 3.75992 9 3.54601 9.10899C3.35785 9.20487 3.20487 9.35785 3.10899 9.54601C3 9.75992 3 10.0399 3 10.6V13.4C3 13.9601 3 14.2401 3.10899 14.454C3.20487 14.6422 3.35785 14.7951 3.54601 14.891C3.75992 15 4.03995 15 4.6 15H7.4C7.96005 15 8.24008 15 8.45399 15.109C8.64215 15.2049 8.79513 15.3578 8.89101 15.546C9 15.7599 9 16.0399 9 16.6V19.4C9 19.9601 9 20.2401 9.10899 20.454C9.20487 20.6422 9.35785 20.7951 9.54601 20.891C9.75992 21 10.0399 21 10.6 21H13.4C13.9601 21 14.2401 21 14.454 20.891C14.6422 20.7951 14.7951 20.6422 14.891 20.454C15 20.2401 15 19.9601 15 19.4V16.6C15 16.0399 15 15.7599 15.109 15.546C15.2049 15.3578 15.3578 15.2049 15.546 15.109C15.7599 15 16.0399 15 16.6 15H19.4C19.9601 15 20.2401 15 20.454 14.891C20.6422 14.7951 20.7951 14.6422 20.891 14.454C21 14.2401 21 13.9601 21 13.4V10.6C21 10.0399 21 9.75992 20.891 9.54601C20.7951 9.35785 20.6422 9.20487 20.454 9.10899C20.2401 9 19.9601 9 19.4 9L16.6 9C16.0399 9 15.7599 9 15.546 8.89101C15.3578 8.79513 15.2049 8.64215 15.109 8.45399C15 8.24008 15 7.96005 15 7.4V4.6Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol> </symbol>
<symbol id="arrow-up-right" viewBox="0 0 24 24" fill="none">
<path d="M6 18L18 6M18 6H10M18 6V14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="sats" viewBox="0 0 24 25" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M21 12.5C21 13.6819 20.7672 14.8522 20.3149 15.9442C19.8626 17.0361 19.1997 18.0282 18.364 18.864C17.5282 19.6997 16.5361 20.3626 15.4442 20.8149C14.3522 21.2672 13.1819 21.5 12 21.5C10.8181 21.5 9.64778 21.2672 8.55585 20.8149C7.46392 20.3626 6.47177 19.6997 5.63604 18.864C4.80031 18.0282 4.13738 17.0361 3.68508 15.9442C3.23279 14.8522 3 13.6819 3 12.5C3 10.1131 3.94821 7.82387 5.63604 6.13604C7.32387 4.44821 9.61305 3.5 12 3.5C14.3869 3.5 16.6761 4.44821 18.364 6.13604C20.0518 7.82387 21 10.1131 21 12.5ZM8.693 9.242L16.33 11.305L16.667 9.843L9.029 7.78L8.693 9.242ZM14.219 6.192L13.813 7.966L12.365 7.574L12.772 5.8L14.219 6.192ZM11.227 19.2L11.635 17.426L10.187 17.035L9.779 18.809L11.227 19.2ZM15.648 14.266L8.011 12.2L8.347 10.738L15.984 12.804L15.648 14.266ZM7.332 15.156L14.97 17.22L15.306 15.758L7.668 13.694L7.332 15.156Z" fill="currentColor"/>
</symbol>
</defs> </defs>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -480,22 +480,6 @@ input:disabled {
cursor: not-allowed; cursor: not-allowed;
} }
.f-1 {
flex: 1;
}
.f-2 {
flex: 2;
}
.f-3 {
flex: 3;
}
.f-4 {
flex: 4;
}
.f-ellipsis { .f-ellipsis {
min-width: 0; min-width: 0;
white-space: nowrap; white-space: nowrap;

View File

@ -64,6 +64,7 @@ import { setupWebLNWalletConfig } from "@/Wallet/WebLN";
import { Wallets } from "@/Wallet"; import { Wallets } from "@/Wallet";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import NetworkGraph from "@/Pages/NetworkGraph"; import NetworkGraph from "@/Pages/NetworkGraph";
import WalletPage from "./Pages/WalletPage";
declare global { declare global {
interface Window { interface Window {
@ -279,6 +280,14 @@ const mainRoutes = [
path: "/graph", path: "/graph",
element: <NetworkGraph />, element: <NetworkGraph />,
}, },
{
path: "/wallet",
element: (
<div className="p">
<WalletPage showHistory={true} />
</div>
),
},
...OnboardingRoutes, ...OnboardingRoutes,
...SettingsRoutes, ...SettingsRoutes,
] as Array<RouteObject>; ] as Array<RouteObject>;

View File

@ -156,6 +156,9 @@
"3KNMbJ": { "3KNMbJ": {
"defaultMessage": "Articles" "defaultMessage": "Articles"
}, },
"3QwfJR": {
"defaultMessage": "~{amount}"
},
"3cc4Ct": { "3cc4Ct": {
"defaultMessage": "Light" "defaultMessage": "Light"
}, },
@ -455,6 +458,9 @@
"Dx4ey3": { "Dx4ey3": {
"defaultMessage": "Toggle all" "defaultMessage": "Toggle all"
}, },
"E5ZIPD": {
"defaultMessage": "<big>{amount}</big> <small>sats</small>"
},
"EJbFi7": { "EJbFi7": {
"defaultMessage": "Search notes" "defaultMessage": "Search notes"
}, },
@ -945,9 +951,6 @@
"VL900k": { "VL900k": {
"defaultMessage": "Recommended Relays" "defaultMessage": "Recommended Relays"
}, },
"VN0+Fz": {
"defaultMessage": "Balance: {amount} sats"
},
"VOjC1i": { "VOjC1i": {
"defaultMessage": "Pick which upload service you want to upload attachments to" "defaultMessage": "Pick which upload service you want to upload attachments to"
}, },
@ -1130,10 +1133,6 @@
"d2ebEu": { "d2ebEu": {
"defaultMessage": "Not Subscribed to Push" "defaultMessage": "Not Subscribed to Push"
}, },
"d6CyG5": {
"defaultMessage": "History",
"description": "Wallet transation history"
},
"d7d0/x": { "d7d0/x": {
"defaultMessage": "LN Address" "defaultMessage": "LN Address"
}, },
@ -1468,6 +1467,10 @@
"puLNUJ": { "puLNUJ": {
"defaultMessage": "Pin" "defaultMessage": "Pin"
}, },
"pukxg/": {
"defaultMessage": "Payments",
"description": "Wallet transation history"
},
"pzTOmv": { "pzTOmv": {
"defaultMessage": "Followers" "defaultMessage": "Followers"
}, },
@ -1558,6 +1561,9 @@
"thnRpU": { "thnRpU": {
"defaultMessage": "Getting NIP-05 verified can help:" "defaultMessage": "Getting NIP-05 verified can help:"
}, },
"tj6kdX": {
"defaultMessage": "{sign} {amount} sats"
},
"tjpYlr": { "tjpYlr": {
"defaultMessage": "Relay Metrics" "defaultMessage": "Relay Metrics"
}, },
@ -1618,9 +1624,6 @@
"voxBKC": { "voxBKC": {
"defaultMessage": "Followed by friends" "defaultMessage": "Followed by friends"
}, },
"vrTOHJ": {
"defaultMessage": "{amount} sats"
},
"vxwnbh": { "vxwnbh": {
"defaultMessage": "Amount of work to apply to all published events" "defaultMessage": "Amount of work to apply to all published events"
}, },

View File

@ -51,6 +51,7 @@
"2zJXeA": "Profiles", "2zJXeA": "Profiles",
"39AHJm": "Sign Up", "39AHJm": "Sign Up",
"3KNMbJ": "Articles", "3KNMbJ": "Articles",
"3QwfJR": "~{amount}",
"3cc4Ct": "Light", "3cc4Ct": "Light",
"3gOsZq": "Translators", "3gOsZq": "Translators",
"3qnJlS": "You are voting with {amount} sats", "3qnJlS": "You are voting with {amount} sats",
@ -150,6 +151,7 @@
"DrZqav": "About must be less than {limit} characters", "DrZqav": "About must be less than {limit} characters",
"DtYelJ": "Transfer", "DtYelJ": "Transfer",
"Dx4ey3": "Toggle all", "Dx4ey3": "Toggle all",
"E5ZIPD": "<big>{amount}</big> <small>sats</small>",
"EJbFi7": "Search notes", "EJbFi7": "Search notes",
"ELbg9p": "Data Providers", "ELbg9p": "Data Providers",
"EQKRE4": "Show badges on profile pages", "EQKRE4": "Show badges on profile pages",
@ -311,7 +313,6 @@
"UrKTqQ": "You have an active iris.to account", "UrKTqQ": "You have an active iris.to account",
"UxgyeY": "Your referral code is {code}", "UxgyeY": "Your referral code is {code}",
"VL900k": "Recommended Relays", "VL900k": "Recommended Relays",
"VN0+Fz": "Balance: {amount} sats",
"VOjC1i": "Pick which upload service you want to upload attachments to", "VOjC1i": "Pick which upload service you want to upload attachments to",
"VR5eHw": "Public key (npub/nprofile)", "VR5eHw": "Public key (npub/nprofile)",
"VcwrfF": "Yes please", "VcwrfF": "Yes please",
@ -372,7 +373,6 @@
"cyR7Kh": "Back", "cyR7Kh": "Back",
"d+6YsV": "Lists to mute:", "d+6YsV": "Lists to mute:",
"d2ebEu": "Not Subscribed to Push", "d2ebEu": "Not Subscribed to Push",
"d6CyG5": "History",
"d7d0/x": "LN Address", "d7d0/x": "LN Address",
"d8gpCh": "Try to use less than 5 hashtags to stay on topic 🙏", "d8gpCh": "Try to use less than 5 hashtags to stay on topic 🙏",
"dOQCL8": "Display name", "dOQCL8": "Display name",
@ -484,6 +484,7 @@
"pI+77w": "Downloadable backups from Snort relay", "pI+77w": "Downloadable backups from Snort relay",
"pRess9": "ZapPool", "pRess9": "ZapPool",
"puLNUJ": "Pin", "puLNUJ": "Pin",
"pukxg/": "Payments",
"pzTOmv": "Followers", "pzTOmv": "Followers",
"qD9EUF": "Email <> DM bridge for your Snort nostr address", "qD9EUF": "Email <> DM bridge for your Snort nostr address",
"qDwvZ4": "Unknown error", "qDwvZ4": "Unknown error",
@ -514,6 +515,7 @@
"tOdNiY": "Dark", "tOdNiY": "Dark",
"th5lxp": "Send note to a subset of your write relays", "th5lxp": "Send note to a subset of your write relays",
"thnRpU": "Getting NIP-05 verified can help:", "thnRpU": "Getting NIP-05 verified can help:",
"tj6kdX": "{sign} {amount} sats",
"tjpYlr": "Relay Metrics", "tjpYlr": "Relay Metrics",
"ttxS0b": "Supporter Badge", "ttxS0b": "Supporter Badge",
"u+LyXc": "Interactions", "u+LyXc": "Interactions",
@ -534,7 +536,6 @@
"vhlWFg": "Poll Options", "vhlWFg": "Poll Options",
"vlbWtt": "Get a free one", "vlbWtt": "Get a free one",
"voxBKC": "Followed by friends", "voxBKC": "Followed by friends",
"vrTOHJ": "{amount} sats",
"vxwnbh": "Amount of work to apply to all published events", "vxwnbh": "Amount of work to apply to all published events",
"w1Fanr": "Business", "w1Fanr": "Business",
"w6qrwX": "NSFW", "w6qrwX": "NSFW",