diff --git a/tools/walletextension/frontend/src/api/ethRequests.ts b/tools/walletextension/frontend/src/api/ethRequests.ts new file mode 100644 index 0000000000..b4ba8ed933 --- /dev/null +++ b/tools/walletextension/frontend/src/api/ethRequests.ts @@ -0,0 +1,152 @@ +import { + nativeCurrency, + tenChainIDDecimal, + tenChainIDHex, + tenscanLink, +} from "@/lib/constants"; +import { getNetworkName, getRandomIntAsString, isTenChain } from "@/lib/utils"; +import { requestMethods } from "@/routes"; +import { ethers } from "ethers"; +import { accountIsAuthenticated, authenticateUser } from "./gateway"; + +const { ethereum } = typeof window !== "undefined" ? window : ({} as any); + +const typedData = { + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + ], + Authentication: [{ name: "Encryption Token", type: "address" }], + }, + primaryType: "Authentication", + domain: { + name: "Ten", + version: "1.0", + chainId: tenChainIDDecimal, + }, + message: { + "Encryption Token": "0x", + }, +}; + +export const switchToTenNetwork = async () => { + if (!ethereum) { + throw new Error("No ethereum object found"); + } + try { + await ethereum.request({ + method: requestMethods.switchNetwork, + params: [{ chainId: tenChainIDHex }], + }); + + return 0; + } catch (error: any) { + return error.code; + } +}; + +export const connectAccounts = async () => { + if (!ethereum) { + throw new Error("No ethereum object found"); + } + try { + return await ethereum.request({ + method: requestMethods.connectAccounts, + }); + } catch (error) { + console.error(error); + throw error; + } +}; + +export const getSignature = async (account: string, data: any) => { + if (!ethereum) { + throw new Error("No ethereum object found"); + } + return await ethereum.request({ + method: requestMethods.signTypedData, + params: [account, JSON.stringify(data)], + }); +}; + +export const getUserID = async (provider: ethers.providers.Web3Provider) => { + if (!provider) { + return null; + } + + try { + if (await isTenChain()) { + const id = await provider.send(requestMethods.getStorageAt, [ + "getUserID", + getRandomIntAsString(0, 1000), + null, + ]); + return id; + } else { + return null; + } + } catch (e: any) { + console.error(e); + throw e; + } +}; + +export async function addNetworkToMetaMask(rpcUrls: string[]) { + if (!ethereum) { + throw new Error("No ethereum object found"); + } + try { + await ethereum.request({ + method: requestMethods.addNetwork, + params: [ + { + chainId: tenChainIDHex, + chainName: getNetworkName(), + nativeCurrency, + rpcUrls, + blockExplorerUrls: [tenscanLink], + }, + ], + }); + return true; + } catch (error) { + console.error(error); + throw error; + } +} + +export async function authenticateAccountWithTenGatewayEIP712( + userID: string, + account: string +): Promise { + if (!userID) { + return; + } + try { + const isAuthenticated = await accountIsAuthenticated(userID, account); + if (isAuthenticated.status) { + return { + status: true, + message: "Account already authenticated", + }; + } + const data = { + ...typedData, + message: { + ...typedData.message, + "Encryption Token": "0x" + userID, + }, + }; + const signature = await getSignature(account, data); + + const auth = await authenticateUser(userID, { + signature, + address: account, + }); + return auth; + } catch (error) { + throw error; + } +} diff --git a/tools/walletextension/frontend/src/api/gateway.ts b/tools/walletextension/frontend/src/api/gateway.ts index 35a0610329..244f246ffb 100644 --- a/tools/walletextension/frontend/src/api/gateway.ts +++ b/tools/walletextension/frontend/src/api/gateway.ts @@ -1,53 +1,8 @@ -import { apiRoutes, requestMethods } from "../routes"; +import { apiRoutes } from "../routes"; import { httpRequest } from "."; import { pathToUrl } from "../routes/router"; -import { getNetworkName } from "../lib/utils"; -import { - tenChainIDHex, - tenscanLink, - nativeCurrency, - tenChainIDDecimal, -} from "../lib/constants"; import { AuthenticationResponse } from "@/types/interfaces/GatewayInterfaces"; -const typedData = { - types: { - EIP712Domain: [ - { name: "name", type: "string" }, - { name: "version", type: "string" }, - { name: "chainId", type: "uint256" }, - ], - Authentication: [{ name: "Encryption Token", type: "address" }], - }, - primaryType: "Authentication", - domain: { - name: "Ten", - version: "1.0", - chainId: tenChainIDDecimal, - }, - message: { - "Encryption Token": "0x", - }, -}; - -const { ethereum } = typeof window !== "undefined" ? window : ({} as any); - -export async function switchToTenNetwork() { - if (!ethereum) { - return; - } - try { - await (window as any).ethereum.request({ - method: requestMethods.switchNetwork, - params: [{ chainId: tenChainIDHex }], - }); - - return 0; - } catch (switchError: any) { - return switchError.code; - } -} - export async function fetchVersion(): Promise { return await httpRequest({ method: "get", @@ -69,48 +24,7 @@ export async function accountIsAuthenticated( }); } -const getSignature = async (account: string, data: any) => { - if (!ethereum) { - return; - } - return await ethereum.request({ - method: requestMethods.signTypedData, - params: [account, JSON.stringify(data)], - }); -}; - -export async function authenticateAccountWithTenGatewayEIP712( - userID: string, - account: string -): Promise { - if (!userID) { - return; - } - try { - const isAuthenticated = await accountIsAuthenticated(userID, account); - if (isAuthenticated.status) { - return "Account is already authenticated"; - } - const data = { - ...typedData, - message: { - ...typedData.message, - "Encryption Token": "0x" + userID, - }, - }; - const signature = await getSignature(account, data); - - const auth = await authenticateUser(userID, { - signature, - address: account, - }); - return auth; - } catch (error) { - throw error; - } -} - -const authenticateUser = async ( +export const authenticateUser = async ( userID: string, authenticateFields: { signature: string; @@ -143,27 +57,3 @@ export async function joinTestnet(): Promise { url: pathToUrl(apiRoutes.join), }); } - -export async function addNetworkToMetaMask(rpcUrls: string[]) { - if (!ethereum) { - return; - } - try { - await ethereum.request({ - method: requestMethods.addNetwork, - params: [ - { - chainId: tenChainIDHex, - chainName: getNetworkName(), - nativeCurrency, - rpcUrls, - blockExplorerUrls: [tenscanLink], - }, - ], - }); - return true; - } catch (error) { - console.error(error); - return error; - } -} diff --git a/tools/walletextension/frontend/src/components/modules/home/connected.tsx b/tools/walletextension/frontend/src/components/modules/home/connected.tsx index 08c6636267..39bb850b64 100644 --- a/tools/walletextension/frontend/src/components/modules/home/connected.tsx +++ b/tools/walletextension/frontend/src/components/modules/home/connected.tsx @@ -75,7 +75,7 @@ const Connected = () => { size={"sm"} onClick={() => connectAccount(account.name)} > - {account.connected ? "Disconnect" : "Connect"} + Connect )} diff --git a/tools/walletextension/frontend/src/components/modules/home/disconnected.tsx b/tools/walletextension/frontend/src/components/modules/home/disconnected.tsx index 6cb9758fd4..2ec744042d 100644 --- a/tools/walletextension/frontend/src/components/modules/home/disconnected.tsx +++ b/tools/walletextension/frontend/src/components/modules/home/disconnected.tsx @@ -16,7 +16,6 @@ import { } from "../../ui/dialog"; import Copy from "../common/copy"; import { testnetUrls, tenChainIDDecimal } from "../../../lib/constants"; -import Link from "next/link"; const CONNECTION_STEPS = [ "Hit Connect to Ten and start your journey", diff --git a/tools/walletextension/frontend/src/components/providers/wallet-provider.tsx b/tools/walletextension/frontend/src/components/providers/wallet-provider.tsx index f85edca95b..27110522ac 100644 --- a/tools/walletextension/frontend/src/components/providers/wallet-provider.tsx +++ b/tools/walletextension/frontend/src/components/providers/wallet-provider.tsx @@ -1,25 +1,23 @@ import { createContext, useContext, useEffect, useState } from "react"; -import { ethers } from "ethers"; import { WalletConnectionContextType, WalletConnectionProviderProps, Account, } from "../../types/interfaces/WalletInterfaces"; import { showToast } from "../ui/use-toast"; -import { - getRandomIntAsString, - isTenChain, - isValidUserIDFormat, -} from "../../lib/utils"; +import { isValidUserIDFormat } from "../../lib/utils"; import { accountIsAuthenticated, - authenticateAccountWithTenGatewayEIP712, fetchVersion, revokeAccountsApi, } from "../../api/gateway"; -import { METAMASK_CONNECTION_TIMEOUT } from "../../lib/constants"; -import { requestMethods } from "@/routes"; import { ToastType } from "@/types/interfaces"; +import { + authenticateAccountWithTenGatewayEIP712, + getUserID, +} from "@/api/ethRequests"; +import { ethers } from "ethers"; +import ethService from "@/services/ethService"; const { ethereum } = typeof window !== "undefined" ? window : ({} as any); @@ -28,6 +26,7 @@ const WalletConnectionContext = export const useWalletConnection = (): WalletConnectionContextType => { const context = useContext(WalletConnectionContext); + if (!context) { throw new Error( "useWalletConnection must be used within a WalletConnectionProvider" @@ -52,7 +51,9 @@ export const WalletConnectionProvider = ({ return; } if (userID && isValidUserIDFormat(userID)) { - await displayCorrectScreenBasedOnMetamaskAndUserID(userID, provider); + const status = + await ethService.getCorrectScreenBasedOnMetamaskAndUserID(userID); + setWalletConnected(status); } }; ethereum.on("accountsChanged", handleAccountsChanged); @@ -63,89 +64,27 @@ export const WalletConnectionProvider = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const initialize = async () => { + const providerInstance = new ethers.providers.Web3Provider(ethereum); + setProvider(providerInstance); + await ethService.checkIfMetamaskIsLoaded(providerInstance); + const id = await getUserID(providerInstance); + setUserID(id); + const status = await ethService.getCorrectScreenBasedOnMetamaskAndUserID( + id + ); + setWalletConnected(status); + const accounts = await ethService.getAccounts(providerInstance); + setAccounts(accounts || null); + setVersion(await fetchVersion()); + setLoading(false); + }; + useEffect(() => { - const initialize = async () => { - await checkIfMetamaskIsLoaded(); - }; initialize(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const checkIfMetamaskIsLoaded = async () => { - if (ethereum) { - await handleEthereum(); - } else { - showToast(ToastType.INFO, "Connecting to MetaMask..."); - window.addEventListener("ethereum#initialized", handleEthereum, { - once: true, - }); - setTimeout(handleEthereum, METAMASK_CONNECTION_TIMEOUT); - } - }; - - const handleEthereum = async () => { - if (ethereum && ethereum.isMetaMask) { - const provider = new ethers.providers.Web3Provider(ethereum); - setProvider(provider); - const fetchedUserID = await getUserID(provider); - await displayCorrectScreenBasedOnMetamaskAndUserID( - fetchedUserID, - provider - ); - } else { - showToast( - ToastType.WARNING, - "Please install MetaMask to use Ten Gateway." - ); - } - }; - - const getUserID = async (provider: ethers.providers.Web3Provider) => { - if (!provider) { - return null; - } - - try { - if (await isTenChain()) { - const id = await provider.send(requestMethods.getStorageAt, [ - "getUserID", - getRandomIntAsString(0, 1000), - null, - ]); - setUserID(id); - return id; - } else { - return null; - } - } catch (e: any) { - showToast( - ToastType.DESTRUCTIVE, - `${e.message} ${e.data?.message}` || - "Error: Could not fetch your user ID. Please try again later." - ); - console.error(e); - return null; - } - }; - - const displayCorrectScreenBasedOnMetamaskAndUserID = async ( - userID: string, - provider: ethers.providers.Web3Provider - ) => { - setVersion(await fetchVersion()); - if (await isTenChain()) { - if (userID) { - await getAccounts(provider, userID); - } else { - setWalletConnected(false); - } - } else { - setWalletConnected(false); - } - - setLoading(false); - }; - const connectAccount = async (account: string) => { if (!userID) { return; @@ -182,69 +121,27 @@ export const WalletConnectionProvider = ({ showToast(ToastType.DESTRUCTIVE, "Accounts revoked!"); setAccounts(null); setWalletConnected(false); + setUserID(""); } }; - const getAccounts = async ( - provider: ethers.providers.Web3Provider, - id: string - ) => { - try { - if (!provider) { - showToast( - ToastType.DESTRUCTIVE, - "No provider found. Please try again later." - ); - return; - } - - showToast(ToastType.INFO, "Getting accounts..."); - - if (!(await isTenChain())) { - showToast(ToastType.DESTRUCTIVE, "Please connect to the Ten chain."); - return; - } - - const accounts = await provider.listAccounts(); - - if (accounts.length === 0) { - showToast(ToastType.DESTRUCTIVE, "No MetaMask accounts found."); - return; - } - - let updatedAccounts: Account[] = []; - - for (let i = 0; i < accounts.length; i++) { - const account = accounts[i]; - authenticateAccountWithTenGatewayEIP712(id, account); - const { status } = await accountIsAuthenticated(id, account); - updatedAccounts.push({ - name: account, - connected: status, - }); - } - - setAccounts(updatedAccounts); - setWalletConnected(true); - - showToast(ToastType.SUCCESS, "Accounts authenticated"); - } catch (error) { - console.error(error); - showToast(ToastType.DESTRUCTIVE, "An error occurred. Please try again."); - } + const fetchUserAccounts = async () => { + const accounts = await ethService.getAccounts(provider); + setAccounts(accounts || null); + setWalletConnected(true); }; const walletConnectionContextValue: WalletConnectionContextType = { walletConnected, accounts, userID, - setUserID, connectAccount, - provider, version, revokeAccounts, - getAccounts, loading, + provider, + fetchUserAccounts, + setLoading, }; return ( diff --git a/tools/walletextension/frontend/src/services/ethService.ts b/tools/walletextension/frontend/src/services/ethService.ts new file mode 100644 index 0000000000..a27865e1e2 --- /dev/null +++ b/tools/walletextension/frontend/src/services/ethService.ts @@ -0,0 +1,129 @@ +import { + authenticateAccountWithTenGatewayEIP712, + getUserID, +} from "@/api/ethRequests"; +import { accountIsAuthenticated } from "@/api/gateway"; +import { showToast } from "@/components/ui/use-toast"; +import { METAMASK_CONNECTION_TIMEOUT } from "@/lib/constants"; +import { isTenChain, isValidUserIDFormat } from "@/lib/utils"; +import { ToastType } from "@/types/interfaces"; +import { Account } from "@/types/interfaces/WalletInterfaces"; +import { ethers } from "ethers"; + +const { ethereum } = typeof window !== "undefined" ? window : ({} as any); + +const ethService = { + checkIfMetamaskIsLoaded: async (provider: ethers.providers.Web3Provider) => { + if (ethereum) { + await ethService.handleEthereum(provider); + } else { + showToast(ToastType.INFO, "Connecting to MetaMask..."); + + const handleEthereumOnce = () => { + ethService.handleEthereum(provider); + }; + + window.addEventListener("ethereum#initialized", handleEthereumOnce, { + once: true, + }); + + setTimeout(() => { + handleEthereumOnce(); // Call the handler function after the timeout + }, METAMASK_CONNECTION_TIMEOUT); + } + }, + + handleEthereum: async (provider: ethers.providers.Web3Provider) => { + if (ethereum && ethereum.isMetaMask) { + const fetchedUserID = await getUserID(provider); + if (fetchedUserID && isValidUserIDFormat(fetchedUserID)) { + showToast(ToastType.SUCCESS, "MetaMask connected!"); + } else { + showToast( + ToastType.WARNING, + "Please connect to the Ten chain to use Ten Gateway." + ); + } + } else { + showToast( + ToastType.WARNING, + "Please install MetaMask to use Ten Gateway." + ); + } + }, + + fetchUserID: async (provider: ethers.providers.Web3Provider) => { + try { + return await getUserID(provider); + } catch (e: any) { + showToast( + ToastType.DESTRUCTIVE, + `${e.message} ${e.data?.message}` || + "Error: Could not fetch your user ID. Please try again later." + ); + return null; + } + }, + + getCorrectScreenBasedOnMetamaskAndUserID: async (userID: string) => { + if (await isTenChain()) { + if (userID && isValidUserIDFormat(userID)) { + return true; + } else { + return false; + } + } else { + return false; + } + }, + + getAccounts: async (provider: ethers.providers.Web3Provider) => { + const id = await getUserID(provider); + if (!id || !isValidUserIDFormat(id)) { + return; + } + + try { + if (!provider) { + showToast( + ToastType.DESTRUCTIVE, + "No provider found. Please try again later." + ); + return; + } + + showToast(ToastType.INFO, "Getting accounts..."); + + if (!(await isTenChain())) { + showToast(ToastType.DESTRUCTIVE, "Please connect to the Ten chain."); + return; + } + + const accounts = await provider.listAccounts(); + + if (accounts.length === 0) { + showToast(ToastType.DESTRUCTIVE, "No MetaMask accounts found."); + return []; + } + + let updatedAccounts: Account[] = []; + + for (let i = 0; i < accounts.length; i++) { + const account = accounts[i]; + await authenticateAccountWithTenGatewayEIP712(id, account); + const { status } = await accountIsAuthenticated(id, account); + updatedAccounts.push({ + name: account, + connected: status, + }); + } + showToast(ToastType.SUCCESS, "Accounts fetched successfully."); + return updatedAccounts; + } catch (error) { + console.error(error); + showToast(ToastType.DESTRUCTIVE, "An error occurred. Please try again."); + } + }, +}; + +export default ethService; diff --git a/tools/walletextension/frontend/src/services/useGatewayService.ts b/tools/walletextension/frontend/src/services/useGatewayService.ts index a28428d4e4..388ba1c7b4 100644 --- a/tools/walletextension/frontend/src/services/useGatewayService.ts +++ b/tools/walletextension/frontend/src/services/useGatewayService.ts @@ -1,35 +1,18 @@ import { ToastType } from "@/types/interfaces"; -import { - addNetworkToMetaMask, - joinTestnet, - switchToTenNetwork, -} from "../api/gateway"; +import { joinTestnet } from "../api/gateway"; import { useWalletConnection } from "../components/providers/wallet-provider"; import { showToast } from "../components/ui/use-toast"; import { SWITCHED_CODE, tenGatewayVersion } from "../lib/constants"; import { getRPCFromUrl, isTenChain, isValidUserIDFormat } from "../lib/utils"; -import { requestMethods } from "../routes"; - -const { ethereum } = typeof window !== "undefined" ? window : ({} as any); +import { + addNetworkToMetaMask, + connectAccounts, + switchToTenNetwork, +} from "@/api/ethRequests"; const useGatewayService = () => { - const { provider } = useWalletConnection(); - const { userID, setUserID, getAccounts } = useWalletConnection(); - - const connectAccounts = async () => { - if (!ethereum) { - return null; - } - try { - await ethereum.request({ - method: requestMethods.connectAccounts, - }); - showToast(ToastType.SUCCESS, "Connected to Ten Network"); - } catch (error) { - showToast(ToastType.DESTRUCTIVE, "Unable to connect to Ten Network"); - return null; - } - }; + const { userID, provider, fetchUserAccounts, setLoading } = + useWalletConnection(); const isMetamaskConnected = async () => { if (!provider) { @@ -45,12 +28,15 @@ const useGatewayService = () => { }; const connectToTenTestnet = async () => { + setLoading(true); try { if (await isTenChain()) { if (!userID || !isValidUserIDFormat(userID)) { - throw new Error( + showToast( + ToastType.WARNING, "Existing Ten network detected in MetaMask. Please remove before hitting begin" ); + return; } } @@ -61,7 +47,6 @@ const useGatewayService = () => { (userID && !isValidUserIDFormat(userID)) ) { const user = await joinTestnet(); - setUserID(user); const rpcUrls = [ `${getRPCFromUrl()}/${tenGatewayVersion}/?token=${user}`, ]; @@ -71,14 +56,15 @@ const useGatewayService = () => { if (!(await isMetamaskConnected())) { showToast(ToastType.INFO, "No accounts found, connecting..."); await connectAccounts(); + showToast(ToastType.SUCCESS, "Connected to Ten Network"); } - if (!provider || !userID) { - return; - } - await getAccounts(provider, userID); + await fetchUserAccounts(); } catch (error: any) { showToast(ToastType.DESTRUCTIVE, `${error.message}`); + throw error; + } finally { + setLoading(false); } }; diff --git a/tools/walletextension/frontend/src/types/interfaces/WalletInterfaces.ts b/tools/walletextension/frontend/src/types/interfaces/WalletInterfaces.ts index b01bc25f08..3ca123984a 100644 --- a/tools/walletextension/frontend/src/types/interfaces/WalletInterfaces.ts +++ b/tools/walletextension/frontend/src/types/interfaces/WalletInterfaces.ts @@ -5,15 +5,12 @@ export interface WalletConnectionContextType { walletConnected: boolean; connectAccount: (account: string) => Promise; userID: string | null; - setUserID: (userID: string) => void; - provider: ethers.providers.Web3Provider | null; version: string | null; revokeAccounts: () => void; - getAccounts: ( - provider: ethers.providers.Web3Provider, - userID: string - ) => Promise; loading: boolean; + provider: ethers.providers.Web3Provider; + fetchUserAccounts: () => Promise; + setLoading: (loading: boolean) => void; } export interface Props {