Add bitcoin connect (#215)

* feat: add bitcoin connect

* feat: improve zap screen
This commit is contained in:
雨宮蓮 2024-06-21 14:56:10 +07:00 committed by GitHub
parent 1283432632
commit 3fbd66dece
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 545 additions and 445 deletions

View File

@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@getalby/bitcoin-connect-react": "^3.5.3",
"@lume/icons": "workspace:^", "@lume/icons": "workspace:^",
"@lume/system": "workspace:^", "@lume/system": "workspace:^",
"@lume/ui": "workspace:^", "@lume/ui": "workspace:^",

View File

@ -19,42 +19,33 @@ function Screen() {
return ( return (
<Container withDrag> <Container withDrag>
<div className="h-full w-full flex-1 px-5"> <div className="flex-1 w-full h-full px-5">
{!isDone ? ( <div className="flex flex-col gap-2">
<> <div>
<div className="flex flex-col gap-2"> <h3 className="text-2xl font-light">
<div className="inline-flex size-14 items-center justify-center rounded-xl bg-black text-white shadow-md"> Connect <span className="font-semibold">bitcoin wallet</span> to
<ZapIcon className="size-5" /> start zapping to your favorite content and creator.
</div> </h3>
<div> </div>
<h3 className="text-2xl font-light"> </div>
Connect <span className="font-semibold">bitcoin wallet</span>{" "} <div className="flex flex-col gap-2 mt-10">
to start zapping to your favorite content and creator. <div className="flex flex-col gap-1.5">
</h3> <label>Paste a Nostr Wallet Connect connection string</label>
</div> <textarea
</div> value={uri}
<div className="mt-10 flex flex-col gap-2"> onChange={(e) => setUri(e.target.value)}
<div className="flex flex-col gap-1.5"> placeholder="nostrconnect://"
<label>Paste a Nostr Wallet Connect connection string</label> className="w-full h-24 px-3 bg-transparent rounded-lg border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
<textarea />
value={uri} </div>
onChange={(e) => setUri(e.target.value)} <button
placeholder="nostrconnect://" type="button"
className="h-24 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" onClick={save}
/> className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
</div> >
<button Save & Connect
type="button" </button>
onClick={save} </div>
className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
>
Save & Connect
</button>
</div>
</>
) : (
<div>Done</div>
)}
</div> </div>
</Container> </Container>
); );

View File

@ -79,7 +79,7 @@ function Screen() {
); );
}} }}
</Link> </Link>
<Link to="/settings/zap"> <Link to="/settings/wallet">
{({ isActive }) => { {({ isActive }) => {
return ( return (
<div <div
@ -91,9 +91,7 @@ function Screen() {
)} )}
> >
<ZapIcon className="size-5 shrink-0" /> <ZapIcon className="size-5 shrink-0" />
<p className="text-sm font-medium"> <p className="text-sm font-medium">Wallet</p>
{t("settings.zap.title")}
</p>
</div> </div>
); );
}} }}

View File

