Skip to content

Safe support to claim a hypercert #496

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 21 additions & 90 deletions components/profile/unclaimed-hypercert-batchClaim-button.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
"use client";

import { AllowListRecord } from "@/allowlists/actions/getAllowListRecordsForAddressByClaimed";
import { revalidatePathServerAction } from "@/app/actions/revalidatePathServerAction";
import { useHypercertClient } from "@/hooks/use-hypercert-client";
import { ChainFactory } from "@/lib/chainFactory";
import { errorToast } from "@/lib/errorToast";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { ByteArray, getAddress, Hex } from "viem";
import { waitForTransactionReceipt } from "viem/actions";
import { useAccount, useSwitchChain, useWalletClient } from "wagmi";
import { createExtraContent } from "../global/extra-content";
import { useStepProcessDialogContext } from "../global/step-process-dialog";
import { Button } from "../ui/button";
import { useAccount, useSwitchChain } from "wagmi";
import { useState } from "react";
import { getAddress, Hex, ByteArray } from "viem";
import { errorToast } from "@/lib/errorToast";
import { ChainFactory } from "@/lib/chainFactory";
import { useClaimHypercertStrategy } from "@/hypercerts/hooks/useClaimHypercertStrategy";
import { useAccountStore } from "@/lib/account-store";

