Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/transfer fraction button #1203

Merged
merged 4 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading