Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(feat: hook-store): create bundle hooks tenderly simulation #4943

Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/cowswap-frontend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,4 @@ REACT_APP_MOCK=true
# REACT_APP_DOMAIN_REGEX_ENS="(:?^cowswap\.eth|ipfs)"

# Path regex (to detect environment)
# REACT_APP_PATH_REGEX_ENS="/ipfs"
# REACT_APP_PATH_REGEX_ENS="/ipfs"
2 changes: 1 addition & 1 deletion apps/cowswap-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@
"dependencies": {},
"devDependencies": {},
"nx": {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { useMemo } from 'react'

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

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

import * as styledEl from './styled'

import { TenderlySimulate } from '../../containers/TenderlySimulate'
Expand All @@ -23,25 +27,21 @@ interface HookItemProp {
index: number
}

// TODO: remove once a tenderly bundle simulation is ready
const isBundleSimulationReady = false
// TODO: refactor tu use single simulation as fallback
const isBundleSimulationReady = true

export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHook, removeHook, index }: HookItemProp) {
// TODO: Determine the simulation status based on actual simulation results
// For demonstration, using a placeholder. Replace with actual logic.
const simulationPassed = true // TODO: Replace with actual condition
const simulationStatus = simulationPassed ? 'Simulation successful' : 'Simulation failed'
const simulationTooltip = simulationPassed
? 'The Tenderly simulation was successful. Your transaction is expected to succeed.'
: 'The Tenderly simulation failed. Please review your transaction.'
const { isValidating, data } = useTenderlyBundleSimulateSWR()

// TODO: Placeholder for Tenderly simulation URL; replace with actual logic when available
const tenderlySimulationUrl = '' // e.g., 'https://tenderly.co/simulation/12345'
const simulationData = useMemo(() => {
if (!data) return
return data[hookDetails.uuid]
}, [data, hookDetails.uuid])

// TODO: Determine if simulation passed or failed
const isSimulationSuccessful = simulationPassed

if (!dapp) return null
const simulationStatus = simulationData?.status ? 'Simulation successful' : 'Simulation failed'
const simulationTooltip = simulationData?.status
? 'The Tenderly simulation was successful. Your transaction is expected to succeed.'
: 'The Tenderly simulation failed. Please review your transaction.'

return (
<styledEl.HookItemWrapper data-uid={hookDetails.uuid} as="li">
Expand All @@ -51,8 +51,9 @@ export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHoo
<SVG src={ICON_GRID} />
</styledEl.DragIcon>
<styledEl.HookNumber>{index + 1}</styledEl.HookNumber>
<img src={dapp.image} alt={dapp.name} />
<span>{dapp.name}</span>
<img src={dapp?.image || ''} alt={dapp?.name} />
<span>{dapp?.name}</span>
{isValidating && <styledEl.Spinner />}
</styledEl.HookItemInfo>
<styledEl.HookItemActions>
<styledEl.ActionBtn onClick={() => editHook(hookDetails.uuid)}>
Expand All @@ -64,15 +65,15 @@ export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHoo
</styledEl.HookItemActions>
</styledEl.HookItemHeader>

{account && isBundleSimulationReady && (
<styledEl.SimulateContainer isSuccessful={isSimulationSuccessful}>
{isSimulationSuccessful ? (
{account && isBundleSimulationReady && simulationData && (
<styledEl.SimulateContainer isSuccessful={simulationData.status}>
{simulationData.status ? (
<SVG src={ICON_CHECK_ICON} color="green" width={16} height={16} aria-label="Simulation Successful" />
) : (
<SVG src={ICON_X} color="red" width={14} height={14} aria-label="Simulation Failed" />
)}
{tenderlySimulationUrl ? (
<a href={tenderlySimulationUrl} target="_blank" rel="noopener noreferrer">
{simulationData.link ? (
<a href={simulationData.link} target="_blank" rel="noopener noreferrer">
{simulationStatus}
<ExternalLinkIcon size={14} />
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,19 @@ export const SimulateFooter = styled.div`
padding: 2px;
}
`

export const Spinner = styled.div`
alfetopito marked this conversation as resolved.
Show resolved Hide resolved
border: 5px solid transparent;
border-top-color: ${`var(${UI.COLOR_PRIMARY_LIGHTER})`};
border-radius: 50%;
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);
}
}
`
6 changes: 6 additions & 0 deletions apps/cowswap-frontend/src/modules/tenderly/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const TENDERLY_ORG_NAME = process.env.REACT_APP_TENDERLY_ORG_NAME
const TENDERLY_PROJECT_NAME = process.env.REACT_APP_TENDERLY_PROJECT_NAME

export const getSimulationLink = (simulationId: string): string => {
return `https://dashboard.tenderly.co/${TENDERLY_ORG_NAME}/${TENDERLY_PROJECT_NAME}/simulator/${simulationId}`
}
yvesfracari marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useCallback } from 'react'

import { useWalletInfo } from '@cowprotocol/wallet'

import useSWR from 'swr'

import { useHooks } from 'modules/hooksStore'
import { useOrderParams } from 'modules/hooksStore/hooks/useOrderParams'

import { useTokenContract } from 'common/hooks/useContract'

import { useTopTokenHolders } from './useTopTokenHolders'

import { bundleSimulation } from '../utils/bundleSimulation'
import { generateNewSimulationData, generateSimulationDataToError } from '../utils/generateSimulationData'
import { getTokenTransferInfo } from '../utils/getTokenTransferInfo'

export function useTenderlyBundleSimulateSWR() {
yvesfracari marked this conversation as resolved.
Show resolved Hide resolved
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 { data: buyTokenTopHolders, isValidating: isTopTokenHoldersValidating } = useTopTokenHolders({
tokenAddress: tokenBuy?.address,
chainId,
})
yvesfracari marked this conversation as resolved.
Show resolved Hide resolved

const getNewSimulationData = useCallback(async () => {
if (postHooks.length === 0 && preHooks.length === 0) return {}

if (!account || !buyTokenTopHolders || !tokenBuy || !orderParams || !tokenSell || !buyAmount) {
return generateSimulationDataToError({ postHooks, preHooks })
}

const tokenBuyTransferInfo = getTokenTransferInfo({
tokenHolders: buyTokenTopHolders,
amountToTransfer: buyAmount,
})

const paramsComplete = {
postHooks,
preHooks,
tokenBuy,
tokenBuyTransferInfo,
orderParams,
tokenSell,
account,
chainId,
}

try {
const response = await bundleSimulation(paramsComplete)
return generateNewSimulationData(response, paramsComplete)
} catch {
return generateSimulationDataToError(paramsComplete)
}
}, [account, chainId, buyTokenTopHolders, tokenBuy, postHooks, preHooks, buyAmount])

const { data, isValidating: isBundleSimulationLoading } = useSWR(
['tenderly-bundle-simulation', postHooks, preHooks, orderParams?.sellTokenAddress, orderParams?.buyTokenAddress],
getNewSimulationData,
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
refreshWhenOffline: false,
},
)

return { data, isValidating: isBundleSimulationLoading || isTopTokenHoldersValidating }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { BFF_BASE_URL } from '@cowprotocol/common-const'
import { SupportedChainId } from '@cowprotocol/cow-sdk'

import useSWR from 'swr'

import { TokenHolder } from '../types'

export interface GetTopTokenHoldersParams {
tokenAddress?: string
chainId: SupportedChainId
}

export async function getTopTokenHolder({ tokenAddress, chainId }: GetTopTokenHoldersParams) {
if (!tokenAddress) return

return (await fetch(`${BFF_BASE_URL}/${chainId}/tokens/${tokenAddress}/topHolders`, {
method: 'GET',
}).then((res) => res.json())) as TokenHolder[]
}

export function useTopTokenHolders(params: GetTopTokenHoldersParams) {
return useSWR(['topTokenHolders', params], () => getTopTokenHolder(params))
}
19 changes: 19 additions & 0 deletions apps/cowswap-frontend/src/modules/tenderly/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface SimulationInput {
input: string
from: string
to: string
value?: string
gas?: number
gas_price?: string
}

export interface SimulationData {
link: string
status: boolean
id: string
}

export interface TokenHolder {
address: string
balance: string
}
105 changes: 105 additions & 0 deletions apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { Erc20 } 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 { Currency, CurrencyAmount } from '@uniswap/sdk-core'

import { CowHook, HookDappOrderParams } from 'modules/hooksStore/types/hooks'

import { SimulationData, SimulationInput } from '../types'

export interface GetTransferTenderlySimulationInput {
currencyAmount: CurrencyAmount<Currency>
from: string
receiver: string
token: Erc20
}

export type TokenBuyTransferInfo = {
sender: string
amount: CurrencyAmount<Currency>
}[]
export interface PostBundleSimulationParams {
account: string
chainId: SupportedChainId
tokenSell: Erc20
tokenBuy: Erc20
preHooks: CowHookDetails[]
postHooks: CowHookDetails[]
orderParams: HookDappOrderParams
tokenBuyTransferInfo: TokenBuyTransferInfo
}

export const bundleSimulation = async (params: PostBundleSimulationParams): Promise<SimulationData[]> => {
const input = getBundleTenderlySimulationInput(params)
const response = await fetch(`${BFF_BASE_URL}/${params.chainId}/simulation/simulateBundle`, {
method: 'POST',
body: JSON.stringify(input),
headers: {
'Content-Type': 'application/json',
},
}).then((res) => res.json())

return response as SimulationData[]
}

export function getCoWHookTenderlySimulationInput(from: string, params: CowHook): SimulationInput {
return {
input: params.callData,
to: params.target,
from,
}
}

export function getTransferTenderlySimulationInput({
currencyAmount,
from,
receiver,
token,
}: GetTransferTenderlySimulationInput): SimulationInput {
const callData = token.interface.encodeFunctionData('transfer', [receiver, currencyAmount.quotient.toString()])

return {
input: callData,
to: token.address,
from,
}
}

export function getBundleTenderlySimulationInput({
account,
chainId,
tokenSell,
tokenBuy,
preHooks,
postHooks,
orderParams,
tokenBuyTransferInfo,
}: PostBundleSimulationParams): SimulationInput[] {
const settlementAddress = COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId]
const preHooksSimulations = preHooks.map((hook) => getCoWHookTenderlySimulationInput(settlementAddress, hook.hook))
const postHooksSimulations = postHooks.map((hook) => getCoWHookTenderlySimulationInput(settlementAddress, hook.hook))

// If there are no post hooks, we don't need to simulate the transfer
if (postHooks.length === 0) return preHooksSimulations

const receiver = postHooks[0].recipientOverride || orderParams.receiver

const sellTokenTransfer = getTransferTenderlySimulationInput({
currencyAmount: orderParams.sellAmount,
from: account,
receiver: COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId],
token: tokenSell,
})

const buyTokenTransfers = tokenBuyTransferInfo.map((transferInfo) =>
getTransferTenderlySimulationInput({
currencyAmount: transferInfo.amount,
from: transferInfo.sender,
receiver,
token: tokenBuy,
}),
)

return [...preHooksSimulations, sellTokenTransfer, ...buyTokenTransfers, ...postHooksSimulations]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { PostBundleSimulationParams } from './bundleSimulation'

import { SimulationData } from '../types'

export function generateSimulationDataToError(
postParams: Pick<PostBundleSimulationParams, 'preHooks' | 'postHooks'>,
): Record<string, SimulationData> {
const preHooksKeys = postParams.preHooks.map((hookDetails) => hookDetails.uuid)
const postHooksKeys = postParams.postHooks.map((hookDetails) => hookDetails.uuid)
const hooksKeys = [...preHooksKeys, ...postHooksKeys]

return hooksKeys.reduce(
(acc, key) => ({
...acc,
[key]: { link: '', status: false, id: key },
}),
{},
)
}

export function generateNewSimulationData(
simulationData: SimulationData[],
postParams: PostBundleSimulationParams,
): Record<string, SimulationData> {
const preHooksKeys = postParams.preHooks.map((hookDetails) => hookDetails.uuid)
const postHooksKeys = postParams.postHooks.map((hookDetails) => hookDetails.uuid)

const preHooksData = simulationData.slice(0, preHooksKeys.length)

const postHooksData = simulationData.slice(-postHooksKeys.length)

return {
...preHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: preHooksData[index] }), {}),
...postHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: postHooksData[index] }), {}),
}
}
Loading
Loading