Skip to content

Commit 73a5443

Browse files
authored
Merge pull request #452 from hypercerts-org/safe-integration-marketplace-buy
Safe integration marketplace buy
2 parents 49013ae + a83a5a8 commit 73a5443

File tree

12 files changed

+739
-2643
lines changed

12 files changed

+739
-2643
lines changed
Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,90 @@
11
import { Button } from "@/components/ui/button";
2+
import { ExternalLink } from "lucide-react";
23
import type { Chain, TransactionReceipt } from "viem";
3-
import { generateBlockExplorerLink } from "@/lib/utils";
4+
import { generateBlockExplorerLink, generateSafeAppLink } from "@/lib/utils";
45

6+
interface ExtraContentProps {
7+
message?: React.ReactNode;
8+
hypercertId: string;
9+
onClose?: () => void;
10+
chain: Chain;
11+
isSafe?: boolean;
12+
safeAddress?: `0x${string}`;
13+
receipt?: TransactionReceipt;
14+
}
15+
16+
export function ExtraContent({
17+
message = "Your hypercert has been minted successfully!",
18+
hypercertId,
19+
onClose,
20+
chain,
21+
isSafe,
22+
safeAddress,
23+
receipt,
24+
}: ExtraContentProps) {
25+
const handleHypercertClick = () => {
26+
if (onClose) {
27+
window.location.href = `/hypercerts/${hypercertId}`;
28+
onClose();
29+
}
30+
};
31+
32+
return (
33+
<div className="flex flex-col space-y-2">
34+
<p className="text-lg font-medium">Success</p>
35+
<p className="text-sm font-medium">{message}</p>
36+
<div className="flex space-x-4 py-4 justify-center">
37+
<Button
38+
onClick={onClose ? handleHypercertClick : undefined}
39+
asChild={!onClose}
40+
>
41+
{onClose ? (
42+
"View hypercert"
43+
) : (
44+
<a
45+
href={`/hypercerts/${hypercertId}`}
46+
target="_blank"
47+
rel="noopener noreferrer"
48+
>
49+
View hypercert
50+
</a>
51+
)}
52+
</Button>
53+
54+
{chain && (
55+
<Button asChild>
56+
{isSafe ? (
57+
<a
58+
href={generateSafeAppLink(chain, safeAddress!)}
59+
target="_blank"
60+
rel="noopener noreferrer"
61+
>
62+
View Safe <ExternalLink size={14} className="ml-2" />
63+
</a>
64+
) : (
65+
<a
66+
href={generateBlockExplorerLink(
67+
chain,
68+
receipt?.transactionHash ?? "",
69+
)}
70+
target="_blank"
71+
rel="noopener noreferrer"
72+
>
73+
View transaction <ExternalLink size={14} className="ml-2" />
74+
</a>
75+
)}
76+
</Button>
77+
)}
78+
</div>
79+
<p className="text-sm font-medium">
80+
New ownership will not be immediately visible on the Hypercerts page,
81+
but will be visible in 5-10 minutes.
82+
</p>
83+
</div>
84+
);
85+
}
86+
87+
// For backwards compatibility
588
export const createExtraContent = ({
689
receipt,
790
hypercertId,
@@ -11,43 +94,9 @@ export const createExtraContent = ({
1194
hypercertId?: string;
1295
chain: Chain;
1396
}) => {
14-
const receiptButton = receipt && (
15-
<>
16-
<a
17-
href={generateBlockExplorerLink(chain, receipt.transactionHash)}
18-
target="_blank"
19-
rel="noopener noreferrer"
20-
>
21-
<Button size="default" variant={"secondary"}>
22-
View transaction
23-
</Button>
24-
</a>
25-
</>
26-
);
27-
28-
const hypercertButton = hypercertId && (
29-
<>
30-
<a
31-
href={`/hypercerts/${hypercertId}`}
32-
target="_blank"
33-
rel="noopener noreferrer"
34-
>
35-
<Button size="default" variant={"default"}>
36-
View hypercert
37-
</Button>
38-
</a>
39-
</>
40-
);
97+
if (!hypercertId) return null;
4198

4299
return (
43-
<div className="flex flex-col space-y-2">
44-
<p className="text-sm font-medium">
45-
Your hypercert has been minted successfully!
46-
</p>
47-
<div className="flex space-x-4">
48-
{receiptButton}
49-
{hypercertButton}
50-
</div>
51-
</div>
100+
<ExtraContent hypercertId={hypercertId} chain={chain} receipt={receipt} />
52101
);
53102
};

components/marketplace/hypercert-listings-table.tsx

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import {
1010
} from "@/components/ui/table";
1111
import { useToast } from "@/components/ui/use-toast";
1212
import { DEFAULT_DISPLAY_CURRENCY } from "@/configs/hypercerts";
13-
import { useHypercertExchangeClient } from "@/hooks/use-hypercert-exchange-client";
13+
// TODO: Is there a way to silence these errors?
14+
// We could extract the types to a separate file and make a note why these errors occur,
15+
// so that the next person stumbling over this doesn't try to solve them to no avail.
1416
import { HypercertFull } from "@/hypercerts/fragments/hypercert-full.fragment";
15-
import { cn } from "@/lib/utils";
1617
import { OrderFragment } from "@/marketplace/fragments/order.fragment";
18+
import { useAccountStore } from "@/lib/account-store";
19+
import { cn } from "@/lib/utils";
1720
import { CancelOrderParams, useCancelOrder } from "@/marketplace/hooks";
1821
import {
1922
flexRender,
@@ -46,8 +49,9 @@ export default function HypercertListingsTable({
4649
initialHypercert: HypercertFull;
4750
searchParams: Record<string, string>;
4851
}) {
49-
const { client: hypercertExchangeClient } = useHypercertExchangeClient();
50-
const { address, chainId: connectedChainId } = useAccount();
52+
const { address: connectedAddress } = useAccount();
53+
const { selectedAccount } = useAccountStore();
54+
const activeAddress = selectedAccount?.address || connectedAddress;
5155
const [chainId] = hypercertId.split("-");
5256
const router = useRouter();
5357
const { toast } = useToast();
@@ -58,17 +62,6 @@ export default function HypercertListingsTable({
5862

5963
const displayCurrency = searchParams?.currency || DEFAULT_DISPLAY_CURRENCY;
6064

61-
const refreshOrderValidity = async (tokenId: string) => {
62-
if (!hypercertExchangeClient || !connectedChainId) {
63-
console.log("No hypercert exchange client or invalid chain ID");
64-
return;
65-
}
66-
await hypercertExchangeClient.api.updateOrderValidity(
67-
[BigInt(tokenId)],
68-
connectedChainId,
69-
);
70-
};
71-
7265
const { mutateAsync: cancelOrderMutation } = useCancelOrder();
7366

7467
const cancelOrder = useCallback(
@@ -122,7 +115,7 @@ export default function HypercertListingsTable({
122115
id: "action",
123116
cell: (row: any) => {
124117
const order = row.row.original;
125-
const isOwner = address && order.signer === address;
118+
const isOwner = activeAddress && order.signer === activeAddress;
126119
const isProcessing = order.orderNonce === activeOrderNonce;
127120
const isCancelling = order.orderNonce === cancellingOrderNonce;
128121

@@ -184,13 +177,13 @@ export default function HypercertListingsTable({
184177
) : (
185178
<Button
186179
disabled={
187-
!address ||
180+
!activeAddress ||
188181
order.chainId !== chainId ||
189182
!!activeOrderNonce ||
190183
!!cancellingOrderNonce
191184
}
192185
>
193-
{!address ? "Connect wallet" : "Buy"}
186+
{!activeAddress ? "Connect wallet" : "Buy"}
194187
</Button>
195188
)
196189
}

hooks/use-hypercert-exchange-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export const useHypercertExchangeClient = () => {
3030
{
3131
apiEndpoint: HYPERCERTS_API_URL_REST,
3232
},
33+
walletClient,
3334
);
3435
}, [walletClient, chainId, provider, signer]);
3536

lib/utils.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,38 @@ export const formatTransferRestriction = (
5454
}
5555
};
5656

57+
export const getSafeChainAbbreviation = (chain: Chain | undefined) => {
58+
if (!chain) {
59+
return "";
60+
}
61+
switch (chain.id) {
62+
case 11155111:
63+
return "sep";
64+
case 8453:
65+
return "base";
66+
case 10:
67+
return "oeth";
68+
case 84532:
69+
return "basesep";
70+
case 42220:
71+
return "celo";
72+
case 42161:
73+
return "arb1";
74+
default:
75+
return chain.name;
76+
}
77+
};
78+
79+
export const generateSafeAppLink = (
80+
chain: Chain | undefined,
81+
safeAddress: `0x${string}`,
82+
) => {
83+
if (!chain) {
84+
return "";
85+
}
86+
return `https://app.safe.global/transactions/queue?safe=${getSafeChainAbbreviation(chain)}:${safeAddress}`;
87+
};
88+
5789
export const generateBlockExplorerLink = (
5890
chain: Chain | undefined,
5991
transactionHash: string,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
HypercertExchangeClient,
3+
WETHAbi,
4+
addressesByNetwork,
5+
utils,
6+
} from "@hypercerts-org/marketplace-sdk";
7+
import { readContract } from "viem/actions";
8+
import { UseWalletClientReturnType } from "wagmi";
9+
import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
10+
import { useStepProcessDialogContext } from "@/components/global/step-process-dialog";
11+
12+
import { MarketplaceOrder } from "./types";
13+
14+
export abstract class BuyFractionalStrategy {
15+
constructor(
16+
protected address: `0x${string}`,
17+
protected chainId: number,
18+
protected exchangeClient: HypercertExchangeClient,
19+
protected dialogContext: ReturnType<typeof useStepProcessDialogContext>,
20+
protected walletClient: UseWalletClientReturnType,
21+
protected router: AppRouterInstance,
22+
) {}
23+
24+
abstract execute(params: {
25+
order: MarketplaceOrder;
26+
unitAmount: bigint;
27+
pricePerUnit: string;
28+
hypercertName?: string | null;
29+
totalUnitsInHypercert?: bigint;
30+
}): Promise<void>;
31+
32+
async getERC20Allowance(currency: `0x${string}`): Promise<bigint> {
33+
const hypercertsExchangeAddress =
34+
addressesByNetwork[utils.asDeployedChain(this.chainId)].EXCHANGE_V2;
35+
36+
if (!this.walletClient.data) {
37+
return BigInt(0);
38+
}
39+
40+
return (await readContract(this.walletClient.data, {
41+
abi: WETHAbi,
42+
address: currency as `0x${string}`,
43+
functionName: "allowance",
44+
args: [this.address, hypercertsExchangeAddress],
45+
})) as bigint;
46+
}
47+
}

0 commit comments

Comments
 (0)