Skip to content

Commit

Permalink
feat(balances): update balances for priority tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
shoom3301 committed Nov 19, 2023
1 parent ea3af64 commit a645cce
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ReactNode, useEffect } from 'react'

import { PriorityTokensUpdater } from '@cowprotocol/balances-and-allowances'
import { maxAmountSpend } from '@cowprotocol/common-utils'
import { isInjectedWidget } from '@cowprotocol/common-utils'
import { useIsSafeWallet, useWalletDetails, useWalletInfo } from '@cowprotocol/wallet'
Expand Down Expand Up @@ -28,6 +29,7 @@ import { PoweredFooter } from 'common/pure/PoweredFooter'
import * as styledEl from './styled'
import { TradeWidgetModals } from './TradeWidgetModals'

import { usePriorityTokenAddresses } from '../../hooks/usePriorityTokenAddresses'
import { CommonTradeUpdater } from '../../updaters/CommonTradeUpdater'
import { DisableNativeTokenSellingUpdater } from '../../updaters/DisableNativeTokenSellingUpdater'
import { PriceImpactUpdater } from '../../updaters/PriceImpactUpdater'
Expand Down Expand Up @@ -92,12 +94,13 @@ export function TradeWidget(props: TradeWidgetProps) {
disablePriceImpact,
} = params

const { chainId } = useWalletInfo()
const { chainId, account } = useWalletInfo()
const isWrapOrUnwrap = useIsWrapOrUnwrap()
const { allowsOffchainSigning } = useWalletDetails()
const isChainIdUnsupported = useIsProviderNetworkUnsupported()
const isSafeWallet = useIsSafeWallet()
const openTokenSelectWidget = useOpenTokenSelectWidget()
const priorityTokenAddresses = usePriorityTokenAddresses()

const currenciesLoadingInProgress = !inputCurrencyInfo.currency && !outputCurrencyInfo.currency

