diff --git a/packages/app/public/icons.svg b/packages/app/public/icons.svg
index 43c7b52..806a5df 100644
--- a/packages/app/public/icons.svg
+++ b/packages/app/public/icons.svg
@@ -154,5 +154,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx
index a9f523c..219738b 100644
--- a/packages/app/src/Element/NoteFooter.tsx
+++ b/packages/app/src/Element/NoteFooter.tsx
@@ -10,7 +10,7 @@ import Spinner from "Icons/Spinner";
import { formatShort } from "Number";
import useEventPublisher from "Feed/EventPublisher";
-import { bech32ToHex, delay, normalizeReaction, unwrap } from "Util";
+import { delay, normalizeReaction, unwrap } from "Util";
import { NoteCreator } from "Element/NoteCreator";
import { ReBroadcaster } from "Element/ReBroadcaster";
import Reactions from "Element/Reactions";
@@ -25,13 +25,13 @@ import {
reset as resetReBroadcast,
} from "State/ReBroadcast";
import useModeration from "Hooks/useModeration";
-import { SnortPubKey, TranslateHost } from "Const";
+import { TranslateHost } from "Const";
import { LNURL } from "LNURL";
-import { DonateLNURL } from "Pages/DonatePage";
import { useWallet } from "Wallet";
import useLogin from "Hooks/useLogin";
import { setBookmarked, setPinned } from "Login";
import { useInteractionCache } from "Hooks/useInteractionCache";
+import { ZapPoolController } from "ZapPoolController";
import messages from "./messages";
@@ -160,7 +160,6 @@ export default function NoteFooter(props: NoteFooterProps) {
setZapping(true);
try {
await fastZapInner(lnurl, prefs.defaultZapAmount, ev.pubkey, ev.id);
- fastZapDonate();
} catch (e) {
console.warn("Fast zap failed", e);
if (!(e instanceof Error) || e.message !== "User rejected") {
@@ -184,26 +183,12 @@ export default function NoteFooter(props: NoteFooterProps) {
const zap = handler.canZap && publisher ? await publisher.zap(amount * 1000, key, zr, id) : undefined;
const invoice = await handler.getInvoice(amount, undefined, zap);
await wallet?.payInvoice(unwrap(invoice.pr));
+ ZapPoolController.allocate(amount);
await interactionCache.zap();
});
}
- function fastZapDonate() {
- queueMicrotask(async () => {
- if (prefs.fastZapDonate > 0) {
- // spin off donate
- const donateAmount = Math.floor(prefs.defaultZapAmount * prefs.fastZapDonate);
- if (donateAmount > 0) {
- console.debug(`Donating ${donateAmount} sats to ${DonateLNURL}`);
- fastZapInner(DonateLNURL, donateAmount, bech32ToHex(SnortPubKey))
- .then(() => console.debug("Donation sent! Thank You!"))
- .catch(() => console.debug("Failed to donate"));
- }
- }
- });
- }
-
useEffect(() => {
if (prefs.autoZap && !didZap && !isMine && !zapping) {
const lnurl = getLNURL();
@@ -212,7 +197,6 @@ export default function NoteFooter(props: NoteFooterProps) {
queueMicrotask(async () => {
try {
await fastZapInner(lnurl, prefs.defaultZapAmount, ev.pubkey, ev.id);
- fastZapDonate();
} catch {
// ignored
} finally {
@@ -457,6 +441,7 @@ export default function NoteFooter(props: NoteFooterProps) {
author={author?.pubkey}
target={getTargetName()}
note={ev.id}
+ allocatePool={true}
/>
diff --git a/packages/app/src/Element/SendSats.tsx b/packages/app/src/Element/SendSats.tsx
index 6228f7b..60ac094 100644
--- a/packages/app/src/Element/SendSats.tsx
+++ b/packages/app/src/Element/SendSats.tsx
@@ -16,6 +16,7 @@ import { useWallet } from "Wallet";
import useLogin from "Hooks/useLogin";
import { generateRandomKey } from "Login";
import { EventPublisher } from "System/EventPublisher";
+import { ZapPoolController } from "ZapPoolController";
import messages from "./messages";
@@ -36,6 +37,7 @@ export interface SendSatsProps {
target?: string;
note?: HexKey;
author?: HexKey;
+ allocatePool?: boolean;
}
export default function SendSats(props: SendSatsProps) {
@@ -194,9 +196,12 @@ export default function SendSats(props: SendSatsProps) {
async function payWithWallet(invoice: LNURLInvoice) {
try {
- if (wallet?.isReady) {
+ if (wallet?.isReady()) {
setPaying(true);
const res = await wallet.payInvoice(invoice?.pr ?? "");
+ if (props.allocatePool) {
+ ZapPoolController.allocate(amount);
+ }
console.log(res);
setSuccess(invoice?.successAction ?? {});
}
diff --git a/packages/app/src/Pages/ZapPool.css b/packages/app/src/Pages/ZapPool.css
new file mode 100644
index 0000000..611dfd0
--- /dev/null
+++ b/packages/app/src/Pages/ZapPool.css
@@ -0,0 +1,3 @@
+.zap-pool input[type="range"] {
+ width: 200px;
+}
diff --git a/packages/app/src/Pages/ZapPool.tsx b/packages/app/src/Pages/ZapPool.tsx
new file mode 100644
index 0000000..7bc46e9
--- /dev/null
+++ b/packages/app/src/Pages/ZapPool.tsx
@@ -0,0 +1,185 @@
+import "./ZapPool.css";
+
+import { useMemo, useSyncExternalStore } from "react";
+import { FormattedMessage, FormattedNumber } from "react-intl";
+
+import { SnortPubKey } from "Const";
+import ProfilePreview from "Element/ProfilePreview";
+import useLogin from "Hooks/useLogin";
+import { System } from "System";
+import { UploaderServices } from "Upload";
+import { bech32ToHex, getRelayName, unwrap } from "Util";
+import { ZapPoolController, ZapPoolRecipient, ZapPoolRecipientType } from "ZapPoolController";
+import { useUserProfile } from "Hooks/useUserProfile";
+import AsyncButton from "Element/AsyncButton";
+import { useWallet } from "Wallet";
+
+function ZapTarget({ target }: { target: ZapPoolRecipient }) {
+ const login = useLogin();
+ const profile = useUserProfile(target.pubkey);
+ const hasAddress = profile?.lud16 || profile?.lud06;
+ const defaultZapMount = Math.ceil(login.preferences.defaultZapAmount * (target.split / 100));
+ return (
+
+
+ % (
+ )
+
+
+ ZapPoolController.set({
+ ...target,
+ split: e.target.valueAsNumber,
+ })
+ }
+ />
+
+ ) : (
+
+ )
+ }
+ />
+ );
+}
+
+export default function ZapPoolPage() {
+ const login = useLogin();
+ const zapPool = useSyncExternalStore(
+ c => ZapPoolController.hook(c),
+ () => ZapPoolController.snapshot()
+ );
+ const { wallet } = useWallet();
+
+ const relayConnections = useMemo(() => {
+ return [...System.Sockets.values()]
+ .map(a => {
+ if (a.Info?.pubkey) {
+ return {
+ address: a.Address,
+ pubkey: a.Info.pubkey,
+ };
+ }
+ })
+ .filter(a => a !== undefined)
+ .map(unwrap);
+ }, [login.relays]);
+
+ const sumPending = zapPool.reduce((acc, v) => acc + v.sum, 0);
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+ ),
+ nOut: (
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+ {wallet && (
+ ZapPoolController.payout(wallet)}>
+
+
+ )}
+
+
+ b.pubkey === bech32ToHex(SnortPubKey) && b.type === ZapPoolRecipientType.Generic) ?? {
+ type: ZapPoolRecipientType.Generic,
+ pubkey: bech32ToHex(SnortPubKey),
+ split: 0,
+ sum: 0,
+ }
+ }
+ />
+
+
+
+
+ {relayConnections.map(a => (
+
+
{getRelayName(a.address)}
+ b.pubkey === a.pubkey && b.type === ZapPoolRecipientType.Relay) ?? {
+ type: ZapPoolRecipientType.Relay,
+ pubkey: a.pubkey,
+ split: 0,
+ sum: 0,
+ }
+ }
+ />
+
+ ))}
+
+
+
+ {UploaderServices.map(a => (
+
+
{a.name}
+ b.pubkey === a.owner && b.type === ZapPoolRecipientType.FileHost) ?? {
+ type: ZapPoolRecipientType.FileHost,
+ pubkey: a.owner,
+ split: 0,
+ sum: 0,
+ }
+ }
+ />
+
+ ))}
+
+ );
+}
diff --git a/packages/app/src/Pages/settings/Index.tsx b/packages/app/src/Pages/settings/Index.tsx
index 7ccc5d0..7c929d4 100644
--- a/packages/app/src/Pages/settings/Index.tsx
+++ b/packages/app/src/Pages/settings/Index.tsx
@@ -83,7 +83,11 @@ const SettingsIndex = () => {
-
+ navigate("/zap-pool")}>
+
+
+
+
diff --git a/packages/app/src/Upload/index.ts b/packages/app/src/Upload/index.ts
index a93b67e..9f28600 100644
--- a/packages/app/src/Upload/index.ts
+++ b/packages/app/src/Upload/index.ts
@@ -4,6 +4,8 @@ import { RawEvent } from "@snort/nostr";
import NostrBuild from "Upload/NostrBuild";
import VoidCat from "Upload/VoidCat";
import NostrImg from "Upload/NostrImg";
+import { KieranPubKey } from "Const";
+import { bech32ToHex } from "Util";
export interface UploadResult {
url?: string;
@@ -15,6 +17,24 @@ export interface UploadResult {
header?: RawEvent;
}
+/**
+ * List of supported upload services and their owners on nostr
+ */
+export const UploaderServices = [
+ {
+ name: "void.cat",
+ owner: bech32ToHex(KieranPubKey),
+ },
+ {
+ name: "nostr.build",
+ owner: bech32ToHex("npub1nxy4qpqnld6kmpphjykvx2lqwvxmuxluddwjamm4nc29ds3elyzsm5avr7"),
+ },
+ {
+ name: "nostrimg.com",
+ owner: bech32ToHex("npub1xv6axulxcx6mce5mfvfzpsy89r4gee3zuknulm45cqqpmyw7680q5pxea6"),
+ },
+];
+
export interface Uploader {
upload: (f: File | Blob, filename: string) => Promise
;
}
diff --git a/packages/app/src/ZapPoolController.ts b/packages/app/src/ZapPoolController.ts
new file mode 100644
index 0000000..d02dc99
--- /dev/null
+++ b/packages/app/src/ZapPoolController.ts
@@ -0,0 +1,124 @@
+import { UserCache } from "Cache";
+import ExternalStore from "ExternalStore";
+import { LNURL } from "LNURL";
+import { LNWallet, WalletInvoiceState } from "Wallet";
+
+export enum ZapPoolRecipientType {
+ Generic = 0,
+ Relay = 1,
+ FileHost = 2,
+}
+
+export interface ZapPoolRecipient {
+ type: ZapPoolRecipientType;
+ pubkey: string;
+ split: number;
+ sum: number;
+}
+
+class ZapPool extends ExternalStore> {
+ #store: Map;
+ #isPayoutInProgress = false;
+
+ constructor() {
+ super();
+ this.#store = new Map();
+ this.#load();
+ }
+
+ async payout(wallet: LNWallet) {
+ if (this.#isPayoutInProgress) {
+ throw new Error("Payout already in progress");
+ }
+ this.#isPayoutInProgress = true;
+ for (const x of this.#store.values()) {
+ if (x.sum === 0) continue;
+ try {
+ const profile = await UserCache.get(x.pubkey);
+ if (!profile) {
+ throw new Error(`Failed to get profile for ${x.pubkey}`);
+ }
+ const svc = new LNURL(profile.lud16 || profile.lud06 || "");
+ await svc.load();
+ const amtSend = x.sum;
+ const invoice = await svc.getInvoice(amtSend, `SnortZapPool: ${x.split}%`);
+ if (invoice.pr) {
+ const result = await wallet.payInvoice(invoice.pr);
+ if (result.state === WalletInvoiceState.Paid) {
+ x.sum -= amtSend;
+ } else {
+ throw new Error("Payment failed");
+ }
+ } else {
+ throw new Error(invoice.reason ?? "Failed to get invoice");
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ this.#save();
+ this.notifyChange();
+ this.#isPayoutInProgress = false;
+ }
+
+ calcAllocation(n: number) {
+ let res = 0;
+ for (const x of this.#store.values()) {
+ res += Math.ceil(n * (x.split / 100));
+ }
+ return res;
+ }
+
+ allocate(n: number) {
+ if (this.#isPayoutInProgress) {
+ throw new Error("Payout is in progress, cannot allocate to pool");
+ }
+ for (const x of this.#store.values()) {
+ x.sum += Math.ceil(n * (x.split / 100));
+ }
+ this.#save();
+ this.notifyChange();
+ }
+
+ getOrDefault(rcpt: ZapPoolRecipient) {
+ const k = this.#key(rcpt);
+ if (this.#store.has(k)) {
+ return { ...this.#store.get(k) };
+ }
+ return rcpt;
+ }
+
+ set(rcpt: ZapPoolRecipient) {
+ const k = this.#key(rcpt);
+ // delete entry if split is 0 and sum is 0
+ if (rcpt.split === 0 && rcpt.sum === 0 && this.#store.has(k)) {
+ this.#store.delete(k);
+ } else {
+ this.#store.set(k, rcpt);
+ }
+ this.#save();
+ this.notifyChange();
+ }
+
+ #key(rcpt: ZapPoolRecipient) {
+ return `${rcpt.pubkey}-${rcpt.type}`;
+ }
+
+ #save() {
+ self.localStorage.setItem("zap-pool", JSON.stringify(this.takeSnapshot()));
+ }
+
+ #load() {
+ const existing = self.localStorage.getItem("zap-pool");
+ if (existing) {
+ const arr = JSON.parse(existing) as Array;
+ this.#store = new Map(arr.map(a => [`${a.pubkey}-${a.type}`, a]));
+ }
+ }
+
+ takeSnapshot(): ZapPoolRecipient[] {
+ return [...this.#store.values()];
+ }
+}
+
+export const ZapPoolController = new ZapPool();
diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx
index 788af70..2268c1f 100644
--- a/packages/app/src/index.tsx
+++ b/packages/app/src/index.tsx
@@ -30,7 +30,7 @@ import { WalletRoutes } from "Pages/WalletPage";
import NostrLinkHandler from "Pages/NostrLinkHandler";
import Thread from "Element/Thread";
import { SubscribeRoutes } from "Pages/subscribe";
-import Discover from "Pages/Discover";
+import ZapPoolPage from "Pages/ZapPool";
/**
* HTTP query provider
@@ -94,6 +94,10 @@ export const router = createBrowserRouter([
path: "/search/:keyword?",
element: ,
},
+ {
+ path: "/zap-pool",
+ element: ,
+ },
...NewUserRoutes,
...WalletRoutes,
...SubscribeRoutes,