Skip to content

Commit

Permalink
feat: add ability to clawback tokens via FT manage (#4)
Browse files Browse the repository at this point in the history
* feat: add ability to clawback tokens via FT manage
* feat: update coreum-js library
  • Loading branch information
akhlopiachyi authored Nov 18, 2024
1 parent 28095a4 commit 6e416de
Show file tree
Hide file tree
Showing 15 changed files with 1,913 additions and 6 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"big.js": "^6.2.1",
"chain-registry": "1.33.6",
"classnames": "^2.5.1",
"coreum-js": "^2.13.0",
"coreum-js": "^2.14.0",
"graz": "^0.1.11",
"next": "14.2.2",
"pino-pretty": "^11.0.0",
Expand Down
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;
Loading

0 comments on commit 6e416de

Please sign in to comment.