forked from cerc-io/laconic-wallet
Add support for sending cosmos transactions (#116)
* Update readme to contain instructions for creating file * Add method for sending transaction to chain * Rename approve transaction component to approve transfer * Create component for approving transactions * Display transaction message in Approve transaction component * Install registry-sdk * Display gas limit on receiving transaction request * Add functionality for sending transaction to chain * Add memo in simulate gas method * Remove unnecessary TODO * Display error in dialog box * Add support for onboarding transaction * Pass address of signer in wallet connect request from app
This commit is contained in:
parent
adf9efe6f8
commit
83723d4086
1
.npmrc
Normal file
1
.npmrc
Normal file
@ -0,0 +1 @@
|
||||
@cerc-io:registry=https://git.vdb.to/api/packages/cerc-io/npm/
|
15
README.md
15
README.md
@ -25,7 +25,7 @@
|
||||
|
||||
- Install Android SDK
|
||||
|
||||
- Open Android Studio -> Configure -> SDK Manager -> SDK PLatfrom Tab.
|
||||
- Open Android Studio -> Configure -> SDK Manager -> SDK Platform Tab.
|
||||
|
||||
- Check the box next to "Show Package Details" in the bottom right corner. Look for and expand the Android 13 (Tiramisu) entry, then make sure the following items are checked:
|
||||
|
||||
@ -94,19 +94,26 @@
|
||||
WALLET_CONNECT_PROJECT_ID=39bc93c...
|
||||
```
|
||||
|
||||
5. Set up the Android device
|
||||
5. Add SDK directory to project
|
||||
- Inside the [`android`](./android/) directory, create a file `local.properties` and add your Android SDK path
|
||||
```
|
||||
sdk.dir = /home/USERNAME/Android/Sdk
|
||||
```
|
||||
Where `USERNAME` is your linux username
|
||||
|
||||
6. Set up the Android device
|
||||
|
||||
- For a physical device, refer to the [React Native documentation for running on a physical device](https://reactnative.dev/docs/running-on-device)
|
||||
|
||||
- For a virtual device, continue with the steps
|
||||
|
||||
6. Start the application
|
||||
7. Start the application
|
||||
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
7. Press `a` to run the application on android
|
||||
8. Press `a` to run the application on android
|
||||
|
||||
## Flow for the app
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cerc-io/registry-sdk": "^0.2.1",
|
||||
"@cosmjs/amino": "^0.32.3",
|
||||
"@cosmjs/crypto": "^0.32.3",
|
||||
"@cosmjs/proto-signing": "^0.32.3",
|
||||
@ -28,6 +29,7 @@
|
||||
"@walletconnect/react-native-compat": "^2.11.2",
|
||||
"@walletconnect/utils": "^2.12.2",
|
||||
"@walletconnect/web3wallet": "^1.10.2",
|
||||
"assert": "^2.1.0",
|
||||
"chain-registry": "^1.41.2",
|
||||
"cosmjs-types": "^0.9.0",
|
||||
"ethers": "5.7.2",
|
||||
|
36
src/App.tsx
36
src/App.tsx
@ -22,16 +22,18 @@ import HomeScreen from './screens/HomeScreen';
|
||||
import SignRequest from './screens/SignRequest';
|
||||
import AddSession from './screens/AddSession';
|
||||
import WalletConnect from './screens/WalletConnect';
|
||||
import ApproveTransaction from './screens/ApproveTransaction';
|
||||
import { StackParamsList } from './types';
|
||||
import { web3wallet } from './utils/wallet-connect/WalletConnectUtils';
|
||||
import { EIP155_SIGNING_METHODS } from './utils/wallet-connect/EIP155Data';
|
||||
import { getSignParamsMessage } from './utils/wallet-connect/helpers';
|
||||
import ApproveTransaction from './screens/ApproveTransaction';
|
||||
import ApproveTransfer from './screens/ApproveTransfer';
|
||||
import AddNetwork from './screens/AddNetwork';
|
||||
import EditNetwork from './screens/EditNetwork';
|
||||
import { COSMOS, EIP155 } from './utils/constants';
|
||||
import { useNetworks } from './context/NetworksContext';
|
||||
import { NETWORK_METHODS } from './utils/wallet-connect/common-data';
|
||||
import { COSMOS_METHODS } from './utils/wallet-connect/COSMOSData';
|
||||
|
||||
const Stack = createNativeStackNavigator<StackParamsList>();
|
||||
|
||||
@ -114,7 +116,7 @@ const App = (): React.JSX.Element => {
|
||||
break;
|
||||
|
||||
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
|
||||
navigation.navigate('ApproveTransaction', {
|
||||
navigation.navigate('ApproveTransfer', {
|
||||
transaction: request.params[0],
|
||||
requestEvent,
|
||||
requestSessionData,
|
||||
@ -131,7 +133,7 @@ const App = (): React.JSX.Element => {
|
||||
});
|
||||
break;
|
||||
|
||||
case 'cosmos_signDirect':
|
||||
case COSMOS_METHODS.COSMOS_SIGN_DIRECT:
|
||||
const message = {
|
||||
txbody: TxBody.toJSON(
|
||||
TxBody.decode(
|
||||
@ -157,7 +159,7 @@ const App = (): React.JSX.Element => {
|
||||
});
|
||||
break;
|
||||
|
||||
case 'cosmos_signAmino':
|
||||
case COSMOS_METHODS.COSMOS_SIGN_AMINO:
|
||||
navigation.navigate('SignRequest', {
|
||||
namespace: COSMOS,
|
||||
address: request.params.signerAddress,
|
||||
@ -167,14 +169,23 @@ const App = (): React.JSX.Element => {
|
||||
});
|
||||
break;
|
||||
|
||||
case 'cosmos_sendTokens':
|
||||
navigation.navigate('ApproveTransaction', {
|
||||
case COSMOS_METHODS.COSMOS_SEND_TOKENS:
|
||||
navigation.navigate('ApproveTransfer', {
|
||||
transaction: request.params[0],
|
||||
requestEvent,
|
||||
requestSessionData,
|
||||
});
|
||||
break;
|
||||
|
||||
case COSMOS_METHODS.COSMOS_SEND_TRANSACTION:
|
||||
navigation.navigate('ApproveTransaction', {
|
||||
transactionMessage: request.params.transactionMessage,
|
||||
signer: request.params.signer,
|
||||
requestEvent,
|
||||
requestSessionData,
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Invalid method');
|
||||
}
|
||||
@ -268,10 +279,10 @@ const App = (): React.JSX.Element => {
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
name="ApproveTransaction"
|
||||
component={ApproveTransaction}
|
||||
name="ApproveTransfer"
|
||||
component={ApproveTransfer}
|
||||
options={{
|
||||
title: 'Approve transaction',
|
||||
title: 'Approve transfer',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@ -288,6 +299,13 @@ const App = (): React.JSX.Element => {
|
||||
title: 'Edit Network',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ApproveTransaction"
|
||||
component={ApproveTransaction}
|
||||
options={{
|
||||
title: 'Approve Transaction',
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
<PairingModal
|
||||
visible={modalVisible}
|
||||
|
@ -1,103 +1,62 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Image, ScrollView, View } from 'react-native';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Button,
|
||||
Text,
|
||||
Appbar,
|
||||
TextInput,
|
||||
} from 'react-native-paper';
|
||||
import { providers, BigNumber } from 'ethers';
|
||||
import { Button, Text, TextInput } from 'react-native-paper';
|
||||
import { SvgUri } from 'react-native-svg';
|
||||
import Config from 'react-native-config';
|
||||
import { Deferrable } from 'ethers/lib/utils';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import {
|
||||
NativeStackNavigationProp,
|
||||
NativeStackScreenProps,
|
||||
} from '@react-navigation/native-stack';
|
||||
import { getHeaderTitle } from '@react-navigation/elements';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
|
||||
import {
|
||||
calculateFee,
|
||||
GasPrice,
|
||||
MsgSendEncodeObject,
|
||||
SigningStargateClient,
|
||||
} from '@cosmjs/stargate';
|
||||
import { LaconicClient } from '@cerc-io/registry-sdk/dist/laconic-client';
|
||||
import { GasPrice, calculateFee } from '@cosmjs/stargate';
|
||||
import { formatJsonRpcError } from '@json-rpc-tools/utils';
|
||||
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
import { Account, StackParamsList } from '../types';
|
||||
import AccountDetails from '../components/AccountDetails';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { COSMOS, IS_NUMBER_REGEX } from '../utils/constants';
|
||||
import { retrieveSingleAccount } from '../utils/accounts';
|
||||
import { getPathKey } from '../utils/misc';
|
||||
import {
|
||||
approveWalletConnectRequest,
|
||||
rejectWalletConnectRequest,
|
||||
WalletConnectRequests,
|
||||
approveWalletConnectRequest,
|
||||
} from '../utils/wallet-connect/wallet-connect-requests';
|
||||
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import DataBox from '../components/DataBox';
|
||||
import { getPathKey } from '../utils/misc';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
import { COSMOS, EIP155, IS_NUMBER_REGEX } from '../utils/constants';
|
||||
import { MEMO } from './ApproveTransfer';
|
||||
import TxErrorDialog from '../components/TxErrorDialog';
|
||||
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
|
||||
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData';
|
||||
|
||||
const MEMO = 'Sending signed tx from Laconic Wallet';
|
||||
// Reference: https://ethereum.org/en/developers/docs/gas/#what-is-gas-limit
|
||||
const ETH_MINIMUM_GAS = 21000;
|
||||
|
||||
type SignRequestProps = NativeStackScreenProps<
|
||||
type ApproveTransactionProps = NativeStackScreenProps<
|
||||
StackParamsList,
|
||||
'ApproveTransaction'
|
||||
>;
|
||||
|
||||
const ApproveTransaction = ({ route }: SignRequestProps) => {
|
||||
const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
|
||||
const { networksData } = useNetworks();
|
||||
|
||||
const requestSession = route.params.requestSessionData;
|
||||
const requestName = requestSession.peer.metadata.name;
|
||||
const requestIcon = requestSession.peer.metadata.icons[0];
|
||||
const requestURL = requestSession.peer.metadata.url;
|
||||
const transaction = route.params.transaction;
|
||||
const transactionMessage = route.params.transactionMessage;
|
||||
const signer = route.params.signer;
|
||||
const requestEvent = route.params.requestEvent;
|
||||
const chainId = requestEvent.params.chainId;
|
||||
const requestMethod = requestEvent.params.request.method;
|
||||
const requestEventId = requestEvent.id;
|
||||
const topic = requestEvent.topic;
|
||||
|
||||
const [account, setAccount] = useState<Account>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [balance, setBalance] = useState<string>('');
|
||||
const [isTxLoading, setIsTxLoading] = useState(false);
|
||||
const [cosmosStargateClient, setCosmosStargateClient] =
|
||||
useState<SigningStargateClient>();
|
||||
const [fees, setFees] = useState<string>();
|
||||
useState<LaconicClient>();
|
||||
const [cosmosGasLimit, setCosmosGasLimit] = useState<string>();
|
||||
const [fees, setFees] = useState<string>();
|
||||
const [txError, setTxError] = useState<string>();
|
||||
const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false);
|
||||
const [ethGasPrice, setEthGasPrice] = useState<BigNumber | null>();
|
||||
const [ethGasLimit, setEthGasLimit] = useState<BigNumber>();
|
||||
const [ethMaxFee, setEthMaxFee] = useState<BigNumber | null>();
|
||||
const [ethMaxPriorityFee, setEthMaxPriorityFee] =
|
||||
useState<BigNumber | null>();
|
||||
|
||||
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 navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
const requestedNetwork = networksData.find(
|
||||
networkData =>
|
||||
@ -105,22 +64,6 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
|
||||
);
|
||||
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;
|
||||
@ -144,41 +87,21 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
|
||||
);
|
||||
|
||||
try {
|
||||
const client = await SigningStargateClient.connectWithSigner(
|
||||
const client = await LaconicClient.connectWithSigner(
|
||||
requestedNetwork?.rpcUrl!,
|
||||
sender,
|
||||
);
|
||||
|
||||
setCosmosStargateClient(client);
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
const response = formatJsonRpcError(requestEventId, error.message);
|
||||
await web3wallet!.respondSessionRequest({ topic, response });
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
setClient();
|
||||
}, [account, requestedNetwork, chainId, namespace]);
|
||||
|
||||
const provider = useMemo(() => {
|
||||
if (namespace === EIP155) {
|
||||
if (!requestedNetwork) {
|
||||
throw new Error('Requested chain not supported');
|
||||
}
|
||||
try {
|
||||
const ethProvider = new providers.JsonRpcProvider(
|
||||
requestedNetwork.rpcUrl,
|
||||
);
|
||||
|
||||
return ethProvider;
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
}
|
||||
}, [requestedNetwork, namespace]);
|
||||
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
}, [account, requestedNetwork, chainId, namespace, requestEventId, topic]);
|
||||
|
||||
const retrieveData = useCallback(
|
||||
async (requestAddress: string) => {
|
||||
@ -198,245 +121,8 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Set loading to false when gas values for requested chain are fetched
|
||||
// If requested chain is EVM compatible, the cosmos gas values will be undefined and vice-versa, hence the condition checks only one of them at the same time
|
||||
if (
|
||||
// If requested chain is EVM compatible, set loading to false when ethMaxFee and ethPriorityFee have been populated
|
||||
(ethMaxFee !== undefined && ethMaxPriorityFee !== undefined) ||
|
||||
// Or if requested chain is a cosmos chain, set loading to false when cosmosGasLimit has been populated
|
||||
cosmosGasLimit !== undefined
|
||||
) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [ethMaxFee, ethMaxPriorityFee, cosmosGasLimit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (namespace === EIP155) {
|
||||
const ethFees = BigNumber.from(ethGasLimit ?? 0)
|
||||
.mul(BigNumber.from(ethMaxFee ?? ethGasPrice ?? 0))
|
||||
.toString();
|
||||
setFees(ethFees);
|
||||
} else {
|
||||
const gasPrice = GasPrice.fromString(
|
||||
requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
|
||||
);
|
||||
|
||||
if (!cosmosGasLimit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cosmosFees = calculateFee(Number(cosmosGasLimit), gasPrice);
|
||||
|
||||
setFees(cosmosFees.amount[0].amount);
|
||||
}
|
||||
}, [
|
||||
transaction,
|
||||
namespace,
|
||||
ethGasLimit,
|
||||
ethGasPrice,
|
||||
cosmosGasLimit,
|
||||
requestedNetwork,
|
||||
ethMaxFee,
|
||||
]);
|
||||
useEffect(() => {
|
||||
retrieveData(transaction.from!);
|
||||
}, [retrieveData, transaction]);
|
||||
|
||||
const isEIP1559 = useMemo(() => {
|
||||
if (cosmosGasLimit) {
|
||||
return;
|
||||
}
|
||||
if (ethMaxFee !== null && ethMaxPriorityFee !== null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [cosmosGasLimit, ethMaxFee, ethMaxPriorityFee]);
|
||||
|
||||
const acceptRequestHandler = async () => {
|
||||
setIsTxLoading(true);
|
||||
try {
|
||||
if (!account) {
|
||||
throw new Error('account not found');
|
||||
}
|
||||
|
||||
if (ethGasLimit && ethGasLimit.lt(ETH_MINIMUM_GAS)) {
|
||||
throw new Error(`Atleast ${ETH_MINIMUM_GAS} gas limit is required`);
|
||||
}
|
||||
|
||||
if (ethMaxFee && ethMaxPriorityFee && ethMaxFee.lte(ethMaxPriorityFee)) {
|
||||
throw new Error(
|
||||
`Max fee per gas (${ethMaxFee.toNumber()}) cannot be lower than or equal to max priority fee per gas (${ethMaxPriorityFee.toNumber()})`,
|
||||
);
|
||||
}
|
||||
|
||||
let options: WalletConnectRequests;
|
||||
|
||||
switch (requestMethod) {
|
||||
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
|
||||
if (
|
||||
ethMaxFee === undefined ||
|
||||
ethMaxPriorityFee === undefined ||
|
||||
ethGasPrice === undefined
|
||||
) {
|
||||
throw new Error('Gas values not found');
|
||||
}
|
||||
|
||||
options = {
|
||||
type: 'eth_sendTransaction',
|
||||
provider: provider!,
|
||||
ethGasLimit: BigNumber.from(ethGasLimit),
|
||||
ethGasPrice: ethGasPrice ? ethGasPrice.toHexString() : null,
|
||||
maxFeePerGas: ethMaxFee,
|
||||
maxPriorityFeePerGas: ethMaxPriorityFee,
|
||||
};
|
||||
break;
|
||||
case COSMOS_METHODS.COSMOS_SEND_TOKENS:
|
||||
if (!cosmosStargateClient) {
|
||||
throw new Error('Cosmos stargate client not found');
|
||||
}
|
||||
|
||||
options = {
|
||||
type: 'cosmos_sendTokens',
|
||||
signingStargateClient: cosmosStargateClient,
|
||||
// StdFee object
|
||||
cosmosFee: {
|
||||
// This amount is total fees required for transaction
|
||||
amount: [
|
||||
{
|
||||
amount: fees!,
|
||||
denom: requestedNetwork!.nativeDenom!,
|
||||
},
|
||||
],
|
||||
gas: cosmosGasLimit!,
|
||||
},
|
||||
sendMsg,
|
||||
memo: MEMO,
|
||||
};
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid method');
|
||||
}
|
||||
|
||||
const response = await approveWalletConnectRequest(
|
||||
requestEvent,
|
||||
account,
|
||||
namespace,
|
||||
requestedNetwork!.chainId,
|
||||
options,
|
||||
);
|
||||
|
||||
const { topic } = requestEvent;
|
||||
await web3wallet!.respondSessionRequest({ topic, response });
|
||||
navigation.navigate('Laconic');
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
setIsTxLoading(false);
|
||||
};
|
||||
|
||||
const rejectRequestHandler = async () => {
|
||||
const response = rejectWalletConnectRequest(requestEvent);
|
||||
const { topic } = requestEvent;
|
||||
await web3wallet!.respondSessionRequest({
|
||||
topic,
|
||||
response,
|
||||
});
|
||||
|
||||
navigation.navigate('Laconic');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getAccountBalance = async () => {
|
||||
try {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
if (namespace === EIP155) {
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
const fetchedBalance = await provider.getBalance(account.address);
|
||||
setBalance(fetchedBalance ? fetchedBalance.toString() : '0');
|
||||
} else {
|
||||
const cosmosBalance = await cosmosStargateClient?.getBalance(
|
||||
account.address,
|
||||
requestedNetwork!.nativeDenom!.toLowerCase(),
|
||||
);
|
||||
|
||||
setBalance(cosmosBalance?.amount!);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
getAccountBalance();
|
||||
}, [account, provider, namespace, cosmosStargateClient, requestedNetwork]);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
header: ({ options, back }) => {
|
||||
const title = getHeaderTitle(options, 'Approve Transaction');
|
||||
|
||||
return (
|
||||
<Appbar.Header>
|
||||
{back && (
|
||||
<Appbar.BackAction
|
||||
onPress={async () => {
|
||||
await rejectRequestHandler();
|
||||
navigation.navigate('Laconic');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Appbar.Content title={title} />
|
||||
</Appbar.Header>
|
||||
);
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [navigation, route.name]);
|
||||
|
||||
useEffect(() => {
|
||||
const getEthGas = async () => {
|
||||
try {
|
||||
if (!isSufficientFunds || !provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await provider.getFeeData();
|
||||
|
||||
setEthMaxFee(data.maxFeePerGas);
|
||||
setEthMaxPriorityFee(data.maxPriorityFeePerGas);
|
||||
setEthGasPrice(data.gasPrice);
|
||||
|
||||
if (transaction.gasLimit) {
|
||||
setEthGasLimit(BigNumber.from(transaction.gasLimit));
|
||||
} else {
|
||||
const transactionObject: Deferrable<providers.TransactionRequest> = {
|
||||
from: transaction.from!,
|
||||
to: transaction.to!,
|
||||
data: transaction.data!,
|
||||
value: transaction.value!,
|
||||
maxFeePerGas: data.maxFeePerGas ?? undefined,
|
||||
maxPriorityFeePerGas: data.maxPriorityFeePerGas ?? undefined,
|
||||
gasPrice: data.maxFeePerGas
|
||||
? undefined
|
||||
: data.gasPrice ?? undefined,
|
||||
};
|
||||
const gasLimit = await provider.estimateGas(transactionObject);
|
||||
setEthGasLimit(gasLimit);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
getEthGas();
|
||||
}, [provider, transaction, isSufficientFunds]);
|
||||
retrieveData(signer);
|
||||
}, [retrieveData, signer]);
|
||||
|
||||
useEffect(() => {
|
||||
const getCosmosGas = async () => {
|
||||
@ -444,13 +130,9 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
|
||||
if (!cosmosStargateClient) {
|
||||
return;
|
||||
}
|
||||
if (!isSufficientFunds) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gasEstimation = await cosmosStargateClient.simulate(
|
||||
transaction.from!,
|
||||
[sendMsg],
|
||||
const gasEstimation = await cosmosStargateClient!.simulate(
|
||||
transactionMessage.value.participant!,
|
||||
[transactionMessage],
|
||||
MEMO,
|
||||
);
|
||||
|
||||
@ -461,183 +143,123 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
|
||||
);
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
const response = formatJsonRpcError(requestEventId, error.message);
|
||||
await web3wallet!.respondSessionRequest({ topic, response });
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
getCosmosGas();
|
||||
}, [cosmosStargateClient, isSufficientFunds, sendMsg, transaction]);
|
||||
}, [cosmosStargateClient, transactionMessage, requestEventId, topic]);
|
||||
|
||||
useEffect(() => {
|
||||
if (balance && !isSufficientFunds) {
|
||||
setTxError('Insufficient funds');
|
||||
const gasPrice = GasPrice.fromString(
|
||||
requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
|
||||
);
|
||||
|
||||
if (!cosmosGasLimit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cosmosFees = calculateFee(Number(cosmosGasLimit), gasPrice);
|
||||
|
||||
setFees(cosmosFees.amount[0].amount);
|
||||
}, [namespace, cosmosGasLimit, requestedNetwork]);
|
||||
|
||||
const acceptRequestHandler = async () => {
|
||||
try {
|
||||
if (!account) {
|
||||
throw new Error('account not found');
|
||||
}
|
||||
|
||||
let options: WalletConnectRequests;
|
||||
|
||||
if (!cosmosStargateClient) {
|
||||
throw new Error('Cosmos stargate client not found');
|
||||
}
|
||||
|
||||
options = {
|
||||
type: 'cosmos_sendTransaction',
|
||||
LaconicClient: cosmosStargateClient,
|
||||
// StdFee object
|
||||
cosmosFee: {
|
||||
// This amount is total fees required for transaction
|
||||
amount: [
|
||||
{
|
||||
amount: fees!,
|
||||
denom: requestedNetwork!.nativeDenom!,
|
||||
},
|
||||
],
|
||||
gas: cosmosGasLimit!,
|
||||
},
|
||||
txMsg: transactionMessage,
|
||||
};
|
||||
|
||||
const response = await approveWalletConnectRequest(
|
||||
requestEvent,
|
||||
account,
|
||||
namespace,
|
||||
requestedNetwork!.chainId,
|
||||
options,
|
||||
);
|
||||
|
||||
await web3wallet!.respondSessionRequest({ topic, response });
|
||||
navigation.navigate('Laconic');
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
const response = formatJsonRpcError(requestEventId, error.message);
|
||||
await web3wallet!.respondSessionRequest({ topic, response });
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
}, [isSufficientFunds, balance]);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<View style={styles.spinnerContainer}>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<ScrollView contentContainerStyle={styles.appContainer}>
|
||||
<View style={styles.dappDetails}>
|
||||
{requestIcon && (
|
||||
<Image
|
||||
style={styles.dappLogo}
|
||||
source={requestIcon ? { uri: requestIcon } : undefined}
|
||||
/>
|
||||
<ScrollView contentContainerStyle={styles.appContainer}>
|
||||
<View style={styles.dappDetails}>
|
||||
{requestIcon && (
|
||||
<>
|
||||
{requestIcon.endsWith('.svg') ? (
|
||||
<View style={styles.dappLogo}>
|
||||
<SvgUri height="50" width="50" uri={requestIcon} />
|
||||
</View>
|
||||
) : (
|
||||
<Image style={styles.dappLogo} source={{ uri: requestIcon }} />
|
||||
)}
|
||||
<Text>{requestName}</Text>
|
||||
<Text variant="bodyMedium">{requestURL}</Text>
|
||||
</View>
|
||||
<View style={styles.dataBoxContainer}>
|
||||
<Text style={styles.dataBoxLabel}>From</Text>
|
||||
<View style={styles.dataBox}>
|
||||
<AccountDetails account={account} />
|
||||
</View>
|
||||
</View>
|
||||
<DataBox
|
||||
label={`Balance (${
|
||||
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
|
||||
})`}
|
||||
data={
|
||||
balance === '' || balance === undefined
|
||||
? 'Loading balance...'
|
||||
: `${balance}`
|
||||
</>
|
||||
)}
|
||||
<Text>{requestName}</Text>
|
||||
<Text variant="bodyMedium">{requestURL}</Text>
|
||||
</View>
|
||||
<Text style={styles.dataBoxLabel}>TX</Text>
|
||||
<View style={styles.messageBody}>
|
||||
<Text variant="bodyLarge">
|
||||
{JSON.stringify(transactionMessage, null, 2)}
|
||||
</Text>
|
||||
</View>
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={cosmosGasLimit}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setCosmosGasLimit(value);
|
||||
}
|
||||
/>
|
||||
{transaction && (
|
||||
<View style={styles.approveTransaction}>
|
||||
<DataBox label="To" data={transaction.to!} />
|
||||
<DataBox
|
||||
label={`Amount (${
|
||||
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
|
||||
})`}
|
||||
data={BigNumber.from(
|
||||
transaction.value?.toString(),
|
||||
).toString()}
|
||||
/>
|
||||
|
||||
{namespace === EIP155 ? (
|
||||
<>
|
||||
{isEIP1559 === false ? (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
{'Gas Price (wei)'}
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethGasPrice?.toNumber().toString()}
|
||||
onChangeText={value =>
|
||||
setEthGasPrice(BigNumber.from(value))
|
||||
}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
Max Fee Per Gas (wei)
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethMaxFee?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthMaxFee(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
Max Priority Fee Per Gas (wei)
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethMaxPriorityFee?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthMaxPriorityFee(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethGasLimit?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthGasLimit(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<DataBox
|
||||
label={`${
|
||||
isEIP1559 === true ? 'Max Fee' : 'Gas Fee'
|
||||
} (wei)`}
|
||||
data={fees!}
|
||||
/>
|
||||
<DataBox label="Data" data={transaction.data!} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>{`Fee (${
|
||||
requestedNetwork!.nativeDenom
|
||||
})`}</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={fees}
|
||||
onChangeText={value => setFees(value)}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={cosmosGasLimit}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setCosmosGasLimit(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
}}
|
||||
/>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={acceptRequestHandler}
|
||||
loading={isTxLoading}
|
||||
disabled={!balance || !fees}>
|
||||
{isTxLoading ? 'Processing' : 'Yes'}
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={rejectRequestHandler}
|
||||
buttonColor="#B82B0D">
|
||||
No
|
||||
<Button mode="contained" onPress={acceptRequestHandler}>
|
||||
Send tx to chain
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
<TxErrorDialog
|
||||
error={txError!}
|
||||
visible={isTxErrorDialogOpen}
|
||||
hideDialog={() => {
|
||||
hideDialog={async () => {
|
||||
setIsTxErrorDialogOpen(false);
|
||||
if (!isSufficientFunds || !balance || !fees) {
|
||||
rejectRequestHandler();
|
||||
navigation.navigate('Laconic');
|
||||
}
|
||||
navigation.navigate('Laconic');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
648
src/screens/ApproveTransfer.tsx
Normal file
648
src/screens/ApproveTransfer.tsx
Normal file
@ -0,0 +1,648 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Image, ScrollView, View } from 'react-native';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Button,
|
||||
Text,
|
||||
Appbar,
|
||||
TextInput,
|
||||
} from 'react-native-paper';
|
||||
import { providers, BigNumber } from 'ethers';
|
||||
import Config from 'react-native-config';
|
||||
import { Deferrable } from 'ethers/lib/utils';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import {
|
||||
NativeStackNavigationProp,
|
||||
NativeStackScreenProps,
|
||||
} from '@react-navigation/native-stack';
|
||||
import { getHeaderTitle } from '@react-navigation/elements';
|
||||
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
|
||||
import {
|
||||
calculateFee,
|
||||
GasPrice,
|
||||
MsgSendEncodeObject,
|
||||
SigningStargateClient,
|
||||
} from '@cosmjs/stargate';
|
||||
|
||||
import { Account, StackParamsList } from '../types';
|
||||
import AccountDetails from '../components/AccountDetails';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { retrieveSingleAccount } from '../utils/accounts';
|
||||
import {
|
||||
approveWalletConnectRequest,
|
||||
rejectWalletConnectRequest,
|
||||
WalletConnectRequests,
|
||||
} from '../utils/wallet-connect/wallet-connect-requests';
|
||||
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import DataBox from '../components/DataBox';
|
||||
import { getPathKey } from '../utils/misc';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
import { COSMOS, EIP155, IS_NUMBER_REGEX } from '../utils/constants';
|
||||
import TxErrorDialog from '../components/TxErrorDialog';
|
||||
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
|
||||
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData';
|
||||
|
||||
export const MEMO = 'Sending signed tx from Laconic Wallet';
|
||||
// Reference: https://ethereum.org/en/developers/docs/gas/#what-is-gas-limit
|
||||
const ETH_MINIMUM_GAS = 21000;
|
||||
|
||||
type SignRequestProps = NativeStackScreenProps<
|
||||
StackParamsList,
|
||||
'ApproveTransfer'
|
||||
>;
|
||||
|
||||
const ApproveTransfer = ({ route }: SignRequestProps) => {
|
||||
const { networksData } = useNetworks();
|
||||
|
||||
const requestSession = route.params.requestSessionData;
|
||||
const requestName = requestSession.peer.metadata.name;
|
||||
const requestIcon = requestSession.peer.metadata.icons[0];
|
||||
const requestURL = requestSession.peer.metadata.url;
|
||||
const transaction = route.params.transaction;
|
||||
const requestEvent = route.params.requestEvent;
|
||||
const chainId = requestEvent.params.chainId;
|
||||
const requestMethod = requestEvent.params.request.method;
|
||||
|
||||
const [account, setAccount] = useState<Account>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [balance, setBalance] = useState<string>('');
|
||||
const [isTxLoading, setIsTxLoading] = useState(false);
|
||||
const [cosmosStargateClient, setCosmosStargateClient] =
|
||||
useState<SigningStargateClient>();
|
||||
const [fees, setFees] = useState<string>();
|
||||
const [cosmosGasLimit, setCosmosGasLimit] = useState<string>();
|
||||
const [txError, setTxError] = useState<string>();
|
||||
const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false);
|
||||
const [ethGasPrice, setEthGasPrice] = useState<BigNumber | null>();
|
||||
const [ethGasLimit, setEthGasLimit] = useState<BigNumber>();
|
||||
const [ethMaxFee, setEthMaxFee] = useState<BigNumber | null>();
|
||||
const [ethMaxPriorityFee, setEthMaxPriorityFee] =
|
||||
useState<BigNumber | null>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const setClient = async () => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cosmosPrivKey = (
|
||||
await getPathKey(
|
||||
`${requestedNetwork?.namespace}:${requestedNetwork?.chainId}`,
|
||||
account.index,
|
||||
)
|
||||
).privKey;
|
||||
|
||||
const sender = await DirectSecp256k1Wallet.fromKey(
|
||||
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
|
||||
requestedNetwork?.addressPrefix,
|
||||
);
|
||||
|
||||
try {
|
||||
const client = await SigningStargateClient.connectWithSigner(
|
||||
requestedNetwork?.rpcUrl!,
|
||||
sender,
|
||||
);
|
||||
|
||||
setCosmosStargateClient(client);
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
setClient();
|
||||
}, [account, requestedNetwork, chainId, namespace]);
|
||||
|
||||
const provider = useMemo(() => {
|
||||
if (namespace === EIP155) {
|
||||
if (!requestedNetwork) {
|
||||
throw new Error('Requested chain not supported');
|
||||
}
|
||||
try {
|
||||
const ethProvider = new providers.JsonRpcProvider(
|
||||
requestedNetwork.rpcUrl,
|
||||
);
|
||||
|
||||
return ethProvider;
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
}
|
||||
}, [requestedNetwork, namespace]);
|
||||
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
const retrieveData = useCallback(
|
||||
async (requestAddress: string) => {
|
||||
const requestAccount = await retrieveSingleAccount(
|
||||
requestedNetwork!.namespace,
|
||||
requestedNetwork!.chainId,
|
||||
requestAddress,
|
||||
);
|
||||
if (!requestAccount) {
|
||||
navigation.navigate('InvalidPath');
|
||||
return;
|
||||
}
|
||||
|
||||
setAccount(requestAccount);
|
||||
},
|
||||
[navigation, requestedNetwork],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Set loading to false when gas values for requested chain are fetched
|
||||
// If requested chain is EVM compatible, the cosmos gas values will be undefined and vice-versa, hence the condition checks only one of them at the same time
|
||||
if (
|
||||
// If requested chain is EVM compatible, set loading to false when ethMaxFee and ethPriorityFee have been populated
|
||||
(ethMaxFee !== undefined && ethMaxPriorityFee !== undefined) ||
|
||||
// Or if requested chain is a cosmos chain, set loading to false when cosmosGasLimit has been populated
|
||||
cosmosGasLimit !== undefined
|
||||
) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [ethMaxFee, ethMaxPriorityFee, cosmosGasLimit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (namespace === EIP155) {
|
||||
const ethFees = BigNumber.from(ethGasLimit ?? 0)
|
||||
.mul(BigNumber.from(ethMaxFee ?? ethGasPrice ?? 0))
|
||||
.toString();
|
||||
setFees(ethFees);
|
||||
} else {
|
||||
const gasPrice = GasPrice.fromString(
|
||||
requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
|
||||
);
|
||||
|
||||
if (!cosmosGasLimit) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cosmosFees = calculateFee(Number(cosmosGasLimit), gasPrice);
|
||||
|
||||
setFees(cosmosFees.amount[0].amount);
|
||||
}
|
||||
}, [
|
||||
transaction,
|
||||
namespace,
|
||||
ethGasLimit,
|
||||
ethGasPrice,
|
||||
cosmosGasLimit,
|
||||
requestedNetwork,
|
||||
ethMaxFee,
|
||||
]);
|
||||
useEffect(() => {
|
||||
retrieveData(transaction.from!);
|
||||
}, [retrieveData, transaction]);
|
||||
|
||||
const isEIP1559 = useMemo(() => {
|
||||
if (cosmosGasLimit) {
|
||||
return;
|
||||
}
|
||||
if (ethMaxFee !== null && ethMaxPriorityFee !== null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}, [cosmosGasLimit, ethMaxFee, ethMaxPriorityFee]);
|
||||
|
||||
const acceptRequestHandler = async () => {
|
||||
setIsTxLoading(true);
|
||||
try {
|
||||
if (!account) {
|
||||
throw new Error('account not found');
|
||||
}
|
||||
|
||||
if (ethGasLimit && ethGasLimit.lt(ETH_MINIMUM_GAS)) {
|
||||
throw new Error(`Atleast ${ETH_MINIMUM_GAS} gas limit is required`);
|
||||
}
|
||||
|
||||
if (ethMaxFee && ethMaxPriorityFee && ethMaxFee.lte(ethMaxPriorityFee)) {
|
||||
throw new Error(
|
||||
`Max fee per gas (${ethMaxFee.toNumber()}) cannot be lower than or equal to max priority fee per gas (${ethMaxPriorityFee.toNumber()})`,
|
||||
);
|
||||
}
|
||||
|
||||
let options: WalletConnectRequests;
|
||||
|
||||
switch (requestMethod) {
|
||||
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
|
||||
if (
|
||||
ethMaxFee === undefined ||
|
||||
ethMaxPriorityFee === undefined ||
|
||||
ethGasPrice === undefined
|
||||
) {
|
||||
throw new Error('Gas values not found');
|
||||
}
|
||||
|
||||
options = {
|
||||
type: 'eth_sendTransaction',
|
||||
provider: provider!,
|
||||
ethGasLimit: BigNumber.from(ethGasLimit),
|
||||
ethGasPrice: ethGasPrice ? ethGasPrice.toHexString() : null,
|
||||
maxFeePerGas: ethMaxFee,
|
||||
maxPriorityFeePerGas: ethMaxPriorityFee,
|
||||
};
|
||||
break;
|
||||
case COSMOS_METHODS.COSMOS_SEND_TOKENS:
|
||||
if (!cosmosStargateClient) {
|
||||
throw new Error('Cosmos stargate client not found');
|
||||
}
|
||||
|
||||
options = {
|
||||
type: 'cosmos_sendTokens',
|
||||
signingStargateClient: cosmosStargateClient,
|
||||
// StdFee object
|
||||
cosmosFee: {
|
||||
// This amount is total fees required for transaction
|
||||
amount: [
|
||||
{
|
||||
amount: fees!,
|
||||
denom: requestedNetwork!.nativeDenom!,
|
||||
},
|
||||
],
|
||||
gas: cosmosGasLimit!,
|
||||
},
|
||||
sendMsg,
|
||||
memo: MEMO,
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('Invalid method');
|
||||
}
|
||||
|
||||
const response = await approveWalletConnectRequest(
|
||||
requestEvent,
|
||||
account,
|
||||
namespace,
|
||||
requestedNetwork!.chainId,
|
||||
options,
|
||||
);
|
||||
|
||||
const { topic } = requestEvent;
|
||||
await web3wallet!.respondSessionRequest({ topic, response });
|
||||
navigation.navigate('Laconic');
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
setIsTxLoading(false);
|
||||
};
|
||||
|
||||
const rejectRequestHandler = async () => {
|
||||
const response = rejectWalletConnectRequest(requestEvent);
|
||||
const { topic } = requestEvent;
|
||||
await web3wallet!.respondSessionRequest({
|
||||
topic,
|
||||
response,
|
||||
});
|
||||
|
||||
navigation.navigate('Laconic');
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getAccountBalance = async () => {
|
||||
try {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
if (namespace === EIP155) {
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
const fetchedBalance = await provider.getBalance(account.address);
|
||||
setBalance(fetchedBalance ? fetchedBalance.toString() : '0');
|
||||
} else {
|
||||
const cosmosBalance = await cosmosStargateClient?.getBalance(
|
||||
account.address,
|
||||
requestedNetwork!.nativeDenom!.toLowerCase(),
|
||||
);
|
||||
|
||||
setBalance(cosmosBalance?.amount!);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
getAccountBalance();
|
||||
}, [account, provider, namespace, cosmosStargateClient, requestedNetwork]);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
header: ({ options, back }) => {
|
||||
const title = getHeaderTitle(options, 'Approve Transaction');
|
||||
|
||||
return (
|
||||
<Appbar.Header>
|
||||
{back && (
|
||||
<Appbar.BackAction
|
||||
onPress={async () => {
|
||||
await rejectRequestHandler();
|
||||
navigation.navigate('Laconic');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Appbar.Content title={title} />
|
||||
</Appbar.Header>
|
||||
);
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [navigation, route.name]);
|
||||
|
||||
useEffect(() => {
|
||||
const getEthGas = async () => {
|
||||
try {
|
||||
if (!isSufficientFunds || !provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await provider.getFeeData();
|
||||
|
||||
setEthMaxFee(data.maxFeePerGas);
|
||||
setEthMaxPriorityFee(data.maxPriorityFeePerGas);
|
||||
setEthGasPrice(data.gasPrice);
|
||||
|
||||
if (transaction.gasLimit) {
|
||||
setEthGasLimit(BigNumber.from(transaction.gasLimit));
|
||||
} else {
|
||||
const transactionObject: Deferrable<providers.TransactionRequest> = {
|
||||
from: transaction.from!,
|
||||
to: transaction.to!,
|
||||
data: transaction.data!,
|
||||
value: transaction.value!,
|
||||
maxFeePerGas: data.maxFeePerGas ?? undefined,
|
||||
maxPriorityFeePerGas: data.maxPriorityFeePerGas ?? undefined,
|
||||
gasPrice: data.maxFeePerGas
|
||||
? undefined
|
||||
: data.gasPrice ?? undefined,
|
||||
};
|
||||
const gasLimit = await provider.estimateGas(transactionObject);
|
||||
setEthGasLimit(gasLimit);
|
||||
}
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
getEthGas();
|
||||
}, [provider, transaction, isSufficientFunds]);
|
||||
|
||||
useEffect(() => {
|
||||
const getCosmosGas = async () => {
|
||||
try {
|
||||
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)),
|
||||
),
|
||||
);
|
||||
} catch (error: any) {
|
||||
setTxError(error.message);
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
};
|
||||
getCosmosGas();
|
||||
}, [cosmosStargateClient, isSufficientFunds, sendMsg, transaction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (balance && !isSufficientFunds) {
|
||||
setTxError('Insufficient funds');
|
||||
setIsTxErrorDialogOpen(true);
|
||||
}
|
||||
}, [isSufficientFunds, balance]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<View style={styles.spinnerContainer}>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<ScrollView contentContainerStyle={styles.appContainer}>
|
||||
<View style={styles.dappDetails}>
|
||||
{requestIcon && (
|
||||
<Image
|
||||
style={styles.dappLogo}
|
||||
source={requestIcon ? { uri: requestIcon } : undefined}
|
||||
/>
|
||||
)}
|
||||
<Text>{requestName}</Text>
|
||||
<Text variant="bodyMedium">{requestURL}</Text>
|
||||
</View>
|
||||
<View style={styles.dataBoxContainer}>
|
||||
<Text style={styles.dataBoxLabel}>From</Text>
|
||||
<View style={styles.dataBox}>
|
||||
<AccountDetails account={account} />
|
||||
</View>
|
||||
</View>
|
||||
<DataBox
|
||||
label={`Balance (${
|
||||
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
|
||||
})`}
|
||||
data={
|
||||
balance === '' || balance === undefined
|
||||
? 'Loading balance...'
|
||||
: `${balance}`
|
||||
}
|
||||
/>
|
||||
{transaction && (
|
||||
<View style={styles.approveTransfer}>
|
||||
<DataBox label="To" data={transaction.to!} />
|
||||
<DataBox
|
||||
label={`Amount (${
|
||||
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
|
||||
})`}
|
||||
data={BigNumber.from(
|
||||
transaction.value?.toString(),
|
||||
).toString()}
|
||||
/>
|
||||
|
||||
{namespace === EIP155 ? (
|
||||
<>
|
||||
{isEIP1559 === false ? (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
{'Gas Price (wei)'}
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethGasPrice?.toNumber().toString()}
|
||||
onChangeText={value =>
|
||||
setEthGasPrice(BigNumber.from(value))
|
||||
}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
Max Fee Per Gas (wei)
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethMaxFee?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthMaxFee(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
Max Priority Fee Per Gas (wei)
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethMaxPriorityFee?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthMaxPriorityFee(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethGasLimit?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthGasLimit(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<DataBox
|
||||
label={`${
|
||||
isEIP1559 === true ? 'Max Fee' : 'Gas Fee'
|
||||
} (wei)`}
|
||||
data={fees!}
|
||||
/>
|
||||
<DataBox label="Data" data={transaction.data!} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>{`Fee (${
|
||||
requestedNetwork!.nativeDenom
|
||||
})`}</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={fees}
|
||||
onChangeText={value => setFees(value)}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={cosmosGasLimit}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setCosmosGasLimit(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={acceptRequestHandler}
|
||||
loading={isTxLoading}
|
||||
disabled={!balance || !fees}>
|
||||
{isTxLoading ? 'Processing' : 'Yes'}
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={rejectRequestHandler}
|
||||
buttonColor="#B82B0D">
|
||||
No
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
<TxErrorDialog
|
||||
error={txError!}
|
||||
visible={isTxErrorDialogOpen}
|
||||
hideDialog={() => {
|
||||
setIsTxErrorDialogOpen(false);
|
||||
if (!isSufficientFunds || !balance || !fees) {
|
||||
rejectRequestHandler();
|
||||
navigation.navigate('Laconic');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApproveTransfer;
|
@ -136,7 +136,7 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
padding: 8,
|
||||
},
|
||||
approveTransaction: {
|
||||
approveTransfer: {
|
||||
height: '40%',
|
||||
marginBottom: 30,
|
||||
},
|
||||
|
@ -2,6 +2,7 @@ import { PopulatedTransaction } from 'ethers';
|
||||
|
||||
import { SignClientTypes, SessionTypes } from '@walletconnect/types';
|
||||
import { Web3WalletTypes } from '@walletconnect/web3wallet';
|
||||
import { EncodeObject } from '@cosmjs/proto-signing';
|
||||
|
||||
export type StackParamsList = {
|
||||
Laconic: undefined;
|
||||
@ -17,7 +18,7 @@ export type StackParamsList = {
|
||||
requestEvent?: Web3WalletTypes.SessionRequest;
|
||||
requestSessionData?: SessionTypes.Struct;
|
||||
};
|
||||
ApproveTransaction: {
|
||||
ApproveTransfer: {
|
||||
transaction: PopulatedTransaction;
|
||||
requestEvent: Web3WalletTypes.SessionRequest;
|
||||
requestSessionData: SessionTypes.Struct;
|
||||
@ -29,6 +30,12 @@ export type StackParamsList = {
|
||||
EditNetwork: {
|
||||
selectedNetwork: NetworksDataState;
|
||||
};
|
||||
ApproveTransaction: {
|
||||
transactionMessage: EncodeObject;
|
||||
signer: string;
|
||||
requestEvent: Web3WalletTypes.SessionRequest;
|
||||
requestSessionData: SessionTypes.Struct;
|
||||
};
|
||||
};
|
||||
|
||||
export type Account = {
|
||||
|
@ -38,4 +38,5 @@ export const COSMOS_SIGNING_METHODS = {
|
||||
export const COSMOS_METHODS = {
|
||||
...COSMOS_SIGNING_METHODS,
|
||||
COSMOS_SEND_TOKENS: 'cosmos_sendTokens', // Added for pay.laconic.com
|
||||
COSMOS_SEND_TRANSACTION: 'cosmos_sendTransaction', // Added for testnet onboarding app
|
||||
};
|
||||
|
@ -9,6 +9,8 @@ import {
|
||||
StdFee,
|
||||
MsgSendEncodeObject,
|
||||
} from '@cosmjs/stargate';
|
||||
import { EncodeObject } from '@cosmjs/proto-signing';
|
||||
import { LaconicClient } from '@cerc-io/registry-sdk/dist/laconic-client';
|
||||
|
||||
import { EIP155_SIGNING_METHODS } from './EIP155Data';
|
||||
import { signDirectMessage, signEthMessage } from '../sign-message';
|
||||
@ -49,12 +51,20 @@ interface CosmosSendTokens {
|
||||
memo: string;
|
||||
}
|
||||
|
||||
interface CosmosSendTransaction {
|
||||
type: 'cosmos_sendTransaction';
|
||||
LaconicClient: LaconicClient;
|
||||
cosmosFee: StdFee;
|
||||
txMsg: EncodeObject;
|
||||
}
|
||||
|
||||
export type WalletConnectRequests =
|
||||
| EthSendTransaction
|
||||
| EthPersonalSign
|
||||
| CosmosSignDirect
|
||||
| CosmosSignAmino
|
||||
| CosmosSendTokens;
|
||||
| CosmosSendTokens
|
||||
| CosmosSendTransaction;
|
||||
|
||||
export async function approveWalletConnectRequest(
|
||||
requestEvent: SignClientTypes.EventArguments['session_request'],
|
||||
@ -181,6 +191,20 @@ export async function approveWalletConnectRequest(
|
||||
signature: result.transactionHash,
|
||||
});
|
||||
|
||||
case COSMOS_METHODS.COSMOS_SEND_TRANSACTION:
|
||||
if (!(options.type === 'cosmos_sendTransaction')) {
|
||||
throw new Error('Incorrect parameters passed');
|
||||
}
|
||||
const resultFromTx = await options.LaconicClient.signAndBroadcast(
|
||||
address,
|
||||
[options.txMsg],
|
||||
options.cosmosFee,
|
||||
);
|
||||
|
||||
return formatJsonRpcResult(id, {
|
||||
code: resultFromTx.code,
|
||||
});
|
||||
|
||||
default:
|
||||
throw new Error(getSdkError('INVALID_METHOD').message);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user