Skip to content

Commit

Permalink
fix(combinedBalances): Optimize balance diff calculations (#5082)
Browse files Browse the repository at this point in the history
* chore: create state for balance combined and optimize applyBalanceDiffs function

* fix: avoid negative user balance

* fix: remove refetch tendernly simulation on mount

* fix: build error

* feat: add simulation link on order review
  • Loading branch information
yvesfracari authored Nov 14, 2024
1 parent 20f543a commit 38aae71
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 58 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { useState } from 'react'

import { HookToDappMatch } from '@cowprotocol/hook-dapp-lib'
import { CowHookDetails, HookToDappMatch } from '@cowprotocol/hook-dapp-lib'

import { ChevronDown, ChevronUp } from 'react-feather'

import { clickOnHooks } from 'modules/analytics'
import { useSimulationData } from 'modules/tenderly/hooks/useSimulationData'

import * as styledEl from './styled'

export function HookItem({ item, index }: { item: HookToDappMatch; index: number }) {
export function HookItem({ details, item, index }: { details?: CowHookDetails; item: HookToDappMatch; index: number }) {
const [isOpen, setIsOpen] = useState(false)

const simulationData = useSimulationData(details?.uuid)

const handleLinkClick = () => {
clickOnHooks(item.dapp?.name || 'Unknown hook dapp')
}
Expand All @@ -37,6 +40,16 @@ export function HookItem({ item, index }: { item: HookToDappMatch; index: number
<styledEl.HookItemContent>
{item.dapp && (
<>
{simulationData && (
<p>
<b>Simulation:</b>
<styledEl.SimulationLink status={simulationData.status}>
<a href={simulationData.link} target="_blank" rel="noopener noreferrer">
{simulationData.status ? 'Simulation successful' : 'Simulation failed'}
</a>
</styledEl.SimulationLink>
</p>
)}
<p>
<b>Description:</b> {item.dapp.descriptionShort}
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,12 @@ export const ToggleIcon = styled.div<{ isOpen: boolean }>`
}
}
`

export const SimulationLink = styled.span<{ status: boolean }>`
color: var(${({ status }) => (status ? UI.COLOR_SUCCESS : UI.COLOR_DANGER)});
border-radius: 8px;
&:hover {
color: var(${({ status }) => (status ? UI.COLOR_SUCCESS_TEXT : UI.COLOR_DANGER_TEXT)});
}
`
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { ReactElement, useEffect, useMemo, useState } from 'react'

import { latest } from '@cowprotocol/app-data'
import { HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib'
import { CowHookDetails, HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib'
import { InfoTooltip } from '@cowprotocol/ui'

import { ChevronDown, ChevronUp } from 'react-feather'

import { AppDataInfo, decodeAppData } from 'modules/appData'
import { useCustomHookDapps } from 'modules/hooksStore'
import { useCustomHookDapps, useHooks, useHooksStateWithSimulatedGas } from 'modules/hooksStore'
import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation'

import { HookItem } from './HookItem'
Expand All @@ -28,6 +28,8 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai
const preCustomHookDapps = useCustomHookDapps(true)
const postCustomHookDapps = useCustomHookDapps(false)

const hooks = useHooksStateWithSimulatedGas()

const { mutate, isValidating, data } = useTenderlyBundleSimulation()

useEffect(() => {
Expand Down Expand Up @@ -74,8 +76,8 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai
</styledEl.Summary>
{isOpen && (
<styledEl.Details>
<HooksInfo data={preHooksToDapp} title="Pre Hooks" />
<HooksInfo data={postHooksToDapp} title="Post Hooks" />
<HooksInfo data={preHooksToDapp} hooks={hooks.preHooks} title="Pre Hooks" />
<HooksInfo data={postHooksToDapp} hooks={hooks.postHooks} title="Post Hooks" />
</styledEl.Details>
)}
</styledEl.Wrapper>,
Expand All @@ -85,9 +87,10 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai
interface HooksInfoProps {
data: HookToDappMatch[]
title: string
hooks: CowHookDetails[]
}

function HooksInfo({ data, title }: HooksInfoProps) {
function HooksInfo({ data, title, hooks }: HooksInfoProps) {
return (
<>
{data.length ? (
Expand All @@ -96,9 +99,11 @@ function HooksInfo({ data, title }: HooksInfoProps) {
{title} <CircleCount>{data.length}</CircleCount>
</h3>
<styledEl.HooksList>
{data.map((item, index) => (
<HookItem key={item.hook.callData + item.hook.target + item.hook.gasLimit} item={item} index={index} />
))}
{data.map((item, index) => {
const key = item.hook.callData + item.hook.target + item.hook.gasLimit
const details = hooks.find(({ hook }) => key === hook.callData + hook.target + hook.gasLimit)
return <HookItem key={key} item={item} index={index} details={details} />
})}
</styledEl.HooksList>
</styledEl.InfoWrapper>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { GasPriceStrategyUpdater } from 'legacy/state/gas/gas-price-strategy-upd

import { addListAnalytics, removeListAnalytics } from 'modules/analytics'
import { UploadToIpfsUpdater } from 'modules/appData/updater/UploadToIpfsUpdater'
import { BalancesCombinedUpdater } from 'modules/combinedBalances/updater/BalancesCombinedUpdater'
import { CowEventsUpdater, InjectedWidgetUpdater, useInjectedWidgetParams } from 'modules/injectedWidget'
import { FinalizeTxUpdater } from 'modules/onchainTransactions'
import { OrdersNotificationsUpdater } from 'modules/orders'
Expand Down Expand Up @@ -90,6 +91,7 @@ export function Updaters() {
<PoolsInfoUpdater />
<LpTokensWithBalancesUpdater />
<VampireAttackUpdater />
<BalancesCombinedUpdater />
</>
)
}
Original file line number Diff line number Diff line change
@@ -1,45 +1,7 @@
import { useMemo } from 'react'
import { useAtomValue } from 'jotai'

import { BalancesState, useTokensBalances } from '@cowprotocol/balances-and-allowances'
import { useWalletInfo } from '@cowprotocol/wallet'

import { BigNumber } from 'ethers'

import { usePreHookBalanceDiff } from 'modules/hooksStore/hooks/useBalancesDiff'
import { useIsHooksTradeType } from 'modules/trade'
import { balancesCombinedAtom } from '../state/balanceCombinedAtom'

export function useTokensBalancesCombined() {
const { account } = useWalletInfo()
const preHooksBalancesDiff = usePreHookBalanceDiff()
const tokenBalances = useTokensBalances()
const isHooksTradeType = useIsHooksTradeType()

return useMemo(() => {
if (!account || !isHooksTradeType) return tokenBalances
const accountBalancesDiff = preHooksBalancesDiff[account.toLowerCase()] || {}
return applyBalanceDiffs(tokenBalances, accountBalancesDiff)
}, [account, preHooksBalancesDiff, tokenBalances, isHooksTradeType])
}

function applyBalanceDiffs(currentBalances: BalancesState, balanceDiff: Record<string, string>): BalancesState {
// Get all unique addresses from both objects
const allAddresses = [...new Set([...Object.keys(currentBalances.values), ...Object.keys(balanceDiff)])]

const normalizedValues = allAddresses.reduce(
(acc, address) => {
const currentBalance = currentBalances.values[address] || BigNumber.from(0)
const diff = balanceDiff[address] ? BigNumber.from(balanceDiff[address]) : BigNumber.from(0)

return {
...acc,
[address]: currentBalance.add(diff),
}
},
{} as Record<string, BigNumber>,
)

return {
isLoading: currentBalances.isLoading,
values: normalizedValues,
}
return useAtomValue(balancesCombinedAtom)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { atomWithReset } from 'jotai/utils'

import { BalancesState } from '@cowprotocol/balances-and-allowances'

export const balancesCombinedAtom = atomWithReset<BalancesState>({ isLoading: false, values: {} })
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useEffect } from 'react'

import { BalancesState, useTokensBalances } from '@cowprotocol/balances-and-allowances'
import { useWalletInfo } from '@cowprotocol/wallet'

import { BigNumber } from 'ethers'

import { useHooks } from 'modules/hooksStore'
import { usePreHookBalanceDiff } from 'modules/hooksStore/hooks/useBalancesDiff'
import { useIsHooksTradeType } from 'modules/trade'
import { useSetAtom } from 'jotai'
import { balancesCombinedAtom } from '../state/balanceCombinedAtom'

export function BalancesCombinedUpdater() {
const { account } = useWalletInfo()
const setBalancesCombined = useSetAtom(balancesCombinedAtom)
const preHooksBalancesDiff = usePreHookBalanceDiff()
const { preHooks } = useHooks()
const tokenBalances = useTokensBalances()
const isHooksTradeType = useIsHooksTradeType()

useEffect(() => {
if (!account || !isHooksTradeType || !preHooks.length) {
setBalancesCombined(tokenBalances)
return
}
const accountBalancesDiff = preHooksBalancesDiff[account.toLowerCase()] || {}
setBalancesCombined(applyBalanceDiffs(tokenBalances, accountBalancesDiff))
}, [account, preHooksBalancesDiff, isHooksTradeType, tokenBalances])

return null
}

function applyBalanceDiffs(currentBalances: BalancesState, balanceDiff: Record<string, string>): BalancesState {
const normalizedValues = { ...currentBalances.values }

// Only process addresses that have balance differences
// This optimizes since the balances diff object is usually smaller than the balances object
Object.entries(balanceDiff).forEach(([address, diff]) => {
const currentBalance = normalizedValues[address]
if (currentBalance === undefined) return
const balanceWithDiff = currentBalance.add(BigNumber.from(diff))

// If the balance with diff is negative, set the balance to 0
// This avoid the UI crashing in case of some error
normalizedValues[address] = balanceWithDiff.isNegative()
? BigNumber.from(0)
: currentBalance.add(BigNumber.from(diff))
})

return {
isLoading: currentBalances.isLoading,
values: normalizedValues,
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { useMemo } from 'react'

import ICON_CHECK_ICON from '@cowprotocol/assets/cow-swap/check-singular.svg'
import ICON_GRID from '@cowprotocol/assets/cow-swap/grid.svg'
import TenderlyLogo from '@cowprotocol/assets/cow-swap/tenderly-logo.svg'
Expand All @@ -10,6 +8,7 @@ import { InfoTooltip } from '@cowprotocol/ui'
import { Edit2, Trash2, ExternalLink as ExternalLinkIcon, RefreshCw } from 'react-feather'
import SVG from 'react-inlinesvg'

import { useSimulationData } from 'modules/tenderly/hooks/useSimulationData'
import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation'

import * as styledEl from './styled'
Expand All @@ -31,12 +30,9 @@ interface HookItemProp {
const isBundleSimulationReady = true

export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHook, removeHook, index }: HookItemProp) {
const { isValidating, data, mutate } = useTenderlyBundleSimulation()
const { isValidating, mutate } = useTenderlyBundleSimulation()

const simulationData = useMemo(() => {
if (!data) return
return data[hookDetails.uuid]
}, [data, hookDetails.uuid])
const simulationData = useSimulationData(hookDetails.uuid)

const simulationStatus = simulationData?.status ? 'Simulation successful' : 'Simulation failed'
const simulationTooltip = simulationData?.status
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useMemo } from 'react'

import { useTenderlyBundleSimulation } from './useTenderlyBundleSimulation'

export function useSimulationData(hookUuid?: string) {
const { data } = useTenderlyBundleSimulation()

return useMemo(() => {
if (!data || !hookUuid) return
return data[hookUuid]
}, [data, hookUuid])
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export function useTenderlyBundleSimulation() {
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
revalidateOnMount: false,
},
)
}

0 comments on commit 38aae71

Please sign in to comment.