Compare commits

..
Author SHA1 Message Date
IshaVenikar 899cb90d40 Implement functionality to import wallet from mnemonic 2024-08-09 15:38:26 +05:30
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
nabarunandShreerang Kale 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
15 changed files with 224 additions and 103 deletions
+1
View File
@@ -21,6 +21,7 @@
"@hookform/resolvers": "^3.3.4",
"@json-rpc-tools/utils": "^1.7.6",
"@react-native-async-storage/async-storage": "^1.22.3",
"@react-native-clipboard/clipboard": "^1.14.1",
"@react-native-community/netinfo": "^11.3.1",
"@react-navigation/elements": "^1.3.30",
"@react-navigation/native": "^6.1.10",
+31
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
-1
View File
@@ -60,7 +60,6 @@ const Accounts = () => {
networksData,
selectedNetwork!,
accounts,
currentIndex,
);
if (!updatedNamespaces) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { View } from 'react-native';
import React from 'react';
import { View } from 'react-native';
import { Button } from 'react-native-paper';
import { CreateWalletProps } from '../types';
-40
View File
@@ -1,40 +0,0 @@
import React from 'react';
import { View } from 'react-native';
import { Button, Dialog, Portal, Text } from 'react-native-paper';
import styles from '../styles/stylesheet';
import GridView from './Grid';
import { CustomDialogProps } from '../types';
const DialogComponent = ({
visible,
hideDialog,
contentText,
}: CustomDialogProps) => {
const words = contentText.split(' ');
return (
<Portal>
<Dialog visible={visible} onDismiss={hideDialog}>
<Dialog.Content>
<Text variant="titleLarge">Mnemonic</Text>
<View style={styles.dialogTitle}>
<Text variant="titleMedium">
Your mnemonic provides full access to your wallet and funds. Make
sure to note it down.{' '}
</Text>
<Text variant="titleMedium" style={styles.dialogWarning}>
Do not share your mnemonic with anyone
</Text>
<GridView words={words} />
</View>
</Dialog.Content>
<Dialog.Actions>
<Button onPress={hideDialog}>Done</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);
};
export { DialogComponent };
+60
View File
@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import { View, TextInput } from 'react-native';
import { Dialog, Portal, Paragraph, Button } from 'react-native-paper';
import styles from '../styles/stylesheet';
const ImportWalletDialog = ({
visible,
hideDialog,
importWalletHandler,
}: {
visible: boolean;
hideDialog: () => void;
importWalletHandler: (recoveryPhrase: string) => Promise<void>;
}) => {
const [words, setWords] = useState<string[]>(Array(12).fill(''));
const handleWordChange = (index: number, value: string) => {
const newWords = [...words];
newWords[index] = value;
setWords(newWords);
};
return (
<Portal>
<Dialog visible={visible} onDismiss={hideDialog}>
<Dialog.Title>Import your wallet from your mnemonic</Dialog.Title>
<Dialog.Content>
<Paragraph>
(You can paste your entire mnemonic into the first textbox)
</Paragraph>
<View style={styles.container}>
{words.map((word, index) => (
<View style={styles.inputContainer} key={index}>
<TextInput
value={word}
onChangeText={text => handleWordChange(index, text)}
placeholder={`Word ${index + 1}`}
style={styles.textInput}
/>
</View>
))}
</View>
</Dialog.Content>
<Dialog.Actions>
<Button
mode="contained"
onPress={() => importWalletHandler(words.join(' '))}>
Import Wallet
</Button>
<Button mode="text" onPress={hideDialog}>
Cancel
</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);
};
export default ImportWalletDialog;
+58
View File
@@ -0,0 +1,58 @@
import React, { useState } from 'react';
import { View } from 'react-native';
import { Button, Dialog, Portal, Snackbar, Text } from 'react-native-paper';
import Clipboard from '@react-native-clipboard/clipboard';
import styles from '../styles/stylesheet';
import GridView from './Grid';
import { CustomDialogProps } from '../types';
const MnemonicDialog = ({
visible,
hideDialog,
contentText,
}: CustomDialogProps) => {
const [toastVisible, setToastVisible] = useState(false);
const words = contentText.split(' ');
const handleCopy = () => {
Clipboard.setString(contentText);
setToastVisible(true);
};
return (
<>
<Portal>
<Dialog visible={visible} onDismiss={hideDialog}>
<Dialog.Content>
<Text variant="titleLarge">Mnemonic</Text>
<View style={styles.dialogTitle}>
<Text variant="titleMedium">
Your mnemonic provides full access to your wallet and funds.
Make sure to note it down.{' '}
</Text>
<Text variant="titleMedium" style={styles.dialogWarning}>
Do not share your mnemonic with anyone
</Text>
<GridView words={words} />
</View>
</Dialog.Content>
<Dialog.Actions>
<Button onPress={handleCopy}>Copy</Button>
<Button onPress={hideDialog}>Done</Button>
</Dialog.Actions>
</Dialog>
</Portal>
<Snackbar
visible={toastVisible}
onDismiss={() => setToastVisible(false)}
duration={1000}>
Mnemonic copied to clipboard
</Snackbar>
</>
);
};
export { MnemonicDialog };
+1 -3
View File
@@ -20,7 +20,7 @@ const PairingModal = ({
setModalVisible,
setToastVisible,
}: PairingModalProps) => {
const { accounts, currentIndex } = useAccounts();
const { accounts } = useAccounts();
const { selectedNetwork, networksData } = useNetworks();
const [isLoading, setIsLoading] = useState(false);
const [chainError, setChainError] = useState('');
@@ -103,7 +103,6 @@ const PairingModal = ({
networksData,
selectedNetwork!,
accounts,
currentIndex,
);
setSupportedNamespaces(nameSpaces);
} catch (err) {
@@ -129,7 +128,6 @@ const PairingModal = ({
networksData,
selectedNetwork,
accounts,
currentIndex,
web3wallet,
setCurrentProposal,
setModalVisible,
+37 -11
View File
@@ -7,7 +7,8 @@ import { useNavigation } from '@react-navigation/native';
import { getSdkError } from '@walletconnect/utils';
import { createWallet, resetWallet, retrieveAccounts } from '../utils/accounts';
import { DialogComponent } from '../components/Dialog';
import { MnemonicDialog } from '../components/MnemonicDialog';
import ImportWalletDialog from '../components/ImportWalletDialog';
import { NetworkDropdown } from '../components/NetworkDropdown';
import Accounts from '../components/Accounts';
import CreateWallet from '../components/CreateWallet';
@@ -55,12 +56,13 @@ const HomeScreen = () => {
const [isWalletCreated, setIsWalletCreated] = useState<boolean>(false);
const [isWalletCreating, setIsWalletCreating] = useState<boolean>(false);
const [walletDialog, setWalletDialog] = useState<boolean>(false);
const [importWalletDialog, setImportWalletDialog] = useState<boolean>(false);
const [mnemonicDialog, setMnemonicDialog] = useState<boolean>(false);
const [resetWalletDialog, setResetWalletDialog] = useState<boolean>(false);
const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(false);
const [phrase, setPhrase] = useState('');
const hideWalletDialog = () => setWalletDialog(false);
const hideMnemonicDialog = () => setMnemonicDialog(false);
const hideResetDialog = () => setResetWalletDialog(false);
const fetchAccounts = useCallback(async () => {
@@ -83,12 +85,22 @@ const HomeScreen = () => {
const mnemonic = await createWallet(networksData);
if (mnemonic) {
fetchAccounts();
setWalletDialog(true);
setMnemonicDialog(true);
setPhrase(mnemonic);
setSelectedNetwork(networksData[0]);
}
};
const importWalletHandler = async (recoveryPhrase: string) => {
const mnemonic = await createWallet(networksData, recoveryPhrase);
if (mnemonic) {
fetchAccounts();
setPhrase(mnemonic);
setSelectedNetwork(networksData[0]);
setImportWalletDialog(false);
}
};
const confirmResetWallet = useCallback(async () => {
setIsWalletCreated(false);
setIsWalletCreating(false);
@@ -152,14 +164,28 @@ const HomeScreen = () => {
</View>
</>
) : (
<CreateWallet
isWalletCreating={isWalletCreating}
createWalletHandler={createWalletHandler}
/>
<>
<CreateWallet
isWalletCreating={isWalletCreating}
createWalletHandler={createWalletHandler}
/>
<View style={styles.createWalletContainer}>
<Button
mode="contained"
onPress={() => setImportWalletDialog(true)}>
Import Wallet
</Button>
</View>
</>
)}
<DialogComponent
visible={walletDialog}
hideDialog={hideWalletDialog}
<ImportWalletDialog
visible={importWalletDialog}
hideDialog={() => setImportWalletDialog(false)}
importWalletHandler={importWalletHandler}
/>
<MnemonicDialog
visible={mnemonicDialog}
hideDialog={hideMnemonicDialog}
contentText={phrase}
/>
<ConfirmDialog
+5 -13
View File
@@ -88,21 +88,13 @@ const SignRequest = ({ route }: SignRequestProps) => {
return;
}
if (requestAccount !== account) {
setAccount(requestAccount);
}
if (requestMessage !== message) {
setMessage(decodeURIComponent(requestMessage));
}
if (requestNamespace !== namespace) {
setNamespace(requestNamespace);
}
if (requestChainId !== chainId) {
setChainId(requestChainId);
}
setAccount(requestAccount);
setMessage(decodeURIComponent(requestMessage));
setNamespace(requestNamespace);
setChainId(requestChainId);
setIsLoading(false);
},
[account, message, navigation, namespace, chainId],
[navigation],
);
const sanitizePath = useCallback(
+6
View File
@@ -117,6 +117,12 @@ const styles = StyleSheet.create({
fontSize: 18,
padding: 10,
},
textInput: {
height: 40,
borderWidth: 1,
borderRadius: 4,
paddingHorizontal: 10,
},
requestMessage: {
borderWidth: 1,
borderRadius: 5,
+14 -3
View File
@@ -27,12 +27,25 @@ import { COSMOS, EIP155 } from './constants';
const createWallet = async (
networksData: NetworksDataState[],
recoveryPhrase?: string,
): Promise<string> => {
const mnemonic = utils.entropyToMnemonic(utils.randomBytes(16));
const mnemonic = recoveryPhrase
? recoveryPhrase
: utils.entropyToMnemonic(utils.randomBytes(16));
await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic);
const hdNode = HDNode.fromMnemonic(mnemonic);
await createWalletFromMnemonic(networksData, hdNode, mnemonic);
return mnemonic;
};
const createWalletFromMnemonic = async (
networksData: NetworksDataState[],
hdNode: HDNode,
mnemonic: string,
): Promise<void> => {
for (const network of networksData) {
const hdPath = `m/44'/${network.coinType}'/0'/0/0`;
const node = hdNode.derivePath(hdPath);
@@ -73,8 +86,6 @@ const createWallet = async (
),
]);
}
return mnemonic;
};
const addAccount = async (
+1 -3
View File
@@ -1,5 +1,3 @@
import { DENOM } from '@cerc-io/registry-sdk';
import Config from 'react-native-config';
import { COSMOS_TESTNET_CHAINS } from './wallet-connect/COSMOSData';
import { EIP155_CHAINS } from './wallet-connect/EIP155Data';
@@ -13,7 +11,7 @@ export const DEFAULT_NETWORKS = [
namespace: COSMOS,
rpcUrl: Config.LACONICD_RPC_URL,
blockExplorerUrl: '',
nativeDenom: DENOM,
nativeDenom: 'alnt',
addressPrefix: 'laconic',
coinType: '118',
gasPrice: '1',
+4 -28
View File
@@ -40,7 +40,6 @@ export const getNamespaces = async (
networksData: NetworksDataState[],
selectedNetwork: NetworksDataState,
accounts: Account[],
currentIndex: number,
) => {
const namespaceChainId = `${selectedNetwork.namespace}:${selectedNetwork.chainId}`;
@@ -101,23 +100,6 @@ export const getNamespaces = async (
const requiredAddressesArray = await Promise.all(requiredAddressesPromise);
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
const newNamespaces = {
eip155: {
@@ -133,7 +115,7 @@ export const getNamespaces = async (
...(optionalNamespaces.eip155?.events ?? []),
...(requiredNamespaces.eip155?.events ?? []),
],
accounts: sortedAccounts.filter(account => account.includes(EIP155)),
accounts: requiredAddresses.filter(account => account.includes(EIP155)),
},
cosmos: {
chains: walletConnectChains.filter(chain => chain.includes(COSMOS)),
@@ -147,18 +129,12 @@ export const getNamespaces = async (
...(optionalNamespaces.cosmos?.events ?? []),
...(requiredNamespaces.cosmos?.events ?? []),
],
accounts: sortedAccounts.filter(account => account.includes(COSMOS)),
accounts: requiredAddresses.filter(account => account.includes(COSMOS)),
},
};
return newNamespaces;
} else {
// Set selected account as the first account in supported namespaces
const sortedAccounts = [
accounts[currentIndex],
...accounts.filter((account, index) => index !== currentIndex),
];
switch (selectedNetwork.namespace) {
case EIP155:
return {
@@ -175,7 +151,7 @@ export const getNamespaces = async (
...(optionalNamespaces.eip155?.events ?? []),
...(requiredNamespaces.eip155?.events ?? []),
],
accounts: sortedAccounts.map(ethAccount => {
accounts: accounts.map(ethAccount => {
return `${namespaceChainId}:${ethAccount.address}`;
}),
},
@@ -200,7 +176,7 @@ export const getNamespaces = async (
...(optionalNamespaces.cosmos?.events ?? []),
...(requiredNamespaces.cosmos?.events ?? []),
],
accounts: sortedAccounts.map(cosmosAccount => {
accounts: accounts.map(cosmosAccount => {
return `${namespaceChainId}:${cosmosAccount.address}`;
}),
},
+5
View File
@@ -2071,6 +2071,11 @@
dependencies:
merge-options "^3.0.4"
"@react-native-clipboard/clipboard@^1.14.1":
version "1.14.1"
resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.14.1.tgz#835f82fc86881a0808a8405f2576617bb5383554"
integrity sha512-SM3el0A28SwoeJljVNhF217o0nI4E7RfalLmuRQcT1/7tGcxUjgFa3jyrEndYUct8/uxxK5EUNGUu1YEDqzxqw==
"@react-native-community/cli-clean@12.3.2":
version "12.3.2"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-12.3.2.tgz#d4f1730c3d22d816b4d513d330d5f3896a3f5921"