Skip to content

Commit

Permalink
feat(hook-store): create bundle hooks tenderly simulation (#4943)
Browse files Browse the repository at this point in the history
* chore: init tenderly module

* feat: enable bundle simulations

* refactor: change bundle simulation to use SWR

* chore: consider custom recipient

* chore: remove goldrush sdk

* fix: error on post hooks get simulation

* refactor: use bff on bundle simulation feature

* chore: remove console.logs

* chore: fix leandro comments

* chore: remove unused tenderly consts

* refactor: rename tenderly simulation hook

* chore: refactor top token holder swr to jotai with cache

* chore: rename hook to match file name

* refactor: use seconds for cache time in toptokenholder state
  • Loading branch information
yvesfracari authored Oct 23, 2024
1 parent f6f6f8c commit 435bfdf
Show file tree
Hide file tree
Showing 13 changed files with 448 additions and 24 deletions.
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 @@ -21,6 +21,7 @@ export function useSetupHooksStoreOrderParams() {
buyAmount: orderParams.outputAmount.quotient.toString(),
sellTokenAddress: getCurrencyAddress(orderParams.inputAmount.currency),
buyTokenAddress: getCurrencyAddress(orderParams.outputAmount.currency),
receiver: orderParams.recipient,
})
}
}, [orderParams, setOrderParams])
Expand Down
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 { useTenderlyBundleSimulation } 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 } = useTenderlyBundleSimulation()

// 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`
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);
}
}
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useAtom } from 'jotai'
import { useCallback } from 'react'

import { topTokenHoldersAtom } from '../state/topTokenHolders'
import { GetTopTokenHoldersParams } from '../types'

export function useGetTopTokenHolders() {
const [cachedData, fetchTopTokenHolders] = useAtom(topTokenHoldersAtom)

return useCallback(
async (params: GetTopTokenHoldersParams) => {
const key = `${params.chainId}-${params.tokenAddress}`
if (cachedData[key]?.value) {
return cachedData[key].value
}
return fetchTopTokenHolders(params)
},
[cachedData, fetchTopTokenHolders],
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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 { useGetTopTokenHolders } from './useGetTopTokenHolders'

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

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

if (!postHooks.length)
return preHooksBundleSimulation({
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 (!simulationData) {
return {}
}

return generateNewSimulationData(simulationData, { preHooks, postHooks })
} catch {
return generateSimulationDataToError({ preHooks, postHooks })
}
}, [preHooks, postHooks, simulateBundle])

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 }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { atom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'

import { BFF_BASE_URL } from '@cowprotocol/common-const'
import { SupportedChainId } from '@cowprotocol/cow-sdk'

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

export interface TokenHolder {
address: string
balance: string
}

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[]
}

interface CachedValue<T> {
value: T
timestamp: number
}

const baseTopTokenHolderAtom = atomWithStorage<Record<string, CachedValue<TokenHolder[] | undefined>>>(
'topTokenHolders:v1',
{},
)

export const topTokenHoldersAtom = atom(
(get) => get(baseTopTokenHolderAtom),
async (get, set, params: GetTopTokenHoldersParams) => {
const key = `${params.chainId}:${params.tokenAddress?.toLowerCase()}`
const cachedData = get(baseTopTokenHolderAtom)
const currentTime = Date.now() / 1000

// 1 hour in seconds
if (cachedData[key] && currentTime - cachedData[key].timestamp <= 3600) {
return cachedData[key].value
}

const newValue = await getTopTokenHolder(params)
set(baseTopTokenHolderAtom, {
...cachedData,
[key]: { value: newValue, timestamp: currentTime },
})

return newValue
},
)
26 changes: 26 additions & 0 deletions apps/cowswap-frontend/src/modules/tenderly/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SupportedChainId } from '@cowprotocol/cow-sdk'

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 GetTopTokenHoldersParams {
tokenAddress?: string
chainId: SupportedChainId
}

export interface TokenHolder {
address: string
balance: string
}
Loading

0 comments on commit 435bfdf

Please sign in to comment.