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 {