refactor: move zapper to @snort/wallet

This commit is contained in:
2024-09-19 13:55:26 +01:00
parent 38b9d132d5
commit 7350acce95
16 changed files with 25 additions and 29 deletions

View File

@ -2,6 +2,7 @@
import { fetchNip05Pubkey, unixNow } from "@snort/shared";
import { EventBuilder, EventKind, NostrLink, NostrPrefix, TaggedNostrEvent, tryParseNostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { ZapTarget } from "@snort/wallet";
import { Menu, MenuItem } from "@szhsin/react-menu";
import classNames from "classnames";
import { ClipboardEventHandler, DragEvent, useEffect } from "react";
@ -29,7 +30,6 @@ import { useNoteCreator } from "@/State/NoteCreator";
import { openFile, trackEvent } from "@/Utils";
import useFileUpload, { addExtensionToNip94Url, nip94TagsToIMeta, readNip94Tags } from "@/Utils/Upload";
import { GetPowWorker } from "@/Utils/wasm";
import { ZapTarget } from "@/Utils/Zapper";
import { OkResponseRow } from "./OkResponseRow";

View File

@ -1,6 +1,7 @@
import { barrierQueue } from "@snort/shared";
import { NostrLink, ParsedZap, TaggedNostrEvent } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { Zapper, ZapTarget } from "@snort/wallet";
import React, { useEffect, useState } from "react";
import { useIntl } from "react-intl";
import { useLongPress } from "use-long-press";
@ -13,7 +14,6 @@ import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { getDisplayName } from "@/Utils";
import { Zapper, ZapTarget } from "@/Utils/Zapper";
import { ZapPoolController } from "@/Utils/ZapPoolController";
import { useWallet } from "@/Wallet";
@ -140,13 +140,7 @@ export const FooterZapButton = ({ ev, zaps, onClickZappers }: ZapIconProps) => {
<ZapsSummary zaps={zaps} onClick={onClickZappers ?? (() => {})} />
</div>
{showZapModal && (
<ZapModal
targets={getZapTarget()}
onClose={() => setShowZapModal(false)}
note={ev.id}
show={true}
allocatePool={true}
/>
<ZapModal targets={getZapTarget()} onClose={() => setShowZapModal(false)} show={true} allocatePool={true} />
)}
</>
)}

View File

