This commit is contained in:
Kieran 2023-02-13 15:29:25 +00:00
parent 7db8960914
commit 1b363ec15f
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 171 additions and 85 deletions

View File

@ -19,6 +19,7 @@ import { LNURL, LNURLError, LNURLErrorCode, LNURLInvoice, LNURLSuccessAction } f
import { debounce } from "Util"; import { debounce } from "Util";
import messages from "./messages"; import messages from "./messages";
import { openWallet } from "Wallet";
enum ZapType { enum ZapType {
PublicZap = 1, PublicZap = 1,
@ -297,6 +298,16 @@ export default function SendSats(props: SendSatsProps) {
</> </>
); );
} }
async function payWithWallet(pr: string) {
const cfg = window.localStorage.getItem("wallet-lndhub");
if (cfg) {
const wallet = await openWallet(cfg);
const rsp = await wallet.payInvoice(pr);
console.debug(rsp);
setSuccess(rsp as LNURLSuccessAction);
}
}
function payInvoice() { function payInvoice() {
if (success || !invoice) return null; if (success || !invoice) return null;
@ -321,6 +332,9 @@ export default function SendSats(props: SendSatsProps) {
<button className="wallet-action" type="button" onClick={() => window.open(`lightning:${invoice}`)}> <button className="wallet-action" type="button" onClick={() => window.open(`lightning:${invoice}`)}>
<FormattedMessage {...messages.OpenWallet} /> <FormattedMessage {...messages.OpenWallet} />
</button> </button>
<button className="wallet-action" type="button" onClick={() => payWithWallet(pr)}>
<FormattedMessage defaultMessage="Pay with Snort" />
</button>
</> </>
)} )}
</div> </div>
@ -351,9 +365,9 @@ export default function SendSats(props: SendSatsProps) {
const defaultTitle = handler?.canZap ? formatMessage(messages.SendZap) : formatMessage(messages.SendSats); const defaultTitle = handler?.canZap ? formatMessage(messages.SendZap) : formatMessage(messages.SendSats);
const title = target const title = target
? formatMessage(messages.ToTarget, { ? formatMessage(messages.ToTarget, {
action: defaultTitle, action: defaultTitle,
target, target,
}) })
: defaultTitle; : defaultTitle;
if (!(props.show ?? false)) return null; if (!(props.show ?? false)) return null;
return ( return (

View File

@ -8,6 +8,7 @@ import { HexKey, RawEvent, u256, UserMetadata, Lists } from "@snort/nostr";
import { bech32ToHex, delay, unwrap } from "Util"; import { bech32ToHex, delay, unwrap } from "Util";
import { DefaultRelays, HashtagRegex } from "Const"; import { DefaultRelays, HashtagRegex } from "Const";
import { System } from "System"; import { System } from "System";
import { useMemo } from "react";
declare global { declare global {
interface Window { interface Window {
@ -23,6 +24,8 @@ declare global {
} }
} }
export type EventPublisher = ReturnType<typeof useEventPublisher>;
export default function useEventPublisher() { export default function useEventPublisher() {
const pubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey); const pubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey); const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
@ -78,7 +81,7 @@ export default function useEventPublisher() {
ev.Content = content; ev.Content = content;
} }
return { const ret = {
nip42Auth: async (challenge: string, relay: string) => { nip42Auth: async (challenge: string, relay: string) => {
if (pubKey) { if (pubKey) {
const ev = NEvent.ForPubKey(pubKey); const ev = NEvent.ForPubKey(pubKey);
@ -393,7 +396,17 @@ export default function useEventPublisher() {
publicKey: pubKey, publicKey: pubKey,
}; };
}, },
generic: async (content: string, kind: EventKind) => {
if (pubKey) {
const ev = NEvent.ForPubKey(pubKey);
ev.Kind = kind;
ev.Content = content;
return await signEvent(ev);
}
},
}; };
return useMemo(() => ret, [pubKey, relays, follows]);
} }
let isNip07Busy = false; let isNip07Busy = false;

View File

@ -1,12 +1,6 @@
const Bitcoin = () => { const Bitcoin = () => {
return ( return (
<svg <svg width="16" height="22" viewBox="0 0 16 22" fill="none" xmlns="http://www.w3.org/2000/svg">
width="16"
height="22"
viewBox="0 0 16 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path <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" 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" stroke="currentColor"

View File

@ -6,13 +6,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck, faClock, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faCheck, faClock, faXmark } from "@fortawesome/free-solid-svg-icons";
import NoteTime from "Element/NoteTime"; import NoteTime from "Element/NoteTime";
import { import { openWallet, WalletInvoice, Sats, WalletInfo, WalletInvoiceState, useWallet, LNWallet } from "Wallet";
openWallet,
WalletInvoice,
Sats,
WalletInfo,
WalletInvoiceState,
} from "Wallet";
export const WalletRoutes: RouteObject[] = [ export const WalletRoutes: RouteObject[] = [
{ {
@ -25,28 +19,25 @@ export default function WalletPage() {
const [info, setInfo] = useState<WalletInfo>(); 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 wallet = useWallet();
async function loadWallet() { async function loadWallet(wallet: LNWallet) {
let cfg = window.localStorage.getItem("wallet-lndhub"); const i = await wallet.getInfo();
if (cfg) { if ("error" in i) {
let wallet = await openWallet(cfg); return;
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)
);
} }
setInfo(i as WalletInfo);
const b = await wallet.getBalance();
setBalance(b as Sats);
const h = await wallet.getInvoices();
setHistory((h as WalletInvoice[]).sort((a, b) => b.timestamp - a.timestamp));
} }
useEffect(() => { useEffect(() => {
loadWallet().catch(console.warn); if (wallet) {
}, []); loadWallet(wallet).catch(console.warn);
}
}, [wallet]);
function stateIcon(s: WalletInvoiceState) { function stateIcon(s: WalletInvoiceState) {
switch (s) { switch (s) {
@ -58,26 +49,35 @@ export default function WalletPage() {
return <FontAwesomeIcon icon={faXmark} className="mr5" />; return <FontAwesomeIcon icon={faXmark} className="mr5" />;
} }
} }
async function createInvoice() {
const cfg = window.localStorage.getItem("wallet-lndhub");
if (cfg) {
const wallet = await openWallet(cfg);
const rsp = await wallet.createInvoice({
memo: "test",
amount: 100,
});
console.debug(rsp);
}
}
return ( return (
<> <>
<h3>{info?.alias}</h3> <h3>{info?.alias}</h3>
<b>Balance: {(balance ?? 0).toLocaleString()} sats</b> <b>Balance: {(balance ?? 0).toLocaleString()} sats</b>
<div className="flex wallet-buttons"> <div className="flex wallet-buttons">
<button>Send</button> <button>Send</button>
<button>Receive</button> <button onClick={() => createInvoice()}>Receive</button>
</div> </div>
<h3>History</h3> <h3>History</h3>
{history?.map((a) => ( {history?.map(a => (
<div className="card flex wallet-history-item" key={a.timestamp}> <div className="card flex wallet-history-item" key={a.timestamp}>
<div className="f-grow f-col"> <div className="f-grow f-col">
<NoteTime from={a.timestamp * 1000} /> <NoteTime from={a.timestamp * 1000} />
<div>{(a.memo ?? "").length === 0 ? <>&nbsp;</> : a.memo}</div> <div>{(a.memo ?? "").length === 0 ? <>&nbsp;</> : a.memo}</div>
</div> </div>
<div <div className={`${a.state === WalletInvoiceState.Paid ? "success" : "pending"}`}>
className={`${
a.state === WalletInvoiceState.Paid ? "success" : "pending"
}`}
>
{stateIcon(a.state)} {stateIcon(a.state)}
{a.amount.toLocaleString()} sats {a.amount.toLocaleString()} sats
</div> </div>

View File

@ -1,8 +0,0 @@
import { defineMessages } from "react-intl";
import { addIdAndDefaultMessageToMessages } from "Util";
const messages = defineMessages({
Login: "Login",
});
export default addIdAndDefaultMessageToMessages(messages, "Pages");

View File

@ -1,3 +1,5 @@
import { EventPublisher } from "Feed/EventPublisher";
import EventKind from "Nostr/EventKind";
import { import {
InvoiceRequest, InvoiceRequest,
LNWallet, LNWallet,
@ -15,20 +17,35 @@ const defaultHeaders = {
}; };
export default class LNDHubWallet implements LNWallet { export default class LNDHubWallet implements LNWallet {
type: "lndhub" | "snort";
url: string; url: string;
user: string; user: string;
password: string; password: string;
auth?: AuthResponse; auth?: AuthResponse;
publisher?: EventPublisher;
constructor(url: string) { constructor(url: string, publisher?: EventPublisher) {
const regex = /^lndhub:\/\/([\S-]+):([\S-]+)@(.*)$/i; if (url.startsWith("lndhub://")) {
const parsedUrl = url.match(regex); const regex = /^lndhub:\/\/([\S-]+):([\S-]+)@(.*)$/i;
if (!parsedUrl || parsedUrl.length !== 4) { const parsedUrl = url.match(regex);
throw new Error("Invalid LNDHUB config"); console.debug(parsedUrl);
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];
this.type = "lndhub";
} else if (url.startsWith("snort://")) {
const u = new URL(url);
this.url = `https://${u.host}${u.pathname}`;
this.user = "";
this.password = "";
this.type = "snort";
this.publisher = publisher;
} else {
throw new Error("Invalid config");
} }
this.url = new URL(parsedUrl[3]).toString();
this.user = parsedUrl[1];
this.password = parsedUrl[2];
} }
async createAccount() { async createAccount() {
@ -40,6 +57,8 @@ export default class LNDHubWallet implements LNWallet {
} }
async login() { async login() {
if (this.type === "snort") 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,
@ -53,31 +72,56 @@ export default class LNDHubWallet implements LNWallet {
} }
async getBalance(): Promise<Sats | WalletError> { async getBalance(): Promise<Sats | WalletError> {
let rsp = await this.getJson<GetBalanceResponse>("GET", "/balance"); const rsp = await this.getJson<GetBalanceResponse>("GET", "/balance");
if ("error" in rsp) { if ("error" in rsp) {
return rsp as WalletError; return rsp as WalletError;
} }
let 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) {
return Promise.resolve(UnknownWalletError); const rsp = await this.getJson<UserInvoicesResponse>("POST", "/addinvoice", {
} amt: req.amount,
memo: req.memo,
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) { if ("error" in rsp) {
return rsp as WalletError; return rsp as WalletError;
} }
return (rsp as GetUserInvoicesResponse[]).map((a) => {
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) {
const rsp = await this.getJson<PayInvoiceResponse>("POST", "/payinvoice", {
invoice: pr,
});
if ("error" in rsp) {
return rsp as WalletError;
}
const pRsp = rsp as PayInvoiceResponse;
return {
pr: pr,
paymentHash: pRsp.payment_hash,
state: pRsp.payment_error === undefined ? WalletInvoiceState.Paid : WalletInvoiceState.Pending,
} as WalletInvoice;
}
async getInvoices(): Promise<WalletInvoice[] | WalletError> {
const rsp = await this.getJson<UserInvoicesResponse[]>("GET", "/getuserinvoices");
if ("error" in rsp) {
return rsp as WalletError;
}
return (rsp as UserInvoicesResponse[]).map(a => {
return { return {
memo: a.description, memo: a.description,
amount: Math.floor(a.amt), amount: Math.floor(a.amt),
@ -89,17 +133,18 @@ export default class LNDHubWallet implements LNWallet {
}); });
} }
private async getJson<T>( private async getJson<T>(method: "GET" | "POST", path: string, body?: any): Promise<T | WalletError> {
method: "GET" | "POST", let auth = `Bearer ${this.auth?.access_token}`;
path: string, if (this.type === "snort") {
body?: any const ev = await this.publisher?.generic(`${new URL(this.url).pathname}${path}`, EventKind.Ephemeral);
): Promise<T | WalletError> { auth = JSON.stringify(ev?.ToObject());
}
const rsp = await fetch(`${this.url}${path}`, { const rsp = await fetch(`${this.url}${path}`, {
method: method, method: method,
body: body ? JSON.stringify(body) : undefined, body: body ? JSON.stringify(body) : undefined,
headers: { headers: {
...defaultHeaders, ...defaultHeaders,
Authorization: `Bearer ${this.auth?.access_token}`, Authorization: auth,
}, },
}); });
const json = await rsp.json(); const json = await rsp.json();
@ -122,7 +167,7 @@ interface GetBalanceResponse {
}; };
} }
interface GetUserInvoicesResponse { interface UserInvoicesResponse {
amt: number; amt: number;
description: string; description: string;
ispaid: boolean; ispaid: boolean;
@ -131,4 +176,12 @@ interface GetUserInvoicesResponse {
pay_req: string; pay_req: string;
payment_hash: string; payment_hash: string;
payment_request: 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 };
} }

View File

@ -1,3 +1,5 @@
import useEventPublisher, { EventPublisher } from "Feed/EventPublisher";
import { useEffect, useState } from "react";
import LNDHubWallet from "./LNDHub"; import LNDHubWallet from "./LNDHub";
export enum WalletErrorCode { export enum WalletErrorCode {
@ -74,8 +76,26 @@ export interface LNWallet {
getInvoices: () => Promise<WalletInvoice[] | WalletError>; getInvoices: () => Promise<WalletInvoice[] | WalletError>;
} }
export async function openWallet(config: string) { export async function openWallet(config: string, publisher?: EventPublisher) {
let wallet = new LNDHubWallet(config); const wallet = new LNDHubWallet(config, publisher);
await wallet.login(); await wallet.login();
return wallet; return wallet;
} }
export function useWallet() {
const [wallet, setWallet] = useState<LNWallet>();
const publisher = useEventPublisher();
useEffect(() => {
if (publisher) {
const cfg = window.localStorage.getItem("wallet-lndhub");
if (cfg) {
openWallet(cfg, publisher)
.then(a => setWallet(a))
.catch(console.error);
}
}
}, [publisher]);
return wallet;
}

View File

@ -26,10 +26,9 @@ import HashTagsPage from "Pages/HashTagsPage";
import SearchPage from "Pages/SearchPage"; import SearchPage from "Pages/SearchPage";
import HelpPage from "Pages/HelpPage"; import HelpPage from "Pages/HelpPage";
import { NewUserRoutes } from "Pages/new"; import { NewUserRoutes } from "Pages/new";
import NostrLinkHandler from "Pages/NostrLinkHandler";
import { IntlProvider } from "./IntlProvider";
import { unwrap } from "Util";
import { WalletRoutes } from "Pages/WalletPage"; import { WalletRoutes } from "Pages/WalletPage";
import NostrLinkHandler from "Pages/NostrLinkHandler";
import { unwrap } from "Util";
/** /**
* HTTP query provider * HTTP query provider

View File

@ -9,6 +9,7 @@ enum EventKind {
Repost = 6, // NIP-18 Repost = 6, // NIP-18
Reaction = 7, // NIP-25 Reaction = 7, // NIP-25
Relays = 10002, // NIP-65 Relays = 10002, // NIP-65
Ephemeral = 20_000,
Auth = 22242, // NIP-42 Auth = 22242, // NIP-42
PubkeyLists = 30000, // NIP-51a PubkeyLists = 30000, // NIP-51a
NoteLists = 30001, // NIP-51b NoteLists = 30001, // NIP-51b