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): override Hook Gas limit and tokens balances with Tenderly Simulation Data #5039

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
3ac32e9
chore: init tenderly module
yvesfracari Oct 2, 2024
2260a54
feat: enable bundle simulations
yvesfracari Oct 2, 2024
e1b5c4c
refactor: change bundle simulation to use SWR
yvesfracari Oct 3, 2024
4bae430
chore: consider custom recipient
yvesfracari Oct 3, 2024
be46417
chore: merge into develop
yvesfracari Oct 3, 2024
8bed508
chore: remove goldrush sdk
yvesfracari Oct 11, 2024
12e0ee5
fix: error on post hooks get simulation
yvesfracari Oct 11, 2024
450e330
refactor: use bff on bundle simulation feature
yvesfracari Oct 11, 2024
495a56b
chore: merge into develop
yvesfracari Oct 11, 2024
7eff54d
chore: remove console.logs
yvesfracari Oct 11, 2024
beccb79
chore: fix leandro comments
yvesfracari Oct 16, 2024
0d3254c
chore: remove unused tenderly consts
yvesfracari Oct 17, 2024
2726bb1
refactor: rename tenderly simulation hook
yvesfracari Oct 17, 2024
6ea9624
chore: refactor top token holder swr to jotai with cache
yvesfracari Oct 17, 2024
732ecc9
chore: merge into develop
yvesfracari Oct 17, 2024
5cb5b72
chore: rename hook to match file name
yvesfracari Oct 18, 2024
22501d4
refactor: use seconds for cache time in toptokenholder state
yvesfracari Oct 22, 2024
c80d504
chore: merge into develop
yvesfracari Oct 22, 2024
b8bb1f4
chore: create hooks to use token balances with pre hooks
yvesfracari Oct 22, 2024
f9ade14
chore: use combined hooks on swap components
yvesfracari Oct 22, 2024
5c3f35e
feat: expose balance diff to hook dapp context
yvesfracari Oct 22, 2024
5667d1f
chore: add testing console log for balances diff access on hook dapp
yvesfracari Oct 23, 2024
aab2974
feat: use tenderly simulation gas on hook creation
yvesfracari Oct 24, 2024
050c189
chore: merge into main
yvesfracari Nov 4, 2024
21eb41c
chore: merge into main
yvesfracari Nov 4, 2024
468af55
refactor: implement PR code suggestions
yvesfracari Nov 4, 2024
eff88c7
chore: remove console logs
yvesfracari Nov 4, 2024
dd857e3
fix: use lower case address on combined balance hook
yvesfracari Nov 4, 2024
2a123c5
fix: not use combined balance on non hook trade type
yvesfracari Nov 4, 2024
4e9741f
fix: trigger simulation on order change if simulation data is null
yvesfracari Nov 6, 2024
48da7de
refactor: simulate hooks even if order isnt filled
yvesfracari Nov 6, 2024
74a8c0c
chore: improve variable documentation and simplify functions
yvesfracari Nov 7, 2024
e78b56b
Merge branch 'develop' into pedro/cow-414-make-hook-gas-limit-overrid…
yvesfracari Nov 7, 2024
06e8f32
chore: remove simulated gas buffer
yvesfracari Nov 7, 2024
f2bc296
feat: add refresh simulation button
yvesfracari Nov 8, 2024
e30bfc4
fix: not use tenderly gas used if failed
yvesfracari Nov 8, 2024
1745ea6
fix: infinite load on order change
yvesfracari Nov 8, 2024
4d83f03
feat: trigger extra hook simulation on order review
yvesfracari Nov 8, 2024
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
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand All @@ -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)

Expand All @@ -42,6 +51,8 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai
<styledEl.Label>
Hooks
<InfoTooltip content="Hooks are interactions before/after order execution." />
{hasSomeFailedSimulation && <styledEl.ErrorLabel>Simulation failed</styledEl.ErrorLabel>}
{isValidating && <styledEl.Spinner />}
</styledEl.Label>
<styledEl.Content onClick={() => setOpen(!isOpen)}>
{preHooksToDapp.length > 0 && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
`
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -33,7 +33,7 @@ function useAgnosticPermitDataIfUserHasNoAllowance(): Nullish<PermitHookData> {
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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TokenWithLogo> | 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])
}
Original file line number Diff line number Diff line change
@@ -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<string, string>): 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<string, BigNumber>,
)

return {
isLoading: currentBalances.isLoading,
values: normalizedValues,
}
}
2 changes: 2 additions & 0 deletions apps/cowswap-frontend/src/modules/combinedBalances/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './hooks/useCurrencyAmountBalanceCombined'
export * from './hooks/useTokensBalancesCombined'
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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])
Expand All @@ -49,6 +51,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho
isSmartContract,
isPreHook,
isDarkMode,
balancesDiff,
editHook(...args) {
editHook(...args)
onDismiss()
Expand Down
120 changes: 120 additions & 0 deletions apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {}

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking the type of the BalancesDiff I see is a structure to hold token balances. Having Diff in its name feels a bit constraining cause you can use it for example to hold the current balances, etc.

I would drop the diff from the name and would make the merging utilities more generic so they live not in the hookStore but in a more central place. Or at least can be moved to a utility instead of being in a hook file

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the main difference between this and a token balances is that it can be negative. Another difference between this and the token balances used on the rest of the app is the first key. Another difference is that the outer key of BalanceDiff is the address which the difference is related to while the BalanceState of the rest of the app the outer key is the chainId since it is always related to the connected account. For now, I don't see any other place on the app where we would store balances diff from addresses, so I think that there isn't too much gain of moving this to a utility for now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, makes sense

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this logic very similar to the other one that would apply the diffs to the current balances?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is similar, however is used for only one value instead of all of them and that is why I am not importing the function from there.

}
}
}
}
}

return result
}
Original file line number Diff line number Diff line change
@@ -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])
}
1 change: 1 addition & 0 deletions apps/cowswap-frontend/src/modules/hooksStore/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -56,6 +56,9 @@ export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHoo
{isValidating && <styledEl.Spinner />}
</styledEl.HookItemInfo>
<styledEl.HookItemActions>
<styledEl.ActionBtn onClick={() => mutate()} disabled={isValidating}>
<RefreshCw size={14} />
</styledEl.ActionBtn>
<styledEl.ActionBtn onClick={() => editHook(hookDetails.uuid)}>
<Edit2 size={14} />
</styledEl.ActionBtn>
Expand Down
Loading
Loading