Skip to content

Commit 2b76864

Browse files
committed
feat: add safe support for batch claiming hypercerts
In the previous commit 5ca3a2c I missed adding support for batch claiming hypercerts. This patch adds another set of strategies for this case, nicely abstracting all these code paths into their own strategy.
1 parent 2a93a97 commit 2b76864

9 files changed

+270
-113
lines changed

components/profile/unclaimed-hypercert-batchClaim-button.tsx

Lines changed: 22 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
"use client";
22

3-
import { AllowListRecord } from "@/allowlists/actions/getAllowListRecordsForAddressByClaimed";
4-
import { revalidatePathServerAction } from "@/app/actions/revalidatePathServerAction";
5-
import { useHypercertClient } from "@/hooks/use-hypercert-client";
6-
import { ChainFactory } from "@/lib/chainFactory";
7-
import { errorToast } from "@/lib/errorToast";
3+
import { AllowListRecord } from "@/allowlists/getAllowListRecordsForAddressByClaimed";
4+
import { Button } from "../ui/button";
5+
import { useAccount, useSwitchChain } from "wagmi";
86
import { useRouter } from "next/navigation";
97
import { useState } from "react";
10-
import { ByteArray, getAddress, Hex } from "viem";
11-
import { waitForTransactionReceipt } from "viem/actions";
12-
import { useAccount, useSwitchChain, useWalletClient } from "wagmi";
13-
import { createExtraContent } from "../global/extra-content";
14-
import { useStepProcessDialogContext } from "../global/step-process-dialog";
15-
import { Button } from "../ui/button";
8+
import { getAddress, Hex, ByteArray } from "viem";
9+
import { errorToast } from "@/lib/errorToast";
10+
import { ChainFactory } from "@/lib/chainFactory";
11+
import { useClaimHypercertStrategy } from "@/hypercerts/hooks/useClaimHypercertStrategy";
12+
import { useAccountStore } from "@/lib/account-store";
1613

