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

feat: add ability to clawback tokens via FT manage #4

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
1,628 changes: 1,628 additions & 0 deletions public/images/modal/clawback-bg.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/assets/ConfirmationModalImage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const ConfirmationModalImage: FC<ConfirmationModalProps> = ({
return <Image priority={false} className="w-full max-w-full" src="/images/modal/success-bg.svg" width="480" height="180" alt="mint" />;
case ConfirmationModalImageType.Burn:
return <Image priority={false} className="w-full max-w-full" src="/images/modal/burn-bg.svg" width="480" height="180" alt="mint" />;
case ConfirmationModalImageType.Clawback:
return <Image priority={false} className="w-full max-w-full" src="/images/modal/clawback-bg.svg" width="480" height="180" alt="mint" />;
default:
return null
}
Expand Down
65 changes: 65 additions & 0 deletions src/components/ClawbackTokens/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { GeneralIcon } from "@/assets/GeneralIcon";
import { GeneralIconType, ButtonType, Token } from "@/shared/types";
import { FC } from "react";
import { Button } from "../Button";
import { Input } from "../Input";
import { pasteValueFromClipboard } from "@/helpers/pasteValueFromClipboard";

interface ClawbackTokensProps {
selectedCurrency: Token | null;
clawbackAmount: string;
setClawbackAmount: (value: string) => void;
walletAddress: string;
setWalletAddress: (value: string) => void;
handleClawbackTokens: () => void;
walletAddressValidationError: string;
}

export const ClawbackTokens: FC<ClawbackTokensProps> = ({
selectedCurrency,
clawbackAmount,
setClawbackAmount,
walletAddress,
setWalletAddress,
handleClawbackTokens,
walletAddressValidationError,
}) => {
return (
<div className="flex flex-col w-full gap-8">
<Input
label="Account Address"
value={walletAddress}
onChange={setWalletAddress}
placeholder="Enter wallet address"
buttonLabel={walletAddress.length ? '' : 'Paste'}
error={walletAddressValidationError}
handleOnButtonClick={() => !walletAddress.length && pasteValueFromClipboard(setWalletAddress)}
/>
<Input
label="Clawback Amount"
value={clawbackAmount}
onChange={setClawbackAmount}
placeholder="0"
type="number"
buttonLabel={(
<div className="flex items-center gap-1.5 text-[#EEE]">
<GeneralIcon type={GeneralIconType.DefaultToken} />
{selectedCurrency?.symbol.toUpperCase()}
</div>
)}
decimals={selectedCurrency?.precision || 0}
/>
<div className="flex w-full justify-end">
<div className="flex items-center">
<Button
label="Continue"
onClick={handleClawbackTokens}
type={ButtonType.Primary}
className="text-sm !py-2 px-6 rounded-[10px] font-semibold w-[160px]"
disabled={!clawbackAmount.length || +clawbackAmount === 0 || !walletAddress.length || !!walletAddressValidationError.length}
/>
</div>
</div>
</div>
);
};
141 changes: 141 additions & 0 deletions src/components/ConfirmClawbackModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { AlertType, ButtonType, ConfirmationModalImageType } from "@/shared/types";
import { ConfirmationModal } from "../ConfirmationModal";
import { ConfirmationModalImage } from "@/assets/ConfirmationModalImage";
import { Button } from "../Button";
import { useAppDispatch, useAppSelector } from "@/store/hooks";
import { useCallback, useMemo, useState } from "react";
import { setIsConfirmClawbackModalOpen, setIsTxExecuting } from "@/features/general/generalSlice";
import { convertUnitToSubunit } from "@/helpers/convertUnitToSubunit";
import { useEstimateTxGasFee } from "@/hooks/useEstimateTxGasFee";
import { FT } from "coreum-js";
import { setSelectedCurrency, shouldRefetchCurrencies } from "@/features/currencies/currenciesSlice";
import { ModalInfoRow } from "../ModalInfoRow";
import { dispatchAlert } from "@/features/alerts/alertsSlice";
import { shortenAddress } from "@/helpers/shortenAddress";
import { Decimal } from "../Decimal";
import { shouldRefetchBalances } from "@/features/balances/balancesSlice";
import { setClawbackAmount, setClawbackWalletAddress } from "@/features/clawback/clawbackSlice";

export const ConfirmClawbackModal = () => {
const isConfirmClawbakcModalOpen = useAppSelector(state => state.general.isConfirmClawbackModalOpen);
const clawbackAmount = useAppSelector(state => state.clawback.amount);
const walletAddress = useAppSelector(state => state.clawback.walletAddress);
const account = useAppSelector(state => state.general.account);
const selectedCurrency = useAppSelector(state => state.currencies.selectedCurrency);
const isTxExecuting = useAppSelector(state => state.general.isTxExecuting);

const [isTxSuccessful, setIsTxSuccessful] = useState<boolean>(false);

const dispatch = useAppDispatch();
const { signingClient, getTxFee } = useEstimateTxGasFee();

const handleClose = useCallback(() => {
dispatch(setClawbackAmount('0'));
dispatch(setClawbackWalletAddress(''));
dispatch(setIsConfirmClawbackModalOpen(false));
dispatch(setSelectedCurrency(null));
setIsTxSuccessful(false);
}, []);

const handleConfirm = useCallback(async () => {
dispatch(setIsTxExecuting(true));

try {
await new Promise(resolve => setTimeout(resolve, 2000));
const clawbackFTMsg = FT.Clawback({
sender: account,
account: walletAddress,
coin: {
denom: selectedCurrency!.denom,
amount: convertUnitToSubunit({
amount: clawbackAmount,
precision: selectedCurrency!.precision,
}),
},
});
const txFee = await getTxFee([clawbackFTMsg]);
await signingClient?.signAndBroadcast(account, [clawbackFTMsg], txFee ? txFee.fee : 'auto');
setIsTxSuccessful(true);
dispatch(shouldRefetchBalances(true));
dispatch(shouldRefetchCurrencies(true));
} catch (error) {
dispatch(dispatchAlert({
type: AlertType.Error,
title: 'Fungible Token Clawback Failed',
message: (error as { message: string}).message,
}));
}

dispatch(setIsTxExecuting(false));
}, [account, getTxFee, selectedCurrency, signingClient, walletAddress, clawbackAmount]);

const renderContent = useMemo(() => {
if (isTxSuccessful) {
return (
<div className="flex flex-col w-full p-8 gap-8">
<div className="flex flex-col text-center gap-6">
<div className="font-space-grotesk text-lg text-[#EEE] font-medium">
Successfully Clawback
</div>
<div className="flex flex-col items-center w-full gap-2">
<ModalInfoRow label="Wallet Address" value={shortenAddress(walletAddress)} />
<ModalInfoRow
label="Clawback Amount"
value={(
<div className="flex flex-wrap max-w-full gap-1 w-full items-baseline justify-end">
<Decimal className="break-all max-w-full !inline" value={clawbackAmount} precision={selectedCurrency?.precision || 0} />
<span className="text-left text-xs max-w-full break-all">{selectedCurrency?.symbol.toUpperCase()}</span>
</div>
)}
/>
</div>
</div>
<div className="flex items-center w-full">
<Button
label="Done"
onClick={handleClose}
type={ButtonType.Primary}
className="text-sm !py-2 px-6 rounded-[10px] font-semibold"
/>
</div>
</div>
);
}

return (
<div className="flex flex-col w-full p-8 gap-8">
<div className="flex flex-col text-center gap-2">
<div className="font-space-grotesk text-lg text-[#EEE] font-medium">
Clawback Account
</div>
<div className="font-noto-sans text-sm text-[#868991]">
This action will be applied and affect this targeted holder. Would you like to proceed?
</div>
</div>
<div className="grid grid-cols-2 gap-2">
<Button
label="Cancel"
onClick={handleClose}
type={ButtonType.Secondary}
className="text-sm !py-2 px-6 rounded-[10px] font-semibold w-[160px]"
/>
<Button
label="Confirm"
onClick={handleConfirm}
type={ButtonType.Primary}
className="text-sm !py-2 px-6 rounded-[10px] font-semibold w-[160px]"
loading={isTxExecuting}
disabled={isTxExecuting}
/>
</div>
</div>
);
}, [handleClose, handleConfirm, isTxExecuting, isTxSuccessful, selectedCurrency, walletAddress, clawbackAmount]);

return (
<ConfirmationModal isOpen={isConfirmClawbakcModalOpen}>
<ConfirmationModalImage type={isTxSuccessful ? ConfirmationModalImageType.Success : ConfirmationModalImageType.Clawback} />
{renderContent}
</ConfirmationModal>
);
};
2 changes: 2 additions & 0 deletions src/components/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { ViewNFTCollectionModal } from "../ViewNFTCollectionModal";
import { WhitelistNFTModal } from "../WhitelistNFTModal";
import { DisclaimerModal } from "../DisclaimerModal";
import { isBrowser } from "@/helpers/isBrowser";
import { ConfirmClawbackModal } from "../ConfirmClawbackModal";

interface LayoutProps {
children: React.ReactNode;
Expand Down Expand Up @@ -132,6 +133,7 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
<ConfirmUnfreezeModal />
<ConfirmGlobalUnfreezeModal />
<ConfirmWhitelistModal />
<ConfirmClawbackModal />
</>
);
default:
Expand Down
24 changes: 23 additions & 1 deletion src/components/ManageTokensModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { Modal } from "../Modal";
import { useAppDispatch, useAppSelector } from "@/store/hooks";
import { setSelectedCurrency } from "@/features/currencies/currenciesSlice";
import { setIsConfirmFreezeModalOpen, setIsConfirmGlobalFreezeModalOpen, setIsConfirmGlobalUnfreezeModalOpen, setIsConfirmMintModalOpen, setIsConfirmUnfreezeModalOpen, setIsConfirmWhitelistModalOpen, setIsManageCurrencyModalOpen } from "@/features/general/generalSlice";
import { setIsConfirmClawbackModalOpen, setIsConfirmFreezeModalOpen, setIsConfirmGlobalFreezeModalOpen, setIsConfirmGlobalUnfreezeModalOpen, setIsConfirmMintModalOpen, setIsConfirmUnfreezeModalOpen, setIsConfirmWhitelistModalOpen, setIsManageCurrencyModalOpen } from "@/features/general/generalSlice";
import { ChainInfo, TabItem, TabItemType } from "@/shared/types";
import { Tabs } from "../Tabs";
import { MintTokens } from "../MintTokens";
Expand All @@ -17,6 +17,8 @@ import { setUnfreezeAmount, setUnfreezeWalletAddress } from "@/features/unfreeze
import { setWhitelistAmount, setWhitelistWalletAddress } from "@/features/whitelist/whitelistSlice";
import { getManageFTTabs } from "@/helpers/getManageFtTabs";
import { validateAddress } from "@/helpers/validateAddress";
import { ClawbackTokens } from "../ClawbackTokens";
import { setClawbackAmount, setClawbackWalletAddress } from "@/features/clawback/clawbackSlice";

export const ManageTokensModal = () => {
const selectedCurrency = useAppSelector(state => state.currencies.selectedCurrency);
Expand Down Expand Up @@ -104,6 +106,14 @@ export const ManageTokensModal = () => {
handleClearState();
}, [amount, walletAddress]);

const handleClawbackTokens = useCallback(() => {
dispatch(setClawbackAmount(amount));
dispatch(setClawbackWalletAddress(walletAddress));
dispatch(setIsManageCurrencyModalOpen(false));
dispatch(setIsConfirmClawbackModalOpen(true));
handleClearState();
}, [amount, walletAddress]);

const renderTitle = useMemo(() => {
if (!manageFtTokensTabs.length) {
return null;
Expand Down Expand Up @@ -185,6 +195,18 @@ export const ManageTokensModal = () => {
walletAddressValidationError={walletAddressValidationError}
/>
);
case TabItemType.Clawback:
return (
<ClawbackTokens
selectedCurrency={selectedCurrency}
clawbackAmount={amount}
setClawbackAmount={setAmount}
walletAddress={walletAddress}
setWalletAddress={setWalletAddress}
handleClawbackTokens={handleClawbackTokens}
walletAddressValidationError={walletAddressValidationError}
/>
);
default:
}
}, [amount, selectedCurrency, selectedTab, walletAddress, walletAddressValidationError]);
Expand Down
3 changes: 3 additions & 0 deletions src/components/ModalsWrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const ModalsHandler = () => {
const isConfirmUnfreezeModalOpen = useAppSelector(state => state.general.isConfirmUnfreezeModalOpen);
const isConfirmGlobalUnfreezeModalOpen = useAppSelector(state => state.general.isConfirmGlobalUnfreezeModalOpen);
const isConfirmWhitelistModalOpen = useAppSelector(state => state.general.isConfirmWhitelistModalOpen);
const isConfirmClawbackModalOpen = useAppSelector(state => state.general.isConfirmClawbackModalOpen);
const isConfirmBurnModalOpen = useAppSelector(state => state.general.isConfirmBurnModalOpen);
const isSelectNFTModalOpen = useAppSelector(state => state.general.isSelectNFTModalOpen);
const isNFTCollectionViewModalOpen = useAppSelector(state => state.general.isNFTCollectionViewModalOpen);
Expand Down Expand Up @@ -52,6 +53,7 @@ export const ModalsHandler = () => {
|| isConfirmUnfreezeModalOpen
|| isConfirmGlobalUnfreezeModalOpen
|| isConfirmWhitelistModalOpen
|| isConfirmClawbackModalOpen
|| isConfirmBurnModalOpen
|| isSelectNFTModalOpen
|| isNFTCollectionViewModalOpen
Expand Down Expand Up @@ -80,6 +82,7 @@ export const ModalsHandler = () => {
isConfirmMintModalOpen,
isConfirmNFTBurnModalOpen,
isConfirmNFTDeWhitelistModalOpen,
isConfirmClawbackModalOpen,
isConfirmNFTFreezeModalOpen,
isConfirmNFTMintModalOpen,
isConfirmNFTUnfreezeModalOpen,
Expand Down
4 changes: 4 additions & 0 deletions src/constants/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ export const MANAGE_FT_TOKENS_TABS = {
'whitelisting': {
id: TabItemType.Whitelist,
label: 'Whitelist',
},
'clawback': {
id: TabItemType.Clawback,
label: 'Clawback',
}
};

Expand Down
28 changes: 28 additions & 0 deletions src/features/clawback/clawbackSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

export interface ClawbackState {
amount: string;
walletAddress: string;
}

export const initialClawbackState: ClawbackState = {
amount: '0',
walletAddress: '',
};

const clawbackSlice = createSlice({
name: 'clawback',
initialState: initialClawbackState,
reducers: {
setClawbackAmount(state, action: PayloadAction<string>) {
state.amount = action.payload;
},
setClawbackWalletAddress(state, action: PayloadAction<string>) {
state.walletAddress = action.payload;
},
},
});

export const { setClawbackAmount, setClawbackWalletAddress } = clawbackSlice.actions;
export const clawbackReducer = clawbackSlice.reducer;
export default clawbackSlice.reducer;
6 changes: 6 additions & 0 deletions src/features/general/generalSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface GeneralState {
isConfirmUnfreezeModalOpen: boolean;
isConfirmGlobalUnfreezeModalOpen: boolean;
isConfirmWhitelistModalOpen: boolean;
isConfirmClawbackModalOpen: boolean;
isConfirmBurnModalOpen: boolean;
isSelectNFTModalOpen: boolean;
isNFTCollectionViewModalOpen: boolean;
Expand Down Expand Up @@ -57,6 +58,7 @@ export const initialGeneralState: GeneralState = {
isConfirmUnfreezeModalOpen: false,
isConfirmGlobalUnfreezeModalOpen: false,
isConfirmWhitelistModalOpen: false,
isConfirmClawbackModalOpen: false,
isConfirmBurnModalOpen: false,
isSelectNFTModalOpen: false,
isNFTCollectionViewModalOpen: false,
Expand Down Expand Up @@ -129,6 +131,9 @@ const generalSlice = createSlice({
setIsConfirmWhitelistModalOpen(state, action: PayloadAction<boolean>) {
state.isConfirmWhitelistModalOpen = action.payload;
},
setIsConfirmClawbackModalOpen(state, action: PayloadAction<boolean>) {
state.isConfirmClawbackModalOpen = action.payload;
},
setIsConfirmBurnModalOpen(state, action: PayloadAction<boolean>) {
state.isConfirmBurnModalOpen = action.payload;
},
Expand Down Expand Up @@ -201,6 +206,7 @@ export const {
setIsConfirmUnfreezeModalOpen,
setIsConfirmGlobalUnfreezeModalOpen,
setIsConfirmWhitelistModalOpen,
setIsConfirmClawbackModalOpen,
setIsConfirmBurnModalOpen,
setIsSelectNFTModalOpen,
setIsNFTCollectionViewModalOpen,
Expand Down
2 changes: 2 additions & 0 deletions src/helpers/getManageFtTabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const getManageFTTabs = (currency: Token | null) => {
switch (feature) {
case 'minting':
case 'whitelisting':
case 'clawback':
resultTabs.push(MANAGE_FT_TOKENS_TABS[feature]);
break;
case 'freezing':
Expand Down Expand Up @@ -44,6 +45,7 @@ export const getFTCurrencyOptions = (currency: Token) => {
case 'minting':
case 'freezing':
case 'whitelisting':
case 'clawback':
isTokenManageable = true;
case 'burning':
isBurnEnabled = true;
Expand Down
Loading