feat: basic wallet
This commit is contained in:
parent
dc31ba9ad3
commit
7db8960914
21
packages/app/src/Icons/Bitcoin.tsx
Normal file
21
packages/app/src/Icons/Bitcoin.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
const Bitcoin = () => {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="22"
|
||||
viewBox="0 0 16 22"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.5 1V3M5.5 19V21M9.5 1V3M9.5 19V21M3.5 3H10C12.2091 3 14 4.79086 14 7C14 9.20914 12.2091 11 10 11H3.5H11C13.2091 11 15 12.7909 15 15C15 17.2091 13.2091 19 11 19H3.5M3.5 3H1.5M3.5 3V19M3.5 19H1.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Bitcoin;
|
@ -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 (
|
||||
<div className="header-actions">
|
||||
<div className="btn btn-rnd" onClick={() => navigate("/wallet")}>
|
||||
<Bitcoin />
|
||||
</div>
|
||||
<div className="btn btn-rnd" onClick={() => navigate("/search")}>
|
||||
<Search />
|
||||
</div>
|
||||
|
16
packages/app/src/Pages/WalletPage.css
Normal file
16
packages/app/src/Pages/WalletPage.css
Normal file
@ -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;
|
||||
}
|
88
packages/app/src/Pages/WalletPage.tsx
Normal file
88
packages/app/src/Pages/WalletPage.tsx
Normal file
@ -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: <WalletPage />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function WalletPage() {
|
||||
const [info, setInfo] = useState<WalletInfo>();
|
||||
const [balance, setBalance] = useState<Sats>();
|
||||
const [history, setHistory] = useState<WalletInvoice[]>();
|
||||
|
||||
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 <FontAwesomeIcon icon={faClock} className="mr5" />;
|
||||
case WalletInvoiceState.Paid:
|
||||
return <FontAwesomeIcon icon={faCheck} className="mr5" />;
|
||||
case WalletInvoiceState.Expired:
|
||||
return <FontAwesomeIcon icon={faXmark} className="mr5" />;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h3>{info?.alias}</h3>
|
||||
<b>Balance: {(balance ?? 0).toLocaleString()} sats</b>
|
||||
<div className="flex wallet-buttons">
|
||||
<button>Send</button>
|
||||
<button>Receive</button>
|
||||
</div>
|
||||
<h3>History</h3>
|
||||
{history?.map((a) => (
|
||||
<div className="card flex wallet-history-item" key={a.timestamp}>
|
||||
<div className="f-grow f-col">
|
||||
<NoteTime from={a.timestamp * 1000} />
|
||||
<div>{(a.memo ?? "").length === 0 ? <> </> : a.memo}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
a.state === WalletInvoiceState.Paid ? "success" : "pending"
|
||||
}`}
|
||||
>
|
||||
{stateIcon(a.state)}
|
||||
{a.amount.toLocaleString()} sats
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
8
packages/app/src/Pages/messages.js
Normal file
8
packages/app/src/Pages/messages.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { defineMessages } from "react-intl";
|
||||
import { addIdAndDefaultMessageToMessages } from "Util";
|
||||
|
||||
const messages = defineMessages({
|
||||
Login: "Login",
|
||||
});
|
||||
|
||||
export default addIdAndDefaultMessageToMessages(messages, "Pages");
|
134
packages/app/src/Wallet/LNDHub.ts
Normal file
134
packages/app/src/Wallet/LNDHub.ts
Normal file
@ -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<WalletInfo>("GET", "/getinfo");
|
||||
}
|
||||
|
||||
async login() {
|
||||
const rsp = await this.getJson<AuthResponse>("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<Sats | WalletError> {
|
||||
let rsp = await this.getJson<GetBalanceResponse>("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<WalletInvoice[] | WalletError> {
|
||||
let rsp = await this.getJson<GetUserInvoicesResponse[]>(
|
||||
"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<T>(
|
||||
method: "GET" | "POST",
|
||||
path: string,
|
||||
body?: any
|
||||
): Promise<T | WalletError> {
|
||||
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;
|
||||
}
|
81
packages/app/src/Wallet/index.ts
Normal file
81
packages/app/src/Wallet/index.ts
Normal file
@ -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<void>;
|
||||
load: () => Promise<void>;
|
||||
}
|
||||
|
||||
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<Login | WalletError>;
|
||||
getInfo: () => Promise<WalletInfo | WalletError>;
|
||||
login: () => Promise<boolean | WalletError>;
|
||||
getBalance: () => Promise<Sats | WalletError>;
|
||||
createInvoice: (req: InvoiceRequest) => Promise<WalletInvoice | WalletError>;
|
||||
payInvoice: (pr: string) => Promise<WalletInvoice | WalletError>;
|
||||
getInvoices: () => Promise<WalletInvoice[] | WalletError>;
|
||||
}
|
||||
|
||||
export async function openWallet(config: string) {
|
||||
let wallet = new LNDHubWallet(config);
|
||||
await wallet.login();
|
||||
return wallet;
|
||||
}
|
@ -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: <NostrLinkHandler />,
|
||||
},
|
||||
...NewUserRoutes,
|
||||
...WalletRoutes,
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
Loading…
x
Reference in New Issue
Block a user