Skip to content

Commit

Permalink
feat(token reward): transfer pool ownership (#1542)
Browse files Browse the repository at this point in the history
* feat(TokenRewardCardEditMenu): add "Copy pool ID" button

* feat(token reward): transfer pool ownership

* fix(EditTokenModal): edit snapshot requirement properly

* fix(ClaimTokenButton): get `guildPlatform` from `TokenRewardContext`
  • Loading branch information
BrickheadJohnny authored Nov 11, 2024
1 parent 9e5db04 commit 02ca324
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 13 deletions.
16 changes: 10 additions & 6 deletions src/rewards/Token/ClaimTokenButton.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ButtonProps, Tooltip, useDisclosure } from "@chakra-ui/react"
import { useRolePlatform } from "components/[guild]/RolePlatforms/components/RolePlatformProvider"
import dynamic from "next/dynamic"
import { claimTextButtonTooltipLabel } from "rewards/SecretText/TextCardButton"
import { RewardCardButton } from "rewards/components/RewardCardButton"
Expand All @@ -12,25 +11,30 @@ import {
GeogatedCountryPopover,
useIsFromGeogatedCountry,
} from "./GeogatedCountryAlert"
import { TokenRewardProvider } from "./TokenRewardContext"
import { TokenRewardProvider, useTokenRewardContext } from "./TokenRewardContext"

type Props = {
isDisabled?: boolean
rolePlatform: RolePlatform
rolePlatform: RolePlatform // TODO: decide if it should be a prop or we should get it from context!
} & ButtonProps

const DynamicClaimTokenModal = dynamic(() => import("./ClaimTokenModal"))

