From 868843642f0ceec9fc21f7594fe2956f35bf9505 Mon Sep 17 00:00:00 2001
From: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com>
Date: Tue, 14 Jan 2025 20:11:01 +0800
Subject: [PATCH] feat: add account UI (#475)

* feat: add account ui

---------

Co-authored-by: khanti42 <florin.dzeladini@consensys.net>
---
 packages/wallet-ui/src/App.tsx                | 12 ++-
 .../TransactionsList.view.tsx                 | 35 ++++----
 .../AddTokenModal/AddTokenModal.view.tsx      |  8 +-
 .../SendSummaryModal.view.tsx                 | 13 +--
 .../ui/organism/SideBar/SideBar.style.ts      |  5 ++
 .../ui/organism/SideBar/SideBar.view.tsx      |  4 +-
 .../wallet-ui/src/services/useStarkNetSnap.ts | 87 +++++++++++++++++++
 packages/wallet-ui/src/slices/walletSlice.ts  | 21 ++++-
 packages/wallet-ui/src/types/index.ts         |  1 +
 9 files changed, 150 insertions(+), 36 deletions(-)

diff --git a/packages/wallet-ui/src/App.tsx b/packages/wallet-ui/src/App.tsx
index d0cee58f..140af5fe 100644
--- a/packages/wallet-ui/src/App.tsx
+++ b/packages/wallet-ui/src/App.tsx
@@ -37,11 +37,10 @@ function App() {
   } = useAppSelector((state) => state.modals);
   const { loader } = useAppSelector((state) => state.UI);
   const networks = useAppSelector((state) => state.networks);
-  const { accounts } = useAppSelector((state) => state.wallet);
+  const { currentAccount } = useAppSelector((state) => state.wallet);
   const { hasMetamask } = useHasMetamask();
-
-  const address =
-    accounts?.length > 0 ? (accounts[0] as unknown as string) : DUMMY_ADDRESS;
+  const chainId = networks.items?.[networks.activeNetwork]?.chainId;
+  const address = currentAccount ?? DUMMY_ADDRESS;
 
   useEffect(() => {
     if (!provider) {
@@ -57,12 +56,11 @@ function App() {
   }, [connected, forceReconnect, hasMetamask, provider]);
 
   useEffect(() => {
-    if (provider && networks.items.length > 0) {
-      const chainId = networks.items[networks.activeNetwork].chainId;
+    if (provider && networks.items.length > 0 && chainId) {
       getWalletData(chainId);
     }
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [networks.activeNetwork, provider]);
+  }, [networks.activeNetwork, provider, chainId]);
 
   const loading = loader.isLoading;
   return (
diff --git a/packages/wallet-ui/src/components/ui/molecule/TransactionsList/TransactionsList.view.tsx b/packages/wallet-ui/src/components/ui/molecule/TransactionsList/TransactionsList.view.tsx
index efa01a26..70482521 100644
--- a/packages/wallet-ui/src/components/ui/molecule/TransactionsList/TransactionsList.view.tsx
+++ b/packages/wallet-ui/src/components/ui/molecule/TransactionsList/TransactionsList.view.tsx
@@ -16,19 +16,23 @@ export const TransactionsListView = ({ transactions }: Props) => {
   const networks = useAppSelector((state) => state.networks);
   const wallet = useAppSelector((state) => state.wallet);
   const timeoutHandle = useRef(setTimeout(() => {}));
+  const chainId = networks.items[networks.activeNetwork]?.chainId;
+  const {
+    currentAccount,
+    erc20TokenBalanceSelected,
+    transactions: walletTransactions,
+  } = wallet;
 
   useEffect(() => {
-    const chain = networks.items[networks.activeNetwork]?.chainId;
-    const address = wallet.accounts?.[0] as unknown as string;
-    if (chain && address) {
+    if (chainId && currentAccount) {
       clearTimeout(timeoutHandle.current); // cancel the timeout that was in-flight
       timeoutHandle.current = setTimeout(
         () =>
           getTransactions(
-            address,
-            wallet.erc20TokenBalanceSelected.address,
+            currentAccount,
+            erc20TokenBalanceSelected.address,
             10,
-            chain,
+            chainId,
             false,
             true,
           ),
@@ -37,30 +41,29 @@ export const TransactionsListView = ({ transactions }: Props) => {
       return () => clearTimeout(timeoutHandle.current);
     }
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [wallet.transactions]);
+  }, [walletTransactions]);
 
   useEffect(
     () => {
-      const chain = networks.items[networks.activeNetwork]?.chainId;
-      const address = wallet.accounts?.[0] as unknown as string;
-      if (chain && address) {
+      if (chainId && currentAccount) {
         clearTimeout(timeoutHandle.current); // cancel the timeout that was in-flight
         getTransactions(
-          address,
-          wallet.erc20TokenBalanceSelected.address,
+          currentAccount,
+          erc20TokenBalanceSelected.address,
           10,
-          chain,
+          chainId,
         );
       }
     },
     // eslint-disable-next-line react-hooks/exhaustive-deps
     [
       // eslint-disable-next-line react-hooks/exhaustive-deps
-      wallet.erc20TokenBalanceSelected.address,
+      erc20TokenBalanceSelected.address,
       // eslint-disable-next-line react-hooks/exhaustive-deps
-      wallet.erc20TokenBalanceSelected.chainId,
+      erc20TokenBalanceSelected.chainId,
       // eslint-disable-next-line react-hooks/exhaustive-deps
-      wallet.accounts?.[0],
+      currentAccount,
+      chainId,
     ],
   );
 
diff --git a/packages/wallet-ui/src/components/ui/organism/AddTokenModal/AddTokenModal.view.tsx b/packages/wallet-ui/src/components/ui/organism/AddTokenModal/AddTokenModal.view.tsx
index 4db86b2d..76031c8f 100644
--- a/packages/wallet-ui/src/components/ui/organism/AddTokenModal/AddTokenModal.view.tsx
+++ b/packages/wallet-ui/src/components/ui/organism/AddTokenModal/AddTokenModal.view.tsx
@@ -26,8 +26,8 @@ export const AddTokenModalView = ({ closeModal }: Props) => {
   const { setErc20TokenBalance, addErc20Token } = useStarkNetSnap();
   const [enabled, setEnabled] = useState(false);
   const networks = useAppSelector((state) => state.networks);
-  const { accounts } = useAppSelector((state) => state.wallet);
-  const chain = networks && networks.items[networks.activeNetwork].chainId;
+  const { currentAccount } = useAppSelector((state) => state.wallet);
+  const chainId = networks && networks.items[networks.activeNetwork].chainId;
   const [isValidAddress, setIsValidAddress] = useState(false);
   const [fields, setFields] = useState({
     address: '',
@@ -105,8 +105,8 @@ export const AddTokenModalView = ({ closeModal }: Props) => {
                 fields.name,
                 fields.symbol,
                 parseFloat(fields.decimal),
-                chain,
-                accounts[0] as unknown as string,
+                chainId,
+                currentAccount,
               );
               if (newToken) {
                 setErc20TokenBalance(newToken);
diff --git a/packages/wallet-ui/src/components/ui/organism/Header/SendSummaryModal/SendSummaryModal.view.tsx b/packages/wallet-ui/src/components/ui/organism/Header/SendSummaryModal/SendSummaryModal.view.tsx
index 95807fbf..945516fb 100644
--- a/packages/wallet-ui/src/components/ui/organism/Header/SendSummaryModal/SendSummaryModal.view.tsx
+++ b/packages/wallet-ui/src/components/ui/organism/Header/SendSummaryModal/SendSummaryModal.view.tsx
@@ -50,6 +50,7 @@ export const SendSummaryModalView = ({
   selectedFeeToken,
 }: Props) => {
   const wallet = useAppSelector((state) => state.wallet);
+  const currentAccount = wallet.currentAccount;
   const [estimatingGas, setEstimatingGas] = useState(true);
   const [gasFees, setGasFees] = useState({
     suggestedMaxFee: '0',
@@ -80,7 +81,7 @@ export const SendSummaryModalView = ({
 
   useEffect(() => {
     const fetchGasFee = () => {
-      if (wallet.accounts) {
+      if (currentAccount) {
         setGasFeesError(false);
         setEstimatingGas(true);
         const amountBN = ethers.utils.parseUnits(
@@ -92,7 +93,7 @@ export const SendSummaryModalView = ({
           wallet.erc20TokenBalanceSelected.address,
           ContractFuncName.Transfer,
           callData,
-          wallet.accounts[0] as unknown as string,
+          currentAccount,
           chainId,
           selectedFeeToken === FeeToken.STRK
             ? constants.TRANSACTION_VERSION.V3
@@ -113,7 +114,7 @@ export const SendSummaryModalView = ({
       }
     };
     fetchGasFee();
-  }, []);
+  }, [currentAccount]);
 
   useEffect(() => {
     if (gasFees?.suggestedMaxFee) {
@@ -175,7 +176,7 @@ export const SendSummaryModalView = ({
   }, [amount, wallet.erc20TokenBalanceSelected]);
 
   const handleConfirmClick = () => {
-    if (wallet.accounts) {
+    if (currentAccount) {
       const amountBN = ethers.utils.parseUnits(
         amount,
         wallet.erc20TokenBalanceSelected.decimals,
@@ -185,7 +186,7 @@ export const SendSummaryModalView = ({
         wallet.erc20TokenBalanceSelected.address,
         ContractFuncName.Transfer,
         callData,
-        wallet.accounts[0] as unknown as string,
+        currentAccount,
         gasFees.suggestedMaxFee,
         chainId,
         selectedFeeToken,
@@ -194,7 +195,7 @@ export const SendSummaryModalView = ({
           if (result) {
             toastr.success('Transaction sent successfully');
             getTransactions(
-              wallet.accounts[0] as unknown as string,
+              currentAccount,
               wallet.erc20TokenBalanceSelected.address,
               10,
               chainId,
diff --git a/packages/wallet-ui/src/components/ui/organism/SideBar/SideBar.style.ts b/packages/wallet-ui/src/components/ui/organism/SideBar/SideBar.style.ts
index a9f2ef20..fb1d015f 100644
--- a/packages/wallet-ui/src/components/ui/organism/SideBar/SideBar.style.ts
+++ b/packages/wallet-ui/src/components/ui/organism/SideBar/SideBar.style.ts
@@ -39,6 +39,11 @@ export const InfoIcon = styled(RoundedIcon)`
   margin-right: ${(props) => props.theme.spacing.tiny2};
 `;
 
+export const AddIcon = styled(RoundedIcon)`
+  cursor: pointer;
+  margin-left: ${(props) => props.theme.spacing.tiny2};
+`;
+
 export const AddTokenButton = styled(Button).attrs((props) => ({
   textStyle: {
     fontWeight: props.theme.typography.bold.fontWeight,
diff --git a/packages/wallet-ui/src/components/ui/organism/SideBar/SideBar.view.tsx b/packages/wallet-ui/src/components/ui/organism/SideBar/SideBar.view.tsx
index 6221b8aa..b2236945 100644
--- a/packages/wallet-ui/src/components/ui/organism/SideBar/SideBar.view.tsx
+++ b/packages/wallet-ui/src/components/ui/organism/SideBar/SideBar.view.tsx
@@ -12,6 +12,7 @@ import {
   AccountDetailsContent,
   AccountImageStyled,
   AccountLabel,
+  AddIcon,
   AddTokenButton,
   DivList,
   InfoIcon,
@@ -37,7 +38,7 @@ export const SideBarView = ({ address }: Props) => {
   const [accountDetailsOpen, setAccountDetailsOpen] = useState(false);
   const wallet = useAppSelector((state) => state.wallet);
   const [addTokenOpen, setAddTokenOpen] = useState(false);
-  const { getStarkName } = useStarkNetSnap();
+  const { getStarkName, addNewAccount } = useStarkNetSnap();
   const [starkName, setStarkName] = useState<string | undefined>(undefined);
 
   const ref = useRef<HTMLDivElement>();
@@ -114,6 +115,7 @@ export const SideBarView = ({ address }: Props) => {
       <RowDiv>
         <InfoIcon onClick={() => setInfoModalOpen(true)}>i</InfoIcon>
         <AccountAddress address={address} starkName={starkName} />
+        <AddIcon onClick={async () => await addNewAccount(chainId)}>+</AddIcon>
       </RowDiv>
       <DivList ref={ref as any}>
         <AssetsList />
diff --git a/packages/wallet-ui/src/services/useStarkNetSnap.ts b/packages/wallet-ui/src/services/useStarkNetSnap.ts
index 17b95dd7..3d121d2d 100644
--- a/packages/wallet-ui/src/services/useStarkNetSnap.ts
+++ b/packages/wallet-ui/src/services/useStarkNetSnap.ts
@@ -251,6 +251,8 @@ export const useStarkNetSnap = () => {
     }
   };
 
+  // FIXME: this method has to rewrite when switch account enabled
+  // we should get the active account, instead of recovering all accounts
   const getWalletData = async (chainId: string, networks?: Network[]) => {
     if (!loader.isLoading && !networks) {
       dispatch(enableLoadingWithMessage('Getting network data ...'));
@@ -302,10 +304,62 @@ export const useStarkNetSnap = () => {
     dispatch(disableLoading());
   };
 
+  // FIXME: this method has to replace getWalletData()
+  const initWalletData = async ({
+    account,
+  }: {
+    // TODO: we should allow the account to be optional
+    // and get the active account from the snap
+    account: Account;
+  }) => {
+    if (!loader.isLoading) {
+      dispatch(enableLoadingWithMessage('Getting network data ...'));
+    }
+
+    const { address, chainId } = account;
+
+    await setAccount(account);
+
+    await initTokensAndBalances(chainId, address);
+
+    dispatch(disableLoading());
+  };
+
+  const setAccount = async (account: Account) => {
+    const { upgradeRequired, deployRequired } = account;
+    dispatch(setAccounts(account));
+
+    // FIXME: hardcode to set the info modal visible,
+    // but it should only visible when the account is not deployed
+    dispatch(setInfoModalVisible(true));
+
+    dispatch(setUpgradeModalVisible(upgradeRequired));
+    dispatch(setDeployModalVisible(deployRequired));
+  };
+
   const setErc20TokenBalance = (erc20TokenBalance: Erc20TokenBalance) => {
     dispatch(setErc20TokenBalanceSelected(erc20TokenBalance));
   };
 
+  const initTokensAndBalances = async (chainId: string, address: string) => {
+    const tokens = await getTokens(chainId);
+
+    // Get all tokens balance, USD value, and format them into Erc20TokenBalance type
+    const tokensWithBalances: Erc20TokenBalance[] = await Promise.all(
+      tokens.map(async (token) => {
+        const balance = await getTokenBalance(token.address, address, chainId);
+        const usdPrice = await getAssetPriceUSD(token);
+        return await getTokenBalanceWithDetails(balance, token, usdPrice);
+      }),
+    );
+
+    dispatch(setErc20TokenBalances(tokensWithBalances));
+
+    if (tokensWithBalances.length > 0) {
+      setErc20TokenBalance(tokensWithBalances[0]);
+    }
+  };
+
   async function getPrivateKeyFromAddress(address: string, chainId: string) {
     try {
       await provider.request({
@@ -337,6 +391,9 @@ export const useStarkNetSnap = () => {
     chainId: string,
     transactionVersion?: typeof constants.TRANSACTION_VERSION.V3,
   ) {
+    console.log('contractCallData', contractCallData);
+    console.log('contractAddress', contractAddress);
+    console.log('address', address);
     try {
       const invocations: Invocations = [
         {
@@ -974,6 +1031,35 @@ export const useStarkNetSnap = () => {
     }
   };
 
+  const addNewAccount = async (chainId: string) => {
+    dispatch(enableLoadingWithMessage('Adding new account...'));
+    try {
+      const account = (await provider.request({
+        method: 'wallet_invokeSnap',
+        params: {
+          snapId,
+          request: {
+            method: 'starkNet_addAccount',
+            params: {
+              chainId,
+            },
+          },
+        },
+      })) as Account;
+
+      await initWalletData({
+        account,
+      });
+
+      return account;
+    } catch (err: any) {
+      const toastr = new Toastr();
+      toastr.error(err.message as unknown as string);
+    } finally {
+      dispatch(disableLoading());
+    }
+  };
+
   return {
     connectToSnap,
     getNetworks,
@@ -1004,6 +1090,7 @@ export const useStarkNetSnap = () => {
     getCurrentNetwork,
     getStarkName,
     getAddrFromStarkName,
+    addNewAccount,
     satisfiesVersion: oldVersionDetected,
   };
 };
diff --git a/packages/wallet-ui/src/slices/walletSlice.ts b/packages/wallet-ui/src/slices/walletSlice.ts
index 3f61a813..59a1ee9c 100644
--- a/packages/wallet-ui/src/slices/walletSlice.ts
+++ b/packages/wallet-ui/src/slices/walletSlice.ts
@@ -8,7 +8,8 @@ export interface WalletState {
   connected: boolean;
   isLoading: boolean;
   forceReconnect: boolean;
-  accounts: Account[];
+  accounts: string[];
+  currentAccount: string;
   erc20TokenBalances: Erc20TokenBalance[];
   erc20TokenBalanceSelected: Erc20TokenBalance;
   transactions: Transaction[];
@@ -21,6 +22,7 @@ const initialState: WalletState = {
   isLoading: false,
   forceReconnect: false,
   accounts: [],
+  currentAccount: '',
   erc20TokenBalances: [],
   erc20TokenBalanceSelected: {} as Erc20TokenBalance,
   transactions: [],
@@ -41,12 +43,26 @@ export const walletSlice = createSlice({
     setForceReconnect: (state, { payload }) => {
       state.forceReconnect = payload;
     },
-    setAccounts: (state, { payload }) => {
+    setAccounts: (
+      state,
+      {
+        payload,
+      }: {
+        payload: Account | Account[];
+      },
+    ) => {
       if (Array.isArray(payload)) {
+        // When switching networks, we clean up the accounts from the previous network
+        // Hence, we can assume that setAccounts is called with an array of accounts from the same network
         state.accounts = payload.map((account) => account.address);
       } else {
         state.accounts.push(payload.address);
       }
+      // FIXME: this is a hack to set the current account to the last one added
+      // We should have a way to get the active account
+      const currentAccountIdx = state.accounts.length - 1;
+      const currentAccount = state.accounts[currentAccountIdx];
+      state.currentAccount = currentAccount;
     },
     setErc20TokenBalances: (state, { payload }) => {
       state.erc20TokenBalances = payload;
@@ -94,6 +110,7 @@ export const walletSlice = createSlice({
     },
     clearAccounts: (state) => {
       state.accounts = [];
+      state.currentAccount = '';
     },
     resetWallet: (state) => {
       return {
diff --git a/packages/wallet-ui/src/types/index.ts b/packages/wallet-ui/src/types/index.ts
index 657538af..a53da518 100644
--- a/packages/wallet-ui/src/types/index.ts
+++ b/packages/wallet-ui/src/types/index.ts
@@ -32,6 +32,7 @@ export type Account = {
   publicKey: string;
   upgradeRequired: boolean;
   deployRequired: boolean;
+  chainId: string;
 };
 
 export type Network = {