forked from cerc-io/laconic-wallet-web
* Basic setup for react native web * Comment unsupported react-native code * Add interface for keychain methods * Fix storeNetworkData method * Use mui dialog component * Modify key store file name * Fix add network and edit network screens * Fix sign message screen * Use light theme --------- Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
419 lines
12 KiB
TypeScript
419 lines
12 KiB
TypeScript
import React, { useCallback, useEffect, useState } from 'react';
|
|
import { View } from 'react-native';
|
|
import { useForm, Controller, useWatch, FieldErrors } from 'react-hook-form';
|
|
import { TextInput, Button, HelperText } from 'react-native-paper';
|
|
|
|
import { HDNode } from 'ethers/lib/utils';
|
|
import { chains } from 'chain-registry';
|
|
import { useDebouncedCallback } from 'use-debounce';
|
|
import { z } from 'zod';
|
|
|
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
import { useNavigation } from '@react-navigation/native';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
|
import { StackParamsList } from '../types';
|
|
import { SelectNetworkType } from '../components/SelectNetworkType';
|
|
import { storeNetworkData } from '../utils/accounts';
|
|
import { useNetworks } from '../context/NetworksContext';
|
|
import {
|
|
COSMOS,
|
|
EIP155,
|
|
CHAINID_DEBOUNCE_DELAY,
|
|
EMPTY_FIELD_ERROR,
|
|
INVALID_URL_ERROR,
|
|
IS_NUMBER_REGEX,
|
|
} from '../utils/constants';
|
|
import { getCosmosAccounts } from '../utils/accounts';
|
|
import ETH_CHAINS from '../assets/ethereum-chains.json';
|
|
import {
|
|
getInternetCredentials,
|
|
setInternetCredentials,
|
|
} from '../utils/key-store';
|
|
|
|
const ethNetworkDataSchema = z.object({
|
|
chainId: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
|
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
|
rpcUrl: z.string().url({ message: INVALID_URL_ERROR }),
|
|
blockExplorerUrl: z
|
|
.string()
|
|
.url({ message: INVALID_URL_ERROR })
|
|
.or(z.literal('')),
|
|
coinType: z
|
|
.string()
|
|
.nonempty({ message: EMPTY_FIELD_ERROR })
|
|
.regex(IS_NUMBER_REGEX),
|
|
currencySymbol: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
|
});
|
|
|
|
const cosmosNetworkDataSchema = z.object({
|
|
chainId: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
|
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
|
rpcUrl: z.string().url({ message: INVALID_URL_ERROR }),
|
|
blockExplorerUrl: z
|
|
.string()
|
|
.url({ message: INVALID_URL_ERROR })
|
|
.or(z.literal('')),
|
|
coinType: z
|
|
.string()
|
|
.nonempty({ message: EMPTY_FIELD_ERROR })
|
|
.regex(IS_NUMBER_REGEX),
|
|
nativeDenom: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
|
addressPrefix: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
|
|
gasPrice: z
|
|
.string()
|
|
.nonempty({ message: EMPTY_FIELD_ERROR })
|
|
.regex(/^\d+(\.\d+)?$/),
|
|
});
|
|
|
|
const AddNetwork = () => {
|
|
const navigation =
|
|
useNavigation<NativeStackNavigationProp<StackParamsList>>();
|
|
|
|
const { setNetworksData } = useNetworks();
|
|
|
|
const [namespace, setNamespace] = useState<string>(EIP155);
|
|
|
|
const networksFormDataSchema =
|
|
namespace === EIP155 ? ethNetworkDataSchema : cosmosNetworkDataSchema;
|
|
|
|
const {
|
|
control,
|
|
formState: { errors, isSubmitting },
|
|
handleSubmit,
|
|
setValue,
|
|
reset,
|
|
} = useForm<z.infer<typeof networksFormDataSchema>>({
|
|
mode: 'onChange',
|
|
resolver: zodResolver(networksFormDataSchema),
|
|
});
|
|
|
|
const watchChainId = useWatch({
|
|
control,
|
|
name: 'chainId',
|
|
});
|
|
|
|
const updateNetworkType = (newNetworkType: string) => {
|
|
setNamespace(newNetworkType);
|
|
};
|
|
|
|
const fetchChainDetails = useDebouncedCallback((chainId: string) => {
|
|
if (namespace === EIP155) {
|
|
const ethChainDetails = ETH_CHAINS.find(
|
|
chain => chain.chainId === Number(chainId),
|
|
);
|
|
if (!ethChainDetails) {
|
|
return;
|
|
}
|
|
setValue('networkName', ethChainDetails.name);
|
|
setValue('rpcUrl', ethChainDetails.rpc[0]);
|
|
setValue('blockExplorerUrl', ethChainDetails.explorers?.[0].url || '');
|
|
setValue('coinType', String(ethChainDetails.slip44 ?? '60'));
|
|
setValue('currencySymbol', ethChainDetails.nativeCurrency.symbol);
|
|
return;
|
|
}
|
|
const cosmosChainDetails = chains.find(
|
|
({ chain_id }) => chain_id === chainId,
|
|
);
|
|
if (!cosmosChainDetails) {
|
|
return;
|
|
}
|
|
setValue('networkName', cosmosChainDetails.pretty_name);
|
|
setValue('rpcUrl', cosmosChainDetails.apis?.rpc?.[0]?.address || '');
|
|
setValue('blockExplorerUrl', cosmosChainDetails.explorers?.[0].url || '');
|
|
setValue('addressPrefix', cosmosChainDetails.bech32_prefix);
|
|
setValue('coinType', String(cosmosChainDetails.slip44 ?? '118'));
|
|
setValue('nativeDenom', cosmosChainDetails.fees?.fee_tokens[0].denom || '');
|
|
setValue(
|
|
'gasPrice',
|
|
String(
|
|
cosmosChainDetails.fees?.fee_tokens[0].average_gas_price ||
|
|
String(process.env.DEFAULT_GAS_PRICE),
|
|
),
|
|
);
|
|
}, CHAINID_DEBOUNCE_DELAY);
|
|
|
|
const submit = useCallback(
|
|
async (data: z.infer<typeof networksFormDataSchema>) => {
|
|
const newNetworkData = {
|
|
...data,
|
|
namespace,
|
|
isDefault: false,
|
|
};
|
|
|
|
const mnemonicServer = await getInternetCredentials('mnemonicServer');
|
|
const mnemonic = mnemonicServer;
|
|
|
|
if (!mnemonic) {
|
|
throw new Error('Mnemonic not found');
|
|
}
|
|
|
|
const hdNode = HDNode.fromMnemonic(mnemonic);
|
|
|
|
const hdPath = `m/44'/${newNetworkData.coinType}'/0'/0/0`;
|
|
const node = hdNode.derivePath(hdPath);
|
|
let address;
|
|
|
|
switch (newNetworkData.namespace) {
|
|
case EIP155:
|
|
address = node.address;
|
|
break;
|
|
|
|
case COSMOS:
|
|
address = (
|
|
await getCosmosAccounts(
|
|
mnemonic,
|
|
hdPath,
|
|
(newNetworkData as z.infer<typeof cosmosNetworkDataSchema>)
|
|
.addressPrefix,
|
|
)
|
|
).data.address;
|
|
break;
|
|
|
|
default:
|
|
throw new Error('Unsupported namespace');
|
|
}
|
|
|
|
const accountInfo = `${hdPath},${node.privateKey},${node.publicKey},${address}`;
|
|
|
|
const retrievedNetworksData = await storeNetworkData(newNetworkData);
|
|
setNetworksData(retrievedNetworksData);
|
|
|
|
await Promise.all([
|
|
setInternetCredentials(
|
|
`accounts/${newNetworkData.namespace}:${newNetworkData.chainId}/0`,
|
|
'_',
|
|
accountInfo,
|
|
),
|
|
setInternetCredentials(
|
|
`addAccountCounter/${newNetworkData.namespace}:${newNetworkData.chainId}`,
|
|
'_',
|
|
'1',
|
|
),
|
|
setInternetCredentials(
|
|
`accountIndices/${newNetworkData.namespace}:${newNetworkData.chainId}`,
|
|
'_',
|
|
'0',
|
|
),
|
|
]);
|
|
|
|
navigation.navigate('Laconic');
|
|
},
|
|
[navigation, namespace, setNetworksData],
|
|
);
|
|
|
|
useEffect(() => {
|
|
fetchChainDetails(watchChainId);
|
|
}, [watchChainId, fetchChainDetails]);
|
|
|
|
useEffect(() => {
|
|
reset();
|
|
}, [namespace, reset]);
|
|
|
|
return (
|
|
<View>
|
|
<SelectNetworkType updateNetworkType={updateNetworkType} />
|
|
|
|
<Controller
|
|
control={control}
|
|
name="chainId"
|
|
defaultValue=""
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<>
|
|
<TextInput
|
|
mode="outlined"
|
|
value={value}
|
|
label="Chain ID"
|
|
onBlur={onBlur}
|
|
onChangeText={textValue => onChange(textValue)}
|
|
/>
|
|
<HelperText type="error">{errors.chainId?.message}</HelperText>
|
|
</>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
defaultValue=""
|
|
name="networkName"
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<>
|
|
<TextInput
|
|
mode="outlined"
|
|
label="Network Name"
|
|
value={value}
|
|
onBlur={onBlur}
|
|
onChangeText={textValue => onChange(textValue)}
|
|
/>
|
|
<HelperText type="error">{errors.networkName?.message}</HelperText>
|
|
</>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="rpcUrl"
|
|
defaultValue=""
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<>
|
|
<TextInput
|
|
mode="outlined"
|
|
label="New RPC URL"
|
|
onBlur={onBlur}
|
|
value={value}
|
|
onChangeText={textValue => onChange(textValue)}
|
|
/>
|
|
<HelperText type="error">{errors.rpcUrl?.message}</HelperText>
|
|
</>
|
|
)}
|
|
/>
|
|
|
|
<Controller
|
|
control={control}
|
|
defaultValue=""
|
|
name="blockExplorerUrl"
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<>
|
|
<TextInput
|
|
mode="outlined"
|
|
value={value}
|
|
label="Block Explorer URL (Optional)"
|
|
onBlur={onBlur}
|
|
onChangeText={textValue => onChange(textValue)}
|
|
/>
|
|
<HelperText type="error">
|
|
{errors.blockExplorerUrl?.message}
|
|
</HelperText>
|
|
</>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="coinType"
|
|
defaultValue=""
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<>
|
|
<TextInput
|
|
mode="outlined"
|
|
value={value}
|
|
label="Coin Type"
|
|
onBlur={onBlur}
|
|
onChangeText={onChange}
|
|
/>
|
|
<HelperText type="error">{errors.coinType?.message}</HelperText>
|
|
</>
|
|
)}
|
|
/>
|
|
{namespace === EIP155 ? (
|
|
<Controller
|
|
control={control}
|
|
name="currencySymbol"
|
|
defaultValue=""
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<>
|
|
<TextInput
|
|
mode="outlined"
|
|
value={value}
|
|
label="Currency Symbol"
|
|
onBlur={onBlur}
|
|
onChangeText={textValue => onChange(textValue)}
|
|
/>
|
|
<HelperText type="error">
|
|
{
|
|
(errors as FieldErrors<z.infer<typeof ethNetworkDataSchema>>)
|
|
.currencySymbol?.message
|
|
}
|
|
</HelperText>
|
|
</>
|
|
)}
|
|
/>
|
|
) : (
|
|
<>
|
|
<Controller
|
|
control={control}
|
|
name="nativeDenom"
|
|
defaultValue=""
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<>
|
|
<TextInput
|
|
mode="outlined"
|
|
value={value}
|
|
label="Native Denom"
|
|
onBlur={onBlur}
|
|
onChangeText={textValue => onChange(textValue)}
|
|
/>
|
|
<HelperText type="error">
|
|
{
|
|
(
|
|
errors as FieldErrors<
|
|
z.infer<typeof cosmosNetworkDataSchema>
|
|
>
|
|
).nativeDenom?.message
|
|
}
|
|
</HelperText>
|
|
</>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="addressPrefix"
|
|
defaultValue=""
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<>
|
|
<TextInput
|
|
mode="outlined"
|
|
value={value}
|
|
label="Address Prefix"
|
|
onBlur={onBlur}
|
|
onChangeText={textValue => onChange(textValue)}
|
|
/>
|
|
<HelperText type="error">
|
|
{
|
|
(
|
|
errors as FieldErrors<
|
|
z.infer<typeof cosmosNetworkDataSchema>
|
|
>
|
|
).addressPrefix?.message
|
|
}
|
|
</HelperText>
|
|
</>
|
|
)}
|
|
/>
|
|
<Controller
|
|
control={control}
|
|
name="gasPrice"
|
|
defaultValue=""
|
|
render={({ field: { onChange, onBlur, value } }) => (
|
|
<>
|
|
<TextInput
|
|
mode="outlined"
|
|
value={value}
|
|
label="Gas Price"
|
|
onBlur={onBlur}
|
|
onChangeText={onChange}
|
|
/>
|
|
<HelperText type="error">
|
|
{
|
|
(
|
|
errors as FieldErrors<
|
|
z.infer<typeof cosmosNetworkDataSchema>
|
|
>
|
|
).gasPrice?.message
|
|
}
|
|
</HelperText>
|
|
</>
|
|
)}
|
|
/>
|
|
</>
|
|
)}
|
|
<Button
|
|
mode="contained"
|
|
loading={isSubmitting}
|
|
disabled={isSubmitting}
|
|
onPress={handleSubmit(submit)}>
|
|
{isSubmitting ? 'Adding' : 'Submit'}
|
|
</Button>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export default AddNetwork;
|