Skip to content

Commit 8cee6c7

Browse files
authored
Merge pull request #484 from pheuberger/safe-support-hypercert-minting
Safe support for minting a Hypercert
2 parents 025eb79 + ecf41c0 commit 8cee6c7

10 files changed

+384
-187
lines changed

components/global/extra-content.tsx

Lines changed: 2 additions & 0 deletions
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,
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { track } from "@vercel/analytics";
2+
import { waitForTransactionReceipt } from "viem/actions";
3+
4+
import { createExtraContent } from "@/components/global/extra-content";
5+
import { generateHypercertIdFromReceipt } from "@/lib/generateHypercertIdFromReceipt";
6+
import { revalidatePathServerAction } from "@/app/actions/revalidatePathServerAction";
7+
8+
import {
9+
MintHypercertParams,
10+
MintHypercertStrategy,
11+
} from "./MintHypercertStrategy";
12+
import { Address, Chain } from "viem";
13+
import { HypercertClient } from "@hypercerts-org/sdk";
14+
import { UseWalletClientReturnType } from "wagmi";
15+
import { useStepProcessDialogContext } from "@/components/global/step-process-dialog";
16+
import { useQueueMintBlueprint } from "@/blueprints/hooks/queueMintBlueprint";
17+
18+
export class EOAMintHypercertStrategy extends MintHypercertStrategy {
19+
constructor(
20+
protected address: Address,
21+
protected chain: Chain,
22+
protected client: HypercertClient,
23+
protected dialogContext: ReturnType<typeof useStepProcessDialogContext>,
24+
protected queueMintBlueprint: ReturnType<typeof useQueueMintBlueprint>,
25+
protected walletClient: UseWalletClientReturnType,
26+
) {
27+
super(address, chain, client, dialogContext, walletClient);
28+
}
29+
30+
// FIXME: this is a long ass method. Break it down into smaller ones.
31+
async execute({
32+
metaData,
33+
units,
34+
transferRestrictions,
35+
allowlistRecords,
36+
blueprintId,
37+
}: MintHypercertParams) {
38+
const { setDialogStep, setSteps, setOpen, setTitle, setExtraContent } =
39+
this.dialogContext;
40+
const { mutateAsync: queueMintBlueprint } = this.queueMintBlueprint;
41+
const { data: walletClient } = this.walletClient;
42+
43+
if (!this.client) {
44+
setOpen(false);
45+
throw new Error("No client found");
46+
}
47+
48+
const isBlueprint = !!blueprintId;
49+
setOpen(true);
50+
setSteps([
51+
{ id: "preparing", description: "Preparing to mint hypercert..." },
52+
{ id: "minting", description: "Minting hypercert on-chain..." },
53+
...(isBlueprint
54+
? [{ id: "blueprint", description: "Queueing blueprint mint..." }]
55+
: []),
56+
{ id: "confirming", description: "Waiting for on-chain confirmation" },
57+
{ id: "route", description: "Creating your new hypercert's link..." },
58+
{ id: "done", description: "Minting complete!" },
59+
]);
60+
setTitle("Minting hypercert");
61+
await setDialogStep("preparing", "active");
62+
console.log("preparing...");
63+
64+
let hash;
65+
try {
66+
await setDialogStep("minting", "active");
67+
console.log("minting...");
68+
hash = await this.client.mintHypercert({
69+
metaData,
70+
totalUnits: units,
71+
transferRestriction: transferRestrictions,
72+
allowList: allowlistRecords,
73+
});
74+
} catch (error: unknown) {
75+
console.error("Error minting hypercert:", error);
76+
throw new Error(
77+
`Failed to mint hypercert: ${error instanceof Error ? error.message : "Unknown error"}`,
78+
);
79+
}
80+
81+
if (!hash) {
82+
throw new Error("No transaction hash returned");
83+
}
84+
85+
if (blueprintId) {
86+
try {
87+
await setDialogStep("blueprint", "active");
88+
await queueMintBlueprint({
89+
blueprintId,
90+
txHash: hash,
91+
});
92+
} catch (error: unknown) {
93+
console.error("Error queueing blueprint mint:", error);
94+
throw new Error(
95+
`Failed to queue blueprint mint: ${error instanceof Error ? error.message : "Unknown error"}`,
96+
);
97+
}
98+
}
99+
await setDialogStep("confirming", "active");
100+
console.log("Mint submitted", {
101+
hash,
102+
});
103+
track("Mint submitted", {
104+
hash,
105+
});
106+
let receipt;
107+
108+
try {
109+
receipt = await waitForTransactionReceipt(walletClient!, {
110+
confirmations: 3,
111+
hash,
112+
});
113+
console.log({ receipt });
114+
} catch (error: unknown) {
115+
console.error("Error waiting for transaction receipt:", error);
116+
await setDialogStep(
117+
"confirming",
118+
"error",
119+
error instanceof Error ? error.message : "Unknown error",
120+
);
121+
throw new Error(
122+
`Failed to confirm transaction: ${error instanceof Error ? error.message : "Unknown error"}`,
123+
);
124+
}
125+
126+
if (receipt?.status === "reverted") {
127+
throw new Error("Transaction reverted: Minting failed");
128+
}
129+
130+
await setDialogStep("route", "active");
131+
132+
let hypercertId;
133+
try {
134+
hypercertId = generateHypercertIdFromReceipt(receipt, this.chain.id);
135+
console.log("Mint completed", {
136+
hypercertId: hypercertId || "not found",
137+
});
138+
track("Mint completed", {
139+
hypercertId: hypercertId || "not found",
140+
});
141+
console.log({ hypercertId });
142+
} catch (error) {
143+
console.error("Error generating hypercert ID:", error);
144+
await setDialogStep(
145+
"route",
146+
"error",
147+
error instanceof Error ? error.message : "Unknown error",
148+
);
149+
}
150+
151+
const extraContent = createExtraContent({
152+
receipt,
153+
hypercertId,
154+
chain: this.chain,
155+
});
156+
setExtraContent(extraContent);
157+
158+
await setDialogStep("done", "completed");
159+
160+
// TODO: Clean up these revalidations.
161+
// https://github.com/hypercerts-org/hypercerts-app/pull/484#discussion_r2011898721
162+
await revalidatePathServerAction([
163+
"/collections",
164+
"/collections/edit/[collectionId]",
165+
`/profile/${this.address}`,
166+
{ path: `/`, type: "layout" },
167+
]);
168+
}
169+
}

