Simulate gas limit for cosmos transactions (#111)

* Simulate gas limit for cosmos transactions

* Reject request if funds are not sufficient

* Fix submit button disable condition

* Dont estimate gas if funds are not sufficient for evm chains

* Handle review changes
This commit is contained in:
shreerang6921 2024-04-23 15:57:47 +05:30 committed by Nabarun Gogoi
parent b1a0831e78
commit db0b21ddd1
4 changed files with 116 additions and 33 deletions

View File

@ -1,3 +1,4 @@
WALLET_CONNECT_PROJECT_ID=
DEFAULT_GAS_LIMIT=
DEFAULT_GAS_PRICE=
# Reference: https://github.com/cosmos/cosmos-sdk/issues/16020
DEFAULT_GAS_ADJUSTMENT=2

View File

@ -2,8 +2,8 @@
declare module 'react-native-config' {
export interface NativeConfig {
WALLET_CONNECT_PROJECT_ID: string;
DEFAULT_GAS_LIMIT: string;
DEFAULT_GAS_PRICE: string;
DEFAULT_GAS_ADJUSTMENT: string;
}
export const Config: NativeConfig;

View File

@ -20,6 +20,7 @@ import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
import {
calculateFee,
GasPrice,
MsgSendEncodeObject,
SigningStargateClient,
} from '@cosmjs/stargate';
@ -38,6 +39,8 @@ import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155 } from '../utils/constants';
import TxErrorDialog from '../components/TxErrorDialog';
const MEMO = 'Sending signed tx from Laconic Wallet';
type SignRequestProps = NativeStackScreenProps<
StackParamsList,
'ApproveTransaction'
@ -60,21 +63,54 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const [isTxLoading, setIsTxLoading] = useState(false);
const [cosmosStargateClient, setCosmosStargateClient] =
useState<SigningStargateClient>();
const [fees, setFees] = useState('');
const [cosmosGasLimit, setCosmosGasLimit] = useState(
Config.DEFAULT_GAS_LIMIT,
);
const [txError, setTxError] = useState<string>('');
const [fees, setFees] = useState<string>();
const [cosmosGasLimit, setCosmosGasLimit] = useState<string>();
const [txError, setTxError] = useState<string>();
const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false);
const [ethGasPrice, setEthGasPrice] = useState<string>();
const [ethGasLimit, setEthGasLimit] = useState<string>();
const isSufficientFunds = useMemo(() => {
if (!transaction.value) {
return;
}
if (!balance) {
return;
}
const amountBigNum = BigNumber.from(String(transaction.value));
const balanceBigNum = BigNumber.from(balance);
if (amountBigNum.gte(balanceBigNum)) {
return false;
} else {
return true;
}
}, [balance, transaction]);
const requestedNetwork = networksData.find(
networkData =>
`${networkData.namespace}:${networkData.chainId}` === chainId,
);
const namespace = requestedNetwork!.namespace;
const sendMsg: MsgSendEncodeObject = useMemo(() => {
return {
typeUrl: '/cosmos.bank.v1beta1.MsgSend',
value: {
fromAddress: transaction.from,
toAddress: transaction.to,
amount: [
{
amount: String(transaction.value),
denom: requestedNetwork!.nativeDenom!,
},
],
},
};
}, [requestedNetwork, transaction]);
useEffect(() => {
if (namespace !== COSMOS) {
return;
@ -149,6 +185,11 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
const gasPrice = GasPrice.fromString(
requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
);
if (!cosmosGasLimit) {
return;
}
const cosmosFees = calculateFee(Number(cosmosGasLimit), gasPrice);
setFees(cosmosFees.amount[0].amount);
@ -174,7 +215,6 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
try {
const response = await approveWalletConnectRequest({
networksData,
requestEvent,
account,
namespace,
@ -185,11 +225,11 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
// This amount is total fees required for transaction
amount: [
{
amount: fees,
amount: fees!,
denom: requestedNetwork!.nativeDenom!,
},
],
gas: cosmosGasLimit,
gas: cosmosGasLimit!,
},
ethGasLimit: transaction.gasLimit
? String(transaction.gasLimit)
@ -197,6 +237,8 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
ethGasPrice: transaction.gasPrice
? String(transaction.gasPrice)
: ethGasPrice,
sendMsg,
memo: MEMO,
});
const { topic } = requestEvent;
@ -273,6 +315,10 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
return;
}
if (!isSufficientFunds) {
return;
}
if (!provider) {
return;
}
@ -296,7 +342,39 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
};
getEthGas();
}, [provider, transaction]);
}, [provider, transaction, isSufficientFunds]);
useEffect(() => {
const getCosmosGas = async () => {
if (!cosmosStargateClient) {
return;
}
if (!isSufficientFunds) {
return;
}
const gasEstimation = await cosmosStargateClient.simulate(
transaction.from!,
[sendMsg],
MEMO,
);
setCosmosGasLimit(
String(
Math.round(gasEstimation * Number(Config.DEFAULT_GAS_ADJUSTMENT)),
),
);
};
getCosmosGas();
}, [cosmosStargateClient, isSufficientFunds, sendMsg, transaction]);
useEffect(() => {
if (balance && !isSufficientFunds) {
setTxError('Insufficient funds');
setIsTxErrorDialogOpen(true);
}
}, [isSufficientFunds, balance]);
return (
<>
@ -384,7 +462,7 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
mode="contained"
onPress={acceptRequestHandler}
loading={isTxLoading}
disabled={!balance && !cosmosStargateClient}>
disabled={!balance || !fees}>
{isTxLoading ? 'Processing' : 'Yes'}
</Button>
<Button
@ -397,9 +475,15 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
</>
)}
<TxErrorDialog
error={txError}
error={txError!}
visible={isTxErrorDialogOpen}
hideDialog={() => setIsTxErrorDialogOpen(false)}
hideDialog={() => {
setIsTxErrorDialogOpen(false);
if (!isSufficientFunds) {
rejectRequestHandler();
navigation.navigate('Laconic');
}
}}
/>
</>
);

