diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx
index 06d2800412..5e9a27c4f2 100644
--- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx
+++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx
@@ -1,4 +1,4 @@
-import { ReactElement, useMemo, useState } from 'react'
+import { ReactElement, useEffect, useMemo, useState } from 'react'
import { latest } from '@cowprotocol/app-data'
import { HookToDappMatch, matchHooksToDappsRegistry } from '@cowprotocol/hook-dapp-lib'
@@ -12,6 +12,7 @@ import { useCustomHookDapps } from 'modules/hooksStore'
import { HookItem } from './HookItem'
import * as styledEl from './styled'
import { CircleCount } from './styled'
+import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation'
interface OrderHooksDetailsProps {
appData: string | AppDataInfo
@@ -27,10 +28,18 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai
const preCustomHookDapps = useCustomHookDapps(true)
const postCustomHookDapps = useCustomHookDapps(false)
+ const { mutate, isValidating, data } = useTenderlyBundleSimulation()
+
+ useEffect(() => {
+ mutate()
+ }, [])
+
if (!appDataDoc) return null
const metadata = appDataDoc.metadata as latest.Metadata
+ const hasSomeFailedSimulation = Object.values(data || {}).some((hook) => !hook.status)
+
const preHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.pre || [], preCustomHookDapps)
const postHooksToDapp = matchHooksToDappsRegistry(metadata.hooks?.post || [], postCustomHookDapps)
@@ -42,6 +51,8 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai
Hooks
+ {hasSomeFailedSimulation && Simulation failed}
+ {isValidating && }
setOpen(!isOpen)}>
{preHooksToDapp.length > 0 && (
diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx
index 55f511bc35..dfc2f9716c 100644
--- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx
+++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx
@@ -33,6 +33,17 @@ export const Label = styled.span`
gap: 4px;
`
+export const ErrorLabel = styled.span`
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ color: var(${UI.COLOR_DANGER_TEXT});
+ background-color: var(${UI.COLOR_DANGER_BG});
+ border-radius: 8px;
+ margin-left: 4px;
+ padding: 2px 6px;
+`
+
export const Content = styled.div`
display: flex;
width: max-content;
@@ -164,3 +175,21 @@ export const CircleCount = styled.span`
font-weight: var(${UI.FONT_WEIGHT_BOLD});
margin: 0;
`
+
+export const Spinner = styled.div`
+ border: 5px solid transparent;
+ border-top-color: ${`var(${UI.COLOR_PRIMARY_LIGHTER})`};
+ border-radius: 50%;
+ width: 12px;
+ height: 12px;
+ animation: spin 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite;
+
+ @keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+`
diff --git a/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts b/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts
index b58c1e5130..29bc7a012d 100644
--- a/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts
+++ b/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts
@@ -6,7 +6,7 @@ import { useIsSmartContractWallet } from '@cowprotocol/wallet'
import { Nullish } from 'types'
-import { useHooks } from 'modules/hooksStore'
+import { useHooksStateWithSimulatedGas } from 'modules/hooksStore'
import { useAccountAgnosticPermitHookData } from 'modules/permit'
import { useDerivedTradeState, useHasTradeEnoughAllowance, useIsHooksTradeType, useIsSellNative } from 'modules/trade'
@@ -33,7 +33,7 @@ function useAgnosticPermitDataIfUserHasNoAllowance(): Nullish {
export function AppDataHooksUpdater(): null {
const tradeState = useDerivedTradeState()
const isHooksTradeType = useIsHooksTradeType()
- const hooksStoreState = useHooks()
+ const hooksStoreState = useHooksStateWithSimulatedGas()
const preHooks = isHooksTradeType ? hooksStoreState.preHooks : null
const postHooks = isHooksTradeType ? hooksStoreState.postHooks : null
const updateAppDataHooks = useUpdateAppDataHooks()
diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useCurrencyAmountBalanceCombined.ts b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useCurrencyAmountBalanceCombined.ts
new file mode 100644
index 0000000000..5977042e25
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useCurrencyAmountBalanceCombined.ts
@@ -0,0 +1,22 @@
+import { useMemo } from 'react'
+
+import { TokenWithLogo } from '@cowprotocol/common-const'
+import { CurrencyAmount } from '@uniswap/sdk-core'
+
+import { useTokensBalancesCombined } from './useTokensBalancesCombined'
+
+export function useCurrencyAmountBalanceCombined(
+ token: TokenWithLogo | undefined | null,
+): CurrencyAmount | undefined {
+ const { values: balances } = useTokensBalancesCombined()
+
+ return useMemo(() => {
+ if (!token) return undefined
+
+ const balance = balances[token.address.toLowerCase()]
+
+ if (!balance) return undefined
+
+ return CurrencyAmount.fromRawAmount(token, balance.toHexString())
+ }, [token, balances])
+}
diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts
new file mode 100644
index 0000000000..0ad8abb306
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts
@@ -0,0 +1,45 @@
+import { useMemo } from 'react'
+
+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'
+
+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): 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,
+ )
+
+ return {
+ isLoading: currentBalances.isLoading,
+ values: normalizedValues,
+ }
+}
diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/index.ts b/apps/cowswap-frontend/src/modules/combinedBalances/index.ts
new file mode 100644
index 0000000000..3360595102
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/combinedBalances/index.ts
@@ -0,0 +1,2 @@
+export * from './hooks/useCurrencyAmountBalanceCombined'
+export * from './hooks/useTokensBalancesCombined'
diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx
index f678cefe26..014ae5bab8 100644
--- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx
+++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx
@@ -12,6 +12,7 @@ import { useAddHook } from '../../hooks/useAddHook'
import { useEditHook } from '../../hooks/useEditHook'
import { useHookById } from '../../hooks/useHookById'
import { useOrderParams } from '../../hooks/useOrderParams'
+import { useHookBalancesDiff } from '../../hooks/useBalancesDiff'
import { HookDapp, HookDappContext as HookDappContextType } from '../../types/hooks'
import { isHookDappIframe } from '../../utils'
import { IframeDappContainer } from '../IframeDappContainer'
@@ -35,6 +36,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho
const tradeState = useTradeState()
const tradeNavigate = useTradeNavigate()
const isDarkMode = useIsDarkMode()
+ const balancesDiff = useHookBalancesDiff(isPreHook, hookToEditDetails?.uuid)
const { inputCurrencyId = null, outputCurrencyId = null } = tradeState.state || {}
const signer = useMemo(() => provider?.getSigner(), [provider])
@@ -49,6 +51,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho
isSmartContract,
isPreHook,
isDarkMode,
+ balancesDiff,
editHook(...args) {
editHook(...args)
onDismiss()
diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts
new file mode 100644
index 0000000000..fe9557ecf4
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts
@@ -0,0 +1,120 @@
+import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation'
+import { useOrderParams } from './useOrderParams'
+import { useHooks } from './useHooks'
+import { useMemo } from 'react'
+import { useWalletInfo } from '@cowprotocol/wallet'
+import { BalancesDiff } from 'modules/tenderly/types'
+import { BigNumber } from 'ethers'
+
+const EMPTY_BALANCE_DIFF: BalancesDiff = {}
+
+export function usePreHookBalanceDiff(): BalancesDiff {
+ const { data } = useTenderlyBundleSimulation()
+
+ const { preHooks } = useHooks()
+
+ return useMemo(() => {
+ if (!data || !preHooks.length) return EMPTY_BALANCE_DIFF
+
+ const lastPreHook = preHooks[preHooks.length - 1]
+ return data[lastPreHook?.uuid]?.cumulativeBalancesDiff || EMPTY_BALANCE_DIFF
+ }, [data, preHooks])
+}
+
+// Returns all the ERC20 Balance Diff of the current hook to be passed to the iframe context
+export function useHookBalancesDiff(isPreHook: boolean, hookToEditUid?: string): BalancesDiff {
+ const { account } = useWalletInfo()
+ const { data } = useTenderlyBundleSimulation()
+ const orderParams = useOrderParams()
+ const { preHooks, postHooks } = useHooks()
+ const preHookBalanceDiff = usePreHookBalanceDiff()
+
+ // balance diff expected from the order without the simulation
+ // this is used when the order isn't simulated like in the case of only preHooks
+ const orderExpectedBalanceDiff = useMemo(() => {
+ if (!account) return EMPTY_BALANCE_DIFF
+ const balanceDiff: Record = {}
+
+ if (orderParams?.buyAmount && orderParams.buyTokenAddress && account)
+ balanceDiff[orderParams.buyTokenAddress.toLowerCase()] = orderParams.buyAmount
+
+ if (orderParams?.sellAmount && orderParams.sellTokenAddress && account)
+ balanceDiff[orderParams.sellTokenAddress.toLowerCase()] = `-${orderParams.sellAmount}`
+
+ return { account: balanceDiff }
+ }, [orderParams, account])
+
+ const firstPostHookBalanceDiff = useMemo(() => {
+ return mergeBalanceDiffs(preHookBalanceDiff, orderExpectedBalanceDiff)
+ }, [preHookBalanceDiff, orderExpectedBalanceDiff])
+
+ const postHookBalanceDiff = useMemo(() => {
+ // is adding the first post hook or simulation not available
+ if (!data || !postHooks) return firstPostHookBalanceDiff
+
+ const lastPostHook = postHooks[postHooks.length - 1]
+ return data[lastPostHook?.uuid]?.cumulativeBalancesDiff || firstPostHookBalanceDiff
+ }, [data, postHooks, orderExpectedBalanceDiff, preHookBalanceDiff])
+
+ const hookToEditBalanceDiff = useMemo(() => {
+ if (!data || !hookToEditUid) return EMPTY_BALANCE_DIFF
+
+ const otherHooks = isPreHook ? preHooks : postHooks
+
+ const hookToEditIndex = otherHooks.findIndex((hook) => hook.uuid === hookToEditUid)
+
+ // is editing first preHook -> return empty state
+ if (!hookToEditIndex && isPreHook) return EMPTY_BALANCE_DIFF
+
+ // is editing first postHook -> return
+ if (!hookToEditIndex && !isPreHook) return firstPostHookBalanceDiff
+
+ // is editing a non first hook, return the latest available hook state
+ const previousHookIndex = hookToEditIndex - 1
+
+ return data[otherHooks[previousHookIndex]?.uuid]?.cumulativeBalancesDiff || EMPTY_BALANCE_DIFF
+ }, [data, hookToEditUid, isPreHook, preHooks, postHooks, firstPostHookBalanceDiff])
+
+ return useMemo(() => {
+ if (hookToEditUid) return hookToEditBalanceDiff
+ if (isPreHook) return preHookBalanceDiff
+ return postHookBalanceDiff
+ }, [data, orderParams, preHooks, postHooks])
+}
+
+function mergeBalanceDiffs(first: BalancesDiff, second: BalancesDiff): BalancesDiff {
+ const result: BalancesDiff = {}
+
+ // Helper function to add BigNumber strings
+
+ // Process all addresses from first input
+ for (const address of Object.keys(first)) {
+ result[address] = { ...first[address] }
+ }
+
+ // Process all addresses from second input
+ for (const address of Object.keys(second)) {
+ if (!result[address]) {
+ // If address doesn't exist in result, just copy the entire record
+ result[address] = { ...second[address] }
+ } else {
+ // If address exists, we need to merge token balances
+ for (const token of Object.keys(second[address])) {
+ if (!result[address][token]) {
+ // If token doesn't exist for this address, just copy the balance
+ result[address][token] = second[address][token]
+ } else {
+ // If token exists, sum up the balances
+ try {
+ result[address][token] = BigNumber.from(result[address][token]).add(second[address][token]).toString()
+ } catch (error) {
+ console.error(`Error adding balances for address ${address} and token ${token}:`, error)
+ throw error
+ }
+ }
+ }
+ }
+ }
+
+ return result
+}
diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts
new file mode 100644
index 0000000000..0bb2ba3aa2
--- /dev/null
+++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts
@@ -0,0 +1,30 @@
+import { useCallback, useMemo } from 'react'
+
+import { CowHookDetails } from '@cowprotocol/hook-dapp-lib'
+
+import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation'
+
+import { useHooks } from './useHooks'
+
+import { HooksStoreState } from '../state/hookDetailsAtom'
+
+export function useHooksStateWithSimulatedGas(): HooksStoreState {
+ const hooksRaw = useHooks()
+ const { data: tenderlyData } = useTenderlyBundleSimulation()
+
+ const combineHookWithSimulatedGas = useCallback(
+ (hook: CowHookDetails): CowHookDetails => {
+ const hookTenderlyData = tenderlyData?.[hook.uuid]
+ if (!hookTenderlyData?.gasUsed || hookTenderlyData.gasUsed === '0' || !hookTenderlyData.status) return hook
+ const hookData = { ...hook.hook, gasLimit: hookTenderlyData.gasUsed }
+ return { ...hook, hook: hookData }
+ },
+ [tenderlyData],
+ )
+
+ return useMemo(() => {
+ const preHooksCombined = hooksRaw.preHooks.map(combineHookWithSimulatedGas)
+ const postHooksCombined = hooksRaw.postHooks.map(combineHookWithSimulatedGas)
+ return { preHooks: preHooksCombined, postHooks: postHooksCombined }
+ }, [hooksRaw, combineHookWithSimulatedGas])
+}
diff --git a/apps/cowswap-frontend/src/modules/hooksStore/index.ts b/apps/cowswap-frontend/src/modules/hooksStore/index.ts
index 5b922a2406..d54591c6b5 100644
--- a/apps/cowswap-frontend/src/modules/hooksStore/index.ts
+++ b/apps/cowswap-frontend/src/modules/hooksStore/index.ts
@@ -2,3 +2,4 @@ export { HooksStoreWidget } from './containers/HooksStoreWidget'
export { useHooks } from './hooks/useHooks'
export { usePostHooksRecipientOverride } from './hooks/usePostHooksRecipientOverride'
export { useCustomHookDapps } from './hooks/useCustomHookDapps'
+export { useHooksStateWithSimulatedGas } from './hooks/useHooksStateWithSimulatedGas'
diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx
index d30afa2a05..362c2f0f33 100644
--- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx
+++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx
@@ -7,7 +7,7 @@ import ICON_X from '@cowprotocol/assets/cow-swap/x.svg'
import { CowHookDetails } from '@cowprotocol/hook-dapp-lib'
import { InfoTooltip } from '@cowprotocol/ui'
-import { Edit2, Trash2, ExternalLink as ExternalLinkIcon } from 'react-feather'
+import { Edit2, Trash2, ExternalLink as ExternalLinkIcon, RefreshCw } from 'react-feather'
import SVG from 'react-inlinesvg'
import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation'
@@ -31,7 +31,7 @@ interface HookItemProp {
const isBundleSimulationReady = true
export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHook, removeHook, index }: HookItemProp) {
- const { isValidating, data } = useTenderlyBundleSimulation()
+ const { isValidating, data, mutate } = useTenderlyBundleSimulation()
const simulationData = useMemo(() => {
if (!data) return
@@ -56,6 +56,9 @@ export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHoo
{isValidating && }
+ mutate()} disabled={isValidating}>
+
+
editHook(hookDetails.uuid)}>
diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx
index d5c0f61f5c..af1def7605 100644
--- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx
+++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx
@@ -1,6 +1,6 @@
import { ReactNode, useCallback, useMemo, useState } from 'react'
-import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances'
+// import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances'
import { NATIVE_CURRENCIES, TokenWithLogo } from '@cowprotocol/common-const'
import { useIsTradeUnsupported } from '@cowprotocol/tokens'
import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet'
@@ -12,6 +12,7 @@ import { ApplicationModal } from 'legacy/state/application/reducer'
import { Field } from 'legacy/state/types'
import { useRecipientToggleManager, useUserTransactionTTL } from 'legacy/state/user/hooks'
+import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances'
import { useInjectedWidgetParams } from 'modules/injectedWidget'
import { EthFlowModal, EthFlowProps } from 'modules/swap/containers/EthFlow'
import { SwapModals, SwapModalsProps } from 'modules/swap/containers/SwapModals'
@@ -94,8 +95,8 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) {
return TokenWithLogo.fromToken(currencies.OUTPUT)
}, [chainId, currencies.OUTPUT])
- const inputCurrencyBalance = useCurrencyAmountBalance(inputToken) || null
- const outputCurrencyBalance = useCurrencyAmountBalance(outputToken) || null
+ const inputCurrencyBalance = useCurrencyAmountBalanceCombined(inputToken) || null
+ const outputCurrencyBalance = useCurrencyAmountBalanceCombined(outputToken) || null
const isSellTrade = independentField === Field.INPUT
diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts
index 933a0b6c14..7b2b65ae8b 100644
--- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts
+++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts
@@ -1,6 +1,6 @@
import { useMemo } from 'react'
-import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances'
+// import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances'
import { currencyAmountToTokenAmount, getWrappedToken } from '@cowprotocol/common-utils'
import { useIsTradeUnsupported } from '@cowprotocol/tokens'
import {
@@ -16,6 +16,7 @@ import { useToggleWalletModal } from 'legacy/state/application/hooks'
import { useGetQuoteAndStatus, useIsBestQuoteLoading } from 'legacy/state/price/hooks'
import { Field } from 'legacy/state/types'
+import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances'
import { useInjectedWidgetParams } from 'modules/injectedWidget'
import { useTokenSupportsPermit } from 'modules/permit'
import { getSwapButtonState } from 'modules/swap/helpers/getSwapButtonState'
@@ -157,7 +158,9 @@ export function useSwapButtonContext(input: SwapButtonInput, actions: TradeWidge
function useHasEnoughWrappedBalanceForSwap(inputAmount?: CurrencyAmount): boolean {
const { currencies } = useDerivedSwapInfo()
- const wrappedBalance = useCurrencyAmountBalance(currencies.INPUT ? getWrappedToken(currencies.INPUT) : undefined)
+ const wrappedBalance = useCurrencyAmountBalanceCombined(
+ currencies.INPUT ? getWrappedToken(currencies.INPUT) : undefined,
+ )
// is a native currency trade but wrapped token has enough balance
return !!(wrappedBalance && inputAmount && !wrappedBalance.lessThan(inputAmount))
diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx
index 4953671631..c73ed33327 100644
--- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx
+++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx
@@ -1,6 +1,5 @@
import { useCallback, useMemo } from 'react'
-import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances'
import { formatSymbol, getIsNativeToken, isAddress, tryParseCurrencyAmount } from '@cowprotocol/common-utils'
import { useENS } from '@cowprotocol/ens'
import { useAreThereTokensWithSameSymbol, useTokenBySymbolOrAddress } from '@cowprotocol/tokens'
@@ -19,6 +18,7 @@ import { isWrappingTrade } from 'legacy/state/swap/utils'
import { Field } from 'legacy/state/types'
import { changeSwapAmountAnalytics, switchTokensAnalytics } from 'modules/analytics'
+import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances'
import type { TradeWidgetActions } from 'modules/trade'
import { useNavigateOnCurrencySelection } from 'modules/trade/hooks/useNavigateOnCurrencySelection'
import { useTradeNavigate } from 'modules/trade/hooks/useTradeNavigate'
@@ -125,8 +125,8 @@ export function useDerivedSwapInfo(): DerivedSwapInfo {
const recipientLookup = useENS(recipient ?? undefined)
const to: string | null = (recipient ? recipientLookup.address : account) ?? null
- const inputCurrencyBalance = useCurrencyAmountBalance(inputCurrency)
- const outputCurrencyBalance = useCurrencyAmountBalance(outputCurrency)
+ const inputCurrencyBalance = useCurrencyAmountBalanceCombined(inputCurrency)
+ const outputCurrencyBalance = useCurrencyAmountBalanceCombined(outputCurrency)
const isExactIn: boolean = independentField === Field.INPUT
const parsedAmount = useMemo(
diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts
index 194a086a11..3f074a8009 100644
--- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts
+++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts
@@ -10,10 +10,7 @@ export function useGetTopTokenHolders() {
return useCallback(
async (params: GetTopTokenHoldersParams) => {
const key = `${params.chainId}-${params.tokenAddress}`
- if (cachedData[key]?.value) {
- return cachedData[key].value
- }
- return fetchTopTokenHolders(params)
+ return cachedData[key]?.value || fetchTopTokenHolders(params)
},
[cachedData, fetchTopTokenHolders],
)
diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts
index 7ab1478887..9749c0e8f9 100644
--- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts
+++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts
@@ -1,5 +1,6 @@
import { useCallback } from 'react'
+import { CowHookDetails } from '@cowprotocol/hook-dapp-lib'
import { useWalletInfo } from '@cowprotocol/wallet'
import useSWR from 'swr'
@@ -7,93 +8,106 @@ import useSWR from 'swr'
import { useHooks } from 'modules/hooksStore'
import { useOrderParams } from 'modules/hooksStore/hooks/useOrderParams'
-import { useTokenContract } from 'common/hooks/useContract'
-
import { useGetTopTokenHolders } from './useGetTopTokenHolders'
-import { completeBundleSimulation, preHooksBundleSimulation } from '../utils/bundleSimulation'
+import { completeBundleSimulation, hooksBundleSimulation } from '../utils/bundleSimulation'
import { generateNewSimulationData, generateSimulationDataToError } from '../utils/generateSimulationData'
import { getTokenTransferInfo } from '../utils/getTokenTransferInfo'
+type BundleSimulationSwrParams = {
+ preHooks: CowHookDetails[]
+ postHooks: CowHookDetails[]
+}
+
export function useTenderlyBundleSimulation() {
const { account, chainId } = useWalletInfo()
const { preHooks, postHooks } = useHooks()
const orderParams = useOrderParams()
- const tokenSell = useTokenContract(orderParams?.sellTokenAddress)
- const tokenBuy = useTokenContract(orderParams?.buyTokenAddress)
- const buyAmount = orderParams?.buyAmount
- const sellAmount = orderParams?.sellAmount
- const orderReceiver = orderParams?.receiver || account
const getTopTokenHolder = useGetTopTokenHolders()
- const simulateBundle = useCallback(async () => {
- if (postHooks.length === 0 && preHooks.length === 0) return
+ const simulateBundle = useCallback(
+ async ({ preHooks, postHooks }: BundleSimulationSwrParams) => {
+ if (postHooks.length === 0 && preHooks.length === 0) return
+
+ if (!postHooks.length)
+ return hooksBundleSimulation({
+ chainId,
+ preHooks,
+ postHooks: [],
+ })
+
+ if (
+ !account ||
+ !orderParams?.buyTokenAddress ||
+ !orderParams?.sellTokenAddress ||
+ !orderParams?.buyAmount ||
+ !orderParams?.sellAmount ||
+ !orderParams?.receiver
+ ) {
+ return hooksBundleSimulation({
+ chainId,
+ preHooks,
+ postHooks,
+ })
+ }
- if (!postHooks.length)
- return preHooksBundleSimulation({
+ const buyTokenTopHolders = await getTopTokenHolder({
+ tokenAddress: orderParams.buyTokenAddress,
chainId,
- preHooks,
})
- if (!account || !tokenBuy || !tokenSell || !buyAmount || !sellAmount || !orderReceiver) {
- return
- }
-
- const buyTokenTopHolders = await getTopTokenHolder({
- tokenAddress: tokenBuy.address,
- chainId,
- })
-
- if (!buyTokenTopHolders) return
-
- const tokenBuyTransferInfo = getTokenTransferInfo({
- tokenHolders: buyTokenTopHolders,
- amountToTransfer: buyAmount,
- })
-
- const paramsComplete = {
- postHooks,
- preHooks,
- tokenBuy,
- tokenBuyTransferInfo,
- sellAmount,
- orderReceiver,
- tokenSell,
- account,
- chainId,
- }
-
- return completeBundleSimulation(paramsComplete)
- }, [
- account,
- chainId,
- getTopTokenHolder,
- tokenBuy,
- postHooks,
- preHooks,
- buyAmount,
- sellAmount,
- orderReceiver,
- tokenSell,
- ])
-
- const getNewSimulationData = useCallback(async () => {
- try {
- const simulationData = await simulateBundle()
+ if (!buyTokenTopHolders) return
+
+ const tokenBuyTransferInfo = getTokenTransferInfo({
+ tokenHolders: buyTokenTopHolders,
+ amountToTransfer: orderParams.buyAmount,
+ })
+
+ const paramsComplete = {
+ postHooks,
+ preHooks,
+ tokenBuy: orderParams.buyTokenAddress,
+ tokenBuyTransferInfo,
+ sellAmount: orderParams.sellAmount,
+ buyAmount: orderParams.buyAmount,
+ orderReceiver: orderParams.receiver,
+ tokenSell: orderParams.sellTokenAddress,
+ account,
+ chainId,
+ }
+
+ return completeBundleSimulation(paramsComplete)
+ },
+ [account, chainId, getTopTokenHolder, orderParams],
+ )
+
+ const getNewSimulationData = useCallback(
+ async ([_, params]: [string, BundleSimulationSwrParams]) => {
+ const simulationData = await simulateBundle(params)
if (!simulationData) {
return {}
}
- return generateNewSimulationData(simulationData, { preHooks, postHooks })
- } catch {
- return generateSimulationDataToError({ preHooks, postHooks })
- }
- }, [preHooks, postHooks, simulateBundle])
+ try {
+ return generateNewSimulationData(simulationData, { preHooks: params.preHooks, postHooks: params.postHooks })
+ } catch (e) {
+ console.log(`error`, { e, simulationData })
+ return generateSimulationDataToError({ preHooks: params.preHooks, postHooks: params.postHooks })
+ }
+ },
+ [simulateBundle],
+ )
- const { data, isValidating: isBundleSimulationLoading } = useSWR(
- ['tenderly-bundle-simulation', postHooks, preHooks, orderParams?.sellTokenAddress, orderParams?.buyTokenAddress],
+ return useSWR(
+ [
+ 'tenderly-bundle-simulation',
+ {
+ preHooks,
+ postHooks,
+ },
+ ],
getNewSimulationData,
{
revalidateOnFocus: false,
@@ -101,6 +115,4 @@ export function useTenderlyBundleSimulation() {
refreshWhenOffline: false,
},
)
-
- return { data, isValidating: isBundleSimulationLoading }
}
diff --git a/apps/cowswap-frontend/src/modules/tenderly/types.ts b/apps/cowswap-frontend/src/modules/tenderly/types.ts
index 9ef8eb6b56..7c62f33d6b 100644
--- a/apps/cowswap-frontend/src/modules/tenderly/types.ts
+++ b/apps/cowswap-frontend/src/modules/tenderly/types.ts
@@ -9,10 +9,16 @@ export interface SimulationInput {
gas_price?: string
}
+// { [address: string]: { [token: string]: balanceDiff: string } }
+// example: { '0x123': { '0x456': '100', '0xabc': '-100' } }
+export type BalancesDiff = Record>
+
export interface SimulationData {
link: string
status: boolean
id: string
+ cumulativeBalancesDiff: BalancesDiff
+ gasUsed: string
}
export interface GetTopTokenHoldersParams {
diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts
index 63d7c8dd0a..2606308869 100644
--- a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts
+++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts
@@ -1,17 +1,20 @@
-import { Erc20 } from '@cowprotocol/abis'
+import { Erc20Abi } from '@cowprotocol/abis'
import { BFF_BASE_URL } from '@cowprotocol/common-const'
import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk'
import { CowHookDetails } from '@cowprotocol/hook-dapp-lib'
+import { Interface } from 'ethers/lib/utils'
+
import { CowHook } from 'modules/hooksStore/types/hooks'
import { SimulationData, SimulationInput } from '../types'
+const erc20Interface = new Interface(Erc20Abi)
export interface GetTransferTenderlySimulationInput {
currencyAmount: string
from: string
receiver: string
- token: Erc20
+ token: string
}
export type TokenBuyTransferInfo = {
@@ -21,8 +24,8 @@ export type TokenBuyTransferInfo = {
export interface PostBundleSimulationParams {
account: string
chainId: SupportedChainId
- tokenSell: Erc20
- tokenBuy: Erc20
+ tokenSell: string
+ tokenBuy: string
preHooks: CowHookDetails[]
postHooks: CowHookDetails[]
sellAmount: string
@@ -35,10 +38,11 @@ export const completeBundleSimulation = async (params: PostBundleSimulationParam
return simulateBundle(input, params.chainId)
}
-export const preHooksBundleSimulation = async (
- params: Pick,
+export const hooksBundleSimulation = async (
+ params: Pick,
): Promise => {
- const input = params.preHooks.map((hook) =>
+ const hooks = [...params.preHooks, ...params.postHooks]
+ const input = hooks.map((hook) =>
getCoWHookTenderlySimulationInput(COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[params.chainId], hook.hook),
)
return simulateBundle(input, params.chainId)
@@ -70,11 +74,11 @@ export function getTransferTenderlySimulationInput({
receiver,
token,
}: GetTransferTenderlySimulationInput): SimulationInput {
- const callData = token.interface.encodeFunctionData('transfer', [receiver, currencyAmount])
+ const callData = erc20Interface.encodeFunctionData('transfer', [receiver, currencyAmount])
return {
input: callData,
- to: token.address,
+ to: token,
from,
}
}
diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts
index 5d3416b736..54b9f11ab1 100644
--- a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts
+++ b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts
@@ -1,6 +1,6 @@
import { PostBundleSimulationParams } from './bundleSimulation'
-import { SimulationData } from '../types'
+import { BalancesDiff, SimulationData } from '../types'
export function generateSimulationDataToError(
postParams: Pick,
@@ -18,6 +18,25 @@ export function generateSimulationDataToError(
)
}
+function convertBalanceDiffToLowerCaseKeys(data: BalancesDiff): BalancesDiff {
+ return Object.entries(data).reduce((acc, [tokenHolder, tokenHolderDiffs]) => {
+ const lowerOuterKey = tokenHolder.toLowerCase()
+
+ const processedInnerObj = Object.entries(tokenHolderDiffs || {}).reduce((innerAcc, [tokenAddress, balanceDiff]) => {
+ const lowerInnerKey = tokenAddress.toLowerCase()
+ return {
+ ...innerAcc,
+ [lowerInnerKey]: balanceDiff,
+ }
+ }, {})
+
+ return {
+ ...acc,
+ [lowerOuterKey]: processedInnerObj,
+ }
+ }, {})
+}
+
export function generateNewSimulationData(
simulationData: SimulationData[],
postParams: Pick,
@@ -25,9 +44,14 @@ export function generateNewSimulationData(
const preHooksKeys = postParams.preHooks.map((hookDetails) => hookDetails.uuid)
const postHooksKeys = postParams.postHooks.map((hookDetails) => hookDetails.uuid)
- const preHooksData = simulationData.slice(0, preHooksKeys.length)
+ const simulationDataWithLowerCaseBalanceDiffKeys = simulationData.map((data) => ({
+ ...data,
+ cumulativeBalancesDiff: convertBalanceDiffToLowerCaseKeys(data.cumulativeBalancesDiff),
+ }))
+
+ const preHooksData = simulationDataWithLowerCaseBalanceDiffKeys.slice(0, preHooksKeys.length)
- const postHooksData = simulationData.slice(-postHooksKeys.length)
+ const postHooksData = simulationDataWithLowerCaseBalanceDiffKeys.slice(-postHooksKeys.length)
return {
...preHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: preHooksData[index] }), {}),
diff --git a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts
index 6f3bd35770..dde426c17a 100644
--- a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts
+++ b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts
@@ -1,12 +1,9 @@
-import {
- AllowancesState,
- BalancesState,
- useTokensAllowances,
- useTokensBalances,
-} from '@cowprotocol/balances-and-allowances'
+import { AllowancesState, BalancesState, useTokensAllowances } from '@cowprotocol/balances-and-allowances'
import { isEnoughAmount, getAddress, getIsNativeToken, getWrappedToken } from '@cowprotocol/common-utils'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
+import { useTokensBalancesCombined } from 'modules/combinedBalances'
+
export interface UseEnoughBalanceParams {
/**
* Address of the account to check balance (and optionally the allowance)
@@ -39,7 +36,7 @@ const DEFAULT_BALANCE_AND_ALLOWANCE = { enoughBalance: undefined, enoughAllowanc
export function useEnoughBalanceAndAllowance(params: UseEnoughBalanceParams): UseEnoughBalanceAndAllowanceResult {
const { checkAllowanceAddress } = params
- const { values: balances } = useTokensBalances()
+ const { values: balances } = useTokensBalancesCombined()
const { values: allowances } = useTokensAllowances()
return hasEnoughBalanceAndAllowance({
@@ -86,7 +83,7 @@ export function hasEnoughBalanceAndAllowance(params: EnoughBalanceParams): UseEn
function _enoughBalance(
tokenAddress: string | undefined,
amount: CurrencyAmount,
- balances: BalancesState['values']
+ balances: BalancesState['values'],
): boolean | undefined {
const balance = tokenAddress ? balances[tokenAddress] : undefined
@@ -97,7 +94,7 @@ function _enoughAllowance(
tokenAddress: string | undefined,
amount: CurrencyAmount,
allowances: AllowancesState['values'] | undefined,
- isNativeCurrency: boolean
+ isNativeCurrency: boolean,
): boolean | undefined {
if (!tokenAddress || !allowances) {
return undefined
diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx
index 87303e3074..3ea98e232c 100644
--- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx
+++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx
@@ -1,6 +1,5 @@
import { useCallback, useState } from 'react'
-import { useTokensBalances } from '@cowprotocol/balances-and-allowances'
import { TokenWithLogo } from '@cowprotocol/common-const'
import { isInjectedWidget } from '@cowprotocol/common-utils'
import {
@@ -21,6 +20,7 @@ import styled from 'styled-components/macro'
import { Field } from 'legacy/state/types'
import { addListAnalytics } from 'modules/analytics'
+import { useTokensBalancesCombined } from 'modules/combinedBalances'
import { usePermitCompatibleTokens } from 'modules/permit'
import { useLpTokensWithBalances } from 'modules/yield/shared'
@@ -79,7 +79,7 @@ export function SelectTokenWidget({ displayLpTokenLists }: SelectTokenWidgetProp
const favoriteTokens = useFavoriteTokens()
const userAddedTokens = useUserAddedTokens()
const allTokenLists = useAllListsList()
- const balancesState = useTokensBalances()
+ const balancesState = useTokensBalancesCombined()
const unsupportedTokens = useUnsupportedTokens()
const permitCompatibleTokens = usePermitCompatibleTokens()
const onTokenListAddingError = useOnTokenListAddingError()
diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts
index bf389ec6b8..5475d25914 100644
--- a/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts
+++ b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts
@@ -1,13 +1,13 @@
import { Atom, useAtomValue } from 'jotai'
import { useMemo } from 'react'
-import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances'
import { tryParseFractionalAmount } from '@cowprotocol/common-utils'
import { useTokenBySymbolOrAddress } from '@cowprotocol/tokens'
import { Currency, CurrencyAmount } from '@uniswap/sdk-core'
import { Nullish } from 'types'
+import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances'
import { ExtendedTradeRawState } from 'modules/trade/types/TradeRawState'
import { useTradeUsdAmounts } from 'modules/usdAmount'
@@ -24,14 +24,14 @@ export function useBuildTradeDerivedState(stateAtom: Atom
const outputCurrency = useTokenBySymbolOrAddress(rawState.outputCurrencyId)
const inputCurrencyAmount = useMemo(
() => getCurrencyAmount(inputCurrency, rawState.inputCurrencyAmount),
- [inputCurrency, rawState.inputCurrencyAmount]
+ [inputCurrency, rawState.inputCurrencyAmount],
)
const outputCurrencyAmount = useMemo(
() => getCurrencyAmount(outputCurrency, rawState.outputCurrencyAmount),
- [outputCurrency, rawState.outputCurrencyAmount]
+ [outputCurrency, rawState.outputCurrencyAmount],
)
- const inputCurrencyBalance = useCurrencyAmountBalance(inputCurrency) || null
- const outputCurrencyBalance = useCurrencyAmountBalance(outputCurrency) || null
+ const inputCurrencyBalance = useCurrencyAmountBalanceCombined(inputCurrency) || null
+ const outputCurrencyBalance = useCurrencyAmountBalanceCombined(outputCurrency) || null
const {
inputAmount: { value: inputCurrencyFiatAmount },
@@ -61,7 +61,7 @@ export function useBuildTradeDerivedState(stateAtom: Atom
function getCurrencyAmount(
currency: Nullish | null,
- currencyAmount: Nullish
+ currencyAmount: Nullish,
): CurrencyAmount | null {
if (!currency || !currencyAmount) {
return null
diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts
index a1f7abf594..479cde549e 100644
--- a/libs/hook-dapp-lib/src/types.ts
+++ b/libs/hook-dapp-lib/src/types.ts
@@ -58,6 +58,9 @@ export interface HookDappContext {
isSmartContract: boolean | undefined
isPreHook: boolean
isDarkMode: boolean
+ // { [address: string]: { [token: string]: balanceDiff: string } }
+ // example: { '0x123': { '0x456': '100', '0xabc': '-100' } }
+ balancesDiff: Record>
}
export interface HookDappBase {