From 3b935bb525a1f17e87de8f74642a11a24117247e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Fri, 29 Nov 2024 23:48:34 -0300 Subject: [PATCH 1/5] feat: default token selection for pw --- docs/high_level_overview.md | 13 +- src/app/(mobile-ui)/wallet/page.tsx | 5 +- .../Cashout/Components/Initial.view.tsx | 54 +++-- src/components/Claim/Link/Initial.view.tsx | 38 ++-- .../Claim/Link/Onchain/Confirm.view.tsx | 6 +- src/components/Create/Create.tsx | 4 - src/components/Create/Link/Confirm.view.tsx | 8 +- src/components/Create/Link/Input.view.tsx | 54 +++-- src/components/Create/useCreateLink.tsx | 35 ++-- src/components/Global/ChainSelector/index.tsx | 13 +- .../Components/AdvancedButton.tsx | 44 +---- .../Global/TokenSelector/TokenSelector.tsx | 14 +- src/components/Home/WalletCard.tsx | 16 +- src/components/Home/WalletToggleButton.tsx | 5 +- .../Request/Create/Views/Initial.view.tsx | 29 ++- .../Request/Pay/Views/Initial.view.tsx | 27 ++- src/constants/general.consts.ts | 2 - src/constants/zerodev.consts.ts | 3 + src/context/walletContext/walletContext.tsx | 94 +++++++-- src/hooks/useBalance.tsx | 187 ------------------ src/interfaces/wallet.interfaces.ts | 1 + src/utils/balance.utils.ts | 101 ++++++++++ src/utils/index.ts | 1 + 23 files changed, 374 insertions(+), 380 deletions(-) delete mode 100644 src/hooks/useBalance.tsx create mode 100644 src/utils/balance.utils.ts diff --git a/docs/high_level_overview.md b/docs/high_level_overview.md index 9eeaa233c..c9268fab7 100644 --- a/docs/high_level_overview.md +++ b/docs/high_level_overview.md @@ -127,18 +127,9 @@ There are three major contexts provided in the `/context` folder: ### Hooks -The `/hooks` folder contains two custom hooks to help manage wallet functionality: +The `/hooks` folder contains a custom hook to help manage wallet functionality: -1. **useBalance**: - - - Fetches and manages user wallet balances across multiple chains. - - Converts API responses to a structured format and calculates total value per chain. - - Usage: - ```js - const { balances, fetchBalances, valuePerChain, refetchBalances, hasFetchedBalances } = useBalance() - ``` - -2. **useWalletType**: +1. **useWalletType**: - Detects and manages the user's wallet type (e.g., Blockscout or Safe App environment). - Fetches environment and wallet info, then updates state based on the wallet address. - Usage: diff --git a/src/app/(mobile-ui)/wallet/page.tsx b/src/app/(mobile-ui)/wallet/page.tsx index bf8b4b678..99286585e 100644 --- a/src/app/(mobile-ui)/wallet/page.tsx +++ b/src/app/(mobile-ui)/wallet/page.tsx @@ -9,6 +9,7 @@ import { motion } from 'framer-motion' import { HomeLink } from '@/components/Home/HomeLink' import { useAuth } from '@/context/authContext' import { WalletProviderType } from '@/interfaces' +import { formatUnits } from 'viem' const WalletDetailsPage = () => { const { selectedWallet } = useWallet() @@ -39,7 +40,9 @@ const WalletDetailsPage = () => { -
{'$ 420.69'}
+
+ $ {Number(formatUnits(selectedWallet?.balance ?? 0n, 6)).toFixed(2)} +
diff --git a/src/components/Cashout/Components/Initial.view.tsx b/src/components/Cashout/Components/Initial.view.tsx index 1e85595cc..e9104c79f 100644 --- a/src/components/Cashout/Components/Initial.view.tsx +++ b/src/components/Cashout/Components/Initial.view.tsx @@ -6,11 +6,10 @@ import ValidatedInput from '@/components/Global/ValidatedInput' import { useState, useContext, useEffect, useMemo } from 'react' import * as _consts from '../Cashout.consts' import * as context from '@/context' -import { useBalance } from '@/hooks/useBalance' import { useAuth } from '@/context/authContext' import { useCreateLink } from '@/components/Create/useCreateLink' import * as assets from '@/assets' -import { formatIban, validateBankAccount, floorFixed } from '@/utils' +import { formatIban, validateBankAccount, floorFixed, balanceByToken } from '@/utils' import { FAQComponent } from './Faq.comp' import { RecipientInfoComponent } from './RecipientInfo.comp' import Icon from '@/components/Global/Icon' @@ -19,6 +18,7 @@ import { MAX_CASHOUT_LIMIT, MIN_CASHOUT_LIMIT } from '@/components/Offramp/Offra import { Button, Card } from '@/components/0_Bruddle' import { useWallet } from '@/context/walletContext' import { sanitizeBankAccount } from '@/utils/format.utils' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' export const InitialCashoutView = ({ onNext, @@ -31,11 +31,15 @@ export const InitialCashoutView = ({ setOfframpForm, crossChainDetails, }: _consts.ICashoutScreenProps) => { - const { selectedTokenPrice, inputDenomination, selectedChainID, selectedTokenAddress } = useContext( - context.tokenSelectorContext - ) + const { + selectedTokenPrice, + inputDenomination, + selectedChainID, + setSelectedChainID, + selectedTokenAddress, + setSelectedTokenAddress, + } = useContext(context.tokenSelectorContext) - const { balances, hasFetchedBalances, balanceByToken } = useBalance() const { user, fetchUser, isFetchingUser } = useAuth() const [, setUserType] = useState<'NEW' | 'EXISTING' | undefined>(undefined) @@ -68,7 +72,7 @@ export const InitialCashoutView = ({ const { prepareCreateLinkWrapper } = useCreateLink() - const { isConnected, signInModal } = useWallet() + const { isConnected, signInModal, selectedWallet, isExternalWallet, isPeanutWallet } = useWallet() const isBelowMinLimit = useMemo(() => { if (!usdValue) return false @@ -190,11 +194,12 @@ export const InitialCashoutView = ({ } const maxValue = useMemo(() => { - const balance = balanceByToken(selectedChainID, selectedTokenAddress) + if (!selectedWallet?.balances) return '' + const balance = balanceByToken(selectedWallet.balances, selectedChainID, selectedTokenAddress) if (!balance) return '' // 6 decimal places, prettier return floorFixed(balance.amount, 6) - }, [selectedChainID, selectedTokenAddress, balanceByToken]) + }, [selectedChainID, selectedTokenAddress, selectedWallet?.balances]) useEffect(() => { if (!_tokenValue) return @@ -211,6 +216,13 @@ export const InitialCashoutView = ({ } }, [_tokenValue, inputDenomination]) + useEffect(() => { + if (isPeanutWallet) { + setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString()) + setSelectedTokenAddress(PEANUT_WALLET_TOKEN) + } + }, [isPeanutWallet]) + // Update the bank account selection handler const handleBankAccountSelect = (accountIdentifier: string) => { if (!xchainAllowed) return @@ -270,16 +282,20 @@ export const InitialCashoutView = ({ Minimum amount is ${MIN_CASHOUT_LIMIT}
)} - - {hasFetchedBalances && balances.length === 0 && ( -
{ - open() - }} - className="cursor-pointer text-h9 underline" - > - ( Buy Tokens ) -
+ {isExternalWallet && ( + <> + + {selectedWallet!.balances!.length === 0 && ( +
{ + open() + }} + className="cursor-pointer text-h9 underline" + > + ( Buy Tokens ) +
+ )} + )}
diff --git a/src/components/Claim/Link/Initial.view.tsx b/src/components/Claim/Link/Initial.view.tsx index 5073ac98a..6591967ff 100644 --- a/src/components/Claim/Link/Initial.view.tsx +++ b/src/components/Claim/Link/Initial.view.tsx @@ -2,7 +2,7 @@ import GeneralRecipientInput, { GeneralRecipientUpdate } from '@/components/Global/GeneralRecipientInput' import * as _consts from '../Claim.consts' -import { useContext, useEffect, useState, useMemo } from 'react' +import { useContext, useEffect, useState, useMemo, useCallback } from 'react' import Icon from '@/components/Global/Icon' import useClaimLink from '../useClaimLink' import * as context from '@/context' @@ -35,8 +35,9 @@ import { useWallet } from '@/context/walletContext' import { TOOLTIPS } from '@/constants/tooltips' import AddressLink from '@/components/Global/AddressLink' import TokenSelector from '@/components/Global/TokenSelector/TokenSelector' -import { checkTokenSupportsXChain, SQUID_ETH_ADDRESS } from '@/utils/token.utils' +import { SQUID_ETH_ADDRESS } from '@/utils/token.utils' import { getSquidTokenAddress } from '@/utils/token.utils' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' export const InitialClaimLinkView = ({ onNext, @@ -84,10 +85,19 @@ export const InitialClaimLinkView = ({ } = useContext(context.tokenSelectorContext) const { claimLink } = useClaimLink() - // TODO: isConnected needs to be moved in useWallet() - const { isConnected, address, signInModal } = useWallet() + const { isConnected, address, signInModal, isExternalWallet, isPeanutWallet } = useWallet() const { user } = useAuth() + const resetSelectedToken = useCallback(() => { + if (isPeanutWallet) { + setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString()) + setSelectedTokenAddress(PEANUT_WALLET_TOKEN) + } else { + setSelectedChainID(claimLinkData.chainId) + setSelectedTokenAddress(claimLinkData.tokenAddress) + } + }, [claimLinkData, isPeanutWallet]) + // TODO: all handleConnectWallet will need to pass through useWallet() const handleConnectWallet = async () => { if (isConnected && address) { @@ -149,13 +159,9 @@ export const InitialClaimLinkView = ({ } } - const tokenSupportsXChain = useMemo(() => { - return checkTokenSupportsXChain( - claimLinkData.tokenAddress, - claimLinkData.chainId, - supportedSquidChainsAndTokens - ) - }, [claimLinkData.tokenAddress, claimLinkData.chainId, supportedSquidChainsAndTokens]) + useEffect(() => { + if (isPeanutWallet) resetSelectedToken() + }, [resetSelectedToken, isPeanutWallet]) const handleIbanRecipient = async () => { try { @@ -441,16 +447,15 @@ export const InitialClaimLinkView = ({ - {recipientType !== 'iban' && recipientType !== 'us' ? ( + {isExternalWallet && recipientType !== 'iban' && recipientType !== 'us' && ( { - setSelectedChainID(claimLinkData.chainId) - setSelectedTokenAddress(claimLinkData.tokenAddress) + resetSelectedToken() }} /> - ) : null} + )} reset diff --git a/src/components/Claim/Link/Onchain/Confirm.view.tsx b/src/components/Claim/Link/Onchain/Confirm.view.tsx index 670d403db..b12498956 100644 --- a/src/components/Claim/Link/Onchain/Confirm.view.tsx +++ b/src/components/Claim/Link/Onchain/Confirm.view.tsx @@ -10,7 +10,6 @@ import MoreInfo from '@/components/Global/MoreInfo' import * as _interfaces from '../../Claim.interfaces' import * as _utils from '../../Claim.utils' import * as consts from '@/constants' -import { useBalance } from '@/hooks/useBalance' import { Button, Card } from '@/components/0_Bruddle' import { useWallet } from '@/context/walletContext' @@ -26,7 +25,7 @@ export const ConfirmClaimLinkView = ({ attachment, selectedRoute, }: _consts.IClaimScreenProps) => { - const { address } = useWallet() + const { address, refetchBalances } = useWallet() const { claimLinkXchain, claimLink } = useClaimLink() const { selectedChainID, selectedTokenAddress, supportedSquidChainsAndTokens } = useContext( context.tokenSelectorContext @@ -37,7 +36,6 @@ export const ConfirmClaimLinkView = ({ errorMessage: string }>({ showError: false, errorMessage: '' }) const [fileType] = useState('') - const { refetchBalances } = useBalance() const handleOnClaim = async () => { if (!recipient) { @@ -82,7 +80,7 @@ export const ConfirmClaimLinkView = ({ }) setTransactionHash(claimTxHash) onNext() - refetchBalances() + refetchBalances(address ?? '') } else { throw new Error('Error claiming link') } diff --git a/src/components/Create/Create.tsx b/src/components/Create/Create.tsx index e4eca9ce0..2b8882e8a 100644 --- a/src/components/Create/Create.tsx +++ b/src/components/Create/Create.tsx @@ -6,8 +6,6 @@ import { interfaces as peanutInterfaces } from '@squirrel-labs/peanut-sdk' import * as _consts from './Create.consts' import * as context from '@/context' import * as utils from '@/utils' -import SafeAppsSDK from '@safe-global/safe-apps-sdk' -import { useBalance } from '@/hooks/useBalance' import PageContainer from '../0_Bruddle/PageContainer' import { useWallet } from '@/context/walletContext' @@ -15,7 +13,6 @@ export const Create = () => { const [step, setStep] = useState<_consts.ICreateScreenState>(_consts.INIT_VIEW_STATE) const [tokenValue, setTokenValue] = useState(undefined) const [usdValue, setUsdValue] = useState(undefined) - const sdk = new SafeAppsSDK() const [linkDetails, setLinkDetails] = useState() const [password, setPassword] = useState('') @@ -59,7 +56,6 @@ export const Create = () => { const { address } = useWallet() const { resetTokenContextProvider } = useContext(context.tokenSelectorContext) - useBalance() // Fetch balances here, decreases load time on input screen for tokenselector const handleOnNext = () => { if (step.idx === _consts.CREATE_SCREEN_FLOW.length - 1) return diff --git a/src/components/Create/Link/Confirm.view.tsx b/src/components/Create/Link/Confirm.view.tsx index 7a203a99e..1247e47da 100644 --- a/src/components/Create/Link/Confirm.view.tsx +++ b/src/components/Create/Link/Confirm.view.tsx @@ -21,9 +21,7 @@ import { useCreateLink } from '../useCreateLink' import { Button, Card } from '@/components/0_Bruddle' import Divider from '@/components/0_Bruddle/Divider' import { useWallet } from '@/context/walletContext' -import Loading from '@/components/Global/Loading' import MoreInfo from '@/components/Global/MoreInfo' -import { useBalance } from '@/hooks/useBalance' import { useWalletType } from '@/hooks/useWalletType' import { supportedPeanutChains, peanutTokenDetails } from '@/constants' @@ -49,8 +47,6 @@ export const CreateLinkConfirmView = ({ usdValue, }: _consts.ICreateScreenProps) => { const [showMessage, setShowMessage] = useState(false) - const { refetchBalances } = useBalance() - const { selectedChainID, selectedTokenAddress, @@ -76,7 +72,7 @@ export const CreateLinkConfirmView = ({ } = useCreateLink() const { setLoadingState, loadingState, isLoading } = useContext(context.loadingStateContext) - const { address } = useWallet() + const { address, refetchBalances } = useWallet() const selectedChain = useMemo(() => { if (supportedSquidChainsAndTokens[selectedChainID]) { @@ -221,7 +217,7 @@ export const CreateLinkConfirmView = ({ }) onNext() - refetchBalances() + refetchBalances(address ?? '') } catch (error) { const errorString = ErrorHandler(error) setErrorState({ diff --git a/src/components/Create/Link/Input.view.tsx b/src/components/Create/Link/Input.view.tsx index bddc1b759..d797ced7e 100644 --- a/src/components/Create/Link/Input.view.tsx +++ b/src/components/Create/Link/Input.view.tsx @@ -8,17 +8,17 @@ import { useCreateLink } from '../useCreateLink' import * as _consts from '../Create.consts' import { isGaslessDepositPossible } from '../Create.utils' import * as context from '@/context' -import { isNativeCurrency, ErrorHandler, printableAddress, floorFixed } from '@/utils' +import { isNativeCurrency, ErrorHandler, printableAddress, floorFixed, balanceByToken } from '@/utils' import FileUploadInput from '@/components/Global/FileUploadInput' import { interfaces } from '@squirrel-labs/peanut-sdk' import Icon from '@/components/Global/Icon' import MoreInfo from '@/components/Global/MoreInfo' import { useWalletType } from '@/hooks/useWalletType' -import { useBalance } from '@/hooks/useBalance' import { Button, Card } from '@/components/0_Bruddle' import { useWallet } from '@/context/walletContext' import { formatEther } from 'viem' import { WalletProviderType } from '@/interfaces' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' export const CreateLinkInputView = ({ onNext, @@ -53,11 +53,15 @@ export const CreateLinkInputView = ({ estimatePoints, prepareDirectSendTx, } = useCreateLink() - const { selectedTokenPrice, inputDenomination, selectedChainID, selectedTokenAddress } = useContext( - context.tokenSelectorContext - ) + const { + selectedTokenPrice, + inputDenomination, + selectedChainID, + setSelectedChainID, + selectedTokenAddress, + setSelectedTokenAddress, + } = useContext(context.tokenSelectorContext) const { walletType, environmentInfo } = useWalletType() - const { balances, hasFetchedBalances, balanceByToken } = useBalance() const { setLoadingState, loadingState, isLoading } = useContext(context.loadingStateContext) const [errorState, setErrorState] = useState<{ @@ -68,7 +72,7 @@ export const CreateLinkInputView = ({ inputDenomination === 'TOKEN' ? tokenValue : usdValue ) - const { selectedWallet, signInModal, isConnected, address } = useWallet() + const { selectedWallet, signInModal, isConnected, address, isExternalWallet, isPeanutWallet } = useWallet() const handleOnNext = async () => { try { @@ -261,11 +265,12 @@ export const CreateLinkInputView = ({ } const maxValue = useMemo(() => { - const balance = balanceByToken(selectedChainID, selectedTokenAddress) + if (!selectedWallet?.balances) return '' + const balance = balanceByToken(selectedWallet.balances, selectedChainID, selectedTokenAddress) if (!balance) return '' // 6 decimal places, prettier return floorFixed(balance.amount, 6) - }, [selectedChainID, selectedTokenAddress, balanceByToken]) + }, [selectedChainID, selectedTokenAddress, selectedWallet?.balances]) useEffect(() => { if (!_tokenValue) return @@ -282,6 +287,13 @@ export const CreateLinkInputView = ({ } }, [_tokenValue, inputDenomination]) + useEffect(() => { + if (isPeanutWallet) { + setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString()) + setSelectedTokenAddress(PEANUT_WALLET_TOKEN) + } + }, [isPeanutWallet]) + return ( @@ -316,16 +328,20 @@ export const CreateLinkInputView = ({ else handleOnNext() }} /> - - {hasFetchedBalances && balances.length === 0 && ( -
{ - open() - }} - className="cursor-pointer text-h9 underline" - > - ( Buy Tokens ) -
+ {isExternalWallet && ( + <> + + {selectedWallet!.balances!.length === 0 && ( +
{ + open() + }} + className="cursor-pointer text-h9 underline" + > + ( Buy Tokens ) +
+ )} + )} {(createType === 'link' || createType === 'email_link' || createType === 'sms_link') && ( { const { setLoadingState } = useContext(loadingStateContext) const { selectedChainID, selectedTokenData, selectedTokenAddress } = useContext(tokenSelectorContext) - const { balances, refetchBalances, balanceByToken } = useBalance() - const { chain: currentChain, address, selectedWallet } = useWallet() + const { chain: currentChain, address, selectedWallet, refetchBalances } = useWallet() const { switchChainAsync } = useSwitchChain() const { signTypedDataAsync } = useSignTypedData() @@ -49,17 +47,12 @@ export const useCreateLink = () => { throw new Error('Please ensure that the correct token and chain are defined') } // if the userbalances are know, the user must have a balance of the selected token - if (balances.length > 0) { - let balanceAmount = balanceByToken(selectedChainID, selectedTokenAddress)?.amount - if (!balanceAmount) { - balanceAmount = Number( - await peanut.getTokenBalance({ - tokenAddress: selectedTokenAddress, - chainId: selectedChainID, - walletAddress: address ?? '', - }) - ) - } + if ((selectedWallet?.balances?.length ?? 0) > 0) { + let balanceAmount = balanceByToken( + selectedWallet!.balances!, + selectedChainID, + selectedTokenAddress + )?.amount if (!balanceAmount || (balanceAmount && balanceAmount < Number(tokenValue))) { throw new Error( 'Please ensure that you have sufficient balance of the token you are trying to send' @@ -72,7 +65,7 @@ export const useCreateLink = () => { throw new Error('The minimum amount to send is 0.000001') } }, - [selectedChainID, selectedTokenAddress, balances, address, balanceByToken] + [selectedChainID, selectedTokenAddress, selectedWallet?.balances, address] ) const generateLinkDetails = useCallback( @@ -87,7 +80,11 @@ export const useCreateLink = () => { }) => { try { // get tokenDetails (type and decimals) - const tokenDetails = getTokenDetails(selectedTokenAddress, selectedChainID, balances) + const tokenDetails = getTokenDetails( + selectedTokenAddress, + selectedChainID, + selectedWallet?.balances ?? [] + ) // baseUrl let baseUrl = '' @@ -114,7 +111,7 @@ export const useCreateLink = () => { throw new Error('Error getting the linkDetails.') } }, - [selectedTokenAddress, selectedChainID, balances] + [selectedTokenAddress, selectedChainID, selectedWallet?.balances] ) const generatePassword = async () => { @@ -744,7 +741,7 @@ export const useCreateLink = () => { transaction: type === 'deposit' ? response && response.unsignedTxs[0] : undefined, }) - await refetchBalances() + await refetchBalances(address ?? '') return link[0] } catch (error) { diff --git a/src/components/Global/ChainSelector/index.tsx b/src/components/Global/ChainSelector/index.tsx index 995252116..b34f9a377 100644 --- a/src/components/Global/ChainSelector/index.tsx +++ b/src/components/Global/ChainSelector/index.tsx @@ -4,11 +4,11 @@ import { useContext, useMemo, useState } from 'react' import { Menu, Transition } from '@headlessui/react' import Icon from '../Icon' import Search from '../Search' -import { useBalance } from '@/hooks/useBalance' import { supportedPeanutChains } from '@/constants' import * as context from '@/context' import { IPeanutChainDetails } from '@/interfaces' -import * as utils from '@/utils' +import { formatTokenAmount, calculateValuePerChain } from '@/utils' +import { useWallet } from '@/context/walletContext' type Chain = { name: string @@ -27,10 +27,15 @@ interface IChainSelectorProps { const ChainSelector = ({ chainsToDisplay, onChange }: IChainSelectorProps) => { const [, setVisible] = useState(false) const [filterValue, setFilterValue] = useState('') + const { selectedWallet } = useWallet() - const { valuePerChain } = useBalance() const { selectedChainID, setSelectedChainID } = useContext(context.tokenSelectorContext) + const valuePerChain = useMemo( + () => calculateValuePerChain(selectedWallet?.balances ?? []), + [selectedWallet?.balances] + ) + const _chainsToDisplay = useMemo(() => { let chains if (chainsToDisplay) { @@ -142,7 +147,7 @@ const chainItem = ({ {chain.name}
{chain.name}
- {valuePerChain &&
${utils.formatTokenAmount(valuePerChain, 2)}
} + {valuePerChain &&
${formatTokenAmount(valuePerChain, 2)}
} ) } diff --git a/src/components/Global/TokenSelector/Components/AdvancedButton.tsx b/src/components/Global/TokenSelector/Components/AdvancedButton.tsx index 6bc3a09a2..76b091547 100644 --- a/src/components/Global/TokenSelector/Components/AdvancedButton.tsx +++ b/src/components/Global/TokenSelector/Components/AdvancedButton.tsx @@ -3,11 +3,6 @@ import Icon from '../../Icon' import * as utils from '@/utils' import { fetchTokenSymbol } from '@/utils' import * as context from '@/context' -import peanut from '@squirrel-labs/peanut-sdk' -import { useAccount } from 'wagmi' -import { useBalance } from '@/hooks/useBalance' -import Loading from '../../Loading' -import { useWallet } from '@/context/walletContext' interface IAdvancedTokenSelectorButtonProps { onClick: () => void @@ -41,39 +36,8 @@ export const AdvancedTokenSelectorButton = ({ onReset, }: IAdvancedTokenSelectorButtonProps) => { const { selectedChainID, selectedTokenAddress } = useContext(context.tokenSelectorContext) - const { address, isConnected } = useWallet() - const { hasFetchedBalances } = useBalance() - const [_tokenBalance, _setTokenBalance] = useState(tokenBalance) const [_tokenSymbol, _setTokenSymbol] = useState(tokenSymbol) - const getTokenBalance = async () => { - const balance = Number( - await peanut.getTokenBalance({ - chainId: selectedChainID, - tokenAddress: selectedTokenAddress, - walletAddress: address ?? '', - }) - ) - - if (balance) { - _setTokenBalance(balance) - } else { - _setTokenBalance(tokenBalance) - } - } - - useEffect(() => { - if (!isConnected) { - _setTokenBalance(undefined) - return - } - - _setTokenBalance(tokenBalance) - if ((tokenBalance === 0 || !tokenBalance) && address) { - getTokenBalance() - } - }, [tokenBalance, selectedChainID, selectedTokenAddress, address, isConnected]) - useEffect(() => { let isMounted = true if (!tokenSymbol) { @@ -132,14 +96,10 @@ export const AdvancedTokenSelectorButton = ({ {type === 'send' && - (hasFetchedBalances ? ( + (tokenBalance ? (

- Balance: {utils.formatTokenAmount(_tokenBalance ?? 0, 4)} + Balance: {utils.formatTokenAmount(tokenBalance ?? 0, 4)}

- ) : address ? ( -
- Balance: -
) : null)} {tokenAmount && tokenPrice && (

diff --git a/src/components/Global/TokenSelector/TokenSelector.tsx b/src/components/Global/TokenSelector/TokenSelector.tsx index 42b94cd9a..0d50d28d0 100644 --- a/src/components/Global/TokenSelector/TokenSelector.tsx +++ b/src/components/Global/TokenSelector/TokenSelector.tsx @@ -4,7 +4,6 @@ import { useContext, useEffect, useMemo, useRef, useState } from 'react' import Modal from '../Modal' import Search from '../Search' import ChainSelector from '../ChainSelector' -import { useBalance } from '@/hooks/useBalance' import { supportedPeanutChains, peanutTokenDetails } from '@/constants' import * as context from '@/context' import { areAddressesEqual, formatTokenAmount } from '@/utils' @@ -175,8 +174,7 @@ const TokenSelector = ({ const [hasUserChangedChain, setUserChangedChain] = useState(false) const focusButtonRef = useRef(null) - const { balances } = useBalance() - const { isConnected, signInModal } = useWallet() + const { isConnected, signInModal, selectedWallet } = useWallet() const { selectedChainID, selectedTokenAddress, @@ -210,6 +208,7 @@ const TokenSelector = ({ const _balancesToDisplay = useMemo(() => { let balancesToDisplay: IUserBalance[] + const balances = selectedWallet?.balances ?? [] if (safeInfo && walletType === 'blockscout') { balancesToDisplay = balances.filter((balance) => balance.chainId.toString() === safeInfo.chainId.toString()) @@ -236,7 +235,14 @@ const TokenSelector = ({ ] return balancesToDisplay - }, [balances, safeInfo, selectedChainTokens, walletType, showOnlySquidSupported, supportedSquidChainsAndTokens]) + }, [ + selectedWallet?.balances, + safeInfo, + selectedChainTokens, + walletType, + showOnlySquidSupported, + supportedSquidChainsAndTokens, + ]) const filteredBalances = useMemo(() => { // initially show all balances and the tokens on the current chain diff --git a/src/components/Home/WalletCard.tsx b/src/components/Home/WalletCard.tsx index 1e949e8ba..9763c9a51 100644 --- a/src/components/Home/WalletCard.tsx +++ b/src/components/Home/WalletCard.tsx @@ -36,15 +36,13 @@ export function WalletCard({ type, onClick, ...props }: WalletCardProps) { shadowSize="6" onClick={onClick} > - - -

Add your own ETH wallet

-
- - Add BYOW wallet -
- - + +

Add your own ETH wallet

+
+ + Add BYOW wallet +
+
) diff --git a/src/components/Home/WalletToggleButton.tsx b/src/components/Home/WalletToggleButton.tsx index 4af1c85fb..62138379e 100644 --- a/src/components/Home/WalletToggleButton.tsx +++ b/src/components/Home/WalletToggleButton.tsx @@ -1,10 +1,11 @@ import { useWallet } from '@/context/walletContext' import { NavIcons } from '../0_Bruddle' +import { useCallback } from 'react' const WalletToggleButton = () => { const { setSelectedWallet, wallets, selectedWallet, walletColor } = useWallet() - const toggleWallet = () => { + const toggleWallet = useCallback(() => { if (!wallets.length) return const currentIndex = wallets.findIndex((w) => w.address === selectedWallet?.address) @@ -13,7 +14,7 @@ const WalletToggleButton = () => { const nextIndex = (currentIndex + 1) % wallets.length setSelectedWallet(wallets[nextIndex]) - } + }, [wallets, selectedWallet, setSelectedWallet]) return (
{ - const { balances } = useBalance() - const { selectedTokenPrice, inputDenomination, selectedChainID, selectedTokenAddress, selectedTokenData } = - useContext(context.tokenSelectorContext) + const { selectedWallet, isExternalWallet, isPeanutWallet } = useWallet() + const { + selectedTokenPrice, + inputDenomination, + selectedChainID, + setSelectedChainID, + selectedTokenAddress, + setSelectedTokenAddress, + selectedTokenData, + } = useContext(context.tokenSelectorContext) const { setLoadingState, loadingState, isLoading } = useContext(context.loadingStateContext) const [errorState, setErrorState] = useState<{ showError: boolean @@ -140,6 +148,13 @@ export const InitialView = ({ const [isValidRecipient, setIsValidRecipient] = useState(false) const [inputChanging, setInputChanging] = useState(false) + useEffect(() => { + if (isPeanutWallet) { + setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString()) + setSelectedTokenAddress(PEANUT_WALLET_TOKEN) + } + }, [isPeanutWallet]) + return ( @@ -162,13 +177,13 @@ export const InitialView = ({ recipientAddress, tokenAddress: selectedTokenAddress, chainId: selectedChainID, - userBalances: balances, + userBalances: selectedWallet?.balances ?? [], tokenValue, tokenData: selectedTokenData, }) }} /> - + {isExternalWallet && } { const { sendTransactions, checkUserHasEnoughBalance } = useCreateLink() - const { address, signInModal, isConnected, chain: currentChain } = useWallet() + const { address, signInModal, isConnected, chain: currentChain, isExternalWallet, isPeanutWallet } = useWallet() const { switchChainAsync } = useSwitchChain() const { setLoadingState, loadingState, isLoading } = useContext(context.loadingStateContext) @@ -338,10 +338,19 @@ export const InitialView = ({ } } - const resetTokenAndChain = () => { - setSelectedChainID(requestLinkData.chainId) - setSelectedTokenAddress(requestLinkData.tokenAddress) - } + const resetTokenAndChain = useCallback(() => { + if (isPeanutWallet) { + setSelectedChainID(PEANUT_WALLET_CHAIN.id.toString()) + setSelectedTokenAddress(PEANUT_WALLET_TOKEN) + } else { + setSelectedChainID(requestLinkData.chainId) + setSelectedTokenAddress(requestLinkData.tokenAddress) + } + }, [requestLinkData, isPeanutWallet]) + + useEffect(() => { + if (isPeanutWallet) resetTokenAndChain() + }, [resetTokenAndChain, isPeanutWallet]) return ( @@ -391,7 +400,9 @@ export const InitialView = ({ )}
- {tokenSupportsXChain && } + {isExternalWallet && tokenSupportsXChain && ( + + )} {!isFeeEstimationError && ( <>
diff --git a/src/constants/general.consts.ts b/src/constants/general.consts.ts index ad30d86e9..55a7a76e4 100644 --- a/src/constants/general.consts.ts +++ b/src/constants/general.consts.ts @@ -11,8 +11,6 @@ export const infuraRpcUrls: Record = { [arbitrumSepolia.id]: `https://arbitrum-sepolia.infura.io/v3/${infuraApiKey}`, } -export const USDC_ARBITRUM_ADDRESS = '0xaf88d065e77c8cc2239327c5edb3a432268e5831' - export const ipfsProviderArray = [ 'https://ipfs.io/ipfs/', 'https://cloudflare-ipfs.com/ipfs/', diff --git a/src/constants/zerodev.consts.ts b/src/constants/zerodev.consts.ts index 44925ebad..91a6646b0 100644 --- a/src/constants/zerodev.consts.ts +++ b/src/constants/zerodev.consts.ts @@ -12,4 +12,7 @@ export const PASSKEY_SERVER_URL = process.env.NEXT_PUBLIC_ZERO_DEV_PASSKEY_SERVE export const ZERO_DEV_PROJECT_ID = process.env.NEXT_PUBLIC_ZERO_DEV_PASSKEY_PROJECT_ID export const PEANUT_WALLET_CHAIN = arbitrum +//USDC Arbitrum address +export const PEANUT_WALLET_TOKEN = '0xaf88d065e77c8cc2239327c5edb3a432268e5831' + export const USER_OP_ENTRY_POINT = getEntryPoint('0.7') diff --git a/src/context/walletContext/walletContext.tsx b/src/context/walletContext/walletContext.tsx index f4545121f..4bb5e8ffb 100644 --- a/src/context/walletContext/walletContext.tsx +++ b/src/context/walletContext/walletContext.tsx @@ -4,11 +4,11 @@ import { createContext, useContext, useEffect, useState, ReactNode, useCallback, import * as interfaces from '@/interfaces' import { useAccount } from 'wagmi' import { useZeroDev } from './zeroDevContext.context' -import { useQuery } from '@tanstack/react-query' -import { PEANUT_WALLET_CHAIN, USDC_ARBITRUM_ADDRESS } from '@/constants' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' import { Chain, erc20Abi, getAddress } from 'viem' import { useAuth } from '../authContext' -import { backgroundColorFromAddress, areAddressesEqual } from '@/utils' +import { backgroundColorFromAddress, areAddressesEqual, fetchWalletBalances } from '@/utils' import { peanutPublicClient } from '@/constants/viem.consts' interface WalletContextType { @@ -24,15 +24,23 @@ interface WalletContextType { close: () => void } walletColor: string + refetchBalances: (address: string) => Promise + isPeanutWallet: boolean + isExternalWallet: boolean } function isPeanut(wallet: interfaces.IDBWallet | undefined) { return wallet?.walletProviderType === interfaces.WalletProviderType.PEANUT } +function isExternalWallet(wallet: interfaces.IDBWallet | undefined) { + return wallet?.walletProviderType === interfaces.WalletProviderType.BYOW +} + const WalletContext = createContext(undefined) export const WalletProvider = ({ children }: { children: ReactNode }) => { + const queryClient = useQueryClient() const [promptWalletSigninOpen, setPromptWalletSigninOpen] = useState(false) ////// ZeroDev props const { address: kernelClientAddress, isKernelClientReady } = useZeroDev() @@ -68,15 +76,17 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => { queryFn: async () => { // non users can connect BYOW (to pay a request for example) if (!user) { - return wagmiAddress - ? [ - { - ...createDefaultDBWallet(wagmiAddress), - connected: isWalletConnected(createDefaultDBWallet(wagmiAddress)), - balance: BigInt(0), - }, - ] - : [] + if (!wagmiAddress) return [] + + const { balances, totalBalance } = await fetchWalletBalances(wagmiAddress) + return [ + { + ...createDefaultDBWallet(wagmiAddress), + connected: isWalletConnected(createDefaultDBWallet(wagmiAddress)), + balances, + balance: BigInt(Math.floor(totalBalance * 1e6)), + }, + ] } const processedWallets = user.accounts @@ -93,18 +103,26 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => { } let balance = BigInt(0) + let balances: interfaces.IUserBalance[] | undefined + if (isPeanut(dbWallet)) { balance = await peanutPublicClient.readContract({ - address: USDC_ARBITRUM_ADDRESS, + address: PEANUT_WALLET_TOKEN, abi: erc20Abi, functionName: 'balanceOf', args: [getAddress(dbWallet.address)], }) + } else { + // For BYOW wallets, fetch all balances + const { balances: fetchedBalances, totalBalance } = await fetchWalletBalances(dbWallet.address) + balances = fetchedBalances + balance = BigInt(Math.floor(totalBalance * 1e6)) } const wallet: interfaces.IWallet = { ...dbWallet, balance, + balances, connected: false, // Will be set in processedWallets memo } @@ -137,13 +155,13 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => { useEffect(() => { if (!user || !wagmiAddress || !wallets.length) return - const walletExists = wallets.some((wallet) => wallet.address === wagmiAddress) + const walletExists = wallets.some((wallet) => areAddressesEqual(wallet.address, wagmiAddress)) if (!walletExists) { addAccount({ accountIdentifier: wagmiAddress, accountType: interfaces.WalletProviderType.BYOW, userId: user.user.userId as string, - }) + }).catch(console.error) } }, [wagmiAddress, wallets, user, addAccount]) @@ -156,6 +174,49 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => { [wallets, isWalletConnected] ) + const refetchBalances = useCallback( + async (address: string) => { + const wallet = wallets.find((w) => w.address === address) + if (!wallet) return + + try { + if (isPeanut(wallet)) { + const balance = await peanutPublicClient.readContract({ + address: PEANUT_WALLET_TOKEN, + abi: erc20Abi, + functionName: 'balanceOf', + args: [getAddress(address)], + }) + + await queryClient.setQueryData( + ['wallets', user?.accounts, wagmiAddress], + (oldData: interfaces.IWallet[] | undefined) => + oldData?.map((w) => (w.address === address ? { ...w, balance } : w)) + ) + } else { + const { balances, totalBalance } = await fetchWalletBalances(address) + + await queryClient.setQueryData( + ['wallets', user?.accounts, wagmiAddress], + (oldData: interfaces.IWallet[] | undefined) => + oldData?.map((w) => + w.address === address + ? { + ...w, + balances, + balance: BigInt(Math.floor(totalBalance * 1e6)), + } + : w + ) + ) + } + } catch (error) { + console.error('Error refetching balance:', error) + } + }, + [wallets, user?.accounts, wagmiAddress, queryClient] + ) + const contextValue: WalletContextType = { wallets: processedWallets, selectedWallet, @@ -171,6 +232,9 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => { close: () => setPromptWalletSigninOpen(false), }, walletColor: selectedWallet?.address ? backgroundColorFromAddress(selectedWallet.address) : 'rgba(0,0,0,0)', + refetchBalances, + isPeanutWallet: isPeanut(selectedWallet), + isExternalWallet: isExternalWallet(selectedWallet), } return {children} diff --git a/src/hooks/useBalance.tsx b/src/hooks/useBalance.tsx deleted file mode 100644 index 41eb615cf..000000000 --- a/src/hooks/useBalance.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { IUserBalance, ChainValue } from '@/interfaces' -import { useEffect, useState, useRef, useCallback } from 'react' -import { areAddressesEqual, isAddressZero } from '@/utils' -import { useWallet } from '@/context/walletContext' - -/** - * Custom React hook to fetch and manage user's wallet balances across multiple chains, - * convert API response to a structured format, and calculate total value per chain. - */ -export const useBalance = () => { - const [balances, setBalances] = useState([]) - const [hasFetchedBalances, setHasFetchedBalances] = useState(false) - const [valuePerChain, setValuePerChain] = useState([]) - const { address, isConnected } = useWallet() - const prevAddressRef = useRef(undefined) - - useEffect(() => { - if (address && prevAddressRef.current !== address) { - prevAddressRef.current = address - refetchBalances() - } - }, [address]) - - //remove balances on disconnect - useEffect(() => { - if (!isConnected) { - setBalances([]) - } else { - refetchBalances() - } - }, [isConnected]) - - // Function to map the mobula response to the IUserBalance interface - function convertToUserBalances( - data: Array<{ - name: string - symbol: string - chainId: string - value: number - price: number - quantity: { decimals: string; numeric: string } - iconUrl: string - address?: string - }> - ): IUserBalance[] { - return data.map((item) => ({ - chainId: item?.chainId ? item.chainId.split(':')[1] : '1', - address: item?.address ? item.address.split(':')[2] : '0x0000000000000000000000000000000000000000', - name: item.name, - symbol: item.symbol, - decimals: parseInt(item.quantity.decimals), - price: item.price, - amount: parseFloat(item.quantity.numeric), - currency: 'usd', - logoURI: item.iconUrl, - value: item.value.toString(), - })) - } - - function calculateValuePerChain( - balances: { - name: string - symbol: string - chainId: string - value: number - price: number - quantity: { decimals: string; numeric: string } - iconUrl: string - address?: string - }[] - ): ChainValue[] { - let result: ChainValue[] = [] - - try { - const chainValueMap: { [key: string]: number } = {} - balances.forEach((balance) => { - const chainId = balance?.chainId ? balance.chainId.split(':')[1] : '1' - if (!chainValueMap[chainId]) { - chainValueMap[chainId] = 0 - } - if (balance.value) chainValueMap[chainId] += balance.value - }) - - result = Object.keys(chainValueMap).map((chainId) => ({ - chainId, - valuePerChain: chainValueMap[chainId], - })) - - result.sort((a, b) => b.valuePerChain - a.valuePerChain) - } catch (error) { - console.log('Error calculating value per chain: ', error) - } - return result - } - - const fetchBalances = async (address: string) => { - try { - let attempts = 0 - const maxAttempts = 3 - let success = false - let userBalances: IUserBalance[] = [] - - while (!success && attempts < maxAttempts) { - try { - const apiResponse = await fetch('/api/walletconnect/fetch-wallet-balance', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - address, - }), - }) - - if (apiResponse.ok) { - const apiResponseJson = await apiResponse.json() - - userBalances = convertToUserBalances( - apiResponseJson.balances.filter((balance: any) => balance.value > 0.009) - ) - .map((balance) => - balance.chainId === '8508132' - ? { ...balance, chainId: '534352' } - : balance.chainId === '81032' - ? { ...balance, chainId: '81457' } - : balance.chainId === '59160' - ? { ...balance, chainId: '59144' } - : balance - ) - .sort((a, b) => { - const valueA = parseFloat(a.value) - const valueB = parseFloat(b.value) - - if (valueA === valueB) { - if (isAddressZero(a.address)) return -1 - if (isAddressZero(b.address)) return 1 - - return b.amount - a.amount - } else { - return valueB - valueA - } - }) - - const valuePerChain = calculateValuePerChain(apiResponseJson.balances) - setValuePerChain(valuePerChain) - success = true - } else { - throw new Error('API request failed') - } - } catch (error) { - console.log('Error fetching userBalances: ', error) - attempts += 1 - if (attempts >= maxAttempts) { - console.log('Max retry attempts reached for fetching balances using walletconnect. Giving up.') - } - } - } - setTimeout(() => { - setHasFetchedBalances(true) - }, 100) // Delay to prevent flickering, ensuring state is set before finishing this call - return userBalances - } catch (error) { - console.error('Unexpected error loading userBalances: ', error) - } - } - - const refetchBalances = async () => { - if (address) { - const balances = await fetchBalances(address) - if (balances) { - setBalances(balances) - } - } - } - - const balanceByToken = useCallback( - (chainId: string, tokenAddress: string): IUserBalance | undefined => { - if (!chainId || !tokenAddress) return undefined - return balances.find( - (balance) => balance.chainId === chainId && areAddressesEqual(balance.address, tokenAddress) - ) - }, - [balances] - ) - - return { balances, fetchBalances, valuePerChain, refetchBalances, hasFetchedBalances, balanceByToken } -} diff --git a/src/interfaces/wallet.interfaces.ts b/src/interfaces/wallet.interfaces.ts index 30e8ada91..02e0c2774 100644 --- a/src/interfaces/wallet.interfaces.ts +++ b/src/interfaces/wallet.interfaces.ts @@ -29,6 +29,7 @@ export interface IWallet extends IDBWallet { // and the provider will always be connected to that. connected: boolean balance: bigint + balances?: interfaces.IUserBalance[] } export enum WalletErrorType { diff --git a/src/utils/balance.utils.ts b/src/utils/balance.utils.ts new file mode 100644 index 000000000..04c751b7d --- /dev/null +++ b/src/utils/balance.utils.ts @@ -0,0 +1,101 @@ +import { isAddressZero, areAddressesEqual } from '@/utils' +import { IUserBalance, ChainValue } from '@/interfaces' + +export async function fetchWalletBalances( + address: string +): Promise<{ balances: IUserBalance[]; totalBalance: number }> { + try { + const apiResponse = await fetch('/api/walletconnect/fetch-wallet-balance', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ address }), + }) + + if (!apiResponse.ok) { + throw new Error('API request failed') + } + + const apiResponseJson = await apiResponse.json() + + const processedBalances = apiResponseJson.balances + .filter((balance: any) => balance.value > 0.009) + .map((item: any) => ({ + chainId: item?.chainId ? item.chainId.split(':')[1] : '1', + address: item?.address ? item.address.split(':')[2] : '0x0000000000000000000000000000000000000000', + name: item.name, + symbol: item.symbol, + decimals: parseInt(item.quantity.decimals), + price: item.price, + amount: parseFloat(item.quantity.numeric), + currency: 'usd', + logoURI: item.iconUrl, + value: item.value.toString(), + })) + .map((balance: any) => + balance.chainId === '8508132' + ? { ...balance, chainId: '534352' } + : balance.chainId === '81032' + ? { ...balance, chainId: '81457' } + : balance.chainId === '59160' + ? { ...balance, chainId: '59144' } + : balance + ) + .sort((a: any, b: any) => { + const valueA = parseFloat(a.value) + const valueB = parseFloat(b.value) + + if (valueA === valueB) { + if (isAddressZero(a.address)) return -1 + if (isAddressZero(b.address)) return 1 + return b.amount - a.amount + } + return valueB - valueA + }) + + const totalBalance = processedBalances.reduce((acc: number, balance: any) => acc + Number(balance.value), 0) + + return { + balances: processedBalances, + totalBalance, + } + } catch (error) { + console.error('Error fetching wallet balances:', error) + return { balances: [], totalBalance: 0 } + } +} + +export function balanceByToken( + balances: IUserBalance[], + chainId: string, + tokenAddress: string +): IUserBalance | undefined { + if (!chainId || !tokenAddress) return undefined + return balances.find((balance) => balance.chainId === chainId && areAddressesEqual(balance.address, tokenAddress)) +} + +export function calculateValuePerChain(balances: IUserBalance[]): ChainValue[] { + let result: ChainValue[] = [] + + try { + const chainValueMap: { [key: string]: number } = {} + balances.forEach((balance) => { + const chainId = balance?.chainId ? balance.chainId.split(':')[1] : '1' + if (!chainValueMap[chainId]) { + chainValueMap[chainId] = 0 + } + if (balance.value) chainValueMap[chainId] += Number(balance.value) + }) + + result = Object.keys(chainValueMap).map((chainId) => ({ + chainId, + valuePerChain: chainValueMap[chainId], + })) + + result.sort((a, b) => b.valuePerChain - a.valuePerChain) + } catch (error) { + console.log('Error calculating value per chain: ', error) + } + return result +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 5a3df1c3a..8973c186f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,3 +2,4 @@ export * from './general.utils' export * from './sdkErrorHandler.utils' export * from './fetch.utils' export * from './cashout.utils' +export * from './balance.utils' From 67455d9557e7cf96b417400679e92bb63292d829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Mon, 2 Dec 2024 14:19:13 -0300 Subject: [PATCH 2/5] fix: add missing function deinition --- src/components/Cashout/Components/Initial.view.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Cashout/Components/Initial.view.tsx b/src/components/Cashout/Components/Initial.view.tsx index e9104c79f..09441a74d 100644 --- a/src/components/Cashout/Components/Initial.view.tsx +++ b/src/components/Cashout/Components/Initial.view.tsx @@ -19,6 +19,7 @@ import { Button, Card } from '@/components/0_Bruddle' import { useWallet } from '@/context/walletContext' import { sanitizeBankAccount } from '@/utils/format.utils' import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' +import { useWeb3Modal } from '@web3modal/wagmi/react' export const InitialCashoutView = ({ onNext, @@ -73,6 +74,7 @@ export const InitialCashoutView = ({ const { prepareCreateLinkWrapper } = useCreateLink() const { isConnected, signInModal, selectedWallet, isExternalWallet, isPeanutWallet } = useWallet() + const { open: web3modalOpen } = useWeb3Modal() const isBelowMinLimit = useMemo(() => { if (!usdValue) return false @@ -288,7 +290,7 @@ export const InitialCashoutView = ({ {selectedWallet!.balances!.length === 0 && (
{ - open() + web3modalOpen() }} className="cursor-pointer text-h9 underline" > From cac8494587c04b2c769ddfe240fa7e1f9e2b4055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Mon, 2 Dec 2024 14:42:28 -0300 Subject: [PATCH 3/5] chore: remove superflous docs --- docs/high_level_overview.md | 178 ------------------------------------ 1 file changed, 178 deletions(-) delete mode 100644 docs/high_level_overview.md diff --git a/docs/high_level_overview.md b/docs/high_level_overview.md deleted file mode 100644 index c9268fab7..000000000 --- a/docs/high_level_overview.md +++ /dev/null @@ -1,178 +0,0 @@ -# # Next.js Project Overview - -The peanut-ui repository contains a Next.js application structured for scalability and maintainability. Below is a comprehensive overview of the directory structure, core functionality, and configuration details to help developers get up to speed quickly. - -## Table of Contents - -- [# Next.js Project Overview](#-nextjs-project-overview) - - [Table of Contents](#table-of-contents) - - [Project Structure](#project-structure) - - [Next.js App Router](#nextjs-app-router) - - [Key Concepts:](#key-concepts) - - [Assets](#assets) - - [Configuration](#configuration) - - [Contexts](#contexts) - - [Hooks](#hooks) - - [Middleware](#middleware) - - [Key Highlights:](#key-highlights) - - [Tailwind Configuration](#tailwind-configuration) - - [How to Get Started](#how-to-get-started) - -## Project Structure - -### Next.js App Router - -This Next.js application leverages the **App Router** introduced in Next.js 13, providing a more modular and file-based system for defining routes. The app structure is as follows: - -- **App Directory (`/app`)**: The core of the routing system, where all pages, layouts, and API routes are defined. - -#### Key Concepts: - -1. **Pages**: - - - Each file in the `/app` directory represents a route. Pages are defined by `page.tsx` files within specific folders. - - For example, `/app/home/page.tsx` would serve the route `/home`. - - The `page.tsx` file defines the front-end UI for that route. - -2. **API Routes**: - - - API routes are located in the `/app/api` folder and follow the same structure. Instead of using `page.tsx`, API routes are defined with `route.ts` files. - - For example, `/app/api/user/route.ts` defines the API endpoint for `/api/user`. - - These routes are server-side and provide backend functionality within the same app. - -3. **Nested Routing**: - - The App Router allows for nested routes with shared layouts. This means multiple routes can share a common layout while having distinct page content. - - This modular structure improves code organization and scalability. - -This setup simplifies both front-end and back-end routing while providing flexibility to create nested, reusable layouts and API endpoints within the same directory. The new App Router enhances server-side rendering (SSR), static site generation (SSG), and hybrid rendering options, allowing for more efficient app development. - -For more details on how the App Router works, you can refer to the official Next.js documentation [here](https://nextjs.org/docs/app/building-your-application/routing#the-app-router). - -### Assets - -All static assets such as images, logos, icons, and more are stored in the `/assets` folder. These assets can be easily imported throughout the application using: - -```js -import * as assets from '/assets' - -// Example usage: -;Peanut Logo -``` - -This structure allows for a centralized asset management system and avoids the need to import assets individually from scattered files. - -### Configuration - -The `/config` folder holds all configuration-related files, such as: - -- **WAGMI**: Configuration for handling Web3 interactions. -- **WalletConnect**: Configuration for setting up the WalletConnect provider. -- **Google Analytics**: Set up for tracking user interactions. -- **LogRocket**: Provides error and log tracking. -- **Sentry**: Monitors and logs errors and performance issues. - -This setup makes it easy to manage external services and configurations in a single, well-defined location. - -### Contexts - -There are three major contexts provided in the `/context` folder: - -1. **Auth Context (`/context/auth`)**: - - - Handles user authentication, updating user details, adding additional accounts, and logging out. - - Provides a custom hook for child components to easily interact with authentication functionality. - - Allows child components to import and use: - ```js - const { - user, - setUser, - updateBridgeCustomerId, - fetchUser, - updateUserName, - submitProfilePhoto, - addAccount, - isFetchingUser, - logoutUser, - } = useAuthContext() - ``` - -2. **TokenSelector Context (`/context/tokenselector`)**: - - - Manages the selected token and chain ID, handling token price fetching and denomination input. - - Updates context values and resets the provider based on wallet connection status and user preferences. - - Provides: - ```js - const { - selectedTokenAddress, - setSelectedTokenAddress, - selectedChainID, - setSelectedChainID, - selectedTokenPrice, - setSelectedTokenPrice, - inputDenomination, - setInputDenomination, - refetchXchainRoute, - setRefetchXchainRoute, - resetTokenContextProvider, - } = useTokenSelectorContext() - ``` - -3. **LoadingState Context (`/context/loadingstate`)**: - - Tracks the app's loading state and provides mechanisms to update it across the entire component tree. - - It is used for managing loading states such as fetching data, processing transactions, and switching chains. - - Provides: - ```js - const { loadingState, setLoadingState, isLoading } = useLoadingStateContext() - ``` - -### Hooks - -The `/hooks` folder contains a custom hook to help manage wallet functionality: - -1. **useWalletType**: - - Detects and manages the user's wallet type (e.g., Blockscout or Safe App environment). - - Fetches environment and wallet info, then updates state based on the wallet address. - - Usage: - ```js - const { walletType, environmentInfo, safeInfo } = useWalletType() - ``` - -## Middleware - -The middleware logic is defined in `middleware.ts`. It includes a promo link handler, redirection logic, and custom headers for API routes. - -### Key Highlights: - -- **Promo Link Redirection**: Detects and redirects promo links. -- **API Cache Control**: Ensures API routes are not cached by setting appropriate headers. - -## Tailwind Configuration - -The application uses TailwindCSS for styling. A custom configuration file `tailwind.config.ts` is provided, which includes predefined components for consistent design across the application. This configuration ensures that styles can be easily reused and adapted throughout the project. - -## How to Get Started - -To get started with the project, follow these steps: - -1. **Clone the repository**: - - ```bash - git clone https://github.com/peanutprotocol/peanut-ui - cd peanut-ui - ``` - -2. **Install dependencies**: - - ```bash - pnpm install - ``` - -3. **Run the development server**: - - ```bash - pnpm run dev - ``` - -4. **Access the application**: Open `http://localhost:3000` in your browser. - -That's it! You are now ready to develop and extend the functionality of the app. From 8c264ceaddcbd0f307c690ace73abfb362696778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Mon, 2 Dec 2024 15:05:20 -0300 Subject: [PATCH 4/5] fix: correct maxValue for Input --- src/components/Cashout/Components/Initial.view.tsx | 6 ++++-- src/components/Create/Link/Input.view.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/Cashout/Components/Initial.view.tsx b/src/components/Cashout/Components/Initial.view.tsx index 09441a74d..3ee59d40f 100644 --- a/src/components/Cashout/Components/Initial.view.tsx +++ b/src/components/Cashout/Components/Initial.view.tsx @@ -196,12 +196,14 @@ export const InitialCashoutView = ({ } const maxValue = useMemo(() => { - if (!selectedWallet?.balances) return '' + if (!selectedWallet?.balances) { + return selectedWallet?.balance.toString() ?? '' + } const balance = balanceByToken(selectedWallet.balances, selectedChainID, selectedTokenAddress) if (!balance) return '' // 6 decimal places, prettier return floorFixed(balance.amount, 6) - }, [selectedChainID, selectedTokenAddress, selectedWallet?.balances]) + }, [selectedChainID, selectedTokenAddress, selectedWallet?.balances, selectedWallet?.balance]) useEffect(() => { if (!_tokenValue) return diff --git a/src/components/Create/Link/Input.view.tsx b/src/components/Create/Link/Input.view.tsx index d797ced7e..32e863d9e 100644 --- a/src/components/Create/Link/Input.view.tsx +++ b/src/components/Create/Link/Input.view.tsx @@ -265,12 +265,14 @@ export const CreateLinkInputView = ({ } const maxValue = useMemo(() => { - if (!selectedWallet?.balances) return '' + if (!selectedWallet?.balances) { + return selectedWallet?.balance.toString() ?? '' + } const balance = balanceByToken(selectedWallet.balances, selectedChainID, selectedTokenAddress) if (!balance) return '' // 6 decimal places, prettier return floorFixed(balance.amount, 6) - }, [selectedChainID, selectedTokenAddress, selectedWallet?.balances]) + }, [selectedChainID, selectedTokenAddress, selectedWallet?.balances, selectedWallet?.balance]) useEffect(() => { if (!_tokenValue) return From 45af86b3c80e4bab5f274afa6033c92e6af00ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Jos=C3=A9=20Ram=C3=ADrez?= Date: Mon, 2 Dec 2024 15:17:22 -0300 Subject: [PATCH 5/5] refactor: use parseUnits for converting usd to usdc base units --- src/context/walletContext/walletContext.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/context/walletContext/walletContext.tsx b/src/context/walletContext/walletContext.tsx index d54fc27b6..a7ad52464 100644 --- a/src/context/walletContext/walletContext.tsx +++ b/src/context/walletContext/walletContext.tsx @@ -6,7 +6,7 @@ import { useAccount } from 'wagmi' import { useZeroDev } from './zeroDevContext.context' import { useQuery, useQueryClient } from '@tanstack/react-query' import { PEANUT_WALLET_CHAIN, PEANUT_WALLET_TOKEN } from '@/constants' -import { Chain, erc20Abi, getAddress } from 'viem' +import { Chain, erc20Abi, getAddress, parseUnits } from 'viem' import { useAuth } from '../authContext' import { backgroundColorFromAddress, areEvmAddressesEqual, fetchWalletBalances } from '@/utils' import { peanutPublicClient } from '@/constants/viem.consts' @@ -84,7 +84,7 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => { ...createDefaultDBWallet(wagmiAddress), connected: isWalletConnected(createDefaultDBWallet(wagmiAddress)), balances, - balance: BigInt(Math.floor(totalBalance * 1e6)), + balance: parseUnits(totalBalance.toString(), 6), }, ] } @@ -116,7 +116,7 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => { // For BYOW wallets, fetch all balances const { balances: fetchedBalances, totalBalance } = await fetchWalletBalances(dbWallet.address) balances = fetchedBalances - balance = BigInt(Math.floor(totalBalance * 1e6)) + balance = parseUnits(totalBalance.toString(), 6) } const wallet: interfaces.IWallet = { @@ -204,7 +204,7 @@ export const WalletProvider = ({ children }: { children: ReactNode }) => { ? { ...w, balances, - balance: BigInt(Math.floor(totalBalance * 1e6)), + balance: parseUnits(totalBalance.toString(), 6), } : w )