216 lines
5.8 KiB
TypeScript

import NDK, {
NDKEvent,
NDKUser,
NDKSigner,
zapInvoiceFromEvent,
type NostrEvent,
} from "@nostr-dev-kit/ndk";
import { requestProvider } from "webln";
import { bech32 } from "@scure/base";
import { z } from "zod";
import { createZodFetcher } from "zod-fetch";
import { getTagValues, getTagsAllValues } from "../nostr/utils";
import { unixTimeNowInSeconds } from "../nostr/dates";
import { createEvent } from "./create";
import { findEphemeralSigner } from "@/lib/actions/ephemeral";
import { log } from "@/lib/utils";
const fetchWithZod = createZodFetcher();
const ZapEndpointResponseSchema = z.object({
nostrPubkey: z.string(),
});
export async function sendZap(
ndk: NDK,
amount: number,
_event: NostrEvent,
comment?: string,
) {
log("func", "sendZap");
const event = await new NDKEvent(ndk, _event);
log("info", JSON.stringify(event));
const pr = await event.zap(amount * 1000, comment);
if (!pr) {
log("info", "No PR");
return;
}
const webln = await requestProvider();
return await webln.sendPayment(pr);
}
export async function checkPayment(
ndk: NDK,
tagId: string,
pubkey: string,
event: NostrEvent,
) {
const paymentEvents = await ndk.fetchEvents({
kinds: [9735],
["#a"]: [tagId],
});
if (!paymentEvents) return;
const paymentEvent = Array.from(paymentEvents).find(
(e) => zapInvoiceFromEvent(e)?.zappee === pubkey,
);
if (!paymentEvent) return;
const invoice = zapInvoiceFromEvent(paymentEvent);
if (!invoice) {
console.log("No invoice");
return;
}
const zappedUser = ndk.getUser({
hexpubkey: invoice.zapped,
});
await zappedUser.fetchProfile();
if (!zappedUser.profile) {
console.log("No zappedUser profile");
return;
}
const { lud16, lud06 } = zappedUser.profile;
let zapEndpoint: null | string = null;
if (lud16 && !lud16.startsWith("LNURL")) {
const [name, domain] = lud16.split("@");
zapEndpoint = `https://${domain}/.well-known/lnurlp/${name}`;
} else if (lud06) {
const { words } = bech32.decode(lud06, 1e3);
const data = bech32.fromWords(words);
const utf8Decoder = new TextDecoder("utf-8");
zapEndpoint = utf8Decoder.decode(data);
}
if (!zapEndpoint) {
console.log("No zapEndpoint");
return;
}
const { nostrPubkey } = await fetchWithZod(
// The schema you want to validate with
ZapEndpointResponseSchema,
// Any parameters you would usually pass to fetch
zapEndpoint,
{
method: "GET",
},
);
if (!nostrPubkey) return;
console.log("nostrPubkey", nostrPubkey);
console.log("Invoice amount", invoice.amount);
console.log("Price", parseInt(getTagValues("price", event.tags) ?? "0"));
return (
nostrPubkey === paymentEvent.pubkey &&
invoice.amount >= parseInt(getTagValues("price", event.tags) ?? "0")
);
}
export async function updateListUsersFromZaps(
ndk: NDK,
tagId: string,
event: NostrEvent,
) {
log("func", "updateListUsersFromZaps");
const SECONDS_IN_MONTH = 2_628_000;
const SECONDS_IN_YEAR = SECONDS_IN_MONTH * 365;
const paymentEvents = await ndk.fetchEvents({
kinds: [9735],
["#a"]: [tagId],
since: unixTimeNowInSeconds() - SECONDS_IN_YEAR,
});
const paymentInvoices = Array.from(paymentEvents).map((paymentEvent) =>
zapInvoiceFromEvent(paymentEvent),
);
const currentUsers = getTagsAllValues("p", event.tags);
let validUsers: string[][] = currentUsers.filter(
([pubkey, relay, petname, expiryUnix]) =>
parseInt(expiryUnix ?? "0") > unixTimeNowInSeconds(),
);
const newUsers: string[] = currentUsers.map(([pub]) => pub as string);
for (const paymentInvoice of paymentInvoices) {
if (
!paymentInvoice ||
validUsers.find(([pubkey]) => pubkey === paymentInvoice.zappee)
) {
continue;
}
const isValid = await checkPayment(
ndk,
tagId,
paymentInvoice.zappee,
event,
);
if (isValid) {
validUsers.push([
paymentInvoice.zappee,
"",
"",
(unixTimeNowInSeconds() + SECONDS_IN_YEAR).toString(),
]);
newUsers.push(paymentInvoice.zappee);
// Send old codes to user
}
}
log("info", "New users", newUsers.toString());
await sendCodesToNewUsers(ndk, newUsers, tagId, event);
// Add self;
const selfIndex = validUsers.findIndex(([vu]) => vu === event.pubkey);
if (selfIndex === -1) {
validUsers.push([event.pubkey, "", "self", "4000000000"]);
}
return createEvent(ndk, {
...event,
kind: event.kind as number,
tags: [
...event.tags.filter(([key]) => key !== "p"),
...validUsers.map((user) => ["p", ...user]),
],
});
}
async function sendCodesToNewUsers(
ndk: NDK,
users: string[],
tagId: string,
event: NostrEvent,
) {
const signer = await findEphemeralSigner(ndk, ndk.signer!, {
associatedEventNip19: new NDKEvent(ndk, event).encode(),
});
log("func", "sendCodesToNewUsers");
log("info", "Signer", signer?.toString());
if (!signer) return;
const delegate = await signer.user();
const messages = await ndk.fetchEvents({
authors: [delegate.pubkey],
kinds: [4],
["#p"]: [tagId.split(":")?.[1] ?? ""],
});
const codes: [string, string][] = [];
for (const message of Array.from(messages)) {
await message.decrypt();
codes.push([getTagValues("e", message.tags) ?? "", message.content]);
}
log("info", "codes", codes.toString());
for (const user of users) {
for (const [event, code] of codes) {
const messageEvent = new NDKEvent(ndk, {
content: code,
kind: 4,
tags: [
["p", user],
["e", event],
["client", "flockstr"],
],
pubkey: delegate.pubkey,
} as NostrEvent);
log("info", "sending message");
await messageEvent.encrypt(new NDKUser({ hexpubkey: user }), signer);
await messageEvent.sign(signer);
await messageEvent.publish();
}
}
}