Compare commits

..

19 Commits

Author SHA1 Message Date
ec3617ad42 style: sign message and use layout component 2024-08-09 16:06:49 -04:00
393a42fceb feat: home link icon 2024-08-09 16:06:49 -04:00
4ff1c10699 style: add network 2024-08-09 16:06:48 -04:00
17810801dd style: gas price width 2024-08-09 16:06:17 -04:00
Monkey
da3e4534d0 show private key dialog styling 2024-08-09 16:06:17 -04:00
a1747b8ba7 style: edit network and logo 2024-08-09 16:06:17 -04:00
Monkey
c818bc0b9d Show Private Key confirmation dialog button styling 2024-08-09 16:06:17 -04:00
Monkey
b2a043068a use mui/lab LoadingButton for CreateWallet 2024-08-09 16:06:16 -04:00
Monkey
f67569a25e replace Dialog.tsx changes 2024-08-09 16:04:50 -04:00
Monkey
38d68a86d5 reset dialog styles 2024-08-09 16:04:50 -04:00
7b72631206 style: container 2024-08-09 16:04:48 -04:00
45e045d956 style: accordions on home screen 2024-08-09 16:03:18 -04:00
181f9e8b7d style: home screen first pass 2024-08-09 16:03:18 -04:00
cca89775aa style: home screen first pass 2024-08-09 16:03:17 -04:00
Monkey
afd09dc4c1 mnemonic dialog styles 2024-08-09 15:58:01 -04:00
e0632d1a50 style: initial dark mode 2024-08-09 15:57:29 -04:00
ba05a82406 Disable import wallet functionality with const flag (#14)
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: #14
2024-08-09 10:56:51 +00:00
2bb92205ba Implement functionality to import wallet from mnemonic (#13)
Part of #11

Co-authored-by: IshaVenikar <ishavenikar7@gmail.com>
Reviewed-on: #13
2024-08-09 10:24:38 +00:00
c26bddec1a Add support for staking module tx MsgCreateValidator (#12)
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: #12
2024-08-09 09:12:32 +00:00
10 changed files with 236 additions and 25 deletions

18
prettier.config.js Normal file
View File

@ -0,0 +1,18 @@
const config = {
arrowParens: "always",
printWidth: 80,
semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: "es5",
importOrderSeparation: true,
importOrderSortSpecifiers: true,
plugins: ["@trivago/prettier-plugin-sort-imports"],
importOrder: [
"<THIRD_PARTY_MODULES>",
"^(pages|components|utils|icons|test|graphql)/(.*)$",
"^[./]",
],
};
export default config;

View File

@ -1,5 +1,5 @@
import { View } from "react-native";
import React from "react"; import React from "react";
import { View } from "react-native";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import { CreateWalletProps } from "../types"; import { CreateWalletProps } from "../types";

View File

@ -0,0 +1,66 @@
import React, { useEffect, useState } from 'react';
import { Dialog, DialogActions, DialogContent, DialogTitle, TextField, Grid, Button, Typography } from "@mui/material";
const ImportWalletDialog = ({
visible,
hideDialog,
importWalletHandler
}: {
visible: boolean;
hideDialog: () => void;
importWalletHandler: (recoveryPhrase: string) => Promise<void>;
}) => {
const [words, setWords] = useState(Array(12).fill(''));
const handleWordChange = (index: number, value: string) => {
const newWords = [...words];
newWords[index] = value;
setWords(newWords);
};
const handlePaste = (event: React.ClipboardEvent<HTMLDivElement>) => {
const pastedText = event.clipboardData.getData('Text');
const splitWords = pastedText.trim().split(/\s+/);
if (splitWords.length === 12) {
setWords(splitWords);
event.preventDefault();
}
};
useEffect(() => {
setWords(Array(12).fill(''));
},[visible]);
return (
<Dialog open={visible} onClose={hideDialog}>
<DialogTitle>Import your wallet from your mnemonic</DialogTitle>
<DialogContent>
<Typography>
(You can paste your entire mnemonic into the first textbox)
</Typography>
<Grid container spacing={2}>
{words.map((word, index) => (
<Grid item xs={6} sm={4} key={index}>
<TextField
value={word}
onChange={(e) => handleWordChange(index, e.target.value)}
onPaste={index === 0 ? handlePaste : undefined}
placeholder={`Word ${index + 1}`}
fullWidth
margin="normal"
/>
</Grid>
))}
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={() => importWalletHandler(words.join(' '))}>Import Wallet</Button>
<Button onClick={hideDialog}>Cancel</Button>
</DialogActions>
</Dialog>
);
};
export default ImportWalletDialog;

View File

@ -0,0 +1,51 @@
import React from "react";
import {
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Button,
Typography,
} from "@mui/material";
import styles from "../styles/stylesheet";
import GridView from "./Grid";
import { CustomDialogProps } from "../types";
const MnemonicDialog = ({
visible,
hideDialog,
contentText,
}: CustomDialogProps) => {
const words = contentText.split(" ");
const copyMnemonic = () => {
navigator.clipboard.writeText(contentText);
};
return (
<Dialog open={visible} onClose={hideDialog}>
<DialogTitle style={{ ...styles.mnemonicTitle }}>Mnemonic</DialogTitle>
<DialogContent style={{ ...styles.mnemonicContainer }}>
<Typography component="div">
Your mnemonic provides full access to your wallet and funds. <br />
Make sure to note it down.
</Typography>
<Typography component="div" style={{ ...styles.mnomonicDialogWarning }}>
Do not share your mnemonic with anyone
</Typography>
<GridView words={words} />
</DialogContent>
<DialogActions style={{ ...styles.mnemonicButtonRow }}>
<Button style={{ ...styles.mnemonicButton }} onClick={copyMnemonic}>
Copy
</Button>
<Button style={{ ...styles.mnemonicButton }} onClick={hideDialog}>
Done
</Button>
</DialogActions>
</Dialog>
);
};
export { MnemonicDialog };

View File

@ -1,13 +1,14 @@
import React, { useCallback, useEffect, 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 { Button, Text, TextInput } from 'react-native-paper'; import { Button, Text, TextInput } from 'react-native-paper';
import { MsgCreateValidator } from 'cosmjs-types/cosmos/staking/v1beta1/tx';
import { import {
NativeStackNavigationProp, NativeStackNavigationProp,
NativeStackScreenProps, NativeStackScreenProps,
} from '@react-navigation/native-stack'; } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; import { DirectSecp256k1Wallet, EncodeObject } from '@cosmjs/proto-signing';
import { LaconicClient } from '@cerc-io/registry-sdk'; import { LaconicClient } from '@cerc-io/registry-sdk';
import { GasPrice, calculateFee } from '@cosmjs/stargate'; import { GasPrice, calculateFee } from '@cosmjs/stargate';
import { formatJsonRpcError } from '@json-rpc-tools/utils'; import { formatJsonRpcError } from '@json-rpc-tools/utils';
@ -40,7 +41,6 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
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 transactionMessage = route.params.transactionMessage;
const signer = route.params.signer; 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;
@ -67,6 +67,20 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
const { web3wallet } = useWalletConnect(); const { web3wallet } = useWalletConnect();
const transactionMessage = useMemo((): EncodeObject => {
const inputTxMsg = route.params.transactionMessage;
// If it's a MsgCreateValidator, decode the tx msg value using MsgCreateValidator type
if (inputTxMsg.typeUrl.includes('MsgCreateValidator')) {
return {
typeUrl: inputTxMsg.typeUrl,
value: MsgCreateValidator.fromJSON(inputTxMsg.value)
}
}
return inputTxMsg;
}, [route.params.transactionMessage]);
useEffect(() => { useEffect(() => {
if (namespace !== COSMOS) { if (namespace !== COSMOS) {
return; return;
@ -138,7 +152,7 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
return; return;
} }
const gasEstimation = await cosmosStargateClient!.simulate( const gasEstimation = await cosmosStargateClient!.simulate(
transactionMessage.value.participant!, signer,
[transactionMessage], [transactionMessage],
MEMO, MEMO,
); );
@ -160,7 +174,7 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
} }
}; };
getCosmosGas(); getCosmosGas();
}, [cosmosStargateClient, transactionMessage, requestEventId, topic, web3wallet]); }, [cosmosStargateClient, transactionMessage, requestEventId, topic, web3wallet, signer]);
useEffect(() => { useEffect(() => {
const gasPrice = GasPrice.fromString( const gasPrice = GasPrice.fromString(
@ -239,6 +253,13 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
navigation.navigate('Home'); navigation.navigate('Home');
}; };
const replacer = (key: string, value: any): any => {
if (value instanceof Uint8Array) {
return Buffer.from(value).toString('hex');
}
return value;
};
return ( return (
<> <>
<ScrollView contentContainerStyle={styles.approveTransaction}> <ScrollView contentContainerStyle={styles.approveTransaction}>
@ -264,7 +285,7 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
</Text> </Text>
<View style={styles.messageBody}> <View style={styles.messageBody}>
<Text variant="bodyLarge"> <Text variant="bodyLarge">
{JSON.stringify(transactionMessage, null, 2)} {JSON.stringify(transactionMessage, replacer, 2)}
</Text> </Text>
</View> </View>
<> <>

View File

@ -1,14 +1,13 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { View, ActivityIndicator, Image } from "react-native"; import { View, ActivityIndicator, Image } from "react-native";
import { Text } from "react-native-paper"; import { Text } from "react-native-paper";
import { Button, Divider } from "@mui/material"; import { Button, Divider, Portal, Snackbar } from "@mui/material";
import { NativeStackNavigationProp } from "@react-navigation/native-stack"; import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import { getSdkError } from "@walletconnect/utils"; import { getSdkError } from "@walletconnect/utils";
import { createWallet, resetWallet, retrieveAccounts } from "../utils/accounts"; import { createWallet, resetWallet, retrieveAccounts } from "../utils/accounts";
import { DialogComponent } from "../components/Dialog";
import { NetworkDropdown } from "../components/NetworkDropdown"; import { NetworkDropdown } from "../components/NetworkDropdown";
import Accounts from "../components/Accounts"; import Accounts from "../components/Accounts";
import CreateWallet from "../components/CreateWallet"; import CreateWallet from "../components/CreateWallet";
@ -18,6 +17,8 @@ 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 { useNetworks } from "../context/NetworksContext"; import { useNetworks } from "../context/NetworksContext";
import ImportWalletDialog from "../components/ImportWalletDialog";
import { MnemonicDialog } from "../components/MnemonicDialog";
import { Container } from "../components/Container"; import { Container } from "../components/Container";
const WCLogo = () => { const WCLogo = () => {
@ -57,12 +58,15 @@ const HomeScreen = () => {
const [isWalletCreated, setIsWalletCreated] = useState<boolean>(false); const [isWalletCreated, setIsWalletCreated] = useState<boolean>(false);
const [isWalletCreating, setIsWalletCreating] = 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 [resetWalletDialog, setResetWalletDialog] = useState<boolean>(false);
const [toastVisible, setToastVisible] = useState(false);
const [invalidMnemonicError, setInvalidMnemonicError] = useState("");
const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(true); const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(true);
const [phrase, setPhrase] = useState(""); const [phrase, setPhrase] = useState("");
const hideWalletDialog = () => setWalletDialog(false); const hideMnemonicDialog = () => setMnemonicDialog(false);
const hideResetDialog = () => setResetWalletDialog(false); const hideResetDialog = () => setResetWalletDialog(false);
const fetchAccounts = useCallback(async () => { const fetchAccounts = useCallback(async () => {
@ -85,12 +89,27 @@ const HomeScreen = () => {
const mnemonic = await createWallet(networksData); const mnemonic = await createWallet(networksData);
if (mnemonic) { if (mnemonic) {
fetchAccounts(); fetchAccounts();
setWalletDialog(true); setMnemonicDialog(true);
setPhrase(mnemonic); setPhrase(mnemonic);
setSelectedNetwork(networksData[0]); setSelectedNetwork(networksData[0]);
} }
}; };
const importWalletHandler = async (recoveryPhrase: string) => {
try {
const mnemonic = await createWallet(networksData, recoveryPhrase);
if (mnemonic) {
fetchAccounts();
setPhrase(mnemonic);
setSelectedNetwork(networksData[0]);
setImportWalletDialog(false);
}
} catch (error: any) {
setInvalidMnemonicError((error.message as string).toUpperCase());
setToastVisible(true);
}
};
const confirmResetWallet = useCallback(async () => { const confirmResetWallet = useCallback(async () => {
setIsWalletCreated(false); setIsWalletCreated(false);
setIsWalletCreating(false); setIsWalletCreating(false);
@ -154,14 +173,29 @@ const HomeScreen = () => {
</Button> </Button>
</> </>
) : ( ) : (
<CreateWallet <>
isWalletCreating={isWalletCreating} <CreateWallet
createWalletHandler={createWalletHandler} isWalletCreating={isWalletCreating}
/> createWalletHandler={createWalletHandler}
/>
<View style={styles.createWalletContainer}>
<Button
variant="contained"
onClick={() => setImportWalletDialog(true)}
>
Import Wallet
</Button>
</View>
</>
)} )}
<DialogComponent <ImportWalletDialog
visible={walletDialog} visible={importWalletDialog}
hideDialog={hideWalletDialog} hideDialog={() => setImportWalletDialog(false)}
importWalletHandler={importWalletHandler}
/>
<MnemonicDialog
visible={mnemonicDialog}
hideDialog={hideMnemonicDialog}
contentText={phrase} contentText={phrase}
/> />
<ConfirmDialog <ConfirmDialog
@ -171,6 +205,16 @@ const HomeScreen = () => {
onConfirm={confirmResetWallet} onConfirm={confirmResetWallet}
/> />
</Container> </Container>
<Portal>
<Snackbar
open={toastVisible}
autoHideDuration={3000}
message={invalidMnemonicError}
onClose={() => setToastVisible(false)}
anchorOrigin={{ horizontal: "center", vertical: "bottom" }}
ContentProps={{ style: { backgroundColor: "red", color: "white" } }}
/>
</Portal>
</View> </View>
); );
}; };

View File

@ -4,7 +4,7 @@ const styles = StyleSheet.create({
createWalletContainer: { createWalletContainer: {
marginTop: 20, marginTop: 20,
alignSelf: 'center', alignSelf: 'center',
marginBottom: 40 marginBottom: 30,
}, },
signLink: { signLink: {
alignItems: 'flex-end', alignItems: 'flex-end',

View File

@ -27,12 +27,23 @@ import { COSMOS, EIP155 } from './constants';
const createWallet = async ( const createWallet = async (
networksData: NetworksDataState[], networksData: NetworksDataState[],
recoveryPhrase?: string,
): Promise<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); const hdNode = HDNode.fromMnemonic(mnemonic);
await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic);
await createWalletFromMnemonic(networksData, hdNode, mnemonic);
return mnemonic;
};
const createWalletFromMnemonic = async (
networksData: NetworksDataState[],
hdNode: HDNode,
mnemonic: string
): Promise<void> => {
for (const network of networksData) { for (const network of networksData) {
const hdPath = `m/44'/${network.coinType}'/0'/0/0`; const hdPath = `m/44'/${network.coinType}'/0'/0/0`;
const node = hdNode.derivePath(hdPath); const node = hdNode.derivePath(hdPath);
@ -73,8 +84,6 @@ const createWallet = async (
), ),
]); ]);
} }
return mnemonic;
}; };
const addAccount = async ( const addAccount = async (

View File

@ -46,3 +46,5 @@ export const EMPTY_FIELD_ERROR = 'Field cannot be empty';
export const INVALID_URL_ERROR = 'Invalid URL'; export const INVALID_URL_ERROR = 'Invalid URL';
export const IS_NUMBER_REGEX = /^\d+$/; export const IS_NUMBER_REGEX = /^\d+$/;
export const IS_IMPORT_WALLET_ENABLED = false;

View File

@ -7,7 +7,7 @@ import { getSdkError } from '@walletconnect/utils';
import { import {
SigningStargateClient, SigningStargateClient,
StdFee, StdFee,
MsgSendEncodeObject, MsgSendEncodeObject
} from '@cosmjs/stargate'; } from '@cosmjs/stargate';
import { EncodeObject } from '@cosmjs/proto-signing'; import { EncodeObject } from '@cosmjs/proto-signing';
import { LaconicClient } from '@cerc-io/registry-sdk'; import { LaconicClient } from '@cerc-io/registry-sdk';