Compare commits

...

17 Commits

Author SHA1 Message Date
bb5223afda Add support for staking module tx MsgCreateValidator (#14)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#14
2024-08-09 09:41:46 +00:00
e975f4c9f7 Add copy button for mnemonic after creating new wallet (#13)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#13
2024-08-08 07:35:50 +00:00
4827fa8c7c Fix accounts order in namespaces object and prevent re-render from useEffect (#12)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)
- Send namespaces object with correct accounts sequence while pairing with dApps

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#12
2024-08-01 10:38:21 +00:00
36c3adb1b1 Upgrade registry-sdk version to 0.2.5 (#11)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#11
2024-07-30 13:32:32 +00:00
2ca91cad06 Replace photon with alnt for laconicd network (#10)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#10
2024-07-30 13:20:17 +00:00
eab4fd425a Add laconicd as a default network in wallet (#9)
Part of [laconicd testnet validator enrollment
](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#9
2024-07-29 12:55:50 +00:00
361b79b696 Refactor wallet connect instance to use state variables (#8)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)
- Refactor `web3wallet` variable into a state variable

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#8
2024-07-29 12:13:15 +00:00
0bd9dec8a9 Upgrade registry-sdk package version to 0.2.3 (#7)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

- Upgrade registry-sdk to include onboarding module changes

Co-authored-by: Adw8 <adwaitgharpure@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#7
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2024-07-29 05:40:34 +00:00
c027f5b934 Hotfix to remove non-null assertion for WalletConnect instance (#6)
Part of [laconicd testnet validator enrollment](https://www.notion.so/laconicd-testnet-validator-enrollment-6fc1d3cafcc64fef8c5ed3affa27c675)

TODO:
- Refactor WalletConnect instance in laconic wallet to use state variables

Co-authored-by: Shreerang Kale <shreerangkale@gmail.com>
Reviewed-on: cerc-io/laconic-wallet#6
2024-07-26 12:17:28 +00:00
9f5f6c2361 Upgrade registry-sdk package version 2024-07-16 14:32:45 +05:30
Isha Venikar
40ccc4c333
Add spinner to Yes button in ApproveTransaction page (#122)
* Add spinner to Yes button

* Fix indentation

* Modify variable names

* Set isRequestAccepted to false
2024-07-16 09:54:49 +05:30
Adwait Gharpure
9fa39c48ee
Stop importing laconic client from registry-sdk/dist (#121) 2024-07-11 10:16:25 +05:30
Adwait Gharpure
d75260d3dc
Add polyfills for url and https packages (#120)
* Add polyfills for url and https packages

* Use http-browserify as polyfill
2024-07-09 14:36:59 +05:30
Adwait Gharpure
c3f9f7c245
Add support for rejecting transaction request (#119)
* Implement functionality to reject request

* Change button label
2024-07-08 16:40:16 +05:30
Adwait Gharpure
521776ea3e
Display account details in approve transaction page (#118)
* Display account in approve transaction page

* Change label
2024-07-05 15:48:08 +05:30
shreerang6921
2ccb396889
Show private key of selected account (#117)
* Display private key of selected account

* Update dialog box UI

* Refactor show private key dialog

* Refactor code to use context variables
2024-07-04 18:45:13 +05:30
Adwait Gharpure
83723d4086
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
2024-07-04 18:36:20 +05:30
31 changed files with 2113 additions and 664 deletions

View File

@ -2,3 +2,4 @@ WALLET_CONNECT_PROJECT_ID=
DEFAULT_GAS_PRICE=0.025 DEFAULT_GAS_PRICE=0.025
# Reference: https://github.com/cosmos/cosmos-sdk/issues/16020 # Reference: https://github.com/cosmos/cosmos-sdk/issues/16020
DEFAULT_GAS_ADJUSTMENT=2 DEFAULT_GAS_ADJUSTMENT=2
LACONICD_RPC_URL=https://laconicd.laconic.com

1
.npmrc Normal file
View File

@ -0,0 +1 @@
@cerc-io:registry=https://git.vdb.to/api/packages/cerc-io/npm/

View File

@ -25,7 +25,7 @@
- Install Android SDK - 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: - 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... 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 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 - For a virtual device, continue with the steps
6. Start the application 7. Start the application
``` ```
yarn start yarn start
``` ```
7. Press `a` to run the application on android 8. Press `a` to run the application on android
## Flow for the app ## Flow for the app

View File

@ -10,6 +10,9 @@ module.exports = {
crypto: 'react-native-quick-crypto', crypto: 'react-native-quick-crypto',
stream: 'stream-browserify', stream: 'stream-browserify',
buffer: '@craftzdog/react-native-buffer', buffer: '@craftzdog/react-native-buffer',
http: 'http-browserify',
https: 'https-browserify',
url: 'react-native-url-polyfill',
}, },
}, },
], ],

View File

@ -9,7 +9,6 @@
- Function for creating web3 wallet - Function for creating web3 wallet
```js ```js
export let web3wallet: IWeb3Wallet;
export let core: ICore; export let core: ICore;
export async function createWeb3Wallet() { export async function createWeb3Wallet() {
@ -26,23 +25,27 @@
icons: ['https://avatars.githubusercontent.com/u/92608123'], icons: ['https://avatars.githubusercontent.com/u/92608123'],
}, },
}); });
return web3wallet;
} }
``` ```
- Hook used for intializing web3 wallet - Hook used for intializing web3 wallet
```js ```js
export default function useInitialization() { export default function useInitialization(setWeb3wallet) {
const [initialized, setInitialized] = useState(false); const [initialized, setInitialized] = useState(false);
const onInitialize = useCallback(async () => { const onInitialize = useCallback(async () => {
try { try {
await createWeb3Wallet(); const web3walletInstance = await createWeb3Wallet();
setWeb3wallet(web3walletInstance);
setInitialized(true); setInitialized(true);
} catch (err: unknown) { } catch (err: unknown) {
console.log('Error for initializing', err); console.error('Error for initializing', err);
} }
}, []); }, [setWeb3wallet]);
useEffect(() => { useEffect(() => {
if (!initialized) { if (!initialized) {

View File

@ -12,6 +12,7 @@
"prepare": "husky" "prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@cerc-io/registry-sdk": "^0.2.5",
"@cosmjs/amino": "^0.32.3", "@cosmjs/amino": "^0.32.3",
"@cosmjs/crypto": "^0.32.3", "@cosmjs/crypto": "^0.32.3",
"@cosmjs/proto-signing": "^0.32.3", "@cosmjs/proto-signing": "^0.32.3",
@ -20,6 +21,7 @@
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@json-rpc-tools/utils": "^1.7.6", "@json-rpc-tools/utils": "^1.7.6",
"@react-native-async-storage/async-storage": "^1.22.3", "@react-native-async-storage/async-storage": "^1.22.3",
"@react-native-clipboard/clipboard": "^1.14.1",
"@react-native-community/netinfo": "^11.3.1", "@react-native-community/netinfo": "^11.3.1",
"@react-navigation/elements": "^1.3.30", "@react-navigation/elements": "^1.3.30",
"@react-navigation/native": "^6.1.10", "@react-navigation/native": "^6.1.10",
@ -28,9 +30,12 @@
"@walletconnect/react-native-compat": "^2.11.2", "@walletconnect/react-native-compat": "^2.11.2",
"@walletconnect/utils": "^2.12.2", "@walletconnect/utils": "^2.12.2",
"@walletconnect/web3wallet": "^1.10.2", "@walletconnect/web3wallet": "^1.10.2",
"assert": "^2.1.0",
"chain-registry": "^1.41.2", "chain-registry": "^1.41.2",
"cosmjs-types": "^0.9.0", "cosmjs-types": "^0.9.0",
"ethers": "5.7.2", "ethers": "5.7.2",
"http-browserify": "^1.7.0",
"https-browserify": "^1.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"react": "18.2.0", "react": "18.2.0",
@ -45,6 +50,7 @@
"react-native-safe-area-context": "^4.9.0", "react-native-safe-area-context": "^4.9.0",
"react-native-screens": "^3.29.0", "react-native-screens": "^3.29.0",
"react-native-svg": "^15.1.0", "react-native-svg": "^15.1.0",
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "^10.0.3", "react-native-vector-icons": "^10.0.3",
"react-native-vision-camera": "^3.9.0", "react-native-vision-camera": "^3.9.0",
"text-encoding-polyfill": "^0.6.7", "text-encoding-polyfill": "^0.6.7",

View File

@ -4,6 +4,7 @@ declare module 'react-native-config' {
WALLET_CONNECT_PROJECT_ID: string; WALLET_CONNECT_PROJECT_ID: string;
DEFAULT_GAS_PRICE: string; DEFAULT_GAS_PRICE: string;
DEFAULT_GAS_ADJUSTMENT: string; DEFAULT_GAS_ADJUSTMENT: string;
LACONICD_RPC_URL: string;
} }
export const Config: NativeConfig; export const Config: NativeConfig;

31
scripts/build-apk.sh Executable file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# Default value for IS_RELEASE
IS_RELEASE=${IS_RELEASE:-false}
# Install dependencies
echo "Installing dependencies..."
yarn
# Create the necessary directory for assets
mkdir -p android/app/src/main/assets/
# Bundle the React Native application
yarn react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output android/app/src/main/assets/index.android.bundle \
--assets-dest android/app/src/main/res
# Navigate to the android directory
cd android
# Run the Gradle build based on the IS_RELEASE flag
if [ "$IS_RELEASE" = "true" ]; then
echo "Building release version..."
./gradlew assembleRelease
else
echo "Building debug version..."
./gradlew assembleDebug
fi

View File

@ -22,16 +22,17 @@ import HomeScreen from './screens/HomeScreen';
import SignRequest from './screens/SignRequest'; import SignRequest from './screens/SignRequest';
import AddSession from './screens/AddSession'; import AddSession from './screens/AddSession';
import WalletConnect from './screens/WalletConnect'; import WalletConnect from './screens/WalletConnect';
import ApproveTransaction from './screens/ApproveTransaction';
import { StackParamsList } from './types'; import { StackParamsList } from './types';
import { web3wallet } from './utils/wallet-connect/WalletConnectUtils';
import { EIP155_SIGNING_METHODS } from './utils/wallet-connect/EIP155Data'; import { EIP155_SIGNING_METHODS } from './utils/wallet-connect/EIP155Data';
import { getSignParamsMessage } from './utils/wallet-connect/helpers'; import { getSignParamsMessage } from './utils/wallet-connect/helpers';
import ApproveTransaction from './screens/ApproveTransaction'; import ApproveTransfer from './screens/ApproveTransfer';
import AddNetwork from './screens/AddNetwork'; import AddNetwork from './screens/AddNetwork';
import EditNetwork from './screens/EditNetwork'; import EditNetwork from './screens/EditNetwork';
import { COSMOS, EIP155 } from './utils/constants'; import { COSMOS, EIP155 } from './utils/constants';
import { useNetworks } from './context/NetworksContext'; import { useNetworks } from './context/NetworksContext';
import { NETWORK_METHODS } from './utils/wallet-connect/common-data'; import { NETWORK_METHODS } from './utils/wallet-connect/common-data';
import { COSMOS_METHODS } from './utils/wallet-connect/COSMOSData';
const Stack = createNativeStackNavigator<StackParamsList>(); const Stack = createNativeStackNavigator<StackParamsList>();
@ -39,7 +40,7 @@ const App = (): React.JSX.Element => {
const navigation = const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>(); useNavigation<NativeStackNavigationProp<StackParamsList>>();
const { setActiveSessions } = useWalletConnect(); const { web3wallet, setActiveSessions } = useWalletConnect();
const { accounts, setCurrentIndex } = useAccounts(); const { accounts, setCurrentIndex } = useAccounts();
const { networksData, selectedNetwork, setSelectedNetwork } = useNetworks(); const { networksData, selectedNetwork, setSelectedNetwork } = useNetworks();
const [modalVisible, setModalVisible] = useState(false); const [modalVisible, setModalVisible] = useState(false);
@ -61,7 +62,7 @@ const App = (): React.JSX.Element => {
setModalVisible(true); setModalVisible(true);
setCurrentProposal(proposal); setCurrentProposal(proposal);
}, },
[accounts], [accounts, web3wallet],
); );
const onSessionRequest = useCallback( const onSessionRequest = useCallback(
@ -114,7 +115,7 @@ const App = (): React.JSX.Element => {
break; break;
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION: case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
navigation.navigate('ApproveTransaction', { navigation.navigate('ApproveTransfer', {
transaction: request.params[0], transaction: request.params[0],
requestEvent, requestEvent,
requestSessionData, requestSessionData,
@ -131,7 +132,7 @@ const App = (): React.JSX.Element => {
}); });
break; break;
case 'cosmos_signDirect': case COSMOS_METHODS.COSMOS_SIGN_DIRECT:
const message = { const message = {
txbody: TxBody.toJSON( txbody: TxBody.toJSON(
TxBody.decode( TxBody.decode(
@ -157,7 +158,7 @@ const App = (): React.JSX.Element => {
}); });
break; break;
case 'cosmos_signAmino': case COSMOS_METHODS.COSMOS_SIGN_AMINO:
navigation.navigate('SignRequest', { navigation.navigate('SignRequest', {
namespace: COSMOS, namespace: COSMOS,
address: request.params.signerAddress, address: request.params.signerAddress,
@ -167,14 +168,24 @@ const App = (): React.JSX.Element => {
}); });
break; break;
case 'cosmos_sendTokens': case COSMOS_METHODS.COSMOS_SEND_TOKENS:
navigation.navigate('ApproveTransaction', { navigation.navigate('ApproveTransfer', {
transaction: request.params[0], transaction: request.params[0],
requestEvent, requestEvent,
requestSessionData, requestSessionData,
}); });
break; break;
case COSMOS_METHODS.COSMOS_SEND_TRANSACTION:
const { transactionMessage, signer } = request.params;
navigation.navigate('ApproveTransaction', {
transactionMessage,
signer,
requestEvent,
requestSessionData,
});
break;
default: default:
throw new Error('Invalid method'); throw new Error('Invalid method');
} }
@ -185,13 +196,14 @@ const App = (): React.JSX.Element => {
setSelectedNetwork, setSelectedNetwork,
setCurrentIndex, setCurrentIndex,
selectedNetwork, selectedNetwork,
web3wallet,
], ],
); );
const onSessionDelete = useCallback(() => { const onSessionDelete = useCallback(() => {
const sessions = web3wallet!.getActiveSessions(); const sessions = web3wallet!.getActiveSessions();
setActiveSessions(sessions); setActiveSessions(sessions);
}, [setActiveSessions]); }, [setActiveSessions, web3wallet]);
useEffect(() => { useEffect(() => {
web3wallet?.on('session_proposal', onSessionProposal); web3wallet?.on('session_proposal', onSessionProposal);
@ -268,10 +280,10 @@ const App = (): React.JSX.Element => {
/> />
<Stack.Screen <Stack.Screen
name="ApproveTransaction" name="ApproveTransfer"
component={ApproveTransaction} component={ApproveTransfer}
options={{ options={{
title: 'Approve transaction', title: 'Approve transfer',
}} }}
/> />
<Stack.Screen <Stack.Screen
@ -288,6 +300,13 @@ const App = (): React.JSX.Element => {
title: 'Edit Network', title: 'Edit Network',
}} }}
/> />
<Stack.Screen
name="ApproveTransaction"
component={ApproveTransaction}
options={{
title: 'Approve Transaction',
}}
/>
</Stack.Navigator> </Stack.Navigator>
<PairingModal <PairingModal
visible={modalVisible} visible={modalVisible}

View File

@ -6,24 +6,28 @@ import { setInternetCredentials } from 'react-native-keychain';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AccountsProps, StackParamsList, Account } from '../types'; import { StackParamsList, Account } from '../types';
import { addAccount } from '../utils/accounts'; import { addAccount } from '../utils/accounts';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import HDPathDialog from './HDPathDialog'; import HDPathDialog from './HDPathDialog';
import AccountDetails from './AccountDetails'; import AccountDetails from './AccountDetails';
import { useAccounts } from '../context/AccountsContext'; import { useAccounts } from '../context/AccountsContext';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { useNetworks } from '../context/NetworksContext'; import { useNetworks } from '../context/NetworksContext';
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog';
import { getNamespaces } from '../utils/wallet-connect/helpers'; import { getNamespaces } from '../utils/wallet-connect/helpers';
import ShowPKDialog from './ShowPKDialog';
import { useWalletConnect } from '../context/WalletConnectContext';
const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => { const Accounts = () => {
const navigation = const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>(); useNavigation<NativeStackNavigationProp<StackParamsList>>();
const { accounts, setAccounts, setCurrentIndex } = useAccounts(); const { accounts, setAccounts, setCurrentIndex, currentIndex } =
useAccounts();
const { networksData, selectedNetwork, setNetworksData, setSelectedNetwork } = const { networksData, selectedNetwork, setNetworksData, setSelectedNetwork } =
useNetworks(); useNetworks();
const { web3wallet } = useWalletConnect();
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [isAccountCreating, setIsAccountCreating] = useState(false); const [isAccountCreating, setIsAccountCreating] = useState(false);
const [hdDialog, setHdDialog] = useState(false); const [hdDialog, setHdDialog] = useState(false);
@ -56,7 +60,6 @@ const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => {
networksData, networksData,
selectedNetwork!, selectedNetwork!,
accounts, accounts,
currentIndex,
); );
if (!updatedNamespaces) { if (!updatedNamespaces) {
@ -72,7 +75,7 @@ const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => {
// Call the updateSessions function when the 'accounts' dependency changes // Call the updateSessions function when the 'accounts' dependency changes
updateSessions(); updateSessions();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [accounts]); }, [accounts, web3wallet]);
const addAccountHandler = async () => { const addAccountHandler = async () => {
setIsAccountCreating(true); setIsAccountCreating(true);
@ -80,7 +83,7 @@ const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => {
setIsAccountCreating(false); setIsAccountCreating(false);
if (newAccount) { if (newAccount) {
updateAccounts(newAccount); updateAccounts(newAccount);
updateIndex(newAccount.index); setCurrentIndex(newAccount.index);
} }
}; };
@ -90,7 +93,7 @@ const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => {
key={account.index} key={account.index}
title={`Account ${account.index + 1}`} title={`Account ${account.index + 1}`}
onPress={() => { onPress={() => {
updateIndex(account.index); setCurrentIndex(account.index);
setExpanded(false); setExpanded(false);
}} }}
/> />
@ -120,7 +123,6 @@ const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => {
visible={hdDialog} visible={hdDialog}
hideDialog={() => setHdDialog(false)} hideDialog={() => setHdDialog(false)}
updateAccounts={updateAccounts} updateAccounts={updateAccounts}
updateIndex={updateIndex}
pathCode={pathCode} pathCode={pathCode}
/> />
<List.Accordion <List.Accordion
@ -218,6 +220,8 @@ const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => {
hideDialog={hideDeleteNetworkDialog} hideDialog={hideDeleteNetworkDialog}
onConfirm={handleRemove} onConfirm={handleRemove}
/> />
<ShowPKDialog />
</View> </View>
</ScrollView> </ScrollView>
); );

View File

@ -1,6 +1,8 @@
import React from 'react'; import React, { useState } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import { Button, Dialog, Portal, Text } from 'react-native-paper'; import { Button, Dialog, Portal, Snackbar, Text } from 'react-native-paper';
import Clipboard from '@react-native-clipboard/clipboard';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import GridView from './Grid'; import GridView from './Grid';
@ -11,29 +13,45 @@ const DialogComponent = ({
hideDialog, hideDialog,
contentText, contentText,
}: CustomDialogProps) => { }: CustomDialogProps) => {
const [toastVisible, setToastVisible] = useState(false);
const words = contentText.split(' '); const words = contentText.split(' ');
const handleCopy = () => {
Clipboard.setString(contentText);
setToastVisible(true);
};
return ( return (
<Portal> <>
<Dialog visible={visible} onDismiss={hideDialog}> <Portal>
<Dialog.Content> <Dialog visible={visible} onDismiss={hideDialog}>
<Text variant="titleLarge">Mnemonic</Text> <Dialog.Content>
<View style={styles.dialogTitle}> <Text variant="titleLarge">Mnemonic</Text>
<Text variant="titleMedium"> <View style={styles.dialogTitle}>
Your mnemonic provides full access to your wallet and funds. Make <Text variant="titleMedium">
sure to note it down.{' '} Your mnemonic provides full access to your wallet and funds.
</Text> Make sure to note it down.{' '}
<Text variant="titleMedium" style={styles.dialogWarning}> </Text>
Do not share your mnemonic with anyone <Text variant="titleMedium" style={styles.dialogWarning}>
</Text> Do not share your mnemonic with anyone
<GridView words={words} /> </Text>
</View> <GridView words={words} />
</Dialog.Content> </View>
<Dialog.Actions> </Dialog.Content>
<Button onPress={hideDialog}>Done</Button> <Dialog.Actions>
</Dialog.Actions> <Button onPress={handleCopy}>Copy</Button>
</Dialog> <Button onPress={hideDialog}>Done</Button>
</Portal> </Dialog.Actions>
</Dialog>
</Portal>
<Snackbar
visible={toastVisible}
onDismiss={() => setToastVisible(false)}
duration={1000}>
Mnemonic copied to clipboard
</Snackbar>
</>
); );
}; };

View File

@ -5,20 +5,20 @@ import { Button, TextInput } from 'react-native-paper';
import { addAccountFromHDPath } from '../utils/accounts'; import { addAccountFromHDPath } from '../utils/accounts';
import { Account, NetworksDataState, PathState } from '../types'; import { Account, NetworksDataState, PathState } from '../types';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import { useAccounts } from '../context/AccountsContext';
const HDPath = ({ const HDPath = ({
pathCode, pathCode,
updateAccounts, updateAccounts,
updateIndex,
hideDialog, hideDialog,
selectedNetwork, selectedNetwork,
}: { }: {
pathCode: string; pathCode: string;
updateIndex: (index: number) => void;
updateAccounts: (account: Account) => void; updateAccounts: (account: Account) => void;
hideDialog: () => void; hideDialog: () => void;
selectedNetwork: NetworksDataState; selectedNetwork: NetworksDataState;
}) => { }) => {
const { setCurrentIndex } = useAccounts();
const [isAccountCreating, setIsAccountCreating] = useState(false); const [isAccountCreating, setIsAccountCreating] = useState(false);
const [path, setPath] = useState<PathState>({ const [path, setPath] = useState<PathState>({
firstNumber: '', firstNumber: '',
@ -46,7 +46,7 @@ const HDPath = ({
const newAccount = await addAccountFromHDPath(hdPath, selectedNetwork); const newAccount = await addAccountFromHDPath(hdPath, selectedNetwork);
if (newAccount) { if (newAccount) {
updateAccounts(newAccount); updateAccounts(newAccount);
updateIndex(newAccount.index); setCurrentIndex(newAccount.index);
hideDialog(); hideDialog();
} }
} catch (error) { } catch (error) {

View File

@ -8,7 +8,6 @@ import { useNetworks } from '../context/NetworksContext';
const HDPathDialog = ({ const HDPathDialog = ({
visible, visible,
hideDialog, hideDialog,
updateIndex,
updateAccounts, updateAccounts,
pathCode, pathCode,
}: HDPathDialogProps) => { }: HDPathDialogProps) => {
@ -22,7 +21,6 @@ const HDPathDialog = ({
<HDPath <HDPath
selectedNetwork={selectedNetwork!} selectedNetwork={selectedNetwork!}
pathCode={pathCode} pathCode={pathCode}
updateIndex={updateIndex}
updateAccounts={updateAccounts} updateAccounts={updateAccounts}
hideDialog={hideDialog} hideDialog={hideDialog}
/> />

View File

@ -8,7 +8,6 @@ import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils';
import { PairingModalProps } from '../types'; import { PairingModalProps } from '../types';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { useAccounts } from '../context/AccountsContext'; import { useAccounts } from '../context/AccountsContext';
import { useWalletConnect } from '../context/WalletConnectContext'; import { useWalletConnect } from '../context/WalletConnectContext';
import { useNetworks } from '../context/NetworksContext'; import { useNetworks } from '../context/NetworksContext';
@ -21,7 +20,7 @@ const PairingModal = ({
setModalVisible, setModalVisible,
setToastVisible, setToastVisible,
}: PairingModalProps) => { }: PairingModalProps) => {
const { accounts, currentIndex } = useAccounts(); const { accounts } = useAccounts();
const { selectedNetwork, networksData } = useNetworks(); const { selectedNetwork, networksData } = useNetworks();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [chainError, setChainError] = useState(''); const [chainError, setChainError] = useState('');
@ -87,7 +86,7 @@ const PairingModal = ({
}); });
}, [currentProposal]); }, [currentProposal]);
const { setActiveSessions } = useWalletConnect(); const { setActiveSessions, web3wallet } = useWalletConnect();
useEffect(() => { useEffect(() => {
const getSupportedNamespaces = async () => { const getSupportedNamespaces = async () => {
@ -104,7 +103,6 @@ const PairingModal = ({
networksData, networksData,
selectedNetwork!, selectedNetwork!,
accounts, accounts,
currentIndex,
); );
setSupportedNamespaces(nameSpaces); setSupportedNamespaces(nameSpaces);
} catch (err) { } catch (err) {
@ -130,7 +128,7 @@ const PairingModal = ({
networksData, networksData,
selectedNetwork, selectedNetwork,
accounts, accounts,
currentIndex, web3wallet,
setCurrentProposal, setCurrentProposal,
setModalVisible, setModalVisible,
]); ]);

View File

@ -0,0 +1,97 @@
import { TouchableOpacity, View } from 'react-native';
import React, { useState } from 'react';
import { Button, Text, Portal, Dialog, useTheme } from 'react-native-paper';
import styles from '../styles/stylesheet';
import { getPathKey } from '../utils/misc';
import { useNetworks } from '../context/NetworksContext';
import { useAccounts } from '../context/AccountsContext';
const ShowPKDialog = () => {
const { currentIndex } = useAccounts();
const { selectedNetwork } = useNetworks();
const [privateKey, setprivateKey] = useState<string>();
const [showPKDialog, setShowPKDialog] = useState<boolean>(false);
const theme = useTheme();
const handleShowPrivateKey = async () => {
const pathKey = await getPathKey(
`${selectedNetwork!.namespace}:${selectedNetwork!.chainId}`,
currentIndex,
);
setprivateKey(pathKey.privKey);
};
const hideShowPKDialog = () => {
setShowPKDialog(false);
setprivateKey(undefined);
};
return (
<>
<View style={styles.signLink}>
<TouchableOpacity
onPress={() => {
setShowPKDialog(true);
}}>
<Text
variant="titleSmall"
style={[styles.hyperlink, { color: theme.colors.primary }]}>
Show Private Key
</Text>
</TouchableOpacity>
</View>
<View>
<Portal>
<Dialog visible={showPKDialog} onDismiss={hideShowPKDialog}>
<Dialog.Title>
{!privateKey ? (
<Text>Show Private Key?</Text>
) : (
<Text>Private Key</Text>
)}
</Dialog.Title>
<Dialog.Content>
{privateKey && (
<View style={[styles.dataBox, styles.dataBoxContainer]}>
<Text
selectable
variant="bodyMedium"
style={styles.dataBoxData}>
{privateKey}
</Text>
</View>
)}
<View>
<Text variant="bodyMedium" style={styles.dialogWarning}>
<Text style={[styles.highlight, styles.dialogWarning]}>
Warning:
</Text>
Never disclose this key. Anyone with your private keys can
steal any assets held in your account.
</Text>
</View>
</Dialog.Content>
<Dialog.Actions>
{!privateKey ? (
<>
<Button onPress={handleShowPrivateKey} textColor="red">
Yes
</Button>
<Button onPress={hideShowPKDialog}>No</Button>
</>
) : (
<Button onPress={hideShowPKDialog}>Ok</Button>
)}
</Dialog.Actions>
</Dialog>
</Portal>
</View>
</>
);
};
export default ShowPKDialog;

View File

@ -1,14 +1,16 @@
import React, { createContext, useContext, useEffect, useState } from 'react'; import React, { createContext, useContext, useEffect, useState } from 'react';
import { SessionTypes } from '@walletconnect/types'; import { SessionTypes } from '@walletconnect/types';
import { IWeb3Wallet } from '@walletconnect/web3wallet';
import { WalletConnectContextProps } from '../types'; import { WalletConnectContextProps } from '../types';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import useInitialization from '../hooks/useInitialization'; import useInitialization from '../hooks/useInitialization';
const WalletConnectContext = createContext<WalletConnectContextProps>({ const WalletConnectContext = createContext<WalletConnectContextProps>({
activeSessions: {}, activeSessions: {},
setActiveSessions: () => {}, setActiveSessions: () => {},
web3wallet: {} as IWeb3Wallet,
setWeb3wallet: () => {},
}); });
const useWalletConnect = () => { const useWalletConnect = () => {
@ -17,12 +19,14 @@ const useWalletConnect = () => {
}; };
const WalletConnectProvider = ({ children }: { children: React.ReactNode }) => { const WalletConnectProvider = ({ children }: { children: React.ReactNode }) => {
useInitialization(); const [web3wallet, setWeb3wallet] = useState<IWeb3Wallet | undefined>();
useInitialization(setWeb3wallet);
useEffect(() => { useEffect(() => {
const sessions = (web3wallet && web3wallet.getActiveSessions()) || {}; const sessions = (web3wallet && web3wallet.getActiveSessions()) || {};
setActiveSessions(sessions); setActiveSessions(sessions);
}, []); }, [web3wallet]);
const [activeSessions, setActiveSessions] = useState< const [activeSessions, setActiveSessions] = useState<
Record<string, SessionTypes.Struct> Record<string, SessionTypes.Struct>
@ -33,6 +37,8 @@ const WalletConnectProvider = ({ children }: { children: React.ReactNode }) => {
value={{ value={{
activeSessions, activeSessions,
setActiveSessions, setActiveSessions,
web3wallet,
setWeb3wallet,
}}> }}>
{children} {children}
</WalletConnectContext.Provider> </WalletConnectContext.Provider>

View File

@ -1,17 +1,24 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { IWeb3Wallet } from '@walletconnect/web3wallet';
import { createWeb3Wallet } from '../utils/wallet-connect/WalletConnectUtils'; import { createWeb3Wallet } from '../utils/wallet-connect/WalletConnectUtils';
export default function useInitialization() { export default function useInitialization(
setWeb3wallet: React.Dispatch<React.SetStateAction<IWeb3Wallet | undefined>>,
) {
const [initialized, setInitialized] = useState(false); const [initialized, setInitialized] = useState(false);
const onInitialize = useCallback(async () => { const onInitialize = useCallback(async () => {
try { try {
await createWeb3Wallet(); const web3walletInstance = await createWeb3Wallet();
setWeb3wallet(web3walletInstance);
setInitialized(true); setInitialized(true);
} catch (err: unknown) { } catch (err: unknown) {
console.error('Error for initializing', err); console.error('Error for initializing', err);
} }
}, []); }, [setWeb3wallet]);
useEffect(() => { useEffect(() => {
if (!initialized) { if (!initialized) {

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { AppState, TouchableOpacity, View } from 'react-native'; import { AppState, TouchableOpacity, View } from 'react-native';
import { Button, Text, TextInput } from 'react-native-paper'; import { Button, Text, TextInput } from 'react-native-paper';
import { import {
@ -15,6 +15,7 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { web3WalletPair } from '../utils/wallet-connect/WalletConnectUtils'; import { web3WalletPair } from '../utils/wallet-connect/WalletConnectUtils';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import { StackParamsList } from '../types'; import { StackParamsList } from '../types';
import { useWalletConnect } from '../context/WalletConnectContext';
const AddSession = () => { const AddSession = () => {
const navigation = const navigation =
@ -23,6 +24,8 @@ const AddSession = () => {
const { hasPermission, requestPermission } = useCameraPermission(); const { hasPermission, requestPermission } = useCameraPermission();
const device = useCameraDevice('back'); const device = useCameraDevice('back');
const { web3wallet } = useWalletConnect();
const [currentWCURI, setCurrentWCURI] = useState<string>(''); const [currentWCURI, setCurrentWCURI] = useState<string>('');
const [isActive, setIsActive] = useState(AppState.currentState === 'active'); const [isActive, setIsActive] = useState(AppState.currentState === 'active');
const [isScanning, setScanning] = useState(true); const [isScanning, setScanning] = useState(true);
@ -45,11 +48,15 @@ const AddSession = () => {
await Linking.openSettings(); await Linking.openSettings();
}; };
const pair = async () => { const pair = useCallback(async () => {
const pairing = await web3WalletPair({ uri: currentWCURI }); if (!web3wallet) {
return;
}
const pairing = await web3WalletPair(web3wallet, { uri: currentWCURI });
navigation.navigate('WalletConnect'); navigation.navigate('WalletConnect');
return pairing; return pairing;
}; }, [web3wallet, currentWCURI, navigation]);
useEffect(() => { useEffect(() => {
const handleAppStateChange = (newState: string) => { const handleAppStateChange = (newState: string) => {

View File

@ -1,103 +1,67 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Image, ScrollView, View } from 'react-native'; import { Image, ScrollView, View } from 'react-native';
import { import { Button, Text, TextInput } from 'react-native-paper';
ActivityIndicator, import { SvgUri } from 'react-native-svg';
Button,
Text,
Appbar,
TextInput,
} from 'react-native-paper';
import { providers, BigNumber } from 'ethers';
import Config from 'react-native-config'; import Config from 'react-native-config';
import { Deferrable } from 'ethers/lib/utils'; import { MsgCreateValidator } from 'cosmjs-types/cosmos/staking/v1beta1/tx';
import { useNavigation } from '@react-navigation/native';
import { import {
NativeStackNavigationProp, NativeStackNavigationProp,
NativeStackScreenProps, NativeStackScreenProps,
} from '@react-navigation/native-stack'; } from '@react-navigation/native-stack';
import { getHeaderTitle } from '@react-navigation/elements'; import { useNavigation } from '@react-navigation/native';
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; import { DirectSecp256k1Wallet, EncodeObject } from '@cosmjs/proto-signing';
import { import { LaconicClient } from '@cerc-io/registry-sdk';
calculateFee, import { GasPrice, calculateFee } from '@cosmjs/stargate';
GasPrice, import { formatJsonRpcError } from '@json-rpc-tools/utils';
MsgSendEncodeObject,
SigningStargateClient,
} from '@cosmjs/stargate';
import { useNetworks } from '../context/NetworksContext';
import { Account, StackParamsList } from '../types'; import { Account, StackParamsList } from '../types';
import AccountDetails from '../components/AccountDetails';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
import { COSMOS, IS_NUMBER_REGEX } from '../utils/constants';
import { retrieveSingleAccount } from '../utils/accounts'; import { retrieveSingleAccount } from '../utils/accounts';
import { getPathKey } from '../utils/misc';
import { import {
WalletConnectRequests,
approveWalletConnectRequest, approveWalletConnectRequest,
rejectWalletConnectRequest, rejectWalletConnectRequest,
WalletConnectRequests,
} from '../utils/wallet-connect/wallet-connect-requests'; } from '../utils/wallet-connect/wallet-connect-requests';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils'; import { MEMO } from './ApproveTransfer';
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 TxErrorDialog from '../components/TxErrorDialog';
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data'; import AccountDetails from '../components/AccountDetails';
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData'; import { useWalletConnect } from '../context/WalletConnectContext';
const MEMO = 'Sending signed tx from Laconic Wallet'; type ApproveTransactionProps = NativeStackScreenProps<
// Reference: https://ethereum.org/en/developers/docs/gas/#what-is-gas-limit
const ETH_MINIMUM_GAS = 21000;
type SignRequestProps = NativeStackScreenProps<
StackParamsList, StackParamsList,
'ApproveTransaction' 'ApproveTransaction'
>; >;
const ApproveTransaction = ({ route }: SignRequestProps) => { const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
const { networksData } = useNetworks(); const { networksData } = useNetworks();
const requestSession = route.params.requestSessionData; const requestSession = route.params.requestSessionData;
const requestName = requestSession.peer.metadata.name; const requestName = requestSession.peer.metadata.name;
const requestIcon = requestSession.peer.metadata.icons[0]; const requestIcon = requestSession.peer.metadata.icons[0];
const requestURL = requestSession.peer.metadata.url; const requestURL = requestSession.peer.metadata.url;
const transaction = route.params.transaction; const signer = route.params.signer;
const requestEvent = route.params.requestEvent; const requestEvent = route.params.requestEvent;
const chainId = requestEvent.params.chainId; const chainId = requestEvent.params.chainId;
const requestMethod = requestEvent.params.request.method; const requestEventId = requestEvent.id;
const topic = requestEvent.topic;
const { web3wallet } = useWalletConnect();
const [account, setAccount] = useState<Account>(); const [account, setAccount] = useState<Account>();
const [isLoading, setIsLoading] = useState(true);
const [balance, setBalance] = useState<string>('');
const [isTxLoading, setIsTxLoading] = useState(false);
const [cosmosStargateClient, setCosmosStargateClient] = const [cosmosStargateClient, setCosmosStargateClient] =
useState<SigningStargateClient>(); useState<LaconicClient>();
const [fees, setFees] = useState<string>();
const [cosmosGasLimit, setCosmosGasLimit] = useState<string>(); const [cosmosGasLimit, setCosmosGasLimit] = useState<string>();
const [fees, setFees] = useState<string>();
const [txError, setTxError] = useState<string>(); const [txError, setTxError] = useState<string>();
const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false); const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false);
const [ethGasPrice, setEthGasPrice] = useState<BigNumber | null>(); const [isRequestAccepted, setIsRequestAccepted] = useState(false);
const [ethGasLimit, setEthGasLimit] = useState<BigNumber>();
const [ethMaxFee, setEthMaxFee] = useState<BigNumber | null>();
const [ethMaxPriorityFee, setEthMaxPriorityFee] =
useState<BigNumber | null>();
const isSufficientFunds = useMemo(() => { const navigation =
if (!transaction.value) { useNavigation<NativeStackNavigationProp<StackParamsList>>();
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( const requestedNetwork = networksData.find(
networkData => networkData =>
@ -105,21 +69,19 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
); );
const namespace = requestedNetwork!.namespace; const namespace = requestedNetwork!.namespace;
const sendMsg: MsgSendEncodeObject = useMemo(() => { const transactionMessage = useMemo((): EncodeObject => {
return { const inputTxMsg = route.params.transactionMessage;
typeUrl: '/cosmos.bank.v1beta1.MsgSend',
value: { // If it's a MsgCreateValidator, decode the tx msg value using MsgCreateValidator type
fromAddress: transaction.from, if (inputTxMsg.typeUrl.includes('MsgCreateValidator')) {
toAddress: transaction.to, return {
amount: [ typeUrl: inputTxMsg.typeUrl,
{ value: MsgCreateValidator.fromJSON(inputTxMsg.value),
amount: String(transaction.value), };
denom: requestedNetwork!.nativeDenom!, }
},
], return inputTxMsg;
}, }, [route.params.transactionMessage]);
};
}, [requestedNetwork, transaction]);
useEffect(() => { useEffect(() => {
if (namespace !== COSMOS) { if (namespace !== COSMOS) {
@ -144,41 +106,29 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
); );
try { try {
const client = await SigningStargateClient.connectWithSigner( const client = await LaconicClient.connectWithSigner(
requestedNetwork?.rpcUrl!, requestedNetwork?.rpcUrl!,
sender, sender,
); );
setCosmosStargateClient(client); setCosmosStargateClient(client);
} catch (error: any) { } catch (error: any) {
setTxError(error.message); setTxError(error.message);
setIsTxErrorDialogOpen(true); setIsTxErrorDialogOpen(true);
const response = formatJsonRpcError(requestEventId, error.message);
await web3wallet!.respondSessionRequest({ topic, response });
} }
}; };
setClient(); setClient();
}, [account, requestedNetwork, chainId, namespace]); }, [
account,
const provider = useMemo(() => { requestedNetwork,
if (namespace === EIP155) { chainId,
if (!requestedNetwork) { namespace,
throw new Error('Requested chain not supported'); requestEventId,
} topic,
try { web3wallet,
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( const retrieveData = useCallback(
async (requestAddress: string) => { async (requestAddress: string) => {
@ -198,245 +148,8 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
); );
useEffect(() => { useEffect(() => {
// Set loading to false when gas values for requested chain are fetched retrieveData(signer);
// 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 }, [retrieveData, signer]);
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(() => { useEffect(() => {
const getCosmosGas = async () => { const getCosmosGas = async () => {
@ -444,13 +157,9 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
if (!cosmosStargateClient) { if (!cosmosStargateClient) {
return; return;
} }
if (!isSufficientFunds) { const gasEstimation = await cosmosStargateClient!.simulate(
return; signer,
} [transactionMessage],
const gasEstimation = await cosmosStargateClient.simulate(
transaction.from!,
[sendMsg],
MEMO, MEMO,
); );
@ -462,163 +171,148 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
} catch (error: any) { } catch (error: any) {
setTxError(error.message); setTxError(error.message);
setIsTxErrorDialogOpen(true); setIsTxErrorDialogOpen(true);
const response = formatJsonRpcError(requestEventId, error.message);
await web3wallet!.respondSessionRequest({ topic, response });
} }
}; };
getCosmosGas(); getCosmosGas();
}, [cosmosStargateClient, isSufficientFunds, sendMsg, transaction]); }, [
cosmosStargateClient,
transactionMessage,
requestEventId,
topic,
web3wallet,
signer,
]);
useEffect(() => { useEffect(() => {
if (balance && !isSufficientFunds) { const gasPrice = GasPrice.fromString(
setTxError('Insufficient funds'); requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
setIsTxErrorDialogOpen(true); );
if (!cosmosGasLimit) {
return;
} }
}, [isSufficientFunds, balance]);
const cosmosFees = calculateFee(Number(cosmosGasLimit), gasPrice);
setFees(cosmosFees.amount[0].amount);
}, [namespace, cosmosGasLimit, requestedNetwork]);
const acceptRequestHandler = async () => {
try {
setIsRequestAccepted(true);
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 });
setIsRequestAccepted(false);
navigation.navigate('Laconic');
} catch (error: any) {
setTxError(error.message);
setIsTxErrorDialogOpen(true);
const response = formatJsonRpcError(requestEventId, error.message);
await web3wallet!.respondSessionRequest({ topic, response });
}
};
const rejectRequestHandler = async () => {
const response = rejectWalletConnectRequest(requestEvent);
await web3wallet!.respondSessionRequest({
topic,
response,
});
navigation.navigate('Laconic');
};
const replacer = (key: string, value: any): any => {
if (value instanceof Uint8Array) {
return Buffer.from(value).toString('hex');
}
return value;
};
return ( return (
<> <>
{isLoading ? ( <ScrollView contentContainerStyle={styles.approveTransaction}>
<View style={styles.spinnerContainer}> <View style={styles.dappDetails}>
<ActivityIndicator size="large" color="#0000ff" /> {requestIcon && (
</View> <>
) : ( {requestIcon.endsWith('.svg') ? (
<> <View style={styles.dappLogo}>
<ScrollView contentContainerStyle={styles.appContainer}> <SvgUri height="50" width="50" uri={requestIcon} />
<View style={styles.dappDetails}> </View>
{requestIcon && ( ) : (
<Image <Image style={styles.dappLogo} source={{ uri: requestIcon }} />
style={styles.dappLogo}
source={requestIcon ? { uri: requestIcon } : undefined}
/>
)} )}
<Text>{requestName}</Text> </>
<Text variant="bodyMedium">{requestURL}</Text> )}
</View> <Text>{requestName}</Text>
<View style={styles.dataBoxContainer}> <Text variant="bodySmall">{requestURL}</Text>
<Text style={styles.dataBoxLabel}>From</Text> </View>
<View style={styles.dataBox}> <AccountDetails account={account} />
<AccountDetails account={account} /> <Text variant="bodyLarge" style={styles.transactionLabel}>
</View> Message:
</View> </Text>
<DataBox <View style={styles.messageBody}>
label={`Balance (${ <Text variant="bodyLarge">
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom {JSON.stringify(transactionMessage, replacer, 2)}
})`} </Text>
data={ </View>
balance === '' || balance === undefined <>
? 'Loading balance...' <Text variant="bodyLarge" style={styles.transactionLabel}>
: `${balance}` Gas Limit:
</Text>
<TextInput
mode="outlined"
style={styles.transactionFeesInput}
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}> <View style={styles.buttonContainer}>
<Button <Button
mode="contained" mode="contained"
onPress={acceptRequestHandler} onPress={acceptRequestHandler}
loading={isTxLoading} loading={isRequestAccepted}
disabled={!balance || !fees}> disabled={isRequestAccepted}>
{isTxLoading ? 'Processing' : 'Yes'} Yes
</Button> </Button>
<Button <Button
mode="contained" mode="contained"
@ -628,16 +322,13 @@ const ApproveTransaction = ({ route }: SignRequestProps) => {
</Button> </Button>
</View> </View>
</> </>
)} </ScrollView>
<TxErrorDialog <TxErrorDialog
error={txError!} error={txError!}
visible={isTxErrorDialogOpen} visible={isTxErrorDialogOpen}
hideDialog={() => { hideDialog={async () => {
setIsTxErrorDialogOpen(false); setIsTxErrorDialogOpen(false);
if (!isSufficientFunds || !balance || !fees) { navigation.navigate('Laconic');
rejectRequestHandler();
navigation.navigate('Laconic');
}
}} }}
/> />
</> </>

View File

@ -0,0 +1,650 @@
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 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';
import { useWalletConnect } from '../context/WalletConnectContext';
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 { web3wallet } = useWalletConnect();
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;

View File

@ -16,7 +16,6 @@ import styles from '../styles/stylesheet';
import { useAccounts } from '../context/AccountsContext'; import { useAccounts } from '../context/AccountsContext';
import { useWalletConnect } from '../context/WalletConnectContext'; import { useWalletConnect } from '../context/WalletConnectContext';
import { NetworksDataState, StackParamsList } from '../types'; import { NetworksDataState, StackParamsList } from '../types';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { useNetworks } from '../context/NetworksContext'; import { useNetworks } from '../context/NetworksContext';
const WCLogo = () => { const WCLogo = () => {
@ -29,12 +28,11 @@ const WCLogo = () => {
}; };
const HomeScreen = () => { const HomeScreen = () => {
const { accounts, setAccounts, currentIndex, setCurrentIndex } = const { accounts, setAccounts, setCurrentIndex } = useAccounts();
useAccounts();
const { networksData, selectedNetwork, setSelectedNetwork, setNetworksData } = const { networksData, selectedNetwork, setSelectedNetwork, setNetworksData } =
useNetworks(); useNetworks();
const { setActiveSessions } = useWalletConnect(); const { web3wallet, setActiveSessions } = useWalletConnect();
const navigation = const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>(); useNavigation<NativeStackNavigationProp<StackParamsList>>();
@ -111,6 +109,7 @@ const HomeScreen = () => {
hideResetDialog(); hideResetDialog();
}, [ }, [
web3wallet,
setAccounts, setAccounts,
setActiveSessions, setActiveSessions,
setCurrentIndex, setCurrentIndex,
@ -123,10 +122,6 @@ const HomeScreen = () => {
setCurrentIndex(0); setCurrentIndex(0);
}; };
const updateIndex = (index: number) => {
setCurrentIndex(index);
};
useEffect(() => { useEffect(() => {
fetchAccounts(); fetchAccounts();
}, [networksData, setAccounts, selectedNetwork, fetchAccounts]); }, [networksData, setAccounts, selectedNetwork, fetchAccounts]);
@ -142,7 +137,7 @@ const HomeScreen = () => {
<> <>
<NetworkDropdown updateNetwork={updateNetwork} /> <NetworkDropdown updateNetwork={updateNetwork} />
<View style={styles.accountComponent}> <View style={styles.accountComponent}>
<Accounts currentIndex={currentIndex} updateIndex={updateIndex} /> <Accounts />
</View> </View>
<View style={styles.resetContainer}> <View style={styles.resetContainer}>
<Button <Button

View File

@ -20,10 +20,10 @@ import {
rejectWalletConnectRequest, rejectWalletConnectRequest,
WalletConnectRequests, WalletConnectRequests,
} from '../utils/wallet-connect/wallet-connect-requests'; } from '../utils/wallet-connect/wallet-connect-requests';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data'; import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
import { useNetworks } from '../context/NetworksContext'; import { useNetworks } from '../context/NetworksContext';
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData'; import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData';
import { useWalletConnect } from '../context/WalletConnectContext';
type SignRequestProps = NativeStackScreenProps<StackParamsList, 'SignRequest'>; type SignRequestProps = NativeStackScreenProps<StackParamsList, 'SignRequest'>;
@ -35,6 +35,8 @@ const SignRequest = ({ route }: SignRequestProps) => {
const requestIcon = requestSession?.peer?.metadata?.icons[0]; const requestIcon = requestSession?.peer?.metadata?.icons[0];
const requestURL = requestSession?.peer?.metadata?.url; const requestURL = requestSession?.peer?.metadata?.url;
const { web3wallet } = useWalletConnect();
const [account, setAccount] = useState<Account>(); const [account, setAccount] = useState<Account>();
const [message, setMessage] = useState<string>(''); const [message, setMessage] = useState<string>('');
const [namespace, setNamespace] = useState<string>(''); const [namespace, setNamespace] = useState<string>('');
@ -86,21 +88,13 @@ const SignRequest = ({ route }: SignRequestProps) => {
return; return;
} }
if (requestAccount !== account) { setAccount(requestAccount);
setAccount(requestAccount); setMessage(decodeURIComponent(requestMessage));
} setNamespace(requestNamespace);
if (requestMessage !== message) { setChainId(requestChainId);
setMessage(decodeURIComponent(requestMessage));
}
if (requestNamespace !== namespace) {
setNamespace(requestNamespace);
}
if (requestChainId !== chainId) {
setChainId(requestChainId);
}
setIsLoading(false); setIsLoading(false);
}, },
[account, message, navigation, namespace, chainId], [navigation],
); );
const sanitizePath = useCallback( const sanitizePath = useCallback(

View File

@ -6,11 +6,10 @@ import { SvgUri } from 'react-native-svg';
import { getSdkError } from '@walletconnect/utils'; import { getSdkError } from '@walletconnect/utils';
import { useWalletConnect } from '../context/WalletConnectContext'; import { useWalletConnect } from '../context/WalletConnectContext';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import styles from '../styles/stylesheet'; import styles from '../styles/stylesheet';
export default function WalletConnect() { export default function WalletConnect() {
const { activeSessions, setActiveSessions } = useWalletConnect(); const { web3wallet, activeSessions, setActiveSessions } = useWalletConnect();
const disconnect = async (sessionId: string) => { const disconnect = async (sessionId: string) => {
await web3wallet!.disconnectSession({ await web3wallet!.disconnectSession({
@ -23,9 +22,9 @@ export default function WalletConnect() {
}; };
useEffect(() => { useEffect(() => {
const sessions = web3wallet!.getActiveSessions(); const sessions = web3wallet?.getActiveSessions() || {};
setActiveSessions(sessions); setActiveSessions(sessions);
}, [setActiveSessions]); }, [web3wallet, setActiveSessions]);
return ( return (
<View> <View>

View File

@ -136,7 +136,7 @@ const styles = StyleSheet.create({
justifyContent: 'center', justifyContent: 'center',
padding: 8, padding: 8,
}, },
approveTransaction: { approveTransfer: {
height: '40%', height: '40%',
marginBottom: 30, marginBottom: 30,
}, },
@ -213,10 +213,10 @@ const styles = StyleSheet.create({
}, },
messageBody: { messageBody: {
borderWidth: 1, borderWidth: 1,
borderRadius: 10, borderRadius: 6,
paddingVertical: 10, paddingVertical: 10,
paddingHorizontal: 10, paddingHorizontal: 10,
marginVertical: 20, marginVertical: 3,
}, },
cameraContainer: { cameraContainer: {
justifyContent: 'center', justifyContent: 'center',
@ -272,6 +272,16 @@ const styles = StyleSheet.create({
marginVertical: 10, marginVertical: 10,
}, },
transactionFeesInput: { marginBottom: 10 }, transactionFeesInput: { marginBottom: 10 },
approveTransaction: {
flexGrow: 1,
marginTop: 0,
paddingHorizontal: 24,
paddingVertical: 5,
},
transactionLabel: {
fontWeight: '700',
padding: 8,
},
}); });
export default styles; export default styles;

View File

@ -1,7 +1,8 @@
import { PopulatedTransaction } from 'ethers'; import { PopulatedTransaction } from 'ethers';
import { SignClientTypes, SessionTypes } from '@walletconnect/types'; import { SignClientTypes, SessionTypes } from '@walletconnect/types';
import { Web3WalletTypes } from '@walletconnect/web3wallet'; import { IWeb3Wallet, Web3WalletTypes } from '@walletconnect/web3wallet';
import { EncodeObject } from '@cosmjs/proto-signing';
export type StackParamsList = { export type StackParamsList = {
Laconic: undefined; Laconic: undefined;
@ -17,7 +18,7 @@ export type StackParamsList = {
requestEvent?: Web3WalletTypes.SessionRequest; requestEvent?: Web3WalletTypes.SessionRequest;
requestSessionData?: SessionTypes.Struct; requestSessionData?: SessionTypes.Struct;
}; };
ApproveTransaction: { ApproveTransfer: {
transaction: PopulatedTransaction; transaction: PopulatedTransaction;
requestEvent: Web3WalletTypes.SessionRequest; requestEvent: Web3WalletTypes.SessionRequest;
requestSessionData: SessionTypes.Struct; requestSessionData: SessionTypes.Struct;
@ -29,6 +30,12 @@ export type StackParamsList = {
EditNetwork: { EditNetwork: {
selectedNetwork: NetworksDataState; selectedNetwork: NetworksDataState;
}; };
ApproveTransaction: {
transactionMessage: EncodeObject;
signer: string;
requestEvent: Web3WalletTypes.SessionRequest;
requestSessionData: SessionTypes.Struct;
};
}; };
export type Account = { export type Account = {
@ -38,11 +45,6 @@ export type Account = {
hdPath: string; hdPath: string;
}; };
export type AccountsProps = {
currentIndex: number;
updateIndex: (index: number) => void;
};
export type NetworkDropdownProps = { export type NetworkDropdownProps = {
updateNetwork: (networksData: NetworksDataState) => void; updateNetwork: (networksData: NetworksDataState) => void;
}; };
@ -88,7 +90,6 @@ export type HDPathDialogProps = {
pathCode: string; pathCode: string;
visible: boolean; visible: boolean;
hideDialog: () => void; hideDialog: () => void;
updateIndex: (index: number) => void;
updateAccounts: (account: Account) => void; updateAccounts: (account: Account) => void;
}; };
@ -126,4 +127,6 @@ export interface WalletConnectContextProps {
setActiveSessions: ( setActiveSessions: (
activeSessions: Record<string, SessionTypes.Struct>, activeSessions: Record<string, SessionTypes.Struct>,
) => void; ) => void;
web3wallet: IWeb3Wallet | undefined;
setWeb3wallet: React.Dispatch<React.SetStateAction<IWeb3Wallet | undefined>>;
} }

View File

@ -1,9 +1,22 @@
import Config from 'react-native-config';
import { COSMOS_TESTNET_CHAINS } from './wallet-connect/COSMOSData'; import { COSMOS_TESTNET_CHAINS } from './wallet-connect/COSMOSData';
import { EIP155_CHAINS } from './wallet-connect/EIP155Data'; import { EIP155_CHAINS } from './wallet-connect/EIP155Data';
export const EIP155 = 'eip155'; export const EIP155 = 'eip155';
export const COSMOS = 'cosmos'; export const COSMOS = 'cosmos';
export const DEFAULT_NETWORKS = [ export const DEFAULT_NETWORKS = [
{
chainId: 'laconic_9000-1',
networkName: 'laconicd',
namespace: COSMOS,
rpcUrl: Config.LACONICD_RPC_URL,
blockExplorerUrl: '',
nativeDenom: 'alnt',
addressPrefix: 'laconic',
coinType: '118',
gasPrice: '1',
isDefault: true,
},
{ {
chainId: '1', chainId: '1',
networkName: EIP155_CHAINS['eip155:1'].name, networkName: EIP155_CHAINS['eip155:1'].name,

View File

@ -38,4 +38,5 @@ export const COSMOS_SIGNING_METHODS = {
export const COSMOS_METHODS = { export const COSMOS_METHODS = {
...COSMOS_SIGNING_METHODS, ...COSMOS_SIGNING_METHODS,
COSMOS_SEND_TOKENS: 'cosmos_sendTokens', // Added for pay.laconic.com COSMOS_SEND_TOKENS: 'cosmos_sendTokens', // Added for pay.laconic.com
COSMOS_SEND_TRANSACTION: 'cosmos_sendTransaction', // Added for testnet onboarding app
}; };

View File

@ -6,7 +6,6 @@ import { Core } from '@walletconnect/core';
import { ICore } from '@walletconnect/types'; import { ICore } from '@walletconnect/types';
import { Web3Wallet, IWeb3Wallet } from '@walletconnect/web3wallet'; import { Web3Wallet, IWeb3Wallet } from '@walletconnect/web3wallet';
export let web3wallet: IWeb3Wallet | undefined;
export let core: ICore; export let core: ICore;
export async function createWeb3Wallet() { export async function createWeb3Wallet() {
@ -14,7 +13,7 @@ export async function createWeb3Wallet() {
projectId: Config.WALLET_CONNECT_PROJECT_ID, projectId: Config.WALLET_CONNECT_PROJECT_ID,
}); });
web3wallet = await Web3Wallet.init({ const web3wallet = await Web3Wallet.init({
core, core,
metadata: { metadata: {
name: 'Laconic Wallet', name: 'Laconic Wallet',
@ -23,9 +22,14 @@ export async function createWeb3Wallet() {
icons: ['https://avatars.githubusercontent.com/u/92608123'], icons: ['https://avatars.githubusercontent.com/u/92608123'],
}, },
}); });
return web3wallet;
} }
export async function web3WalletPair(params: { uri: string }) { export async function web3WalletPair(
web3wallet: IWeb3Wallet,
params: { uri: string },
) {
if (web3wallet) { if (web3wallet) {
return await web3wallet.core.pairing.pair({ uri: params.uri }); return await web3wallet.core.pairing.pair({ uri: params.uri });
} }

View File

@ -40,7 +40,6 @@ export const getNamespaces = async (
networksData: NetworksDataState[], networksData: NetworksDataState[],
selectedNetwork: NetworksDataState, selectedNetwork: NetworksDataState,
accounts: Account[], accounts: Account[],
currentIndex: number,
) => { ) => {
const namespaceChainId = `${selectedNetwork.namespace}:${selectedNetwork.chainId}`; const namespaceChainId = `${selectedNetwork.namespace}:${selectedNetwork.chainId}`;
@ -101,23 +100,6 @@ export const getNamespaces = async (
const requiredAddressesArray = await Promise.all(requiredAddressesPromise); const requiredAddressesArray = await Promise.all(requiredAddressesPromise);
const requiredAddresses = requiredAddressesArray.flat(); const requiredAddresses = requiredAddressesArray.flat();
let sortedAccounts = requiredAddresses;
// If selected network is included in chains requested from dApp,
// Put selected account as first account
if (walletConnectChains.includes(namespaceChainId)) {
const currentAddresses = requiredAddresses.filter(address =>
address.includes(namespaceChainId),
);
sortedAccounts = [
currentAddresses[currentIndex],
...currentAddresses.filter((address, index) => index !== currentIndex),
...requiredAddresses.filter(
address => !currentAddresses.includes(address),
),
];
}
// construct namespace object // construct namespace object
const newNamespaces = { const newNamespaces = {
eip155: { eip155: {
@ -133,7 +115,7 @@ export const getNamespaces = async (
...(optionalNamespaces.eip155?.events ?? []), ...(optionalNamespaces.eip155?.events ?? []),
...(requiredNamespaces.eip155?.events ?? []), ...(requiredNamespaces.eip155?.events ?? []),
], ],
accounts: sortedAccounts.filter(account => account.includes(EIP155)), accounts: requiredAddresses.filter(account => account.includes(EIP155)),
}, },
cosmos: { cosmos: {
chains: walletConnectChains.filter(chain => chain.includes(COSMOS)), chains: walletConnectChains.filter(chain => chain.includes(COSMOS)),
@ -147,18 +129,12 @@ export const getNamespaces = async (
...(optionalNamespaces.cosmos?.events ?? []), ...(optionalNamespaces.cosmos?.events ?? []),
...(requiredNamespaces.cosmos?.events ?? []), ...(requiredNamespaces.cosmos?.events ?? []),
], ],
accounts: sortedAccounts.filter(account => account.includes(COSMOS)), accounts: requiredAddresses.filter(account => account.includes(COSMOS)),
}, },
}; };
return newNamespaces; return newNamespaces;
} else { } else {
// Set selected account as the first account in supported namespaces
const sortedAccounts = [
accounts[currentIndex],
...accounts.filter((account, index) => index !== currentIndex),
];
switch (selectedNetwork.namespace) { switch (selectedNetwork.namespace) {
case EIP155: case EIP155:
return { return {
@ -175,7 +151,7 @@ export const getNamespaces = async (
...(optionalNamespaces.eip155?.events ?? []), ...(optionalNamespaces.eip155?.events ?? []),
...(requiredNamespaces.eip155?.events ?? []), ...(requiredNamespaces.eip155?.events ?? []),
], ],
accounts: sortedAccounts.map(ethAccount => { accounts: accounts.map(ethAccount => {
return `${namespaceChainId}:${ethAccount.address}`; return `${namespaceChainId}:${ethAccount.address}`;
}), }),
}, },
@ -200,7 +176,7 @@ export const getNamespaces = async (
...(optionalNamespaces.cosmos?.events ?? []), ...(optionalNamespaces.cosmos?.events ?? []),
...(requiredNamespaces.cosmos?.events ?? []), ...(requiredNamespaces.cosmos?.events ?? []),
], ],
accounts: sortedAccounts.map(cosmosAccount => { accounts: accounts.map(cosmosAccount => {
return `${namespaceChainId}:${cosmosAccount.address}`; return `${namespaceChainId}:${cosmosAccount.address}`;
}), }),
}, },

View File

@ -9,6 +9,8 @@ import {
StdFee, StdFee,
MsgSendEncodeObject, MsgSendEncodeObject,
} from '@cosmjs/stargate'; } from '@cosmjs/stargate';
import { EncodeObject } from '@cosmjs/proto-signing';
import { LaconicClient } from '@cerc-io/registry-sdk';
import { EIP155_SIGNING_METHODS } from './EIP155Data'; import { EIP155_SIGNING_METHODS } from './EIP155Data';
import { signDirectMessage, signEthMessage } from '../sign-message'; import { signDirectMessage, signEthMessage } from '../sign-message';
@ -49,12 +51,20 @@ interface CosmosSendTokens {
memo: string; memo: string;
} }
interface CosmosSendTransaction {
type: 'cosmos_sendTransaction';
LaconicClient: LaconicClient;
cosmosFee: StdFee;
txMsg: EncodeObject;
}
export type WalletConnectRequests = export type WalletConnectRequests =
| EthSendTransaction | EthSendTransaction
| EthPersonalSign | EthPersonalSign
| CosmosSignDirect | CosmosSignDirect
| CosmosSignAmino | CosmosSignAmino
| CosmosSendTokens; | CosmosSendTokens
| CosmosSendTransaction;
export async function approveWalletConnectRequest( export async function approveWalletConnectRequest(
requestEvent: SignClientTypes.EventArguments['session_request'], requestEvent: SignClientTypes.EventArguments['session_request'],
@ -181,6 +191,20 @@ export async function approveWalletConnectRequest(
signature: result.transactionHash, 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: default:
throw new Error(getSdkError('INVALID_METHOD').message); throw new Error(getSdkError('INVALID_METHOD').message);
} }

932
yarn.lock

File diff suppressed because it is too large Load Diff