diff --git a/.env.example b/.env.example index 7171052..2e44075 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,5 @@ REACT_APP_DEFAULT_GAS_PRICE=0.025 REACT_APP_GAS_ADJUSTMENT=2 REACT_APP_LACONICD_RPC_URL=https://laconicd-sapo.laconic.com -REACT_APP_ZENITHD_RPC_URL= - # Example: https://example-url-1.com,https://example-url-2.com REACT_APP_ALLOWED_URLS= diff --git a/package.json b/package.json index 862ee0a..1bd8843 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-wallet", - "version": "0.1.6", + "version": "0.1.7", "private": true, "dependencies": { "@cerc-io/registry-sdk": "^0.2.5", diff --git a/src/App.tsx b/src/App.tsx index fd8c034..9cbe0bf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -45,6 +45,7 @@ import SignRequestEmbed from "./screens/SignRequestEmbed"; import useAddAccountEmbed from "./hooks/useAddAccountEmbed"; import useExportPKEmbed from "./hooks/useExportPrivateKeyEmbed"; import { SignTxEmbed } from "./screens/SignTxEmbed"; +import useCreateNetwork from "./hooks/useCreateNetwork"; const Stack = createStackNavigator(); @@ -287,6 +288,7 @@ const App = (): React.JSX.Element => { useWebViewHandler(); useAddAccountEmbed(); useExportPKEmbed(); + useCreateNetwork(); return ( diff --git a/src/hooks/useCreateNetwork.ts b/src/hooks/useCreateNetwork.ts new file mode 100644 index 0000000..e86a67e --- /dev/null +++ b/src/hooks/useCreateNetwork.ts @@ -0,0 +1,99 @@ +import { useEffect, useCallback } from "react"; + +import { addNewNetwork, createWallet, checkNetworkForChainID, isWalletCreated } from "../utils/accounts"; +import { useNetworks } from "../context/NetworksContext"; +import { NETWORK_ADDED_RESPONSE, NETWORK_ADD_FAILED_RESPONSE, NETWORK_ALREADY_EXISTS_RESPONSE, REQUEST_ADD_NETWORK } from "../utils/constants"; +import { NetworksFormData } from "../types"; +import { sendMessage } from "../utils/misc"; + +const REACT_APP_ALLOWED_URLS = import.meta.env.REACT_APP_ALLOWED_URLS; + +const useCreateNetwork = () => { + const { networksData, setNetworksData } = useNetworks(); + + const getOrCreateNetwork = useCallback( + async (chainId: string, networkData: NetworksFormData, sourceOrigin?: string) => { + if (!sourceOrigin) { + return; + } + + try { + const isCreated = await isWalletCreated(); + + if (!isCreated) { + console.log("Wallet not found, creating wallet..."); + await createWallet(networksData); + } + + const isNetworkPresent = await checkNetworkForChainID(chainId); + + if (!isNetworkPresent) { + console.log("ChainId not found. Adding network"); + + const resolvedNetworkData = { + chainId, + namespace: networkData.namespace, + networkName: networkData.networkName, + rpcUrl: networkData.rpcUrl, + blockExplorerUrl: networkData.blockExplorerUrl || "", + addressPrefix: networkData.addressPrefix || "", + coinType: networkData.coinType, + nativeDenom: networkData.nativeDenom || "", + gasPrice: networkData.gasPrice || String(import.meta.env.REACT_APP_DEFAULT_GAS_PRICE), + currencySymbol: networkData.currencySymbol || "", + isDefault: false, + }; + + const retrievedNetworksData = await addNewNetwork(resolvedNetworkData); + setNetworksData(retrievedNetworksData); + + sendMessage(window.parent, NETWORK_ADDED_RESPONSE, { + type: NETWORK_ADDED_RESPONSE, + chainId + }, sourceOrigin); + } else { + sendMessage(window.parent, NETWORK_ALREADY_EXISTS_RESPONSE, { + type: NETWORK_ALREADY_EXISTS_RESPONSE, + chainId + }, sourceOrigin); + } + } catch (error) { + console.error("Error in getOrCreateNetwork:", error); + sendMessage(window.parent, NETWORK_ADD_FAILED_RESPONSE, { + type: NETWORK_ADD_FAILED_RESPONSE, + message: error instanceof Error ? error.message : "Unknown error" + }, sourceOrigin); + } + }, + [networksData, setNetworksData] + ); + + useEffect(() => { + const handleCreateNetwork = async (event: MessageEvent) => { + if (event.data.type !== REQUEST_ADD_NETWORK) return; + + if (!REACT_APP_ALLOWED_URLS) { + console.log("Allowed URLs are not set"); + return; + } + + const allowedUrls = REACT_APP_ALLOWED_URLS.split(",").map(url => url.trim()); + + if (!allowedUrls.includes(event.origin)) { + console.log("Unauthorized app."); + return; + } + + const { chainId, networkData } = event.data; + + await getOrCreateNetwork(chainId, networkData, event.origin); + }; + + window.addEventListener("message", handleCreateNetwork); + return () => { + window.removeEventListener("message", handleCreateNetwork); + }; + }, [getOrCreateNetwork]); +}; + +export default useCreateNetwork; diff --git a/src/hooks/useGetOrCreateAccounts.ts b/src/hooks/useGetOrCreateAccounts.ts index 62005c7..ce6b950 100644 --- a/src/hooks/useGetOrCreateAccounts.ts +++ b/src/hooks/useGetOrCreateAccounts.ts @@ -1,6 +1,6 @@ import { useEffect, useCallback } from "react"; -import { createWallet } from "../utils/accounts"; +import { createWallet, isWalletCreated } from "../utils/accounts"; import { sendMessage } from "../utils/misc"; import useAccountsData from "./useAccountsData"; import { useNetworks } from "../context/NetworksContext"; @@ -16,14 +16,15 @@ const useGetOrCreateAccounts = () => { // Wrap the function in useCallback to prevent recreation on each render const getOrCreateAccountsForChain = useCallback(async (chainId: string) => { - let accountsData = await getAccountsData(chainId); + const isCreated = await isWalletCreated(); - if (accountsData.length === 0) { + if (!isCreated) { console.log("Accounts not found, creating wallet..."); await createWallet(networksData); - accountsData = await getAccountsData(chainId); } + const accountsData = await getAccountsData(chainId); + // Update the AccountsContext with the new accounts setAccounts(accountsData); diff --git a/src/import-meta-env.d.ts b/src/import-meta-env.d.ts index 0a6dc72..388d2b7 100644 --- a/src/import-meta-env.d.ts +++ b/src/import-meta-env.d.ts @@ -5,7 +5,6 @@ interface ImportMetaEnv { readonly REACT_APP_DEFAULT_GAS_PRICE: string; readonly REACT_APP_GAS_ADJUSTMENT: string; readonly REACT_APP_LACONICD_RPC_URL: string; - readonly REACT_APP_ZENITHD_RPC_URL: string; readonly REACT_APP_ALLOWED_URLS: string; } diff --git a/src/screens/AddNetwork.tsx b/src/screens/AddNetwork.tsx index 338ec3f..e2d9e71 100644 --- a/src/screens/AddNetwork.tsx +++ b/src/screens/AddNetwork.tsx @@ -1,8 +1,6 @@ import React, { useCallback, useEffect, useState } from "react"; import { useForm, Controller, useWatch, FieldErrors } from "react-hook-form"; import { TextInput, 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"; @@ -10,27 +8,21 @@ 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 { addAccountsForNetwork, getNextAccountId, storeNetworkData } from "../utils/accounts"; +import { addNewNetwork } 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 { getCosmosAccountByHDPath } from "../utils/accounts"; import ETH_CHAINS from "../assets/ethereum-chains.json"; -import { - getInternetCredentials, - setInternetCredentials, -} from "../utils/key-store"; -import { Divider, Grid } from "@mui/material"; -import { LoadingButton } from "@mui/lab"; import { Layout } from "../components/Layout"; const ethNetworkDataSchema = z.object({ @@ -143,73 +135,10 @@ const AddNetwork = () => { isDefault: false, }; - const mnemonicServer = await getInternetCredentials("mnemonicServer"); - const mnemonic = mnemonicServer; + const retrievedNetworksData = await addNewNetwork(newNetworkData); - 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 getCosmosAccountByHDPath( - mnemonic, - hdPath, - (newNetworkData as z.infer) - .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); - // Get number of accounts in first network - const nextAccountId = await getNextAccountId( - `${retrievedNetworksData[0].namespace}:${retrievedNetworksData[0].chainId}`, - ); - - const selectedNetwork = retrievedNetworksData.find( - (network) => network.chainId === newNetworkData.chainId, - ); - - 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", - ), - ]); - - await addAccountsForNetwork(selectedNetwork!, nextAccountId - 1); - navigation.navigate("Home"); }, [navigation, namespace, setNetworksData], diff --git a/src/types.ts b/src/types.ts index ae93310..f75f10b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -64,7 +64,7 @@ export type NetworksFormData = { namespace: string; nativeDenom?: string; addressPrefix?: string; - coinType?: string; + coinType: string; gasPrice?: string; isDefault: boolean; }; diff --git a/src/utils/accounts.ts b/src/utils/accounts.ts index b65e63d..380922f 100644 --- a/src/utils/accounts.ts +++ b/src/utils/accounts.ts @@ -160,6 +160,77 @@ const addAccountFromHDPath = async ( } }; +const addNewNetwork = async ( + newNetworkData: NetworksFormData +): Promise => { + 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 getCosmosAccountByHDPath( + mnemonic, + hdPath, + newNetworkData.addressPrefix, + ) + ).data.address; + break; + + default: + throw new Error("Unsupported namespace"); + } + + const accountInfo = `${hdPath},${node.privateKey},${node.publicKey},${address}`; + + 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", + ), + ]); + + const retrievedNetworksData = await storeNetworkData(newNetworkData); + + // Get number of accounts in first network + const nextAccountId = await getNextAccountId( + `${retrievedNetworksData[0].namespace}:${retrievedNetworksData[0].chainId}`, + ); + + const selectedNetwork = retrievedNetworksData.find( + (network) => network.chainId === newNetworkData.chainId, + ); + + await addAccountsForNetwork(selectedNetwork!, nextAccountId - 1); + + return retrievedNetworksData; +} + const storeNetworkData = async ( networkData: NetworksFormData, ): Promise => { @@ -180,21 +251,25 @@ const storeNetworkData = async ( networkId: String(networkId), }, ]; + await setInternetCredentials( 'networks', '_', JSON.stringify(updatedNetworks), ); + return updatedNetworks; }; const retrieveNetworksData = async (): Promise => { const networks = await getInternetCredentials('networks'); - if(!networks){ + if (!networks) { return []; } + const parsedNetworks: NetworksDataState[] = JSON.parse(networks); + return parsedNetworks; }; @@ -217,6 +292,7 @@ export const retrieveAccountsForNetwork = async ( address, hdPath: path, }; + return account; }), ); @@ -234,6 +310,7 @@ const retrieveAccounts = async ( if (!accountIndices) { return; } + const loadedAccounts = await retrieveAccountsForNetwork( `${currentNetworkData.namespace}:${currentNetworkData.chainId}`, accountIndices, @@ -368,6 +445,28 @@ const getCosmosAccountByHDPath = async ( return { cosmosWallet, data }; }; +const checkNetworkForChainID = async ( + chainId: string, +): Promise => { + const networks = await getInternetCredentials('networks'); + + if (!networks) { + return false; + } + + const networksData: NetworksFormData[] = JSON.parse(networks); + + return networksData.some((network) => network.chainId === chainId); +} + +const isWalletCreated = async ( +): Promise => { + const mnemonicServer = await getInternetCredentials("mnemonicServer"); + const mnemonic = mnemonicServer; + + return mnemonic !== null; +}; + export { createWallet, addAccount, @@ -382,4 +481,7 @@ export { getNextAccountId, updateAccountCounter, getCosmosAccountByHDPath, + addNewNetwork, + checkNetworkForChainID, + isWalletCreated }; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index a657f5a..3f93d29 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -18,30 +18,6 @@ export const DEFAULT_NETWORKS: NetworksFormData[] = [ gasPrice: '0.001', isDefault: true, }, - { - chainId: 'zenith-testnet-stage0', - networkName: 'zenithd stage 0 testnet', - namespace: COSMOS, - rpcUrl: import.meta.env.REACT_APP_ZENITHD_RPC_URL || '', - blockExplorerUrl: '', - nativeDenom: 'znt', - addressPrefix: 'zenith', - coinType: '118', - gasPrice: '0.01', - isDefault: true, - }, - { - chainId: 'zenith-testnet-stage1', - networkName: 'zenithd stage 1 testnet', - namespace: COSMOS, - rpcUrl: import.meta.env.REACT_APP_ZENITHD_RPC_URL || '', - blockExplorerUrl: '', - nativeDenom: 'znt', - addressPrefix: 'zenith', - coinType: '118', - gasPrice: '0.01', - isDefault: true, - }, { chainId: 'laconic_9000-1', networkName: 'laconicd', @@ -98,6 +74,7 @@ export const REQUEST_ACCOUNT_PK = 'REQUEST_ACCOUNT_PK'; export const REQUEST_ADD_ACCOUNT = 'REQUEST_ADD_ACCOUNT'; export const AUTO_SIGN_IN = 'AUTO_SIGN_IN'; export const CHECK_BALANCE = 'CHECK_BALANCE'; +export const REQUEST_ADD_NETWORK = 'REQUEST_ADD_NETWORK'; // iframe response types export const COSMOS_ACCOUNTS_RESPONSE = 'COSMOS_ACCOUNTS_RESPONSE'; @@ -109,3 +86,6 @@ export const ACCOUNT_PK_RESPONSE = 'ACCOUNT_PK_RESPONSE'; export const ADD_ACCOUNT_RESPONSE = 'ADD_ACCOUNT_RESPONSE'; export const WALLET_ACCOUNTS_DATA = 'WALLET_ACCOUNTS_DATA'; export const IS_SUFFICIENT = 'IS_SUFFICIENT'; +export const NETWORK_ADDED_RESPONSE = "NETWORK_ADDED_RESPONSE"; +export const NETWORK_ALREADY_EXISTS_RESPONSE = "NETWORK_ALREADY_EXISTS_RESPONSE"; +export const NETWORK_ADD_FAILED_RESPONSE = "NETWORK_ADD_FAILED_RESPONSE"; diff --git a/stack/stack-orchestrator/config/app/run.sh b/stack/stack-orchestrator/config/app/run.sh index be6b210..a9b2f31 100755 --- a/stack/stack-orchestrator/config/app/run.sh +++ b/stack/stack-orchestrator/config/app/run.sh @@ -10,7 +10,6 @@ echo "WALLET_CONNECT_ID: ${WALLET_CONNECT_ID}" echo "CERC_DEFAULT_GAS_PRICE: ${CERC_DEFAULT_GAS_PRICE}" echo "CERC_GAS_ADJUSTMENT: ${CERC_GAS_ADJUSTMENT}" echo "CERC_LACONICD_RPC_URL: ${CERC_LACONICD_RPC_URL}" -echo "CERC_ZENITHD_RPC_URL: ${CERC_ZENITHD_RPC_URL}" echo "CERC_ALLOWED_URLS: ${CERC_ALLOWED_URLS}" # Build with required env @@ -18,7 +17,6 @@ export REACT_APP_WALLET_CONNECT_PROJECT_ID=$WALLET_CONNECT_ID export REACT_APP_DEFAULT_GAS_PRICE=$CERC_DEFAULT_GAS_PRICE export REACT_APP_GAS_ADJUSTMENT=$CERC_GAS_ADJUSTMENT export REACT_APP_LACONICD_RPC_URL=$CERC_LACONICD_RPC_URL -export REACT_APP_ZENITHD_RPC_URL=$CERC_ZENITHD_RPC_URL export REACT_APP_ALLOWED_URLS=$CERC_ALLOWED_URLS # Set env variables in build