diff --git a/frontend/src/components/SideBar.tsx b/frontend/src/components/SideBar.tsx
index 628ac5393..bf939a40a 100644
--- a/frontend/src/components/SideBar.tsx
+++ b/frontend/src/components/SideBar.tsx
@@ -117,7 +117,7 @@ const MenuItem = ({
authzSupport: boolean;
metamaskSupport: boolean;
}) => {
- // Here when the url(pathName) includes validator,
+ // Here when the url(pathName) includes validator,
// we are setting the module to staking (to highlight the staking module)
const path =
pathName === 'overview'
@@ -167,8 +167,8 @@ const MenuItem = ({
diff --git a/frontend/src/custom-hooks/useContracts.ts b/frontend/src/custom-hooks/useContracts.ts
new file mode 100644
index 000000000..2597b41e1
--- /dev/null
+++ b/frontend/src/custom-hooks/useContracts.ts
@@ -0,0 +1,330 @@
+import {
+ connectWithSigner,
+ getContract,
+ queryContract,
+} from '@/store/features/cosmwasm/cosmwasmService';
+import { extractContractMessages } from '@/utils/util';
+import { useState } from 'react';
+import { useDummyWallet } from './useDummyWallet';
+import chainDenoms from '@/utils/chainDenoms.json';
+import useGetChainInfo from './useGetChainInfo';
+import { Event } from 'cosmjs-types/tendermint/abci/types';
+import { toUtf8 } from '@cosmjs/encoding';
+
+declare let window: WalletWindow;
+
+const dummyQuery = {
+ '': '',
+};
+
+const assetsData = chainDenoms as AssetData;
+
+const GAS = '900000';
+
+const getCodeIdFromEvents = (events: Event[]) => {
+ let codeId = '';
+ for (let i = 0; i < events.length; i++) {
+ const event = events[i];
+ if (event.type === 'store_code') {
+ for (let j = 0; j < event.attributes.length; j++) {
+ const attribute = event.attributes[j];
+ if (attribute.key === 'code_id') {
+ codeId = attribute.value;
+ break;
+ }
+ }
+ }
+ }
+ return codeId;
+};
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+const getCodeId = (txData: any) => {
+ return getCodeIdFromEvents(txData?.events || []);
+};
+
+const useContracts = () => {
+ const [contractLoading, setContractLoading] = useState(false);
+ const [contractError, setContractError] = useState('');
+
+ const [messagesLoading, setMessagesLoading] = useState(false);
+
+ const { getDummyWallet } = useDummyWallet();
+ const { getChainInfo } = useGetChainInfo();
+
+ const getContractInfo = async ({
+ address,
+ baseURLs,
+ }: {
+ baseURLs: string[];
+ address: string;
+ }) => {
+ try {
+ setContractLoading(true);
+ setContractError('');
+ const res = await getContract(baseURLs, address);
+ setContractError('');
+ return {
+ data: await res.json(),
+ };
+ /* eslint-disable @typescript-eslint/no-explicit-any */
+ } catch (error: any) {
+ setContractError(error.message);
+ } finally {
+ setContractLoading(false);
+ }
+ return {
+ data: null,
+ };
+ };
+
+ const getContractMessages = async ({
+ address,
+ baseURLs,
+ }: {
+ address: string;
+ baseURLs: string[];
+ }) => {
+ let messages = [];
+ try {
+ setMessagesLoading(true);
+ setContractError('');
+ await queryContract(baseURLs, address, btoa(JSON.stringify(dummyQuery)));
+ return {
+ messages: [],
+ };
+ /* eslint-disable @typescript-eslint/no-explicit-any */
+ } catch (error: any) {
+ messages = extractContractMessages(error.message);
+ } finally {
+ setMessagesLoading(false);
+ }
+ return {
+ messages,
+ };
+ };
+
+ const getQueryContract = async ({
+ address,
+ baseURLs,
+ queryData,
+ }: GetQueryContractFunctionInputs) => {
+ try {
+ const respose = await queryContract(baseURLs, address, btoa(queryData));
+ return {
+ data: await respose.json(),
+ };
+ /* eslint-disable @typescript-eslint/no-explicit-any */
+ } catch (error: any) {
+ throw new Error(error.message);
+ }
+ };
+
+ const getExecuteMessages = async ({
+ rpcURLs,
+ chainID,
+ contractAddress,
+ }: {
+ rpcURLs: string[];
+ chainID: string;
+ contractAddress: string;
+ }) => {
+ const { dummyAddress, dummyWallet } = await getDummyWallet({ chainID });
+ const client = await connectWithSigner(rpcURLs, dummyWallet);
+ try {
+ await client.simulate(
+ dummyAddress,
+ [
+ {
+ typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
+ value: {
+ sender: dummyAddress,
+ contract: contractAddress,
+ msg: Buffer.from('{"": {}}'),
+ funds: [],
+ },
+ },
+ ],
+ undefined
+ );
+ } catch (error: any) {
+ console.log(error);
+ }
+ };
+
+ const getExecutionOutput = async ({
+ rpcURLs,
+ chainID,
+ contractAddress,
+ walletAddress,
+ msgs,
+ funds,
+ }: GetExecutionOutputFunctionInputs) => {
+ const offlineSigner = window.wallet.getOfflineSigner(chainID);
+ const client = await connectWithSigner(rpcURLs, offlineSigner);
+ const { feeAmount, feeCurrencies } = getChainInfo(chainID);
+ const { coinDecimals, coinDenom } = feeCurrencies[0];
+ const fee = {
+ amount: [
+ {
+ amount: (feeAmount * 10 ** coinDecimals).toString(),
+ denom: coinDenom,
+ },
+ ],
+ gas: GAS,
+ };
+ try {
+ const response = await client.signAndBroadcast(
+ walletAddress,
+ [
+ {
+ typeUrl: '/cosmwasm.wasm.v1.MsgExecuteContract',
+ value: {
+ sender: walletAddress,
+ contract: contractAddress,
+ msg: toUtf8(msgs),
+ funds,
+ },
+ },
+ ],
+ fee,
+ ''
+ );
+ return { txHash: response.transactionHash };
+ } catch (error: any) {
+ throw new Error(error?.message || 'Failed to execute contract');
+ }
+ };
+
+ const uploadContract = async ({
+ chainID,
+ address,
+ messages,
+ }: UploadContractFunctionInputs) => {
+ const { feeAmount, feeCurrencies, rpcURLs } = getChainInfo(chainID);
+ const { coinDecimals, coinDenom } = feeCurrencies[0];
+ const offlineSigner = window.wallet.getOfflineSigner(chainID);
+ const client = await connectWithSigner(rpcURLs, offlineSigner);
+
+ const fee = {
+ amount: [
+ {
+ amount: (feeAmount * 10 ** coinDecimals).toString(),
+ denom: coinDenom,
+ },
+ ],
+ gas: '1100000',
+ };
+ try {
+ const response = await client.signAndBroadcast(
+ address,
+ messages,
+ fee,
+ undefined,
+ undefined
+ );
+ const codeId = getCodeId(response);
+ return { codeId, txHash: response?.transactionHash };
+ } catch (error: any) {
+ throw new Error(error?.message || 'Failed to upload contract');
+ }
+ };
+
+ const instantiateContract = async ({
+ chainID,
+ codeId,
+ msg,
+ label,
+ admin,
+ funds,
+ }: InstantiateContractFunctionInputs) => {
+ const {
+ feeAmount,
+ feeCurrencies,
+ rpcURLs,
+ address: senderAddress,
+ } = getChainInfo(chainID);
+ const { coinDecimals, coinDenom } = feeCurrencies[0];
+ const offlineSigner = window.wallet.getOfflineSigner(chainID);
+ const client = await connectWithSigner(rpcURLs, offlineSigner);
+ const fee = {
+ amount: [
+ {
+ amount: (feeAmount * 10 ** coinDecimals).toString(),
+ denom: coinDenom,
+ },
+ ],
+ gas: GAS,
+ };
+ try {
+ const response = await client.signAndBroadcast(
+ senderAddress,
+ [
+ {
+ typeUrl: '/cosmwasm.wasm.v1.MsgInstantiateContract',
+ value: {
+ sender: senderAddress,
+ codeId: codeId,
+ msg: toUtf8(msg),
+ label: label,
+ funds: funds || [],
+ admin: admin,
+ },
+ },
+ ],
+ fee,
+ ''
+ );
+ const instantiateEvent = response.events.find(
+ (event) => event.type === 'instantiate'
+ );
+ const contractAddress =
+ instantiateEvent?.attributes.find(
+ (attr) => attr.key === '_contract_address'
+ )?.value || '';
+ const uploadedCodeId =
+ instantiateEvent?.attributes.find((attr) => attr.key === 'code_id')
+ ?.value || '';
+ return {
+ codeId: uploadedCodeId,
+ contractAddress,
+ txHash: response?.transactionHash,
+ };
+ } catch (error: any) {
+ throw new Error(error.message || 'Failed to instantiate');
+ }
+ };
+
+ const getChainAssets = (chainName: string) => {
+ const chainAssets = assetsData?.[chainName];
+ const assetsList: {
+ coinMinimalDenom: string;
+ decimals: number;
+ symbol: string;
+ }[] = [];
+ chainAssets?.forEach((asset) => {
+ assetsList.push({
+ symbol: asset.symbol,
+ decimals: asset.decimals,
+ coinMinimalDenom: asset.origin_denom,
+ });
+ });
+ return { assetsList };
+ };
+
+ return {
+ contractLoading,
+ getContractInfo,
+ contractError,
+ getContractMessages,
+ messagesLoading,
+ getQueryContract,
+ getExecuteMessages,
+ getExecutionOutput,
+ getChainAssets,
+ uploadContract,
+ instantiateContract,
+ };
+};
+
+export default useContracts;
diff --git a/frontend/src/custom-hooks/useDummyWallet.ts b/frontend/src/custom-hooks/useDummyWallet.ts
new file mode 100644
index 000000000..5410aca53
--- /dev/null
+++ b/frontend/src/custom-hooks/useDummyWallet.ts
@@ -0,0 +1,29 @@
+import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
+import { useState } from 'react';
+import useGetChainInfo from './useGetChainInfo';
+import { DUMMY_WALLET_MNEMONIC } from '@/utils/constants';
+
+export const useDummyWallet = () => {
+ const { getChainInfo } = useGetChainInfo();
+ const [dummyWallet, setDummyWallet] = useState
();
+ const [dummyAddress, setDummyAddress] = useState('');
+ const getDummyWallet = async ({ chainID }: { chainID: string }) => {
+ console.log(DUMMY_WALLET_MNEMONIC)
+ const { prefix } = getChainInfo(chainID);
+ if (DUMMY_WALLET_MNEMONIC) {
+ const wallet = await DirectSecp256k1HdWallet.fromMnemonic(
+ DUMMY_WALLET_MNEMONIC,
+ {
+ prefix,
+ }
+ );
+
+ setDummyWallet(wallet);
+
+ const { address } = (await wallet.getAccounts())[0];
+ setDummyAddress(address);
+ }
+ return { dummyWallet, dummyAddress };
+ };
+ return { getDummyWallet };
+};
diff --git a/frontend/src/custom-hooks/useGetChainInfo.ts b/frontend/src/custom-hooks/useGetChainInfo.ts
index 295142b50..5c45cf0b1 100644
--- a/frontend/src/custom-hooks/useGetChainInfo.ts
+++ b/frontend/src/custom-hooks/useGetChainInfo.ts
@@ -74,6 +74,7 @@ const useGetChainInfo = () => {
return {
restURLs: config.restURIs,
+ rpcURLs: config.rpcURIs,
baseURL: rest,
chainID,
aminoConfig: aminoCfg,
diff --git a/frontend/src/store/features/cosmwasm/cosmwasmService.ts b/frontend/src/store/features/cosmwasm/cosmwasmService.ts
new file mode 100644
index 000000000..98c1dd693
--- /dev/null
+++ b/frontend/src/store/features/cosmwasm/cosmwasmService.ts
@@ -0,0 +1,86 @@
+'use client';
+
+import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
+
+const getContractURL = (baseURL: string, address: string) =>
+ `${baseURL}/cosmwasm/wasm/v1/contract/${address}`;
+
+const getContractQueryURL = (
+ baseURL: string,
+ address: string,
+ queryData: string
+) => `${baseURL}/cosmwasm/wasm/v1/contract/${address}/smart/${queryData}`;
+
+export const getContract = async (
+ baseURLs: string[],
+ address: string
+): Promise => {
+ for (const url of baseURLs) {
+ const uri = getContractURL(url, address);
+ try {
+ const response = await fetch(uri);
+ if (response.status === 500) {
+ const errorBody = await response.json();
+ throw new Error(errorBody?.message || 'Failed to fetch contract', {
+ cause: 500,
+ });
+ } else if (response.ok) {
+ return response;
+ }
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ } catch (error: any) {
+ if (error.cause === 500) throw new Error(error.message);
+ continue;
+ }
+ }
+ throw new Error('Failed to fetch contract');
+};
+
+export const queryContract = async (
+ baseURLs: string[],
+ address: string,
+ queryData: string
+): Promise => {
+ for (const url of baseURLs) {
+ const uri = getContractQueryURL(url, address, queryData);
+ try {
+ const response = await fetch(uri);
+ if (response.status === 500) {
+ const errorBody = await response.json();
+ throw new Error(errorBody?.message || 'Failed to query contract', {
+ cause: 500,
+ });
+ } else if (response.ok) {
+ return response;
+ }
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ } catch (error: any) {
+ if (error.cause === 500) throw new Error(error.message);
+ continue;
+ }
+ }
+ throw new Error('Failed to query contract');
+};
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+export const connectWithSigner = async (urls: string[], offlineSigner: any) => {
+ for (const url of urls) {
+ try {
+ const signer = await SigningCosmWasmClient.connectWithSigner(
+ url,
+ offlineSigner
+ );
+ return signer;
+ /* eslint-disable @typescript-eslint/no-explicit-any */
+ } catch (error: any) {
+ console.error(`Error connecting to ${url}: ${error.message}`);
+ }
+ }
+ throw new Error('Unable to connect to any RPC URLs');
+};
+
+const result = {
+ contract: getContract,
+};
+
+export default result;
diff --git a/frontend/src/store/features/cosmwasm/cosmwasmSlice.ts b/frontend/src/store/features/cosmwasm/cosmwasmSlice.ts
new file mode 100644
index 000000000..55f6f5072
--- /dev/null
+++ b/frontend/src/store/features/cosmwasm/cosmwasmSlice.ts
@@ -0,0 +1,364 @@
+'use client';
+
+import { TxStatus } from '@/types/enums';
+import { ERR_UNKNOWN } from '@/utils/errors';
+import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { cloneDeep } from 'lodash';
+import { setError } from '../common/commonSlice';
+import axios from 'axios';
+import { cleanURL } from '@/utils/util';
+import { parseTxResult } from '@/utils/signing';
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+export const contractInfoEmptyState = {
+ admin: '',
+ label: '',
+ code_id: '',
+ creator: '',
+ created: {
+ block_height: '',
+ tx_index: '',
+ },
+ ibc_port_id: '',
+ extension: null,
+};
+
+interface Chain {
+ contractAddress: string;
+ contractInfo: ContractInfo;
+ txUpload: {
+ status: TxStatus;
+ error: string;
+ txHash: string;
+ txResponse: ParsedUploadTxnResponse;
+ };
+ txInstantiate: {
+ status: TxStatus;
+ error: string;
+ txHash: string;
+ txResponse: ParsedInstatiateTxnResponse;
+ };
+ txExecute: {
+ status: TxStatus;
+ error: string;
+ txHash: string;
+ txResponse: ParsedExecuteTxnResponse;
+ };
+ query: {
+ status: TxStatus;
+ error: string;
+ queryOutput: string;
+ };
+}
+
+interface Chains {
+ [key: string]: Chain;
+}
+
+interface CosmwasmState {
+ chains: Chains;
+ defaultState: Chain;
+}
+
+const initialState: CosmwasmState = {
+ chains: {},
+ defaultState: {
+ contractAddress: '',
+ contractInfo: contractInfoEmptyState,
+ txUpload: {
+ status: TxStatus.INIT,
+ error: '',
+ txResponse: {
+ code: 0,
+ fee: [],
+ transactionHash: '',
+ rawLog: '',
+ memo: '',
+ codeId: '',
+ },
+ txHash: '',
+ },
+ txInstantiate: {
+ status: TxStatus.INIT,
+ error: '',
+ txHash: '',
+ txResponse: {
+ code: 0,
+ fee: [],
+ transactionHash: '',
+ rawLog: '',
+ memo: '',
+ codeId: '',
+ contractAddress: '',
+ },
+ },
+ txExecute: {
+ status: TxStatus.INIT,
+ error: '',
+ txHash: '',
+ txResponse: {
+ code: 0,
+ fee: [],
+ transactionHash: '',
+ rawLog: '',
+ memo: '',
+ },
+ },
+ query: {
+ queryOutput: '',
+ status: TxStatus.INIT,
+ error: '',
+ },
+ },
+};
+
+export const queryContractInfo = createAsyncThunk(
+ 'cosmwasm/query-contract',
+ async (data: QueryContractInfoInputs, { rejectWithValue, dispatch }) => {
+ try {
+ const response = await data.getQueryContract(data);
+ return {
+ data: response.data,
+ chainID: data.chainID,
+ };
+ } catch (error: any) {
+ const errMsg = error?.message || 'Failed to query contract';
+ dispatch(
+ setError({
+ message: errMsg,
+ type: 'error',
+ })
+ );
+ return rejectWithValue(errMsg);
+ }
+ }
+);
+
+export const executeContract = createAsyncThunk(
+ 'cosmwasm/execute-contract',
+ async (data: ExecuteContractInputs, { rejectWithValue, dispatch }) => {
+ try {
+ const response = await data.getExecutionOutput(data);
+ const txn = await axios.get(
+ cleanURL(data.baseURLs[0]) + '/cosmos/tx/v1beta1/txs/' + response.txHash
+ );
+ const {
+ code,
+ transactionHash,
+ fee = [],
+ memo = '',
+ rawLog = '',
+ } = parseTxResult(txn?.data?.tx_response);
+ return {
+ data: { code, transactionHash, fee, memo, rawLog },
+ chainID: data.chainID,
+ };
+ } catch (error: any) {
+ const errMsg = error?.message || 'Failed to execute contract';
+ dispatch(
+ setError({
+ message: errMsg,
+ type: 'error',
+ })
+ );
+ return rejectWithValue(errMsg);
+ }
+ }
+);
+
+export const uploadCode = createAsyncThunk(
+ 'cosmwasm/upload-code',
+ async (data: UploadCodeInputs, { rejectWithValue, dispatch }) => {
+ try {
+ const response = await data.uploadContract(data);
+ const txn = await axios.get(
+ cleanURL(data.baseURLs[0]) + '/cosmos/tx/v1beta1/txs/' + response.txHash
+ );
+ const {
+ code,
+ transactionHash,
+ fee = [],
+ memo = '',
+ rawLog = '',
+ } = parseTxResult(txn?.data?.tx_response);
+ return {
+ data: {
+ code,
+ transactionHash,
+ fee,
+ memo,
+ rawLog,
+ codeId: response.codeId,
+ },
+ chainID: data.chainID,
+ };
+ } catch (error: any) {
+ const errMsg = error?.message || 'Failed to execute contract';
+ dispatch(
+ setError({
+ message: errMsg,
+ type: 'error',
+ })
+ );
+ return rejectWithValue(errMsg);
+ }
+ }
+);
+
+export const txInstantiateContract = createAsyncThunk(
+ 'cosmwasm/instantiate-contract',
+ async (data: InstantiateContractInputs, { rejectWithValue, dispatch }) => {
+ try {
+ const response = await data.instantiateContract(data);
+ const txn = await axios.get(
+ cleanURL(data.baseURLs[0]) + '/cosmos/tx/v1beta1/txs/' + response.txHash
+ );
+ const {
+ code,
+ transactionHash,
+ fee = [],
+ memo = '',
+ rawLog = '',
+ } = parseTxResult(txn?.data?.tx_response);
+ return {
+ data: {
+ code,
+ transactionHash,
+ fee,
+ memo,
+ rawLog,
+ codeId: response.codeId,
+ contractAddress: response.contractAddress,
+ },
+ chainID: data.chainID,
+ };
+ } catch (error: any) {
+ const errMsg = error?.message || 'Failed to execute contract';
+ dispatch(
+ setError({
+ message: errMsg,
+ type: 'error',
+ })
+ );
+ return rejectWithValue(errMsg);
+ }
+ }
+);
+
+export const cosmwasmSlice = createSlice({
+ name: 'cosmwasm',
+ initialState,
+ reducers: {
+ setContract: (
+ state,
+ action: PayloadAction<{
+ contractAddress: string;
+ contractInfo: ContractInfo;
+ chainID: string;
+ }>
+ ) => {
+ const chainID = action.payload.chainID;
+ if (!state.chains[chainID]) {
+ state.chains[chainID] = cloneDeep(initialState.defaultState);
+ }
+ state.chains[chainID].contractInfo = action.payload.contractInfo;
+ state.chains[chainID].contractAddress = action.payload.contractAddress;
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(queryContractInfo.pending, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ if (!state.chains[chainID]) {
+ state.chains[chainID] = cloneDeep(initialState.defaultState);
+ }
+ state.chains[chainID].query.status = TxStatus.PENDING;
+ state.chains[chainID].query.error = '';
+ })
+ .addCase(queryContractInfo.fulfilled, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ state.chains[chainID].query.status = TxStatus.IDLE;
+ state.chains[chainID].query.error = '';
+ state.chains[chainID].query.queryOutput = action.payload.data;
+ })
+ .addCase(queryContractInfo.rejected, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ state.chains[chainID].query.status = TxStatus.REJECTED;
+ state.chains[chainID].query.error = action.error.message || ERR_UNKNOWN;
+ state.chains[chainID].query.queryOutput = '{}';
+ });
+ builder
+ .addCase(executeContract.pending, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ if (!state.chains[chainID]) {
+ state.chains[chainID] = cloneDeep(initialState.defaultState);
+ }
+ state.chains[chainID].txExecute.status = TxStatus.PENDING;
+ state.chains[chainID].txExecute.error = '';
+ })
+ .addCase(executeContract.fulfilled, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ state.chains[chainID].txExecute.status = TxStatus.IDLE;
+ state.chains[chainID].txExecute.error = '';
+ state.chains[chainID].txExecute.txResponse = action.payload.data;
+ state.chains[chainID].txExecute.txHash =
+ action.payload.data.transactionHash;
+ })
+ .addCase(executeContract.rejected, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ state.chains[chainID].txExecute.status = TxStatus.REJECTED;
+ state.chains[chainID].txExecute.error =
+ action.error.message || ERR_UNKNOWN;
+ });
+ builder
+ .addCase(uploadCode.pending, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ if (!state.chains[chainID]) {
+ state.chains[chainID] = cloneDeep(initialState.defaultState);
+ }
+ state.chains[chainID].txUpload.status = TxStatus.PENDING;
+ state.chains[chainID].txUpload.error = '';
+ })
+ .addCase(uploadCode.fulfilled, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ state.chains[chainID].txUpload.status = TxStatus.IDLE;
+ state.chains[chainID].txUpload.error = '';
+ state.chains[chainID].txUpload.txResponse = action.payload.data;
+ state.chains[chainID].txUpload.txHash =
+ action.payload.data.transactionHash;
+ })
+ .addCase(uploadCode.rejected, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ state.chains[chainID].txUpload.status = TxStatus.REJECTED;
+ state.chains[chainID].txUpload.error =
+ action.error.message || ERR_UNKNOWN;
+ });
+ builder
+ .addCase(txInstantiateContract.pending, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ if (!state.chains[chainID]) {
+ state.chains[chainID] = cloneDeep(initialState.defaultState);
+ }
+ state.chains[chainID].txInstantiate.status = TxStatus.PENDING;
+ state.chains[chainID].txInstantiate.error = '';
+ })
+ .addCase(txInstantiateContract.fulfilled, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ state.chains[chainID].txInstantiate.status = TxStatus.IDLE;
+ state.chains[chainID].txInstantiate.error = '';
+ state.chains[chainID].txInstantiate.txResponse = action.payload.data;
+ state.chains[chainID].txInstantiate.txHash =
+ action.payload.data.transactionHash;
+ })
+ .addCase(txInstantiateContract.rejected, (state, action) => {
+ const chainID = action.meta.arg.chainID;
+ state.chains[chainID].txInstantiate.status = TxStatus.REJECTED;
+ state.chains[chainID].txInstantiate.error =
+ action.error.message || ERR_UNKNOWN;
+ });
+ },
+});
+
+export const { setContract } = cosmwasmSlice.actions;
+
+export default cosmwasmSlice.reducer;
diff --git a/frontend/src/store/features/wallet/walletSlice.ts b/frontend/src/store/features/wallet/walletSlice.ts
index 80d44e9d1..616c19aaf 100644
--- a/frontend/src/store/features/wallet/walletSlice.ts
+++ b/frontend/src/store/features/wallet/walletSlice.ts
@@ -63,7 +63,6 @@ export const establishWalletConnection = createAsyncThunk(
const networks = data.networks;
if (!isWalletInstalled(data.walletName)) {
dispatch(setError({ type: 'error', message: 'Wallet is not installed' }));
-
return rejectWithValue('wallet is not installed');
} else {
window.wallet.defaultOptions = {
diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts
index e961cf3db..c3f5bd4c2 100644
--- a/frontend/src/store/store.ts
+++ b/frontend/src/store/store.ts
@@ -15,6 +15,7 @@ import feegrantSlice from './features/feegrant/feegrantSlice';
import recentTransactionsSlice from './features/recent-transactions/recentTransactionsSlice';
import multiopsSlice from './features/multiops/multiopsSlice';
import swapsSlice from './features/swaps/swapsSlice';
+import cosmwasmSlice from './features/cosmwasm/cosmwasmSlice';
export const store = configureStore({
reducer: {
@@ -32,6 +33,7 @@ export const store = configureStore({
recentTransactions: recentTransactionsSlice,
multiops: multiopsSlice,
swaps: swapsSlice,
+ cosmwasm: cosmwasmSlice,
},
});
diff --git a/frontend/src/types/cosmwasm.d.ts b/frontend/src/types/cosmwasm.d.ts
new file mode 100644
index 000000000..68fe3a635
--- /dev/null
+++ b/frontend/src/types/cosmwasm.d.ts
@@ -0,0 +1,156 @@
+interface ContractInfo {
+ code_id: string;
+ creator: string;
+ admin: string;
+ label: string;
+ created: {
+ block_height: string;
+ tx_index: string;
+ };
+ ibc_port_id: string;
+ extension: string | null;
+}
+
+interface ContractInfoResponse {
+ address: string;
+ contract_info: ContractInfo;
+}
+
+interface AssetInfo {
+ coinMinimalDenom: string;
+ decimals: number;
+ symbol: string;
+}
+
+interface FundInfo {
+ amount: string;
+ denom: string;
+ decimals: number;
+}
+
+interface ParsedExecuteTxnResponse {
+ code: number;
+ fee: Coin[];
+ transactionHash: string;
+ rawLog: string;
+ memo: string;
+}
+
+interface ParsedUploadTxnResponse extends ParsedExecuteTxnResponse {
+ codeId: string;
+}
+
+interface ParsedInstatiateTxnResponse extends ParsedUploadTxnResponse {
+ contractAddress: string;
+}
+
+interface GetQueryContractFunctionInputs {
+ address: string;
+ baseURLs: string[];
+ queryData: string;
+}
+
+interface QueryContractInfoInputs {
+ address: string;
+ baseURLs: string[];
+ queryData: string;
+ chainID: string;
+ getQueryContract: ({
+ address,
+ baseURLs,
+ queryData,
+ }: GetQueryContractFunctionInputs) => Promise<{
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ data: any;
+ }>;
+}
+
+interface GetExecutionOutputFunctionInputs {
+ rpcURLs: string[];
+ chainID: string;
+ contractAddress: string;
+ walletAddress: string;
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ msgs: any;
+ funds:
+ | {
+ amount: string;
+ denom: string;
+ }[]
+ | undefined;
+}
+
+interface ExecuteContractInputs {
+ rpcURLs: string[];
+ chainID: string;
+ contractAddress: string;
+ walletAddress: string;
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ msgs: any;
+ baseURLs: string[];
+ funds: { amount: string; denom: string }[] | undefined;
+ getExecutionOutput: ({
+ rpcURLs,
+ chainID,
+ contractAddress,
+ walletAddress,
+ msgs,
+ funds,
+ }: GetExecutionOutputFunctionInputs) => Promise<{
+ txHash: string;
+ }>;
+}
+
+interface UploadContractFunctionInputs {
+ chainID: string;
+ address: string;
+ messages: Msg[];
+}
+
+interface UploadCodeInputs {
+ chainID: string;
+ address: string;
+ messages: Msg[];
+ baseURLs: string[];
+ uploadContract: ({
+ chainID,
+ address,
+ messages,
+ }: UploadContractFunctionInputs) => Promise<{
+ codeId: string;
+ txHash: string;
+ }>;
+}
+
+interface InstantiateContractFunctionInputs {
+ chainID: string;
+ codeId: number;
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ msg: any;
+ label: string;
+ admin?: string;
+ funds?: Coin[];
+}
+
+interface InstantiateContractInputs {
+ chainID: string;
+ codeId: number;
+ /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
+ msg: any;
+ label: string;
+ admin?: string;
+ funds?: Coin[];
+ baseURLs: string[];
+ instantiateContract: ({
+ chainID,
+ codeId,
+ msg,
+ label,
+ admin,
+ funds,
+ }: InstantiateContractFunctionInputs) => Promise<{
+ codeId: string;
+ contractAddress: string;
+ txHash: string;
+ }>;
+}
diff --git a/frontend/src/types/store.d.ts b/frontend/src/types/store.d.ts
index 8bbc337af..9c96da62c 100644
--- a/frontend/src/types/store.d.ts
+++ b/frontend/src/types/store.d.ts
@@ -1,5 +1,6 @@
interface BasicChainInfo {
restURLs: string[];
+ rpcURLs: string[];
baseURL: string;
chainID: string;
aminoConfig: AminoConfig;
diff --git a/frontend/src/utils/constants.ts b/frontend/src/utils/constants.ts
index 7f1586994..de04668c8 100644
--- a/frontend/src/utils/constants.ts
+++ b/frontend/src/utils/constants.ts
@@ -184,6 +184,14 @@ export const SIDENAV_MENU_ITEMS = [
authzSupported: false,
isMetaMaskSupports: false,
},
+ {
+ name: 'CosmWasm Contracts',
+ icon: '/cosmwasm-icon.svg',
+ activeIcon: '/cosmwasm-icon-active.svg',
+ link: '/cosmwasm',
+ authzSupported: false,
+ isMetaMaskSupports: false,
+ },
];
export const ALL_NETWORKS_ICON = '/all-networks-icon.png';
export const CHANGE_NETWORK_ICON = '/switch-icon.svg';
@@ -321,3 +329,5 @@ export const MULTIOPS_SAMPLE_FILES = {
vote: 'https://raw.githubusercontent.com/vitwit/resolute/a6a02cc1b74ee34604e6df35cfce7a46c39980ea/frontend/src/example-files/vote.csv',
};
export const SWAP_ROUTE_ERROR = 'Failed to fetch routes.';
+export const DUMMY_WALLET_MNEMONIC =
+ process.env.NEXT_PUBLIC_DUMMY_WALLET_MNEMONIC || '';
diff --git a/frontend/src/utils/util.ts b/frontend/src/utils/util.ts
index 608a6a4d8..517e61b1f 100644
--- a/frontend/src/utils/util.ts
+++ b/frontend/src/utils/util.ts
@@ -77,6 +77,10 @@ export const getSelectedPartFromURL = (urlParts: string[]): string => {
return 'History';
case 'validator':
return 'Staking';
+ case 'cosmwasm':
+ return 'Cosmwasm';
+ case 'multiops':
+ return 'Multiops';
default:
return 'Overview';
}
@@ -467,3 +471,45 @@ export function formatValidatorStatsValue(
? '-'
: Number(numValue.toFixed(precision)).toLocaleString();
}
+
+export function extractContractMessages(inputString: string): string[] {
+ const pattern: RegExp = /`(\w+)`/g;
+
+ const matches: string[] = [];
+ let match: RegExpExecArray | null;
+ while ((match = pattern.exec(inputString)) !== null) {
+ matches.push(match[1]);
+ }
+
+ return matches;
+}
+
+export const getFormattedFundsList = (
+ funds: FundInfo[],
+ fundsInput: string,
+ attachFundType: string
+) => {
+ if (attachFundType === 'select') {
+ const result: {
+ denom: string;
+ amount: string;
+ }[] = [];
+ funds.forEach((fund) => {
+ if (fund.amount.length) {
+ result.push({
+ denom: fund.denom,
+ amount: (Number(fund.amount) * 10 ** fund.decimals).toString(),
+ });
+ }
+ });
+ return result;
+ } else if (attachFundType === 'json') {
+ try {
+ const parsedFunds = JSON.parse(fundsInput);
+ return parsedFunds;
+ /* eslint-disable @typescript-eslint/no-explicit-any */
+ } catch (error: any) {
+ console.log(error);
+ }
+ }
+};
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 385484fdf..2f5b51f57 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -170,6 +170,22 @@
long "^4.0.0"
pako "^2.0.2"
+"@cosmjs/cosmwasm-stargate@0.32.2":
+ version "0.32.2"
+ resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.32.2.tgz#32aca8b4c2043cd1bc91cf4d0225b268c166e421"
+ integrity sha512-OwJHzIx2CoJS6AULxOpNR6m+CI0GXxy8z9svHA1ZawzNM3ZGlL0GvHdhmF0WkpX4E7UdrYlJSLpKcgg5Fo6i7Q==
+ dependencies:
+ "@cosmjs/amino" "^0.32.2"
+ "@cosmjs/crypto" "^0.32.2"
+ "@cosmjs/encoding" "^0.32.2"
+ "@cosmjs/math" "^0.32.2"
+ "@cosmjs/proto-signing" "^0.32.2"
+ "@cosmjs/stargate" "^0.32.2"
+ "@cosmjs/tendermint-rpc" "^0.32.2"
+ "@cosmjs/utils" "^0.32.2"
+ cosmjs-types "^0.9.0"
+ pako "^2.0.2"
+
"@cosmjs/cosmwasm-stargate@^0.32.2":
version "0.32.3"
resolved "https://registry.yarnpkg.com/@cosmjs/cosmwasm-stargate/-/cosmwasm-stargate-0.32.3.tgz#26a110a6bb0c15fdeef647e3433bd9553a1acd5f"
@@ -1933,6 +1949,13 @@
dependencies:
"@types/unist" "^2"
+"@types/node-gzip@1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@types/node-gzip/-/node-gzip-1.1.0.tgz#99a7dfab7c0eec545658f3d736e8d6939ed7161e"
+ integrity sha512-j7cGb6HIOZbDx3sqe9/9VAPeSvyt143yu5k35gzRXE3mxEgK6BOZ6BAiJ3ToXBcJqLzL9Cr53dav21jlp3f9gw==
+ dependencies:
+ "@types/node" "*"
+
"@types/node@*", "@types/node@>=13.7.0":
version "20.11.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.5.tgz#be10c622ca7fcaa3cf226cf80166abc31389d86e"
@@ -4668,6 +4691,11 @@ node-gyp-build@^4.2.0:
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd"
integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==
+node-gzip@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/node-gzip/-/node-gzip-1.1.2.tgz#245bd171b31ce7c7f50fc4cd0ca7195534359afb"
+ integrity sha512-ZB6zWpfZHGtxZnPMrJSKHVPrRjURoUzaDbLFj3VO70mpLTW5np96vXyHwft4Id0o+PYIzgDkBUjIzaNHhQ8srw==
+
node-releases@^2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b"