hypercerts/MintHypercertStrategy.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Address, Chain } from "viem";
2+
import {
3+
HypercertClient,
4+
HypercertMetadata,
5+
TransferRestrictions,
6+
AllowlistEntry,
7+
} from "@hypercerts-org/sdk";
8+
import { UseWalletClientReturnType } from "wagmi";
9+
10+
import { useStepProcessDialogContext } from "@/components/global/step-process-dialog";
11+
12+
export interface MintHypercertParams {
13+
metaData: HypercertMetadata;
14+
units: bigint;
15+
transferRestrictions: TransferRestrictions;
16+
allowlistRecords?: AllowlistEntry[] | string;
17+
blueprintId?: number;
18+
}
19+
20+
export abstract class MintHypercertStrategy {
21+
constructor(
22+
protected address: Address,
23+
protected chain: Chain,
24+
protected client: HypercertClient,
25+
protected dialogContext: ReturnType<typeof useStepProcessDialogContext>,
26+
protected walletClient: UseWalletClientReturnType,
27+
) {}
28+
29+
abstract execute(params: MintHypercertParams): Promise<void>;
30+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {
2+
MintHypercertStrategy,
3+
MintHypercertParams,
4+
} from "./MintHypercertStrategy";
5+
6+
import { Button } from "@/components/ui/button";
7+
import { generateSafeAppLink } from "@/lib/utils";
8+
import { ExternalLink } from "lucide-react";
9+
import { Chain } from "viem";
10+
11+
export class SafeMintHypercertStrategy extends MintHypercertStrategy {
12+
async execute({
13+
metaData,
14+
units,
15+
transferRestrictions,
16+
allowlistRecords,
17+
}: MintHypercertParams) {
18+
const { setDialogStep, setSteps, setOpen, setTitle, setExtraContent } =
19+
this.dialogContext;
20+
21+
if (!this.client) {
22+
setOpen(false);
23+
throw new Error("No client found");
24+
}
25+
26+
setOpen(true);
27+
setTitle("Minting hypercert");
28+
setSteps([
29+
{ id: "preparing", description: "Preparing to mint hypercert..." },
30+
{ id: "submitting", description: "Submitting to Safe..." },
31+
{ id: "queued", description: "Transaction queued in Safe" },
32+
]);
33+
34+
await setDialogStep("preparing", "active");
35+
36+
try {
37+
await setDialogStep("submitting", "active");
38+
await this.client.mintHypercert({
39+
metaData,
40+
totalUnits: units,
41+
transferRestriction: transferRestrictions,
42+
allowList: allowlistRecords,
43+
overrides: {
44+
safeAddress: this.address as `0x${string}`,
45+
},
46+
});
47+
48+
await setDialogStep("queued", "completed");
49+
50+
setExtraContent(() => (
51+
<DialogFooter chain={this.chain} safeAddress={this.address} />
52+
));
53+
} catch (error) {
54+
console.error(error);
55+
await setDialogStep(
56+
"submitting",
57+
"error",
58+
error instanceof Error ? error.message : "Unknown error",
59+
);
60+
throw error;
61+
}
62+
}
63+
}
64+
65+
function DialogFooter({
66+
chain,
67+
safeAddress,
68+
}: {
69+
chain: Chain;
70+
safeAddress: string;
71+
}) {
72+
return (
73+
<div className="flex flex-col space-y-2">
74+
<p className="text-lg font-medium">Success</p>
75+
<p className="text-sm font-medium">
76+
We&apos;ve submitted the transaction requests to the connected Safe.
77+
</p>
78+
<div className="flex space-x-4 py-4 justify-center">
79+
{chain && (
80+
<Button asChild>
81+
<a
82+
href={generateSafeAppLink(chain, safeAddress as `0x${string}`)}
83+
target="_blank"
84+
rel="noopener noreferrer"
85+
>
86+
View Safe <ExternalLink size={14} className="ml-2" />
87+
</a>
88+
</Button>
89+
)}
90+
</div>
91+
</div>
92+
);
93+
}

0 commit comments

Comments
 (0)