diff --git a/frontend/src/components/interchain-agent/ChatSuggestions.tsx b/frontend/src/components/interchain-agent/ChatSuggestions.tsx index 08254c185..e29f8afd4 100644 --- a/frontend/src/components/interchain-agent/ChatSuggestions.tsx +++ b/frontend/src/components/interchain-agent/ChatSuggestions.tsx @@ -17,6 +17,11 @@ const SUGGESTIONS = [ text: 'send 1 ATOM to cosmos.... from chainID', icon: '/sidebar-menu-icons/transfers-icon.svg', }, + { + title: 'IBC Swap', + text: 'swap 0.1 OSMO of osmosis to ATOM of cosmoshub', + icon: '/sidebar-menu-icons/transfers-icon.svg', + }, ]; const ChatSuggestions = ({ diff --git a/frontend/src/components/interchain-agent/InterchainAgentDialog.tsx b/frontend/src/components/interchain-agent/InterchainAgentDialog.tsx index 0a931159a..b09436cb0 100644 --- a/frontend/src/components/interchain-agent/InterchainAgentDialog.tsx +++ b/frontend/src/components/interchain-agent/InterchainAgentDialog.tsx @@ -35,6 +35,10 @@ function parseTransaction(input: string): { type: string; data: any } | null { const regexWithoutChainID = /^(\w+)\s+(\d+(?:\.\d+)?)\s+(\w+)\s+to\s+([a-zA-Z0-9]+)$/i; + // Regex for "swap of to of " + const regexForIBCSwap = + /^(\w+)\s+(\d+(?:\.\d+)?)\s+(\w+)\s+of\s+([\w-]+)\s+to\s+(\w+)\s+of\s+([\w-]+)$/i; + let match; // First, check for the regex with chainID @@ -79,6 +83,30 @@ function parseTransaction(input: string): { type: string; data: any } | null { }; } + match = input.match(regexForIBCSwap); + if(match){ + const [ + , + typeWithoutChainID, + amount, + sourceDenom, + sourceChainName, + destinationDenom, + destinationChainName + ] = match; + + return { + type: typeWithoutChainID, + data: { + amount: amount, + denom: sourceDenom.toLowerCase(), + sourceChainName: sourceChainName.toLowerCase(), + destinationDenom: destinationDenom.toLowerCase(), + destinationChainName: destinationChainName.toLowerCase(), + }, + }; + } + // 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 62117f462..b991b9a5a 100644 --- a/frontend/src/custom-hooks/interchain-agent/useTransactions.ts +++ b/frontend/src/custom-hooks/interchain-agent/useTransactions.ts @@ -17,8 +17,14 @@ import { txTransfer, resetTxStatus as resetIBCTxStatus, } from '@/store/features/ibc/ibcSlice'; +import { txIBCSwap } from '@/store/features/swaps/swapsSlice'; +import useAccount from '@/custom-hooks/useAccount'; +import useChain from '@/custom-hooks/useChain'; +import useSwaps from '@/custom-hooks/useSwaps'; +import useGetChains from '@/custom-hooks/useGetChains'; +// import useGetAssets from '@/custom-hooks/useGetAssets'; -const SUPPORTED_TXNS = ['send', 'delegate']; +const SUPPORTED_TXNS = ['send', 'delegate', 'swap']; const useTransactions = ({ userInput, @@ -27,6 +33,19 @@ const useTransactions = ({ userInput: string; chatInputTime: string; }) => { + // Get Signer + const { getAccountAddress } = useAccount(); + + // To fetch 4 rest endpoints from chain-registry + const { getChainEndpoints, getExplorerEndpoints } = useChain(); + + const { getSwapRoute, routeError } = useSwaps(); + + const { chainsData } = useGetChains(); + + // const { getTokensByChainID, srcAssetsLoading, destAssetLoading } = + // useGetAssets(); + const dispatch = useAppDispatch(); const { getChainIDByCoinDenom, @@ -90,7 +109,7 @@ const useTransactions = ({ return ''; }; - const initiateTransaction = ({ + const initiateTransaction = async ({ parsedData, }: { /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -177,6 +196,90 @@ const useTransactions = ({ }) ); } + if (parsedData.type === 'swap') { + // IBC Swap via Interchain GPT bot + const selectedSourceChain: any = {}; + const selectedDestChain: any = {}; + const supportedChains = chainsData; + const sourceChain: any = supportedChains.find( + (chain: any) => + chain.axelarChainName === parsedData.data.sourceChainName + ); + + // validate if source chain is supported + if (sourceChain) { + selectedSourceChain.chainID = sourceChain.chainId; + } + + const destinationChain: any = supportedChains.find( + (chain: any) => + chain.axelarChainName === parsedData.data.destinationChainName + ); + + // validate if destination chain is supported + if (destinationChain) { + selectedDestChain.chainID = destinationChain.chainId; + } + + const { address: fromAddress } = await getAccountAddress( + sourceChain.chainId || '' + ); + const { address: toAddress } = await getAccountAddress( + destinationChain.chainId || '' + ); + + const { rpcs, apis } = getChainEndpoints( + selectedSourceChain?.chainID || '' + ); + + const { explorerEndpoint } = getExplorerEndpoints( + selectedSourceChain?.chainID || '' + ); + const { decimals } = getDenomInfo(selectedSourceChain.chainID); + + const { route } = await getSwapRoute({ + amount: Number(parsedData.data.amount) * 10 ** (decimals || 1), + destChainID: selectedDestChain?.chainID || '', + destDenom: `u${parsedData.data.destinationDenom}` || '', + sourceChainID: selectedSourceChain?.chainID || '', + sourceDenom: `u${parsedData.data?.denom}` || '', + fromAddress, + toAddress, + slippage: Number(0.5), + }); + console.log("swap router error ", routeError); + + if (routeError.length > 0) { + dispatch( + addSessionItem({ + request: { + [userInput]: { + errMessage: '', + result: `Transaction failed: ${routeError}`, + status: 'failed', + date: chatInputTime, + }, + }, + sessionID: currentSessionID, + }) + ); + return; + } + + if (route?.estimate) { + dispatch( + txIBCSwap({ + rpcURLs: rpcs, + signerAddress: fromAddress, + sourceChainID: selectedSourceChain?.chainID || '', + destChainID: selectedDestChain?.chainID || '', + swapRoute: route, + explorerEndpoint, + baseURLs: apis, + }) + ); + } + } }; useEffect(() => { diff --git a/frontend/src/custom-hooks/useGetChains.ts b/frontend/src/custom-hooks/useGetChains.ts index 50c7a179f..eef18156f 100644 --- a/frontend/src/custom-hooks/useGetChains.ts +++ b/frontend/src/custom-hooks/useGetChains.ts @@ -57,6 +57,7 @@ const useGetChains = () => { chainsInfo, getChainConfig, getChainLogoURI, + chainsData }; }; diff --git a/frontend/src/store/features/swaps/swapsSlice.ts b/frontend/src/store/features/swaps/swapsSlice.ts index 535d15ffe..cadb74f69 100644 --- a/frontend/src/store/features/swaps/swapsSlice.ts +++ b/frontend/src/store/features/swaps/swapsSlice.ts @@ -14,7 +14,7 @@ import { trackTransactionStatus, txExecuteSwap, } from './swapsService'; -import { setError } from '../common/commonSlice'; +import { setError, setGenericTxStatus } from '../common/commonSlice'; import { ERR_UNKNOWN } from '@/utils/errors'; import { OfflineDirectSigner } from '@cosmjs/proto-signing'; import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; @@ -103,6 +103,12 @@ export const txIBCSwap = createAsyncThunk( status: 'success', }) ); + dispatch( + setGenericTxStatus({ + status: TxStatus.IDLE, + errMsg: '', + }) + ); } else if (txStatus === 'needs_gas') { dispatch( setError({ @@ -110,6 +116,12 @@ export const txIBCSwap = createAsyncThunk( type: 'error', }) ); + dispatch( + setGenericTxStatus({ + status: TxStatus.REJECTED, + errMsg: 'Transaction could not be completed, needs gas', + }) + ); } else if (txStatus === 'partial_success') { dispatch( setTxDestSuccess({ @@ -117,6 +129,12 @@ export const txIBCSwap = createAsyncThunk( status: 'partial_success', }) ); + dispatch( + setGenericTxStatus({ + status: TxStatus.REJECTED, + errMsg: 'Transaction Partially Successful', + }) + ); } else if (txStatus === 'not_found') { dispatch( setError({