@ -2,11 +2,11 @@ import "./ZapButton.css";
import { HexKey, NostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { ZapTarget } from "@snort/wallet";
import { useState } from "react";
import Icon from "@/Components/Icons/Icon";
import ZapModal from "@/Components/ZapModal/ZapModal";
import { ZapTarget } from "@/Utils/Zapper";
const ZapButton = ({
pubkey,

View File

@ -1,6 +1,7 @@
import "./ZapGoal.css";
import { NostrEvent, NostrLink } from "@snort/system";
import { Zapper } from "@snort/wallet";
import { useState } from "react";
import { FormattedNumber } from "react-intl";
@ -10,7 +11,6 @@ import ZapModal from "@/Components/ZapModal/ZapModal";
import useZapsFeed from "@/Feed/ZapsFeed";
import { findTag } from "@/Utils";
import { formatShort } from "@/Utils/Number";
import { Zapper } from "@/Utils/Zapper";
export function ZapGoal({ ev }: { ev: NostrEvent }) {
const [zap, setZap] = useState(false);

View File

@ -1,6 +1,7 @@
import "./ZapModal.css";
import { LNURLSuccessAction } from "@snort/shared";
import { Zapper, ZapTarget, ZapTargetResult } from "@snort/wallet";
import { ReactNode, useEffect, useState } from "react";
import CloseButton from "@/Components/Button/CloseButton";
@ -12,7 +13,6 @@ import { ZapModalTitle } from "@/Components/ZapModal/ZapModalTitle";
import { ZapType } from "@/Components/ZapModal/ZapType";
import useEventPublisher from "@/Hooks/useEventPublisher";
import { debounce } from "@/Utils";
import { Zapper, ZapTarget, ZapTargetResult } from "@/Utils/Zapper";
import { useWallet } from "@/Wallet";
export interface SendSatsProps {

View File

@ -1,3 +1,4 @@
import { Zapper } from "@snort/wallet";
import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
@ -9,7 +10,6 @@ import { ZapTypeSelector } from "@/Components/ZapModal/ZapTypeSelector";
import useLogin from "@/Hooks/useLogin";
import usePreferences from "@/Hooks/usePreferences";
import { formatShort } from "@/Utils/Number";
import { Zapper } from "@/Utils/Zapper";
export interface SendSatsInputSelection {
amount: number;

View File

@ -1,10 +1,9 @@
import { LNWallet } from "@snort/wallet";
import { LNWallet, ZapTargetResult } from "@snort/wallet";
import { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import Copy from "@/Components/Copy/Copy";
import QrCode from "@/Components/QrCode";
import { ZapTargetResult } from "@/Utils/Zapper";
export function ZapModalInvoice(props: {
invoice: Array<ZapTargetResult>;

View File

@ -1,10 +1,9 @@
import React from "react";
import { Zapper, ZapTarget } from "@snort/wallet";
import { FormattedMessage } from "react-intl";
import ProfileImage from "@/Components/User/ProfileImage";
import { SendSatsInputSelection } from "@/Components/ZapModal/ZapModalInput";
import { formatShort } from "@/Utils/Number";
import { Zapper, ZapTarget } from "@/Utils/Zapper";
export function ZapModalTitle({
targets,

View File

@ -1,5 +1,6 @@
import { LNURL } from "@snort/shared";
import { CachedMetadata, encodeTLVEntries, NostrLink, NostrPrefix, TLVEntryType } from "@snort/system";
import { ZapTarget } from "@snort/wallet";
import React, { useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Link, useNavigate } from "react-router-dom";
@ -19,7 +20,6 @@ import ZapModal from "@/Components/ZapModal/ZapModal";
import useModeration from "@/Hooks/useModeration";
import { hexToBech32 } from "@/Utils";
import { LoginSessionType, LoginStore } from "@/Utils/Login";
import { ZapTarget } from "@/Utils/Zapper";
const AvatarSection = ({
user,

View File

@ -1,9 +1,8 @@
import { ExternalStore } from "@snort/shared";
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
import { ZapTarget } from "@snort/wallet";
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";
import { ZapTarget } from "@/Utils/Zapper";
interface NoteCreatorDataSnapshot {
show: boolean;
note: string;

View File

@ -1,213 +0,0 @@
import { isHex, LNURL } from "@snort/shared";
import { EventPublisher, NostrEvent, NostrLink, SystemInterface } from "@snort/system";
import { LNWallet, WalletInvoiceState } from "@snort/wallet";
import { generateRandomKey } from "@/Utils/Login";
export interface ZapTarget {
type: "lnurl" | "pubkey";
value: string;
weight: number;
memo?: string;
name?: string;
zap?: {
pubkey: string;
anon: boolean;
event?: NostrLink;
};
}
export interface ZapTargetResult {
target: ZapTarget;
paid: boolean;
sent: number;
fee: number;
pr: string;
error?: Error;
}
interface ZapTargetLoaded {
target: ZapTarget;
svc?: LNURL;
}
export class Zapper {
#inProgress = false;
#loadedTargets?: Array<ZapTargetLoaded>;
constructor(
readonly system: SystemInterface,
readonly publisher?: EventPublisher,
readonly onResult?: (r: ZapTargetResult) => void,
) {}
/**
* Create targets from Event
*/
static fromEvent(ev: NostrEvent) {
if (ev.tags.some(a => a[0] === "zap")) {
return ev.tags
.filter(a => a[0] === "zap")
.map(v => {
if (v[1].length === 64 && isHex(v[1]) && v.length === 4) {
// NIP-57.G
return {
type: "pubkey",
value: v[1],
weight: Number(v[3] ?? 0),
zap: {
pubkey: v[1],
event: NostrLink.fromEvent(ev),
},
} as ZapTarget;
} else {
// assume event specific zap target
return {
type: "lnurl",
value: v[1],
weight: 1,
zap: {
pubkey: ev.pubkey,
event: NostrLink.fromEvent(ev),
},
} as ZapTarget;
}
});
} else {
return [
{
type: "pubkey",
value: ev.pubkey,
weight: 1,
zap: {
pubkey: ev.pubkey,
event: NostrLink.fromEvent(ev),
},
} as ZapTarget,
];
}
}
async send(wallet: LNWallet | undefined, targets: Array<ZapTarget>, amount: number) {
if (this.#inProgress) {
throw new Error("Payout already in progress");
}
this.#inProgress = true;
const total = targets.reduce((acc, v) => (acc += v.weight), 0);
const ret = [] as Array<ZapTargetResult>;
for (const t of targets) {
const toSend = Math.floor(amount * (t.weight / total));
try {
const svc = await this.#getService(t);
if (!svc) {
throw new Error(`Failed to get invoice from ${t.value}`);
}
const relays = [...this.system.pool].filter(([, v]) => !v.ephemeral).map(([k]) => k);
const pub = t.zap?.anon ?? false ? EventPublisher.privateKey(generateRandomKey().privateKey) : this.publisher;
const zap =
t.zap && svc.canZap
? await pub?.zap(toSend * 1000, t.zap.pubkey, relays, t.zap?.event, t.memo, eb => {
if (t.zap?.anon) {
eb.tag(["anon", ""]);
}
return eb;
})
: undefined;
const invoice = await svc.getInvoice(toSend, t.memo, zap);
if (invoice?.pr) {
const res = await wallet?.payInvoice(invoice.pr);
ret.push({
target: t,
paid: res?.state === WalletInvoiceState.Paid,
sent: toSend,
pr: invoice.pr,
fee: res?.fees ?? 0,
});
this.onResult?.(ret[ret.length - 1]);
} else {
throw new Error(`Failed to get invoice from ${t.value}`);
}
} catch (e) {
ret.push({
target: t,
paid: false,
sent: 0,
fee: 0,
pr: "",
error: e as Error,
});
this.onResult?.(ret[ret.length - 1]);
}
}
this.#inProgress = false;
return ret;
}
async load(targets: Array<ZapTarget>) {
const svcs = targets.map(async a => {
return {
target: a,
loading: await this.#getService(a),
};
});
const loaded = await Promise.all(svcs);
this.#loadedTargets = loaded.map(a => ({
target: a.target,
svc: a.loading,
}));
}
/**
* Any target supports zaps
*/
canZap() {
return this.#loadedTargets?.some(a => a.svc?.canZap ?? false);
}
/**
* Max comment length which can be sent to all (smallest comment length)
*/
maxComment() {
return (
this.#loadedTargets
?.map(a => (a.svc?.canZap ? 255 : a.svc?.maxCommentLength ?? 0))
.reduce((acc, v) => (acc > v ? v : acc), 255) ?? 0
);
}
/**
* Max of the min amounts
*/
minAmount() {
return this.#loadedTargets?.map(a => a.svc?.min ?? 0).reduce((acc, v) => (acc < v ? v : acc), 1000) ?? 0;
}
/**
* Min of the max amounts
*/
maxAmount() {
return this.#loadedTargets?.map(a => a.svc?.max ?? 100e9).reduce((acc, v) => (acc > v ? v : acc), 100e9) ?? 0;
}
async #getService(t: ZapTarget) {
try {
if (t.type === "lnurl") {
const svc = new LNURL(t.value);
await svc.load();
return svc;
} else if (t.type === "pubkey") {
const profile = await this.system.profileLoader.fetch(t.value);
if (profile) {
const svc = new LNURL(profile.lud16 ?? profile.lud06 ?? "");
await svc.load();
return svc;
}
}
} catch {
// nothing
}
}
}