Skip to content

Commit

Permalink
Feature/transfer fraction button (#1203)
Browse files Browse the repository at this point in the history
* add transfer fraction button inside split fraction button

* add confirmation dialog when transferring fraction

* use override chain id in hypercert fetcher within plasmic

* add reading of transfer restrictions to transfer fraction button
  • Loading branch information
Jipperism authored Dec 1, 2023
1 parent 5c94088 commit 6ef5b40
Show file tree
Hide file tree
Showing 7 changed files with 402 additions and 18 deletions.
6 changes: 5 additions & 1 deletion frontend/components/hypercert-fetcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface HypercertFetcherProps {
useQueryString?: boolean; // Forces us to try the query string first
byClaimId?: string; // Fetch by claimId
byMetadataUri?: string; // Fetch by metadataUri; If both are specified, byMetadataUri will override the URI in the claim
overrideChainId?: number; // Override the chainId
}

export function HypercertFetcher(props: HypercertFetcherProps) {
Expand All @@ -40,9 +41,12 @@ export function HypercertFetcher(props: HypercertFetcherProps) {
useQueryString,
byClaimId,
byMetadataUri,
overrideChainId,
} = props;
const [data, setData] = React.useState<Hypercert | undefined>();
const { client } = useHypercertClient();
const { client } = useHypercertClient({
overrideChainId,
});

React.useEffect(() => {
if (!client) {
Expand Down
2 changes: 2 additions & 0 deletions frontend/components/split-fraction-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PlusIcon } from "primereact/icons/plus";
import { Delete } from "@mui/icons-material";
import { useSplitFractionUnits } from "../hooks/splitClaimUnits";
import { toast } from "react-toastify";
import { TransferFractionButton } from "./transfer-fraction-button";

const style = {
position: "absolute",
Expand Down Expand Up @@ -183,6 +184,7 @@ export function SplitFractionButton({
</Formik>
</Box>
</Modal>
<TransferFractionButton fractionId={fractionId} />
</>
);
}
243 changes: 229 additions & 14 deletions frontend/components/transfer-fraction-button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
import { Button } from "@mui/material";
import React from "react";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
IconButton,
Modal,
TextField,
Typography,
} from "@mui/material";
import { Send } from "@mui/icons-material";
import React, { useState } from "react";
import { useTransferFraction } from "../hooks/transferFraction";
import { Form, Formik } from "formik";
import { isAddress } from "viem";
import { useClaimById, useFractionById } from "../hooks/fractions";
import { useAccountLowerCase } from "../hooks/account";
import { formatAddress } from "../lib/formatting";
import { TransferRestrictions } from "@hypercerts-org/sdk";
import { useReadTransferRestrictions } from "../hooks/readTransferRestriction";

const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "background.paper",
boxShadow: 24,
pt: 2,
px: 4,
pb: 3,
};

interface Props {
fractionId: string;
Expand All @@ -13,18 +47,199 @@ export function TransferFractionButton({
className,
disabled,
}: Props) {
const [open, setOpen] = useState(false);
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const { address } = useAccountLowerCase();

const { write, readOnly, txPending } = useTransferFraction({
onComplete: () => {
handleClose();
},
});

const [dialogOpen, setDialogOpen] = React.useState(false);

const handleDialogOpen = () => {
setDialogOpen(true);
};

const handleDialogClose = () => {
setDialogOpen(false);
};

const { data: fractionData, isLoading: isLoadingFraction } =
useFractionById(fractionId);

const { data: claim, isLoading: isLoadingClaim } = useClaimById(
fractionData?.claimToken?.claim.id,
);

const {
data: transferRestrictions,
isLoading: isLoadingTransferRestrictions,
} = useReadTransferRestrictions(claim?.claim?.tokenID);

const determineCanTransfer = () => {
if (!address) {
return false;
}

if (!transferRestrictions) {
return false;
}

if (!(transferRestrictions in TransferRestrictions)) {
return false;
}

const transferRestrictionValue =
TransferRestrictions[
transferRestrictions as keyof typeof TransferRestrictions
];

if (transferRestrictionValue === TransferRestrictions.DisallowAll) {
return false;
}

if (transferRestrictionValue === TransferRestrictions.AllowAll) {
return true;
}

if (transferRestrictionValue === TransferRestrictions.FromCreatorOnly) {
return claim?.claim?.creator === address;
}

return false;
};

const canTransfer = determineCanTransfer();

const tokenId = fractionId.split("-")[1];
const _disabled =
txPending ||
readOnly ||
disabled ||
isLoadingFraction ||
isLoadingClaim ||
isLoadingTransferRestrictions;

if (!canTransfer) {
return null;
}

return (
<Button
onClick={() =>
console.log({
fractionId,
text,
className,
disabled,
})
}
>
Send
</Button>
<>
<IconButton
size="small"
aria-label="transfer"
color="primary"
disabled={_disabled}
onClick={() => {
console.log({
fractionId,
text,
className,
disabled,
});
handleOpen();
}}
>
<Send fontSize="inherit" />
</IconButton>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={{ ...style, width: 400 }}>
<Formik
initialValues={{ to: "" }}
validate={(values) => {
if (!values.to || values.to === "") {
return { to: "Required" };
}

if (!isAddress(values.to)) {
return { to: "Invalid address" };
}
}}
onSubmit={() => {
handleDialogOpen();
}}
>
{({ isSubmitting, isValid, setFieldValue, errors, values }) => {
const isDisabled = _disabled || isSubmitting;
return (
<>
<Form
style={{
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: "1rem",
}}
>
<Typography
id="modal-modal-title"
variant="h6"
component="h2"
>
Transfer this fraction
</Typography>
<TextField
error={!!errors.to}
helperText={errors.to}
name="to"
required
style={{ width: "100%" }}
disabled={isDisabled}
placeholder="Recipient address"
onChange={(e) => {
setFieldValue("to", e.target.value);
}}
/>
<Button disabled={!isValid} type={"submit"}>
Transfer
</Button>
</Form>{" "}
<Dialog
open={dialogOpen}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
Are you sure you want to transfer?
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Transferring to {formatAddress(values.to)}. This action
cannot be reversed.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button color="error" onClick={handleDialogClose}>
cancel
</Button>
<Button
color="primary"
onClick={async () => {
await write(BigInt(tokenId), values.to);
}}
autoFocus
>
transfer
</Button>
</DialogActions>
</Dialog>
</>
);
}}
</Formik>
</Box>
</Modal>
</>
);
}
28 changes: 28 additions & 0 deletions frontend/hooks/fractions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,31 @@ export const useFractionById = (fractionId: string) => {
{ enabled: !!fractionId },
);
};

export const useClaimById = (claimId?: string | null) => {
const { client } = useHypercertClient();

return useQuery(
["graph", "claims", claimId],
() => {
if (!client) return null;
if (!claimId) return null;
return client.indexer.claimById(claimId);
},
{ enabled: !!claimId && !!client },
);
};

export const useClaimMetadataByUri = (uri?: string | null) => {
const { client } = useHypercertClient();

return useQuery(
["graph", "claim-metadata", uri],
() => {
if (!client) return null;
if (!uri) return null;
return client.storage.getMetadata(uri);
},
{ enabled: !!uri && !!client },
);
};
11 changes: 8 additions & 3 deletions frontend/hooks/hypercerts-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { NFT_STORAGE_TOKEN, WEB3_STORAGE_TOKEN } from "../lib/config";
import { HypercertClient, HypercertClientConfig } from "@hypercerts-org/sdk";
import { useWalletClient, useNetwork } from "wagmi";

export const useHypercertClient = () => {
export const useHypercertClient = ({
overrideChainId,
}: {
overrideChainId?: number;
} = {}) => {
const { chain } = useNetwork();
const clientConfig = {
chain,
Expand All @@ -26,13 +30,14 @@ export const useHypercertClient = () => {
} = useWalletClient();

useEffect(() => {
if (chain?.id && !walletClientLoading && !isError && walletClient) {
const chainId = overrideChainId || chain?.id;
if (chainId && !walletClientLoading && !isError && walletClient) {
setIsLoading(true);

try {
const config: Partial<HypercertClientConfig> = {
...clientConfig,
chain: { id: chain.id },
chain: { id: chainId },
walletClient,
};

Expand Down
28 changes: 28 additions & 0 deletions frontend/hooks/readTransferRestriction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useHypercertClient } from "./hypercerts-client";
import { useWalletClient } from "wagmi";
import { useQuery } from "@tanstack/react-query";
import { readContract } from "viem/actions";

export const useReadTransferRestrictions = (tokenId?: bigint) => {
const { client } = useHypercertClient();
const { data: walletClient } = useWalletClient();

return useQuery(
["read-transfer-restrictions", tokenId],
async () => {
if (!client) return null;
if (!tokenId) return null;
if (!walletClient) return null;
const contract = client.contract;

if (!contract) return null;

return (await readContract(walletClient, {
...contract,
functionName: "readTransferRestriction",
args: [tokenId],
})) as string;
},
{ enabled: !!tokenId && !!client },
);
};
Loading

0 comments on commit 6ef5b40

Please sign in to comment.