Skip to content

Commit 304f4f7

Browse files
committed
Merge branch 'dev' into fix/profile_server_client_refresh
2 parents 06b1297 + 0849cd4 commit 304f4f7

16 files changed

Lines changed: 529 additions & 190 deletions

app/profile/[address]/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { HypercertsTabContent } from "@/components/profile/hypercerts-tab/hyperc
88
import { CollectionsTabContent } from "@/app/profile/[address]/collections-tab-content";
99
import { MarketplaceTabContent } from "@/app/profile/[address]/marketplace-tab-content";
1010
import { BlueprintsTabContent } from "@/app/profile/[address]/blueprint-tab-content";
11+
import { ContractAccountBanner } from "@/components/profile/contract-accounts-banner";
12+
import { ProfileAccountSwitcher } from "@/components/profile/account-switcher";
1113

1214
export default function ProfilePage({
1315
params,
@@ -22,6 +24,8 @@ export default function ProfilePage({
2224

2325
return (
2426
<section className="flex flex-col gap-2">
27+
<ContractAccountBanner address={address} />
28+
<ProfileAccountSwitcher address={address} />
2529
<section className="flex flex-wrap gap-2 items-center">
2630
<h1 className="font-serif text-3xl lg:text-5xl tracking-tight">
2731
Profile

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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"use client";
2+
3+
import { useAccount } from "wagmi";
4+
import { useEffect } from "react";
5+
import { useRouter } from "next/navigation";
6+
7+
import { useAccountStore } from "@/lib/account-store";
8+
import { useSafeAccounts } from "@/hooks/useSafeAccounts";
9+
10+
export function ProfileAccountSwitcher({ address }: { address: string }) {
11+
const { address: connectedAddress } = useAccount();
12+
const { safeAccounts } = useSafeAccounts();
13+
const router = useRouter();
14+
const selectedAccount = useAccountStore((state) => state.selectedAccount);
15+
16+
useEffect(() => {
17+
if (!selectedAccount || !connectedAddress) return;
18+
19+
const currentAddress = address.toLowerCase();
20+
const accounts = [
21+
{ type: "eoa", address: connectedAddress },
22+
...safeAccounts,
23+
];
24+
25+
// Find current account index
26+
const currentIndex = accounts.findIndex(
27+
(account) => account.address.toLowerCase() === currentAddress,
28+
);
29+
30+
// If current address matches the connected address or a safe address the user is a signer on,
31+
// and it's not the selected account, redirect to the selected account
32+
if (
33+
currentIndex !== -1 &&
34+
currentAddress !== selectedAccount.address.toLowerCase()
35+
) {
36+
router.push(`/profile/${selectedAccount.address}`);
37+
}
38+
}, [selectedAccount, address, connectedAddress, safeAccounts, router]);
39+
40+
return null;
41+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { isContract } from "@/lib/isContract";
2+
3+
export async function ContractAccountBanner({ address }: { address: string }) {
4+
const isContractAddress = await isContract(address);
5+
6+
if (!isContractAddress) return null;
7+
8+
return (
9+
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-4">
10+
<div className="text-sm text-yellow-700">
11+
This is a smart contract address. Contract ownership may vary across
12+
networks. Please verify ownership details for each network.
13+
</div>
14+
</div>
15+
);
16+
}

hooks/use-hypercert-client.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@ import { useEffect, useState } from "react";
44
import { useAccount, useWalletClient } from "wagmi";
55
import { ENVIRONMENT, SUPPORTED_CHAINS } from "@/configs/constants";
66
import { EvmClientFactory } from "@/lib/evmClient";
7+
import { PublicClient } from "viem";
78

89
export const useHypercertClient = () => {
910
const { data: walletClient } = useWalletClient();
1011
const { isConnected } = useAccount();
1112
const [client, setClient] = useState<HypercertClient>();
1213

13-
const publicClient = walletClient?.chain.id
14-
? EvmClientFactory.createClient(walletClient.chain.id)
15-
: undefined;
14+
let publicClient: PublicClient | undefined;
15+
try {
16+
publicClient = walletClient?.chain.id
17+
? EvmClientFactory.createClient(walletClient.chain.id)
18+
: undefined;
19+
} catch (error) {
20+
console.error(`Error creating public client: ${error}`);
21+
}
1622

1723
useEffect(() => {
1824
if (!walletClient || !isConnected) {

hooks/useIsContract.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useState, useEffect } from "react";
2+
3+
import { ChainFactory } from "../lib/chainFactory";
4+
import { EvmClientFactory } from "../lib/evmClient";
5+
6+
const contractCache = new Map<string, boolean>();
7+
8+
export function useIsContract(address: string) {
9+
const [isLoading, setIsLoading] = useState(false);
10+
11+
useEffect(() => {
12+
if (isLoading || contractCache.has(address)) return;
13+
14+
async function checkContract() {
15+
setIsLoading(true);
16+
try {
17+
const supportedChains = ChainFactory.getSupportedChains();
18+
const clients = supportedChains.map((chainId) =>
19+
EvmClientFactory.createClient(chainId),
20+
);
21+
22+
const results = await Promise.allSettled(
23+
clients.map((client) =>
24+
client.getCode({ address: address as `0x${string}` }),
25+
),
26+
);
27+
28+
const result = results.some(
29+
(result) =>
30+
result.status === "fulfilled" &&
31+
result.value !== undefined &&
32+
result.value !== "0x",
33+
);
34+
35+
contractCache.set(address, result);
36+
} finally {
37+
setIsLoading(false);
38+
}
39+
}
40+
41+
checkContract();
42+
}, [address]);
43+
44+
return {
45+
isContract: contractCache.get(address) ?? null,
46+
isLoading,
47+
};
48+
}
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+
}
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+
}

0 commit comments

Comments
 (0)