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 useRouteChange from "@/lib/hooks/useRouteChange";
|
||||
import { NDKProvider } from "./ndk";
|
||||
import SignerProvider from "./signer";
|
||||
import { RELAYS } from "@/constants";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
@ -16,13 +17,12 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
||||
useRouteChange(handleRouteChange);
|
||||
return (
|
||||
<>
|
||||
<NDKProvider
|
||||
relayUrls={RELAYS}
|
||||
>
|
||||
|
||||
<Toaster richColors className="dark:hidden" />
|
||||
<Toaster theme="dark" className="hidden dark:block" />
|
||||
<ModalProvider>{children}</ModalProvider>
|
||||
<NDKProvider relayUrls={RELAYS}>
|
||||
<SignerProvider>
|
||||
<Toaster richColors className="dark:hidden" />
|
||||
<Toaster theme="dark" className="hidden dark:block" />
|
||||
<ModalProvider>{children}</ModalProvider>
|
||||
</SignerProvider>
|
||||
</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,
|
||||
DefaultValues,
|
||||
Path,
|
||||
PathValue,
|
||||
} from "react-hook-form";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Template from "./Template";
|
||||
@ -37,6 +38,7 @@ type FieldOptions =
|
||||
| "toggle"
|
||||
| "horizontal-tabs"
|
||||
| "input"
|
||||
| "number"
|
||||
| "text-area"
|
||||
| "custom";
|
||||
|
||||
@ -44,11 +46,13 @@ type DefaultFieldType<TSchema> = {
|
||||
label: string;
|
||||
slug: keyof z.infer<z.Schema<TSchema>> & string;
|
||||
placeholder?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
lines?: number;
|
||||
styles?: string;
|
||||
value?: string | number | boolean;
|
||||
custom?: ReactNode;
|
||||
condition?: keyof z.infer<z.Schema<TSchema>> & string;
|
||||
options?: { label: string; value: string; icon?: ReactNode }[];
|
||||
};
|
||||
type FieldType<TSchema> = DefaultFieldType<TSchema> &
|
||||
@ -91,10 +95,11 @@ export default function FormModal<TSchema extends FieldValues>({
|
||||
defaultValues: defaultValues as DefaultValues<TSchema>,
|
||||
mode: "onChange",
|
||||
});
|
||||
const { watch, setValue } = form;
|
||||
return (
|
||||
<Template title={title} className="md:max-w-[400px]">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
{fields.map(
|
||||
({
|
||||
label,
|
||||
@ -102,69 +107,171 @@ export default function FormModal<TSchema extends FieldValues>({
|
||||
type,
|
||||
placeholder,
|
||||
description,
|
||||
subtitle,
|
||||
condition,
|
||||
...fieldProps
|
||||
}) => (
|
||||
<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}</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}
|
||||
}) => {
|
||||
if (!condition) {
|
||||
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}</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}
|
||||
/>
|
||||
</FormControl>
|
||||
) : (
|
||||
<FormControl>
|
||||
<Input placeholder={placeholder} {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
{!!description && (
|
||||
<FormDescription>{description}</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</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>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
<Input placeholder={placeholder} {...field} />
|
||||
</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>
|
||||
) : (
|
||||
<FormControl>
|
||||
<Input placeholder={placeholder} {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
{!!description && (
|
||||
<FormDescription>{description}</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
) : 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}>
|
||||
{cta.text}
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
import { RiSubtractFill, RiAddFill } from "react-icons/ri";
|
||||
import { formatCount } from "@/lib/utils";
|
||||
import LoginModal from "./Login";
|
||||
import CreateList from "./CreateList";
|
||||
|
||||
export default function NewEventModal() {
|
||||
const modal = useModal();
|
||||
@ -37,7 +38,12 @@ export default function NewEventModal() {
|
||||
<HiNewspaper className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Button className="w-full gap-x-1">
|
||||
<Button
|
||||
onClick={() => {
|
||||
modal?.swap(<CreateList />);
|
||||
}}
|
||||
className="w-full gap-x-1"
|
||||
>
|
||||
<span>Content List</span>
|
||||
<HiBookmarkSquare className="h-4 w-4" />
|
||||
</Button>
|
||||
|
@ -18,7 +18,7 @@ export default function Template({ title, children, className }: ModalProps) {
|
||||
return (
|
||||
<div
|
||||
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,
|
||||
)}
|
||||
>
|
||||
|
@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
import * as React from "react";
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Avatar = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||
@ -13,12 +13,12 @@ const Avatar = React.forwardRef<
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||
));
|
||||
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||
|
||||
const AvatarImage = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||
@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef<
|
||||
className={cn("aspect-square h-full w-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||
));
|
||||
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||
|
||||
const AvatarFallback = React.forwardRef<
|
||||
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||
@ -39,12 +39,12 @@ const AvatarFallback = React.forwardRef<
|
||||
<AvatarPrimitive.Fallback
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
className
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted uppercase",
|
||||
className,
|
||||
)}
|
||||
{...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) {
|
||||
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