Expand Down Expand Up @@ -132,6 +135,7 @@ export function TradeWidget(props: TradeWidgetProps) {

return (
<styledEl.Container id={id}>
<PriorityTokensUpdater account={account} chainId={chainId} tokenAddresses={priorityTokenAddresses} />
<RecipientAddressUpdater />

{!disableQuotePolling && <TradeQuoteUpdater />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useMemo } from 'react'

import { getAddress, isTruthy } from '@cowprotocol/common-utils'
import { OrderClass } from '@cowprotocol/cow-sdk'
import { useWalletInfo } from '@cowprotocol/wallet'

import { useSelector } from 'react-redux'

import { AppState } from 'legacy/state'
import { PartialOrdersMap } from 'legacy/state/orders/reducer'

import { useDerivedTradeState } from './useDerivedTradeState'

export function usePriorityTokenAddresses(): string[] {
const { chainId } = useWalletInfo()
const tradeState = useDerivedTradeState()

const pending = useSelector<AppState, PartialOrdersMap | undefined>((state) => {
return state.orders?.[chainId]?.pending
})

const pendingOrdersTokenAddresses = useMemo(() => {
if (!pending) return undefined

return Object.values(pending)
.filter(isTruthy)
.filter(({ order }) => order.class === OrderClass.MARKET)
.map(({ order }) => {
return [order.inputToken.address, order.outputToken.address]
})
.flat()
}, [pending])

const inputCurrency = tradeState?.state?.inputCurrency
const outputCurrency = tradeState?.state?.outputCurrency

const inputCurrencyAddress = getAddress(inputCurrency)
const outputCurrencyAddress = getAddress(outputCurrency)

return useMemo(() => {
return (pendingOrdersTokenAddresses || []).concat(inputCurrencyAddress || [], outputCurrencyAddress || [])
}, [inputCurrencyAddress, outputCurrencyAddress, pendingOrdersTokenAddresses])
}
17 changes: 12 additions & 5 deletions libs/balances-and-allowances/src/hooks/useNativeTokenBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,26 @@ import { useWalletInfo } from '@cowprotocol/wallet'
import { BigNumber } from '@ethersproject/bignumber'
import { useWeb3React } from '@web3-react/core'

import ms from 'ms.macro'
import useSWR, { SWRResponse } from 'swr'

const SWR_CONFIG = { refreshInterval: ms`11s` }

export function useNativeTokenBalance(customAccount?: string): SWRResponse<BigNumber | undefined> {
const { provider } = useWeb3React()
const { account } = useWalletInfo()

const balanceAccount = customAccount || account

return useSWR(['useNativeTokenBalance', balanceAccount, provider], async () => {
if (!provider || !balanceAccount) return undefined
return useSWR(
['useNativeTokenBalance', balanceAccount, provider],
async () => {
if (!provider || !balanceAccount) return undefined

const contract = getMulticallContract(provider)
const contract = getMulticallContract(provider)

return contract.callStatic.getEthBalance(balanceAccount)
})
return contract.callStatic.getEthBalance(balanceAccount)
},
SWR_CONFIG
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { useSetAtom } from 'jotai/index'
import { useEffect, useMemo } from 'react'

import { GP_VAULT_RELAYER } from '@cowprotocol/common-const'
import { getIsNativeToken } from '@cowprotocol/common-utils'
import { SupportedChainId } from '@cowprotocol/cow-sdk'
import { useMultipleContractSingleData } from '@cowprotocol/multicall'
import { BigNumber } from '@ethersproject/bignumber'

import { SWRConfiguration } from 'swr'

import { erc20Interface } from '../const'
import { AllowancesState, allowancesState } from '../state/allowancesAtom'
import { balancesAtom, BalancesState } from '../state/balancesAtom'

const MULTICALL_OPTIONS = {}

export interface PersistBalancesAndAllowancesParams {
account: string | undefined
chainId: SupportedChainId
tokenAddresses: string[]
balancesSwrConfig: SWRConfiguration
allowancesSwrConfig: SWRConfiguration
setLoadingState?: boolean
}

export function usePersistBalancesAndAllowances(params: PersistBalancesAndAllowancesParams) {
const { account, chainId, tokenAddresses, setLoadingState, balancesSwrConfig, allowancesSwrConfig } = params

const setBalances = useSetAtom(balancesAtom)
const setAllowances = useSetAtom(allowancesState)

const spender = GP_VAULT_RELAYER[chainId]

const balanceOfParams = useMemo(() => (account ? [account] : undefined), [account])
const allowanceParams = useMemo(() => (account && spender ? [account, spender] : undefined), [account, spender])

const { isLoading: isBalancesLoading, data: balances } = useMultipleContractSingleData<{ balance: BigNumber }>(
tokenAddresses,
erc20Interface,
'balanceOf',
balanceOfParams,
MULTICALL_OPTIONS,
balancesSwrConfig
)

const { isLoading: isAllowancesLoading, data: allowances } = useMultipleContractSingleData<[BigNumber]>(
tokenAddresses,
erc20Interface,
'allowance',
allowanceParams,
MULTICALL_OPTIONS,
allowancesSwrConfig
)

// Set balances loading state
useEffect(() => {
if (!setLoadingState) return

setBalances((state) => ({ ...state, isLoading: isBalancesLoading }))
}, [setBalances, isBalancesLoading, setLoadingState])

// Set allwoances loading state
useEffect(() => {
if (!setLoadingState) return

setAllowances((state) => ({ ...state, isLoading: isAllowancesLoading }))
}, [setAllowances, isAllowancesLoading, setLoadingState])

// Set balances to the store
useEffect(() => {
if (!balances || !balances.length) return

const balancesState = tokenAddresses.reduce<BalancesState['values']>((acc, address, index) => {
if (getIsNativeToken(chainId, address)) return acc

acc[address.toLowerCase()] = balances[index]?.balance
return acc
}, {})

setBalances((state) => {
return {
...state,
values: { ...state.values, ...balancesState },
...(setLoadingState ? { isLoading: false } : {}),
}
})
}, [balances, tokenAddresses, setBalances, chainId, setLoadingState])

// Set allowances to the store
useEffect(() => {
if (!allowances || !allowances.length) return

const allowancesState = tokenAddresses.reduce<AllowancesState['values']>((acc, address, index) => {
acc[address.toLowerCase()] = allowances[index]?.[0]
return acc
}, {})

setAllowances((state) => {
return {
...state,
values: { ...state.values, ...allowancesState },
...(setLoadingState ? { isLoading: false } : {}),
}
})
}, [allowances, tokenAddresses, setAllowances, setLoadingState])
}
1 change: 1 addition & 0 deletions libs/balances-and-allowances/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Updater
export { BalancesAndAllowancesUpdater } from './updaters/BalancesAndAllowancesUpdater'
export { PriorityTokensUpdater } from './updaters/PriorityTokensUpdater'

// Hooks
export { useTokensBalances } from './hooks/useTokensBalances'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,96 +1,48 @@
import { useSetAtom } from 'jotai'
import { useSetAtom } from 'jotai/index'
import { useEffect, useMemo } from 'react'

import { GP_VAULT_RELAYER, NATIVE_CURRENCY_BUY_TOKEN } from '@cowprotocol/common-const'
import { NATIVE_CURRENCY_BUY_TOKEN } from '@cowprotocol/common-const'
import type { SupportedChainId } from '@cowprotocol/cow-sdk'
import { useMultipleContractSingleData } from '@cowprotocol/multicall'
import { useAllTokens } from '@cowprotocol/tokens'
import type { BigNumber } from '@ethersproject/bignumber'

import ms from 'ms.macro'

import { erc20Interface } from '../const'
import { useNativeTokenBalance } from '../hooks/useNativeTokenBalance'
import { AllowancesState, allowancesState } from '../state/allowancesAtom'
import { balancesAtom, BalancesState } from '../state/balancesAtom'
import { usePersistBalancesAndAllowances } from '../hooks/usePersistBalancesAndAllowances'
import { balancesAtom } from '../state/balancesAtom'

const MULTICALL_OPTIONS = {}
// A small gap between balances and allowances refresh intervals is needed to avoid high load to the node at the same time
const BALANCES_SWR_CONFIG = { refreshInterval: ms`28s` }
const ALLOWANCES_SWR_CONFIG = { refreshInterval: ms`30s` }
const BALANCES_SWR_CONFIG = { refreshInterval: ms`31s` }
const ALLOWANCES_SWR_CONFIG = { refreshInterval: ms`33s` }

export interface BalancesAndAllowancesUpdaterProps {
account: string | undefined
chainId: SupportedChainId
}
export function BalancesAndAllowancesUpdater({ account, chainId }: BalancesAndAllowancesUpdaterProps) {
const allTokens = useAllTokens()

const setBalances = useSetAtom(balancesAtom)
const setAllowances = useSetAtom(allowancesState)
const { data: nativeTokenBalance } = useNativeTokenBalance()

const spender = GP_VAULT_RELAYER[chainId]
const allTokens = useAllTokens()
const { data: nativeTokenBalance } = useNativeTokenBalance()

const tokenAddresses = useMemo(() => allTokens.map((token) => token.address), [allTokens])
const balanceOfParams = useMemo(() => (account ? [account] : undefined), [account])
const allowanceParams = useMemo(() => (account && spender ? [account, spender] : undefined), [account, spender])

const { isLoading: isBalancesLoading, data: balances } = useMultipleContractSingleData<{ balance: BigNumber }>(
usePersistBalancesAndAllowances({
account,
chainId,
tokenAddresses,
erc20Interface,
'balanceOf',
balanceOfParams,
MULTICALL_OPTIONS,
BALANCES_SWR_CONFIG
)

const { isLoading: isAllowancesLoading, data: allowances } = useMultipleContractSingleData<[BigNumber]>(
tokenAddresses,
erc20Interface,
'allowance',
allowanceParams,
MULTICALL_OPTIONS,
ALLOWANCES_SWR_CONFIG
)

// Set balances loading state
useEffect(() => {
setBalances((state) => ({ ...state, isLoading: isBalancesLoading }))
}, [setBalances, isBalancesLoading])
setLoadingState: true,
balancesSwrConfig: BALANCES_SWR_CONFIG,
allowancesSwrConfig: ALLOWANCES_SWR_CONFIG,
})

// Set allwoances loading state
// Add native token balance to the store as well
useEffect(() => {
setAllowances((state) => ({ ...state, isLoading: isAllowancesLoading }))
}, [setAllowances, isAllowancesLoading])

// Set balances to the store
useEffect(() => {
if (!balances || !balances.length) return

const balancesState = tokenAddresses.reduce<BalancesState['values']>((acc, address, index) => {
acc[address.toLowerCase()] = balances[index]?.balance
return acc
}, {})

// Add native token balance to the store as well
const nativeToken = NATIVE_CURRENCY_BUY_TOKEN[chainId]
const nativeBalanceState = nativeTokenBalance ? { [nativeToken.address.toLowerCase()]: nativeTokenBalance } : {}

setBalances({ isLoading: false, values: { ...balancesState, ...nativeBalanceState } })
}, [balances, tokenAddresses, setBalances, nativeTokenBalance, chainId])

// Set allowances to the store
useEffect(() => {
if (!allowances || !allowances.length) return

const allowancesState = tokenAddresses.reduce<AllowancesState['values']>((acc, address, index) => {
acc[address.toLowerCase()] = allowances[index]?.[0]
return acc
}, {})

setAllowances({ isLoading: false, values: allowancesState })
}, [allowances, tokenAddresses, setAllowances])
setBalances((state) => ({ ...state, values: { ...state.values, ...nativeBalanceState } }))
}, [nativeTokenBalance, chainId, setBalances])

return null
}
25 changes: 25 additions & 0 deletions libs/balances-and-allowances/src/updaters/PriorityTokensUpdater.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { SupportedChainId } from '@cowprotocol/cow-sdk'

import ms from 'ms.macro'

import { usePersistBalancesAndAllowances } from '../hooks/usePersistBalancesAndAllowances'

// A small gap between balances and allowances refresh intervals is needed to avoid high load to the node at the same time
const BALANCES_SWR_CONFIG = { refreshInterval: ms`11s` }
const ALLOWANCES_SWR_CONFIG = { refreshInterval: ms`13s` }

export interface PriorityTokensUpdaterProps {
account: string | undefined
chainId: SupportedChainId
tokenAddresses: string[]
}

export function PriorityTokensUpdater(props: PriorityTokensUpdaterProps) {
usePersistBalancesAndAllowances({
...props,
balancesSwrConfig: BALANCES_SWR_CONFIG,
allowancesSwrConfig: ALLOWANCES_SWR_CONFIG,
})

return null
}

0 comments on commit a645cce

Please sign in to comment.