@ -0,0 +1,39 @@
import { Button, init } from "@getalby/bitcoin-connect-react";
import { NostrAccount } from "@lume/system";
import { createFileRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
export const Route = createFileRoute("/settings/bitcoin-connect")({
beforeLoad: () => {
init({
appName: "Lume",
filters: ["nwc"],
showBalance: true,
});
},
component: Screen,
});
function Screen() {
const setNwcUri = async (uri: string) => {
const cmd = await NostrAccount.setWallet(uri);
if (cmd) getCurrent().close();
};
return (
<div className="flex items-center justify-center size-full">
<div className="flex flex-col items-center justify-center gap-3 text-center">
<div>
<p className="text-sm text-black/70 dark:text-white/70">
Click to the button below to connect with your Bitcoin wallet.
</p>
</div>
<Button
onConnected={(provider) =>
setNwcUri(provider.client.nostrWalletConnectUrl)
}
/>
</div>
</div>
);
}

View File

@ -51,7 +51,7 @@ function Screen() {
return ( return (
<div className="w-full max-w-xl mx-auto"> <div className="w-full max-w-xl mx-auto">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex items-center w-full h-12 px-3 text-sm rounded-xl bg-black/5 dark:bg-white/5"> <div className="flex items-center w-full px-3 text-sm rounded-lg h-11 bg-black/5 dark:bg-white/5">
* Setting changes require restarting the app to take effect. * Setting changes require restarting the app to take effect.
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">

View File

@ -0,0 +1,59 @@
import { NostrAccount } from "@lume/system";
import { getBitcoinDisplayValues } from "@lume/utils";
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/settings/wallet")({
beforeLoad: async () => {
const wallet = await NostrAccount.loadWallet();
if (!wallet) {
throw redirect({ to: "/settings/bitcoin-connect" });
}
const balance = getBitcoinDisplayValues(wallet);
return { balance };
},
component: Screen,
});
function Screen() {
const { balance } = Route.useRouteContext();
const disconnect = async () => {
window.localStorage.removeItem("bc:config");
await NostrAccount.removeWallet();
return redirect({ to: "/settings/bitcoin-connect" });
};
return (
<div className="w-full max-w-xl mx-auto">
<div className="flex flex-col w-full gap-3">
<div className="flex flex-col w-full px-3 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-between w-full gap-4 py-3">
<div className="flex-1">
<h3 className="font-medium">Connection</h3>
</div>
<div className="flex justify-end w-36 shrink-0">
<button
type="button"
onClick={() => disconnect()}
className="h-8 w-max px-2.5 text-sm rounded-lg inline-flex items-center justify-center bg-black/10 dark:bg-white/10 hover:bg-black/20 dark:hover:bg-white/20"
>
Disconnect
</button>
</div>
</div>
</div>
<div className="flex flex-col w-full px-3 bg-black/5 dark:bg-white/5 rounded-xl">
<div className="flex items-center justify-between w-full gap-4 py-3">
<div className="flex-1">
<h3 className="font-medium">Current Balance</h3>
</div>
<div className="flex justify-end w-36 shrink-0">
{balance.bitcoinFormatted}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,102 +0,0 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react";
export const Route = createLazyFileRoute("/settings/zap")({
component: Screen,
});
function Screen() {
return (
<div className="w-full max-w-xl mx-auto">
<div className="flex flex-col gap-3 divide-y divide-neutral-300 dark:divide-neutral-700">
<div className="flex flex-col gap-6 py-3">
<Connection />
<DefaultAmount />
</div>
</div>
</div>
);
}
function Connection() {
const [uri, setUri] = useState("");
const connect = async () => {
try {
await invoke("set_nwc", { uri });
} catch (e) {
await message(String(e), { title: "Zap", kind: "info" });
}
};
return (
<div className="flex items-start gap-6">
<div className="text-sm font-medium w-36 shrink-0 text-end">
Connection
</div>
<div className="flex-1">
<div className="flex flex-col w-full gap-1">
<label
htmlFor="nwc"
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Nostr Wallet Connect
</label>
<div className="flex items-center gap-2">
<input
name="nwc"
type="text"
value={uri}
onChange={(e) => setUri(e.target.value)}
placeholder="nostrconnect://"
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
<button
type="button"
onClick={() => connect()}
className="inline-flex items-center justify-center w-24 text-sm font-medium rounded-lg h-9 bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
>
Connect
</button>
</div>
</div>
</div>
</div>
);
}
function DefaultAmount() {
return (
<div className="flex items-start gap-6">
<div className="text-sm font-medium w-36 shrink-0 text-end">
Default amount
</div>
<div className="flex-1">
<div className="flex flex-col w-full gap-1">
<label
htmlFor="amount"
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
>
Set default amount for quick zapping
</label>
<div className="flex items-center gap-2">
<input
name="amount"
type="number"
value={21}
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
/>
<button
type="button"
className="inline-flex items-center justify-center w-24 text-sm font-medium rounded-lg h-9 bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
>
Update
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,127 +0,0 @@
import { Balance } from "@/components/balance";
import { User } from "@/components/user";
import { LumeEvent } from "@lume/system";
import { Box, Container } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react";
import CurrencyInput from "react-currency-input-field";
import { useTranslation } from "react-i18next";
const DEFAULT_VALUES = [69, 100, 200, 500];
export const Route = createLazyFileRoute("/zap/$id")({
component: Screen,
});
function Screen() {
const { t } = useTranslation();
const { id } = Route.useParams();
// @ts-ignore, magic !!!
const { pubkey, account } = Route.useSearch();
const [amount, setAmount] = useState(21);
const [content, setContent] = useState("");
const [isCompleted, setIsCompleted] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const submit = async () => {
try {
// start loading
setIsLoading(true);
const val = await LumeEvent.zap(id, amount, content);
if (val) {
setIsCompleted(true);
const window = getCurrent();
// close current window
window.close();
}
} catch (e) {
setIsLoading(false);
await message(String(e), {
title: "Zap",
kind: "error",
});
}
};
return (
<Container>
<Balance account={account} />
<Box className="flex flex-col gap-3">
<div className="flex flex-col justify-between h-full py-5">
<div className="flex items-center justify-center gap-2 h-11 shrink-0">
{t("note.zap.modalTitle")}{" "}
<User.Provider pubkey={pubkey}>
<User.Root className="inline-flex items-center gap-2 p-1 rounded-full bg-neutral-100 dark:bg-neutral-900">
<User.Avatar className="rounded-full size-6" />
<User.Name className="pr-2 text-sm font-medium" />
</User.Root>
</User.Provider>
</div>
<div className="flex flex-col justify-between flex-1 px-5">
<div className="relative flex flex-col flex-1 pb-8">
<div className="inline-flex items-center justify-center flex-1 h-full gap-1">
<CurrencyInput
placeholder="0"
defaultValue={21}
value={amount}
decimalsLimit={2}
min={0} // 0 sats
max={10000} // 1M sats
maxLength={10000} // 1M sats
onValueChange={(value) => setAmount(Number(value))}
className="flex-1 w-full text-4xl font-semibold text-right bg-transparent border-none placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
/>
<span className="flex-1 w-full text-4xl font-semibold text-left text-neutral-500 dark:text-neutral-400">
sats
</span>
</div>
<div className="inline-flex items-center justify-center gap-2">
{DEFAULT_VALUES.map((value) => (
<button
key={value}
type="button"
onClick={() => setAmount(value)}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
{value} sats
</button>
))}
</div>
</div>
<div className="flex flex-col w-full gap-2">
<input
name="message"
value={content}
onChange={(e) => setContent(e.target.value)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder={t("note.zap.messagePlaceholder")}
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:text-neutral-400"
/>
<div className="flex flex-col gap-2">
<button
type="button"
onClick={() => submit()}
className="inline-flex h-9 w-full items-center justify-center rounded-lg border-t border-neutral-900 bg-neutral-950 pb-[2px] font-semibold text-neutral-50 hover:bg-neutral-900 dark:border-neutral-800 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
{isCompleted
? t("note.zap.buttonFinish")
: isLoading
? t("note.zap.buttonLoading")
: t("note.zap.zap")}
</button>
</div>
</div>
</div>
</div>
</Box>
</Container>
);
}

View File

@ -0,0 +1,119 @@
import { User } from "@/components/user";
import { NostrQuery } from "@lume/system";
import { createFileRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { message } from "@tauri-apps/plugin-dialog";
import { useState } from "react";
import CurrencyInput from "react-currency-input-field";
const DEFAULT_VALUES = [21, 50, 100, 200];
export const Route = createFileRoute("/zap/$id")({
beforeLoad: async ({ params }) => {
const event = await NostrQuery.getEvent(params.id);
return { event };
},
component: Screen,
});
function Screen() {
const { event } = Route.useRouteContext();
const [amount, setAmount] = useState(21);
const [content, setContent] = useState("");
const [isCompleted, setIsCompleted] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const submit = async () => {
try {
// start loading
setIsLoading(true);
// Zap
const val = await event.zap(amount, content);
if (val) {
setIsCompleted(true);
// close current window
await getCurrent().close();
}
} catch (e) {
setIsLoading(false);
await message(String(e), {
title: "Zap",
kind: "error",
});
}
};
return (
<div data-tauri-drag-region className="flex flex-col pb-5 size-full">
<div
data-tauri-drag-region
className="flex items-center justify-center h-24 gap-2 shrink-0"
>
<p className="text-sm">Send zap to </p>
<User.Provider pubkey={event.pubkey}>
<User.Root className="inline-flex items-center gap-2 p-1 rounded-full bg-black/5 dark:bg-white/5">
<User.Avatar className="rounded-full size-6" />
<User.Name className="pr-2 text-sm font-medium" />
</User.Root>
</User.Provider>
</div>
<div className="flex flex-col justify-between h-full">
<div className="flex flex-col justify-between flex-1 px-5">
<div className="relative flex flex-col flex-1 pb-8">
<div className="inline-flex items-center justify-center flex-1 h-full gap-1">
<CurrencyInput
placeholder="0"
defaultValue={21}
value={amount}
decimalsLimit={2}
min={0} // 0 sats
max={10000} // 1M sats
maxLength={10000} // 1M sats
onValueChange={(value) => setAmount(Number(value))}
className="flex-1 w-full text-4xl font-semibold text-right bg-transparent border-none placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
/>
<span className="flex-1 w-full text-4xl font-semibold text-left text-neutral-500 dark:text-neutral-400">
sats
</span>
</div>
<div className="inline-flex items-center justify-center gap-2">
{DEFAULT_VALUES.map((value) => (
<button
key={value}
type="button"
onClick={() => setAmount(value)}
className="w-max rounded-full bg-black/10 px-2.5 py-1 text-xs font-medium hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
>
{value} sats
</button>
))}
</div>
</div>
<div className="flex flex-col w-full gap-2">
<input
name="message"
value={content}
onChange={(e) => setContent(e.target.value)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="Enter message (optional)"
className="h-11 w-full resize-none rounded-xl border-transparent bg-black/5 px-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/5"
/>
<button
type="button"
onClick={() => submit()}
className="inline-flex items-center justify-center w-full h-10 font-medium rounded-xl bg-neutral-950 text-neutral-50 hover:bg-neutral-900 dark:bg-white/20 dark:hover:bg-white/30"
>
{isCompleted ? "Zapped" : isLoading ? "Processing..." : "Zap"}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,6 +1,6 @@
import type { Metadata } from "@lume/types"; import type { Metadata } from "@lume/types";
import { type Result, commands } from "./commands";
import { Window } from "@tauri-apps/api/window"; import { Window } from "@tauri-apps/api/window";
import { type Result, commands } from "./commands";
export class NostrAccount { export class NostrAccount {
static async getAccounts() { static async getAccounts() {
@ -99,8 +99,28 @@ export class NostrAccount {
} }
} }
static async loadWallet() {
const query = await commands.loadWallet();
if (query.status === "ok") {
return Number.parseInt(query.data);
} else {
throw new Error(query.error);
}
}
static async setWallet(uri: string) { static async setWallet(uri: string) {
const query = await commands.setNwc(uri); const query = await commands.setWallet(uri);
if (query.status === "ok") {
return query.data;
} else {
throw new Error(query.error);
}
}
static async removeWallet() {
const query = await commands.removeWallet();
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
@ -110,7 +130,7 @@ export class NostrAccount {
} }
static async getProfile() { static async getProfile() {
const query = await commands.getCurrentUserProfile(); const query = await commands.getCurrentProfile();
if (query.status === "ok") { if (query.status === "ok") {
return JSON.parse(query.data) as Metadata; return JSON.parse(query.data) as Metadata;
@ -119,16 +139,6 @@ export class NostrAccount {
} }
} }
static async getBalance() {
const query = await commands.getBalance();
if (query.status === "ok") {
return Number.parseInt(query.data);
} else {
return 0;
}
}
static async getContactList() { static async getContactList() {
const query = await commands.getContactList(); const query = await commands.getContactList();

View File

@ -180,25 +180,25 @@ try {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async setNwc(uri: string) : Promise<Result<boolean, string>> { async setWallet(uri: string) : Promise<Result<boolean, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("set_nwc", { uri }) }; return { status: "ok", data: await TAURI_INVOKE("set_wallet", { uri }) };
} catch (e) { } catch (e) {
if(e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async loadNwc() : Promise<Result<boolean, string>> { async loadWallet() : Promise<Result<string, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("load_nwc") }; return { status: "ok", data: await TAURI_INVOKE("load_wallet") };
} catch (e) { } catch (e) {
if(e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async getBalance() : Promise<Result<string, string>> { async removeWallet() : Promise<Result<null, null>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_balance") }; return { status: "ok", data: await TAURI_INVOKE("remove_wallet") };
} catch (e) { } catch (e) {
if(e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
@ -407,9 +407,9 @@ try {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
async openWindow(label: string, title: string, url: string, width: number, height: number) : Promise<Result<null, string>> { async openWindow(window: Window) : Promise<Result<null, string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("open_window", { label, title, url, width, height }) }; return { status: "ok", data: await TAURI_INVOKE("open_window", { window }) };
} catch (e) { } catch (e) {
if(e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
@ -438,6 +438,7 @@ export type Meta = { content: string; images: string[]; videos: string[]; events
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null } export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
export type RichEvent = { raw: string; parsed: Meta | null } export type RichEvent = { raw: string; parsed: Meta | null }
export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean } export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean }
export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean }
/** tauri-specta globals **/ /** tauri-specta globals **/

View File

@ -18,7 +18,15 @@ export class LumeWindow {
const label = `event-${event.id}`; const label = `event-${event.id}`;
const url = `/events/${root ?? reply ?? event.id}`; const url = `/events/${root ?? reply ?? event.id}`;
const query = await commands.openWindow(label, "Thread", url, 500, 800); const query = await commands.openWindow({
label,
url,
title: "Thread",
width: 500,
height: 800,
maximizable: true,
minimizable: true,
});
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
@ -29,13 +37,15 @@ export class LumeWindow {
static async openProfile(pubkey: string) { static async openProfile(pubkey: string) {
const label = `user-${pubkey}`; const label = `user-${pubkey}`;
const query = await commands.openWindow( const query = await commands.openWindow({
label, label,
"Profile", url: `/users/${pubkey}`,
`/users/${pubkey}`, title: "Profile",
500, width: 500,
800, height: 800,
); maximizable: true,
minimizable: true,
});
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
@ -60,7 +70,15 @@ export class LumeWindow {
} }
const label = `editor-${reply_to ? reply_to : 0}`; const label = `editor-${reply_to ? reply_to : 0}`;
const query = await commands.openWindow(label, "Editor", url, 560, 340); const query = await commands.openWindow({
label,
url,
title: "Editor",
width: 560,
height: 340,
maximizable: true,
minimizable: false,
});
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
@ -69,45 +87,35 @@ export class LumeWindow {
} }
} }
static async openZap(id: string, pubkey: string) { static async openZap(id: string) {
const nwc = await commands.loadNwc(); const wallet = await commands.loadWallet();
if (nwc.status === "ok") { if (wallet.status === "ok") {
const status = nwc.data; await commands.openWindow({
label: `zap-${id}`,
if (!status) { url: `/zap/${id}`,
const label = "nwc"; title: "Zap",
await commands.openWindow( width: 360,
label, height: 460,
"Nostr Wallet Connect", maximizable: false,
"/nwc", minimizable: false,
400, });
600,
);
} else {
const label = `zap-${id}`;
await commands.openWindow(
label,
"Zap",
`/zap/${id}?pubkey=${pubkey}`,
400,
500,
);
}
} else { } else {
throw new Error(nwc.error); await LumeWindow.openSettings("bitcoin-connect");
} }
} }
static async openSettings() { static async openSettings(path?: string) {
const label = "settings"; const label = "settings";
const query = await commands.openWindow( const query = await commands.openWindow({
label, label,
"Settings", url: path ? `/settings/${path}` : "/settings/general",
"/settings/general", title: "Settings",
800, width: 800,
500, height: 500,
); maximizable: false,
minimizable: false,
});
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;
@ -118,30 +126,15 @@ export class LumeWindow {
static async openSearch() { static async openSearch() {
const label = "search"; const label = "search";
const query = await commands.openWindow( const query = await commands.openWindow({
label, label,
"Search", url: "/search",
"/search", title: "Search",
400, width: 400,
600, height: 600,
); maximizable: false,
minimizable: false,
if (query.status === "ok") { });
return query.data;
} else {
throw new Error(query.error);
}
}
static async openActivity(account: string) {
const label = "activity";
const query = await commands.openWindow(
label,
"Activity",
`/activity/${account}/texts`,
400,
600,
);
if (query.status === "ok") { if (query.status === "ok") {
return query.data; return query.data;

View File

@ -81,7 +81,7 @@ export function displayLongHandle(str: string) {
const handle = split[0]; const handle = split[0];
const service = split[1]; const service = split[1];
return handle.substring(0, 16) + "..." + "@" + service; return `${handle.substring(0, 16)}...@${service}`;
} }
// convert number to K, M, B, T, etc. // convert number to K, M, B, T, etc.
@ -127,7 +127,7 @@ export function getBitcoinDisplayValues(satoshis: number) {
.reverse() .reverse()
.forEach((c, index) => { .forEach((c, index) => {
if (index > 0 && index % 3 === 0) { if (index > 0 && index % 3 === 0) {
res = " " + res; res = ` ${res}`;
} }
res = c + res; res = c + res;
}); });

114
pnpm-lock.yaml generated
View File

@ -54,6 +54,9 @@ importers:
apps/desktop2: apps/desktop2:
dependencies: dependencies:
'@getalby/bitcoin-connect-react':
specifier: ^3.5.3
version: 3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5)
'@lume/icons': '@lume/icons':
specifier: workspace:^ specifier: workspace:^
version: link:../../packages/icons version: link:../../packages/icons
@ -1196,6 +1199,49 @@ packages:
resolution: {integrity: sha512-7ncjjSpRSRKvjJEoru092iFiEoC89lz4oG4+SGg9hh7DI/5SXf+kE+dg+6Fv/bwiK/WJCo4Q2gvPZGRlCE5mcA==} resolution: {integrity: sha512-7ncjjSpRSRKvjJEoru092iFiEoC89lz4oG4+SGg9hh7DI/5SXf+kE+dg+6Fv/bwiK/WJCo4Q2gvPZGRlCE5mcA==}
dev: false dev: false
/@getalby/bitcoin-connect-react@3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5):
resolution: {integrity: sha512-/oAPFFva/T946JzuNv6X/AuCGb46co2rLxfiINy4am/jFN+mAZ1HNGjOycTodpsTXnNpr3Ih37wV+YuI/03ueQ==}
peerDependencies:
react: ^18.2.0
dependencies:
'@getalby/bitcoin-connect': 3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5)
react: 18.3.1
transitivePeerDependencies:
- '@types/react'
- immer
- typescript
dev: false
/@getalby/bitcoin-connect@3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5):
resolution: {integrity: sha512-csVNT4gXzuJtXP3ZyXnNnSGpt4JEQJvynLhc/aG3VNZVqPhNgot5Npj1J4XIewrsn4sWVIr9WkAtCxGfQ6XmSQ==}
dependencies:
'@getalby/lightning-tools': 5.0.3
'@getalby/sdk': 3.5.1(typescript@5.4.5)
'@lightninglabs/lnc-web': 0.3.1-alpha
qrcode-generator: 1.4.4
zustand: 4.5.2(@types/react@18.3.3)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
- react
- typescript
dev: false
/@getalby/lightning-tools@5.0.3:
resolution: {integrity: sha512-QG3/SBI5n2py5IgsjP3K+c8eq55eiI3PQB12yo9Pot0b5hcN7TNNoTKn0fgLJjO1iEVCUkF513kDOpjjXwK0hQ==}
engines: {node: '>=14'}
dev: false
/@getalby/sdk@3.5.1(typescript@5.4.5):
resolution: {integrity: sha512-Qz9GgXMoVpupDLqbzA2CHpru+9yqijQrxeRN7CDfV6l39js/BGwin93MFTh7eFj2TsMo+i8JeM3BVn+SJn/iRg==}
engines: {node: '>=14'}
dependencies:
eventemitter3: 5.0.1
nostr-tools: 1.17.0(typescript@5.4.5)
transitivePeerDependencies:
- typescript
dev: false
/@img/sharp-darwin-arm64@0.33.4: /@img/sharp-darwin-arm64@0.33.4:
resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==} resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==}
engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
@ -1433,6 +1479,21 @@ packages:
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
dev: false dev: false
/@lightninglabs/lnc-core@0.3.1-alpha:
resolution: {integrity: sha512-I/hThdItLWJ6RU8Z27ZIXhpBS2JJuD3+TjtaQXX2CabaUYXlcN4sk+Kx8N/zG/fk8qZvjlRWum4vHu4ZX554Fg==}
dev: false
/@lightninglabs/lnc-web@0.3.1-alpha:
resolution: {integrity: sha512-yL5SgBkl6kd6ISzJHGlSN7TXbiDoo1pfGvTOIdVWYVyXtEeW8PT+x6YGOmyQXGFT2OOf7fC7PfP9VnskDPuFaA==}
dependencies:
'@lightninglabs/lnc-core': 0.3.1-alpha
crypto-js: 4.2.0
dev: false
/@noble/ciphers@0.2.0:
resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==}
dev: false
/@noble/ciphers@0.5.3: /@noble/ciphers@0.5.3:
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==} resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
dev: false dev: false
@ -3434,6 +3495,10 @@ packages:
shebang-command: 2.0.0 shebang-command: 2.0.0
which: 2.0.2 which: 2.0.2
/crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
dev: false
/cssesc@3.0.0: /cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@ -4720,6 +4785,23 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
/nostr-tools@1.17.0(typescript@5.4.5):
resolution: {integrity: sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==}
peerDependencies:
typescript: '>=5.0.0'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@noble/ciphers': 0.2.0
'@noble/curves': 1.1.0
'@noble/hashes': 1.3.1
'@scure/base': 1.1.1
'@scure/bip32': 1.3.1
'@scure/bip39': 1.2.1
typescript: 5.4.5
dev: false
/nostr-tools@2.7.0(typescript@5.4.5): /nostr-tools@2.7.0(typescript@5.4.5):
resolution: {integrity: sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==} resolution: {integrity: sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==}
peerDependencies: peerDependencies:
@ -5019,6 +5101,10 @@ packages:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
dev: false dev: false
/qrcode-generator@1.4.4:
resolution: {integrity: sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==}
dev: false
/queue-microtask@1.2.3: /queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -5921,6 +6007,14 @@ packages:
tslib: 2.6.3 tslib: 2.6.3
dev: false dev: false
/use-sync-external-store@1.2.0(react@18.3.1):
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.3.1
dev: false
/use-sync-external-store@1.2.2(react@18.3.1): /use-sync-external-store@1.2.2(react@18.3.1):
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==} resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
peerDependencies: peerDependencies:
@ -6327,6 +6421,26 @@ packages:
/zod@3.23.8: /zod@3.23.8:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
/zustand@4.5.2(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
immer: '>=9.0.6'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
dependencies:
'@types/react': 18.3.3
react: 18.3.1
use-sync-external-store: 1.2.0(react@18.3.1)
dev: false
/zwitch@2.0.4: /zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false dev: false

View File

@ -3,6 +3,8 @@ use std::str::FromStr;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use cocoa::{appkit::NSApp, base::nil, foundation::NSString}; use cocoa::{appkit::NSApp, base::nil, foundation::NSString};
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::{LogicalPosition, LogicalSize, Manager, State, WebviewUrl}; use tauri::{LogicalPosition, LogicalSize, Manager, State, WebviewUrl};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::TitleBarStyle; use tauri::TitleBarStyle;
@ -14,6 +16,17 @@ use url::Url;
use crate::Nostr; use crate::Nostr;
#[derive(Serialize, Deserialize, Type)]
pub struct Window {
label: String,
title: String,
url: String,
width: f64,
height: f64,
maximizable: bool,
minimizable: bool,
}
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn create_column( pub fn create_column(
@ -121,15 +134,8 @@ pub fn resize_column(
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn open_window( pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
label: &str, if let Some(window) = app_handle.get_window(&window.label) {
title: &str,
url: &str,
width: f64,
height: f64,
app_handle: tauri::AppHandle,
) -> Result<(), String> {
if let Some(window) = app_handle.get_window(label) {
if window.is_visible().unwrap_or_default() { if window.is_visible().unwrap_or_default() {
let _ = window.set_focus(); let _ = window.set_focus();
} else { } else {
@ -138,21 +144,27 @@ pub fn open_window(
}; };
} else { } else {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url))) let window = WebviewWindowBuilder::new(
.title(title) &app_handle,
.min_inner_size(width, height) &window.label,
.inner_size(width, height) WebviewUrl::App(PathBuf::from(window.url)),
.hidden_title(true) )
.title_bar_style(TitleBarStyle::Overlay) .title(&window.title)
.transparent(true) .min_inner_size(window.width, window.height)
.effects(WindowEffectsConfig { .inner_size(window.width, window.height)
state: None, .hidden_title(true)
effects: vec![Effect::WindowBackground], .title_bar_style(TitleBarStyle::Overlay)
radius: None, .transparent(true)
color: None, .minimizable(window.minimizable)
}) .maximizable(window.maximizable)
.build() .effects(WindowEffectsConfig {
.unwrap(); state: None,
effects: vec![Effect::WindowBackground],
radius: None,
color: None,
})
.build()
.unwrap();
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url))) let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
@ -180,10 +192,6 @@ pub fn open_window(
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
// Create a custom titlebar for Windows // Create a custom titlebar for Windows
window.create_overlay_titlebar().unwrap(); window.create_overlay_titlebar().unwrap();
// Set a custom inset to the traffic lights
#[cfg(target_os = "macos")]
window.set_traffic_lights_inset(8.0, 16.0).unwrap();
} }
Ok(()) Ok(())

View File

@ -9,20 +9,20 @@ extern crate cocoa;
#[macro_use] #[macro_use]
extern crate objc; extern crate objc;
use std::sync::Mutex;
use std::time::Duration;
use std::{ use std::{
fs, fs,
io::{self, BufRead}, io::{self, BufRead},
str::FromStr, str::FromStr,
}; };
use std::sync::Mutex;
use std::time::Duration;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
use tauri::{Manager, path::BaseDirectory};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::tray::{MouseButtonState, TrayIconEvent}; use tauri::tray::{MouseButtonState, TrayIconEvent};
use tauri::{path::BaseDirectory, Manager};
use tauri_nspanel::ManagerExt; use tauri_nspanel::ManagerExt;
use tauri_plugin_decorum::WebviewWindowExt; use tauri_plugin_decorum::WebviewWindowExt;
@ -98,9 +98,9 @@ fn main() {
nostr::metadata::toggle_contact, nostr::metadata::toggle_contact,
nostr::metadata::get_nstore, nostr::metadata::get_nstore,
nostr::metadata::set_nstore, nostr::metadata::set_nstore,
nostr::metadata::set_nwc, nostr::metadata::set_wallet,
nostr::metadata::load_nwc, nostr::metadata::load_wallet,
nostr::metadata::get_balance, nostr::metadata::remove_wallet,
nostr::metadata::zap_profile, nostr::metadata::zap_profile,
nostr::metadata::zap_event, nostr::metadata::zap_event,
nostr::metadata::friend_to_friend, nostr::metadata::friend_to_friend,

View File

@ -310,12 +310,12 @@ pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result<String, St
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> { pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client; let client = &state.client;
if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(uri) { if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(uri) {
let nwc = NWC::new(nwc_uri); let nwc = NWC::new(nwc_uri);
let keyring = Entry::new("Lume Secret Storage", "NWC").map_err(|e| e.to_string())?; let keyring = Entry::new("Lume Secret", "Bitcoin Connect").map_err(|e| e.to_string())?;
keyring.set_password(uri).map_err(|e| e.to_string())?; keyring.set_password(uri).map_err(|e| e.to_string())?;
client.set_zapper(nwc).await; client.set_zapper(nwc).await;
@ -327,38 +327,42 @@ pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result<bool, String>
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn load_nwc(state: State<'_, Nostr>) -> Result<bool, String> { pub async fn load_wallet(state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client; let client = &state.client;
let keyring = Entry::new("Lume Secret Storage", "NWC").map_err(|e| e.to_string())?; let keyring = Entry::new("Lume Secret", "Bitcoin Connect").unwrap();
match keyring.get_password() { match keyring.get_password() {
Ok(val) => { Ok(val) => {
let uri = NostrWalletConnectURI::from_str(&val).map_err(|e| e.to_string())?; let uri = NostrWalletConnectURI::from_str(&val).unwrap();
let nwc = NWC::new(uri); let nwc = NWC::new(uri);
// Get current balance
let balance = nwc.get_balance().await;
// Update zapper
client.set_zapper(nwc).await; client.set_zapper(nwc).await;
Ok(true) match balance {
Ok(val) => Ok(val.to_string()),
Err(_) => Err("Get balance failed.".into()),
}
} }
Err(_) => Ok(false), Err(_) => Err("NWC not found.".into()),
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_balance() -> Result<String, String> { pub async fn remove_wallet(state: State<'_, Nostr>) -> Result<(), ()> {
let keyring = Entry::new("Lume Secret Storage", "NWC").map_err(|e| e.to_string())?; let client = &state.client;
let keyring = Entry::new("Lume Secret", "Bitcoin Connect").unwrap();
match keyring.get_password() { match keyring.delete_password() {
Ok(val) => { Ok(_) => {
let uri = NostrWalletConnectURI::from_str(&val).map_err(|e| e.to_string())?; client.unset_zapper().await;
let nwc = NWC::new(uri); Ok(())
nwc
.get_balance()
.await
.map(|balance| balance.to_string())
.map_err(|_| "Get balance failed".into())
} }
Err(_) => Err("Something wrong".into()), Err(_) => Err(()),
} }
} }
@ -371,32 +375,28 @@ pub async fn zap_profile(
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let client = &state.client; let client = &state.client;
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) { let public_key = match Nip19::from_bech32(id) {
Ok(val) => match val { Ok(val) => match val {
Nip19::Pubkey(key) => Some(key), Nip19::Pubkey(key) => key,
Nip19::Profile(profile) => Some(profile.public_key), Nip19::Profile(profile) => profile.public_key,
_ => None, _ => return Err("Public Key is not valid.".into()),
}, },
Err(_) => match PublicKey::from_str(id) { Err(_) => match PublicKey::from_str(id) {
Ok(val) => Some(val), Ok(val) => val,
Err(_) => None, Err(_) => return Err("Public Key is not valid.".into()),
}, },
}; };
if let Some(recipient) = public_key { let details = ZapDetails::new(ZapType::Private).message(message);
let details = ZapDetails::new(ZapType::Public).message(message); let num = match amount.parse::<u64>() {
let num = match amount.parse::<u64>() { Ok(val) => val,
Ok(val) => val, Err(_) => return Err("Invalid amount.".into()),
Err(_) => return Err("Invalid amount.".into()), };
};
if client.zap(recipient, num, Some(details)).await.is_ok() { if client.zap(public_key, num, Some(details)).await.is_ok() {
Ok(true) Ok(true)
} else {
Err("Zap profile failed".into())
}
} else { } else {
Err("Parse public key failed".into()) Err("Zap profile failed".into())
} }
} }
@ -409,32 +409,28 @@ pub async fn zap_event(
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<bool, String> { ) -> Result<bool, String> {
let client = &state.client; let client = &state.client;
let event_id: Option<EventId> = match Nip19::from_bech32(id) { let event_id = match Nip19::from_bech32(id) {
Ok(val) => match val { Ok(val) => match val {
Nip19::EventId(id) => Some(id), Nip19::EventId(id) => id,
Nip19::Event(event) => Some(event.event_id), Nip19::Event(event) => event.event_id,
_ => None, _ => return Err("Event ID is invalid.".into()),
}, },
Err(_) => match EventId::from_hex(id) { Err(_) => match EventId::from_hex(id) {
Ok(val) => Some(val), Ok(val) => val,
Err(_) => None, Err(_) => return Err("Event ID is invalid.".into()),
}, },
}; };
if let Some(recipient) = event_id { let details = ZapDetails::new(ZapType::Private).message(message);
let details = ZapDetails::new(ZapType::Public).message(message); let num = match amount.parse::<u64>() {
let num = match amount.parse::<u64>() { Ok(val) => val,
Ok(val) => val, Err(_) => return Err("Invalid amount.".into()),
Err(_) => return Err("Invalid amount.".into()), };
};
if client.zap(recipient, num, Some(details)).await.is_ok() { if client.zap(event_id, num, Some(details)).await.is_ok() {
Ok(true) Ok(true)
} else {
Err("Zap event failed".into())
}
} else { } else {
Err("Parse event ID failed".into()) Err("Zap event failed".into())
} }
} }