diff --git a/packages/app/src/Icons/Bitcoin.tsx b/packages/app/src/Icons/Bitcoin.tsx
new file mode 100644
index 00000000..807496fb
--- /dev/null
+++ b/packages/app/src/Icons/Bitcoin.tsx
@@ -0,0 +1,21 @@
+const Bitcoin = () => {
+ return (
+
+ );
+};
+
+export default Bitcoin;
diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx
index f30033ca..dfa95165 100644
--- a/packages/app/src/Pages/Layout.tsx
+++ b/packages/app/src/Pages/Layout.tsx
@@ -23,6 +23,7 @@ import Plus from "Icons/Plus";
import { RelaySettings } from "@snort/nostr";
import { FormattedMessage } from "react-intl";
import messages from "./messages";
+import Bitcoin from "Icons/Bitcoin";
export default function Layout() {
const location = useLocation();
@@ -44,6 +45,9 @@ export default function Layout() {
const [pageClass, setPageClass] = useState("page");
const pub = useEventPublisher();
useLoginFeed();
+ useEffect(() => {
+ System.nip42Auth = pub.nip42Auth;
+ }, [pub]);
const shouldHideNoteCreator = useMemo(() => {
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/p/"];
@@ -197,6 +201,9 @@ export default function Layout() {
function accountHeader() {
return (
+
navigate("/wallet")}>
+
+
navigate("/search")}>
diff --git a/packages/app/src/Pages/WalletPage.css b/packages/app/src/Pages/WalletPage.css
new file mode 100644
index 00000000..b6cf0090
--- /dev/null
+++ b/packages/app/src/Pages/WalletPage.css
@@ -0,0 +1,16 @@
+.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;
+}
diff --git a/packages/app/src/Pages/WalletPage.tsx b/packages/app/src/Pages/WalletPage.tsx
new file mode 100644
index 00000000..64c148fb
--- /dev/null
+++ b/packages/app/src/Pages/WalletPage.tsx
@@ -0,0 +1,88 @@
+import "./WalletPage.css";
+
+import { useEffect, useState } from "react";
+import { RouteObject } from "react-router-dom";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faCheck, faClock, faXmark } from "@fortawesome/free-solid-svg-icons";
+
+import NoteTime from "Element/NoteTime";
+import {
+ openWallet,
+ WalletInvoice,
+ Sats,
+ WalletInfo,
+ WalletInvoiceState,
+} from "Wallet";
+
+export const WalletRoutes: RouteObject[] = [
+ {
+ path: "/wallet",
+ element:
,
+ },
+];
+
+export default function WalletPage() {
+ const [info, setInfo] = useState
();
+ const [balance, setBalance] = useState();
+ const [history, setHistory] = useState();
+
+ async function loadWallet() {
+ let cfg = window.localStorage.getItem("wallet-lndhub");
+ if (cfg) {
+ let wallet = await openWallet(cfg);
+ let i = await wallet.getInfo();
+ if ("error" in i) {
+ return;
+ }
+ setInfo(i as WalletInfo);
+ let b = await wallet.getBalance();
+ setBalance(b as Sats);
+ let h = await wallet.getInvoices();
+ setHistory(
+ (h as WalletInvoice[]).sort((a, b) => b.timestamp - a.timestamp)
+ );
+ }
+ }
+
+ useEffect(() => {
+ loadWallet().catch(console.warn);
+ }, []);
+
+ function stateIcon(s: WalletInvoiceState) {
+ switch (s) {
+ case WalletInvoiceState.Pending:
+ return ;
+ case WalletInvoiceState.Paid:
+ return ;
+ case WalletInvoiceState.Expired:
+ return ;
+ }
+ }
+ return (
+ <>
+ {info?.alias}
+ Balance: {(balance ?? 0).toLocaleString()} sats
+
+
+
+
+ History
+ {history?.map((a) => (
+
+
+
+
{(a.memo ?? "").length === 0 ? <> > : a.memo}
+
+
+ {stateIcon(a.state)}
+ {a.amount.toLocaleString()} sats
+
+
+ ))}
+ >
+ );
+}
diff --git a/packages/app/src/Pages/messages.js b/packages/app/src/Pages/messages.js
new file mode 100644
index 00000000..87a38ad9
--- /dev/null
+++ b/packages/app/src/Pages/messages.js
@@ -0,0 +1,8 @@
+import { defineMessages } from "react-intl";
+import { addIdAndDefaultMessageToMessages } from "Util";
+
+const messages = defineMessages({
+ Login: "Login",
+});
+
+export default addIdAndDefaultMessageToMessages(messages, "Pages");
diff --git a/packages/app/src/Wallet/LNDHub.ts b/packages/app/src/Wallet/LNDHub.ts
new file mode 100644
index 00000000..0a35dd5a
--- /dev/null
+++ b/packages/app/src/Wallet/LNDHub.ts
@@ -0,0 +1,134 @@
+import {
+ InvoiceRequest,
+ LNWallet,
+ Sats,
+ UnknownWalletError,
+ WalletError,
+ WalletInfo,
+ WalletInvoice,
+ WalletInvoiceState,
+} from "Wallet";
+
+const defaultHeaders = {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+};
+
+export default class LNDHubWallet implements LNWallet {
+ url: string;
+ user: string;
+ password: string;
+ auth?: AuthResponse;
+
+ constructor(url: string) {
+ 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]).toString();
+ this.user = parsedUrl[1];
+ this.password = parsedUrl[2];
+ }
+
+ async createAccount() {
+ return Promise.resolve(UnknownWalletError);
+ }
+
+ async getInfo() {
+ return await this.getJson("GET", "/getinfo");
+ }
+
+ async login() {
+ const rsp = await this.getJson("POST", "/auth?type=auth", {
+ login: this.user,
+ password: this.password,
+ });
+
+ if ("error" in rsp) {
+ return rsp as WalletError;
+ }
+ this.auth = rsp as AuthResponse;
+ return true;
+ }
+
+ async getBalance(): Promise {
+ let rsp = await this.getJson("GET", "/balance");
+ if ("error" in rsp) {
+ return rsp as WalletError;
+ }
+ let bal = Math.floor((rsp as GetBalanceResponse).BTC.AvailableBalance);
+ return bal as Sats;
+ }
+
+ async createInvoice(req: InvoiceRequest) {
+ return Promise.resolve(UnknownWalletError);
+ }
+
+ async payInvoice(pr: string) {
+ return Promise.resolve(UnknownWalletError);
+ }
+
+ async getInvoices(): Promise {
+ let rsp = await this.getJson(
+ "GET",
+ "/getuserinvoices"
+ );
+ if ("error" in rsp) {
+ return rsp as WalletError;
+ }
+ return (rsp as GetUserInvoicesResponse[]).map((a) => {
+ return {
+ memo: a.description,
+ amount: Math.floor(a.amt),
+ timestamp: a.timestamp,
+ state: a.ispaid ? WalletInvoiceState.Paid : WalletInvoiceState.Pending,
+ pr: a.payment_request,
+ paymentHash: a.payment_hash,
+ } as WalletInvoice;
+ });
+ }
+
+ private async getJson(
+ method: "GET" | "POST",
+ path: string,
+ body?: any
+ ): Promise {
+ const rsp = await fetch(`${this.url}${path}`, {
+ method: method,
+ body: body ? JSON.stringify(body) : undefined,
+ headers: {
+ ...defaultHeaders,
+ Authorization: `Bearer ${this.auth?.access_token}`,
+ },
+ });
+ const json = await rsp.json();
+ if ("error" in json) {
+ return json as WalletError;
+ }
+ return json as T;
+ }
+}
+
+interface AuthResponse {
+ refresh_token?: string;
+ access_token?: string;
+ token_type?: string;
+}
+
+interface GetBalanceResponse {
+ BTC: {
+ AvailableBalance: number;
+ };
+}
+
+interface GetUserInvoicesResponse {
+ amt: number;
+ description: string;
+ ispaid: boolean;
+ type: string;
+ timestamp: number;
+ pay_req: string;
+ payment_hash: string;
+ payment_request: string;
+}
diff --git a/packages/app/src/Wallet/index.ts b/packages/app/src/Wallet/index.ts
new file mode 100644
index 00000000..101d3097
--- /dev/null
+++ b/packages/app/src/Wallet/index.ts
@@ -0,0 +1,81 @@
+import LNDHubWallet from "./LNDHub";
+
+export enum WalletErrorCode {
+ BadAuth = 1,
+ NotEnoughBalance = 2,
+ BadPartner = 3,
+ InvalidInvoice = 4,
+ RouteNotFound = 5,
+ GeneralError = 6,
+ NodeFailure = 7,
+}
+
+export interface WalletError {
+ code: WalletErrorCode;
+ message: string;
+}
+
+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;
+ load: () => Promise;
+}
+
+export interface InvoiceRequest {
+ amount: number;
+ memo?: string;
+ expiry?: number;
+}
+
+export enum WalletInvoiceState {
+ Pending = 0,
+ Paid = 1,
+ Expired = 2,
+}
+
+export interface WalletInvoice {
+ pr: string;
+ paymentHash: string;
+ memo: string;
+ amount: number;
+ fees: number;
+ timestamp: number;
+ state: WalletInvoiceState;
+}
+
+export type Sats = number;
+
+export interface LNWallet {
+ createAccount: () => Promise;
+ getInfo: () => Promise;
+ login: () => Promise;
+ getBalance: () => Promise;
+ createInvoice: (req: InvoiceRequest) => Promise;
+ payInvoice: (pr: string) => Promise;
+ getInvoices: () => Promise;
+}
+
+export async function openWallet(config: string) {
+ let wallet = new LNDHubWallet(config);
+ await wallet.login();
+ return wallet;
+}
diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx
index 585ff3e8..f15f79c5 100644
--- a/packages/app/src/index.tsx
+++ b/packages/app/src/index.tsx
@@ -8,6 +8,7 @@ import { Provider } from "react-redux";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import * as serviceWorkerRegistration from "serviceWorkerRegistration";
+import { IntlProvider } from "IntlProvider";
import Store from "State/Store";
import EventPage from "Pages/EventPage";
import Layout from "Pages/Layout";
@@ -28,6 +29,7 @@ import { NewUserRoutes } from "Pages/new";
import NostrLinkHandler from "Pages/NostrLinkHandler";
import { IntlProvider } from "./IntlProvider";
import { unwrap } from "Util";
+import { WalletRoutes } from "Pages/WalletPage";
/**
* HTTP query provider
@@ -99,6 +101,7 @@ export const router = createBrowserRouter([
element: ,
},
...NewUserRoutes,
+ ...WalletRoutes,
],
},
]);