const ClaimTokenButton = ({ isDisabled, children, ...rest }: Props) => {
const rolePlatform = useRolePlatform()
const ClaimTokenButton = ({
isDisabled,
rolePlatform,
children,
...rest
}: Props) => {
const { guildPlatform } = useTokenRewardContext()

const { isOpen, onOpen, onClose } = useDisclosure()
const isFromGeogatedCountry = useIsFromGeogatedCountry()

const { isAvailable } = getRolePlatformTimeframeInfo(rolePlatform)

return (
<TokenRewardProvider guildPlatform={rolePlatform.guildPlatform}>
<TokenRewardProvider guildPlatform={guildPlatform}>
<GeogatedCountryPopover isDisabled={!isFromGeogatedCountry}>
<Tooltip
isDisabled={!isAvailable}
Expand Down
2 changes: 1 addition & 1 deletion src/rewards/Token/EditTokenModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ const EditTokenModal = ({
if (!!snapshotRequirement && changeSnapshot) {
await submitEditRequirement({
...snapshotRequirement,
...data.snapshotRequirement,
id: snapshotRequirement?.id!, // just to make TS happy...
data: data?.data?.guildPlatformId ? data.data : snapshotRequirement?.data,
})
}

Expand Down
74 changes: 68 additions & 6 deletions src/rewards/Token/TokenRewardCardEditMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"
import { useDisclosure } from "@/hooks/useDisclosure"
import { MenuDivider, MenuItem, useColorModeValue } from "@chakra-ui/react"
import {
MenuDivider,
MenuItem,
useColorModeValue,
useDisclosure,
} from "@chakra-ui/react"
import { Coin, Pencil, TrashSimple, Wallet } from "@phosphor-icons/react"
ArrowsLeftRight,
Check,
Coin,
Copy,
Pencil,
TrashSimple,
Wallet,
} from "@phosphor-icons/react"
import EditRewardAvailabilityMenuItem from "components/[guild]/AccessHub/components/EditRewardAvailabilityMenuItem"
import PlatformCardMenu from "components/[guild]/RolePlatforms/components/PlatformCard/components/PlatformCardMenu"
import useToast from "hooks/useToast"
import dynamic from "next/dynamic"
import { useState } from "react"
import { GuildPlatform } from "types"
import { ERC20_SUPPORTED_CHAINS } from "utils/guildCheckout/constants"
import { useAccount } from "wagmi"
import EditTokenModal from "./EditTokenModal"
import FundPoolModal from "./FundPoolModal"
import RemoveTokenRewardConfirmation from "./RemoveTokenRewardConfirmation"
import WithdrawPoolModal from "./WithdrawPoolModal"
import usePool from "./hooks/usePool"

const DynamicTransferPoolOwnershipDialog = dynamic(() =>
import("./TransferPoolOwnershipDialog").then(
(module) => module.TransferPoolOwnershipDialog
)
)

const TokenRewardCardEditMenu = ({
guildPlatform,
Expand All @@ -37,6 +53,14 @@ const TokenRewardCardEditMenu = ({
onClose: editOnClose,
} = useDisclosure()

const {
onOpen: transferOwnershipOnOpen,
isOpen: transferOwnershipIsOpen,
setValue: transferOwnershipSetValue,
} = useDisclosure()

const { copyToClipboard, hasCopied } = useCopyToClipboard()

const {
isOpen: deleteIsOpen,
onOpen: deleteOnOpen,
Expand All @@ -47,6 +71,18 @@ const TokenRewardCardEditMenu = ({

const removeColor = useColorModeValue("red.600", "red.300")

const { address } = useAccount()
const { data } = usePool(
// TODO: should we use `guildPlatform.platformGuildData.contractAddress` here instead?
guildPlatform.platformGuildData
?.chain as (typeof ERC20_SUPPORTED_CHAINS)[number],
BigInt(guildPlatform.platformGuildData?.poolId ?? "0") // We'll never use this fallback, since poolId is defined at this point
)

const isPoolOwner = data?.owner?.toLowerCase() === address?.toLowerCase()

const [shouldHideTransferButton, setShouldHideTransferButton] = useState(false)

return (
<>
<PlatformCardMenu>
Expand All @@ -63,6 +99,21 @@ const TokenRewardCardEditMenu = ({
<MenuItem icon={<Wallet />} onClick={withdrawOnOpen}>
Withdraw from pool
</MenuItem>
{isPoolOwner && !shouldHideTransferButton && (
<MenuItem icon={<ArrowsLeftRight />} onClick={transferOwnershipOnOpen}>
Transfer pool ownership
</MenuItem>
)}
<MenuItem
icon={hasCopied ? <Check /> : <Copy />}
onClick={() =>
copyToClipboard(
guildPlatform.platformGuildData?.poolId?.toString() ?? "Unknown pool"
)
}
>
Copy pool ID
</MenuItem>
<MenuDivider />
<MenuItem icon={<TrashSimple />} onClick={deleteOnOpen} color={removeColor}>
Remove reward
Expand Down Expand Up @@ -95,6 +146,17 @@ const TokenRewardCardEditMenu = ({
/>

<EditTokenModal onClose={editOnClose} isOpen={editIsOpen} />

{isPoolOwner && (
<DynamicTransferPoolOwnershipDialog
open={transferOwnershipIsOpen}
onOpenChange={transferOwnershipSetValue}
/**
* The proper way of doing this would be to wait for the TX receipt when transferring ownership, then mutate `usePool`'s data, but this will work too for now
*/
onSuccess={() => setShouldHideTransferButton(true)}
/>
)}
</>
)
}
Expand Down
137 changes: 137 additions & 0 deletions src/rewards/Token/TransferPoolOwnershipDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Button } from "@/components/ui/Button"
import {
Dialog,
DialogBody,
DialogCloseButton,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/Dialog"
import {
FormControl,
FormErrorMessage,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/Form"
import { Input } from "@/components/ui/Input"
import { useErrorToast } from "@/components/ui/hooks/useErrorToast"
import { useToast } from "@/components/ui/hooks/useToast"
import { zodResolver } from "@hookform/resolvers/zod"
import { useRolePlatform } from "components/[guild]/RolePlatforms/components/RolePlatformProvider"
import { Dispatch, SetStateAction } from "react"
import { FormProvider, useForm } from "react-hook-form"
import tokenRewardPoolAbi from "static/abis/tokenRewardPool"
import {
ERC20_CONTRACTS,
ERC20_SUPPORTED_CHAINS,
} from "utils/guildCheckout/constants"
import processViemContractError from "utils/processViemContractError"
import { WriteContractParameters } from "viem"
import { useWriteContract } from "wagmi"
import { z } from "zod"

type Props = {
open: boolean
onOpenChange: Dispatch<SetStateAction<boolean>>
onSuccess: () => void
}

const TransferPoolOwnershipFormSchema = z.object({
transferPoolOwnershipTo: z
.string()
.regex(/^0x[0-9a-f]{40}$/i, "Invalid EVM address")
.transform((str) => str as `0x${string}`),
})

const TransferPoolOwnershipDialog = ({ open, onOpenChange, onSuccess }: Props) => {
const { guildPlatform } = useRolePlatform()

const form = useForm<z.infer<typeof TransferPoolOwnershipFormSchema>>({
resolver: zodResolver(TransferPoolOwnershipFormSchema),
defaultValues: {
transferPoolOwnershipTo: "" as `0x${string}`,
},
})

const { writeContract, isPending } = useWriteContract()

const { toast } = useToast()
const errorToast = useErrorToast()

return (
<Dialog open={open} onOpenChange={(open) => onOpenChange(open)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Transfer pool ownership</DialogTitle>
</DialogHeader>

<DialogBody>
<FormProvider {...form}>
<FormField
control={form.control}
name="transferPoolOwnershipTo"
render={({ field }) => (
<FormItem>
<FormLabel>New owner's EVM address:</FormLabel>
<FormControl>
<Input placeholder="0x..." {...field} />
</FormControl>
<FormErrorMessage />
</FormItem>
)}
/>
</FormProvider>
</DialogBody>

<DialogFooter>
<Button
colorScheme="destructive"
isLoading={isPending}
loadingText="Transferring ownership"
onClick={form.handleSubmit((data) =>
writeContract(
{
abi: tokenRewardPoolAbi,
// TODO: should we use `guildPlatform.platformGuildData.contractAddress` here instead?
address:
ERC20_CONTRACTS[
guildPlatform.platformGuildData
.chain as (typeof ERC20_SUPPORTED_CHAINS)[number]
],
functionName: "transferPoolOwnership",
args: [
BigInt(guildPlatform.platformGuildData.poolId),
data.transferPoolOwnershipTo,
],
// This looks pretty strange, but we need it until we don't switch to strictNullChecks: true...
} as any satisfies WriteContractParameters<
typeof tokenRewardPoolAbi
>,
{
onSuccess: () => {
toast({
variant: "success",
title: "Successfully transferred pool ownership",
})
onOpenChange(false)
onSuccess()
},
onError: (error) =>
errorToast(processViemContractError(error) ?? "Unknown error"),
}
)
)}
>
Transfer ownership
</Button>
</DialogFooter>

<DialogCloseButton />
</DialogContent>
</Dialog>
)
}

export { TransferPoolOwnershipDialog }

0 comments on commit 02ca324

Please sign in to comment.