create lists with subscriptions
This commit is contained in:
parent
9a83cdc53e
commit
8434c358ff
@ -4,6 +4,7 @@ import { Toaster } from "sonner";
|
|||||||
import { ModalProvider } from "./modal/provider";
|
import { ModalProvider } from "./modal/provider";
|
||||||
import useRouteChange from "@/lib/hooks/useRouteChange";
|
import useRouteChange from "@/lib/hooks/useRouteChange";
|
||||||
import { NDKProvider } from "./ndk";
|
import { NDKProvider } from "./ndk";
|
||||||
|
import SignerProvider from "./signer";
|
||||||
import { RELAYS } from "@/constants";
|
import { RELAYS } from "@/constants";
|
||||||
|
|
||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
@ -16,13 +17,12 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
|||||||
useRouteChange(handleRouteChange);
|
useRouteChange(handleRouteChange);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NDKProvider
|
<NDKProvider relayUrls={RELAYS}>
|
||||||
relayUrls={RELAYS}
|
<SignerProvider>
|
||||||
>
|
|
||||||
|
|
||||||
<Toaster richColors className="dark:hidden" />
|
<Toaster richColors className="dark:hidden" />
|
||||||
<Toaster theme="dark" className="hidden dark:block" />
|
<Toaster theme="dark" className="hidden dark:block" />
|
||||||
<ModalProvider>{children}</ModalProvider>
|
<ModalProvider>{children}</ModalProvider>
|
||||||
|
</SignerProvider>
|
||||||
</NDKProvider>
|
</NDKProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
111
app/_providers/signer/index.tsx
Normal file
111
app/_providers/signer/index.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
useMemo,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import useCurrentUser from "@/lib/hooks/useCurrentUser";
|
||||||
|
import { findEphemeralSigner } from "@/lib/actions/ephemeral";
|
||||||
|
import {
|
||||||
|
NDKPrivateKeySigner,
|
||||||
|
NDKUser,
|
||||||
|
type NDKSigner,
|
||||||
|
type NDKList,
|
||||||
|
} from "@nostr-dev-kit/ndk";
|
||||||
|
import { useNDK } from "../ndk";
|
||||||
|
|
||||||
|
export type SignerStoreItem = {
|
||||||
|
signer: NDKPrivateKeySigner;
|
||||||
|
user: NDKUser;
|
||||||
|
saved: boolean;
|
||||||
|
title?: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
type SignerItems = Map<string, SignerStoreItem>;
|
||||||
|
|
||||||
|
type SignerContextProps = {
|
||||||
|
npubSigners: Map<string, NDKSigner>;
|
||||||
|
signers: SignerItems;
|
||||||
|
getSigner: (list: NDKList) => Promise<SignerStoreItem>;
|
||||||
|
};
|
||||||
|
const SignerContext = createContext<SignerContextProps | undefined>(undefined);
|
||||||
|
|
||||||
|
export default function SignerProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children?: ReactNode | undefined;
|
||||||
|
}) {
|
||||||
|
const { currentUser } = useCurrentUser();
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
const [signers, setSigners] = useState(new Map());
|
||||||
|
const npubSigners = useMemo(() => {
|
||||||
|
const npubs = new Map<string, NDKSigner>();
|
||||||
|
for (const entry of signers) {
|
||||||
|
const { user, signer } = entry[1];
|
||||||
|
npubs.set(user.npub, signer);
|
||||||
|
}
|
||||||
|
return npubs;
|
||||||
|
}, [signers]);
|
||||||
|
|
||||||
|
async function getDelegatedSignerName(list: NDKList) {
|
||||||
|
let name = "";
|
||||||
|
console.log("getDelegatedSignerName", list);
|
||||||
|
if (!currentUser?.profile) {
|
||||||
|
console.log("fetching user profile");
|
||||||
|
await currentUser?.fetchProfile();
|
||||||
|
}
|
||||||
|
name = `${
|
||||||
|
currentUser?.profile?.displayName ??
|
||||||
|
currentUser?.profile?.name ??
|
||||||
|
currentUser?.profile?.nip05 ??
|
||||||
|
`${currentUser?.npub.slice(0, 9)}...`
|
||||||
|
}'s `;
|
||||||
|
|
||||||
|
return name + list.title ?? "List";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSigner(list: NDKList): Promise<SignerStoreItem> {
|
||||||
|
const id = list.encode();
|
||||||
|
let item = signers.get(id);
|
||||||
|
if (item) return item;
|
||||||
|
let signer = await findEphemeralSigner(ndk!, ndk!.signer!, {
|
||||||
|
associatedEventNip19: list.encode(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (signer) {
|
||||||
|
console.log(`found a signer for list ${list.title}`);
|
||||||
|
item = {
|
||||||
|
signer: signer!,
|
||||||
|
user: await signer.user(),
|
||||||
|
saved: true,
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log(`no signer found for list ${list.title}`);
|
||||||
|
signer = NDKPrivateKeySigner.generate();
|
||||||
|
console.log(`Signer generated ${JSON.stringify(signer)}`);
|
||||||
|
item = {
|
||||||
|
signer,
|
||||||
|
user: await signer.user(),
|
||||||
|
saved: false,
|
||||||
|
title: await getDelegatedSignerName(list),
|
||||||
|
id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
item.user.ndk = ndk;
|
||||||
|
setSigners((prev) => new Map(prev.set(id, item)));
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SignerContext.Provider value={{ signers, npubSigners, getSigner }}>
|
||||||
|
{children}
|
||||||
|
</SignerContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSigner() {
|
||||||
|
return useContext(SignerContext);
|
||||||
|
}
|
126
components/Modals/CreateList.tsx
Normal file
126
components/Modals/CreateList.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import FormModal from "./FormModal";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { useModal } from "@/app/_providers/modal/provider";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { generateRandomString } from "@/lib/nostr";
|
||||||
|
import { satsToBtc } from "@/lib/utils";
|
||||||
|
import useCurrentUser from "@/lib/hooks/useCurrentUser";
|
||||||
|
import { useNDK } from "@/app/_providers/ndk";
|
||||||
|
import { useSigner, type SignerStoreItem } from "@/app/_providers/signer";
|
||||||
|
import { createEvent } from "@/lib/actions/create";
|
||||||
|
import { getTagValues } from "@/lib/nostr/utils";
|
||||||
|
import { NDKList } from "@nostr-dev-kit/ndk";
|
||||||
|
import { saveEphemeralSigner } from "@/lib/actions/ephemeral";
|
||||||
|
|
||||||
|
const CreateListSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
image: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
subscriptions: z.boolean(),
|
||||||
|
price: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type CreateListType = z.infer<typeof CreateListSchema>;
|
||||||
|
|
||||||
|
export default function CreateList() {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const modal = useModal();
|
||||||
|
|
||||||
|
const { currentUser, updateUser } = useCurrentUser();
|
||||||
|
const { ndk } = useNDK();
|
||||||
|
const { getSigner } = useSigner()!;
|
||||||
|
async function handleSubmit(data: CreateListType) {
|
||||||
|
setIsLoading(true);
|
||||||
|
const random = generateRandomString();
|
||||||
|
const tags = [
|
||||||
|
["title", data.title],
|
||||||
|
["name", data.title],
|
||||||
|
["d", random],
|
||||||
|
];
|
||||||
|
if (data.description) {
|
||||||
|
tags.push(["description", data.description]);
|
||||||
|
tags.push(["summary", data.description]);
|
||||||
|
}
|
||||||
|
if (data.image) {
|
||||||
|
tags.push(["image", data.image]);
|
||||||
|
tags.push(["picture", data.image]);
|
||||||
|
}
|
||||||
|
if (data.subscriptions) {
|
||||||
|
tags.push(["subscriptions", "true"]);
|
||||||
|
}
|
||||||
|
if (data.price) {
|
||||||
|
tags.push(["price", satsToBtc(data.price).toString(), "btc", "year"]);
|
||||||
|
}
|
||||||
|
const event = await createEvent(ndk!, {
|
||||||
|
content: "",
|
||||||
|
kind: 30001,
|
||||||
|
tags: tags,
|
||||||
|
});
|
||||||
|
if (event && getTagValues("subscriptions", event.tags)) {
|
||||||
|
await getSigner(new NDKList(ndk, event.rawEvent()))
|
||||||
|
.then((delegateSigner) =>
|
||||||
|
saveEphemeralSigner(ndk!, delegateSigner.signer, {
|
||||||
|
associatedEvent: event,
|
||||||
|
keyProfile: {
|
||||||
|
name: delegateSigner.title,
|
||||||
|
picture: currentUser?.profile?.image,
|
||||||
|
lud06: currentUser?.profile?.lud06,
|
||||||
|
lud16: currentUser?.profile?.lud16,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
(savedSigner) => savedSigner,
|
||||||
|
// updateList(ndk!, event.rawEvent(), [
|
||||||
|
// ["delegate", savedSigner.hexpubkey],
|
||||||
|
// ]),
|
||||||
|
)
|
||||||
|
.catch((err) => console.log("Error creating delegate"));
|
||||||
|
}
|
||||||
|
// getLists(currentUser!.hexpubkey);
|
||||||
|
setIsLoading(false);
|
||||||
|
toast.success("List Created!");
|
||||||
|
modal?.hide();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormModal
|
||||||
|
title="Create List"
|
||||||
|
fields={[
|
||||||
|
{
|
||||||
|
label: "Title",
|
||||||
|
type: "input",
|
||||||
|
slug: "title",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Description",
|
||||||
|
type: "text-area",
|
||||||
|
slug: "description",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Image",
|
||||||
|
type: "input",
|
||||||
|
slug: "image",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Enable subscriptions",
|
||||||
|
type: "toggle",
|
||||||
|
slug: "subscriptions",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Price",
|
||||||
|
subtitle: "sats/year",
|
||||||
|
type: "number",
|
||||||
|
slug: "price",
|
||||||
|
condition: "subscriptions",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
formSchema={CreateListSchema}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
isSubmitting={isLoading}
|
||||||
|
cta={{
|
||||||
|
text: "Create List",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -9,6 +9,7 @@ import {
|
|||||||
FieldValues,
|
FieldValues,
|
||||||
DefaultValues,
|
DefaultValues,
|
||||||
Path,
|
Path,
|
||||||
|
PathValue,
|
||||||
} from "react-hook-form";
|
} from "react-hook-form";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Template from "./Template";
|
import Template from "./Template";
|
||||||
@ -37,6 +38,7 @@ type FieldOptions =
|
|||||||
| "toggle"
|
| "toggle"
|
||||||
| "horizontal-tabs"
|
| "horizontal-tabs"
|
||||||
| "input"
|
| "input"
|
||||||
|
| "number"
|
||||||
| "text-area"
|
| "text-area"
|
||||||
| "custom";
|
| "custom";
|
||||||
|
|
||||||
@ -44,11 +46,13 @@ type DefaultFieldType<TSchema> = {
|
|||||||
label: string;
|
label: string;
|
||||||
slug: keyof z.infer<z.Schema<TSchema>> & string;
|
slug: keyof z.infer<z.Schema<TSchema>> & string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
subtitle?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
lines?: number;
|
lines?: number;
|
||||||
styles?: string;
|
styles?: string;
|
||||||
value?: string | number | boolean;
|
value?: string | number | boolean;
|
||||||
custom?: ReactNode;
|
custom?: ReactNode;
|
||||||
|
condition?: keyof z.infer<z.Schema<TSchema>> & string;
|
||||||
options?: { label: string; value: string; icon?: ReactNode }[];
|
options?: { label: string; value: string; icon?: ReactNode }[];
|
||||||
};
|
};
|
||||||
type FieldType<TSchema> = DefaultFieldType<TSchema> &
|
type FieldType<TSchema> = DefaultFieldType<TSchema> &
|
||||||
@ -91,10 +95,11 @@ export default function FormModal<TSchema extends FieldValues>({
|
|||||||
defaultValues: defaultValues as DefaultValues<TSchema>,
|
defaultValues: defaultValues as DefaultValues<TSchema>,
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
});
|
});
|
||||||
|
const { watch, setValue } = form;
|
||||||
return (
|
return (
|
||||||
<Template title={title} className="md:max-w-[400px]">
|
<Template title={title} className="md:max-w-[400px]">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
{fields.map(
|
{fields.map(
|
||||||
({
|
({
|
||||||
label,
|
label,
|
||||||
@ -102,8 +107,12 @@ export default function FormModal<TSchema extends FieldValues>({
|
|||||||
type,
|
type,
|
||||||
placeholder,
|
placeholder,
|
||||||
description,
|
description,
|
||||||
|
subtitle,
|
||||||
|
condition,
|
||||||
...fieldProps
|
...fieldProps
|
||||||
}) => (
|
}) => {
|
||||||
|
if (!condition) {
|
||||||
|
return (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name={slug as Path<TSchema>}
|
name={slug as Path<TSchema>}
|
||||||
@ -152,6 +161,14 @@ export default function FormModal<TSchema extends FieldValues>({
|
|||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
) : type === "number" ? (
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={placeholder}
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
) : (
|
) : (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder={placeholder} {...field} />
|
<Input placeholder={placeholder} {...field} />
|
||||||
@ -164,7 +181,97 @@ export default function FormModal<TSchema extends FieldValues>({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
|
}
|
||||||
|
const state = watch(condition as Path<TSchema>);
|
||||||
|
if (!state) return;
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={slug as Path<TSchema>}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem
|
||||||
|
className={cn(
|
||||||
|
type === "toggle" &&
|
||||||
|
"flex items-center gap-x-3 space-y-0",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FormLabel>
|
||||||
|
{label}
|
||||||
|
{!!subtitle && (
|
||||||
|
<span className="ml-1.5 text-[0.8rem] font-normal text-muted-foreground ">
|
||||||
|
{subtitle}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
{type === "input" ? (
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={placeholder} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
) : type === "text-area" ? (
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...field}
|
||||||
|
className="auto-sizing"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
) : type === "select" ? (
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={placeholder} />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent className="z-modal+">
|
||||||
|
{fieldProps.options?.map((o) => (
|
||||||
|
<SelectItem key={o.value} value={o.value}>
|
||||||
|
{o.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : type === "toggle" ? (
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
) : type === "number" ? (
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder={placeholder}
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue(
|
||||||
|
field.name,
|
||||||
|
parseInt(e.target.value) as PathValue<
|
||||||
|
TSchema,
|
||||||
|
Path<TSchema>
|
||||||
|
>,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={placeholder} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
{!!description && (
|
||||||
|
<FormDescription>{description}</FormDescription>
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
<Button type="submit" className="w-full" loading={isSubmitting}>
|
<Button type="submit" className="w-full" loading={isSubmitting}>
|
||||||
{cta.text}
|
{cta.text}
|
||||||
|
@ -16,6 +16,7 @@ import {
|
|||||||
import { RiSubtractFill, RiAddFill } from "react-icons/ri";
|
import { RiSubtractFill, RiAddFill } from "react-icons/ri";
|
||||||
import { formatCount } from "@/lib/utils";
|
import { formatCount } from "@/lib/utils";
|
||||||
import LoginModal from "./Login";
|
import LoginModal from "./Login";
|
||||||
|
import CreateList from "./CreateList";
|
||||||
|
|
||||||
export default function NewEventModal() {
|
export default function NewEventModal() {
|
||||||
const modal = useModal();
|
const modal = useModal();
|
||||||
@ -37,7 +38,12 @@ export default function NewEventModal() {
|
|||||||
<HiNewspaper className="h-4 w-4" />
|
<HiNewspaper className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button className="w-full gap-x-1">
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
modal?.swap(<CreateList />);
|
||||||
|
}}
|
||||||
|
className="w-full gap-x-1"
|
||||||
|
>
|
||||||
<span>Content List</span>
|
<span>Content List</span>
|
||||||
<HiBookmarkSquare className="h-4 w-4" />
|
<HiBookmarkSquare className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -18,7 +18,7 @@ export default function Template({ title, children, className }: ModalProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative w-full grow bg-background p-4 shadow sm:border md:rounded-lg md:p-6",
|
"relative w-full grow bg-background p-4 shadow md:rounded-lg md:border md:p-6",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const Avatar = React.forwardRef<
|
const Avatar = React.forwardRef<
|
||||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
@ -13,12 +13,12 @@ const Avatar = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||||
|
|
||||||
const AvatarImage = React.forwardRef<
|
const AvatarImage = React.forwardRef<
|
||||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||||
@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef<
|
|||||||
className={cn("aspect-square h-full w-full", className)}
|
className={cn("aspect-square h-full w-full", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||||
|
|
||||||
const AvatarFallback = React.forwardRef<
|
const AvatarFallback = React.forwardRef<
|
||||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||||
@ -39,12 +39,12 @@ const AvatarFallback = React.forwardRef<
|
|||||||
<AvatarPrimitive.Fallback
|
<AvatarPrimitive.Fallback
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
"flex h-full w-full items-center justify-center rounded-full bg-muted uppercase",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||||
|
|
||||||
export { Avatar, AvatarImage, AvatarFallback }
|
export { Avatar, AvatarImage, AvatarFallback };
|
||||||
|
267
lib/actions/create.ts
Normal file
267
lib/actions/create.ts
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import { nip19 } from "nostr-tools";
|
||||||
|
import NDK, {
|
||||||
|
NDKEvent,
|
||||||
|
type NDKPrivateKeySigner,
|
||||||
|
type NDKTag,
|
||||||
|
type NDKList,
|
||||||
|
type NostrEvent,
|
||||||
|
NDKUser,
|
||||||
|
} from "@nostr-dev-kit/ndk";
|
||||||
|
import { generateRandomString, encryptMessage } from "@/lib/nostr";
|
||||||
|
import { unixTimeNowInSeconds } from "@/lib/nostr/dates";
|
||||||
|
import { getTagsValues } from "@/lib/nostr/utils";
|
||||||
|
|
||||||
|
export async function createEvent(
|
||||||
|
ndk: NDK,
|
||||||
|
event: {
|
||||||
|
content: string;
|
||||||
|
kind: number;
|
||||||
|
tags: string[][];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const pubkey = await window.nostr?.getPublicKey();
|
||||||
|
if (!pubkey || !window.nostr) {
|
||||||
|
throw new Error("No public key provided!");
|
||||||
|
}
|
||||||
|
const eventToPublish = new NDKEvent(ndk, {
|
||||||
|
...event,
|
||||||
|
pubkey,
|
||||||
|
created_at: unixTimeNowInSeconds(),
|
||||||
|
} as NostrEvent);
|
||||||
|
console.log("eventToPublish ", eventToPublish);
|
||||||
|
|
||||||
|
const signed = await eventToPublish.sign();
|
||||||
|
console.log("signed", signed);
|
||||||
|
await eventToPublish.publish();
|
||||||
|
return eventToPublish;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
alert("An error has occured");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function createEncryptedEventOnPrivateList(
|
||||||
|
ndk: NDK,
|
||||||
|
event: {
|
||||||
|
content: string;
|
||||||
|
kind: number;
|
||||||
|
tags: string[][];
|
||||||
|
},
|
||||||
|
list: NDKList,
|
||||||
|
delegateSigner?: NDKPrivateKeySigner,
|
||||||
|
) {
|
||||||
|
const pubkey = await window.nostr?.getPublicKey();
|
||||||
|
if (!pubkey || !window.nostr) {
|
||||||
|
throw new Error("No public key provided!");
|
||||||
|
}
|
||||||
|
const eventToPublish = new NDKEvent(ndk, {
|
||||||
|
...event,
|
||||||
|
tags: [...event.tags, ["client", "ordstr"]],
|
||||||
|
pubkey,
|
||||||
|
created_at: unixTimeNowInSeconds(),
|
||||||
|
} as NostrEvent);
|
||||||
|
await eventToPublish.sign();
|
||||||
|
const rawEventString = JSON.stringify(eventToPublish.rawEvent());
|
||||||
|
const passphrase = generateRandomString();
|
||||||
|
// const passphrase = "test";
|
||||||
|
const encryptedRawEventString = await encryptMessage(
|
||||||
|
rawEventString,
|
||||||
|
passphrase,
|
||||||
|
);
|
||||||
|
const signer = delegateSigner ?? ndk.signer!;
|
||||||
|
const user = await signer.user();
|
||||||
|
|
||||||
|
const newEvent = new NDKEvent(ndk, {
|
||||||
|
content: encryptedRawEventString,
|
||||||
|
kind: 3745,
|
||||||
|
tags: [
|
||||||
|
["kind", event.kind.toString()],
|
||||||
|
["client", "ordstr"],
|
||||||
|
],
|
||||||
|
pubkey: user.hexpubkey,
|
||||||
|
} as NostrEvent);
|
||||||
|
|
||||||
|
await newEvent.sign(signer);
|
||||||
|
|
||||||
|
await newEvent.publish();
|
||||||
|
|
||||||
|
const tag = newEvent.tagReference();
|
||||||
|
if (!tag) return;
|
||||||
|
|
||||||
|
// Add event to list
|
||||||
|
await list.addItem(tag, undefined, false);
|
||||||
|
await list.sign();
|
||||||
|
await list.publish();
|
||||||
|
|
||||||
|
// Send DMs to subscribers
|
||||||
|
const subscribers = getTagsValues("p", list.tags);
|
||||||
|
for (const subscriber of subscribers) {
|
||||||
|
const messageEvent = new NDKEvent(ndk, {
|
||||||
|
content: passphrase,
|
||||||
|
kind: 4,
|
||||||
|
tags: [
|
||||||
|
["p", subscriber],
|
||||||
|
["e", newEvent.id],
|
||||||
|
["client", "ordstr"],
|
||||||
|
],
|
||||||
|
pubkey: user.hexpubkey,
|
||||||
|
} as NostrEvent);
|
||||||
|
console.log("message to create", messageEvent);
|
||||||
|
await messageEvent.encrypt(new NDKUser({ hexpubkey: subscriber }), signer);
|
||||||
|
console.log("Encrypted message", messageEvent);
|
||||||
|
await messageEvent.sign(signer);
|
||||||
|
await messageEvent.publish();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
export async function createReaction(
|
||||||
|
ndk: NDK,
|
||||||
|
content: "+" | "-",
|
||||||
|
event: {
|
||||||
|
id: string;
|
||||||
|
pubkey: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return createEvent(ndk, {
|
||||||
|
content,
|
||||||
|
kind: 7,
|
||||||
|
tags: [
|
||||||
|
["e", event.id],
|
||||||
|
["p", event.pubkey],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function createList(
|
||||||
|
ndk: NDK,
|
||||||
|
title: string,
|
||||||
|
description?: string,
|
||||||
|
) {
|
||||||
|
return createEvent(ndk, {
|
||||||
|
content: "",
|
||||||
|
kind: 30001,
|
||||||
|
tags: [
|
||||||
|
["name", title],
|
||||||
|
["description", description ?? ""],
|
||||||
|
["d", generateRandomString()],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function deleteEvent(
|
||||||
|
ndk: NDK,
|
||||||
|
events: [["e", string] | ["a", `${number}:${string}:${string}`]],
|
||||||
|
reason?: string,
|
||||||
|
) {
|
||||||
|
return createEvent(ndk, {
|
||||||
|
kind: 5,
|
||||||
|
content: reason ?? "",
|
||||||
|
tags: events,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateEvent(
|
||||||
|
ndk: NDK,
|
||||||
|
event: {
|
||||||
|
content: string;
|
||||||
|
kind: number;
|
||||||
|
tags: string[][];
|
||||||
|
},
|
||||||
|
delegateSigner?: NDKPrivateKeySigner,
|
||||||
|
): Promise<NDKTag | undefined> {
|
||||||
|
let _value = event.content.trim();
|
||||||
|
|
||||||
|
// if this a relay URL, nrelay-encode it
|
||||||
|
if (_value.startsWith("wss://")) {
|
||||||
|
_value = nip19.nrelayEncode(_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decode = nip19.decode(_value);
|
||||||
|
|
||||||
|
switch (decode.type) {
|
||||||
|
case "naddr":
|
||||||
|
case "note":
|
||||||
|
case "nevent":
|
||||||
|
// We were able to decode something that looks like an event
|
||||||
|
// fetch it
|
||||||
|
const _event = await ndk.fetchEvent(_value);
|
||||||
|
if (_event) {
|
||||||
|
// we were able to fetch it, let's return it
|
||||||
|
return _event.tagReference();
|
||||||
|
} else {
|
||||||
|
// TODO: Generate a NDKTag based on the values
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
case "nrelay":
|
||||||
|
return ["r", decode.data as string];
|
||||||
|
case "npub":
|
||||||
|
return ["p", decode.data as string];
|
||||||
|
case "nprofile":
|
||||||
|
const d = ["p", decode.data.pubkey];
|
||||||
|
if (decode.data.relays && decode.data.relays[0])
|
||||||
|
d.push(decode.data.relays[0]);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("at catch", e);
|
||||||
|
const signer = delegateSigner ?? ndk.signer!;
|
||||||
|
console.log("Signer", signer);
|
||||||
|
const user = await signer.user();
|
||||||
|
console.log("User", user);
|
||||||
|
const newEvent = new NDKEvent(ndk, {
|
||||||
|
content: _value || "",
|
||||||
|
kind: 1,
|
||||||
|
tags: [...event.tags, ["client", "ordstr"]],
|
||||||
|
pubkey: user.hexpubkey,
|
||||||
|
} as NostrEvent);
|
||||||
|
console.log("Event to create", newEvent);
|
||||||
|
|
||||||
|
await newEvent.sign(signer);
|
||||||
|
|
||||||
|
await newEvent.publish();
|
||||||
|
|
||||||
|
return newEvent.tagReference();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function createEventOnList(
|
||||||
|
ndk: NDK,
|
||||||
|
event: {
|
||||||
|
content: string;
|
||||||
|
kind: number;
|
||||||
|
tags: string[][];
|
||||||
|
},
|
||||||
|
list: NDKList,
|
||||||
|
delegateSigner?: NDKPrivateKeySigner,
|
||||||
|
) {
|
||||||
|
const tag = await generateEvent(ndk, event, delegateSigner);
|
||||||
|
|
||||||
|
if (!tag) return;
|
||||||
|
await list.addItem(tag, undefined, false);
|
||||||
|
await list.sign();
|
||||||
|
await list.publish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateList(
|
||||||
|
ndk: NDK,
|
||||||
|
list: NostrEvent,
|
||||||
|
newTags: [string, string][],
|
||||||
|
) {
|
||||||
|
let tags = list.tags;
|
||||||
|
for (const [key, value] of newTags) {
|
||||||
|
const index = tags.findIndex(([tK]) => tK === key);
|
||||||
|
if (index !== -1) {
|
||||||
|
// Replace old
|
||||||
|
tags[index] = [key, value];
|
||||||
|
} else {
|
||||||
|
tags.push([key, value]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("updating list", tags);
|
||||||
|
return createEvent(ndk, {
|
||||||
|
...list,
|
||||||
|
kind: list.kind as number,
|
||||||
|
tags: tags.filter(([_, value]) => value !== undefined),
|
||||||
|
});
|
||||||
|
}
|
161
lib/actions/ephemeral.ts
Normal file
161
lib/actions/ephemeral.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import { type Event as NostrEvent } from "nostr-tools";
|
||||||
|
import NDK, {
|
||||||
|
NDKEvent,
|
||||||
|
NDKPrivateKeySigner,
|
||||||
|
type NDKTag,
|
||||||
|
type NDKFilter,
|
||||||
|
type NDKSigner,
|
||||||
|
type NDKUserProfile,
|
||||||
|
} from "@nostr-dev-kit/ndk";
|
||||||
|
import { getHashedKeyName } from "@/lib/nostr";
|
||||||
|
import { z } from "zod";
|
||||||
|
const SignerSchema = z.object({
|
||||||
|
key: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IFindEphemeralSignerLookups {
|
||||||
|
name?: string;
|
||||||
|
associatedEventNip19?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a named ephemeral signer from a self-DM.
|
||||||
|
*/
|
||||||
|
export async function findEphemeralSigner(
|
||||||
|
ndk: NDK,
|
||||||
|
mainSigner: NDKSigner,
|
||||||
|
opts: IFindEphemeralSignerLookups,
|
||||||
|
): Promise<NDKPrivateKeySigner | undefined> {
|
||||||
|
const mainUser = await mainSigner.user();
|
||||||
|
const filter: NDKFilter = { kinds: [2600 as number] };
|
||||||
|
|
||||||
|
if (opts.name) {
|
||||||
|
const hashedName = await getHashedKeyName(opts.name);
|
||||||
|
filter["#e"] = [hashedName];
|
||||||
|
} else if (opts.associatedEventNip19) {
|
||||||
|
const hashedEventReference = await getHashedKeyName(
|
||||||
|
opts.associatedEventNip19,
|
||||||
|
);
|
||||||
|
filter["#e"] = [hashedEventReference];
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = await ndk.fetchEvent(filter);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
const decryptEventFunction = async (event: NDKEvent) => {
|
||||||
|
await event.decrypt(await mainSigner.user());
|
||||||
|
|
||||||
|
const content = SignerSchema.parse(JSON.parse(event.content));
|
||||||
|
return new NDKPrivateKeySigner(content.key as string);
|
||||||
|
};
|
||||||
|
|
||||||
|
const promise = new Promise<NDKPrivateKeySigner>((resolve, reject) => {
|
||||||
|
let decryptionAttempts = 0;
|
||||||
|
try {
|
||||||
|
decryptionAttempts++;
|
||||||
|
resolve(decryptEventFunction(event));
|
||||||
|
} catch (e) {
|
||||||
|
if (decryptionAttempts > 5) {
|
||||||
|
console.error(
|
||||||
|
`Failed to decrypt ephemeral signer event after ${decryptionAttempts} attempts.`,
|
||||||
|
);
|
||||||
|
reject(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
decryptEventFunction(event);
|
||||||
|
}, 1000 * Math.random());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISaveOpts {
|
||||||
|
associatedEvent?: NDKEvent;
|
||||||
|
name?: string;
|
||||||
|
metadata?: object;
|
||||||
|
keyProfile?: NDKUserProfile;
|
||||||
|
mainSigner?: NDKSigner;
|
||||||
|
}
|
||||||
|
type EphemeralKeyEventContent = {
|
||||||
|
key: string;
|
||||||
|
event?: string;
|
||||||
|
version: string;
|
||||||
|
metadata?: object;
|
||||||
|
};
|
||||||
|
function generateContent(
|
||||||
|
targetSigner: NDKPrivateKeySigner,
|
||||||
|
opts: ISaveOpts = {},
|
||||||
|
) {
|
||||||
|
const content: EphemeralKeyEventContent = {
|
||||||
|
key: targetSigner.privateKey!,
|
||||||
|
version: "v1",
|
||||||
|
...opts.metadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts.associatedEvent) content.event = opts.associatedEvent.encode();
|
||||||
|
|
||||||
|
return JSON.stringify(content);
|
||||||
|
}
|
||||||
|
async function generateTags(mainSigner: NDKSigner, opts: ISaveOpts = {}) {
|
||||||
|
const mainUser = await mainSigner.user();
|
||||||
|
const tags = [
|
||||||
|
["p", mainUser.hexpubkey],
|
||||||
|
["client", "ordstr"],
|
||||||
|
];
|
||||||
|
|
||||||
|
if (opts.associatedEvent) {
|
||||||
|
// TODO: This is trivially reversable; better to encrypt it or hash it with the hexpubkey
|
||||||
|
const hashedEventReference = await getHashedKeyName(
|
||||||
|
opts.associatedEvent.encode(),
|
||||||
|
);
|
||||||
|
tags.push(["e", hashedEventReference]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.name) {
|
||||||
|
const hashedName = await getHashedKeyName(opts.name);
|
||||||
|
tags.push(["e", hashedName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveEphemeralSigner(
|
||||||
|
ndk: NDK,
|
||||||
|
targetSigner: NDKPrivateKeySigner,
|
||||||
|
opts: ISaveOpts = {},
|
||||||
|
) {
|
||||||
|
// Determine current user signer
|
||||||
|
const mainSigner = opts.mainSigner || ndk.signer;
|
||||||
|
|
||||||
|
if (!mainSigner) throw new Error("No main signer provided");
|
||||||
|
|
||||||
|
const mainUser = await mainSigner.user();
|
||||||
|
|
||||||
|
// Create 2600 kind which saves encrypted JSON of the ephemeral signer's private key, the associated list, and other metadata
|
||||||
|
const event = new NDKEvent(ndk, {
|
||||||
|
kind: 2600,
|
||||||
|
content: generateContent(targetSigner, opts),
|
||||||
|
tags: await generateTags(mainSigner, opts),
|
||||||
|
} as NostrEvent);
|
||||||
|
event.pubkey = mainUser.hexpubkey;
|
||||||
|
await event.encrypt(mainUser, mainSigner);
|
||||||
|
await event.publish();
|
||||||
|
|
||||||
|
// Update Ephemeral signers metadata
|
||||||
|
console.log("Checking keyProfile", opts.keyProfile);
|
||||||
|
const user = await targetSigner.user();
|
||||||
|
if (opts.keyProfile) {
|
||||||
|
const event = new NDKEvent(ndk, {
|
||||||
|
kind: 0,
|
||||||
|
content: JSON.stringify(opts.keyProfile),
|
||||||
|
tags: [] as NDKTag[],
|
||||||
|
} as NostrEvent);
|
||||||
|
event.pubkey = user.hexpubkey;
|
||||||
|
await event.sign(targetSigner);
|
||||||
|
await event.publish();
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
174
lib/actions/zap.ts
Normal file
174
lib/actions/zap.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import NDK, {
|
||||||
|
NDKEvent,
|
||||||
|
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";
|
||||||
|
|
||||||
|
const fetchWithZod = createZodFetcher();
|
||||||
|
const ZapEndpointResponseSchema = z.object({
|
||||||
|
nostrPubkey: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function sendZap(
|
||||||
|
ndk: NDK,
|
||||||
|
amount: number,
|
||||||
|
_event: NostrEvent,
|
||||||
|
comment?: string,
|
||||||
|
) {
|
||||||
|
console.log("sendzap called", amount);
|
||||||
|
const event = await new NDKEvent(ndk, _event);
|
||||||
|
const pr = await event.zap(amount * 1000, comment);
|
||||||
|
if (!pr) {
|
||||||
|
console.log("No PR");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("PR", pr);
|
||||||
|
const webln = await requestProvider();
|
||||||
|
return await webln.sendPayment(pr);
|
||||||
|
}
|
||||||
|
export async function checkPayment(
|
||||||
|
ndk: NDK,
|
||||||
|
tagId: string,
|
||||||
|
pubkey: string,
|
||||||
|
event: NostrEvent,
|
||||||
|
) {
|
||||||
|
console.log("Running check payment");
|
||||||
|
const paymentEvent = await ndk.fetchEvent({
|
||||||
|
kinds: [9735],
|
||||||
|
["#a"]: [tagId],
|
||||||
|
["#p"]: [pubkey],
|
||||||
|
});
|
||||||
|
console.log("paymentEvent", paymentEvent);
|
||||||
|
if (!paymentEvent) return;
|
||||||
|
const invoice = zapInvoiceFromEvent(paymentEvent);
|
||||||
|
console.log("invoice", invoice);
|
||||||
|
if (!invoice) {
|
||||||
|
console.log("No invoice");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zappedUser = ndk.getUser({
|
||||||
|
npub: invoice.zappee,
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
) {
|
||||||
|
const SECONDS_IN_MONTH = 2_628_000;
|
||||||
|
const paymentEvents = await ndk.fetchEvents({
|
||||||
|
kinds: [9735],
|
||||||
|
["#a"]: [tagId],
|
||||||
|
since: unixTimeNowInSeconds() - SECONDS_IN_MONTH,
|
||||||
|
});
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const paymentInvoice of paymentInvoices) {
|
||||||
|
if (
|
||||||
|
!paymentInvoice ||
|
||||||
|
validUsers.find(([pubkey]) => pubkey === paymentInvoice.zappee)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const isValid = await checkPayment(
|
||||||
|
ndk,
|
||||||
|
tagId,
|
||||||
|
paymentInvoice.zappee,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
console.log("Is valid?", isValid);
|
||||||
|
if (isValid) {
|
||||||
|
validUsers.push([
|
||||||
|
paymentInvoice.zappee,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
(unixTimeNowInSeconds() + SECONDS_IN_MONTH).toString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add self
|
||||||
|
console.log("Adding self");
|
||||||
|
const selfIndex = validUsers.findIndex(([vu]) => vu === event.pubkey);
|
||||||
|
if (selfIndex !== -1) {
|
||||||
|
console.log("Already there");
|
||||||
|
|
||||||
|
validUsers[selfIndex] = [
|
||||||
|
event.pubkey,
|
||||||
|
"",
|
||||||
|
"self",
|
||||||
|
(unixTimeNowInSeconds() + SECONDS_IN_MONTH).toString(),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
validUsers.push([
|
||||||
|
event.pubkey,
|
||||||
|
"",
|
||||||
|
"self",
|
||||||
|
(unixTimeNowInSeconds() + SECONDS_IN_MONTH).toString(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
console.log("Valid users", validUsers);
|
||||||
|
return createEvent(ndk, {
|
||||||
|
...event,
|
||||||
|
kind: event.kind as number,
|
||||||
|
tags: [
|
||||||
|
...event.tags.filter(([key]) => key !== "p"),
|
||||||
|
...validUsers.map((user) => ["p", ...user]),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
150
lib/nostr/index.ts
Normal file
150
lib/nostr/index.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
// import "websocket-polyfill";
|
||||||
|
import sha256 from "crypto-js/sha256";
|
||||||
|
import Hex from "crypto-js/enc-hex";
|
||||||
|
import { getTagValues } from "./utils";
|
||||||
|
import { sha256 as SHA256 } from "@noble/hashes/sha256";
|
||||||
|
import { bytesToHex } from "@noble/hashes/utils";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
export enum Kind {
|
||||||
|
Metadata = 0,
|
||||||
|
Text = 1,
|
||||||
|
RecommendRelay = 2,
|
||||||
|
Contacts = 3,
|
||||||
|
EncryptedDirectMessage = 4,
|
||||||
|
EventDeletion = 5,
|
||||||
|
Reaction = 7,
|
||||||
|
ChannelCreation = 40,
|
||||||
|
ChannelMetadata = 41,
|
||||||
|
ChannelMessage = 42,
|
||||||
|
ChannelHideMessage = 43,
|
||||||
|
ChannelMuteUser = 44,
|
||||||
|
ProfileList = 30000,
|
||||||
|
GenericList = 30001,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Event = {
|
||||||
|
id?: string;
|
||||||
|
sig?: string;
|
||||||
|
kind: Kind;
|
||||||
|
tags: string[][];
|
||||||
|
pubkey: string;
|
||||||
|
content: string;
|
||||||
|
created_at: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getHashedKeyName(name: string) {
|
||||||
|
let eventHash = SHA256(name);
|
||||||
|
const version = bytesToHex(eventHash);
|
||||||
|
const alt = sha256(name).toString(Hex);
|
||||||
|
console.log("VERSION", version, "vs", alt);
|
||||||
|
console.log("EQUAL?", version === alt);
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace NostrService {
|
||||||
|
export function createEvent(
|
||||||
|
kind: number,
|
||||||
|
publicKey: string,
|
||||||
|
content: string,
|
||||||
|
tags: string[][],
|
||||||
|
) {
|
||||||
|
const event: Event = {
|
||||||
|
kind: kind,
|
||||||
|
pubkey: publicKey,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
content: content,
|
||||||
|
tags: tags,
|
||||||
|
};
|
||||||
|
event.id = getEventHash(event);
|
||||||
|
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeEvent(evt: Event) {
|
||||||
|
return JSON.stringify([
|
||||||
|
0,
|
||||||
|
evt.pubkey,
|
||||||
|
evt.created_at,
|
||||||
|
evt.kind,
|
||||||
|
evt.tags,
|
||||||
|
evt.content,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventHash(event: Event): string {
|
||||||
|
return sha256(serializeEvent(event)).toString(Hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signEvent(event: Event) {
|
||||||
|
try {
|
||||||
|
return await window.nostr?.signEvent(event);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("signing event failed");
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterBlogEvents(eventArray: Event[]) {
|
||||||
|
const filteredEvents = eventArray.filter((e1: Event, index: number) => {
|
||||||
|
if (e1.content === "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const title = getTagValues("title", e1.tags);
|
||||||
|
if (!title || title === "") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// return eventArray.findIndex((e2: Event) => e2.id === e1.id) === index;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return filteredEvents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function encryptMessage(message: string, password: string) {
|
||||||
|
try {
|
||||||
|
const buffer = create32ByteBuffer(password);
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const cipher = crypto.createCipheriv("aes-256-cbc", buffer, iv);
|
||||||
|
|
||||||
|
const encrypted = Buffer.concat([
|
||||||
|
cipher.update(message, "utf-8"),
|
||||||
|
cipher.final(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return encrypted.toString("base64") + "?iv=" + iv.toString("base64");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Function to decrypt a hashed message using a passphrase
|
||||||
|
export function decryptMessage(encryptedMessage: string, password: string) {
|
||||||
|
try {
|
||||||
|
const buffer = create32ByteBuffer(password);
|
||||||
|
// Extract IV from the received message
|
||||||
|
const ivBase64 = encryptedMessage.split("?iv=")[1];
|
||||||
|
if (!ivBase64) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iv = Buffer.from(ivBase64, "base64");
|
||||||
|
|
||||||
|
const encryptedText = Buffer.from(encryptedMessage, "base64");
|
||||||
|
|
||||||
|
const decipher = crypto.createDecipheriv("aes-256-cbc", buffer, iv);
|
||||||
|
|
||||||
|
const decrypted = decipher.update(encryptedText);
|
||||||
|
return Buffer.concat([decrypted, decipher.final()]).toString();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function create32ByteBuffer(inputString: string) {
|
||||||
|
const hash = crypto.createHash("sha256").update(inputString).digest("hex");
|
||||||
|
const buffer = Buffer.from(hash, "hex");
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateRandomString() {
|
||||||
|
return crypto.randomBytes(32).toString("hex");
|
||||||
|
}
|
@ -101,3 +101,6 @@ export function validateUrl(value: string) {
|
|||||||
export function btcToSats(amount: number) {
|
export function btcToSats(amount: number) {
|
||||||
return parseInt((amount * 100_000_000).toFixed());
|
return parseInt((amount * 100_000_000).toFixed());
|
||||||
}
|
}
|
||||||
|
export function satsToBtc(amount: number) {
|
||||||
|
return amount / 100_000_000;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user