Skip to content

Commit 8983d77

Browse files
authored
Merge pull request #494 from hypercerts-org/dev
chore: new release
2 parents dd6ee63 + 8cee6c7 commit 8983d77

14 files changed

+747
-203
lines changed

components/allowlist/create-allowlist-dialog.tsx

+21
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,15 @@ const defaultValues = [
3535

3636
export default function Component({
3737
setAllowlistEntries,
38+
setAllowlistURL,
39+
allowlistURL,
3840
setOpen,
3941
open,
4042
initialValues,
4143
}: {
4244
setAllowlistEntries: (allowlistEntries: AllowlistEntry[]) => void;
45+
setAllowlistURL: (allowlistURL: string) => void;
46+
allowlistURL: string | undefined;
4347
setOpen: (open: boolean) => void;
4448
initialValues?: AllowListItem[];
4549
open: boolean;
@@ -55,6 +59,16 @@ export default function Component({
5559
initialValues?.length ? initialValues : defaultValues,
5660
);
5761

62+
useEffect(() => {
63+
if (open && !allowList[0].address && !allowList[0].percentage) {
64+
if (initialValues && initialValues.length > 0) {
65+
setAllowList(initialValues);
66+
} else {
67+
setAllowList(defaultValues);
68+
}
69+
}
70+
}, [open]);
71+
5872
useEffect(() => {
5973
if (validateAllowlistResponse?.success) {
6074
(async () => {
@@ -141,6 +155,7 @@ export default function Component({
141155
throw new Error("Allow list is empty");
142156
}
143157
validateAllowlist({ allowList: parsedAllowList, totalUnits });
158+
setAllowlistURL("");
144159
} catch (e) {
145160
if (errorHasMessage(e)) {
146161
toast({
@@ -264,6 +279,12 @@ export default function Component({
264279
</Button>
265280
</div>
266281
</div>
282+
{allowlistURL && (
283+
<p className="text-sm text-red-600">
284+
If you edit an original allowlist imported via URL, the original
285+
allowlist will be deleted.
286+
</p>
287+
)}
267288
<CreateAllowListErrorMessage />
268289
<div className="flex gap-2 justify-evenly w-full">
269290
<Button

components/global/extra-content.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ interface ExtraContentProps {
1313
receipt?: TransactionReceipt;
1414
}
1515

16+
// TODO: not really reusable for safe. breaks when minting hypercert from safe.
17+
// We should make this reusable for all strategies.
1618
export function ExtraContent({
1719
message = "Your hypercert has been minted successfully!",
1820
hypercertId,

components/hypercert/hypercert-minting-form/form-steps.tsx

+180-15
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ import {
1717
ArrowRightIcon,
1818
CalendarIcon,
1919
Loader2,
20+
LoaderCircle,
2021
Trash2Icon,
2122
X,
2223
} from "lucide-react";
23-
import { RefObject, useMemo, useState } from "react";
24+
import { RefObject, useEffect, useMemo, useState } from "react";
2425
import rehypeSanitize from "rehype-sanitize";
2526

2627
import CreateAllowlistDialog from "@/components/allowlist/create-allowlist-dialog";
@@ -74,7 +75,11 @@ import Link from "next/link";
7475
import { UseFormReturn } from "react-hook-form";
7576
import { useAccount, useChainId } from "wagmi";
7677
import { ImageUploader, readAsBase64 } from "@/components/image-uploader";
77-
78+
import { useValidateAllowlist } from "@/hypercerts/hooks/useCreateAllowLists";
79+
import Papa from "papaparse";
80+
import { getAddress, parseUnits } from "viem";
81+
import { errorHasMessage } from "@/lib/errorHasMessage";
82+
import { errorToast } from "@/lib/errorToast";
7883
// import Image from "next/image";
7984

8085
interface FormStepsProps {
@@ -418,6 +423,50 @@ const DatesAndPeople = ({ form }: FormStepsProps) => {
418423
);
419424
};
420425

426+
export const parseAllowList = async (data: Response | string) => {
427+
let allowList: AllowlistEntry[];
428+
try {
429+
const text = typeof data === "string" ? data : await data.text();
430+
const parsedData = Papa.parse<AllowlistEntry>(text, {
431+
header: true,
432+
skipEmptyLines: true,
433+
});
434+
435+
const validEntries = parsedData.data.filter(
436+
(entry) => entry.address && entry.units,
437+
);
438+
439+
// Calculate total units
440+
const total = validEntries.reduce(
441+
(sum, entry) => sum + BigInt(entry.units),
442+
BigInt(0),
443+
);
444+
445+
allowList = validEntries.map((entry) => {
446+
const address = getAddress(entry.address);
447+
const originalUnits = BigInt(entry.units);
448+
// Scale units proportionally to DEFAULT_NUM_UNITS
449+
const scaledUnits =
450+
total > 0 ? (originalUnits * DEFAULT_NUM_UNITS) / total : BigInt(0);
451+
452+
return {
453+
address: address,
454+
units: scaledUnits,
455+
};
456+
});
457+
458+
return allowList;
459+
} catch (e) {
460+
if (errorHasMessage(e)) {
461+
errorToast(e.message);
462+
throw new Error(e.message);
463+
} else {
464+
errorToast("Failed to parse allowlist.");
465+
throw new Error("Failed to parse allowlist.");
466+
}
467+
}
468+
};
469+
421470
const calculatePercentageBigInt = (
422471
units: bigint,
423472
total: bigint = DEFAULT_NUM_UNITS,
@@ -433,22 +482,46 @@ const AdvancedAndSubmit = ({ form, isBlueprint }: FormStepsProps) => {
433482
setOpen,
434483
setTitle,
435484
} = useStepProcessDialogContext();
485+
const {
486+
mutate: validateAllowlist,
487+
data: validateAllowlistResponse,
488+
isPending: isPendingValidateAllowlist,
489+
error: createAllowListError,
490+
} = useValidateAllowlist();
436491
const [isUploading, setIsUploading] = useState(false);
437492
const [selectedFile, setSelectedFile] = useState<File | null>(null);
438493
const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
439494
const [createDialogOpen, setCreateDialogOpen] = useState(false);
495+
const setAllowlistURL = (allowlistURL: string) => {
496+
form.setValue("allowlistURL", allowlistURL);
497+
};
440498
const setAllowlistEntries = (allowlistEntries: AllowlistEntry[]) => {
441499
form.setValue("allowlistEntries", allowlistEntries);
442500
};
501+
443502
const allowlistEntries = form.watch("allowlistEntries");
444503

445-
const errorToast = (message: string | undefined) => {
446-
toast({
447-
title: message,
448-
variant: "destructive",
449-
duration: 2000,
450-
});
451-
};
504+
useEffect(() => {
505+
if (createAllowListError) {
506+
toast({
507+
title: "Error",
508+
description: createAllowListError.message,
509+
variant: "destructive",
510+
});
511+
}
512+
}, [createAllowListError]);
513+
514+
useEffect(() => {
515+
if (validateAllowlistResponse?.success) {
516+
const bigintUnits = validateAllowlistResponse.values.map(
517+
(entry: AllowlistEntry) => ({
518+
...entry,
519+
units: BigInt(entry.units),
520+
}),
521+
);
522+
form.setValue("allowlistEntries", bigintUnits);
523+
}
524+
}, [validateAllowlistResponse]);
452525

453526
async function validateFile(file: File) {
454527
if (file.size > MAX_FILE_SIZE) {
@@ -505,6 +578,61 @@ const AdvancedAndSubmit = ({ form, isBlueprint }: FormStepsProps) => {
505578
}
506579
};
507580

581+
const fetchAllowlist = async (value: string) => {
582+
let data: Response;
583+
const url = value.startsWith("ipfs://")
584+
? `https://ipfs.io/ipfs/${value.replace("ipfs://", "")}`
585+
: value.startsWith("https://")
586+
? value
587+
: null;
588+
589+
if (!url) {
590+
errorToast("Invalid URL. URL must start with 'https://' or 'ipfs://'");
591+
throw new Error(
592+
"Invalid URL. URL must start with 'https://' or 'ipfs://'",
593+
);
594+
}
595+
data = await fetch(url);
596+
597+
const contentType = data.headers.get("content-type");
598+
599+
if (
600+
contentType?.includes("text/csv") ||
601+
contentType?.includes("text/plain") ||
602+
value.endsWith(".csv")
603+
) {
604+
const allowList = await parseAllowList(data);
605+
if (!allowList || allowList.length === 0) return;
606+
607+
const totalUnits = DEFAULT_NUM_UNITS;
608+
609+
// validateAllowlist
610+
try {
611+
validateAllowlist({
612+
allowList,
613+
totalUnits,
614+
});
615+
form.setValue("allowlistEntries", allowList);
616+
} catch (e) {
617+
if (errorHasMessage(e)) {
618+
toast({
619+
title: "Error",
620+
description: e.message,
621+
variant: "destructive",
622+
});
623+
} else {
624+
toast({
625+
title: "Error",
626+
description: "Failed to upload allow list",
627+
});
628+
}
629+
}
630+
} else {
631+
errorToast("Invalid file type.");
632+
throw new Error("Invalid file type.");
633+
}
634+
};
635+
508636
return (
509637
<section className="space-y-8">
510638
{isBlueprint && (
@@ -549,7 +677,7 @@ const AdvancedAndSubmit = ({ form, isBlueprint }: FormStepsProps) => {
549677
name="allowlistURL"
550678
render={({ field }) => (
551679
<FormItem>
552-
<div className="flex items-center gap-2">
680+
<div className="flex flex-row items-center gap-2">
553681
<FormLabel>Allowlist (optional)</FormLabel>
554682
<TooltipInfo
555683
tooltipText="Allowlists determine the number of units each address is allowed to mint. You can create a new allowlist, or prefill from an existing, already uploaded file."
@@ -559,8 +687,8 @@ const AdvancedAndSubmit = ({ form, isBlueprint }: FormStepsProps) => {
559687
<FormControl>
560688
<Input
561689
{...field}
562-
value={field.value}
563690
placeholder="https:// | ipfs://"
691+
disabled={!!allowlistEntries}
564692
/>
565693
</FormControl>
566694
<FormMessage />
@@ -571,21 +699,58 @@ const AdvancedAndSubmit = ({ form, isBlueprint }: FormStepsProps) => {
571699
<div className="flex text-xs space-x-2 w-full justify-end">
572700
<Button
573701
type="button"
574-
disabled={!!field.value}
702+
variant="outline"
703+
disabled={
704+
(form.getValues("allowlistEntries")?.length ?? 0) > 0 ||
705+
!field.value
706+
}
707+
onClick={() => fetchAllowlist(field?.value as string)}
708+
>
709+
Import from URL
710+
</Button>
711+
<Button
712+
type="button"
713+
disabled={isPendingValidateAllowlist}
575714
variant="outline"
576715
onClick={() => setCreateDialogOpen(true)}
577716
>
578-
{allowlistEntries ? "Edit allowlist" : "Create allowlist"}
717+
{isPendingValidateAllowlist ? (
718+
<>
719+
<LoaderCircle className="h-4 w-4 animate-spin mr-2" />
720+
Loading...
721+
</>
722+
) : allowlistEntries || field.value ? (
723+
"Edit allowlist"
724+
) : (
725+
"New allowlist"
726+
)}
727+
</Button>
728+
729+
<Button
730+
type="button"
731+
disabled={
732+
(!allowlistEntries && !field.value) ||
733+
isPendingValidateAllowlist
734+
}
735+
onClick={() => {
736+
form.setValue("allowlistEntries", undefined);
737+
form.setValue("allowlistURL", "");
738+
}}
739+
>
740+
<Trash2Icon className="w-4 h-4 mr-2" />
741+
Delete
579742
</Button>
580743

581744
<CreateAllowlistDialog
582745
setAllowlistEntries={setAllowlistEntries}
746+
setAllowlistURL={setAllowlistURL}
747+
allowlistURL={field?.value}
583748
open={createDialogOpen}
584749
setOpen={setCreateDialogOpen}
585750
initialValues={allowlistEntries?.map((entry) => ({
586751
address: entry.address,
587752
percentage: calculatePercentageBigInt(
588-
entry.units,
753+
BigInt(entry.units),
589754
).toString(),
590755
}))}
591756
/>
@@ -610,7 +775,7 @@ const AdvancedAndSubmit = ({ form, isBlueprint }: FormStepsProps) => {
610775
</TableCell>
611776
<TableCell>
612777
{formatNumber(
613-
calculatePercentageBigInt(entry.units),
778+
calculatePercentageBigInt(BigInt(entry.units)),
614779
)}
615780
%
616781
</TableCell>

0 commit comments

Comments
 (0)