From 66b045c3d53ad8973a8a34c4e8f86f22cf3148dd Mon Sep 17 00:00:00 2001 From: Jennifer Echenim Date: Tue, 28 Nov 2023 14:27:24 +0400 Subject: [PATCH] feat: authenticate account with EIP712 - refactor error handling - add loading state --- .../ten-gateway/frontend/src/api/gateway.ts | 65 +++++--- .../src/components/modules/home/index.tsx | 14 +- .../components/providers/wallet-provider.tsx | 145 ++++++++++-------- .../frontend/src/components/ui/use-toast.ts | 9 +- .../ten-gateway/frontend/src/lib/constants.ts | 21 +++ .../api/ten-gateway/frontend/src/lib/utils.ts | 9 +- .../ten-gateway/frontend/src/routes/index.ts | 1 + .../src/services/useGatewayService.ts | 26 +--- .../src/types/interfaces/WalletInterfaces.ts | 3 +- .../frontend/src/types/interfaces/index.ts | 10 +- 10 files changed, 191 insertions(+), 112 deletions(-) diff --git a/tools/walletextension/api/ten-gateway/frontend/src/api/gateway.ts b/tools/walletextension/api/ten-gateway/frontend/src/api/gateway.ts index 638ed8e8b8..47b087d214 100644 --- a/tools/walletextension/api/ten-gateway/frontend/src/api/gateway.ts +++ b/tools/walletextension/api/ten-gateway/frontend/src/api/gateway.ts @@ -7,6 +7,7 @@ import { tenChainIDHex, tenscanLink, nativeCurrency, + typedData, } from "../lib/constants"; export async function switchToTenNetwork() { @@ -43,34 +44,61 @@ export async function accountIsAuthenticated( }); } -export async function authenticateAccountWithTenGateway( +const getSignature = async (account: string, data: any) => { + const { ethereum } = window as any; + const signature = await ethereum.request({ + method: metamaskPersonalSign, + params: [account, JSON.stringify(data)], + }); + + return signature; +}; + +export async function authenticateAccountWithTenGatewayEIP712( userID: string, account: string -): Promise { - const textToSign = `Register ${userID} for ${account.toLowerCase()}`; - const signature = await (window as any).ethereum - .request({ - method: metamaskPersonalSign, - params: [textToSign, account], - }) - .catch((error: any) => -1); +): Promise { + try { + const isAuthenticated = await accountIsAuthenticated(userID, account); + if (isAuthenticated) { + return "Account is already authenticated"; + } + const data = { + ...typedData, + message: { + ...typedData.message, + "Encryption Token": "0x" + userID, + }, + }; + const signature = await getSignature(account, data); - if (signature === -1) { - return "Signing failed"; + const auth = await authenticateUser(userID, { + signature, + address: account, + }); + return auth; + } catch (error) { + throw error; } +} - return await httpRequest({ +const authenticateUser = async ( + userID: string, + authenticateFields: { + signature: string; + address: string; + } +) => { + const authenticateResp = await httpRequest({ method: "post", url: pathToUrl(apiRoutes.authenticate), - data: { - signature, - message: textToSign, - }, + data: authenticateFields, searchParams: { u: userID, }, }); -} + return authenticateResp; +}; export async function revokeAccountsApi(userID: string): Promise { return await httpRequest({ @@ -103,10 +131,9 @@ export async function addNetworkToMetaMask(rpcUrls: string[]) { }, ], }); + return true; } catch (error) { console.error(error); return error; } - - return true; } diff --git a/tools/walletextension/api/ten-gateway/frontend/src/components/modules/home/index.tsx b/tools/walletextension/api/ten-gateway/frontend/src/components/modules/home/index.tsx index 511f81d2c1..e2022eb0bc 100644 --- a/tools/walletextension/api/ten-gateway/frontend/src/components/modules/home/index.tsx +++ b/tools/walletextension/api/ten-gateway/frontend/src/components/modules/home/index.tsx @@ -1,17 +1,21 @@ import React from "react"; -import { Button } from "../../ui/button"; -import { Card, CardHeader, CardTitle, CardContent } from "../../ui/card"; -import { Terminal } from "lucide-react"; import { useWalletConnection } from "../../providers/wallet-provider"; import Connected from "./connected"; import Disconnected from "./disconnected"; +import { Skeleton } from "@/components/ui/skeleton"; const Home = () => { - const { walletConnected } = useWalletConnection(); + const { walletConnected, loading } = useWalletConnection(); return (
- {walletConnected ? : } + {loading ? ( + + ) : walletConnected ? ( + + ) : ( + + )}
); }; diff --git a/tools/walletextension/api/ten-gateway/frontend/src/components/providers/wallet-provider.tsx b/tools/walletextension/api/ten-gateway/frontend/src/components/providers/wallet-provider.tsx index 12d1a11d65..ceac9fbaf6 100644 --- a/tools/walletextension/api/ten-gateway/frontend/src/components/providers/wallet-provider.tsx +++ b/tools/walletextension/api/ten-gateway/frontend/src/components/providers/wallet-provider.tsx @@ -5,7 +5,7 @@ import { WalletConnectionProviderProps, Account, } from "../../types/interfaces/WalletInterfaces"; -import { useToast } from "../ui/use-toast"; +import { showToast } from "../ui/use-toast"; import { getRandomIntAsString, isTenChain, @@ -13,11 +13,13 @@ import { } from "../../lib/utils"; import { accountIsAuthenticated, - authenticateAccountWithTenGateway, + authenticateAccountWithTenGatewayEIP712, fetchVersion, revokeAccountsApi, } from "../../api/gateway"; import { METAMASK_CONNECTION_TIMEOUT } from "../../lib/constants"; +import { requestMethods } from "@/routes"; +import { ToastType } from "@/types/interfaces"; const WalletConnectionContext = createContext(null); @@ -35,11 +37,10 @@ export const useWalletConnection = (): WalletConnectionContextType => { export const WalletConnectionProvider = ({ children, }: WalletConnectionProviderProps) => { - const { toast } = useToast(); - const [walletConnected, setWalletConnected] = useState(false); - const [userID, setUserID] = useState(null); + const [userID, setUserID] = useState(""); const [version, setVersion] = useState(null); + const [loading, setLoading] = useState(true); const [accounts, setAccounts] = useState(null); const [provider, setProvider] = useState(null); @@ -68,10 +69,11 @@ export const WalletConnectionProvider = ({ }, []); const checkIfMetamaskIsLoaded = async () => { - if (window && (window as any).ethereum) { + const { ethereum } = window as any; + if (ethereum) { await handleEthereum(); } else { - toast({ description: "Connecting to MetaMask..." }); + showToast(ToastType.INFO, "Connecting to MetaMask..."); window.addEventListener("ethereum#initialized", handleEthereum, { once: true, }); @@ -82,121 +84,133 @@ export const WalletConnectionProvider = ({ const handleEthereum = async () => { const { ethereum } = window as any; if (ethereum && ethereum.isMetaMask) { - const provider = new ethers.providers.Web3Provider( - (window as any).ethereum - ); + const provider = new ethers.providers.Web3Provider(ethereum); setProvider(provider); - await displayCorrectScreenBasedOnMetamaskAndUserID(); + const fetchedUserID = await getUserID(provider); + await displayCorrectScreenBasedOnMetamaskAndUserID( + fetchedUserID, + provider + ); } else { - toast({ description: "Please install MetaMask to use Ten Gateway." }); + showToast( + ToastType.WARNING, + "Please install MetaMask to use Ten Gateway." + ); } }; - const getUserID = async () => { + const getUserID = async (provider: ethers.providers.Web3Provider) => { if (!provider) { return null; } try { if (await isTenChain()) { - const id = await provider.send("eth_getStorageAt", [ + const id = await provider.send(requestMethods.getStorageAt, [ "getUserID", getRandomIntAsString(0, 1000), null, ]); + setUserID(id); return id; } else { return null; } } catch (e: any) { - toast({ - description: - `${e.message} ${e.data?.message}` || - "Error: Could not fetch your userID. Please try again later.", - }); + 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 () => { + const displayCorrectScreenBasedOnMetamaskAndUserID = async ( + userID?: any, + provider?: any + ) => { setVersion(await fetchVersion()); - if (await isTenChain()) { - const userID = await getUserID(); - setUserID(userID); - if (provider && userID && isValidUserIDFormat(userID)) { - await getAccounts(); + if (userID) { + await getAccounts(provider); } else { setWalletConnected(false); } } else { setWalletConnected(false); } + + setLoading(false); }; const connectAccount = async (account: string) => { + if (loading) { + return; + } + if (!userID) { return; } - await authenticateAccountWithTenGateway(userID, account); + await authenticateAccountWithTenGatewayEIP712(userID, account); }; const revokeAccounts = async () => { if (!userID) { return; } - const revokeResponse = await revokeAccountsApi(userID); - if (revokeResponse === "success") { - toast({ - variant: "success", - description: "Successfully revoked all accounts.", - }); + if (revokeResponse === ToastType.SUCCESS) { + showToast(ToastType.DESTRUCTIVE, "Accounts revoked!"); setAccounts(null); setWalletConnected(false); } }; - const getAccounts = async () => { - if (!provider) { - toast({ - variant: "destructive", - description: "No provider found. Please try again later.", - }); - return; - } + const getAccounts = async (provider: ethers.providers.Web3Provider) => { + try { + if (!provider) { + showToast( + ToastType.DESTRUCTIVE, + "No provider found. Please try again later." + ); + return; + } - toast({ variant: "info", description: "Getting accounts..." }); - if (!(await isTenChain())) { - toast({ - variant: "warning", - description: "Please connect to the Ten chain.", - }); - return; - } - const accounts = await provider.listAccounts(); + showToast(ToastType.INFO, "Getting accounts..."); - if (accounts.length === 0) { - toast({ - variant: "destructive", - description: "No MetaMask accounts found.", - }); - return; - } + if (!(await isTenChain())) { + showToast(ToastType.DESTRUCTIVE, "Please connect to the Ten chain."); + return; + } - const user = await getUserID(); - setUserID(user); + const accounts = await provider.listAccounts(); - setAccounts( - await Promise.all( - accounts.map(async (account) => ({ + if (accounts.length === 0) { + showToast(ToastType.DESTRUCTIVE, "No MetaMask accounts found."); + return; + } + + for (const account of accounts) { + await authenticateAccountWithTenGatewayEIP712(userID, account); + } + + const updatedAccounts = await Promise.all( + accounts.map(async (account: string) => ({ name: account, - connected: await accountIsAuthenticated(user, account), + connected: await accountIsAuthenticated(userID, account), })) - ) - ); - setWalletConnected(true); + ); + + setAccounts(updatedAccounts); + setWalletConnected(true); + + showToast(ToastType.SUCCESS, "Accounts authenticated successfully!"); + } catch (error) { + console.error(error); + showToast(ToastType.DESTRUCTIVE, "An error occurred. Please try again."); + } }; const walletConnectionContextValue: WalletConnectionContextType = { @@ -209,6 +223,7 @@ export const WalletConnectionProvider = ({ version, revokeAccounts, getAccounts, + loading, }; return ( diff --git a/tools/walletextension/api/ten-gateway/frontend/src/components/ui/use-toast.ts b/tools/walletextension/api/ten-gateway/frontend/src/components/ui/use-toast.ts index e380dbbf1c..a693743579 100644 --- a/tools/walletextension/api/ten-gateway/frontend/src/components/ui/use-toast.ts +++ b/tools/walletextension/api/ten-gateway/frontend/src/components/ui/use-toast.ts @@ -186,4 +186,11 @@ function useToast() { }; } -export { useToast, toast }; +const showToast = (variant: ToastProps["variant"], description: string) => { + toast({ + variant, + description, + }); +}; + +export { useToast, showToast, toast }; diff --git a/tools/walletextension/api/ten-gateway/frontend/src/lib/constants.ts b/tools/walletextension/api/ten-gateway/frontend/src/lib/constants.ts index 07a64205f8..8da38091e6 100644 --- a/tools/walletextension/api/ten-gateway/frontend/src/lib/constants.ts +++ b/tools/walletextension/api/ten-gateway/frontend/src/lib/constants.ts @@ -30,6 +30,7 @@ export const testnetUrls = { }; export const SWITCHED_CODE = 4902; +export const userIDHexLength = 40; export const tenGatewayVersion = "v1"; export const tenChainIDDecimal = 443; @@ -44,3 +45,23 @@ export const nativeCurrency = { symbol: "ETH", decimals: 18, }; + +export 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", + }, +}; diff --git a/tools/walletextension/api/ten-gateway/frontend/src/lib/utils.ts b/tools/walletextension/api/ten-gateway/frontend/src/lib/utils.ts index 07e6981afa..da06818815 100644 --- a/tools/walletextension/api/ten-gateway/frontend/src/lib/utils.ts +++ b/tools/walletextension/api/ten-gateway/frontend/src/lib/utils.ts @@ -1,7 +1,12 @@ import { type ClassValue, clsx } from "clsx"; import { formatDistanceToNow } from "date-fns"; import { twMerge } from "tailwind-merge"; -import { tenChainIDHex, tenGatewayAddress, testnetUrls } from "./constants"; +import { + tenChainIDHex, + tenGatewayAddress, + testnetUrls, + userIDHexLength, +} from "./constants"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -13,7 +18,7 @@ export function formatTimeAgo(unixTimestampSeconds: string) { } export function isValidUserIDFormat(value: string) { - return typeof value === "string" && value.length === 64; + return typeof value === "string" && value.length === userIDHexLength; } export function getRandomIntAsString(min: number, max: number) { diff --git a/tools/walletextension/api/ten-gateway/frontend/src/routes/index.ts b/tools/walletextension/api/ten-gateway/frontend/src/routes/index.ts index 6ca4c1695d..0fa649e4d5 100644 --- a/tools/walletextension/api/ten-gateway/frontend/src/routes/index.ts +++ b/tools/walletextension/api/ten-gateway/frontend/src/routes/index.ts @@ -15,4 +15,5 @@ export const requestMethods = { connectAccounts: "eth_requestAccounts", switchNetwork: "wallet_switchEthereumChain", addNetwork: "wallet_addEthereumChain", + getStorageAt: "eth_getStorageAt", }; diff --git a/tools/walletextension/api/ten-gateway/frontend/src/services/useGatewayService.ts b/tools/walletextension/api/ten-gateway/frontend/src/services/useGatewayService.ts index 60c40b0e07..44d16889e8 100644 --- a/tools/walletextension/api/ten-gateway/frontend/src/services/useGatewayService.ts +++ b/tools/walletextension/api/ten-gateway/frontend/src/services/useGatewayService.ts @@ -1,16 +1,16 @@ +import { ToastType } from "@/types/interfaces"; import { addNetworkToMetaMask, joinTestnet, switchToTenNetwork, } from "../api/gateway"; import { useWalletConnection } from "../components/providers/wallet-provider"; -import { useToast } from "../components/ui/use-toast"; +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 useGatewayService = () => { - const { toast } = useToast(); const { provider } = useWalletConnection(); const { userID, setUserID, getAccounts } = useWalletConnection(); @@ -19,12 +19,9 @@ const useGatewayService = () => { await (window as any).ethereum.request({ method: requestMethods.connectAccounts, }); - toast({ variant: "success", description: "Connected to Ten Network" }); + showToast(ToastType.SUCCESS, "Connected to Ten Network"); } catch (error) { - toast({ - variant: "destructive", - description: "Unable to connect to Ten Network", - }); + showToast(ToastType.DESTRUCTIVE, "Unable to connect to Ten Network"); return null; } }; @@ -37,7 +34,7 @@ const useGatewayService = () => { const accounts = await provider.listAccounts(); return accounts.length > 0; } catch (error) { - toast({ variant: "destructive", description: "Unable to get accounts" }); + showToast(ToastType.DESTRUCTIVE, "Unable to get accounts"); } return false; }; @@ -65,23 +62,16 @@ const useGatewayService = () => { } if (!(await isMetamaskConnected())) { - toast({ - variant: "info", - description: "No accounts found, connecting...", - }); + showToast(ToastType.INFO, "No accounts found, connecting..."); await connectAccounts(); } if (!provider) { return; } - await getAccounts(); + await getAccounts(provider); } catch (error: any) { - console.error("Error:", error.message); - toast({ - variant: "destructive", - description: `${error.message}`, - }); + showToast(ToastType.DESTRUCTIVE, `${error.message}`); } }; diff --git a/tools/walletextension/api/ten-gateway/frontend/src/types/interfaces/WalletInterfaces.ts b/tools/walletextension/api/ten-gateway/frontend/src/types/interfaces/WalletInterfaces.ts index f5c803c250..bcd9450990 100644 --- a/tools/walletextension/api/ten-gateway/frontend/src/types/interfaces/WalletInterfaces.ts +++ b/tools/walletextension/api/ten-gateway/frontend/src/types/interfaces/WalletInterfaces.ts @@ -9,7 +9,8 @@ export interface WalletConnectionContextType { provider: ethers.providers.Web3Provider | null; version: string | null; revokeAccounts: () => void; - getAccounts: () => Promise; + getAccounts: (provider: ethers.providers.Web3Provider) => Promise; + loading: boolean; } export interface Props { diff --git a/tools/walletextension/api/ten-gateway/frontend/src/types/interfaces/index.ts b/tools/walletextension/api/ten-gateway/frontend/src/types/interfaces/index.ts index 7558d165ed..fefe210d9b 100644 --- a/tools/walletextension/api/ten-gateway/frontend/src/types/interfaces/index.ts +++ b/tools/walletextension/api/ten-gateway/frontend/src/types/interfaces/index.ts @@ -50,7 +50,7 @@ export interface ResponseDataInterface { item: T; message: string; pagination?: PaginationInterface; - success: string; + success: boolean; } export type NavLink = { @@ -60,3 +60,11 @@ export type NavLink = { isExternal?: boolean; subNavLinks?: NavLink[]; }; + +export enum ToastType { + INFO = "info", + SUCCESS = "success", + WARNING = "warning", + DESTRUCTIVE = "destructive", + DEFAULT = "default", +}