import React, { useCallback, useEffect, useState } from "react"; import { useForm, Controller, useWatch, FieldErrors } from "react-hook-form"; import { TextInput, HelperText } from "react-native-paper"; 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 { Divider, Grid } from "@mui/material"; import { LoadingButton } from "@mui/lab"; import { StackParamsList } from "../types"; import { SelectNetworkType } from "../components/SelectNetworkType"; import { addNewNetwork } from "../utils/accounts"; import { useNetworks } from "../context/NetworksContext"; import { EIP155, CHAINID_DEBOUNCE_DELAY, EMPTY_FIELD_ERROR, INVALID_URL_ERROR, IS_NUMBER_REGEX, } from "../utils/constants"; import ETH_CHAINS from "../assets/ethereum-chains.json"; import { Layout } from "../components/Layout"; 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>(); const { setNetworksData } = useNetworks(); const [namespace, setNamespace] = useState(EIP155); const networksFormDataSchema = namespace === EIP155 ? ethNetworkDataSchema : cosmosNetworkDataSchema; const { control, formState: { errors, isSubmitting }, handleSubmit, setValue, reset, } = useForm>({ 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(import.meta.env.REACT_APP_DEFAULT_GAS_PRICE), ), ); }, CHAINID_DEBOUNCE_DELAY); const submit = useCallback( async (data: z.infer) => { const newNetworkData = { ...data, namespace, isDefault: false, }; const retrievedNetworksData = await addNewNetwork(newNetworkData); setNetworksData(retrievedNetworksData); navigation.navigate("Home"); }, [navigation, namespace, setNetworksData], ); useEffect(() => { fetchChainDetails(watchChainId); }, [watchChainId, fetchChainDetails]); useEffect(() => { reset(); }, [namespace, reset]); return ( ( <> onChange(textValue)} /> {errors.chainId?.message} )} /> ( <> onChange(textValue)} /> {errors.networkName?.message} )} /> ( <> onChange(textValue)} /> {errors.rpcUrl?.message} )} /> ( <> onChange(textValue)} /> {errors.blockExplorerUrl?.message} )} /> ( <> {errors.coinType?.message} )} /> {namespace === EIP155 ? ( ( <> onChange(textValue)} /> { ( errors as FieldErrors< z.infer > ).currencySymbol?.message } )} /> ) : ( <> ( <> onChange(textValue)} /> { ( errors as FieldErrors< z.infer > ).nativeDenom?.message } )} /> ( <> onChange(textValue)} /> { ( errors as FieldErrors< z.infer > ).addressPrefix?.message } )} /> ( <> { ( errors as FieldErrors< z.infer > ).gasPrice?.message } )} /> )} {isSubmitting ? "Adding" : "Submit"} ); }; export default AddNetwork;