diff --git a/frontend/src/app/(routes)/multisig/components/DialogUpdateSequence.tsx b/frontend/src/app/(routes)/multisig/components/DialogUpdateSequence.tsx new file mode 100644 index 000000000..b197f6403 --- /dev/null +++ b/frontend/src/app/(routes)/multisig/components/DialogUpdateSequence.tsx @@ -0,0 +1,78 @@ +import CustomButton from '@/components/common/CustomButton'; +import { Dialog, DialogContent } from '@mui/material'; +import Image from 'next/image'; +import React from 'react'; + +const DialogUpdateSequence = ({ + open, + onClose, + onUpdateSequence, + loading, +}: { + open: boolean; + onClose: () => void; + onUpdateSequence: () => void; + loading: boolean; +}) => { + return ( + + + + + + + + + + + ! + + + + Transaction Sequence Outdated + + Transaction sequence is outdated. To broadcast this + transaction, the sequence number needs to be updated. + + + Would you like to update? + + + + + + After this action all the signers will be required to re-sign. + + + + + + + + ); +}; + +export default DialogUpdateSequence; diff --git a/frontend/src/app/(routes)/multisig/components/common/BroadCastTxn.tsx b/frontend/src/app/(routes)/multisig/components/common/BroadCastTxn.tsx index 77d573500..6ac2cb36c 100644 --- a/frontend/src/app/(routes)/multisig/components/common/BroadCastTxn.tsx +++ b/frontend/src/app/(routes)/multisig/components/common/BroadCastTxn.tsx @@ -3,20 +3,28 @@ import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; import { setError } from '@/store/features/common/commonSlice'; import { broadcastTransaction, + resetUpdateTxnSequences, resetUpdateTxnState, setVerifyDialogOpen, + updateTxnSequences, } from '@/store/features/multisig/multisigSlice'; import { RootState } from '@/store/store'; import { MultisigAddressPubkey, Txn } from '@/types/multisig'; import React, { useEffect, useState } from 'react'; -import { FAILED_TO_BROADCAST_ERROR } from '@/utils/errors'; +import { + CANNOT_BROADCAST_ERROR, + FAILED_TO_BROADCAST_ERROR, + FAILED_TO_BROADCAST_TRY_AGAIN, + FAILED_TO_UPDATE_SEQUENCE, + UPDATED_SEQUENCE_SUCCESSFULLY, +} from '@/utils/errors'; import useVerifyAccount from '@/custom-hooks/useVerifyAccount'; import CustomButton from '@/components/common/CustomButton'; import { useRouter } from 'next/navigation'; -import CustomDialog from '@/components/common/CustomDialog'; -import { Dialog, DialogContent } from '@mui/material'; -import Image from 'next/image'; -import { DELETE_ILLUSTRATION } from '@/constants/image-names'; +import DialogUpdateSequence from '../DialogUpdateSequence'; +import { getAuthToken } from '@/utils/localStorage'; +import { COSMOS_CHAIN_ID } from '@/utils/constants'; +import { TxStatus } from '@/types/enums'; interface BroadCastTxnProps { txn: Txn; @@ -25,12 +33,12 @@ interface BroadCastTxnProps { pubKeys: MultisigAddressPubkey[]; chainID: string; isMember: boolean; - disableBroadcast?: boolean; isOverview?: boolean; broadcastInfo?: { disable: boolean; isSequenceLess: boolean; isSequenceGreater: boolean; + isSequenceAvailable: boolean; }; } @@ -42,7 +50,6 @@ const BroadCastTxn: React.FC = (props) => { threshold, chainID, isMember, - disableBroadcast, isOverview, broadcastInfo, } = props; @@ -59,9 +66,15 @@ const BroadCastTxn: React.FC = (props) => { }); const router = useRouter(); + const [seqNotSyncOpen, setSeqNotSyncOpen] = useState(false); const updateTxnRes = useAppSelector( (state: RootState) => state.multisig.updateTxnRes ); + const updateTxnSequencesStatus = useAppSelector( + (state) => state.multisig.updateTxnSequences + ); + + const authToken = getAuthToken(COSMOS_CHAIN_ID); useEffect(() => { if (updateTxnRes.status === 'rejected') { @@ -81,10 +94,6 @@ const BroadCastTxn: React.FC = (props) => { }, []); const broadcastTxn = async () => { - if (!isAccountVerified()) { - dispatch(setVerifyDialogOpen(true)); - return; - } dispatch( broadcastTransaction({ chainID, @@ -98,26 +107,65 @@ const BroadCastTxn: React.FC = (props) => { }) ); }; - const [pendingSeqOpen, setPendingSeqOpen] = useState(false); - const [seqNotSyncOpen, setSeqNotSyncOpen] = useState(false); + const handleBroadcast = () => { + if (!isAccountVerified()) { + dispatch(setVerifyDialogOpen(true)); + return; + } if (isOverview) { router.push(`/multisig/${chainName}/${multisigAddress}`); } else if (broadcastInfo) { if (broadcastInfo.isSequenceLess) { - // alert('Sequence is not in sync'); setSeqNotSyncOpen(true); } else if (broadcastInfo.isSequenceGreater) { - setPendingSeqOpen(true); - setSeqNotSyncOpen(true); - // alert( - // 'There is a transaction that needs to be broadcasted before this' - // ); + dispatch(setError({ type: 'error', message: CANNOT_BROADCAST_ERROR })); + } else if (!broadcastInfo.isSequenceAvailable) { + // TODO: Sequence number is not available is the txn is signed before adding txn_Sequence into db + // This needs to be handled + dispatch( + setError({ type: 'error', message: "Seqeunce not found" }) + ); } else if (!broadcastInfo.disable) { broadcastTxn(); + } else { + dispatch( + setError({ type: 'error', message: FAILED_TO_BROADCAST_TRY_AGAIN }) + ); } } }; + + const handleUpdateSequence = () => { + dispatch( + updateTxnSequences({ + data: { address: multisigAddress }, + queryParams: { + address: walletAddress, + signature: authToken?.signature || '', + }, + }) + ); + }; + + useEffect(() => { + if (updateTxnSequencesStatus.status === TxStatus.IDLE) { + dispatch( + setError({ type: 'success', message: UPDATED_SEQUENCE_SUCCESSFULLY }) + ); + dispatch(resetUpdateTxnSequences()); + setSeqNotSyncOpen(false); + } + if (updateTxnSequencesStatus.status === TxStatus.REJECTED) { + dispatch( + setError({ + type: 'error', + message: updateTxnSequencesStatus?.error || FAILED_TO_UPDATE_SEQUENCE, + }) + ); + } + }, [updateTxnSequencesStatus]); + return ( <> = (props) => { btnDisabled={!isMember} btnStyles="w-[115px]" /> - setSeqNotSyncOpen(false)} - onUpdateSequence={() => console.log('update')} + onUpdateSequence={handleUpdateSequence} + loading={updateTxnSequencesStatus.status === TxStatus.PENDING} /> > ); }; export default BroadCastTxn; - -const DialogSequenceMissMatch = ({ - open, - onClose, - onUpdateSequence, -}: { - open: boolean; - onClose: () => void; - onUpdateSequence: () => void; -}) => { - return ( - - - - - - - - - - - ! - - - - Sequence Outdated - - Transaction sequence is outdated. To broadcast this - transaction, the sequence number needs to be updated. - - - Would you like to update? - - - - - - After this action all the signers will be required to re-sign. - - - - - - - - ); -}; diff --git a/frontend/src/app/(routes)/multisig/components/common/SignTxn.tsx b/frontend/src/app/(routes)/multisig/components/common/SignTxn.tsx index c17fb9959..fd85194f4 100644 --- a/frontend/src/app/(routes)/multisig/components/common/SignTxn.tsx +++ b/frontend/src/app/(routes)/multisig/components/common/SignTxn.tsx @@ -16,11 +16,16 @@ interface SignTxnProps { unSignedTxn: Txn; isMember: boolean; chainID: string; + isSigned: boolean; isOverview?: boolean; } +// TODO: Sequence number is not available is the txn is signed before adding txn_Sequence into db +// This needs to be handled + const SignTxn: React.FC = (props) => { - const { address, isMember, unSignedTxn, chainID, isOverview } = props; + const { address, isMember, unSignedTxn, chainID, isOverview, isSigned } = + props; const dispatch = useAppDispatch(); const { getChainInfo } = useGetChainInfo(); const { address: walletAddress, rpcURLs, chainName } = getChainInfo(chainID); @@ -69,14 +74,14 @@ const SignTxn: React.FC = (props) => { rpcURLs, txnSequence: unSignedTxn.txn_sequence, toBeBroadcastedCount, - partiallySignedCount: partiallySigned?.length + partiallySignedCount: partiallySigned?.length, }) ); }; return ( { if (isOverview) { diff --git a/frontend/src/app/(routes)/multisig/components/common/TxnsCard.tsx b/frontend/src/app/(routes)/multisig/components/common/TxnsCard.tsx index 162e61e99..8f309e58f 100644 --- a/frontend/src/app/(routes)/multisig/components/common/TxnsCard.tsx +++ b/frontend/src/app/(routes)/multisig/components/common/TxnsCard.tsx @@ -30,6 +30,8 @@ import useVerifyAccount from '@/custom-hooks/useVerifyAccount'; import { fee } from '@/txns/execute'; import DialogRepeatTxn from '../DialogRepeatTxn'; import { getTimeDifferenceToFutureDate } from '@/utils/dataTime'; +import { setError } from '@/store/features/common/commonSlice'; +import { ADMIN_ONLY_ALLOWED } from '@/utils/errors'; export const TxnsCard = ({ txn, @@ -40,7 +42,6 @@ export const TxnsCard = ({ isHistory, onViewError, allowRepeat, - disableBroadcast, isOverview, broadcastInfo, }: { @@ -52,12 +53,12 @@ export const TxnsCard = ({ isHistory: boolean; onViewError?: (errMsg: string) => void; allowRepeat?: boolean; - disableBroadcast?: boolean; isOverview?: boolean; broadcastInfo?: { disable: boolean; isSequenceLess: boolean; isSequenceGreater: boolean; + isSequenceAvailable: boolean; }; }) => { const dispatch = useAppDispatch(); @@ -67,6 +68,11 @@ export const TxnsCard = ({ const { isAccountVerified } = useVerifyAccount({ address: walletAddress, }); + const multisigAccount = useAppSelector( + (state) => state.multisig.multisigAccount + ); + const isAdmin = + multisigAccount?.account?.created_by === (walletAddress || ''); const [showAll, setShowAll] = useState(false); const [viewRawOpen, setViewRawOpen] = useState(false); @@ -87,7 +93,15 @@ export const TxnsCard = ({ const loading = useAppSelector((state) => state.multisig.deleteTxnRes.status); + const isSigned = txn?.signatures?.some( + (sign) => sign.address === walletAddress + ); + const hanldeDeleteTxn = () => { + if (!isAdmin) { + dispatch(setError({ type: 'error', message: ADMIN_ONLY_ALLOWED })); + return; + } if (isAccountVerified()) { setDeleteDialogOpen(true); } else { @@ -278,7 +292,6 @@ export const TxnsCard = ({ threshold={threshold} chainID={chainID} isMember={isMember} - disableBroadcast={disableBroadcast} isOverview={isOverview} broadcastInfo={broadcastInfo} /> @@ -290,6 +303,7 @@ export const TxnsCard = ({ txId={txn.id} unSignedTxn={txn} isOverview={isOverview} + isSigned={isSigned} /> )} > diff --git a/frontend/src/app/(routes)/multisig/components/multisig-account/MultisigAccount.tsx b/frontend/src/app/(routes)/multisig/components/multisig-account/MultisigAccount.tsx index 678bed08b..9ebbe5893 100644 --- a/frontend/src/app/(routes)/multisig/components/multisig-account/MultisigAccount.tsx +++ b/frontend/src/app/(routes)/multisig/components/multisig-account/MultisigAccount.tsx @@ -2,7 +2,6 @@ import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; import { getDelegations as getMultisigDelegations, - getMultisigAccounts, getMultisigBalances, multisigByAddress, resetBroadcastTxnRes, @@ -105,7 +104,6 @@ const MultisigAccount = ({ }) ); dispatch(multisigByAddress({ address: multisigAddress })); - dispatch(getMultisigAccounts(walletAddress)); } }, [chainID]); @@ -215,12 +213,6 @@ const MultisigAccountInfo = ({ const [availableBalance, setAvailableBalance] = useState(0); const [hasIBCTokens, setHasIBCTokens] = useState(false); - const multisigAccount = useAppSelector( - (state) => state.multisig.multisigAccount - ); - const multisigAccounts = useAppSelector( - (state) => state.multisig.multisigAccounts - ); const totalStaked = useAppSelector( (state) => state.multisig?.delegations.totalStaked ); @@ -237,8 +229,22 @@ const MultisigAccountInfo = ({ currency.coinDecimals, currency.coinMinimalDenom ); - const { txnCounts = {} } = multisigAccounts; - const actionsRequired = txnCounts?.[multisigAccount?.account?.address] || 0; + const txnsCount = useAppSelector((state) => state.multisig.txns.Count); + + const getCount = (option: string) => { + let count = 0; + /* eslint-disable @typescript-eslint/no-explicit-any */ + txnsCount && + txnsCount.forEach((t: any) => { + if (t?.computed_status?.toLowerCase() === option.toLowerCase()) { + count = t?.count; + } + }); + + return count; + }; + + const actionsRequired = getCount('to-sign') + getCount('to-broadcast'); useEffect(() => { setAvailableBalance( diff --git a/frontend/src/app/(routes)/multisig/components/multisig-account/Transactions.tsx b/frontend/src/app/(routes)/multisig/components/multisig-account/Transactions.tsx index decfa5cd5..81b89f90e 100644 --- a/frontend/src/app/(routes)/multisig/components/multisig-account/Transactions.tsx +++ b/frontend/src/app/(routes)/multisig/components/multisig-account/Transactions.tsx @@ -1,8 +1,5 @@ import { useAppDispatch, useAppSelector } from '@/custom-hooks/StateHooks'; -import { - getSequenceNumber, - getTxns, -} from '@/store/features/multisig/multisigSlice'; +import { getTxns } from '@/store/features/multisig/multisigSlice'; import { Txn } from '@/types/multisig'; import React, { useEffect, useState } from 'react'; import { TxStatus } from '@/types/enums'; @@ -11,7 +8,6 @@ import useFetchTxns from '@/custom-hooks/multisig/useFetchTxns'; import NoData from '@/components/common/NoData'; import TransactionsLoading from '../loaders/TransactionsLoading'; import DialogTxnFailed from './DialogTxnFailed'; -import useGetChainInfo from '@/custom-hooks/useGetChainInfo'; const TXNS_TYPES = [ { option: 'to-sign', value: 'To be Signed' }, @@ -32,8 +28,6 @@ const Transactions = ({ threshold: number; }) => { const dispatch = useAppDispatch(); - const { getChainInfo } = useGetChainInfo(); - const { restURLs } = getChainInfo(chainID); const txnsState = useAppSelector((state) => state.multisig.txns.list); const [txnsList, setTxnsList] = useState([]); @@ -50,6 +44,9 @@ const Transactions = ({ (state) => state.multisig.signTransactionRes ); const updateTxStatus = useAppSelector((state) => state.multisig.updateTxnRes); + const updateTxnSequencesStatus = useAppSelector( + (state) => state.multisig.updateTxnSequences + ); const handleTxnsTypeChange = (type: string) => { setTxnsType(type); @@ -116,7 +113,8 @@ const Transactions = ({ if ( signTxStatus.status === TxStatus.IDLE || updateTxStatus.status === TxStatus.IDLE || - updateTxStatus.status === TxStatus.REJECTED + updateTxStatus.status === TxStatus.REJECTED || + updateTxnSequencesStatus.status === TxStatus.IDLE ) { if (['failed', 'history', 'completed'].includes(txnsType)) { fetchTxns('history'); @@ -124,7 +122,11 @@ const Transactions = ({ fetchTxns('current'); } } - }, [signTxStatus.status, updateTxStatus.status]); + }, [ + signTxStatus.status, + updateTxStatus.status, + updateTxnSequencesStatus.status, + ]); // To reset state after singing or broadcasting txn useFetchTxns(); @@ -254,12 +256,20 @@ const TransactionsList = ({ (state) => state.multisig.multisigAccountSequenceNumber.value ); const sortedTxns = [...txns].sort((a, b) => { - const dateA = new Date( - txnsType === 'to-broadcast' ? a.txn_sequence ?? a.signed_at : a.created_at - ).getTime(); - const dateB = new Date( - txnsType === 'to-broadcast' ? b.txn_sequence ?? b.signed_at : b.created_at - ).getTime(); + const dateA = + txnsType === 'to-broadcast' + ? a.txn_sequence !== null + ? a.txn_sequence + : new Date(a.signed_at).getTime() + : new Date(a.created_at).getTime(); + + const dateB = + txnsType === 'to-broadcast' + ? b.txn_sequence !== null + ? b.txn_sequence + : new Date(b.signed_at).getTime() + : new Date(b.created_at).getTime(); + return txnsType === 'to-broadcast' ? dateA - dateB : dateB - dateA; }); @@ -276,10 +286,6 @@ const TransactionsList = ({ isHistory={isHistory} onViewError={onViewError} allowRepeat={txnsType === 'completed'} - disableBroadcast={ - txnsType === 'to-broadcast' && - currentSequenceNumber !== txn.txn_sequence - } broadcastInfo={{ disable: txnsType === 'to-broadcast' && @@ -294,6 +300,7 @@ const TransactionsList = ({ txn.txn_sequence !== null && currentSequenceNumber !== null && txn.txn_sequence > currentSequenceNumber, + isSequenceAvailable: Number(txn?.txn_sequence) ? true : false, }} /> ))} diff --git a/frontend/src/app/(routes)/multisig/components/multisig-dashboard/MultisigDashboard.tsx b/frontend/src/app/(routes)/multisig/components/multisig-dashboard/MultisigDashboard.tsx index 6f24b2778..ef0082b3a 100644 --- a/frontend/src/app/(routes)/multisig/components/multisig-dashboard/MultisigDashboard.tsx +++ b/frontend/src/app/(routes)/multisig/components/multisig-dashboard/MultisigDashboard.tsx @@ -5,8 +5,10 @@ import { resetBroadcastTxnRes, resetCreateMultisigRes, resetCreateTxnState, + resetSequenceNumber, resetsignTransactionRes, resetSignTxnState, + resetUpdateTxnSequences, resetUpdateTxnState, } from '@/store/features/multisig/multisigSlice'; import React, { useEffect } from 'react'; @@ -129,6 +131,8 @@ const MultisigDashboard: React.FC = (props) => { dispatch(resetUpdateTxnState()); dispatch(resetBroadcastTxnRes()); dispatch(resetsignTransactionRes()); + dispatch(resetSequenceNumber()); + dispatch(resetUpdateTxnSequences()); }, []); return ( diff --git a/frontend/src/custom-hooks/common/useInitApp.ts b/frontend/src/custom-hooks/common/useInitApp.ts index b379995d3..15195fdcd 100644 --- a/frontend/src/custom-hooks/common/useInitApp.ts +++ b/frontend/src/custom-hooks/common/useInitApp.ts @@ -46,7 +46,7 @@ const useInitApp = () => { const nameToChainIDs = useAppSelector( (state: RootState) => state.wallet.nameToChainIDs ); - const chainIDs = ["mantra-dukong-1"] // Object.values(nameToChainIDs); + const chainIDs = Object.values(nameToChainIDs); const walletState = useAppSelector((state) => state.wallet); const isWalletConnected = useAppSelector( @@ -57,74 +57,74 @@ const useInitApp = () => { const fetchedChains = useRef<{ [key: string]: boolean }>({}); const validatorsFetchedChains = useRef<{ [key: string]: boolean }>({}); - // useEffect(() => { - // if (chainIDs.length > 0 && isWalletConnected) { - // chainIDs.forEach((chainID) => { - // if (!fetchedChains.current[chainID]) { - // const { address, baseURL, restURLs } = getChainInfo(chainID); - - // if (isWalletConnected && address.length) { - // const authzGranterAddress = convertAddress(chainID, authzAddress); - // const { minimalDenom } = getDenomInfo(chainID); - // const chainRequestData = { - // baseURLs: restURLs, - // address: isAuthzMode ? authzGranterAddress : address, - // chainID, - // }; - - // // Fetch delegations - // dispatch( - // isAuthzMode - // ? getAuthzDelegations(chainRequestData) - // : getDelegations(chainRequestData) - // ); - - // // Fetch available balances - // dispatch( - // isAuthzMode - // ? getAuthzBalances({ ...chainRequestData, baseURL }) - // : getBalances({ ...chainRequestData, baseURL }) - // ); - - // // Fetch rewards - // dispatch( - // isAuthzMode - // ? getAuthzDelegatorTotalRewards({ - // ...chainRequestData, - // baseURL, - // denom: minimalDenom, - // }) - // : getDelegatorTotalRewards({ - // ...chainRequestData, - // baseURL, - // denom: minimalDenom, - // }) - // ); - - // // Fetch unbonding delegations - // dispatch( - // isAuthzMode - // ? getAuthzUnbonding(chainRequestData) - // : getUnbonding(chainRequestData) - // ); - - // // Mark chain as fetched - // fetchedChains.current[chainID] = true; - // } - // } - // }); - // } - // }, [ - // isWalletConnected, - // isAuthzMode, - // chainIDs, - // getChainInfo, - // convertAddress, - // getDenomInfo, - // authzAddress, - // dispatch, - // walletState, - // ]); + useEffect(() => { + if (chainIDs.length > 0 && isWalletConnected) { + chainIDs.forEach((chainID) => { + if (!fetchedChains.current[chainID]) { + const { address, baseURL, restURLs } = getChainInfo(chainID); + + if (isWalletConnected && address.length) { + const authzGranterAddress = convertAddress(chainID, authzAddress); + const { minimalDenom } = getDenomInfo(chainID); + const chainRequestData = { + baseURLs: restURLs, + address: isAuthzMode ? authzGranterAddress : address, + chainID, + }; + + // Fetch delegations + dispatch( + isAuthzMode + ? getAuthzDelegations(chainRequestData) + : getDelegations(chainRequestData) + ); + + // Fetch available balances + dispatch( + isAuthzMode + ? getAuthzBalances({ ...chainRequestData, baseURL }) + : getBalances({ ...chainRequestData, baseURL }) + ); + + // Fetch rewards + dispatch( + isAuthzMode + ? getAuthzDelegatorTotalRewards({ + ...chainRequestData, + baseURL, + denom: minimalDenom, + }) + : getDelegatorTotalRewards({ + ...chainRequestData, + baseURL, + denom: minimalDenom, + }) + ); + + // Fetch unbonding delegations + dispatch( + isAuthzMode + ? getAuthzUnbonding(chainRequestData) + : getUnbonding(chainRequestData) + ); + + // Mark chain as fetched + fetchedChains.current[chainID] = true; + } + } + }); + } + }, [ + isWalletConnected, + isAuthzMode, + chainIDs, + getChainInfo, + convertAddress, + getDenomInfo, + authzAddress, + dispatch, + walletState, + ]); useEffect(() => { if (chainIDs.length > 0) { @@ -141,9 +141,9 @@ const useInitApp = () => { } }, [chainIDs, walletState]); - // useFetchPriceInfo(); - // useInitFeegrant({ chainIDs, shouldFetch: isFeegrantModeEnabled }); - // useInitAuthz({ chainIDs, shouldFetch: fetchAuthz(isAuthzMode) }); + useFetchPriceInfo(); + useInitFeegrant({ chainIDs, shouldFetch: isFeegrantModeEnabled }); + useInitAuthz({ chainIDs, shouldFetch: fetchAuthz(isAuthzMode) }); }; export default useInitApp; diff --git a/frontend/src/store/features/multisig/multisigService.ts b/frontend/src/store/features/multisig/multisigService.ts index 34092f03d..a24ea0257 100644 --- a/frontend/src/store/features/multisig/multisigService.ts +++ b/frontend/src/store/features/multisig/multisigService.ts @@ -28,6 +28,9 @@ const SIGNATURE_PARAMS_STRING = (queryParams: QueryParams): string => const CREATE_ACCOUNT = (queryParams: QueryParams): string => `/multisig` + SIGNATURE_PARAMS_STRING(queryParams); +const UPDATE_TXN_SEQUENCES = (address: string) => + `${BASE_URL}/multisig/${address}/reset-txns`; + const SIGN_URL = ( queryParams: QueryParams, address: string, @@ -102,6 +105,14 @@ export const getAccountAllMultisigTxns = ( return Axios.get(uri); }; +export const updateTxnSequences = ( + queryParams: QueryParams, + address: string +): Promise => + Axios.delete( + UPDATE_TXN_SEQUENCES(address) + SIGNATURE_PARAMS_STRING(queryParams), + ); + export const deleteTx = ( queryParams: QueryParams, address: string, @@ -147,4 +158,5 @@ export default { verifyUser, getAccountAllMultisigTxns, getStargateClient, + updateTxnSequences }; diff --git a/frontend/src/store/features/multisig/multisigSlice.ts b/frontend/src/store/features/multisig/multisigSlice.ts index 85ac1a200..8a5da68e8 100644 --- a/frontend/src/store/features/multisig/multisigSlice.ts +++ b/frontend/src/store/features/multisig/multisigSlice.ts @@ -33,9 +33,9 @@ import { MultisigAddressPubkey, MultisigState, QueryParams, - SignTxInputs, Txn, UpdateTxnInputs, + UpdateTxnSequencesInputs, } from '@/types/multisig'; import { getRandomNumber, @@ -163,6 +163,10 @@ const initialState: MultisigState = { status: TxStatus.INIT, error: '', }, + updateTxnSequences: { + status: TxStatus.INIT, + error: '', + }, }; declare let window: WalletWindow; @@ -320,6 +324,25 @@ export const getSequenceNumber = createAsyncThunk( } ); +export const updateTxnSequences = createAsyncThunk( + 'multisig/update-txn-sequences', + async (data: UpdateTxnSequencesInputs, { rejectWithValue }) => { + try { + const response = await multisigService.updateTxnSequences( + data.queryParams, + data.data.address + ); + trackEvent('MULTISIG', 'UPDATE_TXN_SEQUENCES', SUCCESS); + return response.data; + } catch (error) { + trackEvent('MULTISIG', 'UPDATE_TXN_SEQUENCES', FAILED); + if (error instanceof AxiosError) + return rejectWithValue({ message: error.message }); + return rejectWithValue({ message: ERR_UNKNOWN }); + } + } +); + export const getMultisigBalances = createAsyncThunk( 'multisig/multisigBalance', async (data: GetMultisigBalancesInputs, { rejectWithValue }) => { @@ -611,28 +634,6 @@ export const signTransaction = createAsyncThunk( } ); -// export const signTx = createAsyncThunk( -// 'multisig/signTx', -// async (data: SignTxInputs, { rejectWithValue }) => { -// try { -// const response = await multisigService.signTx( -// data.queryParams, -// data.data.address, -// data.data.txId, -// { -// signer: data.data.signer, -// signature: data.data.signature, -// } -// ); -// return response.data; -// } catch (error) { -// if (error instanceof AxiosError) -// return rejectWithValue({ message: error.message }); -// return rejectWithValue({ message: ERR_UNKNOWN }); -// } -// } -// ); - export const importMultisigAccount = createAsyncThunk( 'multisig/importMultisigAccount', async ( @@ -717,6 +718,9 @@ export const multisigSlice = createSlice({ state.multisigAccountSequenceNumber = initialState.multisigAccountSequenceNumber; }, + resetUpdateTxnSequences: (state) => { + state.updateTxnSequences = initialState.updateTxnSequences; + }, setVerifyDialogOpen: (state, action: PayloadAction) => { state.verifyDialogOpen = action.payload; }, @@ -837,6 +841,26 @@ export const multisigSlice = createSlice({ state.multisigAccountSequenceNumber.value = null; state.multisigAccount.error = payload.message || ''; }); + builder + .addCase(updateTxnSequences.pending, (state) => { + state.updateTxnSequences = { + status: TxStatus.PENDING, + error: '', + }; + }) + .addCase(updateTxnSequences.fulfilled, (state) => { + state.updateTxnSequences = { + status: TxStatus.IDLE, + error: '', + }; + }) + .addCase(updateTxnSequences.rejected, (state, action) => { + const payload = action.payload as { message: string }; + state.updateTxnSequences = { + status: TxStatus.REJECTED, + error: payload.message || '', + }; + }); builder .addCase(getMultisigBalances.pending, (state) => { state.balance.status = TxStatus.PENDING; @@ -940,18 +964,6 @@ export const multisigSlice = createSlice({ const payload = action.payload as { message: string }; state.txns.error = payload.message || ''; }); - // builder - // .addCase(signTx.pending, (state) => { - // state.signTxRes.status = TxStatus.PENDING; - // }) - // .addCase(signTx.fulfilled, (state) => { - // state.signTxRes.status = TxStatus.IDLE; - // }) - // .addCase(signTx.rejected, (state, action) => { - // state.signTxRes.status = TxStatus.REJECTED; - // const payload = action.payload as { message: string }; - // state.signTxRes.error = payload.message || ''; - // }); builder .addCase(signTransaction.pending, (state) => { state.signTransactionRes.status = TxStatus.PENDING; @@ -1011,6 +1023,7 @@ export const { resetsignTransactionRes, setVerifyDialogOpen, resetSequenceNumber, + resetUpdateTxnSequences, } = multisigSlice.actions; export default multisigSlice.reducer; diff --git a/frontend/src/types/multisig.d.ts b/frontend/src/types/multisig.d.ts index cc70c8c07..4ecfd837c 100644 --- a/frontend/src/types/multisig.d.ts +++ b/frontend/src/types/multisig.d.ts @@ -69,6 +69,13 @@ interface DeleteTxnInputs { }; } +interface UpdateTxnSequencesInputs { + queryParams: QueryParams; + data: { + address: string; + }; +} + interface MultisigAddressPubkey { address: string; multisig_address: string; @@ -155,6 +162,10 @@ interface MultisigState { status: TxStatus; error: string; }; + updateTxnSequences: { + status: TxStatus; + error: string; + }; } interface VerifyAccountRes { diff --git a/frontend/src/utils/errors.ts b/frontend/src/utils/errors.ts index 4e5fd6ba6..77cd20214 100644 --- a/frontend/src/utils/errors.ts +++ b/frontend/src/utils/errors.ts @@ -27,10 +27,18 @@ export const INSUFFICIENT_BALANCE = 'Insufficient balance'; export const NOT_MULTISIG_MEMBER_ERROR = 'Cannot import account: You are not a member of the multisig account'; export const NOT_MULTISIG_ACCOUNT_ERROR = 'Not a multisig account'; -export const CHAIN_NOT_SELECTED_ERROR = 'Please select at least one network from the left'; -export const MSG_NOT_SELECTED_ERROR = 'Please select at least one transaction message from the left'; +export const CHAIN_NOT_SELECTED_ERROR = + 'Please select at least one network from the left'; +export const MSG_NOT_SELECTED_ERROR = + 'Please select at least one transaction message from the left'; export const PERMISSION_NOT_SELECTED_ERROR = 'Atleast one permission must be selected'; export const FAILED_TO_FETCH = 'Failed to fetch'; export const NETWORK_ERROR = 'Network error'; -export const ERR_TXN_NOT_FOUND = 'TXN not found'; \ No newline at end of file +export const ERR_TXN_NOT_FOUND = 'TXN not found'; +export const CANNOT_BROADCAST_ERROR = + 'Cannot broadcast, There is transaction that need to be broadcasted before this.'; +export const FAILED_TO_UPDATE_SEQUENCE = 'Failed to update sequence'; +export const UPDATED_SEQUENCE_SUCCESSFULLY = 'Successfully updated sequence'; +export const ADMIN_ONLY_ALLOWED = 'Delete action is limited to the admin only'; +export const FAILED_TO_BROADCAST_TRY_AGAIN = 'Failed to broadcast. Please try again.'; diff --git a/server/server.go b/server/server.go index f65c3a289..add9946d1 100644 --- a/server/server.go +++ b/server/server.go @@ -96,7 +96,7 @@ func main() { e.GET("/txns/:chainId/:address", h.GetAllTransactions) e.GET("/txns/:chainId/:address/:txhash", h.GetChainTxHash) e.GET("/search/txns/:txhash", h.GetTxHash) - e.POST("/multisig/:address/reset-txns", h.ResetPendingTransactions, m.AuthMiddleware) + e.DELETE("/multisig/:address/reset-txns", h.ResetPendingTransactions, m.AuthMiddleware, m.IsMultisigMember) // users e.POST("/users/:address/signature", h.CreateUserSignature)