Integrate wallet-connect and add functionality to configure networks #3
4
.env.example
Normal file
4
.env.example
Normal file
@ -0,0 +1,4 @@
|
||||
WALLET_CONNECT_PROJECT_ID=
|
||||
DEFAULT_GAS_PRICE=
|
||||
# Reference: https://github.com/cosmos/cosmos-sdk/issues/16020
|
||||
DEFAULT_GAS_ADJUSTMENT=2
|
||||
@ -1,4 +1,12 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native',
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -64,3 +64,5 @@ yarn-error.log
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
.env
|
||||
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@ -0,0 +1 @@
|
||||
yarn lint
|
||||
66
App.tsx
66
App.tsx
@ -1,66 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
|
||||
import SignMessage from './components/SignMessage';
|
||||
import HomeScreen from './components/HomeScreen';
|
||||
import SignRequest from './components/SignRequest';
|
||||
import InvalidPath from './components/InvalidPath';
|
||||
|
||||
import { StackParamsList } from './types';
|
||||
|
||||
const Stack = createNativeStackNavigator<StackParamsList>();
|
||||
|
||||
const App = (): React.JSX.Element => {
|
||||
const linking = {
|
||||
prefixes: ['https://www.laconic-wallet.com'],
|
||||
config: {
|
||||
screens: {
|
||||
SignRequest: {
|
||||
path: 'sign/:network/:address/:message',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationContainer linking={linking}>
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen
|
||||
name="Laconic"
|
||||
component={HomeScreen}
|
||||
options={{
|
||||
title: 'Laconic Wallet',
|
||||
headerBackVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="SignMessage"
|
||||
component={SignMessage}
|
||||
options={{
|
||||
title: 'Sign Message',
|
||||
}}
|
||||
initialParams={{ selectedNetwork: 'Ethereum' }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="SignRequest"
|
||||
component={SignRequest}
|
||||
options={{
|
||||
title: 'Sign Message?',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="InvalidPath"
|
||||
component={InvalidPath}
|
||||
options={{
|
||||
title: 'Bad Request',
|
||||
headerBackVisible: false,
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
73
README.md
73
README.md
@ -1,6 +1,6 @@
|
||||
# laconic-wallet
|
||||
|
||||
### Install
|
||||
## Install
|
||||
|
||||
- Install [Node](https://nodejs.org/en/download/package-manager/)
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
echo $ANDROID_HOME
|
||||
```
|
||||
|
||||
- and that the appropriate directories have been added to your path by running
|
||||
- Check that the appropriate directories have been added to your path by running
|
||||
|
||||
```bash
|
||||
echo $PATH
|
||||
@ -63,53 +63,66 @@
|
||||
|
||||
## Setup for laconic-wallet
|
||||
|
||||
1. Clone this repository:
|
||||
1. Clone the repository
|
||||
|
||||
```
|
||||
git clone git@git.vdb.to:cerc-io/laconic-wallet.git
|
||||
```
|
||||
|
||||
2. Enter the project directory:
|
||||
2. Enter the project directory
|
||||
|
||||
```
|
||||
cd laconic-wallet
|
||||
```
|
||||
|
||||
3. Install the dependencies:
|
||||
3. Install dependencies
|
||||
|
||||
```
|
||||
yarn
|
||||
```
|
||||
|
||||
4. Set up the Android device:
|
||||
4. Setup .env
|
||||
- Copy and update [`.env`](./.env)
|
||||
|
||||
- For a physical device, refer to the [React Native documentation for running on a physical device]("https://reactnative.dev/docs/running-on-device)
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
- For a virtual device, continue with the steps.
|
||||
- In the `.env` file add your WalletConnect project id. You can generate your own ProjectId at https://cloud.walletconnect.com
|
||||
|
||||
5. Start the application
|
||||
```
|
||||
WALLET_CONNECT_PROJECT_ID=39bc93c...
|
||||
```
|
||||
|
||||
5. Set up the Android device
|
||||
|
||||
- For a physical device, refer to the [React Native documentation for running on a physical device](https://reactnative.dev/docs/running-on-device)
|
||||
|
||||
- For a virtual device, continue with the steps
|
||||
|
||||
6. Start the application
|
||||
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
6. Press `a` to run the application on android
|
||||
7. Press `a` to run the application on android
|
||||
|
||||
## Setup for example-signer-app
|
||||
## Setup for signature-requester-app
|
||||
|
||||
1. Clone the repository:
|
||||
1. Clone the repository
|
||||
|
||||
```
|
||||
git clone git@git.vdb.to:cerc-io/signature-requester-app.git
|
||||
```
|
||||
|
||||
2. Enter the project directory:
|
||||
2. Enter the project directory
|
||||
|
||||
```
|
||||
cd example-signer-app
|
||||
cd signature-requester-app
|
||||
```
|
||||
|
||||
3. Install the dependancies
|
||||
3. Install dependencies
|
||||
```
|
||||
yarn
|
||||
```
|
||||
@ -120,4 +133,32 @@
|
||||
```
|
||||
5. Press `a` to run the application on android
|
||||
|
||||
You should see both the apps running on your emulator or physical device.
|
||||
You should see both the apps running on your emulator or physical device
|
||||
|
||||
## Flow for the app
|
||||
|
||||
- User scans QR Code on dApp from wallet to connect
|
||||
|
||||
- After clicking on pair button, dApp emits an event 'session_proposal'
|
||||
|
||||
- Wallet listens to this event and opens a modal to either accept or reject the proposal
|
||||
|
||||
- Modal shows information about methods, chains and events the dApp is requesting for
|
||||
|
||||
- This information is taken from [namespaces](https://docs.walletconnect.com/advanced/glossary#namespaces) object received from dApp
|
||||
|
||||
- On accepting, wallet sends the [namespaces](https://docs.walletconnect.com/advanced/glossary#namespaces) object with information about the accounts that are present in the wallet in response
|
||||
|
||||
- Once the [session](https://docs.walletconnect.com/advanced/glossary#session) is established, it is shown on the active sessions page
|
||||
|
||||
- The wallet can be connected to multiple dApps
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- To clean the build
|
||||
|
||||
```
|
||||
cd android
|
||||
|
||||
./gradlew clean
|
||||
```
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
|
||||
import 'react-native';
|
||||
import React from 'react';
|
||||
import App from '../App';
|
||||
import App from '../src/App';
|
||||
|
||||
// Note: import explicitly to use the types shipped with jest.
|
||||
import {it} from '@jest/globals';
|
||||
import { it } from '@jest/globals';
|
||||
|
||||
// Note: test renderer must be required after react-native.
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
apply plugin: "com.android.application"
|
||||
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
|
||||
apply plugin: "org.jetbrains.kotlin.android"
|
||||
apply plugin: "com.facebook.react"
|
||||
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")
|
||||
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")
|
||||
|
||||
/**
|
||||
* This is the configuration block to customize your React Native Android app.
|
||||
@ -75,7 +76,7 @@ android {
|
||||
packagingOptions {
|
||||
pickFirst '**/libcrypto.so'
|
||||
}
|
||||
|
||||
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.laconic.wallet">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
@ -25,9 +26,9 @@
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data
|
||||
android:host="www.laconic-wallet.com"
|
||||
android:host="wallet.laconic.com"
|
||||
android:pathPrefix="/sign"
|
||||
android:scheme="https" />
|
||||
android:scheme="https"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
@ -39,3 +39,5 @@ newArchEnabled=false
|
||||
# Use this property to enable or disable the Hermes JS engine.
|
||||
# If set to false, you will be using JSC instead.
|
||||
hermesEnabled=true
|
||||
|
||||
VisionCamera_enableCodeScanner=true
|
||||
|
||||
@ -1,123 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ScrollView, TouchableOpacity, View } from 'react-native';
|
||||
import { Button, List, Text, useTheme } from 'react-native-paper';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
|
||||
import { AccountsProps, StackParamsList, Account } from '../types';
|
||||
import { addAccount } from '../utils/accounts';
|
||||
import styles from '../styles/stylesheet';
|
||||
import HDPathDialog from './HDPathDialog';
|
||||
import AccountDetails from './AccountDetails';
|
||||
|
||||
const Accounts = ({
|
||||
network,
|
||||
accounts,
|
||||
updateAccounts,
|
||||
currentIndex,
|
||||
updateIndex: updateId,
|
||||
}: AccountsProps) => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [isAccountCreating, setIsAccountCreating] = useState(false);
|
||||
const [hdDialog, setHdDialog] = useState(false);
|
||||
const [pathCode, setPathCode] = useState('');
|
||||
const theme = useTheme();
|
||||
|
||||
const handlePress = () => setExpanded(!expanded);
|
||||
|
||||
const addAccountHandler = async () => {
|
||||
setIsAccountCreating(true);
|
||||
const newAccount = await addAccount(network);
|
||||
setIsAccountCreating(false);
|
||||
if (newAccount) {
|
||||
updateAccounts(newAccount);
|
||||
updateId(newAccount.counterId);
|
||||
}
|
||||
};
|
||||
|
||||
const selectedAccounts: Account[] = (() => {
|
||||
switch (network) {
|
||||
case 'eth':
|
||||
return accounts.ethAccounts;
|
||||
case 'cosmos':
|
||||
return accounts.cosmosAccounts;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
})();
|
||||
|
||||
const renderAccountItems = () =>
|
||||
selectedAccounts.map(account => (
|
||||
<List.Item
|
||||
key={account.counterId}
|
||||
title={`Account ${account.counterId + 1}`}
|
||||
onPress={() => {
|
||||
updateId(account.counterId);
|
||||
setExpanded(false);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<View>
|
||||
<HDPathDialog
|
||||
visible={hdDialog}
|
||||
hideDialog={() => setHdDialog(false)}
|
||||
updateAccounts={updateAccounts}
|
||||
updateIndex={updateId}
|
||||
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}>
|
||||
{isAccountCreating ? 'Adding' : 'Add Account'}
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View style={styles.addAccountButton}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
setHdDialog(true);
|
||||
setPathCode(network === 'eth' ? "m/44'/60'/" : "m/44'/118'/");
|
||||
}}>
|
||||
Add Account from HD path
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<AccountDetails account={selectedAccounts[currentIndex]} />
|
||||
|
||||
<View style={styles.signLink}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
navigation.navigate('SignMessage', {
|
||||
selectedNetwork: network,
|
||||
accountInfo: selectedAccounts[currentIndex],
|
||||
});
|
||||
}}>
|
||||
<Text
|
||||
variant="titleSmall"
|
||||
style={[styles.hyperlink, { color: theme.colors.primary }]}>
|
||||
Sign Message
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default Accounts;
|
||||
@ -1,163 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, ActivityIndicator } from 'react-native';
|
||||
import { Button, Text } from 'react-native-paper';
|
||||
|
||||
import { createWallet, resetWallet, retrieveAccounts } from '../utils/accounts';
|
||||
import { DialogComponent } from './Dialog';
|
||||
import { NetworkDropdown } from './NetworkDropdown';
|
||||
import { Account, AccountsState } from '../types';
|
||||
import Accounts from './Accounts';
|
||||
import CreateWallet from './CreateWallet';
|
||||
import ResetWalletDialog from './ResetWalletDialog';
|
||||
import styles from '../styles/stylesheet';
|
||||
|
||||
const HomeScreen = () => {
|
||||
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 [network, setNetwork] = useState<string>('eth');
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||
const [isAccountsFetched, setIsAccountsFetched] = useState<boolean>(false);
|
||||
const [phrase, setPhrase] = useState('');
|
||||
const [accounts, setAccounts] = useState<AccountsState>({
|
||||
ethAccounts: [],
|
||||
cosmosAccounts: [],
|
||||
});
|
||||
|
||||
const hideWalletDialog = () => setWalletDialog(false);
|
||||
const hideResetDialog = () => setResetWalletDialog(false);
|
||||
|
||||
const createWalletHandler = async () => {
|
||||
setIsWalletCreating(true);
|
||||
const { mnemonic, ethAccounts, cosmosAccounts } = await createWallet();
|
||||
if (ethAccounts && cosmosAccounts) {
|
||||
setAccounts({
|
||||
ethAccounts: [...accounts.ethAccounts, ethAccounts],
|
||||
cosmosAccounts: [...accounts.cosmosAccounts, cosmosAccounts],
|
||||
});
|
||||
setWalletDialog(true);
|
||||
setIsWalletCreated(true);
|
||||
setPhrase(mnemonic);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmResetWallet = async () => {
|
||||
await resetWallet();
|
||||
setIsWalletCreated(false);
|
||||
setIsWalletCreating(false);
|
||||
setAccounts({
|
||||
ethAccounts: [],
|
||||
cosmosAccounts: [],
|
||||
});
|
||||
setCurrentIndex(0);
|
||||
hideResetDialog();
|
||||
setNetwork('eth');
|
||||
};
|
||||
|
||||
const updateNetwork = (newNetwork: string) => {
|
||||
setNetwork(newNetwork);
|
||||
setCurrentIndex(0);
|
||||
};
|
||||
|
||||
const updateIndex = (index: number) => {
|
||||
setCurrentIndex(index);
|
||||
};
|
||||
|
||||
const updateAccounts = (account: Account) => {
|
||||
switch (network) {
|
||||
case 'eth':
|
||||
setAccounts({
|
||||
...accounts,
|
||||
ethAccounts: [...accounts.ethAccounts, account],
|
||||
});
|
||||
break;
|
||||
case 'cosmos':
|
||||
setAccounts({
|
||||
...accounts,
|
||||
cosmosAccounts: [...accounts.cosmosAccounts, account],
|
||||
});
|
||||
break;
|
||||
default:
|
||||
console.error('Select a valid network!');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAccounts = async () => {
|
||||
if (isAccountsFetched) {
|
||||
return;
|
||||
}
|
||||
const { ethLoadedAccounts, cosmosLoadedAccounts } =
|
||||
await retrieveAccounts();
|
||||
|
||||
if (ethLoadedAccounts && cosmosLoadedAccounts) {
|
||||
setAccounts({
|
||||
ethAccounts: ethLoadedAccounts,
|
||||
cosmosAccounts: cosmosLoadedAccounts,
|
||||
});
|
||||
|
||||
setIsWalletCreated(true);
|
||||
}
|
||||
|
||||
setIsAccountsFetched(true);
|
||||
};
|
||||
|
||||
fetchAccounts();
|
||||
}, [isAccountsFetched]);
|
||||
|
||||
return (
|
||||
<View style={styles.appContainer}>
|
||||
{!isAccountsFetched ? (
|
||||
<View style={styles.spinnerContainer}>
|
||||
<Text style={styles.LoadingText}>Loading...</Text>
|
||||
<ActivityIndicator size="large" color="#0000ff" />
|
||||
</View>
|
||||
) : isWalletCreated ? (
|
||||
<>
|
||||
<NetworkDropdown
|
||||
selectedNetwork={network}
|
||||
updateNetwork={updateNetwork}
|
||||
/>
|
||||
<View style={styles.accountComponent}>
|
||||
<Accounts
|
||||
network={network}
|
||||
accounts={accounts}
|
||||
currentIndex={currentIndex}
|
||||
updateIndex={updateIndex}
|
||||
updateAccounts={updateAccounts}
|
||||
/>
|
||||
</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}
|
||||
/>
|
||||
<ResetWalletDialog
|
||||
visible={resetWalletDialog}
|
||||
hideDialog={hideResetDialog}
|
||||
onConfirm={confirmResetWallet}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomeScreen;
|
||||
@ -1,43 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { List } from 'react-native-paper';
|
||||
|
||||
import { NetworkDropdownProps } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
|
||||
const NetworkDropdown = ({ updateNetwork }: NetworkDropdownProps) => {
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
const [selectedNetwork, setSelectedNetwork] = useState<string>('Ethereum');
|
||||
|
||||
const handleNetworkPress = (network: string, displayName: string) => {
|
||||
updateNetwork(network);
|
||||
setSelectedNetwork(displayName);
|
||||
setExpanded(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.networkDropdown}>
|
||||
<List.Accordion
|
||||
title={selectedNetwork}
|
||||
expanded={expanded}
|
||||
onPress={() => setExpanded(!expanded)}>
|
||||
{networks.map(network => (
|
||||
<List.Item
|
||||
key={network.value}
|
||||
title={network.displayName}
|
||||
onPress={() =>
|
||||
handleNetworkPress(network.value, network.displayName)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</List.Accordion>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const networks = [
|
||||
{ value: 'eth', displayName: 'Ethereum' },
|
||||
{ value: 'cosmos', displayName: 'Cosmos' },
|
||||
];
|
||||
|
||||
export { NetworkDropdown };
|
||||
@ -1,129 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Alert, View } from 'react-native';
|
||||
import { ActivityIndicator, Button, Text } from 'react-native-paper';
|
||||
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import {
|
||||
NativeStackNavigationProp,
|
||||
NativeStackScreenProps,
|
||||
} from '@react-navigation/native-stack';
|
||||
|
||||
import { Account, StackParamsList } from '../types';
|
||||
import AccountDetails from './AccountDetails';
|
||||
import styles from '../styles/stylesheet';
|
||||
import { signMessage } from '../utils/sign-message';
|
||||
import { retrieveSingleAccount } from '../utils/accounts';
|
||||
|
||||
type SignRequestProps = NativeStackScreenProps<StackParamsList, 'SignRequest'>;
|
||||
|
||||
const SignRequest = ({ route }: SignRequestProps) => {
|
||||
const [account, setAccount] = useState<Account>();
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [network, setNetwork] = useState<string>('');
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
const retrieveData = async (
|
||||
requestNetwork: string,
|
||||
requestAddress: string,
|
||||
requestMessage: string,
|
||||
) => {
|
||||
const requestAccount = await retrieveSingleAccount(
|
||||
requestNetwork,
|
||||
requestAddress,
|
||||
);
|
||||
if (!requestAccount) {
|
||||
navigation.navigate('InvalidPath');
|
||||
}
|
||||
if (requestAccount && requestAccount !== account) {
|
||||
setAccount(requestAccount);
|
||||
}
|
||||
if (requestMessage && requestMessage !== message) {
|
||||
setMessage(decodeURIComponent(requestMessage));
|
||||
}
|
||||
if (requestNetwork && requestNetwork !== network) {
|
||||
setNetwork(requestNetwork);
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizePath = (path: string) => {
|
||||
const regex = /^\/sign\/(eth|cosmos)\/(.+)\/(.+)$/;
|
||||
const match = path.match(regex);
|
||||
|
||||
if (match) {
|
||||
const [network, address, message] = match;
|
||||
return {
|
||||
network,
|
||||
address,
|
||||
message,
|
||||
};
|
||||
} else {
|
||||
navigation.navigate('InvalidPath');
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoaded(true);
|
||||
if (route.path) {
|
||||
const sanitizedRoute = sanitizePath(route.path);
|
||||
sanitizedRoute &&
|
||||
route.params &&
|
||||
retrieveData(
|
||||
route.params?.network,
|
||||
route.params?.address,
|
||||
route.params?.message,
|
||||
);
|
||||
}
|
||||
setLoaded(false);
|
||||
}, [route]);
|
||||
|
||||
const signMessageHandler = async () => {
|
||||
if (!account) {
|
||||
throw new Error('Account is not valid');
|
||||
}
|
||||
if (message) {
|
||||
const signedMessage = await signMessage({
|
||||
message,
|
||||
network,
|
||||
accountId: account.counterId,
|
||||
});
|
||||
Alert.alert('Signature', signedMessage);
|
||||
navigation.navigate('Laconic');
|
||||
}
|
||||
};
|
||||
|
||||
const rejectRequestHandler = async () => {
|
||||
navigation.navigate('Laconic');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!loaded ? (
|
||||
<View style={styles.appContainer}>
|
||||
<AccountDetails account={account} />
|
||||
<View style={styles.requestMessage}>
|
||||
<Text variant="bodyLarge">{message}</Text>
|
||||
</View>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={rejectRequestHandler}
|
||||
buttonColor="#B82B0D">
|
||||
No
|
||||
</Button>
|
||||
<Button mode="contained" onPress={signMessageHandler}>
|
||||
Yes
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<ActivityIndicator />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignRequest;
|
||||
160
development.md
Normal file
160
development.md
Normal file
@ -0,0 +1,160 @@
|
||||
# Development
|
||||
|
||||
## WalletConnect details
|
||||
|
||||
- Docs - https://docs.walletconnect.com/api/sign/overview
|
||||
|
||||
- Doc for terminologies - https://docs.walletconnect.com/advanced/glossary
|
||||
|
||||
- Function for creating web3 wallet
|
||||
|
||||
```js
|
||||
export let web3wallet: IWeb3Wallet;
|
||||
export let core: ICore;
|
||||
|
||||
export async function createWeb3Wallet() {
|
||||
const 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'],
|
||||
},
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- Hook used for intializing web3 wallet
|
||||
|
||||
```js
|
||||
export default function useInitialization() {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
const onInitialize = useCallback(async () => {
|
||||
try {
|
||||
await createWeb3Wallet();
|
||||
setInitialized(true);
|
||||
} catch (err: unknown) {
|
||||
console.log('Error for initializing', err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialized) {
|
||||
onInitialize();
|
||||
}
|
||||
}, [initialized, onInitialize]);
|
||||
|
||||
return initialized;
|
||||
}
|
||||
```
|
||||
|
||||
- Once user clicks on pair, this function is triggered
|
||||
|
||||
```js
|
||||
export async function web3WalletPair(params: { uri: string }) {
|
||||
return await web3wallet.core.pairing.pair({ uri: params.uri });
|
||||
}
|
||||
```
|
||||
|
||||
- In a useEffect, we keep on listening to events that are emitted by dapp and do actions based on it
|
||||
|
||||
```js
|
||||
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);
|
||||
}
|
||||
})
|
||||
```
|
||||
- Signing messages
|
||||
|
||||
- Cosmos methods info
|
||||
|
||||
- The signDoc format for signAmino and signDirect is different
|
||||
|
||||
- Reference - https://docs.leapwallet.io/cosmos/for-dapps-connect-to-leap/api-reference
|
||||
|
||||
- For signDirect, the message is protobuf encoded , hence to sign it , we have to first convert it to uint8Array
|
||||
|
||||
```js
|
||||
const bodyBytesArray = Uint8Array.from(
|
||||
Buffer.from(request.params.signDoc.bodyBytes, 'hex'),
|
||||
);
|
||||
const authInfoBytesArray = Uint8Array.from(
|
||||
Buffer.from(request.params.signDoc.authInfoBytes, 'hex'),
|
||||
);
|
||||
```
|
||||
|
||||
- This will give us correct signature
|
||||
|
||||
## Data structure of keystore
|
||||
|
||||
```json
|
||||
{
|
||||
// Accounts data -> hdpath, privateKey, publicKey, address
|
||||
"accounts/eip155:1/0":{
|
||||
"username": "",
|
||||
"password": "m/44'/60'/0'/0/0,0x0654623fe7a0e3d74f518e22818c1cfd58517e80a232df5d6d20a3afd99fd3bb,0x02cced178c903835bb29e337fa227ba0204a3285eb35797b766ed975b478b4beb6,0xe5fA0Dd92659e287e5e3Fa582Ee20fcf74fb1116"
|
||||
},
|
||||
"accounts/cosmos:cosmoshub-4/0":{
|
||||
"username": "",
|
||||
"password": "m/44'/118'/0'/0/0,0xbb9ccc3029178a61ba847c22108318ba119220451c99e6358efd7b85d7d49ed5,0x03a8002f56f99126f930ca3ac1963731e3f08df30822031db7c1dd50851bdfcc3c,cosmos1s5a5ls3d6xmks5fc8tst9x7tx2jv8mfjmn0aq8"
|
||||
},
|
||||
|
||||
// Networks Data
|
||||
"networks":{
|
||||
"username":"",
|
||||
"password":[
|
||||
{
|
||||
"networkId":"1",
|
||||
"namespace": "eip155",
|
||||
"chainId": "1",
|
||||
"networkName": "Ethereum",
|
||||
"rpcUrl": "https://cloudflare-eth.com/",
|
||||
"currencySymbol": "ETH",
|
||||
"coinType": "60"
|
||||
},
|
||||
{
|
||||
"networkId":"2",
|
||||
"namespace": "cosmos",
|
||||
"chainId": "theta-testnet-001",
|
||||
"networkName": "Cosmos Hub Testnet",
|
||||
"rpcUrl": "https://rpc-t.cosmos.nodestake.top",
|
||||
"nativeDenom": "ATOM",
|
||||
"addressPrefix": "cosmos",
|
||||
"coinType": "118"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// Stores count of total accounts for specific chain
|
||||
"addAccountCounter/eip155:1":{
|
||||
"username": "",
|
||||
"password": "3"
|
||||
},
|
||||
"addAccountCounter/cosmos:cosmoshub-4":{
|
||||
"username": "",
|
||||
"password": "3"
|
||||
},
|
||||
|
||||
// Stores ids of accounts for specific chain
|
||||
"accountIndices/eip155:1":{
|
||||
"username": "",
|
||||
"password": "0,1,2"
|
||||
},
|
||||
"accountIndices/cosmos:cosmoshub-4":{
|
||||
"username": "",
|
||||
"password": "0,1,2"
|
||||
}
|
||||
}
|
||||
```
|
||||
29
index.js
29
index.js
@ -3,13 +3,36 @@ import 'text-encoding-polyfill';
|
||||
import { AppRegistry } from 'react-native';
|
||||
import { PaperProvider } from 'react-native-paper';
|
||||
|
||||
import App from './App';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
|
||||
import App from './src/App';
|
||||
import { AccountsProvider } from './src/context/AccountsContext';
|
||||
import { NetworksProvider } from './src/context/NetworksContext';
|
||||
import { WalletConnectProvider } from './src/context/WalletConnectContext';
|
||||
import { name as appName } from './app.json';
|
||||
|
||||
export default function Main() {
|
||||
const linking = {
|
||||
prefixes: ['https://wallet.laconic.com'],
|
||||
config: {
|
||||
screens: {
|
||||
SignRequest: {
|
||||
path: 'sign/:namespace/:chaindId/:address/:message',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return (
|
||||
<PaperProvider>
|
||||
<App />
|
||||
<PaperProvider theme={'light'}>
|
||||
<NetworksProvider>
|
||||
<AccountsProvider>
|
||||
<WalletConnectProvider>
|
||||
<NavigationContainer linking={linking}>
|
||||
<App />
|
||||
</NavigationContainer>
|
||||
</WalletConnectProvider>
|
||||
</AccountsProvider>
|
||||
</NetworksProvider>
|
||||
</PaperProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const {getDefaultConfig, mergeConfig} = require('@react-native/metro-config');
|
||||
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
|
||||
|
||||
/**
|
||||
* Metro configuration
|
||||
|
||||
35
package.json
35
package.json
@ -5,23 +5,38 @@
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"start": "react-native start",
|
||||
"test": "jest",
|
||||
"postinstall": "patch-package"
|
||||
"postinstall": "patch-package",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cosmjs/amino": "^0.32.2",
|
||||
"@cosmjs/crypto": "^0.32.2",
|
||||
"@cosmjs/amino": "^0.32.3",
|
||||
"@cosmjs/crypto": "^0.32.3",
|
||||
"@cosmjs/proto-signing": "^0.32.3",
|
||||
"@cosmjs/stargate": "^0.32.3",
|
||||
"@ethersproject/shims": "^5.7.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@json-rpc-tools/utils": "^1.7.6",
|
||||
"@react-native-async-storage/async-storage": "^1.22.3",
|
||||
"@react-native-community/netinfo": "^11.3.1",
|
||||
"@react-navigation/native": "^6.1.10",
|
||||
"@react-navigation/native-stack": "^6.9.18",
|
||||
"ethers": "5",
|
||||
"@walletconnect/react-native-compat": "^2.11.2",
|
||||
"@walletconnect/web3wallet": "^1.10.2",
|
||||
"chain-registry": "^1.41.2",
|
||||
"cosmjs-types": "^0.9.0",
|
||||
"ethers": "5.7.2",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"lodash": "^4.17.21",
|
||||
"metro-react-native-babel-preset": "^0.77.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"react": "18.2.0",
|
||||
"react-hook-form": "^7.51.2",
|
||||
"react-native": "0.73.3",
|
||||
"react-native-config": "^1.5.1",
|
||||
"react-native-get-random-values": "^1.10.0",
|
||||
"react-native-keychain": "^8.1.2",
|
||||
"react-native-paper": "^5.12.3",
|
||||
@ -29,8 +44,12 @@
|
||||
"react-native-quick-crypto": "^0.6.1",
|
||||
"react-native-safe-area-context": "^4.9.0",
|
||||
"react-native-screens": "^3.29.0",
|
||||
"react-native-svg": "^15.1.0",
|
||||
"react-native-vector-icons": "^10.0.3",
|
||||
"text-encoding-polyfill": "^0.6.7"
|
||||
"react-native-vision-camera": "^3.9.0",
|
||||
"text-encoding-polyfill": "^0.6.7",
|
||||
"use-debounce": "^10.0.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
@ -40,11 +59,15 @@
|
||||
"@react-native/eslint-config": "0.73.2",
|
||||
"@react-native/metro-config": "0.73.4",
|
||||
"@react-native/typescript-config": "0.73.1",
|
||||
"@types/lodash": "^4.17.0",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react-native-vector-icons": "^6.4.18",
|
||||
"@types/react-test-renderer": "^18.0.0",
|
||||
"@walletconnect/jsonrpc-types": "^1.0.3",
|
||||
"babel-jest": "^29.6.3",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"eslint": "^8.19.0",
|
||||
"husky": "^9.0.11",
|
||||
"jest": "^29.6.3",
|
||||
"metro-babel7-plugin-react-transform": "^0.54.1",
|
||||
"prettier": "2.8.8",
|
||||
|
||||
11
react-native-config.d.ts
vendored
Normal file
11
react-native-config.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
// Reference: https://github.com/lugg/react-native-config?tab=readme-ov-file#typescript-declaration-for-your-env-file
|
||||
declare module 'react-native-config' {
|
||||
export interface NativeConfig {
|
||||
WALLET_CONNECT_PROJECT_ID: string;
|
||||
DEFAULT_GAS_PRICE: string;
|
||||
DEFAULT_GAS_ADJUSTMENT: string;
|
||||
}
|
||||
|
||||
export const Config: NativeConfig;
|
||||
export default Config;
|
||||
}
|
||||
310
src/App.tsx
Normal file
310
src/App.tsx
Normal file
@ -0,0 +1,310 @@
|
||||
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 { StackParamsList } from './types';
|
||||
import { web3wallet } from './utils/wallet-connect/WalletConnectUtils';
|
||||
import { EIP155_SIGNING_METHODS } from './utils/wallet-connect/EIP155Data';
|
||||
import { getSignParamsMessage } from './utils/wallet-connect/helpers';
|
||||
import ApproveTransaction from './screens/ApproveTransaction';
|
||||
import 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';
|
||||
|
||||
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('ApproveTransaction', {
|
||||
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_signDirect':
|
||||
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_signAmino':
|
||||
navigation.navigate('SignRequest', {
|
||||
namespace: COSMOS,
|
||||
address: request.params.signerAddress,
|
||||
message: request.params.signDoc.memo,
|
||||
requestEvent,
|
||||
requestSessionData,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'cosmos_sendTokens':
|
||||
navigation.navigate('ApproveTransaction', {
|
||||
transaction: request.params[0],
|
||||
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);
|
||||
};
|
||||
//TODO: Investigate dependencies
|
||||
});
|
||||
|
||||
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="ApproveTransaction"
|
||||
component={ApproveTransaction}
|
||||
options={{
|
||||
title: 'Approve transaction',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AddNetwork"
|
||||
component={AddNetwork}
|
||||
options={{
|
||||
title: 'Add Network',
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="EditNetwork"
|
||||
component={EditNetwork}
|
||||
options={{
|
||||
title: 'Edit Network',
|
||||
}}
|
||||
/>
|
||||
</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;
|
||||
BIN
src/assets/WalletConnect-Icon-Blueberry.png
Normal file
BIN
src/assets/WalletConnect-Icon-Blueberry.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
src/assets/ethereum-chains.json
Normal file
1
src/assets/ethereum-chains.json
Normal file
File diff suppressed because one or more lines are too long
226
src/components/Accounts.tsx
Normal file
226
src/components/Accounts.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
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 { AccountsProps, 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';
|
||||
|
||||
const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => {
|
||||
const navigation =
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
|
||||
const { accounts, setAccounts, setCurrentIndex } = 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);
|
||||
updateIndex(newAccount.index);
|
||||
}
|
||||
};
|
||||
|
||||
const renderAccountItems = () =>
|
||||
accounts.map(account => (
|
||||
<List.Item
|
||||
key={account.index}
|
||||
title={`Account ${account.index + 1}`}
|
||||
onPress={() => {
|
||||
updateIndex(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}
|
||||
updateIndex={updateIndex}
|
||||
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}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default Accounts;
|
||||
@ -2,7 +2,8 @@ import React from 'react';
|
||||
import { Portal, Dialog, Button, Text } from 'react-native-paper';
|
||||
import { ResetDialogProps } from '../types';
|
||||
|
||||
const ResetWalletDialog = ({
|
||||
const ConfirmDialog = ({
|
||||
title,
|
||||
visible,
|
||||
hideDialog,
|
||||
onConfirm,
|
||||
@ -10,7 +11,7 @@ const ResetWalletDialog = ({
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog visible={visible} onDismiss={hideDialog}>
|
||||
<Dialog.Title>Reset Wallet</Dialog.Title>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<Text variant="bodyMedium">Are you sure?</Text>
|
||||
</Dialog.Content>
|
||||
@ -25,4 +26,4 @@ const ResetWalletDialog = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetWalletDialog;
|
||||
export default ConfirmDialog;
|
||||
@ -15,6 +15,7 @@ const CreateWallet = ({
|
||||
<Button
|
||||
mode="contained"
|
||||
loading={isWalletCreating}
|
||||
disabled={isWalletCreating}
|
||||
onPress={createWalletHandler}>
|
||||
{isWalletCreating ? 'Creating' : 'Create Wallet'}
|
||||
</Button>
|
||||
17
src/components/DataBox.tsx
Normal file
17
src/components/DataBox.tsx
Normal 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;
|
||||
@ -3,7 +3,7 @@ import { ScrollView, View, Text } from 'react-native';
|
||||
import { Button, TextInput } from 'react-native-paper';
|
||||
|
||||
import { addAccountFromHDPath } from '../utils/accounts';
|
||||
import { Account, PathState } from '../types';
|
||||
import { Account, NetworksDataState, PathState } from '../types';
|
||||
import styles from '../styles/stylesheet';
|
||||
|
||||
const HDPath = ({
|
||||
@ -11,11 +11,13 @@ const HDPath = ({
|
||||
updateAccounts,
|
||||
updateIndex,
|
||||
hideDialog,
|
||||
selectedNetwork,
|
||||
}: {
|
||||
pathCode: string;
|
||||
updateIndex: (index: number) => void;
|
||||
updateAccounts: (account: Account) => void;
|
||||
hideDialog: () => void;
|
||||
selectedNetwork: NetworksDataState;
|
||||
}) => {
|
||||
const [isAccountCreating, setIsAccountCreating] = useState(false);
|
||||
const [path, setPath] = useState<PathState>({
|
||||
@ -41,10 +43,10 @@ const HDPath = ({
|
||||
pathCode +
|
||||
`${path.firstNumber}'/${path.secondNumber}/${path.thirdNumber}`;
|
||||
try {
|
||||
const newAccount = await addAccountFromHDPath(hdPath);
|
||||
const newAccount = await addAccountFromHDPath(hdPath, selectedNetwork);
|
||||
if (newAccount) {
|
||||
updateAccounts(newAccount);
|
||||
updateIndex(newAccount.counterId);
|
||||
updateIndex(newAccount.index);
|
||||
hideDialog();
|
||||
}
|
||||
} catch (error) {
|
||||
@ -86,7 +88,8 @@ const HDPath = ({
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={createFromHDPathHandler}
|
||||
loading={isAccountCreating}>
|
||||
loading={isAccountCreating}
|
||||
disabled={isAccountCreating}>
|
||||
{isAccountCreating ? 'Adding' : 'Add Account'}
|
||||
</Button>
|
||||
</View>
|
||||
@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Portal, Dialog } from 'react-native-paper';
|
||||
|
||||
import { HDPathDialogProps } from '../types';
|
||||
import HDPath from './HDPath';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
|
||||
const HDPathDialog = ({
|
||||
visible,
|
||||
@ -10,12 +12,15 @@ const HDPathDialog = ({
|
||||
updateAccounts,
|
||||
pathCode,
|
||||
}: HDPathDialogProps) => {
|
||||
const { selectedNetwork } = useNetworks();
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog visible={visible} onDismiss={hideDialog}>
|
||||
<Dialog.Title>Add account from HD path</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<HDPath
|
||||
selectedNetwork={selectedNetwork!}
|
||||
pathCode={pathCode}
|
||||
updateIndex={updateIndex}
|
||||
updateAccounts={updateAccounts}
|
||||
38
src/components/NetworkDropdown.tsx
Normal file
38
src/components/NetworkDropdown.tsx
Normal 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 };
|
||||
310
src/components/PairingModal.tsx
Normal file
310
src/components/PairingModal.tsx
Normal file
@ -0,0 +1,310 @@
|
||||
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 [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;
|
||||
|
||||
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
src/components/SelectNetworkType.tsx
Normal file
43
src/components/SelectNetworkType.tsx
Normal 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 };
|
||||
28
src/components/TxErrorDialog.tsx
Normal file
28
src/components/TxErrorDialog.tsx
Normal 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
src/context/AccountsContext.tsx
Normal file
39
src/context/AccountsContext.tsx
Normal 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
src/context/NetworksContext.tsx
Normal file
76
src/context/NetworksContext.tsx
Normal 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 };
|
||||
42
src/context/WalletConnectContext.tsx
Normal file
42
src/context/WalletConnectContext.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
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>
|
||||
>({});
|
||||
|
||||
return (
|
||||
<WalletConnectContext.Provider
|
||||
value={{
|
||||
activeSessions,
|
||||
setActiveSessions,
|
||||
}}>
|
||||
{children}
|
||||
</WalletConnectContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { useWalletConnect, WalletConnectProvider };
|
||||
23
src/hooks/useInitialization.ts
Normal file
23
src/hooks/useInitialization.ts
Normal 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
src/hooks/usePrevious.ts
Normal file
10
src/hooks/usePrevious.ts
Normal 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;
|
||||
}
|
||||
418
src/screens/AddNetwork.tsx
Normal file
418
src/screens/AddNetwork.tsx
Normal file
@ -0,0 +1,418 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { ScrollView } from 'react-native';
|
||||
import { useForm, Controller, useWatch, FieldErrors } from 'react-hook-form';
|
||||
import { TextInput, Button, HelperText } from 'react-native-paper';
|
||||
import {
|
||||
getInternetCredentials,
|
||||
setInternetCredentials,
|
||||
} from 'react-native-keychain';
|
||||
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 styles from '../styles/stylesheet';
|
||||
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';
|
||||
|
||||
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 && mnemonicServer.password;
|
||||
|
||||
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 (
|
||||
<ScrollView contentContainerStyle={styles.signPage}>
|
||||
<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>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddNetwork;
|
||||
119
src/screens/AddSession.tsx
Normal file
119
src/screens/AddSession.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
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 ? (
|
||||
<>
|
||||
<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;
|
||||
612
src/screens/ApproveTransaction.tsx
Normal file
612
src/screens/ApproveTransaction.tsx
Normal file
@ -0,0 +1,612 @@
|
||||
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,
|
||||
} from '../utils/wallet-connect/WalletConnectRequests';
|
||||
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';
|
||||
|
||||
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,
|
||||
'ApproveTransaction'
|
||||
>;
|
||||
|
||||
const ApproveTransaction = ({ 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 [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()})`,
|
||||
);
|
||||
}
|
||||
|
||||
const response = await approveWalletConnectRequest({
|
||||
requestEvent,
|
||||
account,
|
||||
namespace,
|
||||
chainId: requestedNetwork!.chainId,
|
||||
provider: namespace === EIP155 ? provider : cosmosStargateClient,
|
||||
// StdFee object
|
||||
cosmosFee: {
|
||||
// This amount is total fees required for transaction
|
||||
amount: [
|
||||
{
|
||||
amount: fees!,
|
||||
denom: requestedNetwork!.nativeDenom!,
|
||||
},
|
||||
],
|
||||
gas: cosmosGasLimit!,
|
||||
},
|
||||
ethGasLimit:
|
||||
namespace === EIP155 ? BigNumber.from(ethGasLimit) : undefined,
|
||||
ethGasPrice: ethGasPrice?.toHexString(),
|
||||
maxPriorityFeePerGas: ethMaxPriorityFee ?? undefined,
|
||||
maxFeePerGas: ethMaxFee ?? undefined,
|
||||
sendMsg,
|
||||
memo: MEMO,
|
||||
});
|
||||
|
||||
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.approveTransaction}>
|
||||
<DataBox label="To" data={transaction.to!} />
|
||||
<DataBox
|
||||
label={`Amount (${
|
||||
namespace === EIP155 ? 'wei' : requestedNetwork!.nativeDenom
|
||||
})`}
|
||||
data={BigNumber.from(
|
||||
transaction.value?.toString(),
|
||||
).toString()}
|
||||
/>
|
||||
|
||||
{namespace === EIP155 ? (
|
||||
<>
|
||||
{isEIP1559 === false ? (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
{'Gas Price (wei)'}
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethGasPrice?.toNumber().toString()}
|
||||
onChangeText={value =>
|
||||
setEthGasPrice(BigNumber.from(value))
|
||||
}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
Max Fee Per Gas (wei)
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethMaxFee?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthMaxFee(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<Text style={styles.dataBoxLabel}>
|
||||
Max Priority Fee Per Gas (wei)
|
||||
</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethMaxPriorityFee?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthMaxPriorityFee(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={ethGasLimit?.toNumber().toString()}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setEthGasLimit(BigNumber.from(value));
|
||||
}
|
||||
}}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<DataBox
|
||||
label={`${
|
||||
isEIP1559 === true ? 'Max Fee' : 'Gas Fee'
|
||||
} (wei)`}
|
||||
data={fees!}
|
||||
/>
|
||||
<DataBox label="Data" data={transaction.data!} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text style={styles.dataBoxLabel}>{`Fee (${
|
||||
requestedNetwork!.nativeDenom
|
||||
})`}</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={fees}
|
||||
onChangeText={value => setFees(value)}
|
||||
style={styles.transactionFeesInput}
|
||||
/>
|
||||
<Text style={styles.dataBoxLabel}>Gas Limit</Text>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
value={cosmosGasLimit}
|
||||
onChangeText={value => {
|
||||
if (IS_NUMBER_REGEX.test(value)) {
|
||||
setCosmosGasLimit(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={acceptRequestHandler}
|
||||
loading={isTxLoading}
|
||||
disabled={!balance || !fees}>
|
||||
{isTxLoading ? 'Processing' : 'Yes'}
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={rejectRequestHandler}
|
||||
buttonColor="#B82B0D">
|
||||
No
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
<TxErrorDialog
|
||||
error={txError!}
|
||||
visible={isTxErrorDialogOpen}
|
||||
hideDialog={() => {
|
||||
setIsTxErrorDialogOpen(false);
|
||||
if (!isSufficientFunds || !balance || !fees) {
|
||||
rejectRequestHandler();
|
||||
navigation.navigate('Laconic');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApproveTransaction;
|
||||
197
src/screens/EditNetwork.tsx
Normal file
197
src/screens/EditNetwork.tsx
Normal file
@ -0,0 +1,197 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import { useForm, Controller, FieldErrors } from 'react-hook-form';
|
||||
import { TextInput, Button, HelperText, Text } from 'react-native-paper';
|
||||
import { setInternetCredentials } from 'react-native-keychain';
|
||||
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 { 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 (
|
||||
<ScrollView contentContainerStyle={styles.signPage}>
|
||||
<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>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditNetwork;
|
||||
180
src/screens/HomeScreen.tsx
Normal file
180
src/screens/HomeScreen.tsx
Normal file
@ -0,0 +1,180 @@
|
||||
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, currentIndex, 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 [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);
|
||||
};
|
||||
|
||||
const updateIndex = (index: number) => {
|
||||
setCurrentIndex(index);
|
||||
};
|
||||
|
||||
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 currentIndex={currentIndex} updateIndex={updateIndex} />
|
||||
</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;
|
||||
@ -13,7 +13,9 @@ const InvalidPath = () => {
|
||||
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
||||
return (
|
||||
<View style={styles.badRequestContainer}>
|
||||
<Text style={styles.messageText}>The signature request was invalid.</Text>
|
||||
<Text style={styles.invalidMessageText}>
|
||||
The signature request was invalid.
|
||||
</Text>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
@ -7,36 +7,33 @@ 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 './AccountDetails';
|
||||
import AccountDetails from '../components/AccountDetails';
|
||||
|
||||
type SignProps = NativeStackScreenProps<StackParamsList, 'SignMessage'>;
|
||||
|
||||
const SignMessage = ({ route }: SignProps) => {
|
||||
const network = route.params?.selectedNetwork;
|
||||
const account = route.params?.accountInfo;
|
||||
const namespace = route.params.selectedNamespace;
|
||||
const chainId = route.params.selectedChainId;
|
||||
const account = route.params.accountInfo;
|
||||
|
||||
const [message, setMessage] = useState<string>('');
|
||||
|
||||
const signMessageHandler = async () => {
|
||||
if (network) {
|
||||
if (!account) {
|
||||
throw new Error('Account is not valid');
|
||||
}
|
||||
const signedMessage = await signMessage({
|
||||
message,
|
||||
network,
|
||||
accountId: account.counterId,
|
||||
});
|
||||
Alert.alert('Signature', signedMessage);
|
||||
}
|
||||
const signedMessage = await signMessage({
|
||||
message,
|
||||
namespace,
|
||||
chainId,
|
||||
accountId: account.index,
|
||||
});
|
||||
Alert.alert('Signature', signedMessage);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.signPage}>
|
||||
<View style={styles.accountInfo}>
|
||||
<View>
|
||||
<Text variant="headlineSmall">
|
||||
{account && `Account ${account.counterId + 1}`}
|
||||
<Text variant="titleMedium">
|
||||
{account && `Account ${account.index + 1}`}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.accountContainer}>
|
||||
302
src/screens/SignRequest.tsx
Normal file
302
src/screens/SignRequest.tsx
Normal file
@ -0,0 +1,302 @@
|
||||
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,
|
||||
} from '../utils/wallet-connect/WalletConnectRequests';
|
||||
import { web3wallet } from '../utils/wallet-connect/WalletConnectUtils';
|
||||
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
|
||||
import { useNetworks } from '../context/NetworksContext';
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
const response = await approveWalletConnectRequest({
|
||||
requestEvent,
|
||||
account,
|
||||
namespace,
|
||||
chainId,
|
||||
message,
|
||||
});
|
||||
|
||||
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
src/screens/WalletConnect.tsx
Normal file
84
src/screens/WalletConnect.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
277
src/styles/stylesheet.js
Normal file
277
src/styles/stylesheet.js
Normal file
@ -0,0 +1,277 @@
|
||||
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,
|
||||
},
|
||||
approveTransaction: {
|
||||
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: 10,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 10,
|
||||
marginVertical: 20,
|
||||
},
|
||||
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 },
|
||||
});
|
||||
|
||||
export default styles;
|
||||
129
src/types.ts
Normal file
129
src/types.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import { PopulatedTransaction } from 'ethers';
|
||||
|
||||
import { SignClientTypes, SessionTypes } from '@walletconnect/types';
|
||||
import { Web3WalletTypes } from '@walletconnect/web3wallet';
|
||||
|
||||
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;
|
||||
};
|
||||
ApproveTransaction: {
|
||||
transaction: PopulatedTransaction;
|
||||
requestEvent: Web3WalletTypes.SessionRequest;
|
||||
requestSessionData: SessionTypes.Struct;
|
||||
};
|
||||
InvalidPath: undefined;
|
||||
WalletConnect: undefined;
|
||||
AddSession: undefined;
|
||||
AddNetwork: undefined;
|
||||
EditNetwork: {
|
||||
selectedNetwork: NetworksDataState;
|
||||
};
|
||||
};
|
||||
|
||||
export type Account = {
|
||||
index: number;
|
||||
pubKey: string;
|
||||
address: string;
|
||||
hdPath: string;
|
||||
};
|
||||
|
||||
export type AccountsProps = {
|
||||
currentIndex: number;
|
||||
updateIndex: (index: number) => void;
|
||||
};
|
||||
|
||||
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;
|
||||
updateIndex: (index: number) => 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>;
|
||||
setActiveSessions: (
|
||||
activeSessions: Record<string, SessionTypes.Struct>,
|
||||
) => void;
|
||||
}
|
||||
342
src/utils/accounts.ts
Normal file
342
src/utils/accounts.ts
Normal file
@ -0,0 +1,342 @@
|
||||
/* 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 'react-native-keychain';
|
||||
|
||||
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');
|
||||
const retrievedNetworks =
|
||||
networks && networks.password ? JSON.parse(networks.password) : [];
|
||||
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');
|
||||
const retrievedNetworks: NetworksDataState[] =
|
||||
networks && networks.password ? JSON.parse(networks.password) : [];
|
||||
|
||||
return retrievedNetworks;
|
||||
};
|
||||
|
||||
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 && accountIndicesServer.password;
|
||||
const loadedAccounts =
|
||||
accountIndices !== false
|
||||
? await retrieveAccountsForNetwork(
|
||||
`${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
|
||||
accountIndices,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return loadedAccounts;
|
||||
};
|
||||
|
||||
const retrieveSingleAccount = async (
|
||||
namespace: string,
|
||||
chainId: string,
|
||||
address: string,
|
||||
) => {
|
||||
let loadedAccounts;
|
||||
|
||||
const accountIndicesServer = await getInternetCredentials(
|
||||
`accountIndices/${namespace}:${chainId}`,
|
||||
);
|
||||
const accountIndices = accountIndicesServer && accountIndicesServer.password;
|
||||
|
||||
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.password;
|
||||
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.password;
|
||||
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
src/utils/constants.ts
Normal file
36
src/utils/constants.ts
Normal 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+$/;
|
||||
158
src/utils/misc.ts
Normal file
158
src/utils/misc.ts
Normal 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 'react-native-keychain';
|
||||
|
||||
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.password;
|
||||
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.password;
|
||||
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.password;
|
||||
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.password);
|
||||
const filteredNetworks = networksData.filter(
|
||||
network => network.namespace === namespace,
|
||||
);
|
||||
|
||||
if (filteredNetworks.length === 0) {
|
||||
throw new Error(`No networks found for namespace ${namespace}.`);
|
||||
}
|
||||
|
||||
filteredNetworks.forEach(async network => {
|
||||
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.password;
|
||||
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,
|
||||
};
|
||||
@ -1,25 +1,30 @@
|
||||
/* 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 { getCosmosAccounts, getMnemonic, getPathKey } from './utils';
|
||||
import { getDirectWallet, getMnemonic, getPathKey } from './misc';
|
||||
import { getCosmosAccounts } from './accounts';
|
||||
import { COSMOS, EIP155 } from './constants';
|
||||
|
||||
const signMessage = async ({
|
||||
message,
|
||||
network,
|
||||
namespace,
|
||||
chainId,
|
||||
accountId,
|
||||
}: SignMessageParams): Promise<string | undefined> => {
|
||||
const path = (await getPathKey(network, accountId)).path;
|
||||
const path = await getPathKey(`${namespace}:${chainId}`, accountId);
|
||||
|
||||
switch (network) {
|
||||
case 'eth':
|
||||
return await signEthMessage(message, accountId);
|
||||
case 'cosmos':
|
||||
return await signCosmosMessage(message, path);
|
||||
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');
|
||||
}
|
||||
@ -28,16 +33,18 @@ const signMessage = async ({
|
||||
const signEthMessage = async (
|
||||
message: string,
|
||||
accountId: number,
|
||||
chainId: string,
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const privKey = (await getPathKey('eth', accountId)).privKey;
|
||||
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);
|
||||
return undefined;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@ -75,8 +82,30 @@ const signCosmosMessage = async (
|
||||
return cosmosSignature.signature.signature;
|
||||
} catch (error) {
|
||||
console.error('Error signing Cosmos message:', error);
|
||||
return undefined;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { signMessage, signEthMessage, signCosmosMessage };
|
||||
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 };
|
||||
57
src/utils/wallet-connect/COSMOSData.ts
Normal file
57
src/utils/wallet-connect/COSMOSData.ts
Normal file
@ -0,0 +1,57 @@
|
||||
// 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_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',
|
||||
},
|
||||
};
|
||||
|
||||
export const COSMOS_MAINNET_CHAINS = {
|
||||
'cosmos:cosmoshub-4': {
|
||||
chainId: 'cosmoshub-4',
|
||||
name: 'Cosmos Hub',
|
||||
logo: '/chain-logos/cosmos-cosmoshub-4.png',
|
||||
rgb: '107, 111, 147',
|
||||
rpc: '',
|
||||
namespace: 'cosmos',
|
||||
},
|
||||
};
|
||||
|
||||
export const COSMOS_CHAINS = {
|
||||
...COSMOS_MAINNET_CHAINS,
|
||||
...COSMOS_TESTNET_CHAINS,
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
144
src/utils/wallet-connect/EIP155Data.ts
Normal file
144
src/utils/wallet-connect/EIP155Data.ts
Normal file
@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @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_MAINNET_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',
|
||||
},
|
||||
'eip155:43114': {
|
||||
chainId: 43114,
|
||||
name: 'Avalanche C-Chain',
|
||||
logo: '/chain-logos/eip155-43113.png',
|
||||
rgb: '232, 65, 66',
|
||||
rpc: 'https://api.avax.network/ext/bc/C/rpc',
|
||||
namespace: 'eip155',
|
||||
},
|
||||
'eip155:137': {
|
||||
chainId: 137,
|
||||
name: 'Polygon',
|
||||
logo: '/chain-logos/eip155-137.png',
|
||||
rgb: '130, 71, 229',
|
||||
rpc: 'https://polygon-rpc.com/',
|
||||
namespace: 'eip155',
|
||||
},
|
||||
'eip155:10': {
|
||||
chainId: 10,
|
||||
name: 'Optimism',
|
||||
logo: '/chain-logos/eip155-10.png',
|
||||
rgb: '235, 0, 25',
|
||||
rpc: 'https://mainnet.optimism.io',
|
||||
namespace: 'eip155',
|
||||
},
|
||||
'eip155:324': {
|
||||
chainId: 324,
|
||||
name: 'zkSync Era',
|
||||
logo: '/chain-logos/eip155-324.svg',
|
||||
rgb: '242, 242, 242',
|
||||
rpc: 'https://mainnet.era.zksync.io/',
|
||||
namespace: 'eip155',
|
||||
},
|
||||
|
||||
// Required chain by SIWE
|
||||
'eip155:42161': {
|
||||
chainId: 42161,
|
||||
name: 'Arbitrum One',
|
||||
logo: '',
|
||||
rgb: '242, 242, 242',
|
||||
rpc: 'https://arb1.arbitrum.io/rpc',
|
||||
namespace: 'eip155',
|
||||
},
|
||||
};
|
||||
|
||||
export const EIP155_TEST_CHAINS: Record<string, EIP155Chain> = {
|
||||
'eip155:5': {
|
||||
chainId: 5,
|
||||
name: 'Ethereum Goerli',
|
||||
logo: '/chain-logos/eip155-1.png',
|
||||
rgb: '99, 125, 234',
|
||||
rpc: 'https://goerli.infura.io/v3/9aa3d95b3bc440fa88ea12eaa4456161',
|
||||
namespace: 'eip155',
|
||||
smartAccountEnabled: true,
|
||||
},
|
||||
'eip155:11155111': {
|
||||
chainId: 11155111,
|
||||
name: 'Ethereum Sepolia',
|
||||
logo: '/chain-logos/eip155-1.png',
|
||||
rgb: '99, 125, 234',
|
||||
rpc: 'https://gateway.tenderly.co/public/sepolia',
|
||||
namespace: 'eip155',
|
||||
smartAccountEnabled: true,
|
||||
},
|
||||
'eip155:43113': {
|
||||
chainId: 43113,
|
||||
name: 'Avalanche Fuji',
|
||||
logo: '/chain-logos/eip155-43113.png',
|
||||
rgb: '232, 65, 66',
|
||||
rpc: 'https://api.avax-test.network/ext/bc/C/rpc',
|
||||
namespace: 'eip155',
|
||||
},
|
||||
'eip155:80001': {
|
||||
chainId: 80001,
|
||||
name: 'Polygon Mumbai',
|
||||
logo: '/chain-logos/eip155-137.png',
|
||||
rgb: '130, 71, 229',
|
||||
rpc: 'https://matic-mumbai.chainstacklabs.com',
|
||||
namespace: 'eip155',
|
||||
smartAccountEnabled: true,
|
||||
},
|
||||
'eip155:420': {
|
||||
chainId: 420,
|
||||
name: 'Optimism Goerli',
|
||||
logo: '/chain-logos/eip155-10.png',
|
||||
rgb: '235, 0, 25',
|
||||
rpc: 'https://goerli.optimism.io',
|
||||
namespace: 'eip155',
|
||||
},
|
||||
'eip155:280': {
|
||||
chainId: 280,
|
||||
name: 'zkSync Era Testnet',
|
||||
logo: '/chain-logos/eip155-324.svg',
|
||||
rgb: '242, 242, 242',
|
||||
rpc: 'https://testnet.era.zksync.dev/',
|
||||
namespace: 'eip155',
|
||||
},
|
||||
};
|
||||
|
||||
export const EIP155_CHAINS = {
|
||||
...EIP155_MAINNET_CHAINS,
|
||||
...EIP155_TEST_CHAINS,
|
||||
};
|
||||
|
||||
/**
|
||||
* Methods
|
||||
*/
|
||||
export const EIP155_SIGNING_METHODS = {
|
||||
PERSONAL_SIGN: 'personal_sign',
|
||||
ETH_SEND_TRANSACTION: 'eth_sendTransaction',
|
||||
};
|
||||
179
src/utils/wallet-connect/WalletConnectRequests.ts
Normal file
179
src/utils/wallet-connect/WalletConnectRequests.ts
Normal file
@ -0,0 +1,179 @@
|
||||
// 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 { EIP155_SIGNING_METHODS } from './EIP155Data';
|
||||
import { signDirectMessage, signEthMessage } from '../sign-message';
|
||||
import { Account } from '../../types';
|
||||
import { getMnemonic, getPathKey } from '../misc';
|
||||
import { getCosmosAccounts } from '../accounts';
|
||||
|
||||
export async function approveWalletConnectRequest({
|
||||
requestEvent,
|
||||
account,
|
||||
namespace,
|
||||
chainId,
|
||||
message,
|
||||
provider,
|
||||
cosmosFee,
|
||||
ethGasLimit,
|
||||
ethGasPrice,
|
||||
sendMsg,
|
||||
memo,
|
||||
maxPriorityFeePerGas,
|
||||
maxFeePerGas,
|
||||
}: {
|
||||
requestEvent: SignClientTypes.EventArguments['session_request'];
|
||||
account: Account;
|
||||
namespace: string;
|
||||
chainId: string;
|
||||
message?: string;
|
||||
provider?: providers.JsonRpcProvider | SigningStargateClient;
|
||||
cosmosFee?: StdFee;
|
||||
ethGasLimit?: BigNumber;
|
||||
ethGasPrice?: string;
|
||||
maxPriorityFeePerGas?: BigNumber;
|
||||
maxFeePerGas?: BigNumber;
|
||||
sendMsg?: MsgSendEncodeObject;
|
||||
memo?: string;
|
||||
}) {
|
||||
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 (!provider) {
|
||||
throw new Error('JSON RPC provider not found');
|
||||
}
|
||||
|
||||
const privKey = (
|
||||
await getPathKey(`${namespace}:${chainId}`, account.index)
|
||||
).privKey;
|
||||
const wallet = new Wallet(privKey);
|
||||
const sendTransaction = request.params[0];
|
||||
const updatedTransaction =
|
||||
maxFeePerGas && maxPriorityFeePerGas
|
||||
? {
|
||||
...sendTransaction,
|
||||
gasLimit: ethGasLimit,
|
||||
maxFeePerGas,
|
||||
maxPriorityFeePerGas,
|
||||
}
|
||||
: {
|
||||
...sendTransaction,
|
||||
gasLimit: ethGasLimit,
|
||||
gasPrice: ethGasPrice,
|
||||
type: 0,
|
||||
};
|
||||
|
||||
if (!(provider instanceof providers.JsonRpcProvider)) {
|
||||
throw new Error('Provider not found');
|
||||
}
|
||||
const connectedWallet = wallet.connect(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 (!message) {
|
||||
throw new Error('Message to be signed not found');
|
||||
}
|
||||
|
||||
const ethSignature = await signEthMessage(
|
||||
message,
|
||||
account.index,
|
||||
chainId,
|
||||
);
|
||||
return formatJsonRpcResult(id, ethSignature);
|
||||
|
||||
case 'cosmos_signDirect':
|
||||
// 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
|
||||
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_signAmino':
|
||||
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_sendTokens':
|
||||
if (!(provider instanceof SigningStargateClient)) {
|
||||
throw new Error('Cosmos stargate client not found');
|
||||
}
|
||||
|
||||
if (!cosmosFee) {
|
||||
throw new Error('Cosmos fee not found');
|
||||
}
|
||||
|
||||
if (!sendMsg) {
|
||||
throw new Error('Message to be sent not found');
|
||||
}
|
||||
|
||||
const result = await provider.signAndBroadcast(
|
||||
address,
|
||||
[sendMsg],
|
||||
cosmosFee,
|
||||
memo,
|
||||
);
|
||||
|
||||
return formatJsonRpcResult(id, {
|
||||
signature: result.transactionHash,
|
||||
});
|
||||
|
||||
default:
|
||||
throw new Error(getSdkError('INVALID_METHOD').message);
|
||||
}
|
||||
}
|
||||
|
||||
export function rejectWalletConnectRequest(
|
||||
request: SignClientTypes.EventArguments['session_request'],
|
||||
) {
|
||||
const { id } = request;
|
||||
|
||||
return formatJsonRpcError(id, getSdkError('USER_REJECTED_METHODS').message);
|
||||
}
|
||||
32
src/utils/wallet-connect/WalletConnectUtils.tsx
Normal file
32
src/utils/wallet-connect/WalletConnectUtils.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
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 | undefined;
|
||||
export let core: ICore;
|
||||
|
||||
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
src/utils/wallet-connect/common-data.ts
Normal file
4
src/utils/wallet-connect/common-data.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export const NETWORK_METHODS = {
|
||||
GET_NETWORKS: 'getNetworks',
|
||||
CHANGE_NETWORK: 'changeNetwork',
|
||||
};
|
||||
218
src/utils/wallet-connect/helpers.ts
Normal file
218
src/utils/wallet-connect/helpers.ts
Normal file
@ -0,0 +1,218 @@
|
||||
// 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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1,142 +0,0 @@
|
||||
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: {
|
||||
marginTop: 24,
|
||||
},
|
||||
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,
|
||||
},
|
||||
accountInfo: {
|
||||
marginTop: 24,
|
||||
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: 50,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
buttonContainer: {
|
||||
marginTop: 50,
|
||||
flexDirection: 'row',
|
||||
marginLeft: 20,
|
||||
justifyContent: 'space-evenly',
|
||||
},
|
||||
badRequestContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 20,
|
||||
},
|
||||
messageText: {
|
||||
color: 'black',
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
marginBottom: 20,
|
||||
},
|
||||
});
|
||||
|
||||
export default styles;
|
||||
@ -1,3 +1,3 @@
|
||||
{
|
||||
"extends": "@react-native/typescript-config/tsconfig.json"
|
||||
"extends": "@react-native/typescript-config/tsconfig.json",
|
||||
}
|
||||
|
||||
84
types.ts
84
types.ts
@ -1,84 +0,0 @@
|
||||
export type StackParamsList = {
|
||||
Laconic: undefined;
|
||||
SignMessage: { selectedNetwork: string; accountInfo: Account } | undefined;
|
||||
SignRequest:
|
||||
| { network: string; address: string; message: string }
|
||||
| undefined;
|
||||
InvalidPath: undefined;
|
||||
};
|
||||
|
||||
export type Account = {
|
||||
counterId: number;
|
||||
pubKey: string;
|
||||
address: string;
|
||||
hdPath: string;
|
||||
};
|
||||
|
||||
export type WalletDetails = {
|
||||
mnemonic: string;
|
||||
ethAccounts: Account | undefined;
|
||||
cosmosAccounts: Account | undefined;
|
||||
};
|
||||
|
||||
export type AccountsProps = {
|
||||
network: string;
|
||||
accounts: {
|
||||
ethAccounts: Account[];
|
||||
cosmosAccounts: Account[];
|
||||
};
|
||||
currentIndex: number;
|
||||
updateIndex: (index: number) => void;
|
||||
updateAccounts: (account: Account) => void;
|
||||
};
|
||||
|
||||
export type NetworkDropdownProps = {
|
||||
selectedNetwork: string;
|
||||
updateNetwork: (network: string) => void;
|
||||
};
|
||||
|
||||
export type AccountsState = {
|
||||
ethAccounts: Account[];
|
||||
cosmosAccounts: Account[];
|
||||
};
|
||||
|
||||
export type SignMessageParams = {
|
||||
message: string;
|
||||
network: string;
|
||||
accountId: number;
|
||||
};
|
||||
|
||||
export type CreateWalletProps = {
|
||||
isWalletCreating: boolean;
|
||||
createWalletHandler: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type ResetDialogProps = {
|
||||
visible: boolean;
|
||||
hideDialog: () => void;
|
||||
onConfirm: () => void;
|
||||
};
|
||||
|
||||
export type HDPathDialogProps = {
|
||||
pathCode: string;
|
||||
visible: boolean;
|
||||
hideDialog: () => void;
|
||||
updateIndex: (index: number) => 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;
|
||||
};
|
||||
@ -1,249 +0,0 @@
|
||||
/* 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 'react-native-keychain';
|
||||
|
||||
import { Account, WalletDetails } from '../types';
|
||||
import {
|
||||
accountInfoFromHDPath,
|
||||
getAddress,
|
||||
getCosmosAccounts,
|
||||
getHDPath,
|
||||
getMnemonic,
|
||||
getNextAccountId,
|
||||
getPathKey,
|
||||
resetKeyServers,
|
||||
updateAccountIndices,
|
||||
updateGlobalCounter,
|
||||
} from './utils';
|
||||
|
||||
const createWallet = async (): Promise<WalletDetails> => {
|
||||
try {
|
||||
const mnemonic = utils.entropyToMnemonic(utils.randomBytes(16));
|
||||
await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic);
|
||||
|
||||
const hdNode = HDNode.fromMnemonic(mnemonic);
|
||||
const ethNode = hdNode.derivePath("m/44'/60'/0'/0/0");
|
||||
const cosmosNode = hdNode.derivePath("m/44'/118'/0'/0/0");
|
||||
|
||||
const ethAddress = ethNode.address;
|
||||
const cosmosAddress = (await getCosmosAccounts(mnemonic, "0'/0/0")).data
|
||||
.address;
|
||||
|
||||
const ethAccountInfo = `${"0'/0/0"},${ethNode.privateKey},${
|
||||
ethNode.publicKey
|
||||
},${ethAddress}`;
|
||||
const cosmosAccountInfo = `${"0'/0/0"},${cosmosNode.privateKey},${
|
||||
cosmosNode.publicKey
|
||||
},${cosmosAddress}`;
|
||||
|
||||
await Promise.all([
|
||||
setInternetCredentials(
|
||||
'eth:keyServer:0',
|
||||
'eth:pathKey:0',
|
||||
ethAccountInfo,
|
||||
),
|
||||
setInternetCredentials(
|
||||
'cosmos:keyServer:0',
|
||||
'cosmos:pathKey:0',
|
||||
cosmosAccountInfo,
|
||||
),
|
||||
setInternetCredentials('eth:accountIndices', 'ethCounter', '0'),
|
||||
setInternetCredentials('cosmos:accountIndices', 'cosmosCounter', '0'),
|
||||
setInternetCredentials('eth:globalCounter', 'ethGlobal', '0'),
|
||||
setInternetCredentials('cosmos:globalCounter', 'cosmosGlobal', '0'),
|
||||
]);
|
||||
|
||||
const ethAccounts = {
|
||||
counterId: 0,
|
||||
pubKey: ethNode.publicKey,
|
||||
address: ethAddress,
|
||||
hdPath: "m/44'/60'/0'/0/0",
|
||||
};
|
||||
|
||||
const cosmosAccounts = {
|
||||
counterId: 0,
|
||||
pubKey: cosmosNode.publicKey,
|
||||
address: cosmosAddress,
|
||||
hdPath: "m/44'/118'/0'/0/0",
|
||||
};
|
||||
|
||||
return { mnemonic, ethAccounts, cosmosAccounts };
|
||||
} catch (error) {
|
||||
console.error('Error creating HD wallet:', error);
|
||||
return { mnemonic: '', ethAccounts: undefined, cosmosAccounts: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
const addAccount = async (network: string): Promise<Account | undefined> => {
|
||||
try {
|
||||
const mnemonic = await getMnemonic();
|
||||
const hdNode = HDNode.fromMnemonic(mnemonic);
|
||||
const id = await getNextAccountId(network);
|
||||
const hdPath = getHDPath(network, `0'/0/${id}`);
|
||||
|
||||
const node = hdNode.derivePath(hdPath);
|
||||
const pubKey = node.publicKey;
|
||||
const address = await getAddress(network, mnemonic, `0'/0/${id}`);
|
||||
|
||||
await updateAccountIndices(network, id);
|
||||
const { counterId } = await updateGlobalCounter(network);
|
||||
|
||||
await Promise.all([
|
||||
setInternetCredentials(
|
||||
`${network}:keyServer:${counterId}`,
|
||||
`${network}:pathKey:${counterId}`,
|
||||
`0'/0/${id},${node.privateKey},${node.publicKey},${address}`,
|
||||
),
|
||||
]);
|
||||
|
||||
return { counterId, pubKey, address, hdPath };
|
||||
} catch (error) {
|
||||
console.error('Error creating account:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const addAccountFromHDPath = async (
|
||||
hdPath: string,
|
||||
): Promise<Account | undefined> => {
|
||||
try {
|
||||
const account = await accountInfoFromHDPath(hdPath);
|
||||
if (!account) {
|
||||
throw new Error('Error while creating account');
|
||||
}
|
||||
|
||||
const parts = hdPath.split('/');
|
||||
const path = parts.slice(-3).join('/');
|
||||
|
||||
const { privKey, pubKey, address, network } = account;
|
||||
|
||||
const counterId = (await updateGlobalCounter(network)).counterId;
|
||||
|
||||
await Promise.all([
|
||||
setInternetCredentials(
|
||||
`${network}:keyServer:${counterId}`,
|
||||
`${network}:pathKey:${counterId}`,
|
||||
`${path},${privKey},${pubKey},${address}`,
|
||||
),
|
||||
]);
|
||||
|
||||
return { counterId, pubKey, address, hdPath };
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const retrieveAccountsForNetwork = async (
|
||||
network: string,
|
||||
count: string,
|
||||
): Promise<Account[]> => {
|
||||
const elementsArray = count.split(',');
|
||||
|
||||
const loadedAccounts = await Promise.all(
|
||||
elementsArray.map(async i => {
|
||||
const pubKey = (await getPathKey(network, Number(i))).pubKey;
|
||||
const address = (await getPathKey(network, Number(i))).address;
|
||||
const path = (await getPathKey(network, Number(i))).path;
|
||||
const hdPath = getHDPath(network, path);
|
||||
|
||||
const account: Account = {
|
||||
counterId: Number(i),
|
||||
pubKey: pubKey,
|
||||
address: address,
|
||||
hdPath: hdPath,
|
||||
};
|
||||
return account;
|
||||
}),
|
||||
);
|
||||
|
||||
return loadedAccounts;
|
||||
};
|
||||
|
||||
const retrieveAccounts = async (): Promise<{
|
||||
ethLoadedAccounts?: Account[];
|
||||
cosmosLoadedAccounts?: Account[];
|
||||
}> => {
|
||||
const ethServer = await getInternetCredentials('eth:globalCounter');
|
||||
const ethCounter = ethServer && ethServer.password;
|
||||
const cosmosServer = await getInternetCredentials('cosmos:globalCounter');
|
||||
const cosmosCounter = cosmosServer && cosmosServer.password;
|
||||
|
||||
const ethLoadedAccounts = ethCounter
|
||||
? await retrieveAccountsForNetwork('eth', ethCounter)
|
||||
: undefined;
|
||||
const cosmosLoadedAccounts = cosmosCounter
|
||||
? await retrieveAccountsForNetwork('cosmos', cosmosCounter)
|
||||
: undefined;
|
||||
|
||||
return { ethLoadedAccounts, cosmosLoadedAccounts };
|
||||
};
|
||||
|
||||
const retrieveSingleAccount = async (network: string, address: string) => {
|
||||
let loadedAccounts;
|
||||
|
||||
switch (network) {
|
||||
case 'eth':
|
||||
const ethServer = await getInternetCredentials('eth:globalCounter');
|
||||
const ethCounter = ethServer && ethServer.password;
|
||||
|
||||
if (ethCounter) {
|
||||
loadedAccounts = await retrieveAccountsForNetwork(network, ethCounter);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'cosmos':
|
||||
const cosmosServer = await getInternetCredentials('cosmos:globalCounter');
|
||||
const cosmosCounter = cosmosServer && cosmosServer.password;
|
||||
|
||||
if (cosmosCounter) {
|
||||
loadedAccounts = await retrieveAccountsForNetwork(
|
||||
network,
|
||||
cosmosCounter,
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (loadedAccounts) {
|
||||
return loadedAccounts.find(account => account.address === address);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const resetWallet = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
resetInternetCredentials('mnemonicServer'),
|
||||
resetKeyServers('eth'),
|
||||
resetKeyServers('cosmos'),
|
||||
resetInternetCredentials('eth:accountIndices'),
|
||||
resetInternetCredentials('cosmos:accountIndices'),
|
||||
resetInternetCredentials('eth:globalCounter'),
|
||||
resetInternetCredentials('cosmos:globalCounter'),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error resetting wallet:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
createWallet,
|
||||
addAccount,
|
||||
addAccountFromHDPath,
|
||||
retrieveAccounts,
|
||||
retrieveSingleAccount,
|
||||
resetWallet,
|
||||
};
|
||||
220
utils/utils.ts
220
utils/utils.ts
@ -1,220 +0,0 @@
|
||||
/* 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 { HDNode } from 'ethers/lib/utils';
|
||||
import {
|
||||
getInternetCredentials,
|
||||
resetInternetCredentials,
|
||||
setInternetCredentials,
|
||||
} from 'react-native-keychain';
|
||||
|
||||
import { AccountData, Secp256k1HdWallet } from '@cosmjs/amino';
|
||||
import { stringToPath } from '@cosmjs/crypto';
|
||||
|
||||
const getMnemonic = async (): Promise<string> => {
|
||||
const mnemonicStore = await getInternetCredentials('mnemonicServer');
|
||||
if (!mnemonicStore) {
|
||||
throw new Error('Mnemonic not found!');
|
||||
}
|
||||
|
||||
const mnemonic = mnemonicStore.password;
|
||||
return mnemonic;
|
||||
};
|
||||
|
||||
const getHDPath = (network: string, path: string): string => {
|
||||
return network === 'eth' ? `m/44'/60'/${path}` : `m/44'/118'/${path}`;
|
||||
};
|
||||
|
||||
const getAddress = async (
|
||||
network: string,
|
||||
mnemonic: string,
|
||||
path: string,
|
||||
): Promise<string> => {
|
||||
switch (network) {
|
||||
case 'eth':
|
||||
return HDNode.fromMnemonic(mnemonic).derivePath(`m/44'/60'/${path}`)
|
||||
.address;
|
||||
case 'cosmos':
|
||||
return (await getCosmosAccounts(mnemonic, `${path}`)).data.address;
|
||||
default:
|
||||
throw new Error('Invalid wallet type');
|
||||
}
|
||||
};
|
||||
|
||||
const getCosmosAccounts = async (
|
||||
mnemonic: string,
|
||||
path: string,
|
||||
): Promise<{ cosmosWallet: Secp256k1HdWallet; data: AccountData }> => {
|
||||
const cosmosWallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, {
|
||||
hdPaths: [stringToPath(`m/44'/118'/${path}`)],
|
||||
});
|
||||
|
||||
const accountsData = await cosmosWallet.getAccounts();
|
||||
const data = accountsData[0];
|
||||
|
||||
return { cosmosWallet, data };
|
||||
};
|
||||
|
||||
const accountInfoFromHDPath = async (
|
||||
hdPath: string,
|
||||
): Promise<
|
||||
| { privKey: string; pubKey: string; address: string; network: string }
|
||||
| undefined
|
||||
> => {
|
||||
const mnemonicStore = await getInternetCredentials('mnemonicServer');
|
||||
if (!mnemonicStore) {
|
||||
throw new Error('Mnemonic not found!');
|
||||
}
|
||||
|
||||
const mnemonic = mnemonicStore.password;
|
||||
const hdNode = HDNode.fromMnemonic(mnemonic);
|
||||
const node = hdNode.derivePath(hdPath);
|
||||
|
||||
const privKey = node.privateKey;
|
||||
const pubKey = node.publicKey;
|
||||
|
||||
const parts = hdPath.split('/');
|
||||
const path = parts.slice(-3).join('/');
|
||||
const coinType = parts[2];
|
||||
|
||||
let network: string;
|
||||
let address: string;
|
||||
|
||||
switch (coinType) {
|
||||
case "60'":
|
||||
network = 'eth';
|
||||
address = node.address;
|
||||
break;
|
||||
case "118'":
|
||||
network = 'cosmos';
|
||||
address = (await getCosmosAccounts(mnemonic, path)).data.address;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid wallet type');
|
||||
}
|
||||
return { privKey, pubKey, address, network };
|
||||
};
|
||||
|
||||
const getPathKey = async (
|
||||
network: string,
|
||||
accountId: number,
|
||||
): Promise<{
|
||||
path: string;
|
||||
privKey: string;
|
||||
pubKey: string;
|
||||
address: string;
|
||||
}> => {
|
||||
const pathKeyStore = await getInternetCredentials(
|
||||
`${network}:keyServer:${accountId}`,
|
||||
);
|
||||
|
||||
if (!pathKeyStore) {
|
||||
throw new Error('Error while fetching counter');
|
||||
}
|
||||
|
||||
const pathKeyVal = pathKeyStore.password;
|
||||
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 getGlobalCounter = async (
|
||||
network: string,
|
||||
): Promise<{
|
||||
accountCounter: string;
|
||||
counterIds: number[];
|
||||
counterId: number;
|
||||
}> => {
|
||||
const counterStore = await getInternetCredentials(`${network}:globalCounter`);
|
||||
|
||||
if (!counterStore) {
|
||||
throw new Error('Error while fetching counter');
|
||||
}
|
||||
|
||||
let accountCounter = counterStore.password;
|
||||
const counterIds = accountCounter.split(',').map(Number);
|
||||
const counterId = counterIds[counterIds.length - 1] + 1;
|
||||
|
||||
return { accountCounter, counterIds, counterId };
|
||||
};
|
||||
|
||||
const updateGlobalCounter = async (
|
||||
network: string,
|
||||
): Promise<{ accountCounter: string; counterId: number }> => {
|
||||
const globalCounterData = await getGlobalCounter(network);
|
||||
const accountCounter = globalCounterData.accountCounter;
|
||||
const counterId = globalCounterData.counterId;
|
||||
const updatedAccountCounter = `${accountCounter},${counterId.toString()}`;
|
||||
|
||||
await resetInternetCredentials(`${network}:globalCounter`);
|
||||
await setInternetCredentials(
|
||||
`${network}:globalCounter`,
|
||||
`${network}Global`,
|
||||
updatedAccountCounter,
|
||||
);
|
||||
|
||||
return { accountCounter: updatedAccountCounter, counterId };
|
||||
};
|
||||
|
||||
const getNextAccountId = async (network: string): Promise<number> => {
|
||||
const idStore = await getInternetCredentials(`${network}:accountIndices`);
|
||||
if (!idStore) {
|
||||
throw new Error('Account id not found');
|
||||
}
|
||||
|
||||
const accountIds = idStore.password;
|
||||
const ids = accountIds.split(',').map(Number);
|
||||
return ids[ids.length - 1] + 1;
|
||||
};
|
||||
|
||||
const updateAccountIndices = async (
|
||||
network: string,
|
||||
id: number,
|
||||
): Promise<void> => {
|
||||
const idStore = await getInternetCredentials(`${network}:accountIndices`);
|
||||
if (!idStore) {
|
||||
throw new Error('Account id not found');
|
||||
}
|
||||
|
||||
const updatedIndices = `${idStore.password},${id.toString()}`;
|
||||
await resetInternetCredentials(`${network}:accountIndices`);
|
||||
await setInternetCredentials(
|
||||
`${network}:accountIndices`,
|
||||
`${network}Counter`,
|
||||
updatedIndices,
|
||||
);
|
||||
};
|
||||
|
||||
const resetKeyServers = async (prefix: string) => {
|
||||
const idStore = await getInternetCredentials(`${prefix}:accountIndices`);
|
||||
if (!idStore) {
|
||||
throw new Error('Account id not found.');
|
||||
}
|
||||
|
||||
const accountIds = idStore.password;
|
||||
const ids = accountIds.split(',').map(Number);
|
||||
const id = ids[ids.length - 1];
|
||||
|
||||
for (let i = 0; i <= id; i++) {
|
||||
await resetInternetCredentials(`${prefix}:keyServer:${i}`);
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
accountInfoFromHDPath,
|
||||
getCosmosAccounts,
|
||||
getMnemonic,
|
||||
getPathKey,
|
||||
getNextAccountId,
|
||||
updateGlobalCounter,
|
||||
updateAccountIndices,
|
||||
getHDPath,
|
||||
getAddress,
|
||||
resetKeyServers,
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user