diff --git a/apps/cowswap-frontend/src/modules/hooksStore/const.ts b/apps/cowswap-frontend/src/modules/hooksStore/const.ts deleted file mode 100644 index 2862d16083..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/const.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Sorry Safe, you need to set up CORS policy :) -// TODO: run our own instance -export const TENDERLY_SIMULATE_ENDPOINT_URL = - process.env.REACT_APP_TENDERLY_SIMULATE_ENDPOINT_URL || 'https://simulation.safe.global/' - -const TENDERLY_ORG_NAME = 'safe' -const TENDERLY_PROJECT_NAME = 'safe-apps' - -export const getSimulationLink = (simulationId: string): string => { - return `https://dashboard.tenderly.co/public/${TENDERLY_ORG_NAME}/${TENDERLY_PROJECT_NAME}/simulator/${simulationId}` -} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx index 0b42f6ffcd..fa7608915d 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -11,6 +11,7 @@ import { useSetupHooksStoreOrderParams } from '../../hooks/useSetupHooksStoreOrd import { HookRegistryList } from '../HookRegistryList' import { PostHookButton } from '../PostHookButton' import { PreHookButton } from '../PreHookButton' +import { BundleTenderlySimulate } from '../TenderlyBundleSimulation' type HookPosition = 'pre' | 'post' @@ -66,7 +67,10 @@ export function HooksStoreWidget() { ) const BottomContent = shouldNotUseHooks ? null : ( - setSelectedHookPosition('post')} onEditHook={onPostHookEdit} /> + <> + setSelectedHookPosition('post')} onEditHook={onPostHookEdit} /> + + ) return diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/PostHookButton/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/PostHookButton/index.tsx index 4f069abc86..77b057ecb8 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/PostHookButton/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/PostHookButton/index.tsx @@ -25,8 +25,8 @@ export function PostHookButton({ onOpen, onEditHook }: PostHookButtonProps) { <> {postHooks.length > 0 && ( {preHooks.length > 0 && ( >({}) + +export function BundleTenderlySimulate() { + const { chainId, account } = useWalletInfo() + const hooksData = useHooks() + const orderParams = useOrderParams() + const sellToken = useTokenContract(orderParams?.sellTokenAddress) + const buyToken = useTokenContract(orderParams?.buyTokenAddress) + const preHooks = hooksData?.preHooks.map(({ hook }) => hook) + const postHooks = hooksData?.postHooks.map(({ hook }) => hook) + + // update this later + const hooksId = useMemo( + () => getSimulationId({ preHooks, postHooks, orderParams }), + [preHooks, postHooks, orderParams], + ) + + const [simulationsSuccess, setSimulationsSuccess] = useAtom(tenderlySimulationSuccessAtom) + const simulationSuccess = simulationsSuccess[hooksId] + + const [isLoading, setIsLoading] = useState(false) + const simulate = useTenderlyBundleSimulate() + + const onSimulate = useCallback(async () => { + if (!sellToken || !buyToken || !orderParams || !account) return + setIsLoading(true) + + try { + const simulateSuccess = await simulate({ + preHooks, + postHooks, + orderParams, + tokenSell: sellToken, + tokenBuy: buyToken, + chainId, + account, + }) + + setSimulationsSuccess({ [hooksId]: simulateSuccess }) + } catch (error: any) { + setSimulationsSuccess({ [hooksId]: false }) + } finally { + setIsLoading(false) + } + }, [simulate, preHooks, postHooks, sellToken, buyToken, orderParams, hooksId, chainId, account]) + + if (isLoading) { + return ( + + + + ) + } + + if (simulationSuccess) { + return Success + } + + if (!simulationSuccess && simulationSuccess !== undefined) { + return ( + + Error + Retry + + ) + } + + return ( + + Simulate + + ) +} + +function getSimulationId({ + preHooks, + postHooks, + orderParams, +}: { + preHooks: CowHook[] + postHooks: CowHook[] + orderParams: HookDappOrderParams | null +}) { + if (!orderParams) return '' + const preHooksPart = preHooks.map(({ target, callData, gasLimit }) => `${target}:${callData}:${gasLimit}`).join(':') + const orderPart = `${orderParams.sellTokenAddress}:${orderParams.buyTokenAddress}:${orderParams.sellAmount}:${orderParams.buyAmount}` + const postHooksPart = postHooks.map(({ target, callData, gasLimit }) => `${target}:${callData}:${gasLimit}`).join(':') + return `${preHooksPart}-${orderPart}-${postHooksPart}` +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlyBundleSimulation/styled.ts b/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlyBundleSimulation/styled.ts new file mode 100644 index 0000000000..ef69f49897 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlyBundleSimulation/styled.ts @@ -0,0 +1,29 @@ +import { UI } from '@cowprotocol/ui' + +import styled from 'styled-components/macro' + +export const ExternalLinkContent = styled.span` + display: inline-flex; + align-items: center; + gap: 4px; + + &:hover { + text-decoration: underline; + } +` + +export const LoaderWrapper = styled.div` + margin: 0 20px; +` + +export const ErrorWrapper = styled.div` + text-align: right; + display: flex; + align-items: end; + flex-direction: column; + gap: 5px; +` + +export const ErrorText = styled.div` + color: var(${UI.COLOR_ALERT_TEXT}); +` diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx index 800da41ee1..1c7edfbd70 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/TenderlySimulate/index.tsx @@ -1,75 +1,17 @@ -import { atom, useAtom } from 'jotai' -import { useCallback, useState } from 'react' +import { ExternalLink, LinkIcon } from '@cowprotocol/ui' -import { errorToString } from '@cowprotocol/common-utils' -import { ButtonOutlined, ExternalLink, LinkIcon, Loader } from '@cowprotocol/ui' +import { useHookSimulationLink } from 'modules/tenderly/state/simulationLink' -import { ErrorText, ErrorWrapper, ExternalLinkContent, LoaderWrapper } from './styled' +import { ExternalLinkContent } from './styled' -import { getSimulationLink } from '../../const' -import { useTenderlySimulate } from '../../hooks/useTenderlySimulate' import { CowHook } from '../../types/hooks' -import { SimulationError, TenderlySimulation } from '../../types/TenderlySimulation' interface TenderlySimulateProps { hook: CowHook } -function isSimulationSuccessful(res: TenderlySimulation | SimulationError): res is TenderlySimulation { - return !!(res as TenderlySimulation).simulation -} - -const tenderlySimulationLinksAtom = atom>({}) -const tenderlySimulationErrorsAtom = atom>({}) - export function TenderlySimulate({ hook }: TenderlySimulateProps) { - const hookId = [hook.target, hook.callData, hook.gasLimit].join(':') - const [simulationLinks, setSimulationLink] = useAtom(tenderlySimulationLinksAtom) - const simulationLink = simulationLinks[hookId] - - const [simulationErrors, setSimulationError] = useAtom(tenderlySimulationErrorsAtom) - const simulationError = simulationErrors[hookId] - - const [isLoading, setIsLoading] = useState(false) - const simulate = useTenderlySimulate() - - const onSimulate = useCallback(async () => { - setIsLoading(true) - - try { - const response = await simulate(hook) - - if (isSimulationSuccessful(response)) { - const link = getSimulationLink(response.simulation.id) - - setSimulationLink({ [hookId]: link }) - setSimulationError({ [hookId]: undefined }) - } else { - setSimulationError({ [hookId]: response.error.message }) - } - } catch (error: any) { - setSimulationError({ [hookId]: errorToString(error) }) - } finally { - setIsLoading(false) - } - }, [simulate, hook, hookId]) - - if (isLoading) { - return ( - - - - ) - } - - if (simulationError) { - return ( - - {simulationError} - Retry - - ) - } + const simulationLink = useHookSimulationLink(hook) if (simulationLink) { return ( @@ -81,5 +23,5 @@ export function TenderlySimulate({ hook }: TenderlySimulateProps) { ) } - return Simulate + return <> } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts index 12c51aeaf2..e69f86b095 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useAddHook.ts @@ -1,7 +1,6 @@ import { useSetAtom } from 'jotai' import { useCallback } from 'react' - import { v4 as uuidv4 } from 'uuid' import { setHooksAtom } from '../state/hookDetailsAtom' diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts index 7bc2cb2dce..6b7a584dd4 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useSetupHooksStoreOrderParams.ts @@ -18,6 +18,9 @@ export function useSetupHooksStoreOrderParams() { validTo: orderParams.validTo, sellTokenAddress: getCurrencyAddress(orderParams.inputAmount.currency), buyTokenAddress: getCurrencyAddress(orderParams.outputAmount.currency), + sellAmount: orderParams.inputAmount, + buyAmount: orderParams.outputAmount, + receiver: orderParams.recipient, }) }, [orderParams]) } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts deleted file mode 100644 index e3d41b223d..0000000000 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useTenderlySimulate.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { useCallback } from 'react' - -import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' -import { useWalletInfo } from '@cowprotocol/wallet' - -import { TENDERLY_SIMULATE_ENDPOINT_URL } from '../const' -import { CowHook } from '../types/hooks' -import { SimulationError, TenderlySimulatePayload, TenderlySimulation } from '../types/TenderlySimulation' - -export function useTenderlySimulate(): (params: CowHook) => Promise { - const { account, chainId } = useWalletInfo() - const settlementContract = COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId] - - return useCallback( - async (params: CowHook) => { - const response = await fetch(TENDERLY_SIMULATE_ENDPOINT_URL, { - method: 'POST', - body: JSON.stringify(getTenderlySimulationInput(settlementContract, chainId, params)), - }).then((res) => res.json()) - - return response as TenderlySimulation | SimulationError - }, - [account, chainId], - ) -} - -function getTenderlySimulationInput(from: string, chainId: SupportedChainId, params: CowHook): TenderlySimulatePayload { - return { - input: params.callData, - to: params.target, - gas: +params.gasLimit, - from, - gas_price: '0', - network_id: chainId.toString(), - save: true, - save_if_fails: true, - } -} 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 7b872225bb..6a6721ba01 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -5,13 +5,14 @@ import { InfoTooltip } from '@cowprotocol/ui' import { Edit2, Trash2 } from 'react-feather' import SVG from 'react-inlinesvg' +import { TenderlySimulate } from 'modules/hooksStore/containers/TenderlySimulate' + import * as styledEl from './styled' -import { TenderlySimulate } from '../../containers/TenderlySimulate' import { CowHookDetailsSerialized } from '../../types/hooks' interface HookItemProp { - account: string | undefined + account?: string hookDetails: CowHookDetailsSerialized isPreHook: boolean removeHook: (uuid: string, isPreHook: boolean) => void @@ -42,7 +43,6 @@ export function AppliedHookItem({ account, hookDetails, isPreHook, editHook, rem - {account && (
diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx index 3d3a33d12f..3214959abc 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookList/index.tsx @@ -16,7 +16,7 @@ const HookList = styled.ul` ` interface AppliedHookListProps { - account: string | undefined + account?: string hooks: CowHookDetailsSerialized[] isPreHook: boolean removeHook: (uuid: string, isPreHook: boolean) => void @@ -59,8 +59,8 @@ export function AppliedHookList({ account, hooks, isPreHook, removeHook, editHoo + buyAmount: CurrencyAmount } export interface HookDappContext { diff --git a/apps/cowswap-frontend/src/modules/tenderly/const.ts b/apps/cowswap-frontend/src/modules/tenderly/const.ts new file mode 100644 index 0000000000..8d96815cf2 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/const.ts @@ -0,0 +1,10 @@ +// Sorry Safe, you need to set up CORS policy :) +// TODO: run our own instance +export const TENDERLY_API_BASE_ENDPOINT = process.env.REACT_APP_TENDERLY_SIMULATE_ENDPOINT_URL + +const TENDERLY_ORG_NAME = 'yvesfracari' +const TENDERLY_PROJECT_NAME = 'personal' + +export const getSimulationLink = (simulationId: string): string => { + return `https://dashboard.tenderly.co/${TENDERLY_ORG_NAME}/${TENDERLY_PROJECT_NAME}/simulator/${simulationId}` +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTokenBalanceSlot.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTokenBalanceSlot.ts new file mode 100644 index 0000000000..23afab6e36 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTokenBalanceSlot.ts @@ -0,0 +1,29 @@ +import { useSetAtom } from 'jotai' +import { useCallback } from 'react' + +import { getTokenBalanceSlotCacheAtom, storeTokenBalanceSlotCacheAtom } from '../state/tokenBalanceSlot' +import { GetTokenBalanceSlotParams } from '../types' +import { calculateTokenBalanceSlot } from '../utils/calculateTokenBalanceSlot' + +export function useGetTokenBalanceSlot(): (params: GetTokenBalanceSlotParams) => Promise { + const getCachedSettlementBalance = useSetAtom(getTokenBalanceSlotCacheAtom) + const storeSettlementBalanceCache = useSetAtom(storeTokenBalanceSlotCacheAtom) + return useCallback( + async (params: GetTokenBalanceSlotParams) => { + const cachedBalance = getCachedSettlementBalance(params) + if (cachedBalance) { + return cachedBalance + } + try { + const memorySlot = await calculateTokenBalanceSlot(params) + console.log(memorySlot) + storeSettlementBalanceCache({ ...params, memorySlot: memorySlot }) + return memorySlot + } catch { + console.error('Error fetching cached balance slot') + return + } + }, + [getCachedSettlementBalance, storeSettlementBalanceCache], + ) +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulate.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulate.ts new file mode 100644 index 0000000000..5d8e19a336 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulate.ts @@ -0,0 +1,45 @@ +import { useSetAtom } from 'jotai' +import { useCallback } from 'react' + +import { useWalletInfo } from '@cowprotocol/wallet' + +import { useGetTokenBalanceSlot } from './useGetTokenBalanceSlot' + +import { generateNewHookSimulationLinks, simulationLinksAtom } from '../state/simulationLink' +import { bundleSimulation, PostBundleSimulationParams } from '../utils/bundleSimulation' +import { checkBundleSimulationError } from '../utils/checkBundleSimulationError' + +export function useTenderlyBundleSimulate(): ( + params: Omit, +) => Promise { + const { account, chainId } = useWalletInfo() + const getTokenBalanceSlot = useGetTokenBalanceSlot() + const setLinks = useSetAtom(simulationLinksAtom) + + return useCallback( + async (params: Omit): Promise => { + if (!account) { + return + } + const balanceSlot = await getTokenBalanceSlot({ + tokenAddress: params.tokenBuy.address, + chainId, + }) + if (!balanceSlot) { + return + } + + const paramsWithSlot = { + ...params, + slotOverride: balanceSlot, + } + const response = await bundleSimulation(paramsWithSlot) + + if (checkBundleSimulationError(response)) return false + const simulationLinks = generateNewHookSimulationLinks(response, paramsWithSlot) + setLinks(simulationLinks) + return true + }, + [account, chainId, getTokenBalanceSlot, setLinks], + ) +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/state/simulationLink.ts b/apps/cowswap-frontend/src/modules/tenderly/state/simulationLink.ts new file mode 100644 index 0000000000..e5df8013d3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/state/simulationLink.ts @@ -0,0 +1,41 @@ +import { atom, useAtomValue } from 'jotai' + +import { CowHook } from 'modules/hooksStore/types/hooks' + +import { getSimulationLink } from '../const' +import { TenderlyBundleSimulationResponse } from '../types' +import { PostBundleSimulationParams } from '../utils/bundleSimulation' + +export const simulationLinksAtom = atom>({}) + +export function useHookSimulationLink(hook: CowHook) { + const simulationsValues = useAtomValue(simulationLinksAtom) + + return simulationsValues[getHookSimulationKey(hook)] +} + +export function getHookSimulationKey(hook: CowHook) { + return [hook.target, hook.callData, hook.gasLimit].join(':') +} + +export function generateNewHookSimulationLinks( + bundleSimulationResponse: TenderlyBundleSimulationResponse, + postParams: PostBundleSimulationParams, +) { + const preHooksKeys = postParams.preHooks.map(getHookSimulationKey) + const postHooksKeys = postParams.postHooks.map(getHookSimulationKey) + const swapKeys = ['sellTransfer', 'buyTransfer'] + + const keys = [...preHooksKeys, ...swapKeys, ...postHooksKeys] + + return keys.reduce( + (acc, key, index) => { + if (bundleSimulationResponse.simulation_results.length <= index) { + return acc + } + acc[key] = getSimulationLink(bundleSimulationResponse.simulation_results[index].simulation.id) + return acc + }, + {} as Record, + ) +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/state/tokenBalanceSlot.ts b/apps/cowswap-frontend/src/modules/tenderly/state/tokenBalanceSlot.ts new file mode 100644 index 0000000000..a299caabe7 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/state/tokenBalanceSlot.ts @@ -0,0 +1,22 @@ +import { atom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' + +import { GetTokenBalanceSlotParams, StoreTokenBalanceSlotParams } from '../types' + +export const tokenBalanceSlotCacheAtom = atomWithStorage>('tokenBalanceSlot:v1', {}) + +export const storeTokenBalanceSlotCacheAtom = atom(null, (get, set, params: StoreTokenBalanceSlotParams) => { + const key = buildKey(params) + + set(tokenBalanceSlotCacheAtom, (permitCache) => ({ ...permitCache, [key]: params.memorySlot })) +}) + +export const getTokenBalanceSlotCacheAtom = atom(null, (get, set, params: GetTokenBalanceSlotParams) => { + const permitCache = get(tokenBalanceSlotCacheAtom) + const key = buildKey(params) + return permitCache[key] +}) + +function buildKey({ chainId, tokenAddress }: GetTokenBalanceSlotParams) { + return `${chainId}-${tokenAddress.toLowerCase()}` +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/types/TenderlySimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/types.ts similarity index 95% rename from apps/cowswap-frontend/src/modules/hooksStore/types/TenderlySimulation.ts rename to apps/cowswap-frontend/src/modules/tenderly/types.ts index b61c0d6762..54149f9281 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/types/TenderlySimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/types.ts @@ -1,3 +1,26 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk' + +export interface GetTokenBalanceSlotParams { + tokenAddress: string + chainId: SupportedChainId +} + +export interface StoreTokenBalanceSlotParams extends GetTokenBalanceSlotParams { + memorySlot: string +} + +export interface TenderlyEncodeStatesResponse { + stateOverrides: Record }> +} + +export interface TenderlyEncodeStatesPayload extends TenderlyEncodeStatesResponse { + networkID: string +} + +export interface TenderlyBundleSimulationResponse { + simulation_results: TenderlySimulation[] +} + // types were found in Uniswap repository // https://github.com/Uniswap/governance-seatbelt/blob/e2c6a0b11d1660f3bd934dab0d9df3ca6f90a1a0/types.d.ts#L123 diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts new file mode 100644 index 0000000000..26bb5d418f --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts @@ -0,0 +1,139 @@ +import { Erc20 } from '@cowprotocol/abis' +import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' + +import { CowHook, HookDappOrderParams } from 'modules/hooksStore/types/hooks' + +import { TENDERLY_API_BASE_ENDPOINT } from '../const' +import { SimulationError, TenderlyBundleSimulationResponse, TenderlySimulatePayload } from '../types' + +export interface GetTransferTenderlySimulationInput { + currencyAmount: CurrencyAmount + from: string + receiver: string + token: Erc20 + chainId: SupportedChainId + slotOverride?: string +} + +export interface PostBundleSimulationParams { + account: string + chainId: SupportedChainId + tokenSell: Erc20 + tokenBuy: Erc20 + preHooks: CowHook[] + postHooks: CowHook[] + orderParams: HookDappOrderParams + slotOverride: string +} + +export const bundleSimulation = async ( + params: PostBundleSimulationParams, +): Promise => { + const response = await fetch(`${TENDERLY_API_BASE_ENDPOINT}/simulate-bundle`, { + method: 'POST', + body: JSON.stringify(getBundleTenderlySimulationInput(params)), + headers: { + 'X-Access-Key': process.env.TENDERLY_API_KEY as string, + }, + }).then((res) => res.json()) + + return response as TenderlyBundleSimulationResponse | SimulationError +} + +export function getCoWHookTenderlySimulationInput( + from: string, + params: CowHook, + chainId: SupportedChainId, +): TenderlySimulatePayload { + return { + input: params.callData, + to: params.target, + gas: +params.gasLimit, + from, + gas_price: '0', + network_id: chainId.toString(), + save: true, + save_if_fails: true, + } +} + +function currencyAmountToHexUint256(amount: CurrencyAmount) { + const valueAsBigInt = BigInt(amount.quotient.toString()) + + let hexString = valueAsBigInt.toString(16) + + hexString = hexString.padStart(64, '0') + return '0x' + hexString +} + +export function getTransferTenderlySimulationInput({ + currencyAmount, + from, + receiver, + token, + chainId, + slotOverride, +}: GetTransferTenderlySimulationInput): TenderlySimulatePayload { + const callData = token.interface.encodeFunctionData('transfer', [receiver, currencyAmount.toExact()]) + + const state_objects = slotOverride + ? { + [token.address]: { + storage: { + [slotOverride]: currencyAmountToHexUint256(currencyAmount), + }, + }, + } + : {} + + return { + input: callData, + to: token.address, + gas: 100000, // TODO: this should be calculated based on the token + from, + gas_price: '0', + network_id: chainId.toString(), + save: true, + save_if_fails: true, + state_objects, + } +} + +export function getBundleTenderlySimulationInput({ + account, + chainId, + tokenSell, + tokenBuy, + preHooks, + postHooks, + orderParams, + slotOverride, +}: PostBundleSimulationParams): { simulations: TenderlySimulatePayload[] } { + const settlementAddress = COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId] + const preHooksSimulations = preHooks.map((hook) => + getCoWHookTenderlySimulationInput(settlementAddress, hook, chainId), + ) + const postHooksSimulations = postHooks.map((hook) => + getCoWHookTenderlySimulationInput(settlementAddress, hook, chainId), + ) + + const sellTokenTransfer = getTransferTenderlySimulationInput({ + currencyAmount: orderParams.sellAmount, + from: account, + receiver: COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId], + token: tokenSell, + chainId, + }) + + const buyTokenSimulation = getTransferTenderlySimulationInput({ + currencyAmount: orderParams.sellAmount, + from: COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId], + receiver: orderParams.receiver, + token: tokenBuy, + chainId, + slotOverride, + }) + + return { simulations: [...preHooksSimulations, sellTokenTransfer, buyTokenSimulation, ...postHooksSimulations] } +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/calculateTokenBalanceSlot.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/calculateTokenBalanceSlot.ts new file mode 100644 index 0000000000..11da551999 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/calculateTokenBalanceSlot.ts @@ -0,0 +1,31 @@ +import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS } from '@cowprotocol/cow-sdk' + +import { TENDERLY_API_BASE_ENDPOINT } from '../const' +import { GetTokenBalanceSlotParams, TenderlyEncodeStatesPayload, TenderlyEncodeStatesResponse } from '../types' + +export const calculateTokenBalanceSlot = async (params: GetTokenBalanceSlotParams) => { + const response = (await fetch(`${TENDERLY_API_BASE_ENDPOINT}/contracts/encode-states`, { + method: 'POST', + body: JSON.stringify(getFetchTokenBalanceSlotInput(params)), + headers: { + 'X-Access-Key': process.env.TENDERLY_API_KEY as string, + }, + }).then((res) => res.json())) as TenderlyEncodeStatesResponse // TODO: error handling + + const balanceSlots = Object.keys(response.stateOverrides[params.tokenAddress.toLowerCase()].value) + + return balanceSlots[0] +} + +function getFetchTokenBalanceSlotInput(params: GetTokenBalanceSlotParams): TenderlyEncodeStatesPayload { + return { + networkID: params.chainId.toString(), + stateOverrides: { + [params.tokenAddress]: { + value: { + [`balances[${COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[params.chainId]}]`]: '0', // this fetch is only used for the slot, so the balance isn't important + }, + }, + }, + } +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/checkBundleSimulationError.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/checkBundleSimulationError.ts new file mode 100644 index 0000000000..8e84ac62b8 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/checkBundleSimulationError.ts @@ -0,0 +1,7 @@ +import { SimulationError, TenderlyBundleSimulationResponse } from '../types' + +export function checkBundleSimulationError( + response: TenderlyBundleSimulationResponse | SimulationError, +): response is SimulationError { + return (response as SimulationError).error !== undefined +}