Setup react native paper (#1)

* Basic setup for react native web

* Comment unsupported react-native code

* Add interface for keychain methods

* Fix storeNetworkData method

* Use mui dialog component

* Modify key store file name

* Fix add network and edit network screens

* Fix sign message screen

* Use light theme

---------

Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
This commit is contained in:
Isha Venikar
2024-07-25 13:00:03 +05:30
committed by GitHub
co-authored by nabarun
parent 5b99becee0
commit 640155aa4a
64 changed files with 20128 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
+9
View File
@@ -0,0 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
+328
View File
@@ -0,0 +1,328 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Button, Snackbar, Text } from 'react-native-paper';
// import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
// import { TxBody, AuthInfo } from 'cosmjs-types/cosmos/tx/v1beta1/tx';
// import { SignClientTypes } from '@walletconnect/types';
import { useNavigation } from '@react-navigation/native';
import {
NativeStackNavigationProp,
createNativeStackNavigator,
} from '@react-navigation/native-stack';
// import { getSdkError } from '@walletconnect/utils';
// import { Web3WalletTypes } from '@walletconnect/web3wallet';
// import { formatJsonRpcResult } from '@json-rpc-tools/utils';
// import PairingModal from './components/PairingModal';
// import { useWalletConnect } from './context/WalletConnectContext';
import { useAccounts } from './context/AccountsContext';
import InvalidPath from './screens/InvalidPath';
import SignMessage from './screens/SignMessage';
import HomeScreen from './screens/HomeScreen';
import SignRequest from './screens/SignRequest';
import AddSession from './screens/AddSession';
// import WalletConnect from './screens/WalletConnect';
import ApproveTransaction from './screens/ApproveTransaction';
import { StackParamsList } from './types';
// import { web3wallet } from './utils/wallet-connect/WalletConnectUtils';
// import { EIP155_SIGNING_METHODS } from './utils/wallet-connect/EIP155Data';
// import { getSignParamsMessage } from './utils/wallet-connect/helpers';
import ApproveTransfer from './screens/ApproveTransfer';
import AddNetwork from './screens/AddNetwork';
import EditNetwork from './screens/EditNetwork';
// import { COSMOS, EIP155 } from './utils/constants';
import { useNetworks } from './context/NetworksContext';
// import { NETWORK_METHODS } from './utils/wallet-connect/common-data';
// import { COSMOS_METHODS } from './utils/wallet-connect/COSMOSData';
const Stack = createNativeStackNavigator<StackParamsList>();
const App = (): React.JSX.Element => {
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
// const { setActiveSessions } = useWalletConnect();
const { accounts, setCurrentIndex } = useAccounts();
const { networksData, selectedNetwork, setSelectedNetwork } = useNetworks();
const [modalVisible, setModalVisible] = useState(false);
const [toastVisible, setToastVisible] = useState(false);
// const [currentProposal, setCurrentProposal] = useState<
// SignClientTypes.EventArguments['session_proposal'] | undefined
// >();
// const onSessionProposal = useCallback(
// async (proposal: SignClientTypes.EventArguments['session_proposal']) => {
// if (!accounts.length || !accounts.length) {
// const { id } = proposal;
// await web3wallet!.rejectSession({
// id,
// reason: getSdkError('UNSUPPORTED_ACCOUNTS'),
// });
// return;
// }
// setModalVisible(true);
// setCurrentProposal(proposal);
// },
// [accounts],
// );
// const onSessionRequest = useCallback(
// async (requestEvent: Web3WalletTypes.SessionRequest) => {
// const { topic, params, id } = requestEvent;
// const { request } = params;
// const requestSessionData =
// web3wallet!.engine.signClient.session.get(topic);
// switch (request.method) {
// case NETWORK_METHODS.GET_NETWORKS:
// const currentNetworkId = networksData.find(
// networkData => networkData.networkId === selectedNetwork!.networkId,
// )?.networkId;
// const networkNamesData = networksData.map(networkData => {
// return {
// id: networkData.networkId,
// name: networkData.networkName,
// };
// });
// const formattedResponse = formatJsonRpcResult(id, {
// currentNetworkId,
// networkNamesData,
// });
// await web3wallet!.respondSessionRequest({
// topic,
// response: formattedResponse,
// });
// break;
// case NETWORK_METHODS.CHANGE_NETWORK:
// const networkNameData = request.params[0];
// const network = networksData.find(
// networkData => networkData.networkId === networkNameData.id,
// );
// setCurrentIndex(0);
// setSelectedNetwork(network);
// const response = formatJsonRpcResult(id, {
// response: 'true',
// });
// await web3wallet!.respondSessionRequest({
// topic,
// response: response,
// });
// break;
// case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
// navigation.navigate('ApproveTransfer', {
// transaction: request.params[0],
// requestEvent,
// requestSessionData,
// });
// break;
// case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
// navigation.navigate('SignRequest', {
// namespace: EIP155,
// address: request.params[1],
// message: getSignParamsMessage(request.params),
// requestEvent,
// requestSessionData,
// });
// break;
// case COSMOS_METHODS.COSMOS_SIGN_DIRECT:
// const message = {
// txbody: TxBody.toJSON(
// TxBody.decode(
// Uint8Array.from(
// Buffer.from(request.params.signDoc.bodyBytes, 'hex'),
// ),
// ),
// ),
// authInfo: AuthInfo.toJSON(
// AuthInfo.decode(
// Uint8Array.from(
// Buffer.from(request.params.signDoc.authInfoBytes, 'hex'),
// ),
// ),
// ),
// };
// navigation.navigate('SignRequest', {
// namespace: COSMOS,
// address: request.params.signerAddress,
// message: JSON.stringify(message, undefined, 2),
// requestEvent,
// requestSessionData,
// });
// break;
// case COSMOS_METHODS.COSMOS_SIGN_AMINO:
// navigation.navigate('SignRequest', {
// namespace: COSMOS,
// address: request.params.signerAddress,
// message: request.params.signDoc.memo,
// requestEvent,
// requestSessionData,
// });
// break;
// case COSMOS_METHODS.COSMOS_SEND_TOKENS:
// navigation.navigate('ApproveTransfer', {
// transaction: request.params[0],
// requestEvent,
// requestSessionData,
// });
// break;
// case COSMOS_METHODS.COSMOS_SEND_TRANSACTION:
// const { transactionMessage, signer } = request.params;
// navigation.navigate('ApproveTransaction', {
// transactionMessage,
// signer,
// requestEvent,
// requestSessionData,
// });
// break;
// default:
// throw new Error('Invalid method');
// }
// },
// [
// navigation,
// networksData,
// setSelectedNetwork,
// setCurrentIndex,
// selectedNetwork,
// ],
// );
// const onSessionDelete = useCallback(() => {
// const sessions = web3wallet!.getActiveSessions();
// setActiveSessions(sessions);
// }, [setActiveSessions]);
// useEffect(() => {
// web3wallet?.on('session_proposal', onSessionProposal);
// web3wallet?.on('session_request', onSessionRequest);
// web3wallet?.on('session_delete', onSessionDelete);
// return () => {
// web3wallet?.off('session_proposal', onSessionProposal);
// web3wallet?.off('session_request', onSessionRequest);
// web3wallet?.off('session_delete', onSessionDelete);
// };
// });
return (
<>
<Stack.Navigator>
<Stack.Screen
name="Laconic"
component={HomeScreen}
options={{
// eslint-disable-next-line react/no-unstable-nested-components
headerTitle: () => <Text variant="titleLarge">Laconic Wallet</Text>,
headerBackVisible: false,
}}
/>
<Stack.Screen
name="SignMessage"
component={SignMessage}
options={{
// eslint-disable-next-line react/no-unstable-nested-components
headerTitle: () => <Text variant="titleLarge">Sign Message</Text>,
}}
/>
{/* <Stack.Screen
name="SignRequest"
component={SignRequest}
options={{
// eslint-disable-next-line react/no-unstable-nested-components
headerTitle: () => <Text variant="titleLarge">Sign Request</Text>,
}}
/> */}
<Stack.Screen
name="InvalidPath"
component={InvalidPath}
options={{
// eslint-disable-next-line react/no-unstable-nested-components
headerTitle: () => <Text variant="titleLarge">Bad Request</Text>,
headerBackVisible: false,
}}
/>
{/* <Stack.Screen
name="WalletConnect"
component={WalletConnect}
options={{
// eslint-disable-next-line react/no-unstable-nested-components
headerTitle: () => <Text variant="titleLarge">WalletConnect</Text>,
// eslint-disable-next-line react/no-unstable-nested-components
headerRight: () => (
<Button
onPress={() => {
navigation.navigate('AddSession');
}}>
{<Icon name={'qrcode-scan'} size={20} />}
</Button>
),
}}
/> */}
<Stack.Screen
name="AddSession"
component={AddSession}
options={{
title: 'New session',
}}
/>
{/* <Stack.Screen
name="ApproveTransfer"
component={ApproveTransfer}
options={{
title: 'Approve transfer',
}}
/> */}
<Stack.Screen
name="AddNetwork"
component={AddNetwork}
options={{
title: 'Add Network',
}}
/>
<Stack.Screen
name="EditNetwork"
component={EditNetwork}
options={{
title: 'Edit Network',
}}
/>
{/* <Stack.Screen
name="ApproveTransaction"
component={ApproveTransaction}
options={{
title: 'Approve Transaction',
}}
/> */}
</Stack.Navigator>
{/* <PairingModal
visible={modalVisible}
setModalVisible={setModalVisible}
currentProposal={currentProposal}
setCurrentProposal={setCurrentProposal}
setToastVisible={setToastVisible}
/> */}
<Snackbar
visible={toastVisible}
onDismiss={() => setToastVisible(false)}
duration={3000}>
Session approved
</Snackbar>
</>
);
};
export default App;
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
import React from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-paper';
import { Account } from '../types';
import styles from '../styles/stylesheet';
interface AccountDetailsProps {
account: Account | undefined;
}
const AccountDetails: React.FC<AccountDetailsProps> = ({ account }) => {
return (
<View style={styles.accountContainer}>
<Text variant="bodyLarge" selectable={true}>
<Text style={styles.highlight}>Address: </Text>
{account?.address}
</Text>
<Text variant="bodyLarge" selectable={true}>
<Text style={styles.highlight}>Public Key: </Text>
{account?.pubKey}
</Text>
<Text variant="bodyLarge">
<Text style={styles.highlight}>HD Path: </Text>
{account?.hdPath}
</Text>
</View>
);
};
export default AccountDetails;
+229
View File
@@ -0,0 +1,229 @@
import React, { useEffect, useState } from 'react';
import { ScrollView, TouchableOpacity, View } from 'react-native';
import { Button, List, Text, useTheme } from 'react-native-paper';
// import { setInternetCredentials } from 'react-native-keychain';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { StackParamsList, Account } from '../types';
import { addAccount } from '../utils/accounts';
import styles from '../styles/stylesheet';
import HDPathDialog from './HDPathDialog';
import AccountDetails from './AccountDetails';
import { useAccounts } from '../context/AccountsContext';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { useNetworks } from '../context/NetworksContext';
import ConfirmDialog from './ConfirmDialog';
import { getNamespaces } from '../utils/wallet-connect/helpers';
import ShowPKDialog from './ShowPKDialog';
const Accounts = () => {
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
const { accounts, setAccounts, setCurrentIndex, currentIndex } =
useAccounts();
const { networksData, selectedNetwork, setNetworksData, setSelectedNetwork } =
useNetworks();
const [expanded, setExpanded] = useState(false);
const [isAccountCreating, setIsAccountCreating] = useState(false);
const [hdDialog, setHdDialog] = useState(false);
const [pathCode, setPathCode] = useState('');
const [deleteNetworkDialog, setDeleteNetworkDialog] =
useState<boolean>(false);
const theme = useTheme();
const handlePress = () => setExpanded(!expanded);
const hideDeleteNetworkDialog = () => setDeleteNetworkDialog(false);
const updateAccounts = (account: Account) => {
setAccounts([...accounts, account]);
};
useEffect(() => {
const updateSessions = async () => {
const sessions = (web3wallet && web3wallet.getActiveSessions()) || {};
// Iterate through each session
for (const topic in sessions) {
const session = sessions[topic];
const { optionalNamespaces, requiredNamespaces } = session;
const updatedNamespaces = await getNamespaces(
optionalNamespaces,
requiredNamespaces,
networksData,
selectedNetwork!,
accounts,
currentIndex,
);
if (!updatedNamespaces) {
return;
}
await web3wallet!.updateSession({
topic,
namespaces: updatedNamespaces,
});
}
};
// Call the updateSessions function when the 'accounts' dependency changes
updateSessions();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accounts]);
const addAccountHandler = async () => {
setIsAccountCreating(true);
const newAccount = await addAccount(selectedNetwork!);
setIsAccountCreating(false);
if (newAccount) {
updateAccounts(newAccount);
setCurrentIndex(newAccount.index);
}
};
const renderAccountItems = () =>
accounts.map(account => (
<List.Item
key={account.index}
title={`Account ${account.index + 1}`}
onPress={() => {
setCurrentIndex(account.index);
setExpanded(false);
}}
/>
));
const handleRemove = async () => {
const updatedNetworks = networksData.filter(
networkData => selectedNetwork!.networkId !== networkData.networkId,
);
// await setInternetCredentials(
// 'networks',
// '_',
// JSON.stringify(updatedNetworks),
// );
setSelectedNetwork(updatedNetworks[0]);
setCurrentIndex(0);
setDeleteNetworkDialog(false);
setNetworksData(updatedNetworks);
};
return (
<ScrollView>
<View>
<HDPathDialog
visible={hdDialog}
hideDialog={() => setHdDialog(false)}
updateAccounts={updateAccounts}
pathCode={pathCode}
/>
<List.Accordion
title={`Account ${currentIndex + 1}`}
expanded={expanded}
onPress={handlePress}>
{renderAccountItems()}
</List.Accordion>
<View style={styles.addAccountButton}>
<Button
mode="contained"
onPress={addAccountHandler}
loading={isAccountCreating}
disabled={isAccountCreating}>
{isAccountCreating ? 'Adding' : 'Add Account'}
</Button>
</View>
<View style={styles.addAccountButton}>
<Button
mode="contained"
onPress={() => {
setHdDialog(true);
setPathCode(`m/44'/${selectedNetwork!.coinType}'/`);
}}>
Add Account from HD path
</Button>
</View>
<AccountDetails account={accounts[currentIndex]} />
<View style={styles.signLink}>
<TouchableOpacity
onPress={() => {
navigation.navigate('SignMessage', {
selectedNamespace: selectedNetwork!.namespace,
selectedChainId: selectedNetwork!.chainId,
accountInfo: accounts[currentIndex],
});
}}>
<Text
variant="titleSmall"
style={[styles.hyperlink, { color: theme.colors.primary }]}>
Sign Message
</Text>
</TouchableOpacity>
</View>
<View style={styles.signLink}>
<TouchableOpacity
onPress={() => {
navigation.navigate('AddNetwork');
}}>
<Text
variant="titleSmall"
style={[styles.hyperlink, { color: theme.colors.primary }]}>
Add Network
</Text>
</TouchableOpacity>
</View>
<View style={styles.signLink}>
<TouchableOpacity
onPress={() => {
navigation.navigate('EditNetwork', {
selectedNetwork: selectedNetwork!,
});
}}>
<Text
variant="titleSmall"
style={[styles.hyperlink, { color: theme.colors.primary }]}>
Edit Network
</Text>
</TouchableOpacity>
</View>
{!selectedNetwork!.isDefault && (
<View style={styles.signLink}>
<TouchableOpacity
onPress={() => {
setDeleteNetworkDialog(true);
}}>
<Text
variant="titleSmall"
style={[styles.hyperlink, { color: theme.colors.primary }]}>
Delete Network
</Text>
</TouchableOpacity>
</View>
)}
<ConfirmDialog
title="Delete Network"
visible={deleteNetworkDialog}
hideDialog={hideDeleteNetworkDialog}
onConfirm={handleRemove}
/>
<ShowPKDialog />
</View>
</ScrollView>
);
};
export default Accounts;
+27
View File
@@ -0,0 +1,27 @@
import React from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } from '@mui/material';
import { ResetDialogProps } from '../types';
const ConfirmDialog = ({
title,
visible,
hideDialog,
onConfirm,
}: ResetDialogProps) => {
return (
<Dialog open={visible} onClose={hideDialog}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<Typography variant="body1">Are you sure?</Typography>
</DialogContent>
<DialogActions>
<Button color="error" onClick={onConfirm}>
Yes
</Button>
<Button onClick={hideDialog}>No</Button>
</DialogActions>
</Dialog>
);
};
export default ConfirmDialog;
+27
View File
@@ -0,0 +1,27 @@
import { View } from 'react-native';
import React from 'react';
import { Button } from 'react-native-paper';
import { CreateWalletProps } from '../types';
import styles from '../styles/stylesheet';
const CreateWallet = ({
isWalletCreating,
createWalletHandler,
}: CreateWalletProps) => {
return (
<View>
<View style={styles.createWalletContainer}>
<Button
mode="contained"
loading={isWalletCreating}
disabled={isWalletCreating}
onPress={createWalletHandler}>
{isWalletCreating ? 'Creating' : 'Create Wallet'}
</Button>
</View>
</View>
);
};
export default CreateWallet;
+17
View File
@@ -0,0 +1,17 @@
import React from 'react';
import { View, Text } from 'react-native';
import styles from '../styles/stylesheet';
const DataBox = ({ label, data }: { label: string; data: string }) => {
return (
<View style={styles.dataBoxContainer}>
<Text style={styles.dataBoxLabel}>{label}</Text>
<View style={styles.dataBox}>
<Text style={styles.dataBoxData}>{data}</Text>
</View>
</View>
);
};
export default DataBox;
+31
View File
@@ -0,0 +1,31 @@
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 DialogComponent = ({ visible, hideDialog, contentText }: CustomDialogProps) => {
const words = contentText.split(' ');
return (
<Dialog open={visible} onClose={hideDialog}>
<DialogTitle>Mnemonic</DialogTitle>
<DialogContent>
<Typography variant="h6" component="div" style={{ color: 'rgba(0, 0, 0, 0.87)' }}>
Your mnemonic provides full access to your wallet and funds. Make sure to note it down.
</Typography>
<Typography variant="h6" component="div" style={{ ...styles.dialogWarning }}>
Do not share your mnemonic with anyone
</Typography>
<GridView words={words} />
</DialogContent>
<DialogActions>
<Button onClick={hideDialog}>Done</Button>
</DialogActions>
</Dialog>
);
};
export { DialogComponent };
+21
View File
@@ -0,0 +1,21 @@
import React from 'react';
import { View } from 'react-native';
import { Text } from 'react-native-paper';
import styles from '../styles/stylesheet';
import { GridViewProps } from '../types';
const GridView = ({ words }: GridViewProps) => {
return (
<View style={styles.gridContainer}>
{words.map((word, index) => (
<View key={index} style={styles.gridItem}>
<Text>{index + 1}. </Text>
<Text variant="bodySmall">{word}</Text>
</View>
))}
</View>
);
};
export default GridView;
+100
View File
@@ -0,0 +1,100 @@
import React, { useState } from 'react';
import { ScrollView, View, Text } from 'react-native';
import { Button, TextInput } from 'react-native-paper';
import { addAccountFromHDPath } from '../utils/accounts';
import { Account, NetworksDataState, PathState } from '../types';
import styles from '../styles/stylesheet';
import { useAccounts } from '../context/AccountsContext';
const HDPath = ({
pathCode,
updateAccounts,
hideDialog,
selectedNetwork,
}: {
pathCode: string;
updateAccounts: (account: Account) => void;
hideDialog: () => void;
selectedNetwork: NetworksDataState;
}) => {
const { setCurrentIndex } = useAccounts();
const [isAccountCreating, setIsAccountCreating] = useState(false);
const [path, setPath] = useState<PathState>({
firstNumber: '',
secondNumber: '',
thirdNumber: '',
});
const handleChange = (key: keyof PathState, value: string) => {
if (key === 'secondNumber' && value !== '' && !['0', '1'].includes(value)) {
return;
}
setPath({
...path,
[key]: value.replace(/[^0-9]/g, ''),
});
};
const createFromHDPathHandler = async () => {
setIsAccountCreating(true);
const hdPath =
pathCode +
`${path.firstNumber}'/${path.secondNumber}/${path.thirdNumber}`;
try {
const newAccount = await addAccountFromHDPath(hdPath, selectedNetwork);
if (newAccount) {
updateAccounts(newAccount);
setCurrentIndex(newAccount.index);
hideDialog();
}
} catch (error) {
console.error('Error creating account:', error);
} finally {
setIsAccountCreating(false);
}
};
return (
<ScrollView style={styles.HDcontainer}>
<View style={styles.HDrowContainer}>
<Text style={styles.HDtext}>{pathCode}</Text>
<TextInput
keyboardType="numeric"
mode="outlined"
onChangeText={text => handleChange('firstNumber', text)}
value={path.firstNumber}
style={styles.HDtextInput}
/>
<Text style={styles.HDtext}>'/</Text>
<TextInput
keyboardType="numeric"
mode="outlined"
onChangeText={text => handleChange('secondNumber', text)}
value={path.secondNumber}
style={styles.HDtextInput}
/>
<Text style={styles.HDtext}>/</Text>
<TextInput
keyboardType="numeric"
mode="outlined"
onChangeText={text => handleChange('thirdNumber', text)}
value={path.thirdNumber}
style={styles.HDtextInput}
/>
</View>
<View style={styles.HDbuttonContainer}>
<Button
mode="contained"
onPress={createFromHDPathHandler}
loading={isAccountCreating}
disabled={isAccountCreating}>
{isAccountCreating ? 'Adding' : 'Add Account'}
</Button>
</View>
</ScrollView>
);
};
export default HDPath;
+31
View File
@@ -0,0 +1,31 @@
import React from 'react';
import { Dialog, DialogTitle, DialogContent } from '@mui/material';
import { HDPathDialogProps } from '../types';
import HDPath from './HDPath';
import { useNetworks } from '../context/NetworksContext';
const HDPathDialog = ({
visible,
hideDialog,
updateAccounts,
pathCode,
}: HDPathDialogProps) => {
const { selectedNetwork } = useNetworks();
return (
<Dialog open={visible} onClose={hideDialog}>
<DialogTitle>Add account from HD path</DialogTitle>
<DialogContent>
<HDPath
selectedNetwork={selectedNetwork!}
pathCode={pathCode}
updateAccounts={updateAccounts}
hideDialog={hideDialog}
/>
</DialogContent>
</Dialog>
);
};
export default HDPathDialog;
+38
View File
@@ -0,0 +1,38 @@
import React, { useState } from 'react';
import { View } from 'react-native';
import { List } from 'react-native-paper';
import { NetworkDropdownProps, NetworksDataState } from '../types';
import styles from '../styles/stylesheet';
import { useNetworks } from '../context/NetworksContext';
const NetworkDropdown = ({ updateNetwork }: NetworkDropdownProps) => {
const { networksData, selectedNetwork, setSelectedNetwork } = useNetworks();
const [expanded, setExpanded] = useState<boolean>(false);
const handleNetworkPress = (networkData: NetworksDataState) => {
updateNetwork(networkData);
setSelectedNetwork(networkData);
setExpanded(false);
};
return (
<View style={styles.networkDropdown}>
<List.Accordion
title={selectedNetwork!.networkName}
expanded={expanded}
onPress={() => setExpanded(!expanded)}>
{networksData.map(networkData => (
<List.Item
key={networkData.networkId}
title={networkData.networkName}
onPress={() => handleNetworkPress(networkData)}
/>
))}
</List.Accordion>
</View>
);
};
export { NetworkDropdown };
+315
View File
@@ -0,0 +1,315 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Image, View, Modal, ScrollView } from 'react-native';
import { Button, Text } from 'react-native-paper';
import { SvgUri } from 'react-native-svg';
import mergeWith from 'lodash/mergeWith';
// import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils';
import { PairingModalProps } from '../types';
import styles from '../styles/stylesheet';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { useAccounts } from '../context/AccountsContext';
import { useWalletConnect } from '../context/WalletConnectContext';
import { useNetworks } from '../context/NetworksContext';
import { getNamespaces } from '../utils/wallet-connect/helpers';
const PairingModal = ({
visible,
currentProposal,
setCurrentProposal,
setModalVisible,
setToastVisible,
}: PairingModalProps) => {
const { accounts, currentIndex } = useAccounts();
const { selectedNetwork, networksData } = useNetworks();
const [isLoading, setIsLoading] = useState(false);
const [chainError, setChainError] = useState('');
// const dappName = currentProposal?.params?.proposer?.metadata.name;
// const url = currentProposal?.params?.proposer?.metadata.url;
// const icon = currentProposal?.params.proposer?.metadata.icons[0];
const dappName = undefined;
const url = undefined;
const icon = '.svg';
const [walletConnectData, setWalletConnectData] = useState<{
walletConnectMethods: string[];
walletConnectEvents: string[];
walletConnectChains: string[];
}>({
walletConnectMethods: [],
walletConnectEvents: [],
walletConnectChains: [],
});
const [supportedNamespaces, setSupportedNamespaces] = useState<
Record<
string,
{
chains: string[];
methods: string[];
events: string[];
accounts: string[];
}
>
>();
useEffect(() => {
if (!currentProposal) {
return;
}
const { params } = currentProposal;
const { requiredNamespaces, optionalNamespaces } = params;
setWalletConnectData({
walletConnectMethods: [],
walletConnectEvents: [],
walletConnectChains: [],
});
const combinedNamespaces = mergeWith(
requiredNamespaces,
optionalNamespaces,
(obj, src) =>
Array.isArray(obj) && Array.isArray(src) ? [...src, ...obj] : undefined,
);
Object.keys(combinedNamespaces).forEach(key => {
const { methods, events, chains } = combinedNamespaces[key];
setWalletConnectData(prevData => {
return {
walletConnectMethods: [...prevData.walletConnectMethods, ...methods],
walletConnectEvents: [...prevData.walletConnectEvents, ...events],
walletConnectChains: chains
? [...prevData.walletConnectChains, ...chains]
: [...prevData.walletConnectChains],
};
});
});
}, [currentProposal]);
const { setActiveSessions } = useWalletConnect();
useEffect(() => {
const getSupportedNamespaces = async () => {
if (!currentProposal) {
return;
}
// const { optionalNamespaces, requiredNamespaces } = currentProposal.params;
const { optionalNamespaces, requiredNamespaces } = currentProposal;
try {
const nameSpaces = await getNamespaces(
optionalNamespaces,
requiredNamespaces,
networksData,
selectedNetwork!,
accounts,
currentIndex,
);
setSupportedNamespaces(nameSpaces);
} catch (err) {
setChainError((err as Error).message);
// const { id } = currentProposal;
// await web3wallet!.rejectSession({
// id,
// reason: getSdkError('UNSUPPORTED_CHAINS'),
// });
setCurrentProposal(undefined);
setWalletConnectData({
walletConnectMethods: [],
walletConnectEvents: [],
walletConnectChains: [],
});
}
};
getSupportedNamespaces();
}, [
currentProposal,
networksData,
selectedNetwork,
accounts,
currentIndex,
setCurrentProposal,
setModalVisible,
]);
const namespaces = useMemo(() => {
return (
currentProposal &&
supportedNamespaces
// &&
// buildApprovedNamespaces({
// proposal: currentProposal.params,
// supportedNamespaces,
// })
);
}, [currentProposal, supportedNamespaces]);
const handleAccept = async () => {
try {
if (currentProposal && namespaces) {
setIsLoading(true);
const { id } = currentProposal;
await web3wallet!.approveSession({
id,
namespaces,
});
const sessions = web3wallet!.getActiveSessions();
setIsLoading(false);
setActiveSessions(sessions);
setModalVisible(false);
setToastVisible(true);
setCurrentProposal(undefined);
setSupportedNamespaces(undefined);
setWalletConnectData({
walletConnectMethods: [],
walletConnectEvents: [],
walletConnectChains: [],
});
}
} catch (error) {
console.error('Error in approve session:', error);
throw error;
}
};
const handleClose = () => {
setChainError('');
setModalVisible(false);
};
const handleReject = async () => {
if (currentProposal) {
// const { id } = currentProposal;
// await web3wallet!.rejectSession({
// id,
// reason: getSdkError('USER_REJECTED_METHODS'),
// });
setModalVisible(false);
setCurrentProposal(undefined);
setWalletConnectData({
walletConnectMethods: [],
walletConnectEvents: [],
walletConnectChains: [],
});
}
};
return (
<View>
{chainError !== '' ? (
<Modal visible={visible} animationType="slide" transparent>
<View style={styles.modalContentContainer}>
<View style={styles.container}>
{icon && (
<>
{icon.endsWith('.svg') ? (
<View style={styles.dappLogo}>
<SvgUri height="50" width="50" uri={icon} />
</View>
) : (
<Image style={styles.dappLogo} source={{ uri: icon }} />
)}
</>
)}
<Text variant="titleMedium">{dappName}</Text>
<Text variant="bodyMedium">{url}</Text>
<Text variant="titleMedium">{chainError}</Text>
</View>
<View style={styles.flexRow}>
<Button mode="outlined" onPress={handleClose}>
Close
</Button>
</View>
</View>
</Modal>
) : (
<Modal visible={visible} animationType="slide" transparent>
<View style={styles.modalOuterContainer}>
<View style={styles.modalContentContainer}>
<ScrollView showsVerticalScrollIndicator={true}>
<View style={styles.container}>
{icon && (
<>
{icon.endsWith('.svg') ? (
<View style={styles.dappLogo}>
<SvgUri height="50" width="50" uri={icon} />
</View>
) : (
<Image style={styles.dappLogo} source={{ uri: icon }} />
)}
</>
)}
<Text variant="titleMedium">{dappName}</Text>
<Text variant="bodyMedium">{url}</Text>
<View style={styles.marginVertical8} />
<Text variant="titleMedium">Connect to this site?</Text>
{walletConnectData.walletConnectMethods.length > 0 && (
<View>
<Text variant="titleMedium">Chains:</Text>
{walletConnectData.walletConnectChains.map(chain => (
<Text style={styles.centerText} key={chain}>
{chain}
</Text>
))}
</View>
)}
{walletConnectData.walletConnectMethods.length > 0 && (
<View style={styles.marginVertical8}>
<Text variant="titleMedium">Methods Requested:</Text>
{walletConnectData.walletConnectMethods.map(method => (
<Text style={styles.centerText} key={method}>
{method}
</Text>
))}
</View>
)}
{walletConnectData.walletConnectEvents.length > 0 && (
<View style={styles.marginVertical8}>
<Text variant="titleMedium">Events Requested:</Text>
{walletConnectData.walletConnectEvents.map(event => (
<Text style={styles.centerText} key={event}>
{event}
</Text>
))}
</View>
)}
</View>
</ScrollView>
<View style={styles.flexRow}>
<Button
mode="contained"
onPress={handleAccept}
loading={isLoading}
disabled={isLoading}>
{isLoading ? 'Connecting' : 'Yes'}
</Button>
<View style={styles.space} />
<Button mode="outlined" onPress={handleReject}>
No
</Button>
</View>
</View>
</View>
</Modal>
)}
</View>
);
};
export default PairingModal;
+43
View File
@@ -0,0 +1,43 @@
import React, { useState } from 'react';
import { View } from 'react-native';
import { Text, List } from 'react-native-paper';
import styles from '../styles/stylesheet';
import { COSMOS, EIP155 } from '../utils/constants';
const SelectNetworkType = ({
updateNetworkType,
}: {
updateNetworkType: (networkType: string) => void;
}) => {
const [expanded, setExpanded] = useState<boolean>(false);
const [selectedNetwork, setSelectedNetwork] = useState<string>('ETH');
const networks = ['ETH', 'COSMOS'];
const handleNetworkPress = (network: string) => {
setSelectedNetwork(network);
updateNetworkType(network === 'ETH' ? EIP155 : COSMOS);
setExpanded(false);
};
return (
<View style={styles.networkDropdown}>
<Text style={styles.selectNetworkText}>Select Network Type</Text>
<List.Accordion
title={selectedNetwork}
expanded={expanded}
onPress={() => setExpanded(!expanded)}>
{networks.map(network => (
<List.Item
key={network}
title={network}
onPress={() => handleNetworkPress(network)}
/>
))}
</List.Accordion>
</View>
);
};
export { SelectNetworkType };
+101
View File
@@ -0,0 +1,101 @@
import React, { useState } from 'react';
import { TouchableOpacity, View } from 'react-native';
import { Button, Typography } from '@mui/material';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import styles from '../styles/stylesheet';
import { getPathKey } from '../utils/misc';
import { useNetworks } from '../context/NetworksContext';
import { useAccounts } from '../context/AccountsContext';
import { Text, useTheme } from 'react-native-paper';
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>
<Dialog open={showPKDialog} onClose={hideShowPKDialog}>
<DialogTitle>
{!privateKey ? (
<Typography>Show Private Key?</Typography>
) : (
<Typography>Private Key</Typography>
)}
</DialogTitle>
<DialogContent>
{privateKey && (
<View style={[styles.dataBox, styles.dataBoxContainer]}>
<Typography
component="pre"
variant="body1"
style={styles.dataBoxData}
>
{privateKey}
</Typography>
</View>
)}
<View>
<Typography variant="body1" style={styles.dialogWarning}>
<Typography component="span">
Warning:
</Typography>
Never disclose this key. Anyone with your private keys can
steal any assets held in your account.
</Typography>
</View>
</DialogContent>
<DialogActions>
{!privateKey ? (
<>
<Button onClick={handleShowPrivateKey} color="error">
Yes
</Button>
<Button onClick={hideShowPKDialog}>No</Button>
</>
) : (
<Button onClick={hideShowPKDialog}>Ok</Button>
)}
</DialogActions>
</Dialog>
</View>
</>
);
};
export default ShowPKDialog;
+28
View File
@@ -0,0 +1,28 @@
import React from 'react';
import { Button, Dialog, Portal, Text } from 'react-native-paper';
const TxErrorDialog = ({
error,
visible,
hideDialog,
}: {
error: string;
visible: boolean;
hideDialog: () => void;
}) => {
return (
<Portal>
<Dialog visible={visible} onDismiss={hideDialog}>
<Dialog.Title>Transaction Error</Dialog.Title>
<Dialog.Content>
<Text variant="bodyMedium">{error}</Text>
</Dialog.Content>
<Dialog.Actions>
<Button onPress={hideDialog}>OK</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);
};
export default TxErrorDialog;
+39
View File
@@ -0,0 +1,39 @@
import React, { createContext, useContext, useState } from 'react';
import { Account } from '../types';
const AccountsContext = createContext<{
accounts: Account[];
setAccounts: (account: Account[]) => void;
currentIndex: number;
setCurrentIndex: (index: number) => void;
}>({
accounts: [],
setAccounts: () => {},
currentIndex: 0,
setCurrentIndex: () => {},
});
const useAccounts = () => {
const accountsContext = useContext(AccountsContext);
return accountsContext;
};
const AccountsProvider = ({ children }: { children: any }) => {
const [accounts, setAccounts] = useState<Account[]>([]);
const [currentIndex, setCurrentIndex] = useState<number>(0);
return (
<AccountsContext.Provider
value={{
accounts,
setAccounts,
currentIndex,
setCurrentIndex,
}}>
{children}
</AccountsContext.Provider>
);
};
export { useAccounts, AccountsProvider };
+76
View File
@@ -0,0 +1,76 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { NetworksDataState } from '../types';
import { retrieveNetworksData, storeNetworkData } from '../utils/accounts';
import { DEFAULT_NETWORKS, EIP155 } from '../utils/constants';
const NetworksContext = createContext<{
networksData: NetworksDataState[];
setNetworksData: React.Dispatch<React.SetStateAction<NetworksDataState[]>>;
networkType: string;
setNetworkType: (networkType: string) => void;
selectedNetwork?: NetworksDataState;
setSelectedNetwork: React.Dispatch<
React.SetStateAction<NetworksDataState | undefined>
>;
}>({
networksData: [],
setNetworksData: () => {},
networkType: '',
setNetworkType: () => {},
selectedNetwork: {} as NetworksDataState,
setSelectedNetwork: () => {},
});
const useNetworks = () => {
const networksContext = useContext(NetworksContext);
return networksContext;
};
const NetworksProvider = ({ children }: { children: any }) => {
const [networksData, setNetworksData] = useState<NetworksDataState[]>([]);
const [networkType, setNetworkType] = useState<string>(EIP155);
const [selectedNetwork, setSelectedNetwork] = useState<NetworksDataState>();
useEffect(() => {
const fetchData = async () => {
const retrievedNetworks = await retrieveNetworksData();
if (retrievedNetworks.length === 0) {
for (const defaultNetwork of DEFAULT_NETWORKS) {
await storeNetworkData(defaultNetwork);
}
}
const retrievedNewNetworks = await retrieveNetworksData();
setNetworksData(retrievedNewNetworks);
setSelectedNetwork(retrievedNewNetworks[0]);
};
if (networksData.length === 0) {
fetchData();
}
}, [networksData]);
useEffect(() => {
setSelectedNetwork(prevSelectedNetwork => {
return networksData.find(
networkData => networkData.networkId === prevSelectedNetwork?.networkId,
);
});
}, [networksData]);
return (
<NetworksContext.Provider
value={{
networksData,
setNetworksData,
networkType,
setNetworkType,
selectedNetwork,
setSelectedNetwork,
}}>
{children}
</NetworksContext.Provider>
);
};
export { useNetworks, NetworksProvider };
+43
View File
@@ -0,0 +1,43 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
// import { SessionTypes } from '@walletconnect/types';
import { WalletConnectContextProps } from '../types';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import useInitialization from '../hooks/useInitialization';
const WalletConnectContext = createContext<WalletConnectContextProps>({
activeSessions: {},
setActiveSessions: () => {},
});
const useWalletConnect = () => {
const walletConnectContext = useContext(WalletConnectContext);
return walletConnectContext;
};
const WalletConnectProvider = ({ children }: { children: React.ReactNode }) => {
useInitialization();
useEffect(() => {
const sessions = (web3wallet && web3wallet.getActiveSessions()) || {};
setActiveSessions(sessions);
}, []);
const [activeSessions, setActiveSessions] = useState<
// Record<string, SessionTypes.Struct>
Record<string, any>
>({});
return (
<WalletConnectContext.Provider
value={{
activeSessions,
setActiveSessions,
}}>
{children}
</WalletConnectContext.Provider>
);
};
export { useWalletConnect, WalletConnectProvider };
+23
View File
@@ -0,0 +1,23 @@
import { useCallback, useEffect, useState } from 'react';
import { createWeb3Wallet } from '../utils/wallet-connect/WalletConnectUtils';
export default function useInitialization() {
const [initialized, setInitialized] = useState(false);
const onInitialize = useCallback(async () => {
try {
await createWeb3Wallet();
setInitialized(true);
} catch (err: unknown) {
console.error('Error for initializing', err);
}
}, []);
useEffect(() => {
if (!initialized) {
onInitialize();
}
}, [initialized, onInitialize]);
return initialized;
}
+10
View File
@@ -0,0 +1,10 @@
import { useEffect, useRef } from 'react';
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef(value);
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
+13
View File
@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
+81
View File
@@ -0,0 +1,81 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { PaperProvider, MD3LightTheme as DefaultTheme, } from 'react-native-paper';
import { NavigationContainer } from '@react-navigation/native';
import { Platform } from 'react-native';
import './index.css';
import App from './App';
import { AccountsProvider } from './context/AccountsContext';
import { NetworksProvider } from './context/NetworksContext';
import reportWebVitals from './reportWebVitals';
import { WalletConnectProvider } from './context/WalletConnectContext';
// // Generate the required CSS
// import iconFont from 'react-native-vector-icons/Fonts/FontAwesome.ttf';
// const iconFontStyles = `@font-face {
// src: url(${iconFont});
// font-family: FontAwesome;
// }`;
// // Create a stylesheet
// const style = document.createElement('style');
// style.type = 'text/css';
// // Append the iconFontStyles to the stylesheet
// if (style.styleSheet) {
// style.styleSheet.cssText = iconFontStyles;
// } else {
// style.appendChild(document.createTextNode(iconFontStyles));
// }
// // Inject the stylesheet into the document head
// document.head.appendChild(style);
const linking = {
prefixes: ['https://wallet.laconic.com'],
config: {
screens: {
SignRequest: {
path: 'sign/:namespace/:chaindId/:address/:message',
},
},
},
};
const theme = {
...DefaultTheme,
dark: false,
};
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<PaperProvider theme={theme}>
<NetworksProvider>
<AccountsProvider>
<WalletConnectProvider>
<NavigationContainer linking={linking}>
<React.Fragment>
{Platform.OS === 'web' ? (
<style type="text/css">{`
@font-face {
font-family: 'MaterialCommunityIcons';
src: url(${require('react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf')}) format('truetype');
}
`}</style>
) : null}
<App />
</React.Fragment>
</NavigationContainer>
</WalletConnectProvider>
</AccountsProvider>
</NetworksProvider>
</PaperProvider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+1
View File
@@ -0,0 +1 @@
/// <reference types="react-scripts" />
+15
View File
@@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
+418
View File
@@ -0,0 +1,418 @@
import React, { useCallback, useEffect, useState } from 'react';
import { View } from 'react-native';
import { useForm, Controller, useWatch, FieldErrors } from 'react-hook-form';
import { TextInput, Button, HelperText } from 'react-native-paper';
import { HDNode } from 'ethers/lib/utils';
import { chains } from 'chain-registry';
import { useDebouncedCallback } from 'use-debounce';
import { z } from 'zod';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
import { zodResolver } from '@hookform/resolvers/zod';
import { StackParamsList } from '../types';
import { SelectNetworkType } from '../components/SelectNetworkType';
import { storeNetworkData } from '../utils/accounts';
import { useNetworks } from '../context/NetworksContext';
import {
COSMOS,
EIP155,
CHAINID_DEBOUNCE_DELAY,
EMPTY_FIELD_ERROR,
INVALID_URL_ERROR,
IS_NUMBER_REGEX,
} from '../utils/constants';
import { getCosmosAccounts } from '../utils/accounts';
import ETH_CHAINS from '../assets/ethereum-chains.json';
import {
getInternetCredentials,
setInternetCredentials,
} from '../utils/key-store';
const ethNetworkDataSchema = z.object({
chainId: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
rpcUrl: z.string().url({ message: INVALID_URL_ERROR }),
blockExplorerUrl: z
.string()
.url({ message: INVALID_URL_ERROR })
.or(z.literal('')),
coinType: z
.string()
.nonempty({ message: EMPTY_FIELD_ERROR })
.regex(IS_NUMBER_REGEX),
currencySymbol: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
});
const cosmosNetworkDataSchema = z.object({
chainId: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
rpcUrl: z.string().url({ message: INVALID_URL_ERROR }),
blockExplorerUrl: z
.string()
.url({ message: INVALID_URL_ERROR })
.or(z.literal('')),
coinType: z
.string()
.nonempty({ message: EMPTY_FIELD_ERROR })
.regex(IS_NUMBER_REGEX),
nativeDenom: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
addressPrefix: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
gasPrice: z
.string()
.nonempty({ message: EMPTY_FIELD_ERROR })
.regex(/^\d+(\.\d+)?$/),
});
const AddNetwork = () => {
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
const { setNetworksData } = useNetworks();
const [namespace, setNamespace] = useState<string>(EIP155);
const networksFormDataSchema =
namespace === EIP155 ? ethNetworkDataSchema : cosmosNetworkDataSchema;
const {
control,
formState: { errors, isSubmitting },
handleSubmit,
setValue,
reset,
} = useForm<z.infer<typeof networksFormDataSchema>>({
mode: 'onChange',
resolver: zodResolver(networksFormDataSchema),
});
const watchChainId = useWatch({
control,
name: 'chainId',
});
const updateNetworkType = (newNetworkType: string) => {
setNamespace(newNetworkType);
};
const fetchChainDetails = useDebouncedCallback((chainId: string) => {
if (namespace === EIP155) {
const ethChainDetails = ETH_CHAINS.find(
chain => chain.chainId === Number(chainId),
);
if (!ethChainDetails) {
return;
}
setValue('networkName', ethChainDetails.name);
setValue('rpcUrl', ethChainDetails.rpc[0]);
setValue('blockExplorerUrl', ethChainDetails.explorers?.[0].url || '');
setValue('coinType', String(ethChainDetails.slip44 ?? '60'));
setValue('currencySymbol', ethChainDetails.nativeCurrency.symbol);
return;
}
const cosmosChainDetails = chains.find(
({ chain_id }) => chain_id === chainId,
);
if (!cosmosChainDetails) {
return;
}
setValue('networkName', cosmosChainDetails.pretty_name);
setValue('rpcUrl', cosmosChainDetails.apis?.rpc?.[0]?.address || '');
setValue('blockExplorerUrl', cosmosChainDetails.explorers?.[0].url || '');
setValue('addressPrefix', cosmosChainDetails.bech32_prefix);
setValue('coinType', String(cosmosChainDetails.slip44 ?? '118'));
setValue('nativeDenom', cosmosChainDetails.fees?.fee_tokens[0].denom || '');
setValue(
'gasPrice',
String(
cosmosChainDetails.fees?.fee_tokens[0].average_gas_price ||
String(process.env.DEFAULT_GAS_PRICE),
),
);
}, CHAINID_DEBOUNCE_DELAY);
const submit = useCallback(
async (data: z.infer<typeof networksFormDataSchema>) => {
const newNetworkData = {
...data,
namespace,
isDefault: false,
};
const mnemonicServer = await getInternetCredentials('mnemonicServer');
const mnemonic = mnemonicServer;
if (!mnemonic) {
throw new Error('Mnemonic not found');
}
const hdNode = HDNode.fromMnemonic(mnemonic);
const hdPath = `m/44'/${newNetworkData.coinType}'/0'/0/0`;
const node = hdNode.derivePath(hdPath);
let address;
switch (newNetworkData.namespace) {
case EIP155:
address = node.address;
break;
case COSMOS:
address = (
await getCosmosAccounts(
mnemonic,
hdPath,
(newNetworkData as z.infer<typeof cosmosNetworkDataSchema>)
.addressPrefix,
)
).data.address;
break;
default:
throw new Error('Unsupported namespace');
}
const accountInfo = `${hdPath},${node.privateKey},${node.publicKey},${address}`;
const retrievedNetworksData = await storeNetworkData(newNetworkData);
setNetworksData(retrievedNetworksData);
await Promise.all([
setInternetCredentials(
`accounts/${newNetworkData.namespace}:${newNetworkData.chainId}/0`,
'_',
accountInfo,
),
setInternetCredentials(
`addAccountCounter/${newNetworkData.namespace}:${newNetworkData.chainId}`,
'_',
'1',
),
setInternetCredentials(
`accountIndices/${newNetworkData.namespace}:${newNetworkData.chainId}`,
'_',
'0',
),
]);
navigation.navigate('Laconic');
},
[navigation, namespace, setNetworksData],
);
useEffect(() => {
fetchChainDetails(watchChainId);
}, [watchChainId, fetchChainDetails]);
useEffect(() => {
reset();
}, [namespace, reset]);
return (
<View>
<SelectNetworkType updateNetworkType={updateNetworkType} />
<Controller
control={control}
name="chainId"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Chain ID"
onBlur={onBlur}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">{errors.chainId?.message}</HelperText>
</>
)}
/>
<Controller
control={control}
defaultValue=""
name="networkName"
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
label="Network Name"
value={value}
onBlur={onBlur}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">{errors.networkName?.message}</HelperText>
</>
)}
/>
<Controller
control={control}
name="rpcUrl"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
label="New RPC URL"
onBlur={onBlur}
value={value}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">{errors.rpcUrl?.message}</HelperText>
</>
)}
/>
<Controller
control={control}
defaultValue=""
name="blockExplorerUrl"
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Block Explorer URL (Optional)"
onBlur={onBlur}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">
{errors.blockExplorerUrl?.message}
</HelperText>
</>
)}
/>
<Controller
control={control}
name="coinType"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Coin Type"
onBlur={onBlur}
onChangeText={onChange}
/>
<HelperText type="error">{errors.coinType?.message}</HelperText>
</>
)}
/>
{namespace === EIP155 ? (
<Controller
control={control}
name="currencySymbol"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Currency Symbol"
onBlur={onBlur}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">
{
(errors as FieldErrors<z.infer<typeof ethNetworkDataSchema>>)
.currencySymbol?.message
}
</HelperText>
</>
)}
/>
) : (
<>
<Controller
control={control}
name="nativeDenom"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Native Denom"
onBlur={onBlur}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">
{
(
errors as FieldErrors<
z.infer<typeof cosmosNetworkDataSchema>
>
).nativeDenom?.message
}
</HelperText>
</>
)}
/>
<Controller
control={control}
name="addressPrefix"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Address Prefix"
onBlur={onBlur}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">
{
(
errors as FieldErrors<
z.infer<typeof cosmosNetworkDataSchema>
>
).addressPrefix?.message
}
</HelperText>
</>
)}
/>
<Controller
control={control}
name="gasPrice"
defaultValue=""
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Gas Price"
onBlur={onBlur}
onChangeText={onChange}
/>
<HelperText type="error">
{
(
errors as FieldErrors<
z.infer<typeof cosmosNetworkDataSchema>
>
).gasPrice?.message
}
</HelperText>
</>
)}
/>
</>
)}
<Button
mode="contained"
loading={isSubmitting}
disabled={isSubmitting}
onPress={handleSubmit(submit)}>
{isSubmitting ? 'Adding' : 'Submit'}
</Button>
</View>
);
};
export default AddNetwork;
+120
View File
@@ -0,0 +1,120 @@
import React, { useEffect, useState } from 'react';
import { AppState, TouchableOpacity, View } from 'react-native';
import { Button, Text, TextInput } from 'react-native-paper';
// import {
// Camera,
// useCameraDevice,
// useCameraPermission,
// useCodeScanner,
// } from 'react-native-vision-camera';
import { Linking } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { web3WalletPair } from '../utils/wallet-connect/WalletConnectUtils';
import styles from '../styles/stylesheet';
import { StackParamsList } from '../types';
const AddSession = () => {
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
// const { hasPermission, requestPermission } = useCameraPermission();
// const device = useCameraDevice('back');
const [currentWCURI, setCurrentWCURI] = useState<string>('');
// const [isActive, setIsActive] = useState(AppState.currentState === 'active');
// const [isScanning, setScanning] = useState(true);
// const codeScanner = useCodeScanner({
// codeTypes: ['qr'],
// onCodeScanned: codes => {
// if (isScanning) {
// codes.forEach(code => {
// if (code.value) {
// setCurrentWCURI(code.value);
// setScanning(false);
// }
// });
// }
// },
// });
const linkToSettings = async () => {
await Linking.openSettings();
};
const pair = async () => {
const pairing = await web3WalletPair({ uri: currentWCURI });
navigation.navigate('WalletConnect');
return pairing;
};
// useEffect(() => {
// const handleAppStateChange = (newState: string) => {
// setIsActive(newState === 'active');
// };
// AppState.addEventListener('change', handleAppStateChange);
// if (!hasPermission) {
// requestPermission();
// }
// }, [hasPermission, requestPermission]);
return (
<View style={styles.appContainer}>
{/* {!hasPermission || !device ? ( */}
{false ? (
<>
{/* <Text>
{!hasPermission
? 'No Camera Permission granted'
: 'No Camera Selected'}
</Text> */}
<TouchableOpacity onPress={linkToSettings}>
<Text variant="titleSmall" style={[styles.hyperlink]}>
Go to settings
</Text>
</TouchableOpacity>
</>
) : (
<>
<View style={styles.cameraContainer}>
{/* {isActive ? (
<Camera
style={styles.camera}
device={device}
isActive={isActive}
codeScanner={codeScanner}
video={false}
/>
) : (
<Text>No Camera Selected!</Text>
)} */}
</View>
<View style={styles.inputContainer}>
<Text variant="titleMedium">Enter WalletConnect URI</Text>
<TextInput
mode="outlined"
onChangeText={setCurrentWCURI}
value={currentWCURI}
numberOfLines={4}
multiline={true}
style={styles.walletConnectUriText}
/>
<View style={styles.signButton}>
<Button mode="contained" onPress={pair}>
Pair Session
</Button>
</View>
</View>
</>
)}
</View>
);
};
export default AddSession;
+300
View File
@@ -0,0 +1,300 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Image, ScrollView, View } from 'react-native';
import { Button, Text, TextInput } from 'react-native-paper';
import { SvgUri } from 'react-native-svg';
import Config from 'react-native-config';
import {
NativeStackNavigationProp,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
import { LaconicClient } from '@cerc-io/registry-sdk';
import { GasPrice, calculateFee } from '@cosmjs/stargate';
import { formatJsonRpcError } from '@json-rpc-tools/utils';
import { useNetworks } from '../context/NetworksContext';
import { Account, StackParamsList } from '../types';
import styles from '../styles/stylesheet';
import { COSMOS, IS_NUMBER_REGEX } from '../utils/constants';
import { retrieveSingleAccount } from '../utils/accounts';
import { getPathKey } from '../utils/misc';
import {
WalletConnectRequests,
approveWalletConnectRequest,
rejectWalletConnectRequest,
} from '../utils/wallet-connect/wallet-connect-requests';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { MEMO } from './ApproveTransfer';
import TxErrorDialog from '../components/TxErrorDialog';
import AccountDetails from '../components/AccountDetails';
type ApproveTransactionProps = NativeStackScreenProps<
StackParamsList,
'ApproveTransaction'
>;
const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
const { networksData } = useNetworks();
const requestSession = route.params.requestSessionData;
const requestName = requestSession.peer.metadata.name;
const requestIcon = requestSession.peer.metadata.icons[0];
const requestURL = requestSession.peer.metadata.url;
const transactionMessage = route.params.transactionMessage;
const signer = route.params.signer;
const requestEvent = route.params.requestEvent;
const chainId = requestEvent.params.chainId;
const requestEventId = requestEvent.id;
const topic = requestEvent.topic;
const [account, setAccount] = useState<Account>();
const [cosmosStargateClient, setCosmosStargateClient] =
useState<LaconicClient>();
const [cosmosGasLimit, setCosmosGasLimit] = useState<string>();
const [fees, setFees] = useState<string>();
const [txError, setTxError] = useState<string>();
const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false);
const [isRequestAccepted, setIsRequestAccepted] = useState(false);
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
const requestedNetwork = networksData.find(
networkData =>
`${networkData.namespace}:${networkData.chainId}` === chainId,
);
const namespace = requestedNetwork!.namespace;
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 LaconicClient.connectWithSigner(
requestedNetwork?.rpcUrl!,
sender,
);
setCosmosStargateClient(client);
} catch (error: any) {
setTxError(error.message);
setIsTxErrorDialogOpen(true);
const response = formatJsonRpcError(requestEventId, error.message);
await web3wallet!.respondSessionRequest({ topic, response });
}
};
setClient();
}, [account, requestedNetwork, chainId, namespace, requestEventId, topic]);
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(() => {
retrieveData(signer);
}, [retrieveData, signer]);
useEffect(() => {
const getCosmosGas = async () => {
try {
if (!cosmosStargateClient) {
return;
}
const gasEstimation = await cosmosStargateClient!.simulate(
transactionMessage.value.participant!,
[transactionMessage],
MEMO,
);
setCosmosGasLimit(
String(
Math.round(gasEstimation * Number(Config.DEFAULT_GAS_ADJUSTMENT)),
),
);
} catch (error: any) {
setTxError(error.message);
setIsTxErrorDialogOpen(true);
const response = formatJsonRpcError(requestEventId, error.message);
await web3wallet!.respondSessionRequest({ topic, response });
}
};
getCosmosGas();
}, [cosmosStargateClient, transactionMessage, requestEventId, topic]);
useEffect(() => {
const gasPrice = GasPrice.fromString(
requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
);
if (!cosmosGasLimit) {
return;
}
const cosmosFees = calculateFee(Number(cosmosGasLimit), gasPrice);
setFees(cosmosFees.amount[0].amount);
}, [namespace, cosmosGasLimit, requestedNetwork]);
const acceptRequestHandler = async () => {
try {
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');
};
return (
<>
<ScrollView contentContainerStyle={styles.approveTransaction}>
<View style={styles.dappDetails}>
{requestIcon && (
<>
{requestIcon.endsWith('.svg') ? (
<View style={styles.dappLogo}>
<SvgUri height="50" width="50" uri={requestIcon} />
</View>
) : (
<Image style={styles.dappLogo} source={{ uri: requestIcon }} />
)}
</>
)}
<Text>{requestName}</Text>
<Text variant="bodySmall">{requestURL}</Text>
</View>
<AccountDetails account={account} />
<Text variant="bodyLarge" style={styles.transactionLabel}>
Message:
</Text>
<View style={styles.messageBody}>
<Text variant="bodyLarge">
{JSON.stringify(transactionMessage, null, 2)}
</Text>
</View>
<>
<Text variant="bodyLarge" style={styles.transactionLabel}>
Gas Limit:
</Text>
<TextInput
mode="outlined"
style={styles.transactionFeesInput}
value={cosmosGasLimit}
onChangeText={value => {
if (IS_NUMBER_REGEX.test(value)) {
setCosmosGasLimit(value);
}
}}
/>
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={acceptRequestHandler}
loading={isRequestAccepted}
disabled={isRequestAccepted}>
Yes
</Button>
<Button
mode="contained"
onPress={rejectRequestHandler}
buttonColor="#B82B0D">
No
</Button>
</View>
</>
</ScrollView>
<TxErrorDialog
error={txError!}
visible={isTxErrorDialogOpen}
hideDialog={async () => {
setIsTxErrorDialogOpen(false);
navigation.navigate('Laconic');
}}
/>
</>
);
};
export default ApproveTransaction;
+648
View File
@@ -0,0 +1,648 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Image, ScrollView, View } from 'react-native';
import {
ActivityIndicator,
Button,
Text,
Appbar,
TextInput,
} from 'react-native-paper';
import { providers, BigNumber } from 'ethers';
import Config from 'react-native-config';
import { Deferrable } from 'ethers/lib/utils';
import { useNavigation } from '@react-navigation/native';
import {
NativeStackNavigationProp,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
import { getHeaderTitle } from '@react-navigation/elements';
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
import {
calculateFee,
GasPrice,
MsgSendEncodeObject,
SigningStargateClient,
} from '@cosmjs/stargate';
import { Account, StackParamsList } from '../types';
import AccountDetails from '../components/AccountDetails';
import styles from '../styles/stylesheet';
import { retrieveSingleAccount } from '../utils/accounts';
import {
approveWalletConnectRequest,
rejectWalletConnectRequest,
WalletConnectRequests,
} from '../utils/wallet-connect/wallet-connect-requests';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import DataBox from '../components/DataBox';
import { getPathKey } from '../utils/misc';
import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155, IS_NUMBER_REGEX } from '../utils/constants';
import TxErrorDialog from '../components/TxErrorDialog';
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData';
export const MEMO = 'Sending signed tx from Laconic Wallet';
// Reference: https://ethereum.org/en/developers/docs/gas/#what-is-gas-limit
const ETH_MINIMUM_GAS = 21000;
type SignRequestProps = NativeStackScreenProps<
StackParamsList,
'ApproveTransfer'
>;
const ApproveTransfer = ({ route }: SignRequestProps) => {
const { networksData } = useNetworks();
const requestSession = route.params.requestSessionData;
const requestName = requestSession.peer.metadata.name;
const requestIcon = requestSession.peer.metadata.icons[0];
const requestURL = requestSession.peer.metadata.url;
const transaction = route.params.transaction;
const requestEvent = route.params.requestEvent;
const chainId = requestEvent.params.chainId;
const requestMethod = requestEvent.params.request.method;
const [account, setAccount] = useState<Account>();
const [isLoading, setIsLoading] = useState(true);
const [balance, setBalance] = useState<string>('');
const [isTxLoading, setIsTxLoading] = useState(false);
const [cosmosStargateClient, setCosmosStargateClient] =
useState<SigningStargateClient>();
const [fees, setFees] = useState<string>();
const [cosmosGasLimit, setCosmosGasLimit] = useState<string>();
const [txError, setTxError] = useState<string>();
const [isTxErrorDialogOpen, setIsTxErrorDialogOpen] = useState(false);
const [ethGasPrice, setEthGasPrice] = useState<BigNumber | null>();
const [ethGasLimit, setEthGasLimit] = useState<BigNumber>();
const [ethMaxFee, setEthMaxFee] = useState<BigNumber | null>();
const [ethMaxPriorityFee, setEthMaxPriorityFee] =
useState<BigNumber | null>();
const isSufficientFunds = useMemo(() => {
if (!transaction.value) {
return;
}
if (!balance) {
return;
}
const amountBigNum = BigNumber.from(String(transaction.value));
const balanceBigNum = BigNumber.from(balance);
if (amountBigNum.gte(balanceBigNum)) {
return false;
} else {
return true;
}
}, [balance, transaction]);
const requestedNetwork = networksData.find(
networkData =>
`${networkData.namespace}:${networkData.chainId}` === chainId,
);
const namespace = requestedNetwork!.namespace;
const sendMsg: MsgSendEncodeObject = useMemo(() => {
return {
typeUrl: '/cosmos.bank.v1beta1.MsgSend',
value: {
fromAddress: transaction.from,
toAddress: transaction.to,
amount: [
{
amount: String(transaction.value),
denom: requestedNetwork!.nativeDenom!,
},
],
},
};
}, [requestedNetwork, transaction]);
useEffect(() => {
if (namespace !== COSMOS) {
return;
}
const setClient = async () => {
if (!account) {
return;
}
const cosmosPrivKey = (
await getPathKey(
`${requestedNetwork?.namespace}:${requestedNetwork?.chainId}`,
account.index,
)
).privKey;
const sender = await DirectSecp256k1Wallet.fromKey(
Buffer.from(cosmosPrivKey.split('0x')[1], 'hex'),
requestedNetwork?.addressPrefix,
);
try {
const client = await SigningStargateClient.connectWithSigner(
requestedNetwork?.rpcUrl!,
sender,
);
setCosmosStargateClient(client);
} catch (error: any) {
setTxError(error.message);
setIsTxErrorDialogOpen(true);
}
};
setClient();
}, [account, requestedNetwork, chainId, namespace]);
const provider = useMemo(() => {
if (namespace === EIP155) {
if (!requestedNetwork) {
throw new Error('Requested chain not supported');
}
try {
const ethProvider = new providers.JsonRpcProvider(
requestedNetwork.rpcUrl,
);
return ethProvider;
} catch (error: any) {
setTxError(error.message);
setIsTxErrorDialogOpen(true);
}
}
}, [requestedNetwork, namespace]);
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
const retrieveData = useCallback(
async (requestAddress: string) => {
const requestAccount = await retrieveSingleAccount(
requestedNetwork!.namespace,
requestedNetwork!.chainId,
requestAddress,
);
if (!requestAccount) {
navigation.navigate('InvalidPath');
return;
}
setAccount(requestAccount);
},
[navigation, requestedNetwork],
);
useEffect(() => {
// Set loading to false when gas values for requested chain are fetched
// If requested chain is EVM compatible, the cosmos gas values will be undefined and vice-versa, hence the condition checks only one of them at the same time
if (
// If requested chain is EVM compatible, set loading to false when ethMaxFee and ethPriorityFee have been populated
(ethMaxFee !== undefined && ethMaxPriorityFee !== undefined) ||
// Or if requested chain is a cosmos chain, set loading to false when cosmosGasLimit has been populated
cosmosGasLimit !== undefined
) {
setIsLoading(false);
}
}, [ethMaxFee, ethMaxPriorityFee, cosmosGasLimit]);
useEffect(() => {
if (namespace === EIP155) {
const ethFees = BigNumber.from(ethGasLimit ?? 0)
.mul(BigNumber.from(ethMaxFee ?? ethGasPrice ?? 0))
.toString();
setFees(ethFees);
} else {
const gasPrice = GasPrice.fromString(
requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
);
if (!cosmosGasLimit) {
return;
}
const cosmosFees = calculateFee(Number(cosmosGasLimit), gasPrice);
setFees(cosmosFees.amount[0].amount);
}
}, [
transaction,
namespace,
ethGasLimit,
ethGasPrice,
cosmosGasLimit,
requestedNetwork,
ethMaxFee,
]);
useEffect(() => {
retrieveData(transaction.from!);
}, [retrieveData, transaction]);
const isEIP1559 = useMemo(() => {
if (cosmosGasLimit) {
return;
}
if (ethMaxFee !== null && ethMaxPriorityFee !== null) {
return true;
}
return false;
}, [cosmosGasLimit, ethMaxFee, ethMaxPriorityFee]);
const acceptRequestHandler = async () => {
setIsTxLoading(true);
try {
if (!account) {
throw new Error('account not found');
}
if (ethGasLimit && ethGasLimit.lt(ETH_MINIMUM_GAS)) {
throw new Error(`Atleast ${ETH_MINIMUM_GAS} gas limit is required`);
}
if (ethMaxFee && ethMaxPriorityFee && ethMaxFee.lte(ethMaxPriorityFee)) {
throw new Error(
`Max fee per gas (${ethMaxFee.toNumber()}) cannot be lower than or equal to max priority fee per gas (${ethMaxPriorityFee.toNumber()})`,
);
}
let options: WalletConnectRequests;
switch (requestMethod) {
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
if (
ethMaxFee === undefined ||
ethMaxPriorityFee === undefined ||
ethGasPrice === undefined
) {
throw new Error('Gas values not found');
}
options = {
type: 'eth_sendTransaction',
provider: provider!,
ethGasLimit: BigNumber.from(ethGasLimit),
ethGasPrice: ethGasPrice ? ethGasPrice.toHexString() : null,
maxFeePerGas: ethMaxFee,
maxPriorityFeePerGas: ethMaxPriorityFee,
};
break;
case COSMOS_METHODS.COSMOS_SEND_TOKENS:
if (!cosmosStargateClient) {
throw new Error('Cosmos stargate client not found');
}
options = {
type: 'cosmos_sendTokens',
signingStargateClient: cosmosStargateClient,
// StdFee object
cosmosFee: {
// This amount is total fees required for transaction
amount: [
{
amount: fees!,
denom: requestedNetwork!.nativeDenom!,
},
],
gas: cosmosGasLimit!,
},
sendMsg,
memo: MEMO,
};
break;
default:
throw new Error('Invalid method');
}
const response = await approveWalletConnectRequest(
requestEvent,
account,
namespace,
requestedNetwork!.chainId,
options,
);
const { topic } = requestEvent;
await web3wallet!.respondSessionRequest({ topic, response });
navigation.navigate('Laconic');
} catch (error: any) {
setTxError(error.message);
setIsTxErrorDialogOpen(true);
}
setIsTxLoading(false);
};
const rejectRequestHandler = async () => {
const response = rejectWalletConnectRequest(requestEvent);
const { topic } = requestEvent;
await web3wallet!.respondSessionRequest({
topic,
response,
});
navigation.navigate('Laconic');
};
useEffect(() => {
const getAccountBalance = async () => {
try {
if (!account) {
return;
}
if (namespace === EIP155) {
if (!provider) {
return;
}
const fetchedBalance = await provider.getBalance(account.address);
setBalance(fetchedBalance ? fetchedBalance.toString() : '0');
} else {
const cosmosBalance = await cosmosStargateClient?.getBalance(
account.address,
requestedNetwork!.nativeDenom!.toLowerCase(),
);
setBalance(cosmosBalance?.amount!);
}
} catch (error: any) {
setTxError(error.message);
setIsTxErrorDialogOpen(true);
}
};
getAccountBalance();
}, [account, provider, namespace, cosmosStargateClient, requestedNetwork]);
useEffect(() => {
navigation.setOptions({
// eslint-disable-next-line react/no-unstable-nested-components
header: ({ options, back }) => {
const title = getHeaderTitle(options, 'Approve Transaction');
return (
<Appbar.Header>
{back && (
<Appbar.BackAction
onPress={async () => {
await rejectRequestHandler();
navigation.navigate('Laconic');
}}
/>
)}
<Appbar.Content title={title} />
</Appbar.Header>
);
},
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation, route.name]);
useEffect(() => {
const getEthGas = async () => {
try {
if (!isSufficientFunds || !provider) {
return;
}
const data = await provider.getFeeData();
setEthMaxFee(data.maxFeePerGas);
setEthMaxPriorityFee(data.maxPriorityFeePerGas);
setEthGasPrice(data.gasPrice);
if (transaction.gasLimit) {
setEthGasLimit(BigNumber.from(transaction.gasLimit));
} else {
const transactionObject: Deferrable<providers.TransactionRequest> = {
from: transaction.from!,
to: transaction.to!,
data: transaction.data!,
value: transaction.value!,
maxFeePerGas: data.maxFeePerGas ?? undefined,
maxPriorityFeePerGas: data.maxPriorityFeePerGas ?? undefined,
gasPrice: data.maxFeePerGas
? undefined
: data.gasPrice ?? undefined,
};
const gasLimit = await provider.estimateGas(transactionObject);
setEthGasLimit(gasLimit);
}
} catch (error: any) {
setTxError(error.message);
setIsTxErrorDialogOpen(true);
}
};
getEthGas();
}, [provider, transaction, isSufficientFunds]);
useEffect(() => {
const getCosmosGas = async () => {
try {
if (!cosmosStargateClient) {
return;
}
if (!isSufficientFunds) {
return;
}
const gasEstimation = await cosmosStargateClient.simulate(
transaction.from!,
[sendMsg],
MEMO,
);
setCosmosGasLimit(
String(
Math.round(gasEstimation * Number(Config.DEFAULT_GAS_ADJUSTMENT)),
),
);
} catch (error: any) {
setTxError(error.message);
setIsTxErrorDialogOpen(true);
}
};
getCosmosGas();
}, [cosmosStargateClient, isSufficientFunds, sendMsg, transaction]);
useEffect(() => {
if (balance && !isSufficientFunds) {
setTxError('Insufficient funds');
setIsTxErrorDialogOpen(true);
}
}, [isSufficientFunds, balance]);
return (
<>
{isLoading ? (
<View style={styles.spinnerContainer}>
<ActivityIndicator size="large" color="#0000ff" />
</View>
) : (
<>
<ScrollView contentContainerStyle={styles.appContainer}>
<View style={styles.dappDetails}>
{requestIcon && (
<Image
style={styles.dappLogo}
source={requestIcon ? { uri: requestIcon } : undefined}
/>
)}
<Text>{requestName}</Text>
<Text variant="bodyMedium">{requestURL}</Text>
</View>
<View style={styles.dataBoxContainer}>
<Text style={styles.dataBoxLabel}>From</Text>
<View style={styles.dataBox}>
<AccountDetails account={account} />
</View>
</View>
<DataBox
label={`Balance (${
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
})`}
data={
balance === '' || balance === undefined
? 'Loading balance...'
: `${balance}`
}
/>
{transaction && (
<View style={styles.approveTransfer}>
<DataBox label="To" data={transaction.to!} />
<DataBox
label={`Amount (${
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
})`}
data={BigNumber.from(
transaction.value?.toString(),
).toString()}
/>
{namespace === EIP155 ? (
<>
{isEIP1559 === false ? (
<>
<Text style={styles.dataBoxLabel}>
{'Gas Price (wei)'}
</Text>
<TextInput
mode="outlined"
value={ethGasPrice?.toNumber().toString()}
onChangeText={value =>
setEthGasPrice(BigNumber.from(value))
}
style={styles.transactionFeesInput}
/>
</>
) : (
<>
<Text style={styles.dataBoxLabel}>
Max Fee Per Gas (wei)
</Text>
<TextInput
mode="outlined"
value={ethMaxFee?.toNumber().toString()}
onChangeText={value => {
if (IS_NUMBER_REGEX.test(value)) {
setEthMaxFee(BigNumber.from(value));
}
}}
style={styles.transactionFeesInput}
/>
<Text style={styles.dataBoxLabel}>
Max Priority Fee Per Gas (wei)
</Text>
<TextInput
mode="outlined"
value={ethMaxPriorityFee?.toNumber().toString()}
onChangeText={value => {
if (IS_NUMBER_REGEX.test(value)) {
setEthMaxPriorityFee(BigNumber.from(value));
}
}}
style={styles.transactionFeesInput}
/>
</>
)}
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
<TextInput
mode="outlined"
value={ethGasLimit?.toNumber().toString()}
onChangeText={value => {
if (IS_NUMBER_REGEX.test(value)) {
setEthGasLimit(BigNumber.from(value));
}
}}
style={styles.transactionFeesInput}
/>
<DataBox
label={`${
isEIP1559 === true ? 'Max Fee' : 'Gas Fee'
} (wei)`}
data={fees!}
/>
<DataBox label="Data" data={transaction.data!} />
</>
) : (
<>
<Text style={styles.dataBoxLabel}>{`Fee (${
requestedNetwork!.nativeDenom
})`}</Text>
<TextInput
mode="outlined"
value={fees}
onChangeText={value => setFees(value)}
style={styles.transactionFeesInput}
/>
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
<TextInput
mode="outlined"
value={cosmosGasLimit}
onChangeText={value => {
if (IS_NUMBER_REGEX.test(value)) {
setCosmosGasLimit(value);
}
}}
/>
</>
)}
</View>
)}
</ScrollView>
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={acceptRequestHandler}
loading={isTxLoading}
disabled={!balance || !fees}>
{isTxLoading ? 'Processing' : 'Yes'}
</Button>
<Button
mode="contained"
onPress={rejectRequestHandler}
buttonColor="#B82B0D">
No
</Button>
</View>
</>
)}
<TxErrorDialog
error={txError!}
visible={isTxErrorDialogOpen}
hideDialog={() => {
setIsTxErrorDialogOpen(false);
if (!isSufficientFunds || !balance || !fees) {
rejectRequestHandler();
navigation.navigate('Laconic');
}
}}
/>
</>
);
};
export default ApproveTransfer;
+197
View File
@@ -0,0 +1,197 @@
import React, { useCallback } from 'react';
import { View } from 'react-native';
import { useForm, Controller, FieldErrors } from 'react-hook-form';
import { TextInput, Button, HelperText, Text } from 'react-native-paper';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import {
NativeStackNavigationProp,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
import { setInternetCredentials } from '../utils/key-store';
import { StackParamsList } from '../types';
import styles from '../styles/stylesheet';
import { retrieveNetworksData } from '../utils/accounts';
import { useNetworks } from '../context/NetworksContext';
import {
COSMOS,
EIP155,
EMPTY_FIELD_ERROR,
INVALID_URL_ERROR,
} from '../utils/constants';
const ethNetworksFormSchema = z.object({
// Adding type field for resolving typescript error
type: z.literal(EIP155).optional(),
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
rpcUrl: z.string().url({ message: INVALID_URL_ERROR }),
blockExplorerUrl: z
.string()
.url({ message: INVALID_URL_ERROR })
.or(z.literal('')),
});
const cosmosNetworksFormDataSchema = z.object({
type: z.literal(COSMOS).optional(),
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
rpcUrl: z.string().url({ message: INVALID_URL_ERROR }),
blockExplorerUrl: z
.string()
.url({ message: INVALID_URL_ERROR })
.or(z.literal('')),
gasPrice: z
.string()
.nonempty({ message: EMPTY_FIELD_ERROR })
.regex(/^\d+(\.\d+)?$/),
});
type EditNetworkProps = NativeStackScreenProps<StackParamsList, 'EditNetwork'>;
const EditNetwork = ({ route }: EditNetworkProps) => {
const { setNetworksData } = useNetworks();
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
const networkData = route.params.selectedNetwork;
const networksFormDataSchema =
networkData.namespace === COSMOS
? cosmosNetworksFormDataSchema
: ethNetworksFormSchema;
const {
control,
formState: { errors, isSubmitting },
handleSubmit,
} = useForm<z.infer<typeof networksFormDataSchema>>({
mode: 'onChange',
resolver: zodResolver(networksFormDataSchema),
});
const submit = useCallback(
async (data: z.infer<typeof networksFormDataSchema>) => {
const retrievedNetworksData = await retrieveNetworksData();
const { type, ...dataWithoutType } = data;
const newNetworkData = { ...networkData, ...dataWithoutType };
const index = retrievedNetworksData.findIndex(
network => network.networkId === networkData.networkId,
);
retrievedNetworksData.splice(index, 1, newNetworkData);
await setInternetCredentials(
'networks',
'_',
JSON.stringify(retrievedNetworksData),
);
setNetworksData(retrievedNetworksData);
navigation.navigate('Laconic');
},
[networkData, navigation, setNetworksData],
);
return (
<View>
<View>
<Text style={styles.subHeading}>
Edit {networkData?.networkName} details
</Text>
</View>
<Controller
control={control}
defaultValue={networkData.networkName}
name="networkName"
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
label="Network Name"
value={value}
onBlur={onBlur}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">{errors.networkName?.message}</HelperText>
</>
)}
/>
<Controller
control={control}
name="rpcUrl"
defaultValue={networkData.rpcUrl}
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
label="New RPC URL"
onBlur={onBlur}
value={value}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">{errors.rpcUrl?.message}</HelperText>
</>
)}
/>
<Controller
control={control}
defaultValue={networkData.blockExplorerUrl}
name="blockExplorerUrl"
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Block Explorer URL (Optional)"
onBlur={onBlur}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">
{errors.blockExplorerUrl?.message}
</HelperText>
</>
)}
/>
{networkData.namespace === COSMOS && (
<Controller
control={control}
name="gasPrice"
defaultValue={networkData.gasPrice}
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Gas Price"
onBlur={onBlur}
onChangeText={onChange}
/>
<HelperText type="error">
{
(
errors as FieldErrors<
z.infer<typeof cosmosNetworksFormDataSchema>
>
).gasPrice?.message
}
</HelperText>
</>
)}
/>
)}
<Button
mode="contained"
loading={isSubmitting}
disabled={isSubmitting}
onPress={handleSubmit(submit)}>
{isSubmitting ? 'Adding' : 'Submit'}
</Button>
</View>
);
};
export default EditNetwork;
+176
View File
@@ -0,0 +1,176 @@
import React, { useCallback, useEffect, useState } from 'react';
import { View, ActivityIndicator, Image } from 'react-native';
import { Button, Text } from 'react-native-paper';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
// import { getSdkError } from '@walletconnect/utils';
import { createWallet, resetWallet, retrieveAccounts } from '../utils/accounts';
import { DialogComponent } from '../components/Dialog';
import { NetworkDropdown } from '../components/NetworkDropdown';
import Accounts from '../components/Accounts';
import CreateWallet from '../components/CreateWallet';
import ConfirmDialog from '../components/ConfirmDialog';
import styles from '../styles/stylesheet';
import { useAccounts } from '../context/AccountsContext';
import { useWalletConnect } from '../context/WalletConnectContext';
import { NetworksDataState, StackParamsList } from '../types';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { useNetworks } from '../context/NetworksContext';
const WCLogo = () => {
return (
<Image
style={styles.walletConnectLogo}
source={require('../assets/WalletConnect-Icon-Blueberry.png')}
/>
);
};
const HomeScreen = () => {
const { accounts, setAccounts, setCurrentIndex } = useAccounts();
const { networksData, selectedNetwork, setSelectedNetwork, setNetworksData } =
useNetworks();
const { setActiveSessions } = useWalletConnect();
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
useEffect(() => {
if (accounts.length > 0) {
navigation.setOptions({
// eslint-disable-next-line react/no-unstable-nested-components
headerRight: () => (
<Button onPress={() => navigation.navigate('WalletConnect')}>
{<WCLogo />}
</Button>
),
});
} else {
navigation.setOptions({
headerRight: undefined,
});
}
}, [navigation, accounts]);
const [isWalletCreated, setIsWalletCreated] = useState<boolean>(false);
const [isWalletCreating, setIsWalletCreating] = useState<boolean>(false);
const [walletDialog, setWalletDialog] = useState<boolean>(false);
const [resetWalletDialog, setResetWalletDialog] = useState<boolean>(false);
// const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(false);
const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(true);
const [phrase, setPhrase] = useState('');
const hideWalletDialog = () => setWalletDialog(false);
const hideResetDialog = () => setResetWalletDialog(false);
const fetchAccounts = useCallback(async () => {
if (!selectedNetwork) {
return;
}
const loadedAccounts = await retrieveAccounts(selectedNetwork);
if (loadedAccounts) {
setAccounts(loadedAccounts);
setIsWalletCreated(true);
}
setIsAccountsFetched(true);
}, [selectedNetwork, setAccounts]);
const createWalletHandler = async () => {
setIsWalletCreating(true);
const mnemonic = await createWallet(networksData);
if (mnemonic) {
fetchAccounts();
setWalletDialog(true);
setPhrase(mnemonic);
setSelectedNetwork(networksData[0]);
}
};
const confirmResetWallet = useCallback(async () => {
setIsWalletCreated(false);
setIsWalletCreating(false);
setAccounts([]);
setCurrentIndex(0);
setNetworksData([]);
setSelectedNetwork(undefined);
await resetWallet();
// const sessions = web3wallet!.getActiveSessions();
// Object.keys(sessions).forEach(async sessionId => {
// await web3wallet!.disconnectSession({
// topic: sessionId,
// reason: getSdkError('USER_DISCONNECTED'),
// });
// });
setActiveSessions({});
hideResetDialog();
}, [
setAccounts,
setActiveSessions,
setCurrentIndex,
setNetworksData,
setSelectedNetwork,
]);
const updateNetwork = (networkData: NetworksDataState) => {
setSelectedNetwork(networkData);
setCurrentIndex(0);
};
useEffect(() => {
fetchAccounts();
}, [networksData, setAccounts, selectedNetwork, fetchAccounts]);
return (
<View style={styles.appContainer}>
{!isAccountsFetched ? (
<View style={styles.spinnerContainer}>
<Text style={styles.LoadingText}>Loading...</Text>
<ActivityIndicator size="large" color="#0000ff" />
</View>
) : isWalletCreated && selectedNetwork ? (
<>
<NetworkDropdown updateNetwork={updateNetwork} />
<View style={styles.accountComponent}>
<Accounts />
</View>
<View style={styles.resetContainer}>
<Button
style={styles.resetButton}
mode="contained"
buttonColor="#B82B0D"
onPress={() => {
setResetWalletDialog(true);
}}>
Reset Wallet
</Button>
</View>
</>
) : (
<CreateWallet
isWalletCreating={isWalletCreating}
createWalletHandler={createWalletHandler}
/>
)}
<DialogComponent
visible={walletDialog}
hideDialog={hideWalletDialog}
contentText={phrase}
/>
<ConfirmDialog
title="Reset wallet"
visible={resetWalletDialog}
hideDialog={hideResetDialog}
onConfirm={confirmResetWallet}
/>
</View>
);
};
export default HomeScreen;
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
import { View, Text } from 'react-native';
import { Button } from 'react-native-paper';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
import { StackParamsList } from '../types';
import styles from '../styles/stylesheet';
const InvalidPath = () => {
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
return (
<View style={styles.badRequestContainer}>
<Text style={styles.invalidMessageText}>
The signature request was invalid.
</Text>
<Button
mode="contained"
onPress={() => {
navigation.navigate('Laconic');
}}>
Home
</Button>
</View>
);
};
export default InvalidPath;
+60
View File
@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import { View, Alert } from 'react-native';
import { Button, Text, TextInput } from 'react-native-paper';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { StackParamsList } from '../types';
import styles from '../styles/stylesheet';
import { signMessage } from '../utils/sign-message';
import AccountDetails from '../components/AccountDetails';
type SignProps = NativeStackScreenProps<StackParamsList, 'SignMessage'>;
const SignMessage = ({ route }: SignProps) => {
const namespace = route.params.selectedNamespace;
const chainId = route.params.selectedChainId;
const account = route.params.accountInfo;
const [message, setMessage] = useState<string>('');
const signMessageHandler = async () => {
const signedMessage = await signMessage({
message,
namespace,
chainId,
accountId: account.index,
});
Alert.alert('Signature', signedMessage);
};
return (
<View style={styles.signPage}>
<View style={styles.accountInfo}>
<View>
<Text variant="titleMedium">
{account && `Account ${account.index + 1}`}
</Text>
</View>
<View style={styles.accountContainer}>
<AccountDetails account={account} />
</View>
</View>
<TextInput
mode="outlined"
placeholder="Enter your message"
onChangeText={text => setMessage(text)}
value={message}
/>
<View style={styles.signButton}>
<Button mode="contained" onPress={signMessageHandler}>
Sign
</Button>
</View>
</View>
);
};
export default SignMessage;
+330
View File
@@ -0,0 +1,330 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Alert, Image, ScrollView, View } from 'react-native';
import { ActivityIndicator, Button, Text, Appbar } from 'react-native-paper';
import { SvgUri } from 'react-native-svg';
import { useNavigation } from '@react-navigation/native';
import {
NativeStackNavigationProp,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
import { getHeaderTitle } from '@react-navigation/elements';
import { Account, StackParamsList } from '../types';
import AccountDetails from '../components/AccountDetails';
import styles from '../styles/stylesheet';
import { signMessage } from '../utils/sign-message';
import { retrieveSingleAccount } from '../utils/accounts';
import {
approveWalletConnectRequest,
rejectWalletConnectRequest,
WalletConnectRequests,
} from '../utils/wallet-connect/wallet-connect-requests';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
import { useNetworks } from '../context/NetworksContext';
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData';
type SignRequestProps = NativeStackScreenProps<StackParamsList, 'SignRequest'>;
const SignRequest = ({ 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 [account, setAccount] = useState<Account>();
const [message, setMessage] = useState<string>('');
const [namespace, setNamespace] = useState<string>('');
const [chainId, setChainId] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [isApproving, setIsApproving] = useState(false);
const [isRejecting, setIsRejecting] = useState(false);
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
const isCosmosSignDirect = useMemo(() => {
const requestParams = route.params.requestEvent;
if (!requestParams) {
return false;
}
return requestParams.params.request.method === 'cosmos_signDirect';
}, [route.params]);
const isEthSendTransaction = useMemo(() => {
const requestParams = route.params.requestEvent;
if (!requestParams) {
return false;
}
return (
requestParams.params.request.method ===
EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION
);
}, [route.params]);
const retrieveData = useCallback(
async (
requestNamespace: string,
requestChainId: string,
requestAddress: string,
requestMessage: string,
) => {
const requestAccount = await retrieveSingleAccount(
requestNamespace,
requestChainId,
requestAddress,
);
if (!requestAccount) {
navigation.navigate('InvalidPath');
return;
}
if (requestAccount !== account) {
setAccount(requestAccount);
}
if (requestMessage !== message) {
setMessage(decodeURIComponent(requestMessage));
}
if (requestNamespace !== namespace) {
setNamespace(requestNamespace);
}
if (requestChainId !== chainId) {
setChainId(requestChainId);
}
setIsLoading(false);
},
[account, message, navigation, namespace, chainId],
);
const sanitizePath = useCallback(
(path: string) => {
const regex = /^\/sign\/(eip155|cosmos)\/(.+)\/(.+)\/(.+)$/;
const match = path.match(regex);
if (match) {
const [, pathNamespace, pathChainId, pathAddress, pathMessage] = match;
return {
namespace: pathNamespace,
chainId: pathChainId,
address: pathAddress,
message: pathMessage,
};
} else {
navigation.navigate('InvalidPath');
}
return null;
},
[navigation],
);
useEffect(() => {
if (route.path) {
const sanitizedRoute = sanitizePath(route.path);
sanitizedRoute &&
retrieveData(
sanitizedRoute.namespace,
sanitizedRoute.chainId,
sanitizedRoute.address,
sanitizedRoute.message,
);
return;
}
const requestEvent = route.params.requestEvent;
const requestChainId = requestEvent?.params.chainId;
const requestedChain = networksData.find(
networkData => networkData.chainId === requestChainId?.split(':')[1],
);
retrieveData(
requestedChain!.namespace,
requestedChain!.chainId,
route.params.address,
route.params.message,
);
}, [retrieveData, sanitizePath, route, networksData]);
const handleWalletConnectRequest = async () => {
const { requestEvent } = route.params || {};
if (!account) {
throw new Error('account not found');
}
if (!requestEvent) {
throw new Error('Request event not found');
}
let options: WalletConnectRequests;
switch (requestEvent.params.request.method) {
case COSMOS_METHODS.COSMOS_SIGN_DIRECT:
options = {
type: 'cosmos_signDirect',
message,
};
break;
case COSMOS_METHODS.COSMOS_SIGN_AMINO:
options = {
type: 'cosmos_signAmino',
message,
};
break;
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
options = {
type: 'personal_sign',
message,
};
break;
default:
throw new Error('Invalid Method');
}
const response = await approveWalletConnectRequest(
requestEvent,
account,
namespace,
chainId,
options,
);
const { topic } = requestEvent;
await web3wallet!.respondSessionRequest({ topic, response });
};
const handleIntent = async () => {
if (!account) {
throw new Error('Account is not valid');
}
if (message) {
const signedMessage = await signMessage({
message,
namespace,
chainId,
accountId: account.index,
});
Alert.alert('Signature', signedMessage);
}
};
const signMessageHandler = async () => {
setIsApproving(true);
if (route.params.requestEvent) {
await handleWalletConnectRequest();
} else {
await handleIntent();
}
setIsApproving(false);
navigation.navigate('Laconic');
};
const rejectRequestHandler = async () => {
setIsRejecting(true);
if (route.params?.requestEvent) {
const response = rejectWalletConnectRequest(route.params?.requestEvent);
const { topic } = route.params?.requestEvent;
await web3wallet!.respondSessionRequest({
topic,
response,
});
}
setIsRejecting(false);
navigation.navigate('Laconic');
};
useEffect(() => {
navigation.setOptions({
// eslint-disable-next-line react/no-unstable-nested-components
header: ({ options, back }) => {
const title = getHeaderTitle(options, 'Sign Request');
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]);
return (
<>
{isLoading ? (
<View style={styles.spinnerContainer}>
<ActivityIndicator size="large" color="#0000ff" />
</View>
) : (
<>
<ScrollView contentContainerStyle={styles.appContainer}>
<View style={styles.dappDetails}>
{requestIcon && (
<>
{requestIcon.endsWith('.svg') ? (
<View style={styles.dappLogo}>
<SvgUri height="50" width="50" uri={requestIcon} />
</View>
) : (
<Image
style={styles.dappLogo}
source={{ uri: requestIcon }}
/>
)}
</>
)}
<Text>{requestName}</Text>
<Text variant="bodyMedium">{requestURL}</Text>
</View>
<AccountDetails account={account} />
{isCosmosSignDirect || isEthSendTransaction ? (
<View style={styles.requestDirectMessage}>
<ScrollView nestedScrollEnabled>
<Text variant="bodyLarge">{message}</Text>
</ScrollView>
</View>
) : (
<View style={styles.requestMessage}>
<Text variant="bodyLarge">{message}</Text>
</View>
)}
</ScrollView>
<View style={styles.buttonContainer}>
<Button
mode="contained"
onPress={signMessageHandler}
loading={isApproving}
disabled={isApproving}>
Yes
</Button>
<Button
mode="contained"
onPress={rejectRequestHandler}
loading={isRejecting}
buttonColor="#B82B0D">
No
</Button>
</View>
</>
)}
</>
);
};
export default SignRequest;
+84
View File
@@ -0,0 +1,84 @@
import React, { useEffect } from 'react';
import { Image, TouchableOpacity, View } from 'react-native';
import { List, Text } from 'react-native-paper';
import { SvgUri } from 'react-native-svg';
// import { getSdkError } from '@walletconnect/utils';
import { useWalletConnect } from '../context/WalletConnectContext';
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
import styles from '../styles/stylesheet';
export default function WalletConnect() {
const { activeSessions, setActiveSessions } = useWalletConnect();
const disconnect = async (sessionId: string) => {
// await web3wallet!.disconnectSession({
// topic: sessionId,
// reason: getSdkError('USER_DISCONNECTED'),
// });
const sessions = web3wallet?.getActiveSessions() || {};
setActiveSessions(sessions);
return;
};
useEffect(() => {
const sessions = web3wallet!.getActiveSessions();
setActiveSessions(sessions);
}, [setActiveSessions]);
return (
<View>
{Object.keys(activeSessions).length > 0 ? (
<>
<View style={styles.sessionsContainer}>
<Text variant="titleMedium">Active Sessions</Text>
</View>
<List.Section>
{Object.entries(activeSessions).map(([sessionId, session]) => (
<List.Item
style={styles.sessionItem}
key={sessionId}
title={`${session.peer.metadata.name}`}
descriptionNumberOfLines={7}
description={`${sessionId} \n\n${session.peer.metadata.url}\n\n${session.peer.metadata.description}`}
// reference: https://github.com/react-navigation/react-navigation/issues/11371#issuecomment-1546543183
// eslint-disable-next-line react/no-unstable-nested-components
left={() => (
<>
{session.peer.metadata.icons[0].endsWith('.svg') ? (
<View style={styles.dappLogo}>
<SvgUri
height="50"
width="50"
uri={session.peer.metadata.icons[0]}
/>
</View>
) : (
<Image
style={styles.dappLogo}
source={{ uri: session.peer.metadata.icons[0] }}
/>
)}
</>
)}
// eslint-disable-next-line react/no-unstable-nested-components
right={() => (
<TouchableOpacity
onPress={() => disconnect(sessionId)}
style={styles.disconnectSession}>
<List.Icon icon="close" />
</TouchableOpacity>
)}
/>
))}
</List.Section>
</>
) : (
<View style={styles.noActiveSessions}>
<Text>You have no active sessions</Text>
</View>
)}
</View>
);
}
+5
View File
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
+287
View File
@@ -0,0 +1,287 @@
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
createWalletContainer: {
marginTop: 20,
width: 150,
alignSelf: 'center',
},
signLink: {
alignItems: 'flex-end',
marginTop: 24,
},
hyperlink: {
fontWeight: '500',
textDecorationLine: 'underline',
},
highlight: {
fontWeight: '700',
},
accountContainer: {
padding: 8,
paddingBottom: 0,
},
addAccountButton: {
marginTop: 24,
alignSelf: 'center',
},
accountComponent: {
flex: 4,
},
appContainer: {
flexGrow: 1,
marginTop: 24,
paddingHorizontal: 24,
},
resetContainer: {
flex: 1,
justifyContent: 'center',
},
resetButton: {
alignSelf: 'center',
},
signButton: {
marginTop: 20,
width: 150,
alignSelf: 'center',
},
signPage: {
paddingHorizontal: 24,
},
addNetwork: {
paddingHorizontal: 24,
marginTop: 30,
},
accountInfo: {
marginTop: 12,
marginBottom: 30,
},
networkDropdown: {
marginBottom: 20,
},
dialogTitle: {
padding: 10,
},
dialogContents: {
marginTop: 24,
padding: 10,
borderWidth: 1,
borderRadius: 10,
},
dialogWarning: {
color: 'red',
},
gridContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
},
gridItem: {
width: '25%',
margin: 8,
padding: 6,
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 8,
alignItems: 'center',
justifyContent: 'flex-start',
},
HDcontainer: {
marginTop: 24,
paddingHorizontal: 8,
},
HDrowContainer: {
flexDirection: 'row',
alignItems: 'center',
},
HDtext: {
color: 'black',
fontSize: 18,
margin: 4,
},
HDtextInput: {
flex: 1,
},
HDbuttonContainer: {
marginTop: 20,
width: 200,
alignSelf: 'center',
},
spinnerContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
LoadingText: {
color: 'black',
fontSize: 18,
padding: 10,
},
requestMessage: {
borderWidth: 1,
borderRadius: 5,
marginTop: 50,
height: 'auto',
alignItems: 'center',
justifyContent: 'center',
padding: 10,
},
requestDirectMessage: {
borderWidth: 1,
borderRadius: 5,
marginTop: 20,
marginBottom: 50,
height: 500,
alignItems: 'center',
justifyContent: 'center',
padding: 8,
},
approveTransfer: {
height: '40%',
marginBottom: 30,
},
buttonContainer: {
flexDirection: 'row',
marginLeft: 20,
marginBottom: 10,
justifyContent: 'space-evenly',
},
badRequestContainer: {
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
invalidMessageText: {
color: 'black',
fontSize: 16,
textAlign: 'center',
marginBottom: 20,
},
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 10,
paddingHorizontal: 20,
},
modalContentContainer: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: 34,
borderBottomStartRadius: 0,
borderBottomEndRadius: 0,
borderWidth: 1,
width: '100%',
height: '50%',
position: 'absolute',
backgroundColor: 'white',
bottom: 0,
},
modalOuterContainer: { flex: 1 },
dappLogo: {
width: 50,
height: 50,
borderRadius: 8,
marginVertical: 16,
overflow: 'hidden',
},
space: {
width: 50,
},
flexRow: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 20,
paddingHorizontal: 16,
marginBottom: 10,
},
marginVertical8: {
marginVertical: 8,
textAlign: 'center',
},
subHeading: {
textAlign: 'center',
fontWeight: 'bold',
marginBottom: 10,
marginTop: 10,
},
centerText: {
textAlign: 'center',
},
messageBody: {
borderWidth: 1,
borderRadius: 6,
paddingVertical: 10,
paddingHorizontal: 10,
marginVertical: 3,
},
cameraContainer: {
justifyContent: 'center',
alignItems: 'center',
},
inputContainer: {
marginTop: 20,
},
camera: {
width: 400,
height: 400,
},
dappDetails: {
display: 'flex',
alignItems: 'center',
},
dataBoxContainer: {
marginBottom: 10,
},
dataBoxLabel: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 3,
color: 'black',
},
dataBox: {
borderWidth: 1,
borderColor: '#ccc',
padding: 10,
borderRadius: 5,
},
dataBoxData: {
fontSize: 16,
color: 'black',
},
transactionText: {
padding: 8,
fontSize: 18,
fontWeight: 'bold',
color: 'black',
},
balancePadding: {
padding: 8,
},
noActiveSessions: { display: 'flex', alignItems: 'center', marginTop: 12 },
disconnectSession: { display: 'flex', justifyContent: 'center' },
sessionItem: { paddingLeft: 12, borderBottomWidth: 0.5 },
sessionsContainer: { paddingLeft: 12, borderBottomWidth: 0.5 },
walletConnectUriText: { padding: 10 },
walletConnectLogo: { width: 24, height: 15, margin: 0 },
selectNetworkText: {
fontWeight: 'bold',
marginVertical: 10,
},
transactionFeesInput: { marginBottom: 10 },
approveTransaction: {
flexGrow: 1,
marginTop: 0,
paddingHorizontal: 24,
paddingVertical: 5,
},
transactionLabel: {
fontWeight: '700',
padding: 8,
},
});
export default styles;
+140
View File
@@ -0,0 +1,140 @@
import { PopulatedTransaction } from 'ethers';
// import { SignClientTypes, SessionTypes } from '@walletconnect/types';
// import { Web3WalletTypes } from '@walletconnect/web3wallet';
import { EncodeObject } from '@cosmjs/proto-signing';
export type StackParamsList = {
Laconic: undefined;
SignMessage: {
selectedNamespace: string;
selectedChainId: string;
accountInfo: Account;
};
SignRequest: {
namespace: string;
address: string;
message: string;
// requestEvent?: Web3WalletTypes.SessionRequest;
// requestSessionData?: SessionTypes.Struct;
requestEvent?: any;
requestSessionData?: any;
};
ApproveTransfer: {
transaction: PopulatedTransaction;
// requestEvent: Web3WalletTypes.SessionRequest;
// requestSessionData: SessionTypes.Struct;
requestEvent: any;
requestSessionData: any;
};
InvalidPath: undefined;
WalletConnect: undefined;
AddSession: undefined;
AddNetwork: undefined;
EditNetwork: {
selectedNetwork: NetworksDataState;
};
ApproveTransaction: {
transactionMessage: EncodeObject;
signer: string;
// requestEvent: Web3WalletTypes.SessionRequest;
// requestSessionData: SessionTypes.Struct;
requestEvent: any;
requestSessionData: any;
};
};
export type Account = {
index: number;
pubKey: string;
address: string;
hdPath: string;
};
export type NetworkDropdownProps = {
updateNetwork: (networksData: NetworksDataState) => void;
};
export type NetworksFormData = {
networkName: string;
rpcUrl: string;
chainId: string;
currencySymbol?: string;
blockExplorerUrl?: string;
namespace: string;
nativeDenom?: string;
addressPrefix?: string;
coinType?: string;
gasPrice?: string;
isDefault: boolean;
};
export interface NetworksDataState extends NetworksFormData {
networkId: string;
}
export type SignMessageParams = {
message: string;
namespace: string;
chainId: string;
accountId: number;
};
export type CreateWalletProps = {
isWalletCreating: boolean;
createWalletHandler: () => Promise<void>;
};
export type ResetDialogProps = {
title: string;
visible: boolean;
hideDialog: () => void;
onConfirm: () => void;
};
export type HDPathDialogProps = {
pathCode: string;
visible: boolean;
hideDialog: () => void;
updateAccounts: (account: Account) => void;
};
export type CustomDialogProps = {
visible: boolean;
hideDialog: () => void;
contentText: string;
titleText?: string;
};
export type GridViewProps = {
words: string[];
};
export type PathState = {
firstNumber: string;
secondNumber: string;
thirdNumber: string;
};
export interface PairingModalProps {
visible: boolean;
setModalVisible: (arg1: boolean) => void;
currentProposal:
// | SignClientTypes.EventArguments['session_proposal']
| undefined;
setCurrentProposal: (
arg1:
// | SignClientTypes.EventArguments['session_proposal']
| undefined,
) => void;
setToastVisible: (arg1: boolean) => void;
}
export interface WalletConnectContextProps {
// activeSessions: Record<string, SessionTypes.Struct>;
activeSessions: Record<string, any>;
setActiveSessions: (
// activeSessions: Record<string, SessionTypes.Struct>,
activeSessions: Record<string, any>,
) => void;
}
+346
View File
@@ -0,0 +1,346 @@
/* Importing this library provides react native with a secure random source.
For more information, "visit https://docs.ethers.org/v5/cookbook/react-native/#cookbook-reactnative-security" */
import 'react-native-get-random-values';
import '@ethersproject/shims';
import { utils } from 'ethers';
import { HDNode } from 'ethers/lib/utils';
import {
setInternetCredentials,
resetInternetCredentials,
getInternetCredentials,
} from './key-store';
import { Secp256k1HdWallet } from '@cosmjs/amino';
import { AccountData } from '@cosmjs/proto-signing';
import { stringToPath } from '@cosmjs/crypto';
import { Account, NetworksDataState, NetworksFormData } from '../types';
import {
getHDPath,
getPathKey,
resetKeyServers,
updateAccountIndices,
} from './misc';
import { COSMOS, EIP155 } from './constants';
const createWallet = async (
networksData: NetworksDataState[],
): Promise<string> => {
const mnemonic = utils.entropyToMnemonic(utils.randomBytes(16));
await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic);
const hdNode = HDNode.fromMnemonic(mnemonic);
for (const network of networksData) {
const hdPath = `m/44'/${network.coinType}'/0'/0/0`;
const node = hdNode.derivePath(hdPath);
let address;
switch (network.namespace) {
case EIP155:
address = node.address;
break;
case COSMOS:
address = (
await getCosmosAccounts(mnemonic, hdPath, network.addressPrefix)
).data.address;
break;
default:
throw new Error('Unsupported namespace');
}
const accountInfo = `${hdPath},${node.privateKey},${node.publicKey},${address}`;
await Promise.all([
setInternetCredentials(
`accounts/${network.namespace}:${network.chainId}/0`,
'_',
accountInfo,
),
setInternetCredentials(
`addAccountCounter/${network.namespace}:${network.chainId}`,
'_',
'1',
),
setInternetCredentials(
`accountIndices/${network.namespace}:${network.chainId}`,
'_',
'0',
),
]);
}
return mnemonic;
};
const addAccount = async (
networkData: NetworksDataState,
): Promise<Account | undefined> => {
try {
const namespaceChainId = `${networkData.namespace}:${networkData.chainId}`;
const id = await getNextAccountId(namespaceChainId);
const hdPath = getHDPath(namespaceChainId, `0'/0/${id}`);
const accounts = await addAccountFromHDPath(hdPath, networkData);
await updateAccountCounter(namespaceChainId, id);
return accounts;
} catch (error) {
console.error('Error creating account:', error);
}
};
const addAccountFromHDPath = async (
hdPath: string,
networkData: NetworksDataState,
): Promise<Account | undefined> => {
try {
const account = await accountInfoFromHDPath(hdPath, networkData);
if (!account) {
throw new Error('Error while creating account');
}
const { privKey, pubKey, address } = account;
const namespaceChainId = `${networkData.namespace}:${networkData.chainId}`;
const index = (await updateAccountIndices(namespaceChainId)).index;
await Promise.all([
setInternetCredentials(
`accounts/${namespaceChainId}/${index}`,
'_',
`${hdPath},${privKey},${pubKey},${address}`,
),
]);
return { index, pubKey, address, hdPath };
} catch (error) {
console.error(error);
}
};
const storeNetworkData = async (
networkData: NetworksFormData,
): Promise<NetworksDataState[]> => {
const networks = await getInternetCredentials('networks');
let retrievedNetworks = [];
if (networks) {
retrievedNetworks = JSON.parse(networks!);
}
let networkId = 0;
if (retrievedNetworks.length > 0) {
networkId = retrievedNetworks[retrievedNetworks.length - 1].networkId + 1;
}
const updatedNetworks: NetworksDataState[] = [
...retrievedNetworks,
{
...networkData,
networkId: String(networkId),
},
];
await setInternetCredentials(
'networks',
'_',
JSON.stringify(updatedNetworks),
);
return updatedNetworks;
};
const retrieveNetworksData = async (): Promise<NetworksDataState[]> => {
const networks = await getInternetCredentials('networks');
if(!networks){
return [];
}
const parsedNetworks: NetworksDataState[] = JSON.parse(networks);
return parsedNetworks;
};
export const retrieveAccountsForNetwork = async (
namespaceChainId: string,
accountsIndices: string,
): Promise<Account[]> => {
const accountsIndexArray = accountsIndices.split(',');
const loadedAccounts = await Promise.all(
accountsIndexArray.map(async i => {
const { address, path, pubKey } = await getPathKey(
namespaceChainId,
Number(i),
);
const account: Account = {
index: Number(i),
pubKey,
address,
hdPath: path,
};
return account;
}),
);
return loadedAccounts;
};
const retrieveAccounts = async (
currentNetworkData: NetworksDataState,
): Promise<Account[] | undefined> => {
const accountIndicesServer = await getInternetCredentials(
`accountIndices/${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
);
const accountIndices = accountIndicesServer;
if (!accountIndices) {
return;
}
const loadedAccounts = await retrieveAccountsForNetwork(
`${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
accountIndices,
)
return loadedAccounts;
};
const retrieveSingleAccount = async (
namespace: string,
chainId: string,
address: string,
) => {
let loadedAccounts;
const accountIndicesServer = await getInternetCredentials(
`accountIndices/${namespace}:${chainId}`,
);
const accountIndices = accountIndicesServer;
if (!accountIndices) {
throw new Error('Indices for given chain not found');
}
loadedAccounts = await retrieveAccountsForNetwork(
`${namespace}:${chainId}`,
accountIndices,
);
if (!loadedAccounts) {
throw new Error('Accounts for given chain not found');
}
return loadedAccounts.find(account => account.address === address);
};
const resetWallet = async () => {
try {
await Promise.all([
resetInternetCredentials('mnemonicServer'),
resetKeyServers(EIP155),
resetKeyServers(COSMOS),
setInternetCredentials('networks', '_', JSON.stringify([])),
]);
} catch (error) {
console.error('Error resetting wallet:', error);
throw error;
}
};
const accountInfoFromHDPath = async (
hdPath: string,
networkData: NetworksDataState,
): Promise<
{ privKey: string; pubKey: string; address: string } | undefined
> => {
const mnemonicStore = await getInternetCredentials('mnemonicServer');
if (!mnemonicStore) {
throw new Error('Mnemonic not found!');
}
const mnemonic = mnemonicStore;
const hdNode = HDNode.fromMnemonic(mnemonic);
const node = hdNode.derivePath(hdPath);
const privKey = node.privateKey;
const pubKey = node.publicKey;
let address: string;
switch (networkData.namespace) {
case EIP155:
address = node.address;
break;
case COSMOS:
address = (
await getCosmosAccounts(mnemonic, hdPath, networkData.addressPrefix)
).data.address;
break;
default:
throw new Error('Invalid wallet type');
}
return { privKey, pubKey, address };
};
const getNextAccountId = async (namespaceChainId: string): Promise<number> => {
const idStore = await getInternetCredentials(
`addAccountCounter/${namespaceChainId}`,
);
if (!idStore) {
throw new Error('Account id not found');
}
const accountCounter = idStore;
const nextCounter = Number(accountCounter);
return nextCounter;
};
const updateAccountCounter = async (
namespaceChainId: string,
id: number,
): Promise<void> => {
const idStore = await getInternetCredentials(
`addAccountCounter/${namespaceChainId}`,
);
if (!idStore) {
throw new Error('Account id not found');
}
const updatedCounter = String(id + 1);
await resetInternetCredentials(`addAccountCounter/${namespaceChainId}`);
await setInternetCredentials(
`addAccountCounter/${namespaceChainId}`,
'_',
updatedCounter,
);
};
const getCosmosAccounts = async (
mnemonic: string,
path: string,
prefix: string = COSMOS,
): Promise<{ cosmosWallet: Secp256k1HdWallet; data: AccountData }> => {
const cosmosWallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, {
hdPaths: [stringToPath(path)],
prefix,
});
const accountsData = await cosmosWallet.getAccounts();
const data = accountsData[0];
return { cosmosWallet, data };
};
export {
createWallet,
addAccount,
addAccountFromHDPath,
storeNetworkData,
retrieveNetworksData,
retrieveAccounts,
retrieveSingleAccount,
resetWallet,
accountInfoFromHDPath,
getNextAccountId,
updateAccountCounter,
getCosmosAccounts,
};
+36
View File
@@ -0,0 +1,36 @@
import { COSMOS_TESTNET_CHAINS } from './wallet-connect/COSMOSData';
import { EIP155_CHAINS } from './wallet-connect/EIP155Data';
export const EIP155 = 'eip155';
export const COSMOS = 'cosmos';
export const DEFAULT_NETWORKS = [
{
chainId: '1',
networkName: EIP155_CHAINS['eip155:1'].name,
namespace: EIP155,
rpcUrl: EIP155_CHAINS['eip155:1'].rpc,
blockExplorerUrl: '',
currencySymbol: 'ETH',
coinType: '60',
isDefault: true,
},
{
chainId: 'theta-testnet-001',
networkName: COSMOS_TESTNET_CHAINS['cosmos:theta-testnet-001'].name,
namespace: COSMOS,
rpcUrl: COSMOS_TESTNET_CHAINS['cosmos:theta-testnet-001'].rpc,
blockExplorerUrl: '',
nativeDenom: 'uatom',
addressPrefix: 'cosmos',
coinType: '118',
gasPrice: '0.025',
isDefault: true,
},
];
export const CHAINID_DEBOUNCE_DELAY = 250;
export const EMPTY_FIELD_ERROR = 'Field cannot be empty';
export const INVALID_URL_ERROR = 'Invalid URL';
export const IS_NUMBER_REGEX = /^\d+$/;
+17
View File
@@ -0,0 +1,17 @@
const setInternetCredentials = (name:string, username:string, password:string) => {
localStorage.setItem(name, password);
};
const getInternetCredentials = (name:string) : string | null => {
return localStorage.getItem(name);
};
const resetInternetCredentials = (name:string) => {
localStorage.removeItem(name);
};
export {
setInternetCredentials,
getInternetCredentials,
resetInternetCredentials
}
+158
View File
@@ -0,0 +1,158 @@
/* Importing this library provides react native with a secure random source.
For more information, "visit https://docs.ethers.org/v5/cookbook/react-native/#cookbook-reactnative-security" */
import 'react-native-get-random-values';
import '@ethersproject/shims';
import {
getInternetCredentials,
resetInternetCredentials,
setInternetCredentials,
} from './key-store';
import { AccountData } from '@cosmjs/amino';
import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
import { stringToPath } from '@cosmjs/crypto';
import { EIP155 } from './constants';
import { NetworksDataState } from '../types';
const getMnemonic = async (): Promise<string> => {
const mnemonicStore = await getInternetCredentials('mnemonicServer');
if (!mnemonicStore) {
throw new Error('Mnemonic not found!');
}
const mnemonic = mnemonicStore;
return mnemonic;
};
const getHDPath = (namespaceChainId: string, path: string): string => {
const namespace = namespaceChainId.split(':')[0];
return namespace === EIP155 ? `m/44'/60'/${path}` : `m/44'/118'/${path}`;
};
export const getDirectWallet = async (
mnemonic: string,
path: string,
): Promise<{ directWallet: DirectSecp256k1HdWallet; data: AccountData }> => {
const directWallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
hdPaths: [stringToPath(`m/44'/118'/${path}`)],
});
const accountsData = await directWallet.getAccounts();
const data = accountsData[0];
return { directWallet, data };
};
const getPathKey = async (
namespaceChainId: string,
accountId: number,
): Promise<{
path: string;
privKey: string;
pubKey: string;
address: string;
}> => {
const pathKeyStore = await getInternetCredentials(
`accounts/${namespaceChainId}/${accountId}`,
);
if (!pathKeyStore) {
throw new Error('Error while fetching counter');
}
const pathKeyVal = pathKeyStore;
const pathkey = pathKeyVal.split(',');
const path = pathkey[0];
const privKey = pathkey[1];
const pubKey = pathkey[2];
const address = pathkey[3];
return { path, privKey, pubKey, address };
};
const getAccountIndices = async (
namespaceChainId: string,
): Promise<{
accountIndices: string;
indices: number[];
index: number;
}> => {
const counterStore = await getInternetCredentials(
`accountIndices/${namespaceChainId}`,
);
if (!counterStore) {
throw new Error('Error while fetching counter');
}
let accountIndices = counterStore;
const indices = accountIndices.split(',').map(Number);
const index = indices[indices.length - 1] + 1;
return { accountIndices, indices, index };
};
const updateAccountIndices = async (
namespaceChainId: string,
): Promise<{ accountIndices: string; index: number }> => {
const accountIndicesData = await getAccountIndices(namespaceChainId);
const accountIndices = accountIndicesData.accountIndices;
const index = accountIndicesData.index;
const updatedAccountIndices = `${accountIndices},${index.toString()}`;
await resetInternetCredentials(`accountIndices/${namespaceChainId}`);
await setInternetCredentials(
`accountIndices/${namespaceChainId}`,
'_',
updatedAccountIndices,
);
return { accountIndices: updatedAccountIndices, index };
};
const resetKeyServers = async (namespace: string) => {
const networksServer = await getInternetCredentials('networks');
if (!networksServer) {
throw new Error('Networks not found.');
}
const networksData: NetworksDataState[] = JSON.parse(networksServer);
const filteredNetworks = networksData.filter(
(network: any) => network.namespace === namespace,
);
if (filteredNetworks.length === 0) {
throw new Error(`No networks found for namespace ${namespace}.`);
}
filteredNetworks.forEach(async (network: any) => {
const { chainId } = network;
const namespaceChainId = `${namespace}:${chainId}`;
const idStore = await getInternetCredentials(
`accountIndices/${namespaceChainId}`,
);
if (!idStore) {
throw new Error(`Account indices not found for ${namespaceChainId}.`);
}
const accountIds = idStore;
const ids = accountIds.split(',').map(Number);
const latestId = Math.max(...ids);
for (let i = 0; i <= latestId; i++) {
await resetInternetCredentials(`accounts/${namespaceChainId}/${i}`);
}
await resetInternetCredentials(`addAccountCounter/${namespaceChainId}`);
await resetInternetCredentials(`accountIndices/${namespaceChainId}`);
});
};
export {
getMnemonic,
getPathKey,
updateAccountIndices,
getHDPath,
resetKeyServers,
};
+111
View File
@@ -0,0 +1,111 @@
/* Importing this library provides react native with a secure random source.
For more information, "visit https://docs.ethers.org/v5/cookbook/react-native/#cookbook-reactnative-security" */
import 'react-native-get-random-values';
import '@ethersproject/shims';
import { Wallet } from 'ethers';
import { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx';
import { SignMessageParams } from '../types';
import { getDirectWallet, getMnemonic, getPathKey } from './misc';
import { getCosmosAccounts } from './accounts';
import { COSMOS, EIP155 } from './constants';
const signMessage = async ({
message,
namespace,
chainId,
accountId,
}: SignMessageParams): Promise<string | undefined> => {
const path = await getPathKey(`${namespace}:${chainId}`, accountId);
switch (namespace) {
case EIP155:
return await signEthMessage(message, accountId, chainId);
case COSMOS:
return await signCosmosMessage(message, path.path);
default:
throw new Error('Invalid wallet type');
}
};
const signEthMessage = async (
message: string,
accountId: number,
chainId: string,
): Promise<string | undefined> => {
try {
const privKey = (await getPathKey(`${EIP155}:${chainId}`, accountId))
.privKey;
const wallet = new Wallet(privKey);
const signature = await wallet.signMessage(message);
return signature;
} catch (error) {
console.error('Error signing Ethereum message:', error);
throw error;
}
};
const signCosmosMessage = async (
message: string,
path: string,
): Promise<string | undefined> => {
try {
const mnemonic = await getMnemonic();
const cosmosAccount = await getCosmosAccounts(mnemonic, path);
const address = cosmosAccount.data.address;
const cosmosSignature = await cosmosAccount.cosmosWallet.signAmino(
address,
{
chain_id: '',
account_number: '0',
sequence: '0',
fee: {
gas: '0',
amount: [],
},
msgs: [
{
type: 'sign/MsgSignData',
value: {
signer: address,
data: btoa(message),
},
},
],
memo: '',
},
);
return cosmosSignature.signature.signature;
} catch (error) {
console.error('Error signing Cosmos message:', error);
throw error;
}
};
const signDirectMessage = async (
namespaceChainId: string,
accountId: number,
signDoc: SignDoc,
): Promise<string | undefined> => {
try {
const path = (await getPathKey(namespaceChainId, accountId)).path;
const mnemonic = await getMnemonic();
const { directWallet, data } = await getDirectWallet(mnemonic, path);
const directSignature = await directWallet.signDirect(
data.address,
signDoc,
);
return directSignature.signature.signature;
} catch (error) {
console.error('Error signing Cosmos message:', error);
throw error;
}
};
export { signMessage, signEthMessage, signCosmosMessage, signDirectMessage };
+42
View File
@@ -0,0 +1,42 @@
// Taken from https://github.com/WalletConnect/web-examples/blob/main/advanced/wallets/react-wallet-v2/src/data/COSMOSData.ts
/**
* Types
*/
export type TCosmosChain = keyof typeof COSMOS_TESTNET_CHAINS;
/**
* Chains
*/
// Added for pay.laconic.com
export const COSMOS_TESTNET_CHAINS: Record<
string,
{
chainId: string;
name: string;
rpc: string;
namespace: string;
}
> = {
'cosmos:theta-testnet-001': {
chainId: 'theta-testnet-001',
name: 'Cosmos Hub Testnet',
rpc: 'https://rpc-t.cosmos.nodestake.top',
namespace: 'cosmos',
},
};
/**
* Methods
*/
export const COSMOS_SIGNING_METHODS = {
COSMOS_SIGN_DIRECT: 'cosmos_signDirect',
COSMOS_SIGN_AMINO: 'cosmos_signAmino',
};
export const COSMOS_METHODS = {
...COSMOS_SIGNING_METHODS,
COSMOS_SEND_TOKENS: 'cosmos_sendTokens', // Added for pay.laconic.com
COSMOS_SEND_TRANSACTION: 'cosmos_sendTransaction', // Added for testnet onboarding app
};
+43
View File
@@ -0,0 +1,43 @@
/**
* @desc Refference list of eip155 chains
* @url https://chainlist.org
*/
// Taken from https://github.com/WalletConnect/web-examples/blob/main/advanced/wallets/react-wallet-v2/src/data/EIP155Data.ts
/**
* Types
*/
export type TEIP155Chain = keyof typeof EIP155_CHAINS;
export type EIP155Chain = {
chainId: number;
name: string;
logo: string;
rgb: string;
rpc: string;
namespace: string;
smartAccountEnabled?: boolean;
};
/**
* Chains
*/
export const EIP155_CHAINS: Record<string, EIP155Chain> = {
'eip155:1': {
chainId: 1,
name: 'Ethereum',
logo: '/chain-logos/eip155-1.png',
rgb: '99, 125, 234',
rpc: 'https://cloudflare-eth.com/',
namespace: 'eip155',
},
};
/**
* Methods
*/
export const EIP155_SIGNING_METHODS = {
PERSONAL_SIGN: 'personal_sign',
ETH_SEND_TRANSACTION: 'eth_sendTransaction',
};
@@ -0,0 +1,36 @@
import Config from 'react-native-config';
// import '@walletconnect/react-native-compat';
import '@ethersproject/shims';
// import { Core } from '@walletconnect/core';
// import { ICore } from '@walletconnect/types';
// import { Web3Wallet, IWeb3Wallet } from '@walletconnect/web3wallet';
export let web3wallet:
// | IWeb3Wallet
| any;
export let core:
// | ICore
| any;
export async function createWeb3Wallet() {
// core = new Core({
// projectId: Config.WALLET_CONNECT_PROJECT_ID,
// });
// web3wallet = await Web3Wallet.init({
// core,
// metadata: {
// name: 'Laconic Wallet',
// description: 'Laconic Wallet',
// url: 'https://wallet.laconic.com/',
// icons: ['https://avatars.githubusercontent.com/u/92608123'],
// },
// });
}
export async function web3WalletPair(params: { uri: string }) {
if (web3wallet) {
// return await web3wallet.core.pairing.pair({ uri: params.uri });
}
}
+4
View File
@@ -0,0 +1,4 @@
export const NETWORK_METHODS = {
GET_NETWORKS: 'getNetworks',
CHANGE_NETWORK: 'changeNetwork',
};
+220
View File
@@ -0,0 +1,220 @@
// Taken from https://medium.com/walletconnect/how-to-build-a-wallet-in-react-native-with-the-web3wallet-sdk-b6f57bf02f9a
import { utils } from 'ethers';
// import { ProposalTypes } from '@walletconnect/types';
import { Account, NetworksDataState } from '../../types';
import { EIP155_SIGNING_METHODS } from './EIP155Data';
import { mergeWith } from 'lodash';
import { retrieveAccounts } from '../accounts';
import { COSMOS, EIP155 } from '../constants';
import { NETWORK_METHODS } from './common-data';
import { COSMOS_METHODS } from './COSMOSData';
/**
* Converts hex to utf8 string if it is valid bytes
*/
export function convertHexToUtf8(value: string) {
if (utils.isHexString(value)) {
return utils.toUtf8String(value);
}
return value;
}
/**
* Gets message from various signing request methods by filtering out
* a value that is not an address (thus is a message).
* If it is a hex string, it gets converted to utf8 string
*/
export function getSignParamsMessage(params: string[]) {
const message = params.filter(p => !utils.isAddress(p))[0];
return convertHexToUtf8(message);
}
export const getNamespaces = async (
// optionalNamespaces: ProposalTypes.OptionalNamespaces,
// requiredNamespaces: ProposalTypes.RequiredNamespaces,
optionalNamespaces: any,
requiredNamespaces: any,
networksData: NetworksDataState[],
selectedNetwork: NetworksDataState,
accounts: Account[],
currentIndex: number,
) => {
const namespaceChainId = `${selectedNetwork.namespace}:${selectedNetwork.chainId}`;
const combinedNamespaces = mergeWith(
requiredNamespaces,
optionalNamespaces,
(obj, src) =>
Array.isArray(obj) && Array.isArray(src) ? [...src, ...obj] : undefined,
);
const walletConnectChains: string[] = [];
Object.keys(combinedNamespaces).forEach(key => {
const { chains } = combinedNamespaces[key];
chains && walletConnectChains.push(...chains);
});
// If combinedNamespaces is not empty, send back namespaces object based on requested chains
// Else send back namespaces object using currently selected network
if (Object.keys(combinedNamespaces).length > 0) {
if (!(walletConnectChains.length > 0)) {
return;
}
// Check for unsupported chains
const networkChains = networksData.map(
network => `${network.namespace}:${network.chainId}`,
);
if (!walletConnectChains.every(chain => networkChains.includes(chain))) {
const unsupportedChains = walletConnectChains.filter(
chain => !networkChains.includes(chain),
);
throw new Error(`Unsupported chains : ${unsupportedChains.join(',')}`);
}
// Get required networks
const requiredNetworks = networksData.filter(network =>
walletConnectChains.includes(`${network.namespace}:${network.chainId}`),
);
// Get accounts for required networks
const requiredAddressesPromise = requiredNetworks.map(
async requiredNetwork => {
const retrievedAccounts = await retrieveAccounts(requiredNetwork);
if (!retrievedAccounts) {
throw new Error('Accounts for given network not found');
}
const addresses = retrievedAccounts.map(
retrieveAccount =>
`${requiredNetwork.namespace}:${requiredNetwork.chainId}:${retrieveAccount.address}`,
);
return addresses;
},
);
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: {
chains: walletConnectChains.filter(chain => chain.includes(EIP155)),
// TODO: Debug optional namespace methods and events being required for approval
methods: [
...Object.values(EIP155_SIGNING_METHODS),
...Object.values(NETWORK_METHODS),
...(optionalNamespaces.eip155?.methods ?? []),
...(requiredNamespaces.eip155?.methods ?? []),
],
events: [
...(optionalNamespaces.eip155?.events ?? []),
...(requiredNamespaces.eip155?.events ?? []),
],
accounts: sortedAccounts.filter(account => account.includes(EIP155)),
},
cosmos: {
chains: walletConnectChains.filter(chain => chain.includes(COSMOS)),
methods: [
...Object.values(COSMOS_METHODS),
...Object.values(NETWORK_METHODS),
...(optionalNamespaces.cosmos?.methods ?? []),
...(requiredNamespaces.cosmos?.methods ?? []),
],
events: [
...(optionalNamespaces.cosmos?.events ?? []),
...(requiredNamespaces.cosmos?.events ?? []),
],
accounts: sortedAccounts.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 {
eip155: {
chains: [namespaceChainId],
// TODO: Debug optional namespace methods and events being required for approval
methods: [
...Object.values(EIP155_SIGNING_METHODS),
...Object.values(NETWORK_METHODS),
...(optionalNamespaces.eip155?.methods ?? []),
...(requiredNamespaces.eip155?.methods ?? []),
],
events: [
...(optionalNamespaces.eip155?.events ?? []),
...(requiredNamespaces.eip155?.events ?? []),
],
accounts: sortedAccounts.map(ethAccount => {
return `${namespaceChainId}:${ethAccount.address}`;
}),
},
cosmos: {
chains: [],
methods: [],
events: [],
accounts: [],
},
};
case COSMOS:
return {
cosmos: {
chains: [namespaceChainId],
methods: [
...Object.values(COSMOS_METHODS),
...Object.values(NETWORK_METHODS),
...(optionalNamespaces.cosmos?.methods ?? []),
...(requiredNamespaces.cosmos?.methods ?? []),
],
events: [
...(optionalNamespaces.cosmos?.events ?? []),
...(requiredNamespaces.cosmos?.events ?? []),
],
accounts: sortedAccounts.map(cosmosAccount => {
return `${namespaceChainId}:${cosmosAccount.address}`;
}),
},
eip155: {
chains: [],
methods: [],
events: [],
accounts: [],
},
};
default:
break;
}
}
};
@@ -0,0 +1,221 @@
// Taken from https://medium.com/walletconnect/how-to-build-a-wallet-in-react-native-with-the-web3wallet-sdk-b6f57bf02f9a
import { BigNumber, Wallet, providers } from 'ethers';
import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils';
// import { SignClientTypes } from '@walletconnect/types';
// import { getSdkError } from '@walletconnect/utils';
import {
SigningStargateClient,
StdFee,
MsgSendEncodeObject,
} from '@cosmjs/stargate';
import { EncodeObject } from '@cosmjs/proto-signing';
import { LaconicClient } from '@cerc-io/registry-sdk';
import { EIP155_SIGNING_METHODS } from './EIP155Data';
import { signDirectMessage, signEthMessage } from '../sign-message';
import { Account } from '../../types';
import { getMnemonic, getPathKey } from '../misc';
import { getCosmosAccounts } from '../accounts';
import { COSMOS_METHODS } from './COSMOSData';
interface EthSendTransaction {
type: 'eth_sendTransaction';
provider: providers.JsonRpcProvider;
ethGasLimit: BigNumber;
ethGasPrice: string | null;
maxPriorityFeePerGas: BigNumber | null;
maxFeePerGas: BigNumber | null;
}
interface SignMessage {
message: string;
}
interface EthPersonalSign extends SignMessage {
type: 'personal_sign';
}
interface CosmosSignDirect extends SignMessage {
type: 'cosmos_signDirect';
}
interface CosmosSignAmino extends SignMessage {
type: 'cosmos_signAmino';
}
interface CosmosSendTokens {
type: 'cosmos_sendTokens';
signingStargateClient: SigningStargateClient;
cosmosFee: StdFee;
sendMsg: MsgSendEncodeObject;
memo: string;
}
interface CosmosSendTransaction {
type: 'cosmos_sendTransaction';
LaconicClient: LaconicClient;
cosmosFee: StdFee;
txMsg: EncodeObject;
}
export type WalletConnectRequests =
| EthSendTransaction
| EthPersonalSign
| CosmosSignDirect
| CosmosSignAmino
| CosmosSendTokens
| CosmosSendTransaction;
export async function approveWalletConnectRequest(
// requestEvent: SignClientTypes.EventArguments['session_request'],
requestEvent: any,
account: Account,
namespace: string,
chainId: string,
options: WalletConnectRequests,
) {
const { params, id } = requestEvent;
const { request } = params;
const path = (await getPathKey(`${namespace}:${chainId}`, account.index))
.path;
const mnemonic = await getMnemonic();
const cosmosAccount = await getCosmosAccounts(mnemonic, path);
const address = account.address;
switch (request.method) {
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
if (!(options.type === 'eth_sendTransaction')) {
throw new Error('Incorrect parameters passed');
}
const privKey = (
await getPathKey(`${namespace}:${chainId}`, account.index)
).privKey;
const wallet = new Wallet(privKey);
const sendTransaction = request.params[0];
const updatedTransaction =
options.maxFeePerGas && options.maxPriorityFeePerGas
? {
...sendTransaction,
gasLimit: options.ethGasLimit,
maxFeePerGas: options.maxFeePerGas,
maxPriorityFeePerGas: options.maxPriorityFeePerGas,
type: 2,
}
: {
...sendTransaction,
gasLimit: options.ethGasLimit,
gasPrice: options.ethGasPrice,
type: 0,
};
const connectedWallet = wallet.connect(options.provider);
const hash = await connectedWallet.sendTransaction(updatedTransaction);
const receipt = typeof hash === 'string' ? hash : hash?.hash;
return formatJsonRpcResult(id, {
signature: receipt,
});
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
if (!(options.type === 'personal_sign')) {
throw new Error('Incorrect parameters passed');
}
const ethSignature = await signEthMessage(
options.message,
account.index,
chainId,
);
return formatJsonRpcResult(id, ethSignature);
case COSMOS_METHODS.COSMOS_SIGN_DIRECT:
// Reference: https://github.com/confio/cosmjs-types/blob/66e52711914fccd2a9d1a03e392d3628fdf499e2/src/cosmos/tx/v1beta1/tx.ts#L51
// According above doc, in the signDoc interface 'bodyBytes' and 'authInfoBytes' have Uint8Array type
if (!(options.type === 'cosmos_signDirect')) {
throw new Error('Incorrect parameters passed');
}
const bodyBytesArray = Uint8Array.from(
Buffer.from(request.params.signDoc.bodyBytes, 'hex'),
);
const authInfoBytesArray = Uint8Array.from(
Buffer.from(request.params.signDoc.authInfoBytes, 'hex'),
);
const cosmosDirectSignature = await signDirectMessage(
`${namespace}:${chainId}`,
account.index,
{
...request.params.signDoc,
bodyBytes: bodyBytesArray,
authInfoBytes: authInfoBytesArray,
},
);
return formatJsonRpcResult(id, {
signature: cosmosDirectSignature,
});
case COSMOS_METHODS.COSMOS_SIGN_AMINO:
if (!(options.type === 'cosmos_signAmino')) {
throw new Error('Incorrect parameters passed');
}
const cosmosAminoSignature = await cosmosAccount.cosmosWallet.signAmino(
address,
request.params.signDoc,
);
if (!cosmosAminoSignature) {
throw new Error('Error signing message');
}
return formatJsonRpcResult(id, {
signature: cosmosAminoSignature.signature.signature,
});
case COSMOS_METHODS.COSMOS_SEND_TOKENS:
if (!(options.type === 'cosmos_sendTokens')) {
throw new Error('Incorrect parameters passed');
}
const result = await options.signingStargateClient.signAndBroadcast(
address,
[options.sendMsg],
options.cosmosFee,
options.memo,
);
return formatJsonRpcResult(id, {
signature: result.transactionHash,
});
case COSMOS_METHODS.COSMOS_SEND_TRANSACTION:
if (!(options.type === 'cosmos_sendTransaction')) {
throw new Error('Incorrect parameters passed');
}
const resultFromTx = await options.LaconicClient.signAndBroadcast(
address,
[options.txMsg],
options.cosmosFee,
);
return formatJsonRpcResult(id, {
code: resultFromTx.code,
});
default:
// throw new Error(getSdkError('INVALID_METHOD').message);
}
}
export function rejectWalletConnectRequest(
// request: SignClientTypes.EventArguments['session_request'],
request: any,
) {
// const { id } = request;
// return formatJsonRpcError(id, getSdkError('USER_REJECTED_METHODS').message);
}