1714
interface TransformedClaimData {
1815
hypercertTokenIds: bigint[];
@@ -39,104 +36,39 @@ export default function UnclaimedHypercertBatchClaimButton({
3936
allowListRecords: AllowListRecord[];
4037
selectedChainId: number | null;
4138
}) {
42-
const router = useRouter();
43-
const { client } = useHypercertClient();
44-
const { data: walletClient } = useWalletClient();
4539
const account = useAccount();
4640
const [isLoading, setIsLoading] = useState(false);
47-
const { setDialogStep, setSteps, setOpen, setTitle, setExtraContent } =
48-
useStepProcessDialogContext();
4941
const { switchChain } = useSwitchChain();
42+
const getStrategy = useClaimHypercertStrategy();
43+
const { selectedAccount } = useAccountStore();
44+
5045
const selectedChain = selectedChainId
5146
? ChainFactory.getChain(selectedChainId)
5247
: null;
5348

54-
const refreshData = async (address: string) => {
55-
const hypercertIds = allowListRecords.map((record) => record.hypercert_id);
56-
57-
const hypercertViewInvalidationPaths = hypercertIds.map((id) => {
58-
return `/hypercerts/${id}`;
59-
});
60-
61-
await revalidatePathServerAction([
62-
`/profile/${address}`,
63-
`/profile/${address}?tab`,
64-
`/profile/${address}?tab=hypercerts-claimable`,
65-
`/profile/${address}?tab=hypercerts-owned`,
66-
...hypercertViewInvalidationPaths,
67-
]).then(async () => {
68-
setTimeout(() => {
69-
// refresh after 5 seconds
70-
router.refresh();
71-
72-
// push to the profile page with the hypercerts-claimable tab
73-
// because revalidatePath will revalidate on the next page visit.
74-
router.push(`/profile/${address}?tab=hypercerts-claimable`);
75-
}, 5000);
76-
});
77-
};
78-
7949
const claimHypercert = async () => {
8050
setIsLoading(true);
81-
setOpen(true);
82-
setSteps([
83-
{ id: "preparing", description: "Preparing to claim fractions..." },
84-
{ id: "claiming", description: "Claiming fractions on-chain..." },
85-
{ id: "confirming", description: "Waiting for on-chain confirmation" },
86-
{ id: "done", description: "Claiming complete!" },
87-
]);
88-
setTitle("Claim fractions from Allowlist");
89-
if (!client) {
90-
throw new Error("No client found");
91-
}
92-
if (!walletClient) {
93-
throw new Error("No wallet client found");
94-
}
95-
if (!account) {
96-
throw new Error("No address found");
97-
}
98-
99-
const claimData = transformAllowListRecords(allowListRecords);
100-
await setDialogStep("preparing, active");
10151
try {
102-
await setDialogStep("claiming", "active");
103-
const tx = await client.batchClaimFractionsFromAllowlists(claimData);
104-
105-
if (!tx) {
106-
await setDialogStep("claiming", "error");
107-
throw new Error("Failed to claim fractions");
108-
}
109-
110-
await setDialogStep("confirming", "active");
111-
const receipt = await waitForTransactionReceipt(walletClient, {
112-
hash: tx,
113-
});
114-
115-
if (receipt.status == "success") {
116-
await setDialogStep("done", "completed");
117-
const extraContent = createExtraContent({
118-
receipt,
119-
chain: account?.chain!,
120-
});
121-
setExtraContent(extraContent);
122-
refreshData(getAddress(account.address!));
123-
} else if (receipt.status == "reverted") {
124-
await setDialogStep("confirming", "error", "Transaction reverted");
125-
}
52+
const claimData = transformAllowListRecords(allowListRecords);
53+
const params = claimData.hypercertTokenIds.map((tokenId, index) => ({
54+
tokenId,
55+
units: claimData.units[index],
56+
proof: claimData.proofs[index] as `0x${string}`[],
57+
}));
58+
await getStrategy(params).execute(params);
12659
} catch (error) {
127-
console.error("Claim error:", error);
128-
await setDialogStep("claiming", "error", "Transaction failed");
60+
console.error(error);
12961
} finally {
13062
setIsLoading(false);
13163
}
13264
};
13365

66+
const activeAddress = selectedAccount?.address || account.address;
13467
const isBatchClaimDisabled =
13568
isLoading ||
13669
!allowListRecords.length ||
137-
!account ||
138-
!client ||
139-
account.address !== getAddress(allowListRecords[0].user_address as string);
70+
!activeAddress ||
71+
activeAddress !== getAddress(allowListRecords[0].user_address as string);
14072

14173
return (
14274
<>

components/profile/unclaimed-hypercert-claim-button.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ export default function UnclaimedHypercertClaimButton({
3535
throw new Error("Invalid allow list record");
3636
}
3737

38-
await claimHypercert({
39-
tokenId: BigInt(selectedHypercert.token_id),
40-
units: BigInt(selectedHypercert.units),
41-
proof: selectedHypercert.proof as `0x${string}`[],
42-
});
38+
await claimHypercert([
39+
{
40+
tokenId: BigInt(selectedHypercert.token_id),
41+
units: BigInt(selectedHypercert.units),
42+
proof: selectedHypercert.proof as `0x${string}`[],
43+
},
44+
]);
4345
} catch (error) {
4446
console.error(error);
4547
} finally {

hypercerts/ClaimHypercertStrategy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ export abstract class ClaimHypercertStrategy {
2121
protected router: AppRouterInstance,
2222
) {}
2323

24-
abstract execute(params: ClaimHypercertParams): Promise<void>;
24+
abstract execute(params: ClaimHypercertParams[]): Promise<void>;
2525
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { waitForTransactionReceipt } from "viem/actions";
2+
3+
import { createExtraContent } from "@/components/global/extra-content";
4+
import { revalidatePathServerAction } from "@/app/actions/revalidatePathServerAction";
5+
6+
import {
7+
ClaimHypercertStrategy,
8+
ClaimHypercertParams,
9+
} from "./ClaimHypercertStrategy";
10+
11+
export class EOABatchClaimHypercertStrategy extends ClaimHypercertStrategy {
12+
async execute(params: ClaimHypercertParams[]): Promise<void> {
13+
const { setDialogStep, setSteps, setOpen, setTitle, setExtraContent } =
14+
this.dialogContext;
15+
const { data: walletClient } = this.walletClient;
16+
17+
if (!this.client) throw new Error("No client found");
18+
if (!walletClient) throw new Error("No wallet client found");
19+
20+
setOpen(true);
21+
setSteps([
22+
{ id: "preparing", description: "Preparing to claim fractions..." },
23+
{ id: "claiming", description: "Claiming fractions on-chain..." },
24+
{ id: "confirming", description: "Waiting for on-chain confirmation" },
25+
{ id: "done", description: "Claiming complete!" },
26+
]);
27+
setTitle("Claim fractions from Allowlist");
28+
29+
try {
30+
await setDialogStep("preparing", "active");
31+
await setDialogStep("claiming", "active");
32+
33+
const tx = await this.client.batchClaimFractionsFromAllowlists(
34+
mapClaimParams(params),
35+
);
36+
if (!tx) {
37+
await setDialogStep("claiming", "error");
38+
throw new Error("Failed to claim fractions");
39+
}
40+
41+
await setDialogStep("confirming", "active");
42+
const receipt = await waitForTransactionReceipt(walletClient, {
43+
hash: tx,
44+
});
45+
46+
if (receipt.status === "success") {
47+
await setDialogStep("done", "completed");
48+
const extraContent = createExtraContent({
49+
receipt,
50+
chain: this.chain,
51+
});
52+
setExtraContent(extraContent);
53+
54+
const hypercertViewInvalidationPaths = params.map((param) => {
55+
return `/hypercerts/${param.tokenId}`;
56+
});
57+
58+
// Revalidate all relevant paths
59+
await revalidatePathServerAction([
60+
`/profile/${this.address}`,
61+
`/profile/${this.address}?tab`,
62+
`/profile/${this.address}?tab=hypercerts-claimable`,
63+
`/profile/${this.address}?tab=hypercerts-owned`,
64+
...hypercertViewInvalidationPaths,
65+
]);
66+
67+
// Wait 5 seconds before refreshing and navigating
68+
setTimeout(() => {
69+
this.router.refresh();
70+
this.router.push(`/profile/${this.address}?tab=hypercerts-claimable`);
71+
}, 5000);
72+
} else if (receipt.status === "reverted") {
73+
await setDialogStep("confirming", "error", "Transaction reverted");
74+
}
75+
} catch (error) {
76+
console.error("Claim error:", error);
77+
await setDialogStep("claiming", "error", "Transaction failed");
78+
throw error;
79+
}
80+
}
81+
}
82+
83+
function mapClaimParams(params: ClaimHypercertParams[]) {
84+
return {
85+
hypercertTokenIds: params.map((p) => p.tokenId),
86+
units: params.map((p) => p.units),
87+
proofs: params.map((p) => p.proof),
88+
};
89+
}

hypercerts/EOAClaimHypercertStrategy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { waitForTransactionReceipt } from "viem/actions";
2+
import assert from "assert";
23

34
import { createExtraContent } from "@/components/global/extra-content";
45
import { revalidatePathServerAction } from "@/app/actions/revalidatePathServerAction";
@@ -9,7 +10,10 @@ import {
910
} from "./ClaimHypercertStrategy";
1011

1112
export class EOAClaimHypercertStrategy extends ClaimHypercertStrategy {
12-
async execute({ tokenId, units, proof }: ClaimHypercertParams) {
13+
async execute(params: ClaimHypercertParams[]) {
14+
assert(params.length === 1, "Only one claim params object allowed");
15+
16+
const { tokenId, units, proof } = params[0];
1317
const { setDialogStep, setSteps, setOpen, setTitle, setExtraContent } =
1418
this.dialogContext;
1519
const { data: walletClient } = this.walletClient;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { Chain } from "viem";
2+
import { ExternalLink } from "lucide-react";
3+
4+
import { Button } from "@/components/ui/button";
5+
import { generateSafeAppLink } from "@/lib/utils";
6+
7+
import {
8+
ClaimHypercertStrategy,
9+
ClaimHypercertParams,
10+
} from "./ClaimHypercertStrategy";
11+
12+
function DialogFooter({
13+
chain,
14+
safeAddress,
15+
}: {
16+
chain: Chain;
17+
safeAddress: string;
18+
}) {
19+
return (
20+
<div className="flex flex-col space-y-2">
21+
<p className="text-lg font-medium">Success</p>
22+
<p className="text-sm font-medium">
23+
We&apos;ve submitted the transaction requests to the connected Safe.
24+
</p>
25+
<div className="flex space-x-4 py-4 justify-center">
26+
{chain && (
27+
<Button asChild>
28+
<a
29+
href={generateSafeAppLink(chain, safeAddress as `0x${string}`)}
30+
target="_blank"
31+
rel="noopener noreferrer"
32+
>
33+
View Safe <ExternalLink size={14} className="ml-2" />
34+
</a>
35+
</Button>
36+
)}
37+
</div>
38+
</div>
39+
);
40+
}
41+
42+
export class SafeBatchClaimHypercertStrategy extends ClaimHypercertStrategy {
43+
async execute(params: ClaimHypercertParams[]): Promise<void> {
44+
console.log("[SafeBatchClaim] Starting execution with params:", params);
45+
const { setDialogStep, setSteps, setOpen, setTitle, setExtraContent } =
46+
this.dialogContext;
47+
48+
if (!this.client) {
49+
console.error("[SafeBatchClaim] No client found");
50+
setOpen(false);
51+
throw new Error("No client found");
52+
}
53+
54+
console.log("[SafeBatchClaim] Setting up dialog UI");
55+
setOpen(true);
56+
setTitle("Claim fractions from Allowlist");
57+
setSteps([
58+
{ id: "preparing", description: "Preparing to claim fractions..." },
59+
{ id: "submitting", description: "Submitting to Safe..." },
60+
{ id: "queued", description: "Transaction queued in Safe" },
61+
]);
62+
63+
await setDialogStep("preparing", "active");
64+
console.log("[SafeBatchClaim] Preparation step completed");
65+
66+
try {
67+
console.log("[SafeBatchClaim] Starting submission to Safe");
68+
await setDialogStep("submitting", "active");
69+
const mappedParams = mapClaimParams(params);
70+
console.log("[SafeBatchClaim] Mapped params:", mappedParams);
71+
72+
await this.client.batchClaimFractionsFromAllowlists({
73+
...mappedParams,
74+
overrides: {
75+
safeAddress: this.address as `0x${string}`,
76+
},
77+
});
78+
79+
console.log("[SafeBatchClaim] Successfully queued transaction in Safe");
80+
await setDialogStep("queued", "completed");
81+
82+
setExtraContent(() => (
83+
<DialogFooter chain={this.chain} safeAddress={this.address} />
84+
));
85+
} catch (error) {
86+
console.error("[SafeBatchClaim] Error during execution:", error);
87+
await setDialogStep(
88+
"submitting",
89+
"error",
90+
error instanceof Error ? error.message : "Unknown error",
91+
);
92+
throw error;
93+
}
94+
}
95+
}
96+
97+
function mapClaimParams(params: ClaimHypercertParams[]) {
98+
return {
99+
hypercertTokenIds: params.map((p) => p.tokenId),
100+
units: params.map((p) => p.units),
101+
proofs: params.map((p) => p.proof),
102+
};
103+
}

0 commit comments

Comments
 (0)