View File

@ -4,16 +4,19 @@ import { Wallet, providers } from 'ethers';
import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils';
import { SignClientTypes } from '@walletconnect/types';
import { getSdkError } from '@walletconnect/utils';
import { SigningStargateClient, coins, StdFee } from '@cosmjs/stargate';
import {
SigningStargateClient,
StdFee,
MsgSendEncodeObject,
} from '@cosmjs/stargate';
import { EIP155_SIGNING_METHODS } from './EIP155Data';
import { signDirectMessage, signEthMessage } from '../sign-message';
import { Account, NetworksDataState } from '../../types';
import { Account } from '../../types';
import { getMnemonic, getPathKey } from '../misc';
import { getCosmosAccounts } from '../accounts';
export async function approveWalletConnectRequest({
networksData,
requestEvent,
account,
namespace,
@ -23,8 +26,9 @@ export async function approveWalletConnectRequest({
cosmosFee,
ethGasLimit,
ethGasPrice,
sendMsg,
memo,
}: {
networksData: NetworksDataState[];
requestEvent: SignClientTypes.EventArguments['session_request'];
account: Account;
namespace: string;
@ -34,15 +38,12 @@ export async function approveWalletConnectRequest({
cosmosFee?: StdFee;
ethGasLimit?: string;
ethGasPrice?: string;
sendMsg?: MsgSendEncodeObject;
memo?: string;
}) {
const { params, id } = requestEvent;
const { request } = params;
const requestChainId = requestEvent.params.chainId;
const requestedChain = networksData.find(
networkData => networkData.chainId === requestChainId.split(':')[1],
);
const path = (await getPathKey(`${namespace}:${chainId}`, account.index))
.path;
const mnemonic = await getMnemonic();
@ -128,12 +129,6 @@ export async function approveWalletConnectRequest({
});
case 'cosmos_sendTokens':
const amount = coins(
request.params[0].value,
requestedChain!.nativeDenom!,
);
const receiverAddress = request.params[0].to;
if (!(provider instanceof SigningStargateClient)) {
throw new Error('Cosmos stargate client not found');
}
@ -142,12 +137,15 @@ export async function approveWalletConnectRequest({
throw new Error('Cosmos fee not found');
}
const result = await provider.sendTokens(
if (!sendMsg) {
throw new Error('Message to be sent not found');
}
const result = await provider.signAndBroadcast(
address,
receiverAddress,
amount,
[sendMsg],
cosmosFee,
'Sending signed tx from Laconic Wallet',
memo,
);
return formatJsonRpcResult(id, {