interface TransformedClaimData {
hypercertTokenIds: bigint[];
Expand All @@ -39,104 +35,39 @@ export default function UnclaimedHypercertBatchClaimButton({
allowListRecords: AllowListRecord[];
selectedChainId: number | null;
}) {
const router = useRouter();
const { client } = useHypercertClient();
const { data: walletClient } = useWalletClient();
const account = useAccount();
const [isLoading, setIsLoading] = useState(false);
const { setDialogStep, setSteps, setOpen, setTitle, setExtraContent } =
useStepProcessDialogContext();
const { switchChain } = useSwitchChain();
const getStrategy = useClaimHypercertStrategy();
const { selectedAccount } = useAccountStore();

const selectedChain = selectedChainId
? ChainFactory.getChain(selectedChainId)
: null;

const refreshData = async (address: string) => {
const hypercertIds = allowListRecords.map((record) => record.hypercert_id);

const hypercertViewInvalidationPaths = hypercertIds.map((id) => {
return `/hypercerts/${id}`;
});

await revalidatePathServerAction([
`/profile/${address}`,
`/profile/${address}?tab`,
`/profile/${address}?tab=hypercerts-claimable`,
`/profile/${address}?tab=hypercerts-owned`,
...hypercertViewInvalidationPaths,
]).then(async () => {
setTimeout(() => {
// refresh after 5 seconds
router.refresh();

// push to the profile page with the hypercerts-claimable tab
// because revalidatePath will revalidate on the next page visit.
router.push(`/profile/${address}?tab=hypercerts-claimable`);
}, 5000);
});
};

const claimHypercert = async () => {
setIsLoading(true);
setOpen(true);
setSteps([
{ id: "preparing", description: "Preparing to claim fractions..." },
{ id: "claiming", description: "Claiming fractions on-chain..." },
{ id: "confirming", description: "Waiting for on-chain confirmation" },
{ id: "done", description: "Claiming complete!" },
]);
setTitle("Claim fractions from Allowlist");
if (!client) {
throw new Error("No client found");
}
if (!walletClient) {
throw new Error("No wallet client found");
}
if (!account) {
throw new Error("No address found");
}

const claimData = transformAllowListRecords(allowListRecords);
await setDialogStep("preparing, active");
try {
await setDialogStep("claiming", "active");
const tx = await client.batchClaimFractionsFromAllowlists(claimData);

if (!tx) {
await setDialogStep("claiming", "error");
throw new Error("Failed to claim fractions");
}

await setDialogStep("confirming", "active");
const receipt = await waitForTransactionReceipt(walletClient, {
hash: tx,
});

if (receipt.status == "success") {
await setDialogStep("done", "completed");
const extraContent = createExtraContent({
receipt,
chain: account?.chain!,
});
setExtraContent(extraContent);
refreshData(getAddress(account.address!));
} else if (receipt.status == "reverted") {
await setDialogStep("confirming", "error", "Transaction reverted");
}
const claimData = transformAllowListRecords(allowListRecords);
const params = claimData.hypercertTokenIds.map((tokenId, index) => ({
tokenId,
units: claimData.units[index],
proof: claimData.proofs[index] as `0x${string}`[],
}));
await getStrategy(params).execute(params);
} catch (error) {
console.error("Claim error:", error);
await setDialogStep("claiming", "error", "Transaction failed");
console.error(error);
} finally {
setIsLoading(false);
}
};

const activeAddress = selectedAccount?.address || account.address;
const isBatchClaimDisabled =
isLoading ||
!allowListRecords.length ||
!account ||
!client ||
account.address !== getAddress(allowListRecords[0].user_address as string);
!activeAddress ||
activeAddress !== getAddress(allowListRecords[0].user_address as string);

return (
<>
Expand Down
134 changes: 30 additions & 104 deletions components/profile/unclaimed-hypercert-claim-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@

import { AllowListRecord } from "@/allowlists/actions/getAllowListRecordsForAddressByClaimed";
import { Button } from "../ui/button";
import { useHypercertClient } from "@/hooks/use-hypercert-client";
import { waitForTransactionReceipt } from "viem/actions";
import { useAccount, useSwitchChain, useWalletClient } from "wagmi";
import { useRouter } from "next/navigation";
import { useAccount, useSwitchChain } from "wagmi";
import { Row } from "@tanstack/react-table";
import { useStepProcessDialogContext } from "../global/step-process-dialog";
import { createExtraContent } from "../global/extra-content";
import { revalidatePathServerAction } from "@/app/actions/revalidatePathServerAction";
import { useState } from "react";
import { getAddress } from "viem";
import { useAccountStore } from "@/lib/account-store";
import { useClaimHypercert } from "@/hypercerts/hooks/useClaimHypercert";

interface UnclaimedHypercertClaimButtonProps {
allowListRecord: Row<AllowListRecord>;
Expand All @@ -20,102 +15,33 @@ interface UnclaimedHypercertClaimButtonProps {
export default function UnclaimedHypercertClaimButton({
allowListRecord,
}: UnclaimedHypercertClaimButtonProps) {
const { client } = useHypercertClient();
const { data: walletClient } = useWalletClient();
const account = useAccount();
const { refresh } = useRouter();
const { address, chain: currentChain } = useAccount();
const { selectedAccount } = useAccountStore();
const [isLoading, setIsLoading] = useState(false);
const { setDialogStep, setSteps, setOpen, setTitle, setExtraContent } =
useStepProcessDialogContext();
const { switchChain } = useSwitchChain();
const router = useRouter();

const selectedHypercert = allowListRecord.original;
const hypercertChainId = selectedHypercert?.hypercert_id?.split("-")[0];
const activeAddress = selectedAccount?.address || (address as `0x${string}`);
const { mutateAsync: claimHypercert } = useClaimHypercert();

const refreshData = async (address: string) => {
await revalidatePathServerAction([
`/profile/${address}`,
`/profile/${address}?tab`,
`/profile/${address}?tab=hypercerts-claimable`,
`/profile/${address}?tab=hypercerts-owned`,
`/hypercerts/${selectedHypercert?.hypercert_id}`,
]).then(() => {
setTimeout(() => {
// refresh after 5 seconds
router.refresh();
// push to the profile page with the hypercerts-claimable tab
// because revalidatePath will revalidate on the next page visit.
router.push(`/profile/${address}?tab=hypercerts-claimable`);
}, 5000);
});
};

const claimHypercert = async () => {
const handleClaim = async () => {
setIsLoading(true);
setOpen(true);
setSteps([
{ id: "preparing", description: "Preparing to claim fraction..." },
{ id: "claiming", description: "Claiming fraction on-chain..." },
{ id: "confirming", description: "Waiting for on-chain confirmation" },
{ id: "route", description: "Creating your new fraction's link..." },
{ id: "done", description: "Claiming complete!" },
]);

setTitle("Claim fraction from Allowlist");
if (!client) {
throw new Error("No client found");
}

if (!walletClient) {
throw new Error("No wallet client found");
}

if (!account) {
throw new Error("No address found");
}

if (
!selectedHypercert?.units ||
!selectedHypercert?.proof ||
!selectedHypercert?.token_id
) {
throw new Error("Invalid allow list record");
}
await setDialogStep("preparing, active");

try {
await setDialogStep("claiming", "active");
const tx = await client.mintClaimFractionFromAllowlist(
BigInt(selectedHypercert?.token_id),
BigInt(selectedHypercert?.units),
selectedHypercert?.proof as `0x${string}`[],
undefined,
);

if (!tx) {
await setDialogStep("claiming", "error");
throw new Error("Failed to claim fraction");
if (
!selectedHypercert.token_id ||
!selectedHypercert.units ||
!selectedHypercert.proof
) {
throw new Error("Invalid allow list record");
}

await setDialogStep("confirming", "active");
const receipt = await waitForTransactionReceipt(walletClient, {
hash: tx,
});

if (receipt.status == "success") {
await setDialogStep("route", "active");
const extraContent = createExtraContent({
receipt: receipt,
hypercertId: selectedHypercert?.hypercert_id!,
chain: account.chain!,
});
setExtraContent(extraContent);
await setDialogStep("done", "completed");
await refreshData(getAddress(account.address!));
} else if (receipt.status == "reverted") {
await setDialogStep("confirming", "error", "Transaction reverted");
}
await claimHypercert([
{
tokenId: BigInt(selectedHypercert.token_id),
units: BigInt(selectedHypercert.units),
proof: selectedHypercert.proof as `0x${string}`[],
},
]);
} catch (error) {
console.error(error);
} finally {
Expand All @@ -126,25 +52,25 @@ export default function UnclaimedHypercertClaimButton({
return (
<Button
variant={
hypercertChainId === account.chainId?.toString() ? "default" : "outline"
hypercertChainId === currentChain?.id?.toString()
? "default"
: "outline"
}
size={"sm"}
onClick={() => {
if (hypercertChainId === account.chainId?.toString()) {
claimHypercert();
if (hypercertChainId === currentChain?.id?.toString()) {
handleClaim();
} else {
switchChain({
chainId: Number(hypercertChainId),
});
}
}}
disabled={
selectedHypercert?.user_address !== account.address || isLoading
}
disabled={selectedHypercert?.user_address !== activeAddress || isLoading}
>
{hypercertChainId === account.chainId?.toString()
? "Claim"
: `Switch chain`}
{hypercertChainId === activeAddress && !currentChain?.id?.toString()
? "Switch chain"
: "Claim"}
</Button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ import UnclaimedHypercertBatchClaimButton from "../unclaimed-hypercert-batchClai
import { TableToolbar } from "./table-toolbar";
import { useMediaQuery } from "@/hooks/use-media-query";
import { UnclaimedFraction } from "../unclaimed-hypercerts-list";
import { useAccountStore } from "@/lib/account-store";
import { useRouter } from "next/navigation";

export interface DataTableProps {
columns: ColumnDef<UnclaimedFraction>[];
data: UnclaimedFraction[];
}

export function UnclaimedFractionTable({ columns, data }: DataTableProps) {
const { selectedAccount } = useAccountStore();
const router = useRouter();
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
Expand Down Expand Up @@ -139,6 +143,11 @@ export function UnclaimedFractionTable({ columns, data }: DataTableProps) {
setSelectedRecords(getSelectedRecords());
}, [rowSelection, getSelectedRecords]);

// Refresh the entire route when account changes
useEffect(() => {
router.refresh();
}, [selectedAccount?.address, router]);

return (
<div className="w-full">
<div className="flex gap-2 py-4 flex-col lg:flex-row">
Expand Down
25 changes: 25 additions & 0 deletions hypercerts/ClaimHypercertStrategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Address, Chain } from "viem";
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
import { HypercertClient } from "@hypercerts-org/sdk";
import { UseWalletClientReturnType } from "wagmi";

import { useStepProcessDialogContext } from "@/components/global/step-process-dialog";

export interface ClaimHypercertParams {
tokenId: bigint;
units: bigint;
proof: `0x${string}`[];
}

export abstract class ClaimHypercertStrategy {
constructor(
protected address: Address,
protected chain: Chain,
protected client: HypercertClient,
protected dialogContext: ReturnType<typeof useStepProcessDialogContext>,
protected walletClient: UseWalletClientReturnType,
protected router: AppRouterInstance,
) {}

abstract execute(params: ClaimHypercertParams[]): Promise<void>;
}
Loading