|
| 1 | +import React, { useMemo, useState } from "react"; |
| 2 | +import styled from "styled-components"; |
| 3 | + |
| 4 | +import { useParams } from "react-router-dom"; |
| 5 | +import { useDebounce } from "react-use"; |
| 6 | +import { useAccount, useBalance, usePublicClient } from "wagmi"; |
| 7 | +import { Field, Button } from "@kleros/ui-components-library"; |
| 8 | + |
| 9 | +import { REFETCH_INTERVAL } from "consts/index"; |
| 10 | +import { useSimulateDisputeKitShutterFundAppeal, useWriteDisputeKitShutterFundAppeal } from "hooks/contracts/generated"; |
| 11 | +import { useSelectedOptionContext, useFundingContext, useCountdownContext } from "hooks/useClassicAppealContext"; |
| 12 | +import { useParsedAmount } from "hooks/useParsedAmount"; |
| 13 | + |
| 14 | +import { isUndefined } from "utils/index"; |
| 15 | +import { wrapWithToast } from "utils/wrapWithToast"; |
| 16 | + |
| 17 | +import { EnsureChain } from "components/EnsureChain"; |
| 18 | +import { ErrorButtonMessage } from "components/ErrorButtonMessage"; |
| 19 | +import ClosedCircleIcon from "components/StyledIcons/ClosedCircleIcon"; |
| 20 | + |
| 21 | +const Container = styled.div` |
| 22 | + display: flex; |
| 23 | + flex-direction: column; |
| 24 | + align-items: center; |
| 25 | + gap: 8px; |
| 26 | +`; |
| 27 | + |
| 28 | +const StyledField = styled(Field)` |
| 29 | + width: 100%; |
| 30 | + & > input { |
| 31 | + text-align: center; |
| 32 | + } |
| 33 | + &:before { |
| 34 | + position: absolute; |
| 35 | + content: "ETH"; |
| 36 | + right: 32px; |
| 37 | + top: 50%; |
| 38 | + transform: translateY(-50%); |
| 39 | + color: ${({ theme }) => theme.primaryText}; |
| 40 | + } |
| 41 | +`; |
| 42 | + |
| 43 | +const StyledButton = styled(Button)` |
| 44 | + margin: auto; |
| 45 | + margin-top: 4px; |
| 46 | +`; |
| 47 | + |
| 48 | +const StyledLabel = styled.label` |
| 49 | + align-self: flex-start; |
| 50 | +`; |
| 51 | + |
| 52 | +const useNeedFund = () => { |
| 53 | + const { loserSideCountdown } = useCountdownContext(); |
| 54 | + const { fundedChoices, winningChoice } = useFundingContext(); |
| 55 | + return ( |
| 56 | + (loserSideCountdown ?? 0) > 0 || |
| 57 | + (!isUndefined(fundedChoices) && |
| 58 | + !isUndefined(winningChoice) && |
| 59 | + fundedChoices.length > 0 && |
| 60 | + !fundedChoices.includes(winningChoice)) |
| 61 | + ); |
| 62 | +}; |
| 63 | + |
| 64 | +const useFundAppeal = (parsedAmount: bigint, insufficientBalance: boolean) => { |
| 65 | + const { id } = useParams(); |
| 66 | + const { selectedOption } = useSelectedOptionContext(); |
| 67 | + const { |
| 68 | + data: fundAppealConfig, |
| 69 | + isLoading, |
| 70 | + isError, |
| 71 | + } = useSimulateDisputeKitShutterFundAppeal({ |
| 72 | + query: { enabled: !isUndefined(id) && !isUndefined(selectedOption) && !insufficientBalance }, |
| 73 | + args: [BigInt(id ?? 0), BigInt(selectedOption?.id ?? 0)], |
| 74 | + value: parsedAmount, |
| 75 | + }); |
| 76 | + const { writeContractAsync: fundAppeal } = useWriteDisputeKitShutterFundAppeal(); |
| 77 | + return { fundAppeal, fundAppealConfig, isLoading, isError }; |
| 78 | +}; |
| 79 | + |
| 80 | +interface IFund { |
| 81 | + amount: `${number}`; |
| 82 | + setAmount: (val: string) => void; |
| 83 | + setIsOpen: (val: boolean) => void; |
| 84 | +} |
| 85 | + |
| 86 | +const Fund: React.FC<IFund> = ({ amount, setAmount, setIsOpen }) => { |
| 87 | + const needFund = useNeedFund(); |
| 88 | + const { address, isDisconnected } = useAccount(); |
| 89 | + const { data: balance } = useBalance({ |
| 90 | + query: { refetchInterval: REFETCH_INTERVAL }, |
| 91 | + address, |
| 92 | + }); |
| 93 | + const publicClient = usePublicClient(); |
| 94 | + const [isSending, setIsSending] = useState(false); |
| 95 | + const [debouncedAmount, setDebouncedAmount] = useState<`${number}` | "">(""); |
| 96 | + useDebounce(() => setDebouncedAmount(amount), 500, [amount]); |
| 97 | + const parsedAmount = useParsedAmount(debouncedAmount as `${number}`); |
| 98 | + const insufficientBalance = useMemo(() => balance && balance.value < parsedAmount, [balance, parsedAmount]); |
| 99 | + const { fundAppealConfig, fundAppeal, isLoading, isError } = useFundAppeal(parsedAmount, insufficientBalance); |
| 100 | + const isFundDisabled = useMemo( |
| 101 | + () => |
| 102 | + isDisconnected || |
| 103 | + isSending || |
| 104 | + !balance || |
| 105 | + insufficientBalance || |
| 106 | + Number(parsedAmount) <= 0 || |
| 107 | + isError || |
| 108 | + isLoading, |
| 109 | + [isDisconnected, isSending, balance, insufficientBalance, parsedAmount, isError, isLoading] |
| 110 | + ); |
| 111 | + |
| 112 | + return needFund ? ( |
| 113 | + <Container> |
| 114 | + <StyledLabel>How much ETH do you want to contribute?</StyledLabel> |
| 115 | + <StyledField |
| 116 | + type="number" |
| 117 | + value={amount} |
| 118 | + onChange={(e) => setAmount(e.target.value)} |
| 119 | + placeholder="Amount to fund" |
| 120 | + /> |
| 121 | + <EnsureChain> |
| 122 | + <div> |
| 123 | + <StyledButton |
| 124 | + disabled={isFundDisabled} |
| 125 | + isLoading={(isSending || isLoading) && !insufficientBalance} |
| 126 | + text={isDisconnected ? "Connect to Fund" : "Fund"} |
| 127 | + onClick={() => { |
| 128 | + if (fundAppeal && fundAppealConfig && publicClient) { |
| 129 | + setIsSending(true); |
| 130 | + wrapWithToast(async () => await fundAppeal(fundAppealConfig.request), publicClient) |
| 131 | + .then((res) => setIsOpen(res.status)) |
| 132 | + .finally(() => setIsSending(false)); |
| 133 | + } |
| 134 | + }} |
| 135 | + /> |
| 136 | + {insufficientBalance && ( |
| 137 | + <ErrorButtonMessage> |
| 138 | + <ClosedCircleIcon /> Insufficient balance |
| 139 | + </ErrorButtonMessage> |
| 140 | + )} |
| 141 | + </div> |
| 142 | + </EnsureChain> |
| 143 | + </Container> |
| 144 | + ) : null; |
| 145 | +}; |
| 146 | + |
| 147 | +export default Fund; |
0 commit comments