From 0736a7610d3fbe2a7e3268d65bedec661f90d3df Mon Sep 17 00:00:00 2001 From: Pavan Soratur Date: Tue, 22 Oct 2024 04:29:07 -0700 Subject: [PATCH 1/3] chore: add selector modals to tangle dapp components --- .../app/bridge/AmountAndTokenInput.tsx | 148 +++++++++++------ .../app/bridge/BridgeContainer.tsx | 3 +- .../tangle-dapp/app/bridge/ChainSelectors.tsx | 150 ++++++++++-------- apps/tangle-dapp/app/bridge/FeeDetails.tsx | 2 +- .../app/bridge/hooks/useActionButton.ts | 2 + .../app/bridge/hooks/useBalance.ts | 44 ++++- .../components/AmountInput/BaseInput.tsx | 6 +- .../components/Lists/AssetList.tsx | 121 ++++++++++++++ .../components/Lists/ChainList.tsx | 98 ++++++++++++ .../components/Lists/ListCardWrapper.tsx | 56 +++++++ .../src/components/FeeDetails/FeeDetails.tsx | 2 +- 11 files changed, 510 insertions(+), 122 deletions(-) create mode 100644 apps/tangle-dapp/components/Lists/AssetList.tsx create mode 100644 apps/tangle-dapp/components/Lists/ChainList.tsx create mode 100644 apps/tangle-dapp/components/Lists/ListCardWrapper.tsx diff --git a/apps/tangle-dapp/app/bridge/AmountAndTokenInput.tsx b/apps/tangle-dapp/app/bridge/AmountAndTokenInput.tsx index 7f3bb7510..a17c32043 100644 --- a/apps/tangle-dapp/app/bridge/AmountAndTokenInput.tsx +++ b/apps/tangle-dapp/app/bridge/AmountAndTokenInput.tsx @@ -1,25 +1,22 @@ 'use client'; -import { DropdownMenuTrigger as DropdownTrigger } from '@radix-ui/react-dropdown-menu'; -import { TokenIcon } from '@webb-tools/icons/TokenIcon'; +import { Modal, ModalContent, useModal } from '@webb-tools/webb-ui-components'; import ChainOrTokenButton from '@webb-tools/webb-ui-components/components/buttons/ChainOrTokenButton'; -import { - Dropdown, - DropdownBody, - DropdownMenuItem, -} from '@webb-tools/webb-ui-components/components/Dropdown'; -import { ScrollArea } from '@webb-tools/webb-ui-components/components/ScrollArea'; import SkeletonLoader from '@webb-tools/webb-ui-components/components/SkeletonLoader'; import { Typography } from '@webb-tools/webb-ui-components/typography/Typography'; import Decimal from 'decimal.js'; -import { FC, useMemo } from 'react'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { twMerge } from 'tailwind-merge'; import AmountInput from '../../components/AmountInput/AmountInput'; +import { AssetConfig, AssetList } from '../../components/Lists/AssetList'; import { BRIDGE_SUPPORTED_TOKENS } from '../../constants/bridge'; import { useBridge } from '../../context/BridgeContext'; +import useExplorerUrl from '../../hooks/useExplorerUrl'; +import { BridgeTokenId } from '../../types/bridge'; import convertDecimalToBn from '../../utils/convertDecimalToBn'; import useBalance from './hooks/useBalance'; +import { useTokenBalances } from './hooks/useBalance'; import useDecimals from './hooks/useDecimals'; import useSelectedToken from './hooks/useSelectedToken'; import useTypedChainId from './hooks/useTypedChainId'; @@ -33,12 +30,15 @@ const AmountAndTokenInput: FC = () => { setIsAmountInputError, isAmountInputError, feeItems, + selectedSourceChain, } = useBridge(); const selectedToken = useSelectedToken(); const { balance, isLoading } = useBalance(); const decimals = useDecimals(); const { sourceTypedChainId } = useTypedChainId(); + const getExplorerUrl = useExplorerUrl(); + const minAmount = useMemo(() => { const existentialDeposit = selectedToken.existentialDeposit[sourceTypedChainId]; @@ -55,11 +55,77 @@ const AmountAndTokenInput: FC = () => { feeItems.sygmaBridge?.amount, ]); + const { + status: isTokenModalOpen, + open: openTokenModal, + close: closeTokenModal, + } = useModal(false); + + const { getTokenBalance } = useTokenBalances(); + const [tokenBalances, setTokenBalances] = useState< + Record + >({}); + + const fetchBalances = useCallback(async () => { + const balances: Record = {}; + for (const tokenId of tokenIdOptions) { + const token = BRIDGE_SUPPORTED_TOKENS[tokenId]; + const erc20TokenContractAddress = + token.erc20TokenContractAddress?.[sourceTypedChainId]; + balances[tokenId] = await getTokenBalance( + erc20TokenContractAddress ?? '0x0', + token.decimals[sourceTypedChainId] ?? 18, + ); + } + setTokenBalances(balances); + }, [tokenIdOptions, getTokenBalance, sourceTypedChainId]); + + useEffect(() => { + fetchBalances(); + }, [fetchBalances]); + + const assets: AssetConfig[] = useMemo(() => { + return tokenIdOptions.map((tokenId) => { + const token = BRIDGE_SUPPORTED_TOKENS[tokenId]; + const erc20TokenContractAddress = + token.erc20TokenContractAddress?.[sourceTypedChainId]; + const selectedChainExplorerUrl = + selectedSourceChain.blockExplorers?.default; + const explorerUrl = getExplorerUrl( + erc20TokenContractAddress ?? '0x0', + 'address', + 'web3', + selectedChainExplorerUrl?.url, + false, + ); + return { + symbol: token.symbol, + balance: tokenBalances[tokenId] ?? new Decimal(0), + explorerUrl: explorerUrl?.toString(), + }; + }); + }, [ + tokenIdOptions, + sourceTypedChainId, + selectedSourceChain.blockExplorers?.default, + getExplorerUrl, + tokenBalances, + ]); + + const onSelectAsset = (asset: AssetConfig) => { + setSelectedTokenId(asset.symbol as BridgeTokenId); + closeTokenModal(); + }; + + const selectedAssetBalance = useMemo(() => { + return tokenBalances[selectedToken.id] ?? new Decimal(0); + }, [tokenBalances, selectedToken.id]); + return (
@@ -83,39 +149,15 @@ const AmountAndTokenInput: FC = () => { } errorMessageClassName="absolute left-0 bottom-[-24px] !text-[14px] !leading-[21px]" /> - - - - - - -
    - {tokenIdOptions.map((tokenId) => { - const token = BRIDGE_SUPPORTED_TOKENS[tokenId]; - return ( -
  • - } - onSelect={() => setSelectedTokenId(tokenId)} - className="px-3 normal-case" - > - {token.symbol} - -
  • - ); - })} -
-
-
-
+ + {/* Token Selector */} +
{isLoading ? ( @@ -129,11 +171,27 @@ const AmountAndTokenInput: FC = () => { className="absolute right-0 bottom-[-24px] text-mono-120 dark:text-mono-100" > Balance:{' '} - {balance !== null - ? `${balance.toString()} ${selectedToken.symbol}` + {selectedAssetBalance !== null + ? `${selectedAssetBalance.toString()} ${selectedToken.symbol}` : 'N/A'} )} + + + {/* Token Selector Modal */} + + + +
); }; diff --git a/apps/tangle-dapp/app/bridge/BridgeContainer.tsx b/apps/tangle-dapp/app/bridge/BridgeContainer.tsx index 4c5845f4e..7b3791b4e 100644 --- a/apps/tangle-dapp/app/bridge/BridgeContainer.tsx +++ b/apps/tangle-dapp/app/bridge/BridgeContainer.tsx @@ -63,8 +63,7 @@ const BridgeContainer: FC = ({ className }) => { <>
void; - className?: string; -} - const ChainSelectors: FC = () => { const { selectedSourceChain, @@ -36,6 +27,18 @@ const ChainSelectors: FC = () => { setAmount, } = useBridge(); + const { + status: isSourceChainModalOpen, + open: openSourceChainModal, + close: closeSourceChainModal, + } = useModal(false); + + const { + status: isDestinationChainModalOpen, + open: openDestinationChainModal, + close: closeDestinationChainModal, + } = useModal(false); + const onSwitchChains = useCallback(() => { const newSelectedDestinationChain = selectedSourceChain; const newSelectedSourceChain = selectedDestinationChain; @@ -81,66 +84,75 @@ const ChainSelectors: FC = () => { ]); return ( -
- +
+ {/* Source Chain Selector */} +
+ + +
-
+
- -
- ); -}; - -const ChainSelector: FC = ({ - selectedChain, - chainOptions, - onSelectChain, - className, -}) => { - return ( - - + {/* Destination Chain Selector */} +
+ - - - -
    - {chainOptions.map((chain) => { - return ( -
  • - } - onSelect={() => onSelectChain(chain)} - className="py-2.5" - > - {chain.name} - -
  • - ); - })} -
-
-
- +
+ + + {/* Source Chain Modal */} + + + + + {/* Destination Chain Modal */} + + + + +
); }; diff --git a/apps/tangle-dapp/app/bridge/FeeDetails.tsx b/apps/tangle-dapp/app/bridge/FeeDetails.tsx index 51901a923..8e866514e 100644 --- a/apps/tangle-dapp/app/bridge/FeeDetails.tsx +++ b/apps/tangle-dapp/app/bridge/FeeDetails.tsx @@ -87,7 +87,7 @@ const FeeDetails = () => { }, ].filter((item) => Boolean(item)) as Array } - className="!bg-mono-20 dark:!bg-mono-160" + className="!bg-mono-20 dark:!bg-mono-170" titleClassName="!text-mono-100 dark:!text-mono-80" itemTitleClassName="!text-mono-100 dark:!text-mono-80" /> diff --git a/apps/tangle-dapp/app/bridge/hooks/useActionButton.ts b/apps/tangle-dapp/app/bridge/hooks/useActionButton.ts index 2cb94078b..66e66c488 100644 --- a/apps/tangle-dapp/app/bridge/hooks/useActionButton.ts +++ b/apps/tangle-dapp/app/bridge/hooks/useActionButton.ts @@ -48,6 +48,8 @@ export default function useActionButton({ setDestinationAddress, } = useBridge(); + console.debug('feeItems', feeItems); + const isNoActiveAccountOrWallet = useMemo(() => { return !activeAccount || !activeWallet; }, [activeAccount, activeWallet]); diff --git a/apps/tangle-dapp/app/bridge/hooks/useBalance.ts b/apps/tangle-dapp/app/bridge/hooks/useBalance.ts index 6e62aa8a3..94bd13449 100644 --- a/apps/tangle-dapp/app/bridge/hooks/useBalance.ts +++ b/apps/tangle-dapp/app/bridge/hooks/useBalance.ts @@ -2,9 +2,10 @@ import { isAddress } from '@polkadot/util-crypto'; import { ChainConfig } from '@webb-tools/dapp-config/chains/chain-config.interface'; +import { AddressType } from '@webb-tools/dapp-config/types'; import { useWebbUI } from '@webb-tools/webb-ui-components/hooks/useWebbUI'; import Decimal from 'decimal.js'; -import { useEffect, useMemo } from 'react'; +import { useCallback,useEffect, useMemo } from 'react'; import useSWR from 'swr'; import { useBridge } from '../../../context/BridgeContext'; @@ -192,3 +193,44 @@ function checkNativeToken(tokenSymbol: string, chainConfig: ChainConfig) { chainConfig.nativeCurrency.symbol.toLowerCase() ); } + +export function useTokenBalances() { + const ethersProvider = useEthersProvider(); + const activeAccountAddress = useActiveAccountAddress(); + const { sourceTypedChainId } = useTypedChainId(); + + const getTokenBalance = useCallback( + async (erc20TokenContractAddress: AddressType, tokenDecimals: number) => { + if ( + !ethersProvider || + !activeAccountAddress || + !isEvmAddress(activeAccountAddress) + ) { + return null; + } + + if (!erc20TokenContractAddress) { + return null; + } + + try { + const balance = await getEvmContractBalance({ + provider: ethersProvider, + contractAddress: erc20TokenContractAddress, + accAddress: activeAccountAddress, + decimals: tokenDecimals, + }); + return balance; + } catch (error) { + console.error( + `Error fetching balance for token ${erc20TokenContractAddress}:`, + error, + ); + return null; + } + }, + [ethersProvider, activeAccountAddress, sourceTypedChainId], + ); + + return { getTokenBalance }; +} diff --git a/apps/tangle-dapp/components/AmountInput/BaseInput.tsx b/apps/tangle-dapp/components/AmountInput/BaseInput.tsx index 08c7a3665..5c4adc069 100644 --- a/apps/tangle-dapp/components/AmountInput/BaseInput.tsx +++ b/apps/tangle-dapp/components/AmountInput/BaseInput.tsx @@ -93,8 +93,8 @@ const BaseInput: FC = ({ 'px-2.5 lg:px-4 py-2', 'flex items-center justify-between gap-2', 'w-[356px] max-w-[356px]', - 'bg-mono-20 dark:bg-mono-160', - 'border border-mono-20 dark:border-mono-160', + 'bg-mono-20 dark:bg-mono-170', + 'border border-mono-40 dark:border-mono-170', hasError && 'border-red-70 dark:border-red-50', isFullWidth && 'w-full max-w-full', wrapperClassName, @@ -103,7 +103,7 @@ const BaseInput: FC = ({