From add8bcc3b42b9404593986c2ef34813335240abe Mon Sep 17 00:00:00 2001 From: hemanthghs Date: Wed, 25 Sep 2024 16:59:54 +0530 Subject: [PATCH] feat: integrate ibc send to interchain agent --- .../InterchainAgentDialog.tsx | 39 +++- .../interchain-agent/useTransactions.ts | 186 ++++++++++++++---- .../src/store/features/common/commonSlice.ts | 12 +- frontend/src/store/features/ibc/ibcSlice.ts | 36 ++-- 4 files changed, 214 insertions(+), 59 deletions(-) diff --git a/frontend/src/components/interchain-agent/InterchainAgentDialog.tsx b/frontend/src/components/interchain-agent/InterchainAgentDialog.tsx index f13907e00..d5acb52ed 100644 --- a/frontend/src/components/interchain-agent/InterchainAgentDialog.tsx +++ b/frontend/src/components/interchain-agent/InterchainAgentDialog.tsx @@ -26,22 +26,45 @@ interface InterchainAgentDialogProps { /* eslint-disable @typescript-eslint/no-explicit-any */ -function parseTransaction(input: string): { type: string; data: any } | null { - const regex = /^(\w+)\s+(\d+(?:\.\d+)?)\s+(\w+)\s+to\s+([a-zA-Z0-9]+)$/i; - const match = input.match(regex); + function parseTransaction(input: string): { type: string; data: any } | null { + // Regex for "send to
from " + const regexWithChainID = /^(\w+)\s+(\d+(?:\.\d+)?)\s+(\w+)\s+to\s+([a-zA-Z0-9]+)\s+from\s+([\w-]+)$/i; + // Regex for "send to
" + const regexWithoutChainID = /^(\w+)\s+(\d+(?:\.\d+)?)\s+(\w+)\s+to\s+([a-zA-Z0-9]+)$/i; + + let match; + + // First, check for the regex with chainID + match = input.match(regexWithChainID); + if (match) { + const [, typeWithChainID, amountWithChainID, denomWithChainID, addressWithChainID, chainID] = match; + return { + type: typeWithChainID, + data: { + amount: amountWithChainID, + denom: denomWithChainID.toUpperCase(), + address: addressWithChainID, + chainID: chainID, + }, + }; + } + + // Then, check for the regex without chainID + match = input.match(regexWithoutChainID); if (match) { - const [, type, amount, denom, address] = match; + const [, typeWithoutChainID, amountWithoutChainID, denomWithoutChainID, addressWithoutChainID] = match; return { - type: type, + type: typeWithoutChainID, data: { - amount: amount, - denom: denom.toUpperCase(), - address, + amount: amountWithoutChainID, + denom: denomWithoutChainID.toUpperCase(), + address: addressWithoutChainID, }, }; } + // If no pattern is matched, return null return null; } diff --git a/frontend/src/custom-hooks/interchain-agent/useTransactions.ts b/frontend/src/custom-hooks/interchain-agent/useTransactions.ts index c841d710c..6913dc0d8 100644 --- a/frontend/src/custom-hooks/interchain-agent/useTransactions.ts +++ b/frontend/src/custom-hooks/interchain-agent/useTransactions.ts @@ -1,12 +1,22 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import useGetChainInfo from '../useGetChainInfo'; import { SendMsg } from '@/txns/bank'; import { useAppDispatch, useAppSelector } from '../StateHooks'; -import { txGeneric } from '@/store/features/common/commonSlice'; +import { + resetGenericTxStatus, + resetTxAndHash, + setGenericTxStatus, + txGeneric, +} from '@/store/features/common/commonSlice'; import { TxStatus } from '@/types/enums'; import { addSessionItem } from '@/store/features/interchain-agent/agentSlice'; import { getTxnURLOnResolute } from '@/utils/util'; import { Delegate } from '@/txns/staking'; +import useGetTxInputs from '../useGetTxInputs'; +import { + txTransfer, + resetTxStatus as resetIBCTxStatus, +} from '@/store/features/ibc/ibcSlice'; const SUPPORTED_TXNS = ['send', 'delegate']; @@ -18,10 +28,19 @@ const useTransactions = ({ chatInputTime: string; }) => { const dispatch = useAppDispatch(); - const { getChainIDByCoinDenom, getDenomInfo, getChainInfo } = - useGetChainInfo(); + const { + getChainIDByCoinDenom, + getDenomInfo, + getChainInfo, + getChainIDFromAddress, + } = useGetChainInfo(); + const { txTransferInputs } = useGetTxInputs(); + const nameToChainIDs = useAppSelector((state) => state.wallet.nameToChainIDs); + const supportedChainIDs = Object.keys(nameToChainIDs).map( + (chainName) => nameToChainIDs[chainName] + ); - const [currentChainID, setCurrenChainID] = useState(''); + const [currentChainID, setCurrentChainID] = useState(''); const txStatus = useAppSelector((state) => state.common.genericTransaction); const tx = useAppSelector((state) => state.common.txSuccess.tx); @@ -29,63 +48,117 @@ const useTransactions = ({ (state) => state.agent.currentSessionID ); const isWalletConnected = useAppSelector((state) => state.wallet.connected); + const ibcTxStatus = useAppSelector((state) => state.ibc.txStatus); + const ibcTxError = useAppSelector((state) => state.ibc.txError); const validateParsedTxnData = ({ parsedData, }: { - /* eslint-disable @typescript-eslint/no-explicit-any */ + /* eslint-disable @typescript-eslint/no-explicit-any */ parsedData: { type: string; data: any }; }) => { - setCurrenChainID(''); + setCurrentChainID(''); if (!isWalletConnected) { return 'Please connect your wallet'; } + const providedChainID = parsedData?.data?.chainID; + if (providedChainID) { + if (supportedChainIDs.includes(providedChainID)) { + setCurrentChainID(providedChainID); + } else { + setCurrentChainID(''); + return `Unsupported/Invalid chain ID: ${providedChainID}`; + } + } const chainID = getChainIDByCoinDenom(parsedData.data.denom); - if (chainID) { - setCurrenChainID(chainID); + if (!providedChainID && chainID) { + setCurrentChainID(chainID); if (!SUPPORTED_TXNS.includes(parsedData.type)) { return `Unsupported transaction type ${parsedData.type}`; } - if (parsedData.type === 'send' || parsedData.type === 'delegate') { - const amount = parseFloat(parsedData.data.amount); - if (isNaN(amount) || amount <= 0) { - return `Invalid amount ${parsedData.data?.amount || ''}`; - } + } + if (!providedChainID && !chainID) { + setCurrentChainID(''); + return 'No chains found with given denom'; + } + if (parsedData.type === 'send' || parsedData.type === 'delegate') { + const amount = parseFloat(parsedData.data.amount); + if (isNaN(amount) || amount <= 0) { + return `Invalid amount ${parsedData.data?.amount || ''}`; } - return ''; } - return 'No chains found with given denom'; + return ''; }; const initiateTransaction = ({ parsedData, }: { - /* eslint-disable @typescript-eslint/no-explicit-any */ + /* eslint-disable @typescript-eslint/no-explicit-any */ parsedData: { type: string; data: any }; }) => { + dispatch(resetIBCTxStatus()); + dispatch(resetTxAndHash()); + dispatch(resetGenericTxStatus()); const chainID = getChainIDByCoinDenom(parsedData.data.denom); - const basicChainInfo = getChainInfo(chainID); - const { decimals, minimalDenom } = getDenomInfo(chainID); + const providedChainID = parsedData?.data?.chainID; + if (parsedData.type === 'send') { - const toAddress = parsedData.data?.address; - const amount = parseFloat(parsedData.data?.amount); - const msg = SendMsg( - basicChainInfo.address, - toAddress, - amount * 10 ** decimals, - minimalDenom - ); - dispatch( - txGeneric({ - basicChainInfo, - msgs: [msg], - memo: '', - denom: minimalDenom, - feegranter: '', - }) - ); + const destChainID = getChainIDFromAddress(parsedData.data?.address); + // Normal send + if ( + !providedChainID || + (providedChainID && + providedChainID === chainID && + destChainID === chainID) + ) { + const basicChainInfo = getChainInfo(chainID); + const { decimals, minimalDenom } = getDenomInfo(chainID); + const toAddress = parsedData.data?.address; + const amount = parseFloat(parsedData.data?.amount); + const msg = SendMsg( + basicChainInfo.address, + toAddress, + amount * 10 ** decimals, + minimalDenom + ); + dispatch( + txGeneric({ + basicChainInfo, + msgs: [msg], + memo: '', + denom: minimalDenom, + feegranter: '', + }) + ); + // IBC Send + } else { + const { decimals, minimalDenom } = getDenomInfo(providedChainID); + const toAddress = parsedData.data?.address; + const amount = parseFloat(parsedData.data?.amount); + const destChainID = getChainIDFromAddress(toAddress); + if (!destChainID) { + dispatch( + setGenericTxStatus({ + status: TxStatus.REJECTED, + errMsg: 'Invalid address', + }) + ); + return; + } + const txInputs = txTransferInputs( + providedChainID, + destChainID, + toAddress, + amount, + minimalDenom, + decimals + ); + dispatch(txTransfer(txInputs)); + } } if (parsedData.type === 'delegate') { + const basicChainInfo = getChainInfo(chainID); + const { decimals, minimalDenom } = getDenomInfo(chainID); const valAddress = parsedData.data?.address; const amount = parseFloat(parsedData.data?.amount); const msg = Delegate( @@ -139,6 +212,47 @@ const useTransactions = ({ } }, [tx, txStatus]); + useEffect(() => { + if ( + ibcTxStatus === TxStatus.IDLE && + tx?.transactionHash && + userInput?.length + ) { + const { chainName } = getChainInfo(currentChainID); + dispatch( + addSessionItem({ + request: { + [userInput]: { + errMessage: '', + result: `Transaction successful: [View here](${getTxnURLOnResolute(chainName, tx?.transactionHash || '')})`, + status: 'success', + date: chatInputTime, + }, + }, + sessionID: currentSessionID, + }) + ); + dispatch(setGenericTxStatus({ status: TxStatus.IDLE, errMsg: '' })); + } else if (ibcTxStatus === TxStatus.REJECTED && userInput?.length) { + dispatch( + addSessionItem({ + request: { + [userInput]: { + errMessage: '', + result: `Transaction failed`, + status: 'failed', + date: chatInputTime, + }, + }, + sessionID: currentSessionID, + }) + ); + dispatch( + setGenericTxStatus({ status: TxStatus.REJECTED, errMsg: ibcTxError }) + ); + } + }, [tx, ibcTxStatus]); + return { validateParsedTxnData, initiateTransaction }; }; diff --git a/frontend/src/store/features/common/commonSlice.ts b/frontend/src/store/features/common/commonSlice.ts index 7067da45b..80e8181bd 100644 --- a/frontend/src/store/features/common/commonSlice.ts +++ b/frontend/src/store/features/common/commonSlice.ts @@ -118,7 +118,7 @@ export const txGeneric = createAsyncThunk( } /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ } catch (error: any) { - console.log("error: ", error); + console.log('error: ', error); const errMessage = error?.response?.data?.error || error?.message; dispatch( setError({ @@ -126,7 +126,7 @@ export const txGeneric = createAsyncThunk( message: errMessage || ERR_UNKNOWN, }) ); - console.log("erro22", errMessage) + console.log('erro22', errMessage); return rejectWithValue(errMessage || ERR_UNKNOWN); } } @@ -196,6 +196,13 @@ export const commonSlice = createSlice({ resetGenericTxStatus: (state) => { state.genericTransaction = initialState.genericTransaction; }, + setGenericTxStatus: ( + state, + action: PayloadAction<{ status: TxStatus; errMsg: string }> + ) => { + state.genericTransaction.status = action.payload.status; + state.genericTransaction.errMsg = action.payload.errMsg; + }, }, extraReducers: (builder) => { builder @@ -265,6 +272,7 @@ export const { setChangeNetworkDialogOpen, setAddNetworkDialogOpen, resetGenericTxStatus, + setGenericTxStatus, } = commonSlice.actions; export default commonSlice.reducer; diff --git a/frontend/src/store/features/ibc/ibcSlice.ts b/frontend/src/store/features/ibc/ibcSlice.ts index bb3c819fa..4e0d37154 100644 --- a/frontend/src/store/features/ibc/ibcSlice.ts +++ b/frontend/src/store/features/ibc/ibcSlice.ts @@ -10,16 +10,24 @@ import { NewIBCTransaction, NewTransaction } from '@/utils/transaction'; import { setError, setTxAndHash } from '../common/commonSlice'; import { capitalize } from 'lodash'; import { getBalances } from '../bank/bankSlice'; -import { addIBCTransaction, updateIBCTransactionStatus } from '../recent-transactions/recentTransactionsSlice'; +import { + addIBCTransaction, + updateIBCTransactionStatus, +} from '../recent-transactions/recentTransactionsSlice'; import { FAILED, SUCCESS } from '@/utils/constants'; import { trackEvent } from '@/utils/util'; export interface IBCState { txStatus: TxStatus; + txError: string; chains: Record; } -const initialState: IBCState = { txStatus: TxStatus.INIT, chains: {} }; +const initialState: IBCState = { + txStatus: TxStatus.INIT, + txError: '', + chains: {}, +}; export const trackTx = createAsyncThunk( 'ibc/trackTx', @@ -29,9 +37,7 @@ export const trackTx = createAsyncThunk( ) => { const onDestChainTxSuccess = (chainID: string, txHash: string) => { dispatch(removeFromPending({ chainID, txHash })); - dispatch( - updateIBCTransactionStatus({ txHash }) - ); + dispatch(updateIBCTransactionStatus({ txHash })); }; try { await trackIBCTx(data.txHash, data.chainID, onDestChainTxSuccess); @@ -56,7 +62,10 @@ export const txTransfer = createAsyncThunk( const onSourceChainTxSuccess = async (chainID: string, txHash: string) => { dispatch(resetTxStatus()); const response = await axios.get( - data.rest + '/cosmos/tx/v1beta1/txs/' + txHash+ `?chain=${data.sourceChainID}` + data.rest + + '/cosmos/tx/v1beta1/txs/' + + txHash + + `?chain=${data.sourceChainID}` ); const msgs = response?.data?.tx?.body?.messages || []; /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -83,7 +92,7 @@ export const txTransfer = createAsyncThunk( true, result.code === 0 ); - if(result.code === 0) { + if (result.code === 0) { trackEvent('TRANSFER', 'IBC_TRANSFER', SUCCESS); } else { trackEvent('TRANSFER', 'IBC_TRANSFER', FAILED); @@ -114,9 +123,7 @@ export const txTransfer = createAsyncThunk( destChain: string ) => { dispatch(removeFromPending({ chainID, txHash })); - dispatch( - updateIBCTransactionStatus({ txHash }) - ); + dispatch(updateIBCTransactionStatus({ txHash })); dispatch( setError({ type: 'success', @@ -159,10 +166,10 @@ export const txTransfer = createAsyncThunk( dispatch( setError({ type: 'error', - message: (err?.message || 'Request rejected').toString(), + message: (err?.log || err?.message || 'Request rejected').toString(), }) ); - return rejectWithValue(err); + return rejectWithValue(err?.log || err?.message || 'Failed to execute'); } } ); @@ -216,12 +223,15 @@ export const ibcSlice = createSlice({ const { sourceChainID } = action.meta.arg; if (!state.chains[sourceChainID]) state.chains[sourceChainID] = []; state.txStatus = TxStatus.PENDING; + state.txError = ''; }) .addCase(txTransfer.fulfilled, (state) => { state.txStatus = TxStatus.IDLE; + state.txError = ''; }) - .addCase(txTransfer.rejected, (state) => { + .addCase(txTransfer.rejected, (state, action) => { state.txStatus = TxStatus.REJECTED; + state.txError = action.payload as string || ''; }); builder