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

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions apps/cowswap-frontend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,9 @@ REACT_APP_MOCK=true

# Path regex (to detect environment)
# REACT_APP_PATH_REGEX_ENS="/ipfs"

# REACT_APP_GOLD_RUSH_API_KEY=
# REACT_APP_TENDERLY_API_KEY=
# REACT_APP_TENDERLY_SIMULATE_ENDPOINT_URL=
# REACT_APP_TENDERLY_ORG_NAME=
# REACT_APP_TENDERLY_PROJECT_NAME=
6 changes: 4 additions & 2 deletions apps/cowswap-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
"last 1 safari version"
]
},
"dependencies": {},
"dependencies": {
"@covalenthq/client-sdk": "^2.1.1"
},
"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,5 +1,7 @@
// src/modules/hooksStore/pure/AppliedHookItem/index.tsx

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 @@ -9,6 +11,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 @@ -24,8 +28,8 @@ 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,
Expand All @@ -36,19 +40,17 @@ export function AppliedHookItem({
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
const simulationStatus = simulationData?.simulationPassed ? 'Simulation successful' : 'Simulation failed'
const simulationTooltip = simulationData?.simulationPassed
? '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 @@ -60,6 +62,7 @@ export function AppliedHookItem({
<styledEl.HookNumber>{index + 1}</styledEl.HookNumber>
<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 @@ -71,15 +74,15 @@ export function AppliedHookItem({
</styledEl.HookItemActions>
</styledEl.HookItemHeader>

{account && isBundleSimulationReady && (
<styledEl.SimulateContainer isSuccessful={isSimulationSuccessful}>
{isSimulationSuccessful ? (
{account && isBundleSimulationReady && simulationData && (
<styledEl.SimulateContainer isSuccessful={simulationData.simulationPassed}>
{simulationData.simulationPassed ? (
<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.tenderlySimulationLink ? (
<a href={simulationData.tenderlySimulationLink} target="_blank" rel="noopener noreferrer">
{simulationStatus}
<ExternalLinkIcon size={14} />
</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,19 @@ export const SimulateFooter = styled.div`
padding: 2px;
}
`

export const Spinner = styled.div`
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);
}
}
`
25 changes: 25 additions & 0 deletions apps/cowswap-frontend/src/modules/tenderly/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SupportedChainId } from '@cowprotocol/cow-sdk'

import { Chain, ChainName } from '@covalenthq/client-sdk'

// 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
export const TENDERLY_API_KEY = process.env.REACT_APP_TENDERLY_API_KEY || ''

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}`
}

export const GOLD_RUSH_API_KEY = process.env.REACT_APP_GOLD_RUSH_API_KEY
export const GOLD_RUSH_API_BASE_URL = 'https://api.covalenthq.com'

export const GOLD_RUSH_CLIENT_NETWORK_MAPPING: Record<SupportedChainId, Chain> = {
[SupportedChainId.MAINNET]: ChainName.ETH_MAINNET,
[SupportedChainId.SEPOLIA]: ChainName.ETH_SEPOLIA,
[SupportedChainId.GNOSIS_CHAIN]: ChainName.GNOSIS_MAINNET,
[SupportedChainId.ARBITRUM_ONE]: ChainName.ARBITRUM_MAINNET,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useSetAtom } from 'jotai'
import { useCallback, useMemo } from 'react'

import { useWalletInfo } from '@cowprotocol/wallet'

import { bundleSimulation } from '../utils/bundleSimulation'
import { checkBundleSimulationError } from '../utils/checkBundleSimulationError'
import { useTopTokenHolders } from './useTopTokenHolders'
import { getTokenTransferInfo } from '../utils/getTokenTransferInfo'
import { useOrderParams } from 'modules/hooksStore/hooks/useOrderParams'
import { useTokenContract } from 'common/hooks/useContract'
import useSWR from 'swr'
import { useHooks } from 'modules/hooksStore'
import { generateNewSimulationData, generateSimulationDataToError } from '../utils/generateSimulationData'

export function useTenderlyBundleSimulateSWR() {
const { account, chainId } = useWalletInfo()
const { preHooks, postHooks } = useHooks()
const orderParams = useOrderParams()
const tokenSell = useTokenContract(orderParams?.sellTokenAddress)
const tokenBuy = useTokenContract(orderParams?.buyTokenAddress)

const { data: buyTokenTopHolders } = useTopTokenHolders({ tokenAddress: tokenBuy?.address, chainId })

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

if (!account || !buyTokenTopHolders || !tokenBuy || !orderParams || !tokenSell) {
throw new Error('Missing required data for simulation')
}

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

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

const response = await bundleSimulation(paramsComplete)

return checkBundleSimulationError(response)
? generateSimulationDataToError(paramsComplete)
: generateNewSimulationData(response, paramsComplete)
}, [account, chainId, buyTokenTopHolders, tokenBuy, postHooks, preHooks])

return useSWR(['tenderly-bundle-simulation', postHooks, preHooks], getNewSimulationData)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SupportedChainId } from '@cowprotocol/cow-sdk'

import useSWR from 'swr'

import { GOLD_RUSH_API_BASE_URL, GOLD_RUSH_API_KEY, GOLD_RUSH_CLIENT_NETWORK_MAPPING } from '../const'
import { TokenHoldersResponse } from '../types'

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

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

const response = (await fetch(
`${GOLD_RUSH_API_BASE_URL}/v1/${GOLD_RUSH_CLIENT_NETWORK_MAPPING[chainId]}/tokens/${tokenAddress}/token_holders_v2/`,
{
method: 'GET',
headers: { Authorization: `Bearer ${GOLD_RUSH_API_KEY}` },
},
).then((res) => res.json())) as TokenHoldersResponse

if (response.error) return

return response.data.items
}

export function useTopTokenHolders(params: GetTopTokenHoldersParams) {
return useSWR(['topTokenHolders', params], () => getTopTokenHolder(params))
}
Loading
Loading