From 3ac32e995c067d64fc08e97ff1ca55454fb03fb6 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Wed, 2 Oct 2024 11:06:00 -0300 Subject: [PATCH 01/31] chore: init tenderly module --- .../hooks/useSetupHooksStoreOrderParams.ts | 3 + .../src/modules/tenderly/const.ts | 13 + .../modules/tenderly/states/simulationLink.ts | 41 ++ .../src/modules/tenderly/types.ts | 574 ++++++++++++++++++ .../tenderly/utils/bundleSimulation.ts | 139 +++++ .../utils/checkBundleSimulationError.ts | 7 + libs/hook-dapp-lib/src/types.ts | 4 + 7 files changed, 781 insertions(+) create mode 100644 apps/cowswap-frontend/src/modules/tenderly/const.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/states/simulationLink.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/types.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/utils/checkBundleSimulationError.ts 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/tenderly/const.ts b/apps/cowswap-frontend/src/modules/tenderly/const.ts new file mode 100644 index 0000000000..c292f16e95 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/const.ts @@ -0,0 +1,13 @@ +import { providers } from 'ethers' + +export const TENDERLY_TESTNET_PROVIDER = new providers.JsonRpcProvider(process.env.TENDERLY_VNET_RPC) +// 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/states/simulationLink.ts b/apps/cowswap-frontend/src/modules/tenderly/states/simulationLink.ts new file mode 100644 index 0000000000..e5df8013d3 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/states/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/types.ts b/apps/cowswap-frontend/src/modules/tenderly/types.ts new file mode 100644 index 0000000000..8bc3f78d2b --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/types.ts @@ -0,0 +1,574 @@ +export interface TenderlyBundleSimulationResponse { + simulation_results: TenderlySimulation[] +} + +// types were found in Uniswap repository +// https://github.com/Uniswap/governance-seatbelt/blob/e2c6a0b11d1660f3bd934dab0d9df3ca6f90a1a0/types.d.ts#L123 + +type StateObject = { + balance?: string + code?: string + storage?: Record +} + +type ContractObject = { + contractName: string + source: string + sourcePath: string + compiler: { + name: 'solc' + version: string + } + networks: Record< + string, + { + events?: Record + links?: Record + address: string + transactionHash?: string + } + > +} + +export type TenderlySimulatePayload = { + network_id: string + block_number?: number + transaction_index?: number + from: string + to: string + input: string + gas: number + gas_price?: string + value?: string + simulation_type?: 'full' | 'quick' + save?: boolean + save_if_fails?: boolean + state_objects?: Record + contracts?: ContractObject[] + block_header?: { + number?: string + timestamp?: string + } + generate_access_list?: boolean +} + +// --- Tenderly types, Response --- +// NOTE: These type definitions were autogenerated using https://app.quicktype.io/, so are almost +// certainly not entirely accurate (and they have some interesting type names) + +export interface TenderlySimulation { + transaction: Transaction + simulation: Simulation + contracts: TenderlyContract[] + generated_access_list: GeneratedAccessList[] +} + +interface TenderlyContract { + id: string + contract_id: string + balance: string + network_id: string + public: boolean + export: boolean + verified_by: string + verification_date: null + address: string + contract_name: string + ens_domain: null + type: string + evm_version: string + compiler_version: string + optimizations_used: boolean + optimization_runs: number + libraries: null + data: Data + creation_block: number + creation_tx: string + creator_address: string + created_at: Date + number_of_watches: null + language: string + in_project: boolean + number_of_files: number + standard?: string + standards?: string[] + token_data?: TokenData +} + +interface Data { + main_contract: number + contract_info: ContractInfo[] + abi: ABI[] + raw_abi: null +} + +interface ABI { + type: ABIType + name: string + constant: boolean + anonymous: boolean + inputs: SoltypeElement[] + outputs: Output[] | null +} + +interface SoltypeElement { + name: string + type: SoltypeType + storage_location: StorageLocation + components: SoltypeElement[] | null + offset: number + index: string + indexed: boolean + simple_type?: Type +} + +interface Type { + type: SimpleTypeType +} + +enum SimpleTypeType { + Address = 'address', + Bool = 'bool', + Bytes = 'bytes', + Slice = 'slice', + String = 'string', + Uint = 'uint', +} + +enum StorageLocation { + Calldata = 'calldata', + Default = 'default', + Memory = 'memory', + Storage = 'storage', +} + +enum SoltypeType { + Address = 'address', + Bool = 'bool', + Bytes32 = 'bytes32', + MappingAddressUint256 = 'mapping (address => uint256)', + MappingUint256Uint256 = 'mapping (uint256 => uint256)', + String = 'string', + Tuple = 'tuple', + TypeAddress = 'address[]', + TypeTuple = 'tuple[]', + Uint16 = 'uint16', + Uint256 = 'uint256', + Uint48 = 'uint48', + Uint56 = 'uint56', + Uint8 = 'uint8', +} + +interface Output { + name: string + type: SoltypeType + storage_location: StorageLocation + components: SoltypeElement[] | null + offset: number + index: string + indexed: boolean + simple_type?: SimpleType +} + +interface SimpleType { + type: SimpleTypeType + nested_type?: Type +} + +enum ABIType { + Constructor = 'constructor', + Event = 'event', + Function = 'function', +} + +interface ContractInfo { + id: number + path: string + name: string + source: string +} + +interface TokenData { + symbol: string + name: string + decimals: number +} + +interface GeneratedAccessList { + address: string + storage_keys: string[] +} + +interface Simulation { + id: string + project_id: string + owner_id: string + network_id: string + block_number: number + transaction_index: number + from: string + to: string + input: string + gas: number + gas_price: string + value: string + method: string + status: boolean + access_list: null + queue_origin: string + created_at: Date +} + +interface ErrorInfo { + error_message: string + address: string +} + +export interface SimulationError { + error: { + id: string + message: string + slug: string + } +} + +interface Transaction { + hash: string + block_hash: string + block_number: number + from: string + gas: number + gas_price: number + gas_fee_cap: number + gas_tip_cap: number + cumulative_gas_used: number + gas_used: number + effective_gas_price: number + input: string + nonce: number + to: string + index: number + error_message?: string + error_info?: ErrorInfo + value: string + access_list: null + status: boolean + addresses: string[] + contract_ids: string[] + network_id: string + function_selector: string + transaction_info: TransactionInfo + timestamp: Date + method: string + decoded_input: null + // Note: manually added (partial keys of `call_trace`) + call_trace: Array<{ + error?: string + input: string + }> +} + +interface TransactionInfo { + contract_id: string + block_number: number + transaction_id: string + contract_address: string + method: string + parameters: null + intrinsic_gas: number + refund_gas: number + call_trace: CallTrace + stack_trace: null | StackTrace[] + logs: Log[] | null + state_diff: StateDiff[] + raw_state_diff: null + console_logs: null + created_at: Date +} + +interface StackTrace { + file_index: number + contract: string + name: string + line: number + error: string + error_reason: string + code: string + op: string + length: number +} + +interface CallTrace { + hash: string + contract_name: string + function_name: string + function_pc: number + function_op: string + function_file_index: number + function_code_start: number + function_line_number: number + function_code_length: number + function_states: CallTraceFunctionState[] + caller_pc: number + caller_op: string + call_type: string + from: string + from_balance: string + to: string + to_balance: string + value: string + caller: Caller + block_timestamp: Date + gas: number + gas_used: number + intrinsic_gas: number + input: string + decoded_input: Input[] + state_diff: StateDiff[] + logs: Log[] + output: string + decoded_output: FunctionVariableElement[] + network_id: string + calls: CallTraceCall[] +} + +interface Caller { + address: string + balance: string +} + +interface CallTraceCall { + hash: string + contract_name: string + function_name: string + function_pc: number + function_op: string + function_file_index: number + function_code_start: number + function_line_number: number + function_code_length: number + function_states: CallTraceFunctionState[] + function_variables: FunctionVariableElement[] + caller_pc: number + caller_op: string + caller_file_index: number + caller_line_number: number + caller_code_start: number + caller_code_length: number + call_type: string + from: string + from_balance: null + to: string + to_balance: null + value: null + caller: Caller + block_timestamp: Date + gas: number + gas_used: number + input: string + decoded_input: Input[] + output: string + decoded_output: FunctionVariableElement[] + network_id: string + calls: PurpleCall[] +} + +interface PurpleCall { + hash: string + contract_name: string + function_name: string + function_pc: number + function_op: string + function_file_index: number + function_code_start: number + function_line_number: number + function_code_length: number + function_states?: FluffyFunctionState[] + function_variables?: FunctionVariable[] + caller_pc: number + caller_op: string + caller_file_index: number + caller_line_number: number + caller_code_start: number + caller_code_length: number + call_type: string + from: string + from_balance: null | string + to: string + to_balance: null | string + value: null | string + caller: Caller + block_timestamp: Date + gas: number + gas_used: number + refund_gas?: number + input: string + decoded_input: Input[] + output: string + decoded_output: FunctionVariable[] | null + network_id: string + calls: FluffyCall[] | null +} + +interface FluffyCall { + hash: string + contract_name: string + function_name?: string + function_pc: number + function_op: string + function_file_index?: number + function_code_start?: number + function_line_number?: number + function_code_length?: number + function_states?: FluffyFunctionState[] + function_variables?: FunctionVariable[] + caller_pc: number + caller_op: string + caller_file_index: number + caller_line_number: number + caller_code_start: number + caller_code_length: number + call_type: string + from: string + from_balance: null | string + to: string + to_balance: null | string + value: null | string + caller?: Caller + block_timestamp: Date + gas: number + gas_used: number + input: string + decoded_input?: FunctionVariable[] + output: string + decoded_output: PurpleDecodedOutput[] | null + network_id: string + calls: TentacledCall[] | null + refund_gas?: number +} + +interface TentacledCall { + hash: string + contract_name: string + function_name: string + function_pc: number + function_op: string + function_file_index: number + function_code_start: number + function_line_number: number + function_code_length: number + function_states: PurpleFunctionState[] + caller_pc: number + caller_op: string + caller_file_index: number + caller_line_number: number + caller_code_start: number + caller_code_length: number + call_type: string + from: string + from_balance: null + to: string + to_balance: null + value: null + caller: Caller + block_timestamp: Date + gas: number + gas_used: number + input: string + decoded_input: FunctionVariableElement[] + output: string + decoded_output: FunctionVariable[] + network_id: string + calls: null +} + +interface FunctionVariableElement { + soltype: SoltypeElement + value: string +} + +interface FunctionVariable { + soltype: SoltypeElement + value: PurpleValue | string +} + +interface PurpleValue { + ballot: string + basedOn: string + configured: string + currency: string + cycleLimit: string + discountRate: string + duration: string + fee: string + id: string + metadata: string + number: string + projectId: string + start: string + tapped: string + target: string + weight: string +} + +interface PurpleFunctionState { + soltype: SoltypeElement + value: Record +} + +interface PurpleDecodedOutput { + soltype: SoltypeElement + value: boolean | PurpleValue | string +} + +interface FluffyFunctionState { + soltype: PurpleSoltype + value: Record +} + +interface PurpleSoltype { + name: string + type: SoltypeType + storage_location: StorageLocation + components: null + offset: number + index: string + indexed: boolean +} + +interface Input { + soltype: SoltypeElement | null + value: boolean | string +} + +interface CallTraceFunctionState { + soltype: PurpleSoltype + value: Record +} + +interface Log { + name: string | null + anonymous: boolean + inputs: Input[] + raw: LogRaw +} + +interface LogRaw { + address: string + topics: string[] + data: string +} + +interface StateDiff { + soltype: SoltypeElement | null + original: string | Record + dirty: string | Record + raw: RawElement[] +} + +interface RawElement { + address: string + key: string + original: string + dirty: string +} 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/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 +} diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts index 32201d1725..3e7d6fdc03 100644 --- a/libs/hook-dapp-lib/src/types.ts +++ b/libs/hook-dapp-lib/src/types.ts @@ -1,4 +1,5 @@ import type { SupportedChainId } from '@cowprotocol/cow-sdk' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' export interface CowHook { target: string @@ -30,6 +31,9 @@ export interface HookDappOrderParams { validTo: number sellTokenAddress: string buyTokenAddress: string + receiver: string + sellAmount: CurrencyAmount + buyAmount: CurrencyAmount } export interface HookDappContext { From 2260a54dbe05a8988f75a1b5add268b6ea4ae4ce Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Wed, 2 Oct 2024 15:55:37 -0300 Subject: [PATCH 02/31] feat: enable bundle simulations --- apps/cowswap-frontend/.env | 6 ++ apps/cowswap-frontend/package.json | 6 +- .../containers/HooksStoreWidget/index.tsx | 12 ++- .../hooksStore/pure/AppliedHookItem/index.tsx | 19 ++--- .../src/modules/tenderly/const.ts | 20 ++++- .../tenderly/hooks/useBundleSimulation.ts | 58 ++++++++++++++ .../tenderly/hooks/useTopTokenHolders.ts | 31 ++++++++ .../src/modules/tenderly/states/simulation.ts | 70 +++++++++++++++++ .../modules/tenderly/states/simulationLink.ts | 41 ---------- .../src/modules/tenderly/types.ts | 31 ++++++++ .../tenderly/utils/bundleSimulation.ts | 75 +++++++++++-------- .../tenderly/utils/getTokenTransferInfo.ts | 44 +++++++++++ apps/cowswap-frontend/yarn.lock | 11 +++ 13 files changed, 332 insertions(+), 92 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/tenderly/hooks/useBundleSimulation.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/states/simulation.ts delete mode 100644 apps/cowswap-frontend/src/modules/tenderly/states/simulationLink.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts diff --git a/apps/cowswap-frontend/.env b/apps/cowswap-frontend/.env index dd31b8b60e..87e5d5cea2 100644 --- a/apps/cowswap-frontend/.env +++ b/apps/cowswap-frontend/.env @@ -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= \ No newline at end of file diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index cac1a34514..7052ed3eff 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -27,7 +27,9 @@ "last 1 safari version" ] }, - "dependencies": {}, + "dependencies": { + "@covalenthq/client-sdk": "^2.1.1" + }, "devDependencies": {}, "nx": {} -} \ No newline at end of file +} 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 d0e1862643..9ce5d4c178 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -1,10 +1,11 @@ -import { useCallback, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import ICON_HOOK from '@cowprotocol/assets/cow-swap/hook.svg' import { BannerOrientation, DismissableInlineBanner } from '@cowprotocol/ui' import styled from 'styled-components/macro' +import { useHooks } from 'modules/hooksStore/hooks/useHooks' import { SwapWidget } from 'modules/swap' import { useIsSellNative } from 'modules/trade' @@ -13,6 +14,7 @@ import { useSetupHooksStoreOrderParams } from '../../hooks/useSetupHooksStoreOrd import { HookRegistryList } from '../HookRegistryList' import { PostHookButton } from '../PostHookButton' import { PreHookButton } from '../PreHookButton' +import { useTenderlyBundleSimulate } from 'modules/tenderly/hooks/useBundleSimulation' type HookPosition = 'pre' | 'post' @@ -28,9 +30,17 @@ const TradeWidgetWrapper = styled.div<{ visible$: boolean }>` export function HooksStoreWidget() { const [selectedHookPosition, setSelectedHookPosition] = useState(null) const [hookToEdit, setHookToEdit] = useState(undefined) + const hooks = useHooks() + const tenderlyBundleSimulate = useTenderlyBundleSimulate() const isNativeSell = useIsSellNative() + useEffect(() => { + const preHooks = hooks.preHooks.map((hook) => hook.hookDetails.hook) + const postHooks = hooks.postHooks.map((hook) => hook.hookDetails.hook) + tenderlyBundleSimulate({ preHooks, postHooks }) + }, [hooks]) + const onDismiss = useCallback(() => { setSelectedHookPosition(null) setHookToEdit(undefined) 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 92add9f383..0529852532 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -9,6 +9,8 @@ import { InfoTooltip } from '@cowprotocol/ui' import { Edit2, Trash2, ExternalLink as ExternalLinkIcon } from 'react-feather' import SVG from 'react-inlinesvg' +import { useHookSimulationData } from 'modules/tenderly/states/simulation' + import * as styledEl from './styled' import { TenderlySimulate } from '../../containers/TenderlySimulate' @@ -25,7 +27,7 @@ interface HookItemProp { } // TODO: remove once a tenderly bundle simulation is ready -const isBundleSimulationReady = false +const isBundleSimulationReady = true export function AppliedHookItem({ account, @@ -36,20 +38,13 @@ 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 { simulationPassed, tenderlySimulationLink, isSimulationSuccessful } = useHookSimulationData(hookDetails.hook) + 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.' - // TODO: Placeholder for Tenderly simulation URL; replace with actual logic when available - const tenderlySimulationUrl = '' // e.g., 'https://tenderly.co/simulation/12345' - - // TODO: Determine if simulation passed or failed - const isSimulationSuccessful = simulationPassed - return ( @@ -78,8 +73,8 @@ export function AppliedHookItem({ ) : ( )} - {tenderlySimulationUrl ? ( - + {tenderlySimulationLink ? ( + {simulationStatus} diff --git a/apps/cowswap-frontend/src/modules/tenderly/const.ts b/apps/cowswap-frontend/src/modules/tenderly/const.ts index c292f16e95..30f5da1355 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/const.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/const.ts @@ -1,13 +1,25 @@ -import { providers } from 'ethers' +import { SupportedChainId } from '@cowprotocol/cow-sdk' + +import { Chain, ChainName } from '@covalenthq/client-sdk' -export const TENDERLY_TESTNET_PROVIDER = new providers.JsonRpcProvider(process.env.TENDERLY_VNET_RPC) // 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 = 'yvesfracari' -const TENDERLY_PROJECT_NAME = 'personal' +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.MAINNET]: ChainName.ETH_MAINNET, + [SupportedChainId.SEPOLIA]: ChainName.ETH_SEPOLIA, + [SupportedChainId.GNOSIS_CHAIN]: ChainName.GNOSIS_MAINNET, + [SupportedChainId.ARBITRUM_ONE]: ChainName.ARBITRUM_MAINNET, +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useBundleSimulation.ts new file mode 100644 index 0000000000..1357f922d7 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useBundleSimulation.ts @@ -0,0 +1,58 @@ +import { useSetAtom } from 'jotai' +import { useCallback } from 'react' + +import { useWalletInfo } from '@cowprotocol/wallet' + +import { bundleSimulation, PostBundleSimulationParams } from '../utils/bundleSimulation' +import { checkBundleSimulationError } from '../utils/checkBundleSimulationError' +import { generateNewSimulationData, generateSimulationDataToError, simulationAtom } from '../states/simulation' +import { useTopTokenHolders } from './useTopTokenHolders' +import { getTokenTransferInfo } from '../utils/getTokenTransferInfo' +import { useOrderParams } from 'modules/hooksStore/hooks/useOrderParams' +import { useTokenContract } from 'common/hooks/useContract' + +export function useTenderlyBundleSimulate(): ( + params: Pick, +) => Promise { + const { account, chainId } = useWalletInfo() + const orderParams = useOrderParams() + const tokenSell = useTokenContract(orderParams?.sellTokenAddress) + const tokenBuy = useTokenContract(orderParams?.buyTokenAddress) + const setSimulationData = useSetAtom(simulationAtom) + + const { data: buyTokenTopHolders } = useTopTokenHolders({ tokenAddress: tokenBuy?.address, chainId }) + + return useCallback( + async (params) => { + if (params.postHooks.length === 0 && params.preHooks.length === 0) return + if (!account || !buyTokenTopHolders || !tokenBuy || !orderParams || !tokenSell) { + return + } + + const tokenBuyTransferInfo = getTokenTransferInfo({ + tokenHolders: buyTokenTopHolders, + amountToTransfer: orderParams.buyAmount, + }) + + const paramsComplete = { + ...params, + tokenBuy, + tokenBuyTransferInfo, + orderParams, + tokenSell, + account, + chainId, + } + + const response = await bundleSimulation(paramsComplete) + + const newSimulationData = checkBundleSimulationError(response) + ? generateSimulationDataToError(paramsComplete) + : generateNewSimulationData(response, paramsComplete) + + setSimulationData(newSimulationData) + return + }, + [account, chainId, buyTokenTopHolders, setSimulationData, tokenBuy], + ) +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts new file mode 100644 index 0000000000..c7f49dcf9c --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts @@ -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)) +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/states/simulation.ts b/apps/cowswap-frontend/src/modules/tenderly/states/simulation.ts new file mode 100644 index 0000000000..98bc01bfbb --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/states/simulation.ts @@ -0,0 +1,70 @@ +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 interface SimulationData { + tenderlySimulationLink: string + simulationPassed: boolean + isSimulationSuccessful: boolean +} + +export const simulationAtom = atom>({}) + +export const EMPTY_STATE = { + tenderlySimulationLink: '', + simulationPassed: false, + isSimulationSuccessful: false, +} + +export function useHookSimulationData(hook: CowHook) { + const simulationsValues = useAtomValue(simulationAtom) + + return simulationsValues[getHookSimulationKey(hook)] || EMPTY_STATE +} + +export function getHookSimulationKey(hook: CowHook) { + return [hook.target, hook.callData, hook.gasLimit].join(':') +} + +export function generateSimulationDataToError(postParams: PostBundleSimulationParams): Record { + const preHooksKeys = postParams.preHooks.map(getHookSimulationKey) + const postHooksKeys = postParams.postHooks.map(getHookSimulationKey) + const hooksKeys = [...preHooksKeys, ...postHooksKeys] + + return hooksKeys.reduce( + (acc, key) => ({ + ...acc, + [key]: { tenderlySimulationLink: '', simulationPassed: false, isSimulationSuccessful: false }, + }), + {}, + ) +} + +export function generateNewSimulationData( + bundleSimulationResponse: TenderlyBundleSimulationResponse, + postParams: PostBundleSimulationParams, +): Record { + const preHooksKeys = postParams.preHooks.map(getHookSimulationKey) + const postHooksKeys = postParams.postHooks.map(getHookSimulationKey) + + const preHooksData = bundleSimulationResponse.simulation_results.slice(0, preHooksKeys.length).map((simulation) => ({ + tenderlySimulationLink: getSimulationLink(simulation.simulation.id), + simulationPassed: true, + isSimulationSuccessful: simulation.simulation.status, + })) + + const postHooksData = bundleSimulationResponse.simulation_results.slice(preHooksKeys.length).map((simulation) => ({ + tenderlySimulationLink: getSimulationLink(simulation.simulation.id), + simulationPassed: true, + isSimulationSuccessful: simulation.simulation.status, + })) + + return { + ...preHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: preHooksData[index] }), {}), + ...postHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: postHooksData[index] }), {}), + } +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/states/simulationLink.ts b/apps/cowswap-frontend/src/modules/tenderly/states/simulationLink.ts deleted file mode 100644 index e5df8013d3..0000000000 --- a/apps/cowswap-frontend/src/modules/tenderly/states/simulationLink.ts +++ /dev/null @@ -1,41 +0,0 @@ -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/types.ts b/apps/cowswap-frontend/src/modules/tenderly/types.ts index 8bc3f78d2b..8cbd9b6057 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/types.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/types.ts @@ -1,3 +1,34 @@ +export interface TokenHolderItem { + contract_decimals: number + contract_name: string + contract_ticker_symbol: string + contract_address: string + supports_erc: string[] + logo_url: string + address: string + balance: string + total_supply: string + block_height: number +} + +export interface TokenHoldersResponse { + data: { + updated_at: string + chain_id: number + chain_name: string + items: TokenHolderItem[] + pagination: { + has_more: boolean + page_number: number + page_size: number + total_count: number + } + } + error: boolean + error_message: null | string + error_code: null | number +} + export interface TenderlyBundleSimulationResponse { simulation_results: TenderlySimulation[] } diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts index 26bb5d418f..74d35c4f21 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts @@ -2,9 +2,11 @@ import { Erc20 } from '@cowprotocol/abis' import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS, SupportedChainId } from '@cowprotocol/cow-sdk' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { BigNumberish } from 'ethers' + import { CowHook, HookDappOrderParams } from 'modules/hooksStore/types/hooks' -import { TENDERLY_API_BASE_ENDPOINT } from '../const' +import { TENDERLY_API_BASE_ENDPOINT, TENDERLY_API_KEY } from '../const' import { SimulationError, TenderlyBundleSimulationResponse, TenderlySimulatePayload } from '../types' export interface GetTransferTenderlySimulationInput { @@ -16,6 +18,10 @@ export interface GetTransferTenderlySimulationInput { slotOverride?: string } +export type TokenBuyTransferInfo = { + sender: string + amount: CurrencyAmount +}[] export interface PostBundleSimulationParams { account: string chainId: SupportedChainId @@ -24,17 +30,19 @@ export interface PostBundleSimulationParams { preHooks: CowHook[] postHooks: CowHook[] orderParams: HookDappOrderParams - slotOverride: string + tokenBuyTransferInfo: TokenBuyTransferInfo } export const bundleSimulation = async ( params: PostBundleSimulationParams, ): Promise => { + const input = getBundleTenderlySimulationInput(params) + console.log({ TENDERLY_API_KEY }) const response = await fetch(`${TENDERLY_API_BASE_ENDPOINT}/simulate-bundle`, { method: 'POST', - body: JSON.stringify(getBundleTenderlySimulationInput(params)), + body: JSON.stringify(input), headers: { - 'X-Access-Key': process.env.TENDERLY_API_KEY as string, + 'X-Access-Key': TENDERLY_API_KEY, }, }).then((res) => res.json()) @@ -57,14 +65,24 @@ export function getCoWHookTenderlySimulationInput( save_if_fails: true, } } +// TODO: check if there is a function to do this conversion +function currencyAmountToBigNumberish(amount: CurrencyAmount): BigNumberish { + // CurrencyAmount already stores the amount as a fraction internally + const fraction = amount.asFraction + + // Get the numerator and denominator as BigInts + const numerator = BigInt(fraction.numerator.toString()) + const denominator = BigInt(fraction.denominator.toString()) -function currencyAmountToHexUint256(amount: CurrencyAmount) { - const valueAsBigInt = BigInt(amount.quotient.toString()) + // Get the decimals of the currency + const decimals = BigInt(amount.currency.decimals) - let hexString = valueAsBigInt.toString(16) + // Perform the division + const scaledNumerator = numerator * 10n ** decimals + const result = scaledNumerator / denominator - hexString = hexString.padStart(64, '0') - return '0x' + hexString + // Convert the result to a string + return result.toString() } export function getTransferTenderlySimulationInput({ @@ -73,19 +91,11 @@ export function getTransferTenderlySimulationInput({ 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), - }, - }, - } - : {} + const callData = token.interface.encodeFunctionData('transfer', [ + receiver, + currencyAmountToBigNumberish(currencyAmount), + ]) return { input: callData, @@ -96,7 +106,6 @@ export function getTransferTenderlySimulationInput({ network_id: chainId.toString(), save: true, save_if_fails: true, - state_objects, } } @@ -108,9 +117,10 @@ export function getBundleTenderlySimulationInput({ preHooks, postHooks, orderParams, - slotOverride, + tokenBuyTransferInfo, }: PostBundleSimulationParams): { simulations: TenderlySimulatePayload[] } { const settlementAddress = COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId] + console.log({ settlementAddress }) const preHooksSimulations = preHooks.map((hook) => getCoWHookTenderlySimulationInput(settlementAddress, hook, chainId), ) @@ -126,14 +136,15 @@ export function getBundleTenderlySimulationInput({ chainId, }) - const buyTokenSimulation = getTransferTenderlySimulationInput({ - currencyAmount: orderParams.sellAmount, - from: COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId], - receiver: orderParams.receiver, - token: tokenBuy, - chainId, - slotOverride, - }) + const buyTokenTransfers = tokenBuyTransferInfo.map((transferInfo) => + getTransferTenderlySimulationInput({ + currencyAmount: transferInfo.amount, + from: transferInfo.sender, + receiver: account, + token: tokenBuy, + chainId, + }), + ) - return { simulations: [...preHooksSimulations, sellTokenTransfer, buyTokenSimulation, ...postHooksSimulations] } + return { simulations: [...preHooksSimulations, sellTokenTransfer, ...buyTokenTransfers, ...postHooksSimulations] } } diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts new file mode 100644 index 0000000000..bfdb603f16 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts @@ -0,0 +1,44 @@ +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' + +import { TokenBuyTransferInfo } from './bundleSimulation' + +import { TokenHolderItem } from '../types' + +export function getTokenTransferInfo({ + tokenHolders, + amountToTransfer, +}: { + tokenHolders: TokenHolderItem[] + amountToTransfer: CurrencyAmount +}): TokenBuyTransferInfo { + let sum = CurrencyAmount.fromRawAmount(amountToTransfer.currency, '0') + const result: TokenBuyTransferInfo = [] + + if (!tokenHolders) { + return result + } + + for (const tokenHolder of tokenHolders) { + // skip token holders with no address or balance + if (!tokenHolder.address || !tokenHolder.balance) continue + + const tokenHolderAmount = CurrencyAmount.fromRawAmount(amountToTransfer.currency, tokenHolder.balance.toString()) + const sumWithTokenHolder = sum.add(tokenHolderAmount) + + if (sumWithTokenHolder.greaterThan(amountToTransfer) || sumWithTokenHolder.equalTo(amountToTransfer)) { + const remainingAmount = amountToTransfer.subtract(sum) + result.push({ + sender: tokenHolder.address, + amount: remainingAmount, + }) + break + } + sum = sum.add(tokenHolderAmount) + result.push({ + sender: tokenHolder.address, + amount: tokenHolderAmount, + }) + } + + return result +} diff --git a/apps/cowswap-frontend/yarn.lock b/apps/cowswap-frontend/yarn.lock index fb57ccd13a..57eee7f609 100644 --- a/apps/cowswap-frontend/yarn.lock +++ b/apps/cowswap-frontend/yarn.lock @@ -2,3 +2,14 @@ # yarn lockfile v1 +"@covalenthq/client-sdk@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@covalenthq/client-sdk/-/client-sdk-2.1.1.tgz#d49a70f67f9f03747acbe28455240ee38b9ecc90" + integrity sha512-wrtb6sn5cUOTOTD+GbE1Xi92b2Q2Wd76roK2wwUMdbOQ591ucahgSPfaJEgZY29nocLc3wDV2vhR11fNX8nZxA== + dependencies: + big.js "^6.2.1" + +big.js@^6.2.1: + version "6.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.2.tgz#be3bb9ac834558b53b099deef2a1d06ac6368e1a" + integrity sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ== From e1b5c4c590ecfdbdd17126e3f2718f89a3220b96 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Thu, 3 Oct 2024 14:23:03 -0300 Subject: [PATCH 03/31] refactor: change bundle simulation to use SWR --- .../containers/HooksStoreWidget/index.tsx | 12 +--- .../hooksStore/pure/AppliedHookItem/index.tsx | 28 +++++--- .../pure/AppliedHookItem/styled.tsx | 16 +++++ .../tenderly/hooks/useBundleSimulation.ts | 58 --------------- .../hooks/useTenderlyBundleSimulation.ts | 56 +++++++++++++++ .../src/modules/tenderly/states/simulation.ts | 70 ------------------- .../src/modules/tenderly/types.ts | 5 ++ .../tenderly/utils/bundleSimulation.ts | 24 +++---- .../tenderly/utils/generateSimulationData.ts | 41 +++++++++++ 9 files changed, 147 insertions(+), 163 deletions(-) delete mode 100644 apps/cowswap-frontend/src/modules/tenderly/hooks/useBundleSimulation.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts delete mode 100644 apps/cowswap-frontend/src/modules/tenderly/states/simulation.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts 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 9ce5d4c178..d0e1862643 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HooksStoreWidget/index.tsx @@ -1,11 +1,10 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useState } from 'react' import ICON_HOOK from '@cowprotocol/assets/cow-swap/hook.svg' import { BannerOrientation, DismissableInlineBanner } from '@cowprotocol/ui' import styled from 'styled-components/macro' -import { useHooks } from 'modules/hooksStore/hooks/useHooks' import { SwapWidget } from 'modules/swap' import { useIsSellNative } from 'modules/trade' @@ -14,7 +13,6 @@ import { useSetupHooksStoreOrderParams } from '../../hooks/useSetupHooksStoreOrd import { HookRegistryList } from '../HookRegistryList' import { PostHookButton } from '../PostHookButton' import { PreHookButton } from '../PreHookButton' -import { useTenderlyBundleSimulate } from 'modules/tenderly/hooks/useBundleSimulation' type HookPosition = 'pre' | 'post' @@ -30,17 +28,9 @@ const TradeWidgetWrapper = styled.div<{ visible$: boolean }>` export function HooksStoreWidget() { const [selectedHookPosition, setSelectedHookPosition] = useState(null) const [hookToEdit, setHookToEdit] = useState(undefined) - const hooks = useHooks() - const tenderlyBundleSimulate = useTenderlyBundleSimulate() const isNativeSell = useIsSellNative() - useEffect(() => { - const preHooks = hooks.preHooks.map((hook) => hook.hookDetails.hook) - const postHooks = hooks.postHooks.map((hook) => hook.hookDetails.hook) - tenderlyBundleSimulate({ preHooks, postHooks }) - }, [hooks]) - const onDismiss = useCallback(() => { setSelectedHookPosition(null) setHookToEdit(undefined) 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 0529852532..dea37b1c8e 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -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' @@ -9,7 +11,7 @@ import { InfoTooltip } from '@cowprotocol/ui' import { Edit2, Trash2, ExternalLink as ExternalLinkIcon } from 'react-feather' import SVG from 'react-inlinesvg' -import { useHookSimulationData } from 'modules/tenderly/states/simulation' +import { useTenderlyBundleSimulateSWR } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' import * as styledEl from './styled' @@ -26,7 +28,7 @@ interface HookItemProp { index: number } -// TODO: remove once a tenderly bundle simulation is ready +// TODO: refactor tu use single simulation as fallback const isBundleSimulationReady = true export function AppliedHookItem({ @@ -38,10 +40,15 @@ export function AppliedHookItem({ removeHook, index, }: HookItemProp) { - const { simulationPassed, tenderlySimulationLink, isSimulationSuccessful } = useHookSimulationData(hookDetails.hook) + const { isValidating, data } = useTenderlyBundleSimulateSWR() + + const simulationData = useMemo(() => { + if (!data) return + return data[hookDetails.uuid] + }, [data, hookDetails.uuid]) - const simulationStatus = simulationPassed ? 'Simulation successful' : 'Simulation failed' - const simulationTooltip = 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.' @@ -55,6 +62,7 @@ export function AppliedHookItem({ {index + 1} {dapp.name} {dapp.name} + {isValidating && } editHook(hookDetails.uuid)}> @@ -66,15 +74,15 @@ export function AppliedHookItem({ - {account && isBundleSimulationReady && ( - - {isSimulationSuccessful ? ( + {account && isBundleSimulationReady && simulationData && ( + + {simulationData.simulationPassed ? ( ) : ( )} - {tenderlySimulationLink ? ( - + {simulationData.tenderlySimulationLink ? ( + {simulationStatus} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx index ac71a21f81..4f62423401 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/styled.tsx @@ -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); + } + } +` diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useBundleSimulation.ts deleted file mode 100644 index 1357f922d7..0000000000 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useBundleSimulation.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useSetAtom } from 'jotai' -import { useCallback } from 'react' - -import { useWalletInfo } from '@cowprotocol/wallet' - -import { bundleSimulation, PostBundleSimulationParams } from '../utils/bundleSimulation' -import { checkBundleSimulationError } from '../utils/checkBundleSimulationError' -import { generateNewSimulationData, generateSimulationDataToError, simulationAtom } from '../states/simulation' -import { useTopTokenHolders } from './useTopTokenHolders' -import { getTokenTransferInfo } from '../utils/getTokenTransferInfo' -import { useOrderParams } from 'modules/hooksStore/hooks/useOrderParams' -import { useTokenContract } from 'common/hooks/useContract' - -export function useTenderlyBundleSimulate(): ( - params: Pick, -) => Promise { - const { account, chainId } = useWalletInfo() - const orderParams = useOrderParams() - const tokenSell = useTokenContract(orderParams?.sellTokenAddress) - const tokenBuy = useTokenContract(orderParams?.buyTokenAddress) - const setSimulationData = useSetAtom(simulationAtom) - - const { data: buyTokenTopHolders } = useTopTokenHolders({ tokenAddress: tokenBuy?.address, chainId }) - - return useCallback( - async (params) => { - if (params.postHooks.length === 0 && params.preHooks.length === 0) return - if (!account || !buyTokenTopHolders || !tokenBuy || !orderParams || !tokenSell) { - return - } - - const tokenBuyTransferInfo = getTokenTransferInfo({ - tokenHolders: buyTokenTopHolders, - amountToTransfer: orderParams.buyAmount, - }) - - const paramsComplete = { - ...params, - tokenBuy, - tokenBuyTransferInfo, - orderParams, - tokenSell, - account, - chainId, - } - - const response = await bundleSimulation(paramsComplete) - - const newSimulationData = checkBundleSimulationError(response) - ? generateSimulationDataToError(paramsComplete) - : generateNewSimulationData(response, paramsComplete) - - setSimulationData(newSimulationData) - return - }, - [account, chainId, buyTokenTopHolders, setSimulationData, tokenBuy], - ) -} diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts new file mode 100644 index 0000000000..b5f464aeed --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -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) +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/states/simulation.ts b/apps/cowswap-frontend/src/modules/tenderly/states/simulation.ts deleted file mode 100644 index 98bc01bfbb..0000000000 --- a/apps/cowswap-frontend/src/modules/tenderly/states/simulation.ts +++ /dev/null @@ -1,70 +0,0 @@ -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 interface SimulationData { - tenderlySimulationLink: string - simulationPassed: boolean - isSimulationSuccessful: boolean -} - -export const simulationAtom = atom>({}) - -export const EMPTY_STATE = { - tenderlySimulationLink: '', - simulationPassed: false, - isSimulationSuccessful: false, -} - -export function useHookSimulationData(hook: CowHook) { - const simulationsValues = useAtomValue(simulationAtom) - - return simulationsValues[getHookSimulationKey(hook)] || EMPTY_STATE -} - -export function getHookSimulationKey(hook: CowHook) { - return [hook.target, hook.callData, hook.gasLimit].join(':') -} - -export function generateSimulationDataToError(postParams: PostBundleSimulationParams): Record { - const preHooksKeys = postParams.preHooks.map(getHookSimulationKey) - const postHooksKeys = postParams.postHooks.map(getHookSimulationKey) - const hooksKeys = [...preHooksKeys, ...postHooksKeys] - - return hooksKeys.reduce( - (acc, key) => ({ - ...acc, - [key]: { tenderlySimulationLink: '', simulationPassed: false, isSimulationSuccessful: false }, - }), - {}, - ) -} - -export function generateNewSimulationData( - bundleSimulationResponse: TenderlyBundleSimulationResponse, - postParams: PostBundleSimulationParams, -): Record { - const preHooksKeys = postParams.preHooks.map(getHookSimulationKey) - const postHooksKeys = postParams.postHooks.map(getHookSimulationKey) - - const preHooksData = bundleSimulationResponse.simulation_results.slice(0, preHooksKeys.length).map((simulation) => ({ - tenderlySimulationLink: getSimulationLink(simulation.simulation.id), - simulationPassed: true, - isSimulationSuccessful: simulation.simulation.status, - })) - - const postHooksData = bundleSimulationResponse.simulation_results.slice(preHooksKeys.length).map((simulation) => ({ - tenderlySimulationLink: getSimulationLink(simulation.simulation.id), - simulationPassed: true, - isSimulationSuccessful: simulation.simulation.status, - })) - - return { - ...preHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: preHooksData[index] }), {}), - ...postHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: postHooksData[index] }), {}), - } -} diff --git a/apps/cowswap-frontend/src/modules/tenderly/types.ts b/apps/cowswap-frontend/src/modules/tenderly/types.ts index 8cbd9b6057..964f640c5e 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/types.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/types.ts @@ -1,3 +1,8 @@ +export interface SimulationData { + tenderlySimulationLink: string + simulationPassed: boolean +} + export interface TokenHolderItem { contract_decimals: number contract_name: string diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts index 74d35c4f21..17ce18e302 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts @@ -4,7 +4,7 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { BigNumberish } from 'ethers' -import { CowHook, HookDappOrderParams } from 'modules/hooksStore/types/hooks' +import { CowHook, CowHookDetailsSerialized, HookDappOrderParams } from 'modules/hooksStore/types/hooks' import { TENDERLY_API_BASE_ENDPOINT, TENDERLY_API_KEY } from '../const' import { SimulationError, TenderlyBundleSimulationResponse, TenderlySimulatePayload } from '../types' @@ -27,8 +27,8 @@ export interface PostBundleSimulationParams { chainId: SupportedChainId tokenSell: Erc20 tokenBuy: Erc20 - preHooks: CowHook[] - postHooks: CowHook[] + preHooks: CowHookDetailsSerialized[] + postHooks: CowHookDetailsSerialized[] orderParams: HookDappOrderParams tokenBuyTransferInfo: TokenBuyTransferInfo } @@ -37,7 +37,6 @@ export const bundleSimulation = async ( params: PostBundleSimulationParams, ): Promise => { const input = getBundleTenderlySimulationInput(params) - console.log({ TENDERLY_API_KEY }) const response = await fetch(`${TENDERLY_API_BASE_ENDPOINT}/simulate-bundle`, { method: 'POST', body: JSON.stringify(input), @@ -74,12 +73,7 @@ function currencyAmountToBigNumberish(amount: CurrencyAmount): BigNumb const numerator = BigInt(fraction.numerator.toString()) const denominator = BigInt(fraction.denominator.toString()) - // Get the decimals of the currency - const decimals = BigInt(amount.currency.decimals) - - // Perform the division - const scaledNumerator = numerator * 10n ** decimals - const result = scaledNumerator / denominator + const result = numerator / denominator // Convert the result to a string return result.toString() @@ -100,7 +94,7 @@ export function getTransferTenderlySimulationInput({ return { input: callData, to: token.address, - gas: 100000, // TODO: this should be calculated based on the token + gas: 100000, // TODO: Check if this is relevant from, gas_price: '0', network_id: chainId.toString(), @@ -120,14 +114,16 @@ export function getBundleTenderlySimulationInput({ tokenBuyTransferInfo, }: PostBundleSimulationParams): { simulations: TenderlySimulatePayload[] } { const settlementAddress = COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId] - console.log({ settlementAddress }) const preHooksSimulations = preHooks.map((hook) => - getCoWHookTenderlySimulationInput(settlementAddress, hook, chainId), + getCoWHookTenderlySimulationInput(settlementAddress, hook.hookDetails.hook, chainId), ) const postHooksSimulations = postHooks.map((hook) => - getCoWHookTenderlySimulationInput(settlementAddress, hook, chainId), + getCoWHookTenderlySimulationInput(settlementAddress, hook.hookDetails.hook, chainId), ) + // If there are no post hooks, we don't need to simulate the transfer + if (postHooks.length === 0) return { simulations: preHooksSimulations } + const sellTokenTransfer = getTransferTenderlySimulationInput({ currencyAmount: orderParams.sellAmount, from: account, diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts new file mode 100644 index 0000000000..c6f7c02316 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts @@ -0,0 +1,41 @@ +import { PostBundleSimulationParams } from './bundleSimulation' + +import { getSimulationLink } from '../const' +import { SimulationData, TenderlyBundleSimulationResponse } from '../types' + +export function generateSimulationDataToError(postParams: PostBundleSimulationParams): Record { + 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]: { tenderlySimulationLink: '', simulationPassed: false }, + }), + {}, + ) +} + +export function generateNewSimulationData( + bundleSimulationResponse: TenderlyBundleSimulationResponse, + postParams: PostBundleSimulationParams, +): Record { + const preHooksKeys = postParams.preHooks.map(({ hookDetails }) => hookDetails.uuid) + const postHooksKeys = postParams.postHooks.map(({ hookDetails }) => hookDetails.uuid) + + const preHooksData = bundleSimulationResponse.simulation_results.slice(0, preHooksKeys.length).map((simulation) => ({ + tenderlySimulationLink: getSimulationLink(simulation.simulation.id), + simulationPassed: simulation.simulation.status, + })) + + const postHooksData = bundleSimulationResponse.simulation_results.slice(preHooksKeys.length).map((simulation) => ({ + tenderlySimulationLink: getSimulationLink(simulation.simulation.id), + simulationPassed: simulation.simulation.status, + })) + + return { + ...preHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: preHooksData[index] }), {}), + ...postHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: postHooksData[index] }), {}), + } +} From 4bae430b616c9086a9f75a25bdb9f83277c5fa9d Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Thu, 3 Oct 2024 14:42:16 -0300 Subject: [PATCH 04/31] chore: consider custom recipient --- .../src/modules/tenderly/utils/bundleSimulation.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts index 17ce18e302..2119ac45ae 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts @@ -124,6 +124,8 @@ export function getBundleTenderlySimulationInput({ // If there are no post hooks, we don't need to simulate the transfer if (postHooks.length === 0) return { simulations: preHooksSimulations } + const receiver = postHooks[0].hookDetails.recipientOverride || orderParams.receiver + const sellTokenTransfer = getTransferTenderlySimulationInput({ currencyAmount: orderParams.sellAmount, from: account, @@ -136,7 +138,7 @@ export function getBundleTenderlySimulationInput({ getTransferTenderlySimulationInput({ currencyAmount: transferInfo.amount, from: transferInfo.sender, - receiver: account, + receiver, token: tokenBuy, chainId, }), From 8bed508b641ca7027a694c6b237d0b24b0fa4d13 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Fri, 11 Oct 2024 16:30:09 -0300 Subject: [PATCH 05/31] chore: remove goldrush sdk --- apps/cowswap-frontend/package.json | 4 +--- apps/cowswap-frontend/src/modules/tenderly/const.ts | 12 +++++------- apps/cowswap-frontend/yarn.lock | 11 ----------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/apps/cowswap-frontend/package.json b/apps/cowswap-frontend/package.json index 8947f0c800..9f3e0e65ec 100644 --- a/apps/cowswap-frontend/package.json +++ b/apps/cowswap-frontend/package.json @@ -27,9 +27,7 @@ "last 1 safari version" ] }, - "dependencies": { - "@covalenthq/client-sdk": "^2.1.1" - }, + "dependencies": {}, "devDependencies": {}, "nx": {} } diff --git a/apps/cowswap-frontend/src/modules/tenderly/const.ts b/apps/cowswap-frontend/src/modules/tenderly/const.ts index 30f5da1355..2adf410b20 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/const.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/const.ts @@ -1,7 +1,5 @@ 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 @@ -17,9 +15,9 @@ export const getSimulationLink = (simulationId: string): string => { 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.MAINNET]: ChainName.ETH_MAINNET, - [SupportedChainId.SEPOLIA]: ChainName.ETH_SEPOLIA, - [SupportedChainId.GNOSIS_CHAIN]: ChainName.GNOSIS_MAINNET, - [SupportedChainId.ARBITRUM_ONE]: ChainName.ARBITRUM_MAINNET, +export const GOLD_RUSH_CLIENT_NETWORK_MAPPING: Record = { + [SupportedChainId.MAINNET]: 'eth-mainnet', + [SupportedChainId.SEPOLIA]: 'eth-sepolia', + [SupportedChainId.GNOSIS_CHAIN]: 'gnosis-mainnet', + [SupportedChainId.ARBITRUM_ONE]: 'arbitrum-mainnet', } diff --git a/apps/cowswap-frontend/yarn.lock b/apps/cowswap-frontend/yarn.lock index 57eee7f609..fb57ccd13a 100644 --- a/apps/cowswap-frontend/yarn.lock +++ b/apps/cowswap-frontend/yarn.lock @@ -2,14 +2,3 @@ # yarn lockfile v1 -"@covalenthq/client-sdk@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@covalenthq/client-sdk/-/client-sdk-2.1.1.tgz#d49a70f67f9f03747acbe28455240ee38b9ecc90" - integrity sha512-wrtb6sn5cUOTOTD+GbE1Xi92b2Q2Wd76roK2wwUMdbOQ591ucahgSPfaJEgZY29nocLc3wDV2vhR11fNX8nZxA== - dependencies: - big.js "^6.2.1" - -big.js@^6.2.1: - version "6.2.2" - resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.2.tgz#be3bb9ac834558b53b099deef2a1d06ac6368e1a" - integrity sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ== From 12e0ee55d680b2f31b09794e4406fd398f072aea Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Fri, 11 Oct 2024 16:30:30 -0300 Subject: [PATCH 06/31] fix: error on post hooks get simulation --- .../src/modules/tenderly/utils/generateSimulationData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts index c6f7c02316..abf14b2b1f 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts @@ -29,7 +29,7 @@ export function generateNewSimulationData( simulationPassed: simulation.simulation.status, })) - const postHooksData = bundleSimulationResponse.simulation_results.slice(preHooksKeys.length).map((simulation) => ({ + const postHooksData = bundleSimulationResponse.simulation_results.slice(-postHooksKeys.length).map((simulation) => ({ tenderlySimulationLink: getSimulationLink(simulation.simulation.id), simulationPassed: simulation.simulation.status, })) From 450e330bffbbad177eaf22924e761427a2d7f24b Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Fri, 11 Oct 2024 18:07:36 -0300 Subject: [PATCH 07/31] refactor: use bff on bundle simulation feature --- apps/cowswap-frontend/.env | 8 +- .../hooksStore/pure/AppliedHookItem/index.tsx | 14 +- .../src/modules/tenderly/const.ts | 8 - .../hooks/useTenderlyBundleSimulation.ts | 39 +- .../tenderly/hooks/useTopTokenHolders.ts | 18 +- .../src/modules/tenderly/types.ts | 607 +----------------- .../tenderly/utils/bundleSimulation.ts | 47 +- .../utils/checkBundleSimulationError.ts | 7 - .../tenderly/utils/generateSimulationData.ts | 21 +- .../tenderly/utils/getTokenTransferInfo.ts | 6 +- 10 files changed, 71 insertions(+), 704 deletions(-) delete mode 100644 apps/cowswap-frontend/src/modules/tenderly/utils/checkBundleSimulationError.ts diff --git a/apps/cowswap-frontend/.env b/apps/cowswap-frontend/.env index 87e5d5cea2..33d6b115c6 100644 --- a/apps/cowswap-frontend/.env +++ b/apps/cowswap-frontend/.env @@ -135,10 +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_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= \ No newline at end of file +# REACT_APP_PATH_REGEX_ENS="/ipfs" \ No newline at end of file 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 dea37b1c8e..089a33e2b3 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -47,8 +47,8 @@ export function AppliedHookItem({ return data[hookDetails.uuid] }, [data, hookDetails.uuid]) - const simulationStatus = simulationData?.simulationPassed ? 'Simulation successful' : 'Simulation failed' - const simulationTooltip = simulationData?.simulationPassed + 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.' @@ -60,7 +60,7 @@ export function AppliedHookItem({ {index + 1} - {dapp.name} + {dapp.name} {dapp.name} {isValidating && } @@ -75,14 +75,14 @@ export function AppliedHookItem({ {account && isBundleSimulationReady && simulationData && ( - - {simulationData.simulationPassed ? ( + + {simulationData.status ? ( ) : ( )} - {simulationData.tenderlySimulationLink ? ( - + {simulationData.link ? ( + {simulationStatus} diff --git a/apps/cowswap-frontend/src/modules/tenderly/const.ts b/apps/cowswap-frontend/src/modules/tenderly/const.ts index 2adf410b20..34d6d8aae2 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/const.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/const.ts @@ -1,10 +1,5 @@ import { SupportedChainId } from '@cowprotocol/cow-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 @@ -12,9 +7,6 @@ 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.MAINNET]: 'eth-mainnet', [SupportedChainId.SEPOLIA]: 'eth-sepolia', diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index b5f464aeed..feaef2931c 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -1,10 +1,8 @@ -import { useSetAtom } from 'jotai' -import { useCallback, useMemo } from 'react' +import { useCallback } 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' @@ -19,19 +17,23 @@ export function useTenderlyBundleSimulateSWR() { const orderParams = useOrderParams() const tokenSell = useTokenContract(orderParams?.sellTokenAddress) const tokenBuy = useTokenContract(orderParams?.buyTokenAddress) + const buyAmount = orderParams?.buyAmount - const { data: buyTokenTopHolders } = useTopTokenHolders({ tokenAddress: tokenBuy?.address, chainId }) + const { data: buyTokenTopHolders, isValidating: isTopTokenHoldersValidating } = 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') + if (!account || !buyTokenTopHolders || !tokenBuy || !orderParams || !tokenSell || !buyAmount) { + return generateSimulationDataToError({ postHooks, preHooks }) } const tokenBuyTransferInfo = getTokenTransferInfo({ tokenHolders: buyTokenTopHolders, - amountToTransfer: orderParams.buyAmount, + amountToTransfer: buyAmount, }) const paramsComplete = { @@ -45,12 +47,23 @@ export function useTenderlyBundleSimulateSWR() { chainId, } - const response = await bundleSimulation(paramsComplete) + try { + const response = await bundleSimulation(paramsComplete) + return generateNewSimulationData(response, paramsComplete) + } catch { + return generateSimulationDataToError(paramsComplete) + } + }, [account, chainId, buyTokenTopHolders, tokenBuy, postHooks, preHooks, buyAmount]) - return checkBundleSimulationError(response) - ? generateSimulationDataToError(paramsComplete) - : generateNewSimulationData(response, paramsComplete) - }, [account, chainId, buyTokenTopHolders, tokenBuy, postHooks, preHooks]) + const { data, isValidating: isBundleSimulationLoading } = useSWR( + ['tenderly-bundle-simulation', postHooks, preHooks, orderParams?.sellTokenAddress, orderParams?.buyTokenAddress], + getNewSimulationData, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }, + ) - return useSWR(['tenderly-bundle-simulation', postHooks, preHooks], getNewSimulationData) + return { data, isValidating: isBundleSimulationLoading || isTopTokenHoldersValidating } } diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts index c7f49dcf9c..57e556d9be 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts @@ -1,9 +1,9 @@ +import { BFF_BASE_URL } from '@cowprotocol/common-const' 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' +import { TokenHolder } from '../types' export interface GetTopTokenHoldersParams { tokenAddress?: string @@ -13,17 +13,9 @@ export interface GetTopTokenHoldersParams { 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 + return (await fetch(`${BFF_BASE_URL}/${chainId}/tokens/${tokenAddress}/topHolders`, { + method: 'GET', + }).then((res) => res.json())) as TokenHolder[] } export function useTopTokenHolders(params: GetTopTokenHoldersParams) { diff --git a/apps/cowswap-frontend/src/modules/tenderly/types.ts b/apps/cowswap-frontend/src/modules/tenderly/types.ts index 964f640c5e..bb813e4215 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/types.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/types.ts @@ -1,610 +1,19 @@ -export interface SimulationData { - tenderlySimulationLink: string - simulationPassed: boolean -} - -export interface TokenHolderItem { - contract_decimals: number - contract_name: string - contract_ticker_symbol: string - contract_address: string - supports_erc: string[] - logo_url: string - address: string - balance: string - total_supply: string - block_height: number -} - -export interface TokenHoldersResponse { - data: { - updated_at: string - chain_id: number - chain_name: string - items: TokenHolderItem[] - pagination: { - has_more: boolean - page_number: number - page_size: number - total_count: number - } - } - error: boolean - error_message: null | string - error_code: null | number -} - -export interface TenderlyBundleSimulationResponse { - simulation_results: TenderlySimulation[] -} - -// types were found in Uniswap repository -// https://github.com/Uniswap/governance-seatbelt/blob/e2c6a0b11d1660f3bd934dab0d9df3ca6f90a1a0/types.d.ts#L123 - -type StateObject = { - balance?: string - code?: string - storage?: Record -} - -type ContractObject = { - contractName: string - source: string - sourcePath: string - compiler: { - name: 'solc' - version: string - } - networks: Record< - string, - { - events?: Record - links?: Record - address: string - transactionHash?: string - } - > -} - -export type TenderlySimulatePayload = { - network_id: string - block_number?: number - transaction_index?: number - from: string - to: string +export interface SimulationInput { input: string - gas: number - gas_price?: string - value?: string - simulation_type?: 'full' | 'quick' - save?: boolean - save_if_fails?: boolean - state_objects?: Record - contracts?: ContractObject[] - block_header?: { - number?: string - timestamp?: string - } - generate_access_list?: boolean -} - -// --- Tenderly types, Response --- -// NOTE: These type definitions were autogenerated using https://app.quicktype.io/, so are almost -// certainly not entirely accurate (and they have some interesting type names) - -export interface TenderlySimulation { - transaction: Transaction - simulation: Simulation - contracts: TenderlyContract[] - generated_access_list: GeneratedAccessList[] -} - -interface TenderlyContract { - id: string - contract_id: string - balance: string - network_id: string - public: boolean - export: boolean - verified_by: string - verification_date: null - address: string - contract_name: string - ens_domain: null - type: string - evm_version: string - compiler_version: string - optimizations_used: boolean - optimization_runs: number - libraries: null - data: Data - creation_block: number - creation_tx: string - creator_address: string - created_at: Date - number_of_watches: null - language: string - in_project: boolean - number_of_files: number - standard?: string - standards?: string[] - token_data?: TokenData -} - -interface Data { - main_contract: number - contract_info: ContractInfo[] - abi: ABI[] - raw_abi: null -} - -interface ABI { - type: ABIType - name: string - constant: boolean - anonymous: boolean - inputs: SoltypeElement[] - outputs: Output[] | null -} - -interface SoltypeElement { - name: string - type: SoltypeType - storage_location: StorageLocation - components: SoltypeElement[] | null - offset: number - index: string - indexed: boolean - simple_type?: Type -} - -interface Type { - type: SimpleTypeType -} - -enum SimpleTypeType { - Address = 'address', - Bool = 'bool', - Bytes = 'bytes', - Slice = 'slice', - String = 'string', - Uint = 'uint', -} - -enum StorageLocation { - Calldata = 'calldata', - Default = 'default', - Memory = 'memory', - Storage = 'storage', -} - -enum SoltypeType { - Address = 'address', - Bool = 'bool', - Bytes32 = 'bytes32', - MappingAddressUint256 = 'mapping (address => uint256)', - MappingUint256Uint256 = 'mapping (uint256 => uint256)', - String = 'string', - Tuple = 'tuple', - TypeAddress = 'address[]', - TypeTuple = 'tuple[]', - Uint16 = 'uint16', - Uint256 = 'uint256', - Uint48 = 'uint48', - Uint56 = 'uint56', - Uint8 = 'uint8', -} - -interface Output { - name: string - type: SoltypeType - storage_location: StorageLocation - components: SoltypeElement[] | null - offset: number - index: string - indexed: boolean - simple_type?: SimpleType -} - -interface SimpleType { - type: SimpleTypeType - nested_type?: Type -} - -enum ABIType { - Constructor = 'constructor', - Event = 'event', - Function = 'function', -} - -interface ContractInfo { - id: number - path: string - name: string - source: string -} - -interface TokenData { - symbol: string - name: string - decimals: number -} - -interface GeneratedAccessList { - address: string - storage_keys: string[] -} - -interface Simulation { - id: string - project_id: string - owner_id: string - network_id: string - block_number: number - transaction_index: number from: string to: string - input: string - gas: number - gas_price: string - value: string - method: string - status: boolean - access_list: null - queue_origin: string - created_at: Date -} - -interface ErrorInfo { - error_message: string - address: string -} - -export interface SimulationError { - error: { - id: string - message: string - slug: string - } + value?: string + gas?: number + gas_price?: string } -interface Transaction { - hash: string - block_hash: string - block_number: number - from: string - gas: number - gas_price: number - gas_fee_cap: number - gas_tip_cap: number - cumulative_gas_used: number - gas_used: number - effective_gas_price: number - input: string - nonce: number - to: string - index: number - error_message?: string - error_info?: ErrorInfo - value: string - access_list: null +export interface SimulationData { + link: string status: boolean - addresses: string[] - contract_ids: string[] - network_id: string - function_selector: string - transaction_info: TransactionInfo - timestamp: Date - method: string - decoded_input: null - // Note: manually added (partial keys of `call_trace`) - call_trace: Array<{ - error?: string - input: string - }> -} - -interface TransactionInfo { - contract_id: string - block_number: number - transaction_id: string - contract_address: string - method: string - parameters: null - intrinsic_gas: number - refund_gas: number - call_trace: CallTrace - stack_trace: null | StackTrace[] - logs: Log[] | null - state_diff: StateDiff[] - raw_state_diff: null - console_logs: null - created_at: Date -} - -interface StackTrace { - file_index: number - contract: string - name: string - line: number - error: string - error_reason: string - code: string - op: string - length: number -} - -interface CallTrace { - hash: string - contract_name: string - function_name: string - function_pc: number - function_op: string - function_file_index: number - function_code_start: number - function_line_number: number - function_code_length: number - function_states: CallTraceFunctionState[] - caller_pc: number - caller_op: string - call_type: string - from: string - from_balance: string - to: string - to_balance: string - value: string - caller: Caller - block_timestamp: Date - gas: number - gas_used: number - intrinsic_gas: number - input: string - decoded_input: Input[] - state_diff: StateDiff[] - logs: Log[] - output: string - decoded_output: FunctionVariableElement[] - network_id: string - calls: CallTraceCall[] -} - -interface Caller { - address: string - balance: string -} - -interface CallTraceCall { - hash: string - contract_name: string - function_name: string - function_pc: number - function_op: string - function_file_index: number - function_code_start: number - function_line_number: number - function_code_length: number - function_states: CallTraceFunctionState[] - function_variables: FunctionVariableElement[] - caller_pc: number - caller_op: string - caller_file_index: number - caller_line_number: number - caller_code_start: number - caller_code_length: number - call_type: string - from: string - from_balance: null - to: string - to_balance: null - value: null - caller: Caller - block_timestamp: Date - gas: number - gas_used: number - input: string - decoded_input: Input[] - output: string - decoded_output: FunctionVariableElement[] - network_id: string - calls: PurpleCall[] -} - -interface PurpleCall { - hash: string - contract_name: string - function_name: string - function_pc: number - function_op: string - function_file_index: number - function_code_start: number - function_line_number: number - function_code_length: number - function_states?: FluffyFunctionState[] - function_variables?: FunctionVariable[] - caller_pc: number - caller_op: string - caller_file_index: number - caller_line_number: number - caller_code_start: number - caller_code_length: number - call_type: string - from: string - from_balance: null | string - to: string - to_balance: null | string - value: null | string - caller: Caller - block_timestamp: Date - gas: number - gas_used: number - refund_gas?: number - input: string - decoded_input: Input[] - output: string - decoded_output: FunctionVariable[] | null - network_id: string - calls: FluffyCall[] | null -} - -interface FluffyCall { - hash: string - contract_name: string - function_name?: string - function_pc: number - function_op: string - function_file_index?: number - function_code_start?: number - function_line_number?: number - function_code_length?: number - function_states?: FluffyFunctionState[] - function_variables?: FunctionVariable[] - caller_pc: number - caller_op: string - caller_file_index: number - caller_line_number: number - caller_code_start: number - caller_code_length: number - call_type: string - from: string - from_balance: null | string - to: string - to_balance: null | string - value: null | string - caller?: Caller - block_timestamp: Date - gas: number - gas_used: number - input: string - decoded_input?: FunctionVariable[] - output: string - decoded_output: PurpleDecodedOutput[] | null - network_id: string - calls: TentacledCall[] | null - refund_gas?: number -} - -interface TentacledCall { - hash: string - contract_name: string - function_name: string - function_pc: number - function_op: string - function_file_index: number - function_code_start: number - function_line_number: number - function_code_length: number - function_states: PurpleFunctionState[] - caller_pc: number - caller_op: string - caller_file_index: number - caller_line_number: number - caller_code_start: number - caller_code_length: number - call_type: string - from: string - from_balance: null - to: string - to_balance: null - value: null - caller: Caller - block_timestamp: Date - gas: number - gas_used: number - input: string - decoded_input: FunctionVariableElement[] - output: string - decoded_output: FunctionVariable[] - network_id: string - calls: null -} - -interface FunctionVariableElement { - soltype: SoltypeElement - value: string -} - -interface FunctionVariable { - soltype: SoltypeElement - value: PurpleValue | string -} - -interface PurpleValue { - ballot: string - basedOn: string - configured: string - currency: string - cycleLimit: string - discountRate: string - duration: string - fee: string id: string - metadata: string - number: string - projectId: string - start: string - tapped: string - target: string - weight: string -} - -interface PurpleFunctionState { - soltype: SoltypeElement - value: Record -} - -interface PurpleDecodedOutput { - soltype: SoltypeElement - value: boolean | PurpleValue | string } -interface FluffyFunctionState { - soltype: PurpleSoltype - value: Record -} - -interface PurpleSoltype { - name: string - type: SoltypeType - storage_location: StorageLocation - components: null - offset: number - index: string - indexed: boolean -} - -interface Input { - soltype: SoltypeElement | null - value: boolean | string -} - -interface CallTraceFunctionState { - soltype: PurpleSoltype - value: Record -} - -interface Log { - name: string | null - anonymous: boolean - inputs: Input[] - raw: LogRaw -} - -interface LogRaw { - address: string - topics: string[] - data: string -} - -interface StateDiff { - soltype: SoltypeElement | null - original: string | Record - dirty: string | Record - raw: RawElement[] -} - -interface RawElement { +export interface TokenHolder { address: string - key: string - original: string - dirty: string + balance: string } diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts index 2119ac45ae..7e2cfe1eb4 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts @@ -1,4 +1,5 @@ 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 { Currency, CurrencyAmount } from '@uniswap/sdk-core' @@ -6,16 +7,13 @@ import { BigNumberish } from 'ethers' import { CowHook, CowHookDetailsSerialized, HookDappOrderParams } from 'modules/hooksStore/types/hooks' -import { TENDERLY_API_BASE_ENDPOINT, TENDERLY_API_KEY } from '../const' -import { SimulationError, TenderlyBundleSimulationResponse, TenderlySimulatePayload } from '../types' +import { SimulationData, SimulationInput } from '../types' export interface GetTransferTenderlySimulationInput { currencyAmount: CurrencyAmount from: string receiver: string token: Erc20 - chainId: SupportedChainId - slotOverride?: string } export type TokenBuyTransferInfo = { @@ -33,35 +31,24 @@ export interface PostBundleSimulationParams { tokenBuyTransferInfo: TokenBuyTransferInfo } -export const bundleSimulation = async ( - params: PostBundleSimulationParams, -): Promise => { +export const bundleSimulation = async (params: PostBundleSimulationParams): Promise => { const input = getBundleTenderlySimulationInput(params) - const response = await fetch(`${TENDERLY_API_BASE_ENDPOINT}/simulate-bundle`, { + const response = await fetch(`${BFF_BASE_URL}/${params.chainId}/simulation/simulateBundle`, { method: 'POST', body: JSON.stringify(input), headers: { - 'X-Access-Key': TENDERLY_API_KEY, + 'Content-Type': 'application/json', }, }).then((res) => res.json()) - return response as TenderlyBundleSimulationResponse | SimulationError + return response as SimulationData[] } -export function getCoWHookTenderlySimulationInput( - from: string, - params: CowHook, - chainId: SupportedChainId, -): TenderlySimulatePayload { +export function getCoWHookTenderlySimulationInput(from: string, params: CowHook): SimulationInput { return { input: params.callData, to: params.target, - gas: +params.gasLimit, from, - gas_price: '0', - network_id: chainId.toString(), - save: true, - save_if_fails: true, } } // TODO: check if there is a function to do this conversion @@ -84,8 +71,7 @@ export function getTransferTenderlySimulationInput({ from, receiver, token, - chainId, -}: GetTransferTenderlySimulationInput): TenderlySimulatePayload { +}: GetTransferTenderlySimulationInput): SimulationInput { const callData = token.interface.encodeFunctionData('transfer', [ receiver, currencyAmountToBigNumberish(currencyAmount), @@ -94,12 +80,7 @@ export function getTransferTenderlySimulationInput({ return { input: callData, to: token.address, - gas: 100000, // TODO: Check if this is relevant from, - gas_price: '0', - network_id: chainId.toString(), - save: true, - save_if_fails: true, } } @@ -112,17 +93,17 @@ export function getBundleTenderlySimulationInput({ postHooks, orderParams, tokenBuyTransferInfo, -}: PostBundleSimulationParams): { simulations: TenderlySimulatePayload[] } { +}: PostBundleSimulationParams): SimulationInput[] { const settlementAddress = COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId] const preHooksSimulations = preHooks.map((hook) => - getCoWHookTenderlySimulationInput(settlementAddress, hook.hookDetails.hook, chainId), + getCoWHookTenderlySimulationInput(settlementAddress, hook.hookDetails.hook), ) const postHooksSimulations = postHooks.map((hook) => - getCoWHookTenderlySimulationInput(settlementAddress, hook.hookDetails.hook, chainId), + getCoWHookTenderlySimulationInput(settlementAddress, hook.hookDetails.hook), ) // If there are no post hooks, we don't need to simulate the transfer - if (postHooks.length === 0) return { simulations: preHooksSimulations } + if (postHooks.length === 0) return preHooksSimulations const receiver = postHooks[0].hookDetails.recipientOverride || orderParams.receiver @@ -131,7 +112,6 @@ export function getBundleTenderlySimulationInput({ from: account, receiver: COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId], token: tokenSell, - chainId, }) const buyTokenTransfers = tokenBuyTransferInfo.map((transferInfo) => @@ -140,9 +120,8 @@ export function getBundleTenderlySimulationInput({ from: transferInfo.sender, receiver, token: tokenBuy, - chainId, }), ) - return { simulations: [...preHooksSimulations, sellTokenTransfer, ...buyTokenTransfers, ...postHooksSimulations] } + return [...preHooksSimulations, sellTokenTransfer, ...buyTokenTransfers, ...postHooksSimulations] } diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/checkBundleSimulationError.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/checkBundleSimulationError.ts deleted file mode 100644 index 8e84ac62b8..0000000000 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/checkBundleSimulationError.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SimulationError, TenderlyBundleSimulationResponse } from '../types' - -export function checkBundleSimulationError( - response: TenderlyBundleSimulationResponse | SimulationError, -): response is SimulationError { - return (response as SimulationError).error !== undefined -} diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts index abf14b2b1f..a49591023f 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts @@ -1,9 +1,10 @@ import { PostBundleSimulationParams } from './bundleSimulation' -import { getSimulationLink } from '../const' -import { SimulationData, TenderlyBundleSimulationResponse } from '../types' +import { SimulationData } from '../types' -export function generateSimulationDataToError(postParams: PostBundleSimulationParams): Record { +export function generateSimulationDataToError( + postParams: Pick, +): Record { const preHooksKeys = postParams.preHooks.map(({ hookDetails }) => hookDetails.uuid) const postHooksKeys = postParams.postHooks.map(({ hookDetails }) => hookDetails.uuid) const hooksKeys = [...preHooksKeys, ...postHooksKeys] @@ -11,28 +12,22 @@ export function generateSimulationDataToError(postParams: PostBundleSimulationPa return hooksKeys.reduce( (acc, key) => ({ ...acc, - [key]: { tenderlySimulationLink: '', simulationPassed: false }, + [key]: { link: '', status: false, id: key }, }), {}, ) } export function generateNewSimulationData( - bundleSimulationResponse: TenderlyBundleSimulationResponse, + simulationData: SimulationData[], postParams: PostBundleSimulationParams, ): Record { const preHooksKeys = postParams.preHooks.map(({ hookDetails }) => hookDetails.uuid) const postHooksKeys = postParams.postHooks.map(({ hookDetails }) => hookDetails.uuid) - const preHooksData = bundleSimulationResponse.simulation_results.slice(0, preHooksKeys.length).map((simulation) => ({ - tenderlySimulationLink: getSimulationLink(simulation.simulation.id), - simulationPassed: simulation.simulation.status, - })) + const preHooksData = simulationData.slice(0, preHooksKeys.length) - const postHooksData = bundleSimulationResponse.simulation_results.slice(-postHooksKeys.length).map((simulation) => ({ - tenderlySimulationLink: getSimulationLink(simulation.simulation.id), - simulationPassed: simulation.simulation.status, - })) + const postHooksData = simulationData.slice(-postHooksKeys.length) return { ...preHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: preHooksData[index] }), {}), diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts index bfdb603f16..69af017d11 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/getTokenTransferInfo.ts @@ -2,13 +2,13 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { TokenBuyTransferInfo } from './bundleSimulation' -import { TokenHolderItem } from '../types' +import { TokenHolder } from '../types' export function getTokenTransferInfo({ tokenHolders, amountToTransfer, }: { - tokenHolders: TokenHolderItem[] + tokenHolders: TokenHolder[] amountToTransfer: CurrencyAmount }): TokenBuyTransferInfo { let sum = CurrencyAmount.fromRawAmount(amountToTransfer.currency, '0') @@ -22,7 +22,7 @@ export function getTokenTransferInfo({ // skip token holders with no address or balance if (!tokenHolder.address || !tokenHolder.balance) continue - const tokenHolderAmount = CurrencyAmount.fromRawAmount(amountToTransfer.currency, tokenHolder.balance.toString()) + const tokenHolderAmount = CurrencyAmount.fromRawAmount(amountToTransfer.currency, tokenHolder.balance) const sumWithTokenHolder = sum.add(tokenHolderAmount) if (sumWithTokenHolder.greaterThan(amountToTransfer) || sumWithTokenHolder.equalTo(amountToTransfer)) { From 7eff54d7d069b18fac0db6732835c0b4faeb43a8 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Fri, 11 Oct 2024 18:34:55 -0300 Subject: [PATCH 08/31] chore: remove console.logs --- .../tenderly/hooks/useTenderlyBundleSimulation.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index d6addbb468..feaef2931c 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -25,18 +25,8 @@ export function useTenderlyBundleSimulateSWR() { }) const getNewSimulationData = useCallback(async () => { - console.log('getNewSimulationData') if (postHooks.length === 0 && preHooks.length === 0) return {} - console.log('getNewSimulationData 2') - console.log({ - account, - buyTokenTopHolders, - tokenBuy, - orderParams, - tokenSell, - buyAmount, - }) if (!account || !buyTokenTopHolders || !tokenBuy || !orderParams || !tokenSell || !buyAmount) { return generateSimulationDataToError({ postHooks, preHooks }) } @@ -57,8 +47,6 @@ export function useTenderlyBundleSimulateSWR() { chainId, } - console.log({ paramsComplete }) - try { const response = await bundleSimulation(paramsComplete) return generateNewSimulationData(response, paramsComplete) From beccb798febf190e3c262c2e3093076b0a3c39d9 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Wed, 16 Oct 2024 12:07:21 -0300 Subject: [PATCH 09/31] chore: fix leandro comments --- .../hooksStore/pure/AppliedHookItem/index.tsx | 2 -- .../src/modules/tenderly/const.ts | 9 -------- .../hooks/useTenderlyBundleSimulation.ts | 14 ++++++++----- .../tenderly/utils/bundleSimulation.ts | 21 +------------------ 4 files changed, 10 insertions(+), 36 deletions(-) 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 1a3f39c4d1..e9162dc903 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -1,5 +1,3 @@ -// src/modules/hooksStore/pure/AppliedHookItem/index.tsx - import { useMemo } from 'react' import ICON_CHECK_ICON from '@cowprotocol/assets/cow-swap/check-singular.svg' diff --git a/apps/cowswap-frontend/src/modules/tenderly/const.ts b/apps/cowswap-frontend/src/modules/tenderly/const.ts index 34d6d8aae2..53f20a1500 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/const.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/const.ts @@ -1,15 +1,6 @@ -import { SupportedChainId } from '@cowprotocol/cow-sdk' - 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_CLIENT_NETWORK_MAPPING: Record = { - [SupportedChainId.MAINNET]: 'eth-mainnet', - [SupportedChainId.SEPOLIA]: 'eth-sepolia', - [SupportedChainId.GNOSIS_CHAIN]: 'gnosis-mainnet', - [SupportedChainId.ARBITRUM_ONE]: 'arbitrum-mainnet', -} diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index feaef2931c..8438f5d61a 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -2,14 +2,18 @@ import { useCallback } from 'react' import { useWalletInfo } from '@cowprotocol/wallet' -import { bundleSimulation } from '../utils/bundleSimulation' -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 { 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() { const { account, chainId } = useWalletInfo() diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts index 3556d5349f..02021ba4b0 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts @@ -4,8 +4,6 @@ import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS, SupportedChainId } from '@cow import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { BigNumberish } from 'ethers' - import { CowHook, HookDappOrderParams } from 'modules/hooksStore/types/hooks' import { SimulationData, SimulationInput } from '../types' @@ -52,20 +50,6 @@ export function getCoWHookTenderlySimulationInput(from: string, params: CowHook) from, } } -// TODO: check if there is a function to do this conversion -function currencyAmountToBigNumberish(amount: CurrencyAmount): BigNumberish { - // CurrencyAmount already stores the amount as a fraction internally - const fraction = amount.asFraction - - // Get the numerator and denominator as BigInts - const numerator = BigInt(fraction.numerator.toString()) - const denominator = BigInt(fraction.denominator.toString()) - - const result = numerator / denominator - - // Convert the result to a string - return result.toString() -} export function getTransferTenderlySimulationInput({ currencyAmount, @@ -73,10 +57,7 @@ export function getTransferTenderlySimulationInput({ receiver, token, }: GetTransferTenderlySimulationInput): SimulationInput { - const callData = token.interface.encodeFunctionData('transfer', [ - receiver, - currencyAmountToBigNumberish(currencyAmount), - ]) + const callData = token.interface.encodeFunctionData('transfer', [receiver, currencyAmount.quotient.toString()]) return { input: callData, From 0d3254c71f6352f8a91882b6e51dda0e5f03bcdb Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Thu, 17 Oct 2024 16:45:55 -0300 Subject: [PATCH 10/31] chore: remove unused tenderly consts --- apps/cowswap-frontend/src/modules/tenderly/const.ts | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 apps/cowswap-frontend/src/modules/tenderly/const.ts diff --git a/apps/cowswap-frontend/src/modules/tenderly/const.ts b/apps/cowswap-frontend/src/modules/tenderly/const.ts deleted file mode 100644 index 53f20a1500..0000000000 --- a/apps/cowswap-frontend/src/modules/tenderly/const.ts +++ /dev/null @@ -1,6 +0,0 @@ -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}` -} From 2726bb1fbfcb9bc4929328fd548c2ea3b83b70ab Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Thu, 17 Oct 2024 16:46:32 -0300 Subject: [PATCH 11/31] refactor: rename tenderly simulation hook --- .../src/modules/hooksStore/pure/AppliedHookItem/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e9162dc903..f491b5cb03 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -10,7 +10,7 @@ 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 { useTenderlyBundleSimulate } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' import * as styledEl from './styled' @@ -31,7 +31,7 @@ interface HookItemProp { const isBundleSimulationReady = true export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHook, removeHook, index }: HookItemProp) { - const { isValidating, data } = useTenderlyBundleSimulateSWR() + const { isValidating, data } = useTenderlyBundleSimulate() const simulationData = useMemo(() => { if (!data) return From 6ea9624f90d0894d8bfed4116ab3aa901d806fc1 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Thu, 17 Oct 2024 16:47:52 -0300 Subject: [PATCH 12/31] chore: refactor top token holder swr to jotai with cache --- .../tenderly/hooks/useGetTopTokenHolders.ts | 20 ++++++ .../hooks/useTenderlyBundleSimulation.ts | 67 ++++++++++++++----- .../tenderly/hooks/useTopTokenHolders.ts | 23 ------- .../modules/tenderly/state/topTokenHolders.ts | 55 +++++++++++++++ .../src/modules/tenderly/types.ts | 7 ++ .../tenderly/utils/bundleSimulation.ts | 31 ++++++--- .../tenderly/utils/generateSimulationData.ts | 2 +- 7 files changed, 155 insertions(+), 50 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts delete mode 100644 apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts create mode 100644 apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts new file mode 100644 index 0000000000..194a086a11 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts @@ -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], + ) +} diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index 8438f5d61a..fa449953d7 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -9,32 +9,44 @@ import { useOrderParams } from 'modules/hooksStore/hooks/useOrderParams' import { useTokenContract } from 'common/hooks/useContract' -import { useTopTokenHolders } from './useTopTokenHolders' +import { useGetTopTokenHolders } from './useGetTopTokenHolders' -import { bundleSimulation } from '../utils/bundleSimulation' +import { completeBundleSimulation, preHooksBundleSimulation } from '../utils/bundleSimulation' import { generateNewSimulationData, generateSimulationDataToError } from '../utils/generateSimulationData' import { getTokenTransferInfo } from '../utils/getTokenTransferInfo' -export function useTenderlyBundleSimulateSWR() { +export function useTenderlyBundleSimulate() { 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 { data: buyTokenTopHolders, isValidating: isTopTokenHoldersValidating } = useTopTokenHolders({ - tokenAddress: tokenBuy?.address, - chainId, - }) + const getTopTokenHolder = useGetTopTokenHolders() - const getNewSimulationData = useCallback(async () => { - if (postHooks.length === 0 && preHooks.length === 0) return {} + const simulateBundle = useCallback(async () => { + if (postHooks.length === 0 && preHooks.length === 0) return + + if (!postHooks.length) + return preHooksBundleSimulation({ + chainId, + preHooks, + }) - if (!account || !buyTokenTopHolders || !tokenBuy || !orderParams || !tokenSell || !buyAmount) { - return generateSimulationDataToError({ postHooks, 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, @@ -45,19 +57,40 @@ export function useTenderlyBundleSimulateSWR() { preHooks, tokenBuy, tokenBuyTransferInfo, - orderParams, + sellAmount, + orderReceiver, tokenSell, account, chainId, } + return completeBundleSimulation(paramsComplete) + }, [ + account, + chainId, + getTopTokenHolder, + tokenBuy, + postHooks, + preHooks, + buyAmount, + sellAmount, + orderReceiver, + tokenSell, + ]) + + const getNewSimulationData = useCallback(async () => { try { - const response = await bundleSimulation(paramsComplete) - return generateNewSimulationData(response, paramsComplete) + const simulationData = await simulateBundle() + + if (!simulationData) { + return {} + } + + return generateNewSimulationData(simulationData, { preHooks, postHooks }) } catch { - return generateSimulationDataToError(paramsComplete) + return generateSimulationDataToError({ preHooks, postHooks }) } - }, [account, chainId, buyTokenTopHolders, tokenBuy, postHooks, preHooks, buyAmount]) + }, [preHooks, postHooks, simulateBundle]) const { data, isValidating: isBundleSimulationLoading } = useSWR( ['tenderly-bundle-simulation', postHooks, preHooks, orderParams?.sellTokenAddress, orderParams?.buyTokenAddress], @@ -69,5 +102,5 @@ export function useTenderlyBundleSimulateSWR() { }, ) - return { data, isValidating: isBundleSimulationLoading || isTopTokenHoldersValidating } + return { data, isValidating: isBundleSimulationLoading } } diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts deleted file mode 100644 index 57e556d9be..0000000000 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTopTokenHolders.ts +++ /dev/null @@ -1,23 +0,0 @@ -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)) -} diff --git a/apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts b/apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts new file mode 100644 index 0000000000..ab5b3baaaa --- /dev/null +++ b/apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts @@ -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 { + value: T + timestamp: number +} + +const baseTopTokenHolderAtom = atomWithStorage>>( + '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() + + // 1 hour in milliseconds + if (cachedData[key] && currentTime - cachedData[key].timestamp <= 3600000) { + return cachedData[key].value + } + + const newValue = await getTopTokenHolder(params) + set(baseTopTokenHolderAtom, { + ...cachedData, + [key]: { value: newValue, timestamp: currentTime }, + }) + + return newValue + }, +) diff --git a/apps/cowswap-frontend/src/modules/tenderly/types.ts b/apps/cowswap-frontend/src/modules/tenderly/types.ts index bb813e4215..9ef8eb6b56 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/types.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/types.ts @@ -1,3 +1,5 @@ +import { SupportedChainId } from '@cowprotocol/cow-sdk' + export interface SimulationInput { input: string from: string @@ -13,6 +15,11 @@ export interface SimulationData { id: string } +export interface GetTopTokenHoldersParams { + tokenAddress?: string + chainId: SupportedChainId +} + export interface TokenHolder { address: string balance: string diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts index 02021ba4b0..aaf28dc334 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts @@ -4,7 +4,7 @@ import { COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS, SupportedChainId } from '@cow import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { CowHook, HookDappOrderParams } from 'modules/hooksStore/types/hooks' +import { CowHook } from 'modules/hooksStore/types/hooks' import { SimulationData, SimulationInput } from '../types' @@ -26,13 +26,27 @@ export interface PostBundleSimulationParams { tokenBuy: Erc20 preHooks: CowHookDetails[] postHooks: CowHookDetails[] - orderParams: HookDappOrderParams + sellAmount: CurrencyAmount + orderReceiver: string tokenBuyTransferInfo: TokenBuyTransferInfo } -export const bundleSimulation = async (params: PostBundleSimulationParams): Promise => { +export const completeBundleSimulation = async (params: PostBundleSimulationParams): Promise => { const input = getBundleTenderlySimulationInput(params) - const response = await fetch(`${BFF_BASE_URL}/${params.chainId}/simulation/simulateBundle`, { + return simulateBundle(input, params.chainId) +} + +export const preHooksBundleSimulation = async ( + params: Pick, +): Promise => { + const input = params.preHooks.map((hook) => + getCoWHookTenderlySimulationInput(COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[params.chainId], hook.hook), + ) + return simulateBundle(input, params.chainId) +} + +const simulateBundle = async (input: SimulationInput[], chainId: SupportedChainId): Promise => { + const response = await fetch(`${BFF_BASE_URL}/${chainId}/simulation/simulateBundle`, { method: 'POST', body: JSON.stringify(input), headers: { @@ -73,7 +87,8 @@ export function getBundleTenderlySimulationInput({ tokenBuy, preHooks, postHooks, - orderParams, + sellAmount, + orderReceiver, tokenBuyTransferInfo, }: PostBundleSimulationParams): SimulationInput[] { const settlementAddress = COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId] @@ -83,10 +98,8 @@ export function getBundleTenderlySimulationInput({ // 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, + currencyAmount: sellAmount, from: account, receiver: COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[chainId], token: tokenSell, @@ -96,7 +109,7 @@ export function getBundleTenderlySimulationInput({ getTransferTenderlySimulationInput({ currencyAmount: transferInfo.amount, from: transferInfo.sender, - receiver, + receiver: postHooks[0].recipientOverride || orderReceiver, token: tokenBuy, }), ) diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts index 06532635f8..5d3416b736 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts @@ -20,7 +20,7 @@ export function generateSimulationDataToError( export function generateNewSimulationData( simulationData: SimulationData[], - postParams: PostBundleSimulationParams, + postParams: Pick, ): Record { const preHooksKeys = postParams.preHooks.map((hookDetails) => hookDetails.uuid) const postHooksKeys = postParams.postHooks.map((hookDetails) => hookDetails.uuid) From 5cb5b7205c50be34310249c1c49cac6835e1cf41 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Fri, 18 Oct 2024 09:20:12 -0300 Subject: [PATCH 13/31] chore: rename hook to match file name --- .../src/modules/hooksStore/pure/AppliedHookItem/index.tsx | 4 ++-- .../src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 f491b5cb03..d30afa2a05 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -10,7 +10,7 @@ import { InfoTooltip } from '@cowprotocol/ui' import { Edit2, Trash2, ExternalLink as ExternalLinkIcon } from 'react-feather' import SVG from 'react-inlinesvg' -import { useTenderlyBundleSimulate } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' +import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' import * as styledEl from './styled' @@ -31,7 +31,7 @@ interface HookItemProp { const isBundleSimulationReady = true export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHook, removeHook, index }: HookItemProp) { - const { isValidating, data } = useTenderlyBundleSimulate() + const { isValidating, data } = useTenderlyBundleSimulation() const simulationData = useMemo(() => { if (!data) return diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index fa449953d7..7ab1478887 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -15,7 +15,7 @@ import { completeBundleSimulation, preHooksBundleSimulation } from '../utils/bun import { generateNewSimulationData, generateSimulationDataToError } from '../utils/generateSimulationData' import { getTokenTransferInfo } from '../utils/getTokenTransferInfo' -export function useTenderlyBundleSimulate() { +export function useTenderlyBundleSimulation() { const { account, chainId } = useWalletInfo() const { preHooks, postHooks } = useHooks() const orderParams = useOrderParams() From 22501d4150b2e0bd0042b402df13787b46b3a9bb Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Tue, 22 Oct 2024 09:32:58 -0300 Subject: [PATCH 14/31] refactor: use seconds for cache time in toptokenholder state --- .../src/modules/tenderly/state/topTokenHolders.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts b/apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts index ab5b3baaaa..64910555ca 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/state/topTokenHolders.ts @@ -37,10 +37,10 @@ export const topTokenHoldersAtom = atom( async (get, set, params: GetTopTokenHoldersParams) => { const key = `${params.chainId}:${params.tokenAddress?.toLowerCase()}` const cachedData = get(baseTopTokenHolderAtom) - const currentTime = Date.now() + const currentTime = Date.now() / 1000 - // 1 hour in milliseconds - if (cachedData[key] && currentTime - cachedData[key].timestamp <= 3600000) { + // 1 hour in seconds + if (cachedData[key] && currentTime - cachedData[key].timestamp <= 3600) { return cachedData[key].value } From b8bb1f40c4a9e44e22de7baa37fb181380da5972 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Tue, 22 Oct 2024 18:46:11 -0300 Subject: [PATCH 15/31] chore: create hooks to use token balances with pre hooks --- .../hooksStore/hooks/useBalancesDiff.ts | 127 ++++++++++++++++++ .../hooks/useCurrencyAmountBalanceCombined.ts | 22 +++ .../swap/hooks/useTokensBalancesCombined.ts | 58 ++++++++ 3 files changed, 207 insertions(+) create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useCurrencyAmountBalanceCombined.ts create mode 100644 apps/cowswap-frontend/src/modules/swap/hooks/useTokensBalancesCombined.ts diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts new file mode 100644 index 0000000000..f5d7cf79a8 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts @@ -0,0 +1,127 @@ +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 { CowHookDetails } from '@cowprotocol/hook-dapp-lib' +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, hookToEditDetails?: CowHookDetails): BalancesDiff { + const { account } = useWalletInfo() + const { data } = useTenderlyBundleSimulation() + const orderParams = useOrderParams() + const { preHooks, postHooks } = useHooks() + const preHookBalanceDiff = usePreHookBalanceDiff() + + const orderMockBalanceDiff = useMemo(() => { + if (!account) return EMPTY_BALANCE_DIFF + const balanceDiff: Record = {} + + if (orderParams?.buyAmount && orderParams.buyTokenAddress && account) + balanceDiff[orderParams.buyTokenAddress] = orderParams.buyAmount + + if (orderParams?.sellAmount && orderParams.sellTokenAddress && account) + balanceDiff[orderParams.sellTokenAddress] = `-${orderParams.sellAmount}` + + return { account: balanceDiff } + }, [orderParams, account]) + + const firstPostHookBalanceDiff = useMemo(() => { + return mergeBalanceDiffs(preHookBalanceDiff, orderMockBalanceDiff) + }, [preHookBalanceDiff, orderMockBalanceDiff]) + + 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, orderMockBalanceDiff, preHookBalanceDiff]) + + const hookToEditBalanceDiff = useMemo(() => { + if (!data || !hookToEditDetails?.uuid) return EMPTY_BALANCE_DIFF + + const otherHooks = isPreHook ? preHooks : postHooks + + const hookToEditIndex = otherHooks.findIndex((hook) => hook.uuid === hookToEditDetails.uuid) + + // 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, hookToEditDetails]) + + return useMemo(() => { + if (hookToEditDetails?.uuid) return hookToEditBalanceDiff + if (isPreHook) return preHookBalanceDiff + return postHookBalanceDiff + }, [data, orderParams, preHooks, postHooks]) +} + +function mergeBalanceDiffs( + first: Record>, + second: Record>, +): Record> { + const result: Record> = {} + + // Helper function to add BigNumber strings + const addBigNumberStrings = (a: string, b: string): string => { + const bigA = BigNumber.from(a) + const bigB = BigNumber.from(b) + return bigA.add(bigB).toString() + } + + // 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] = addBigNumberStrings(result[address][token], second[address][token]) + } catch (error) { + console.error(`Error adding balances for address ${address} and token ${token}:`, error) + throw error + } + } + } + } + } + + return result +} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useCurrencyAmountBalanceCombined.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useCurrencyAmountBalanceCombined.ts new file mode 100644 index 0000000000..5977042e25 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useCurrencyAmountBalanceCombined.ts @@ -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 | 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]) +} diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useTokensBalancesCombined.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useTokensBalancesCombined.ts new file mode 100644 index 0000000000..c1b82bb963 --- /dev/null +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useTokensBalancesCombined.ts @@ -0,0 +1,58 @@ +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' + +export function useTokensBalancesCombined() { + const { account } = useWalletInfo() + const preHooksBalancesDiff = usePreHookBalanceDiff() + const tokenBalances = useTokensBalances() + + return useMemo(() => { + if (!account) return tokenBalances + const accountBalancesDiff = preHooksBalancesDiff[account.toLowerCase()] || {} + return applyBalanceDiffs(tokenBalances, accountBalancesDiff) + }, [account, preHooksBalancesDiff, tokenBalances]) +} + +function applyBalanceDiffs(currentState: BalancesState, diffs: Record): BalancesState { + const normalizedValues: Record = {} + + // Create normalized versions of inputs + const normalizedCurrentValues: Record = {} + const normalizedDiffs: Record = {} + + // Normalize current state values + for (const address of Object.keys(currentState.values)) { + normalizedCurrentValues[address.toLowerCase()] = currentState.values[address] + } + + // Normalize diffs + for (const address of Object.keys(diffs)) { + normalizedDiffs[address.toLowerCase()] = diffs[address] + } + + // Process all addresses in the normalized current state + for (const address of Object.keys(normalizedCurrentValues)) { + const currentBalance = normalizedCurrentValues[address] || BigNumber.from(0) + const diff = normalizedDiffs[address] ? BigNumber.from(normalizedDiffs[address]) : BigNumber.from(0) + normalizedValues[address] = currentBalance.add(diff) + } + + // Process any new addresses from diffs that weren't in the current state + for (const address of Object.keys(normalizedDiffs)) { + if (!normalizedCurrentValues.hasOwnProperty(address)) { + normalizedValues[address] = BigNumber.from(normalizedDiffs[address]) + } + } + + // Return new state object maintaining the isLoading property + return { + isLoading: currentState.isLoading, + values: normalizedValues, + } +} From f9ade1479808ea3a3386bbe2154468e7fda7e1fe Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Tue, 22 Oct 2024 18:46:55 -0300 Subject: [PATCH 16/31] chore: use combined hooks on swap components --- .../containers/HookDappContainer/index.tsx | 3 +++ .../modules/swap/containers/SwapWidget/index.tsx | 7 ++++--- .../modules/swap/hooks/useSwapButtonContext.ts | 7 +++++-- .../src/modules/swap/hooks/useSwapState.tsx | 8 +++++--- .../tenderly/hooks/useTenderlyBundleSimulation.ts | 2 +- .../src/modules/tenderly/types.ts | 5 +++++ .../src/modules/tokens/hooks/useEnoughBalance.ts | 15 ++++++--------- .../containers/SelectTokenWidget/index.tsx | 5 +++-- .../trade/hooks/useBuildTradeDerivedState.ts | 12 ++++++------ 9 files changed, 38 insertions(+), 26 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx index f678cefe26..9d4efdbb6b 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/containers/HookDappContainer/index.tsx @@ -15,6 +15,7 @@ import { useOrderParams } from '../../hooks/useOrderParams' import { HookDapp, HookDappContext as HookDappContextType } from '../../types/hooks' import { isHookDappIframe } from '../../utils' import { IframeDappContainer } from '../IframeDappContainer' +import { useHookBalancesDiff } from 'modules/hooksStore/hooks/useBalancesDiff' interface HookDappContainerProps { dapp: HookDapp @@ -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) const { inputCurrencyId = null, outputCurrencyId = null } = tradeState.state || {} const signer = useMemo(() => provider?.getSigner(), [provider]) @@ -49,6 +51,7 @@ export function HookDappContainer({ dapp, isPreHook, onDismiss, hookToEdit }: Ho isSmartContract, isPreHook, isDarkMode, + balancesDiff, editHook(...args) { editHook(...args) onDismiss() diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx index 30c61ac4bc..5ee4067806 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -1,6 +1,6 @@ import { ReactNode, useCallback, useMemo, useState } from 'react' -import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' +// import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { NATIVE_CURRENCIES, TokenWithLogo } from '@cowprotocol/common-const' import { useIsTradeUnsupported } from '@cowprotocol/tokens' import { useWalletDetails, useWalletInfo } from '@cowprotocol/wallet' @@ -15,6 +15,7 @@ import { useRecipientToggleManager, useUserTransactionTTL } from 'legacy/state/u import { useInjectedWidgetParams } from 'modules/injectedWidget' import { EthFlowModal, EthFlowProps } from 'modules/swap/containers/EthFlow' import { SwapModals, SwapModalsProps } from 'modules/swap/containers/SwapModals' +import { useCurrencyAmountBalanceCombined } from 'modules/swap/hooks/useCurrencyAmountBalanceCombined' import { useShowRecipientControls } from 'modules/swap/hooks/useShowRecipientControls' import { useSwapButtonContext } from 'modules/swap/hooks/useSwapButtonContext' import { useSwapCurrenciesAmounts } from 'modules/swap/hooks/useSwapCurrenciesAmounts' @@ -96,8 +97,8 @@ export function SwapWidget({ topContent, bottomContent }: SwapWidgetProps) { return TokenWithLogo.fromToken(currencies.OUTPUT) }, [chainId, currencies.OUTPUT]) - const inputCurrencyBalance = useCurrencyAmountBalance(inputToken) || null - const outputCurrencyBalance = useCurrencyAmountBalance(outputToken) || null + const inputCurrencyBalance = useCurrencyAmountBalanceCombined(inputToken) || null + const outputCurrencyBalance = useCurrencyAmountBalanceCombined(outputToken) || null const isSellTrade = independentField === Field.INPUT diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index 5efc07fdac..124a537e1a 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' -import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' +// import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { currencyAmountToTokenAmount, getWrappedToken } from '@cowprotocol/common-utils' import { useIsTradeUnsupported } from '@cowprotocol/tokens' import { @@ -29,6 +29,7 @@ import { QuoteDeadlineParams } from 'modules/tradeQuote' import { useApproveState } from 'common/hooks/useApproveState' import { useSafeMemo } from 'common/hooks/useSafeMemo' +import { useCurrencyAmountBalanceCombined } from './useCurrencyAmountBalanceCombined' import { useHandleSwapOrEthFlow } from './useHandleSwapOrEthFlow' import { useDerivedSwapInfo, useSwapActionHandlers } from './useSwapState' @@ -157,7 +158,9 @@ export function useSwapButtonContext(input: SwapButtonInput): SwapButtonsContext function useHasEnoughWrappedBalanceForSwap(inputAmount?: CurrencyAmount): boolean { const { currencies } = useDerivedSwapInfo() - const wrappedBalance = useCurrencyAmountBalance(currencies.INPUT ? getWrappedToken(currencies.INPUT) : undefined) + const wrappedBalance = useCurrencyAmountBalanceCombined( + currencies.INPUT ? getWrappedToken(currencies.INPUT) : undefined, + ) // is a native currency trade but wrapped token has enough balance return !!(wrappedBalance && inputAmount && !wrappedBalance.lessThan(inputAmount)) diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx index 5710c36dd7..868f95d281 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react' -import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' +// import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { formatSymbol, getIsNativeToken, isAddress, tryParseCurrencyAmount } from '@cowprotocol/common-utils' import { useENS } from '@cowprotocol/ens' import { useAreThereTokensWithSameSymbol, useTokenBySymbolOrAddress } from '@cowprotocol/tokens' @@ -28,6 +28,8 @@ import { useVolumeFee } from 'modules/volumeFee' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' import { useSafeMemo } from 'common/hooks/useSafeMemo' +import { useCurrencyAmountBalanceCombined } from './useCurrencyAmountBalanceCombined' + export const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = { '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f': true, // v2 factory '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a': true, // v2 router 01 @@ -132,8 +134,8 @@ export function useDerivedSwapInfo(): DerivedSwapInfo { const recipientLookup = useENS(recipient ?? undefined) const to: string | null = (recipient ? recipientLookup.address : account) ?? null - const inputCurrencyBalance = useCurrencyAmountBalance(inputCurrency) - const outputCurrencyBalance = useCurrencyAmountBalance(outputCurrency) + const inputCurrencyBalance = useCurrencyAmountBalanceCombined(inputCurrency) + const outputCurrencyBalance = useCurrencyAmountBalanceCombined(outputCurrency) const isExactIn: boolean = independentField === Field.INPUT const parsedAmount = useMemo( diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index 7ab1478887..8aece070e5 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -93,7 +93,7 @@ export function useTenderlyBundleSimulation() { }, [preHooks, postHooks, simulateBundle]) const { data, isValidating: isBundleSimulationLoading } = useSWR( - ['tenderly-bundle-simulation', postHooks, preHooks, orderParams?.sellTokenAddress, orderParams?.buyTokenAddress], + ['tenderly-bundle-simulation', postHooks, preHooks], getNewSimulationData, { revalidateOnFocus: false, diff --git a/apps/cowswap-frontend/src/modules/tenderly/types.ts b/apps/cowswap-frontend/src/modules/tenderly/types.ts index 9ef8eb6b56..97e50bf537 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/types.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/types.ts @@ -9,10 +9,15 @@ export interface SimulationInput { gas_price?: string } +// { [address: string]: { [token: string]: balanceDiff: string } } +// example: { '0x123': { '0x456': '100', '0xabc': '-100' } } +export type BalancesDiff = Record> + export interface SimulationData { link: string status: boolean id: string + cumulativeBalancesDiff: BalancesDiff } export interface GetTopTokenHoldersParams { diff --git a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts index 6f3bd35770..a4a50b3611 100644 --- a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts +++ b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts @@ -1,12 +1,9 @@ -import { - AllowancesState, - BalancesState, - useTokensAllowances, - useTokensBalances, -} from '@cowprotocol/balances-and-allowances' +import { AllowancesState, BalancesState, useTokensAllowances } from '@cowprotocol/balances-and-allowances' import { isEnoughAmount, getAddress, getIsNativeToken, getWrappedToken } from '@cowprotocol/common-utils' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { useTokensBalancesCombined } from 'modules/swap/hooks/useTokensBalancesCombined' + export interface UseEnoughBalanceParams { /** * Address of the account to check balance (and optionally the allowance) @@ -39,7 +36,7 @@ const DEFAULT_BALANCE_AND_ALLOWANCE = { enoughBalance: undefined, enoughAllowanc export function useEnoughBalanceAndAllowance(params: UseEnoughBalanceParams): UseEnoughBalanceAndAllowanceResult { const { checkAllowanceAddress } = params - const { values: balances } = useTokensBalances() + const { values: balances } = useTokensBalancesCombined() const { values: allowances } = useTokensAllowances() return hasEnoughBalanceAndAllowance({ @@ -86,7 +83,7 @@ export function hasEnoughBalanceAndAllowance(params: EnoughBalanceParams): UseEn function _enoughBalance( tokenAddress: string | undefined, amount: CurrencyAmount, - balances: BalancesState['values'] + balances: BalancesState['values'], ): boolean | undefined { const balance = tokenAddress ? balances[tokenAddress] : undefined @@ -97,7 +94,7 @@ function _enoughAllowance( tokenAddress: string | undefined, amount: CurrencyAmount, allowances: AllowancesState['values'] | undefined, - isNativeCurrency: boolean + isNativeCurrency: boolean, ): boolean | undefined { if (!tokenAddress || !allowances) { return undefined diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx index e4296a5f64..89a3ab09e9 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx @@ -1,6 +1,6 @@ import { useCallback, useState } from 'react' -import { useTokensBalances } from '@cowprotocol/balances-and-allowances' +// import { useTokensBalances } from '@cowprotocol/balances-and-allowances' import { TokenWithLogo } from '@cowprotocol/common-const' import { isInjectedWidget } from '@cowprotocol/common-utils' import { @@ -19,6 +19,7 @@ import styled from 'styled-components/macro' import { addListAnalytics } from 'modules/analytics' import { usePermitCompatibleTokens } from 'modules/permit' +import { useTokensBalancesCombined } from 'modules/swap/hooks/useTokensBalancesCombined' import { useOnTokenListAddingError } from '../../hooks/useOnTokenListAddingError' import { useSelectTokenWidgetState } from '../../hooks/useSelectTokenWidgetState' @@ -52,7 +53,7 @@ export function SelectTokenWidget() { const favoriteTokens = useFavoriteTokens() const userAddedTokens = useUserAddedTokens() const allTokenLists = useAllListsList() - const balancesState = useTokensBalances() + const balancesState = useTokensBalancesCombined() const unsupportedTokens = useUnsupportedTokens() const permitCompatibleTokens = usePermitCompatibleTokens() const onTokenListAddingError = useOnTokenListAddingError() diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts index bf389ec6b8..b5db63dc32 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts @@ -1,13 +1,13 @@ import { Atom, useAtomValue } from 'jotai' import { useMemo } from 'react' -import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { tryParseFractionalAmount } from '@cowprotocol/common-utils' import { useTokenBySymbolOrAddress } from '@cowprotocol/tokens' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Nullish } from 'types' +import { useCurrencyAmountBalanceCombined } from 'modules/swap/hooks/useCurrencyAmountBalanceCombined' import { ExtendedTradeRawState } from 'modules/trade/types/TradeRawState' import { useTradeUsdAmounts } from 'modules/usdAmount' @@ -24,14 +24,14 @@ export function useBuildTradeDerivedState(stateAtom: Atom const outputCurrency = useTokenBySymbolOrAddress(rawState.outputCurrencyId) const inputCurrencyAmount = useMemo( () => getCurrencyAmount(inputCurrency, rawState.inputCurrencyAmount), - [inputCurrency, rawState.inputCurrencyAmount] + [inputCurrency, rawState.inputCurrencyAmount], ) const outputCurrencyAmount = useMemo( () => getCurrencyAmount(outputCurrency, rawState.outputCurrencyAmount), - [outputCurrency, rawState.outputCurrencyAmount] + [outputCurrency, rawState.outputCurrencyAmount], ) - const inputCurrencyBalance = useCurrencyAmountBalance(inputCurrency) || null - const outputCurrencyBalance = useCurrencyAmountBalance(outputCurrency) || null + const inputCurrencyBalance = useCurrencyAmountBalanceCombined(inputCurrency) || null + const outputCurrencyBalance = useCurrencyAmountBalanceCombined(outputCurrency) || null const { inputAmount: { value: inputCurrencyFiatAmount }, @@ -61,7 +61,7 @@ export function useBuildTradeDerivedState(stateAtom: Atom function getCurrencyAmount( currency: Nullish | null, - currencyAmount: Nullish + currencyAmount: Nullish, ): CurrencyAmount | null { if (!currency || !currencyAmount) { return null From 5c3f35e63c8593135a51edd4ea4d7bd827192edc Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Tue, 22 Oct 2024 18:47:22 -0300 Subject: [PATCH 17/31] feat: expose balance diff to hook dapp context --- libs/hook-dapp-lib/src/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/hook-dapp-lib/src/types.ts b/libs/hook-dapp-lib/src/types.ts index 57c2c2d65d..d22ed6cc83 100644 --- a/libs/hook-dapp-lib/src/types.ts +++ b/libs/hook-dapp-lib/src/types.ts @@ -57,6 +57,9 @@ export interface HookDappContext { isSmartContract: boolean | undefined isPreHook: boolean isDarkMode: boolean + // { [address: string]: { [token: string]: balanceDiff: string } } + // example: { '0x123': { '0x456': '100', '0xabc': '-100' } } + balancesDiff: Record> } export interface HookDappBase { From 5667d1f64e185c4f51ddaae5646e38491a82952c Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Wed, 23 Oct 2024 17:41:47 -0300 Subject: [PATCH 18/31] chore: add testing console log for balances diff access on hook dapp --- .../src/modules/hooksStore/dapps/BuildHookApp/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx index 55a7afffe2..656fc0d899 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx @@ -38,6 +38,10 @@ export function BuildHookApp({ context }: HookDappProps) { const [hook, setHook] = useState(hookToEdit?.hook || DEFAULT_HOOK_STATE) const [errors, setErrors] = useState>(DEFAULT_ERRORS_STATE) + // JUST FOR TESTING PROPOSES + // REMOVE THIS ON MERGE + console.log({ balanceDiff: context.balancesDiff }) + const validateInput = useCallback((name: keyof CowHook, value: string) => { setErrors((prev) => ({ ...prev, [name]: value.trim() ? '' : `${capitalizeFirstLetter(name)} is required` })) }, []) From aab2974498917a6524806754ceb2511492d4b06a Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Thu, 24 Oct 2024 10:56:50 -0300 Subject: [PATCH 19/31] feat: use tenderly simulation gas on hook creation --- .../appData/updater/AppDataHooksUpdater.ts | 4 +-- .../hooks/useHooksStateWithSimulatedGas.ts | 33 +++++++++++++++++++ .../src/modules/hooksStore/index.ts | 1 + .../src/modules/tenderly/types.ts | 1 + 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts diff --git a/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts b/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts index b58c1e5130..29bc7a012d 100644 --- a/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts +++ b/apps/cowswap-frontend/src/modules/appData/updater/AppDataHooksUpdater.ts @@ -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' @@ -33,7 +33,7 @@ function useAgnosticPermitDataIfUserHasNoAllowance(): Nullish { 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() diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts new file mode 100644 index 0000000000..8fd10b73ba --- /dev/null +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts @@ -0,0 +1,33 @@ +import { useCallback, useMemo } from 'react' + +import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' + +import { BigNumber } from 'ethers' + +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 simulatedGasUsed = tenderlyData?.[hook.uuid]?.gasUsed + if (!simulatedGasUsed || simulatedGasUsed === '0') return hook + const gasLimit = BigNumber.from(simulatedGasUsed).mul(110).div(100).toString() // 10% buffer + const hookData = { ...hook.hook, gasLimit } + 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]) +} diff --git a/apps/cowswap-frontend/src/modules/hooksStore/index.ts b/apps/cowswap-frontend/src/modules/hooksStore/index.ts index 5b922a2406..d54591c6b5 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/index.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/index.ts @@ -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' diff --git a/apps/cowswap-frontend/src/modules/tenderly/types.ts b/apps/cowswap-frontend/src/modules/tenderly/types.ts index 97e50bf537..7c62f33d6b 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/types.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/types.ts @@ -18,6 +18,7 @@ export interface SimulationData { status: boolean id: string cumulativeBalancesDiff: BalancesDiff + gasUsed: string } export interface GetTopTokenHoldersParams { From 468af5551537f1583f80542c35b0db1e51ac8cc2 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Mon, 4 Nov 2024 13:35:39 -0300 Subject: [PATCH 20/31] refactor: implement PR code suggestions --- .../hooks/useCurrencyAmountBalanceCombined.ts | 0 .../hooks/useTokensBalancesCombined.ts | 6 ++-- .../src/modules/combinedBalances/index.ts | 2 ++ .../containers/HookDappContainer/index.tsx | 4 +-- .../hooksStore/dapps/BuildHookApp/index.tsx | 4 --- .../hooksStore/hooks/useBalancesDiff.ts | 33 +++++++++---------- .../hooksStore/pure/AppliedHookItem/index.tsx | 1 + .../swap/containers/SwapWidget/index.tsx | 2 +- .../swap/hooks/useSwapButtonContext.ts | 2 +- .../src/modules/swap/hooks/useSwapState.tsx | 4 +-- .../tenderly/hooks/useGetTopTokenHolders.ts | 5 +-- .../hooks/useTenderlyBundleSimulation.ts | 29 +++++++++------- .../tenderly/utils/generateSimulationData.ts | 33 +++++++++++++++++-- .../modules/tokens/hooks/useEnoughBalance.ts | 2 +- .../containers/SelectTokenWidget/index.tsx | 2 +- .../trade/hooks/useBuildTradeDerivedState.ts | 2 +- 16 files changed, 77 insertions(+), 54 deletions(-) rename apps/cowswap-frontend/src/modules/{swap => combinedBalances}/hooks/useCurrencyAmountBalanceCombined.ts (100%) rename apps/cowswap-frontend/src/modules/{swap => combinedBalances}/hooks/useTokensBalancesCombined.ts (89%) create mode 100644 apps/cowswap-frontend/src/modules/combinedBalances/index.ts diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useCurrencyAmountBalanceCombined.ts b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useCurrencyAmountBalanceCombined.ts similarity index 100% rename from apps/cowswap-frontend/src/modules/swap/hooks/useCurrencyAmountBalanceCombined.ts rename to apps/cowswap-frontend/src/modules/combinedBalances/hooks/useCurrencyAmountBalanceCombined.ts diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useTokensBalancesCombined.ts b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts similarity index 89% rename from apps/cowswap-frontend/src/modules/swap/hooks/useTokensBalancesCombined.ts rename to apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts index c1b82bb963..ff683037da 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useTokensBalancesCombined.ts +++ b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts @@ -14,7 +14,7 @@ export function useTokensBalancesCombined() { return useMemo(() => { if (!account) return tokenBalances - const accountBalancesDiff = preHooksBalancesDiff[account.toLowerCase()] || {} + const accountBalancesDiff = preHooksBalancesDiff[account] || {} return applyBalanceDiffs(tokenBalances, accountBalancesDiff) }, [account, preHooksBalancesDiff, tokenBalances]) } @@ -28,12 +28,12 @@ function applyBalanceDiffs(currentState: BalancesState, diffs: Record provider?.getSigner(), [provider]) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx index 656fc0d899..55a7afffe2 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/dapps/BuildHookApp/index.tsx @@ -38,10 +38,6 @@ export function BuildHookApp({ context }: HookDappProps) { const [hook, setHook] = useState(hookToEdit?.hook || DEFAULT_HOOK_STATE) const [errors, setErrors] = useState>(DEFAULT_ERRORS_STATE) - // JUST FOR TESTING PROPOSES - // REMOVE THIS ON MERGE - console.log({ balanceDiff: context.balancesDiff }) - const validateInput = useCallback((name: keyof CowHook, value: string) => { setErrors((prev) => ({ ...prev, [name]: value.trim() ? '' : `${capitalizeFirstLetter(name)} is required` })) }, []) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts index a44371fa32..4a78781799 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts @@ -3,7 +3,6 @@ import { useOrderParams } from './useOrderParams' import { useHooks } from './useHooks' import { useMemo } from 'react' import { useWalletInfo } from '@cowprotocol/wallet' -import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { BalancesDiff } from 'modules/tenderly/types' import { BigNumber } from 'ethers' @@ -23,7 +22,7 @@ export function usePreHookBalanceDiff(): BalancesDiff { } // Returns all the ERC20 Balance Diff of the current hook to be passed to the iframe context -export function useHookBalancesDiff(isPreHook: boolean, hookToEditDetails?: CowHookDetails): BalancesDiff { +export function useHookBalancesDiff(isPreHook: boolean, hookToEditUid?: string): BalancesDiff { const { account } = useWalletInfo() const { data } = useTenderlyBundleSimulation() const orderParams = useOrderParams() @@ -35,10 +34,10 @@ export function useHookBalancesDiff(isPreHook: boolean, hookToEditDetails?: CowH const balanceDiff: Record = {} if (orderParams?.buyAmount && orderParams.buyTokenAddress && account) - balanceDiff[orderParams.buyTokenAddress] = orderParams.buyAmount + balanceDiff[orderParams.buyTokenAddress.toLowerCase()] = orderParams.buyAmount if (orderParams?.sellAmount && orderParams.sellTokenAddress && account) - balanceDiff[orderParams.sellTokenAddress] = `-${orderParams.sellAmount}` + balanceDiff[orderParams.sellTokenAddress.toLowerCase()] = `-${orderParams.sellAmount}` return { account: balanceDiff } }, [orderParams, account]) @@ -56,11 +55,11 @@ export function useHookBalancesDiff(isPreHook: boolean, hookToEditDetails?: CowH }, [data, postHooks, orderMockBalanceDiff, preHookBalanceDiff]) const hookToEditBalanceDiff = useMemo(() => { - if (!data || !hookToEditDetails?.uuid) return EMPTY_BALANCE_DIFF + if (!data || !hookToEditUid) return EMPTY_BALANCE_DIFF const otherHooks = isPreHook ? preHooks : postHooks - const hookToEditIndex = otherHooks.findIndex((hook) => hook.uuid === hookToEditDetails.uuid) + const hookToEditIndex = otherHooks.findIndex((hook) => hook.uuid === hookToEditUid) // is editing first preHook -> return empty state if (!hookToEditIndex && isPreHook) return EMPTY_BALANCE_DIFF @@ -72,27 +71,25 @@ export function useHookBalancesDiff(isPreHook: boolean, hookToEditDetails?: CowH const previousHookIndex = hookToEditIndex - 1 return data[otherHooks[previousHookIndex]?.uuid]?.cumulativeBalancesDiff || EMPTY_BALANCE_DIFF - }, [data, hookToEditDetails, isPreHook, preHooks, postHooks, firstPostHookBalanceDiff]) + }, [data, hookToEditUid, isPreHook, preHooks, postHooks, firstPostHookBalanceDiff]) return useMemo(() => { - if (hookToEditDetails?.uuid) return hookToEditBalanceDiff + if (hookToEditUid) return hookToEditBalanceDiff if (isPreHook) return preHookBalanceDiff return postHookBalanceDiff }, [data, orderParams, preHooks, postHooks]) } -function mergeBalanceDiffs( - first: Record>, - second: Record>, -): Record> { - const result: Record> = {} +const addBigNumberStrings = (a: string, b: string): string => { + const bigA = BigNumber.from(a) + const bigB = BigNumber.from(b) + return bigA.add(bigB).toString() +} + +function mergeBalanceDiffs(first: BalancesDiff, second: BalancesDiff): BalancesDiff { + const result: BalancesDiff = {} // Helper function to add BigNumber strings - const addBigNumberStrings = (a: string, b: string): string => { - const bigA = BigNumber.from(a) - const bigB = BigNumber.from(b) - return bigA.add(bigB).toString() - } // Process all addresses from first input for (const address of Object.keys(first)) { 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 d30afa2a05..8615589ac1 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -34,6 +34,7 @@ export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHoo const { isValidating, data } = useTenderlyBundleSimulation() const simulationData = useMemo(() => { + console.log({ data, uuid: hookDetails.uuid }) if (!data) return return data[hookDetails.uuid] }, [data, hookDetails.uuid]) diff --git a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx index 66015a3597..af1def7605 100644 --- a/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/swap/containers/SwapWidget/index.tsx @@ -12,10 +12,10 @@ import { ApplicationModal } from 'legacy/state/application/reducer' import { Field } from 'legacy/state/types' import { useRecipientToggleManager, useUserTransactionTTL } from 'legacy/state/user/hooks' +import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { EthFlowModal, EthFlowProps } from 'modules/swap/containers/EthFlow' import { SwapModals, SwapModalsProps } from 'modules/swap/containers/SwapModals' -import { useCurrencyAmountBalanceCombined } from 'modules/swap/hooks/useCurrencyAmountBalanceCombined' import { useShowRecipientControls } from 'modules/swap/hooks/useShowRecipientControls' import { useSwapButtonContext } from 'modules/swap/hooks/useSwapButtonContext' import { useSwapCurrenciesAmounts } from 'modules/swap/hooks/useSwapCurrenciesAmounts' diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts index 6561f7be64..7b2b65ae8b 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapButtonContext.ts @@ -16,6 +16,7 @@ import { useToggleWalletModal } from 'legacy/state/application/hooks' import { useGetQuoteAndStatus, useIsBestQuoteLoading } from 'legacy/state/price/hooks' import { Field } from 'legacy/state/types' +import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { useInjectedWidgetParams } from 'modules/injectedWidget' import { useTokenSupportsPermit } from 'modules/permit' import { getSwapButtonState } from 'modules/swap/helpers/getSwapButtonState' @@ -29,7 +30,6 @@ import { QuoteDeadlineParams } from 'modules/tradeQuote' import { useApproveState } from 'common/hooks/useApproveState' import { useSafeMemo } from 'common/hooks/useSafeMemo' -import { useCurrencyAmountBalanceCombined } from './useCurrencyAmountBalanceCombined' import { useHandleSwapOrEthFlow } from './useHandleSwapOrEthFlow' import { useDerivedSwapInfo, useSwapActionHandlers } from './useSwapState' diff --git a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx index 95a61e7d53..c73ed33327 100644 --- a/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx +++ b/apps/cowswap-frontend/src/modules/swap/hooks/useSwapState.tsx @@ -1,6 +1,5 @@ import { useCallback, useMemo } from 'react' -// import { useCurrencyAmountBalance } from '@cowprotocol/balances-and-allowances' import { formatSymbol, getIsNativeToken, isAddress, tryParseCurrencyAmount } from '@cowprotocol/common-utils' import { useENS } from '@cowprotocol/ens' import { useAreThereTokensWithSameSymbol, useTokenBySymbolOrAddress } from '@cowprotocol/tokens' @@ -19,6 +18,7 @@ import { isWrappingTrade } from 'legacy/state/swap/utils' import { Field } from 'legacy/state/types' import { changeSwapAmountAnalytics, switchTokensAnalytics } from 'modules/analytics' +import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import type { TradeWidgetActions } from 'modules/trade' import { useNavigateOnCurrencySelection } from 'modules/trade/hooks/useNavigateOnCurrencySelection' import { useTradeNavigate } from 'modules/trade/hooks/useTradeNavigate' @@ -28,8 +28,6 @@ import { useVolumeFee } from 'modules/volumeFee' import { useIsProviderNetworkUnsupported } from 'common/hooks/useIsProviderNetworkUnsupported' import { useSafeMemo } from 'common/hooks/useSafeMemo' -import { useCurrencyAmountBalanceCombined } from './useCurrencyAmountBalanceCombined' - export const BAD_RECIPIENT_ADDRESSES: { [address: string]: true } = { '0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f': true, // v2 factory '0xf164fC0Ec4E93095b804a4795bBe1e041497b92a': true, // v2 router 01 diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts index 194a086a11..3f074a8009 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useGetTopTokenHolders.ts @@ -10,10 +10,7 @@ export function useGetTopTokenHolders() { return useCallback( async (params: GetTopTokenHoldersParams) => { const key = `${params.chainId}-${params.tokenAddress}` - if (cachedData[key]?.value) { - return cachedData[key].value - } - return fetchTopTokenHolders(params) + return cachedData[key]?.value || fetchTopTokenHolders(params) }, [cachedData, fetchTopTokenHolders], ) diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index 7ab1478887..7addcfdf35 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react' +import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { useWalletInfo } from '@cowprotocol/wallet' import useSWR from 'swr' @@ -78,22 +79,26 @@ export function useTenderlyBundleSimulation() { tokenSell, ]) - const getNewSimulationData = useCallback(async () => { - try { - const simulationData = await simulateBundle() + const getNewSimulationData = useCallback( + async ([_, preHooks, postHooks]: [string, CowHookDetails[], CowHookDetails[]]) => { + console.log({ _, preHooks, postHooks }) + try { + const simulationData = await simulateBundle() - if (!simulationData) { - return {} - } + if (!simulationData) { + return {} + } - return generateNewSimulationData(simulationData, { preHooks, postHooks }) - } catch { - return generateSimulationDataToError({ preHooks, postHooks }) - } - }, [preHooks, postHooks, simulateBundle]) + return generateNewSimulationData(simulationData, { preHooks, postHooks }) + } catch { + return generateSimulationDataToError({ preHooks, postHooks }) + } + }, + [simulateBundle], + ) const { data, isValidating: isBundleSimulationLoading } = useSWR( - ['tenderly-bundle-simulation', postHooks, preHooks, orderParams?.sellTokenAddress, orderParams?.buyTokenAddress], + ['tenderly-bundle-simulation', preHooks, postHooks], getNewSimulationData, { revalidateOnFocus: false, diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts index 5d3416b736..f2effe861f 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts @@ -1,6 +1,6 @@ import { PostBundleSimulationParams } from './bundleSimulation' -import { SimulationData } from '../types' +import { BalancesDiff, SimulationData } from '../types' export function generateSimulationDataToError( postParams: Pick, @@ -18,16 +18,43 @@ export function generateSimulationDataToError( ) } +function convertBalanceDiffToLowerCaseKeys(data: BalancesDiff): BalancesDiff { + console.log({ oi: 'oiiiii', data }) + return Object.entries(data).reduce((acc, [outerKey, innerObj]) => { + const lowerOuterKey = outerKey.toLowerCase() + console.log({ lowerOuterKey, innerObj }) + + const processedInnerObj = Object.entries(innerObj || {}).reduce((innerAcc, [innerKey, value]) => { + const lowerInnerKey = innerKey.toLowerCase() + return { + ...innerAcc, + [lowerInnerKey]: value, + } + }, {}) + + return { + ...acc, + [lowerOuterKey]: processedInnerObj, + } + }, {}) +} + export function generateNewSimulationData( simulationData: SimulationData[], postParams: Pick, ): Record { + console.log({ simulationData, postParams }) const preHooksKeys = postParams.preHooks.map((hookDetails) => hookDetails.uuid) const postHooksKeys = postParams.postHooks.map((hookDetails) => hookDetails.uuid) - const preHooksData = simulationData.slice(0, preHooksKeys.length) + const simulationDataWithLowerCaseBalanceDiffKeys = simulationData.map((data) => ({ + ...data, + cumulativeBalancesDiff: convertBalanceDiffToLowerCaseKeys(data.cumulativeBalancesDiff), + })) + + const preHooksData = simulationDataWithLowerCaseBalanceDiffKeys.slice(0, preHooksKeys.length) - const postHooksData = simulationData.slice(-postHooksKeys.length) + const postHooksData = simulationDataWithLowerCaseBalanceDiffKeys.slice(-postHooksKeys.length) return { ...preHooksKeys.reduce((acc, key, index) => ({ ...acc, [key]: preHooksData[index] }), {}), diff --git a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts index a4a50b3611..dde426c17a 100644 --- a/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts +++ b/apps/cowswap-frontend/src/modules/tokens/hooks/useEnoughBalance.ts @@ -2,7 +2,7 @@ import { AllowancesState, BalancesState, useTokensAllowances } from '@cowprotoco import { isEnoughAmount, getAddress, getIsNativeToken, getWrappedToken } from '@cowprotocol/common-utils' import { Currency, CurrencyAmount } from '@uniswap/sdk-core' -import { useTokensBalancesCombined } from 'modules/swap/hooks/useTokensBalancesCombined' +import { useTokensBalancesCombined } from 'modules/combinedBalances' export interface UseEnoughBalanceParams { /** diff --git a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx index 850028fc58..3ea98e232c 100644 --- a/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx +++ b/apps/cowswap-frontend/src/modules/tokensList/containers/SelectTokenWidget/index.tsx @@ -20,8 +20,8 @@ import styled from 'styled-components/macro' import { Field } from 'legacy/state/types' import { addListAnalytics } from 'modules/analytics' +import { useTokensBalancesCombined } from 'modules/combinedBalances' import { usePermitCompatibleTokens } from 'modules/permit' -import { useTokensBalancesCombined } from 'modules/swap/hooks/useTokensBalancesCombined' import { useLpTokensWithBalances } from 'modules/yield/shared' import { getDefaultTokenListCategories } from './getDefaultTokenListCategories' diff --git a/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts index b5db63dc32..5475d25914 100644 --- a/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts +++ b/apps/cowswap-frontend/src/modules/trade/hooks/useBuildTradeDerivedState.ts @@ -7,7 +7,7 @@ import { Currency, CurrencyAmount } from '@uniswap/sdk-core' import { Nullish } from 'types' -import { useCurrencyAmountBalanceCombined } from 'modules/swap/hooks/useCurrencyAmountBalanceCombined' +import { useCurrencyAmountBalanceCombined } from 'modules/combinedBalances' import { ExtendedTradeRawState } from 'modules/trade/types/TradeRawState' import { useTradeUsdAmounts } from 'modules/usdAmount' From eff88c72a76a08d790c46f7e049623d279396c0a Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Mon, 4 Nov 2024 14:09:50 -0300 Subject: [PATCH 21/31] chore: remove console logs --- .../src/modules/hooksStore/pure/AppliedHookItem/index.tsx | 1 - .../src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts | 1 - .../src/modules/tenderly/utils/generateSimulationData.ts | 3 --- 3 files changed, 5 deletions(-) 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 8615589ac1..d30afa2a05 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -34,7 +34,6 @@ export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHoo const { isValidating, data } = useTenderlyBundleSimulation() const simulationData = useMemo(() => { - console.log({ data, uuid: hookDetails.uuid }) if (!data) return return data[hookDetails.uuid] }, [data, hookDetails.uuid]) diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index 7addcfdf35..dcf3633d9c 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -81,7 +81,6 @@ export function useTenderlyBundleSimulation() { const getNewSimulationData = useCallback( async ([_, preHooks, postHooks]: [string, CowHookDetails[], CowHookDetails[]]) => { - console.log({ _, preHooks, postHooks }) try { const simulationData = await simulateBundle() diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts index f2effe861f..74eed3d7f3 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts @@ -19,10 +19,8 @@ export function generateSimulationDataToError( } function convertBalanceDiffToLowerCaseKeys(data: BalancesDiff): BalancesDiff { - console.log({ oi: 'oiiiii', data }) return Object.entries(data).reduce((acc, [outerKey, innerObj]) => { const lowerOuterKey = outerKey.toLowerCase() - console.log({ lowerOuterKey, innerObj }) const processedInnerObj = Object.entries(innerObj || {}).reduce((innerAcc, [innerKey, value]) => { const lowerInnerKey = innerKey.toLowerCase() @@ -43,7 +41,6 @@ export function generateNewSimulationData( simulationData: SimulationData[], postParams: Pick, ): Record { - console.log({ simulationData, postParams }) const preHooksKeys = postParams.preHooks.map((hookDetails) => hookDetails.uuid) const postHooksKeys = postParams.postHooks.map((hookDetails) => hookDetails.uuid) From dd857e3792854dd0229818b2ef76f911f993a385 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Mon, 4 Nov 2024 14:10:07 -0300 Subject: [PATCH 22/31] fix: use lower case address on combined balance hook --- .../modules/combinedBalances/hooks/useTokensBalancesCombined.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts index ff683037da..fc74d8282c 100644 --- a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts +++ b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts @@ -14,7 +14,7 @@ export function useTokensBalancesCombined() { return useMemo(() => { if (!account) return tokenBalances - const accountBalancesDiff = preHooksBalancesDiff[account] || {} + const accountBalancesDiff = preHooksBalancesDiff[account.toLowerCase()] || {} return applyBalanceDiffs(tokenBalances, accountBalancesDiff) }, [account, preHooksBalancesDiff, tokenBalances]) } From 2a123c5579f86e4c4dc7c94b5f60383603fef580 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Mon, 4 Nov 2024 14:20:37 -0300 Subject: [PATCH 23/31] fix: not use combined balance on non hook trade type --- .../combinedBalances/hooks/useTokensBalancesCombined.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts index fc74d8282c..c6df7dcdf5 100644 --- a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts +++ b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts @@ -6,17 +6,19 @@ 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) return tokenBalances + if (!account || !isHooksTradeType) return tokenBalances const accountBalancesDiff = preHooksBalancesDiff[account.toLowerCase()] || {} return applyBalanceDiffs(tokenBalances, accountBalancesDiff) - }, [account, preHooksBalancesDiff, tokenBalances]) + }, [account, preHooksBalancesDiff, tokenBalances, isHooksTradeType]) } function applyBalanceDiffs(currentState: BalancesState, diffs: Record): BalancesState { From 4e9741f15fa1415ba10b76e85f6bd93d83a65736 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Wed, 6 Nov 2024 10:26:50 -0300 Subject: [PATCH 24/31] fix: trigger simulation on order change if simulation data is null --- .../hooksStore/pure/AppliedHookItem/index.tsx | 16 ++++++++-- .../hooks/useTenderlyBundleSimulation.ts | 29 ++++++++----------- 2 files changed, 26 insertions(+), 19 deletions(-) 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 d30afa2a05..570ae2dba4 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useEffect, 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' @@ -16,6 +16,7 @@ import * as styledEl from './styled' import { TenderlySimulate } from '../../containers/TenderlySimulate' import { HookDapp } from '../../types/hooks' +import { useOrderParams } from 'modules/hooksStore/hooks/useOrderParams' interface HookItemProp { account: string | undefined @@ -31,13 +32,24 @@ 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 orderParams = useOrderParams() const simulationData = useMemo(() => { if (!data) return return data[hookDetails.uuid] }, [data, hookDetails.uuid]) + // Run simulation on orderParams if no data is available + // This is a fallback for the flow where the user + // first adds a post hook and then setup the order + useEffect(() => { + if (isPreHook) return + if (isValidating) return + if (Object.keys(data || {}).length) return + mutate() + }, [orderParams, data, mutate]) + const simulationStatus = simulationData?.status ? 'Simulation successful' : 'Simulation failed' const simulationTooltip = simulationData?.status ? 'The Tenderly simulation was successful. Your transaction is expected to succeed.' diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index dcf3633d9c..82d5db1456 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -81,30 +81,25 @@ export function useTenderlyBundleSimulation() { const getNewSimulationData = useCallback( async ([_, preHooks, postHooks]: [string, CowHookDetails[], CowHookDetails[]]) => { - try { - const simulationData = await simulateBundle() + const simulationData = await simulateBundle() - if (!simulationData) { - return {} - } + if (!simulationData) { + return {} + } + try { return generateNewSimulationData(simulationData, { preHooks, postHooks }) - } catch { + } catch (e) { + console.log(`error`, { e, simulationData }) return generateSimulationDataToError({ preHooks, postHooks }) } }, [simulateBundle], ) - const { data, isValidating: isBundleSimulationLoading } = useSWR( - ['tenderly-bundle-simulation', preHooks, postHooks], - getNewSimulationData, - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }, - ) - - return { data, isValidating: isBundleSimulationLoading } + return useSWR(['tenderly-bundle-simulation', preHooks, postHooks], getNewSimulationData, { + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }) } From 48da7de1fa4b1fbe2478339401641f072f25a8e9 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Wed, 6 Nov 2024 10:50:27 -0300 Subject: [PATCH 25/31] refactor: simulate hooks even if order isnt filled --- .../hooksStore/pure/AppliedHookItem/index.tsx | 16 ++------------ .../hooks/useTenderlyBundleSimulation.ts | 21 +++++++++++++------ .../tenderly/utils/bundleSimulation.ts | 7 ++++--- 3 files changed, 21 insertions(+), 23 deletions(-) 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 570ae2dba4..d30afa2a05 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo } from 'react' +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' @@ -16,7 +16,6 @@ import * as styledEl from './styled' import { TenderlySimulate } from '../../containers/TenderlySimulate' import { HookDapp } from '../../types/hooks' -import { useOrderParams } from 'modules/hooksStore/hooks/useOrderParams' interface HookItemProp { account: string | undefined @@ -32,24 +31,13 @@ interface HookItemProp { const isBundleSimulationReady = true export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHook, removeHook, index }: HookItemProp) { - const { isValidating, data, mutate } = useTenderlyBundleSimulation() - const orderParams = useOrderParams() + const { isValidating, data } = useTenderlyBundleSimulation() const simulationData = useMemo(() => { if (!data) return return data[hookDetails.uuid] }, [data, hookDetails.uuid]) - // Run simulation on orderParams if no data is available - // This is a fallback for the flow where the user - // first adds a post hook and then setup the order - useEffect(() => { - if (isPreHook) return - if (isValidating) return - if (Object.keys(data || {}).length) return - mutate() - }, [orderParams, data, mutate]) - const simulationStatus = simulationData?.status ? 'Simulation successful' : 'Simulation failed' const simulationTooltip = simulationData?.status ? 'The Tenderly simulation was successful. Your transaction is expected to succeed.' diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index 82d5db1456..bf6e0bfa68 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useMemo } from 'react' import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { useWalletInfo } from '@cowprotocol/wallet' @@ -12,7 +12,7 @@ import { useTokenContract } from 'common/hooks/useContract' import { useGetTopTokenHolders } from './useGetTopTokenHolders' -import { completeBundleSimulation, preHooksBundleSimulation } from '../utils/bundleSimulation' +import { completeBundleSimulation, hooksBundleSimulation } from '../utils/bundleSimulation' import { generateNewSimulationData, generateSimulationDataToError } from '../utils/generateSimulationData' import { getTokenTransferInfo } from '../utils/getTokenTransferInfo' @@ -28,17 +28,26 @@ export function useTenderlyBundleSimulation() { const getTopTokenHolder = useGetTopTokenHolders() + const isOrderComplete = useMemo(() => { + return !!account && !!tokenBuy && !!tokenSell && !!buyAmount && !!sellAmount && !!orderReceiver + }, [account, tokenBuy, tokenSell, buyAmount, sellAmount, orderReceiver]) + const simulateBundle = useCallback(async () => { if (postHooks.length === 0 && preHooks.length === 0) return if (!postHooks.length) - return preHooksBundleSimulation({ + return hooksBundleSimulation({ chainId, preHooks, + postHooks: [], }) if (!account || !tokenBuy || !tokenSell || !buyAmount || !sellAmount || !orderReceiver) { - return + return hooksBundleSimulation({ + chainId, + preHooks, + postHooks, + }) } const buyTokenTopHolders = await getTopTokenHolder({ @@ -80,7 +89,7 @@ export function useTenderlyBundleSimulation() { ]) const getNewSimulationData = useCallback( - async ([_, preHooks, postHooks]: [string, CowHookDetails[], CowHookDetails[]]) => { + async ([_, preHooks, postHooks, _isOrderComplete]: [string, CowHookDetails[], CowHookDetails[], boolean]) => { const simulationData = await simulateBundle() if (!simulationData) { @@ -97,7 +106,7 @@ export function useTenderlyBundleSimulation() { [simulateBundle], ) - return useSWR(['tenderly-bundle-simulation', preHooks, postHooks], getNewSimulationData, { + return useSWR(['tenderly-bundle-simulation', preHooks, postHooks, isOrderComplete], getNewSimulationData, { revalidateOnFocus: false, revalidateOnReconnect: false, refreshWhenOffline: false, diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts index 63d7c8dd0a..ea6f50e751 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts @@ -35,10 +35,11 @@ export const completeBundleSimulation = async (params: PostBundleSimulationParam return simulateBundle(input, params.chainId) } -export const preHooksBundleSimulation = async ( - params: Pick, +export const hooksBundleSimulation = async ( + params: Pick, ): Promise => { - const input = params.preHooks.map((hook) => + const hooks = [...params.preHooks, ...params.postHooks] + const input = hooks.map((hook) => getCoWHookTenderlySimulationInput(COW_PROTOCOL_SETTLEMENT_CONTRACT_ADDRESS[params.chainId], hook.hook), ) return simulateBundle(input, params.chainId) From 74a8c0c7b4998856404fea6c15aa62f3ba6bbeb4 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Thu, 7 Nov 2024 14:20:07 -0300 Subject: [PATCH 26/31] chore: improve variable documentation and simplify functions --- .../hooks/useTokensBalancesCombined.ts | 49 +++++++------------ .../hooksStore/hooks/useBalancesDiff.ts | 18 +++---- .../hooks/useHooksStateWithSimulatedGas.ts | 4 +- .../tenderly/utils/generateSimulationData.ts | 10 ++-- 4 files changed, 32 insertions(+), 49 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts index c6df7dcdf5..0ad8abb306 100644 --- a/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts +++ b/apps/cowswap-frontend/src/modules/combinedBalances/hooks/useTokensBalancesCombined.ts @@ -21,40 +21,25 @@ export function useTokensBalancesCombined() { }, [account, preHooksBalancesDiff, tokenBalances, isHooksTradeType]) } -function applyBalanceDiffs(currentState: BalancesState, diffs: Record): BalancesState { - const normalizedValues: Record = {} +function applyBalanceDiffs(currentBalances: BalancesState, balanceDiff: Record): 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, + ) - // Create normalized versions of inputs - const normalizedCurrentValues: Record = {} - const normalizedDiffs: Record = {} - - // Normalize current state values - for (const address of Object.keys(currentState.values)) { - normalizedCurrentValues[address] = currentState.values[address] - } - - // Normalize diffs - for (const address of Object.keys(diffs)) { - normalizedDiffs[address] = diffs[address] - } - - // Process all addresses in the normalized current state - for (const address of Object.keys(normalizedCurrentValues)) { - const currentBalance = normalizedCurrentValues[address] || BigNumber.from(0) - const diff = normalizedDiffs[address] ? BigNumber.from(normalizedDiffs[address]) : BigNumber.from(0) - normalizedValues[address] = currentBalance.add(diff) - } - - // Process any new addresses from diffs that weren't in the current state - for (const address of Object.keys(normalizedDiffs)) { - if (!normalizedCurrentValues.hasOwnProperty(address)) { - normalizedValues[address] = BigNumber.from(normalizedDiffs[address]) - } - } - - // Return new state object maintaining the isLoading property return { - isLoading: currentState.isLoading, + isLoading: currentBalances.isLoading, values: normalizedValues, } } diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts index 4a78781799..fe9557ecf4 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useBalancesDiff.ts @@ -29,7 +29,9 @@ export function useHookBalancesDiff(isPreHook: boolean, hookToEditUid?: string): const { preHooks, postHooks } = useHooks() const preHookBalanceDiff = usePreHookBalanceDiff() - const orderMockBalanceDiff = useMemo(() => { + // 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 = {} @@ -43,8 +45,8 @@ export function useHookBalancesDiff(isPreHook: boolean, hookToEditUid?: string): }, [orderParams, account]) const firstPostHookBalanceDiff = useMemo(() => { - return mergeBalanceDiffs(preHookBalanceDiff, orderMockBalanceDiff) - }, [preHookBalanceDiff, orderMockBalanceDiff]) + return mergeBalanceDiffs(preHookBalanceDiff, orderExpectedBalanceDiff) + }, [preHookBalanceDiff, orderExpectedBalanceDiff]) const postHookBalanceDiff = useMemo(() => { // is adding the first post hook or simulation not available @@ -52,7 +54,7 @@ export function useHookBalancesDiff(isPreHook: boolean, hookToEditUid?: string): const lastPostHook = postHooks[postHooks.length - 1] return data[lastPostHook?.uuid]?.cumulativeBalancesDiff || firstPostHookBalanceDiff - }, [data, postHooks, orderMockBalanceDiff, preHookBalanceDiff]) + }, [data, postHooks, orderExpectedBalanceDiff, preHookBalanceDiff]) const hookToEditBalanceDiff = useMemo(() => { if (!data || !hookToEditUid) return EMPTY_BALANCE_DIFF @@ -80,12 +82,6 @@ export function useHookBalancesDiff(isPreHook: boolean, hookToEditUid?: string): }, [data, orderParams, preHooks, postHooks]) } -const addBigNumberStrings = (a: string, b: string): string => { - const bigA = BigNumber.from(a) - const bigB = BigNumber.from(b) - return bigA.add(bigB).toString() -} - function mergeBalanceDiffs(first: BalancesDiff, second: BalancesDiff): BalancesDiff { const result: BalancesDiff = {} @@ -110,7 +106,7 @@ function mergeBalanceDiffs(first: BalancesDiff, second: BalancesDiff): BalancesD } else { // If token exists, sum up the balances try { - result[address][token] = addBigNumberStrings(result[address][token], second[address][token]) + 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 diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts index 8fd10b73ba..c1c919066f 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts @@ -10,6 +10,8 @@ import { useHooks } from './useHooks' import { HooksStoreState } from '../state/hookDetailsAtom' +const GAS_BUFFER_PERCENTAGE = 110 // 10% buffer + export function useHooksStateWithSimulatedGas(): HooksStoreState { const hooksRaw = useHooks() const { data: tenderlyData } = useTenderlyBundleSimulation() @@ -18,7 +20,7 @@ export function useHooksStateWithSimulatedGas(): HooksStoreState { (hook: CowHookDetails): CowHookDetails => { const simulatedGasUsed = tenderlyData?.[hook.uuid]?.gasUsed if (!simulatedGasUsed || simulatedGasUsed === '0') return hook - const gasLimit = BigNumber.from(simulatedGasUsed).mul(110).div(100).toString() // 10% buffer + const gasLimit = BigNumber.from(simulatedGasUsed).mul(GAS_BUFFER_PERCENTAGE).div(100).toString() const hookData = { ...hook.hook, gasLimit } return { ...hook, hook: hookData } }, diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts index 74eed3d7f3..54b9f11ab1 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/generateSimulationData.ts @@ -19,14 +19,14 @@ export function generateSimulationDataToError( } function convertBalanceDiffToLowerCaseKeys(data: BalancesDiff): BalancesDiff { - return Object.entries(data).reduce((acc, [outerKey, innerObj]) => { - const lowerOuterKey = outerKey.toLowerCase() + return Object.entries(data).reduce((acc, [tokenHolder, tokenHolderDiffs]) => { + const lowerOuterKey = tokenHolder.toLowerCase() - const processedInnerObj = Object.entries(innerObj || {}).reduce((innerAcc, [innerKey, value]) => { - const lowerInnerKey = innerKey.toLowerCase() + const processedInnerObj = Object.entries(tokenHolderDiffs || {}).reduce((innerAcc, [tokenAddress, balanceDiff]) => { + const lowerInnerKey = tokenAddress.toLowerCase() return { ...innerAcc, - [lowerInnerKey]: value, + [lowerInnerKey]: balanceDiff, } }, {}) From 06e8f329cf08790a1b7fa8f2b2b096bb78cfbc26 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Thu, 7 Nov 2024 14:55:03 -0300 Subject: [PATCH 27/31] chore: remove simulated gas buffer --- .../hooksStore/hooks/useHooksStateWithSimulatedGas.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts index c1c919066f..105eb1a7cf 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts @@ -2,16 +2,12 @@ import { useCallback, useMemo } from 'react' import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' -import { BigNumber } from 'ethers' - import { useTenderlyBundleSimulation } from 'modules/tenderly/hooks/useTenderlyBundleSimulation' import { useHooks } from './useHooks' import { HooksStoreState } from '../state/hookDetailsAtom' -const GAS_BUFFER_PERCENTAGE = 110 // 10% buffer - export function useHooksStateWithSimulatedGas(): HooksStoreState { const hooksRaw = useHooks() const { data: tenderlyData } = useTenderlyBundleSimulation() @@ -20,8 +16,7 @@ export function useHooksStateWithSimulatedGas(): HooksStoreState { (hook: CowHookDetails): CowHookDetails => { const simulatedGasUsed = tenderlyData?.[hook.uuid]?.gasUsed if (!simulatedGasUsed || simulatedGasUsed === '0') return hook - const gasLimit = BigNumber.from(simulatedGasUsed).mul(GAS_BUFFER_PERCENTAGE).div(100).toString() - const hookData = { ...hook.hook, gasLimit } + const hookData = { ...hook.hook, gasLimit: simulatedGasUsed } return { ...hook, hook: hookData } }, [tenderlyData], From f2bc296ef48d7586c404a05de6567f89eb3a0e46 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Fri, 8 Nov 2024 12:04:24 -0300 Subject: [PATCH 28/31] feat: add refresh simulation button --- .../src/modules/hooksStore/pure/AppliedHookItem/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 d30afa2a05..362c2f0f33 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx +++ b/apps/cowswap-frontend/src/modules/hooksStore/pure/AppliedHookItem/index.tsx @@ -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' @@ -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 @@ -56,6 +56,9 @@ export function AppliedHookItem({ account, hookDetails, dapp, isPreHook, editHoo {isValidating && } + mutate()} disabled={isValidating}> + + editHook(hookDetails.uuid)}> From e30bfc4bfde4b5e00d5de584c6c0e9c08867d9c0 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Fri, 8 Nov 2024 12:04:30 -0300 Subject: [PATCH 29/31] fix: not use tenderly gas used if failed --- .../hooksStore/hooks/useHooksStateWithSimulatedGas.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts index 105eb1a7cf..0bb2ba3aa2 100644 --- a/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts +++ b/apps/cowswap-frontend/src/modules/hooksStore/hooks/useHooksStateWithSimulatedGas.ts @@ -14,9 +14,9 @@ export function useHooksStateWithSimulatedGas(): HooksStoreState { const combineHookWithSimulatedGas = useCallback( (hook: CowHookDetails): CowHookDetails => { - const simulatedGasUsed = tenderlyData?.[hook.uuid]?.gasUsed - if (!simulatedGasUsed || simulatedGasUsed === '0') return hook - const hookData = { ...hook.hook, gasLimit: simulatedGasUsed } + 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], From 1745ea60e2fd27d021765938e55cc7d8ce95b684 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Fri, 8 Nov 2024 12:04:34 -0300 Subject: [PATCH 30/31] fix: infinite load on order change --- .../hooks/useTenderlyBundleSimulation.ts | 146 +++++++++--------- .../tenderly/utils/bundleSimulation.ts | 15 +- 2 files changed, 84 insertions(+), 77 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts index bf6e0bfa68..9749c0e8f9 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/hooks/useTenderlyBundleSimulation.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react' +import { useCallback } from 'react' import { CowHookDetails } from '@cowprotocol/hook-dapp-lib' import { useWalletInfo } from '@cowprotocol/wallet' @@ -8,107 +8,111 @@ 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, hooksBundleSimulation } from '../utils/bundleSimulation' import { generateNewSimulationData, generateSimulationDataToError } from '../utils/generateSimulationData' import { getTokenTransferInfo } from '../utils/getTokenTransferInfo' +type BundleSimulationSwrParams = { + preHooks: CowHookDetails[] + postHooks: CowHookDetails[] +} + 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 isOrderComplete = useMemo(() => { - return !!account && !!tokenBuy && !!tokenSell && !!buyAmount && !!sellAmount && !!orderReceiver - }, [account, tokenBuy, tokenSell, buyAmount, sellAmount, orderReceiver]) - - const simulateBundle = useCallback(async () => { - if (postHooks.length === 0 && preHooks.length === 0) return + const simulateBundle = useCallback( + async ({ preHooks, postHooks }: BundleSimulationSwrParams) => { + if (postHooks.length === 0 && preHooks.length === 0) return + + if (!postHooks.length) + return hooksBundleSimulation({ + chainId, + preHooks, + postHooks: [], + }) + + if ( + !account || + !orderParams?.buyTokenAddress || + !orderParams?.sellTokenAddress || + !orderParams?.buyAmount || + !orderParams?.sellAmount || + !orderParams?.receiver + ) { + return hooksBundleSimulation({ + chainId, + preHooks, + postHooks, + }) + } - if (!postHooks.length) - return hooksBundleSimulation({ + const buyTokenTopHolders = await getTopTokenHolder({ + tokenAddress: orderParams.buyTokenAddress, chainId, - preHooks, - postHooks: [], }) - if (!account || !tokenBuy || !tokenSell || !buyAmount || !sellAmount || !orderReceiver) { - return hooksBundleSimulation({ - chainId, - preHooks, - postHooks, + if (!buyTokenTopHolders) return + + const tokenBuyTransferInfo = getTokenTransferInfo({ + tokenHolders: buyTokenTopHolders, + amountToTransfer: orderParams.buyAmount, }) - } - - 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 paramsComplete = { + postHooks, + preHooks, + tokenBuy: orderParams.buyTokenAddress, + tokenBuyTransferInfo, + sellAmount: orderParams.sellAmount, + buyAmount: orderParams.buyAmount, + orderReceiver: orderParams.receiver, + tokenSell: orderParams.sellTokenAddress, + account, + chainId, + } + + return completeBundleSimulation(paramsComplete) + }, + [account, chainId, getTopTokenHolder, orderParams], + ) const getNewSimulationData = useCallback( - async ([_, preHooks, postHooks, _isOrderComplete]: [string, CowHookDetails[], CowHookDetails[], boolean]) => { - const simulationData = await simulateBundle() + async ([_, params]: [string, BundleSimulationSwrParams]) => { + const simulationData = await simulateBundle(params) if (!simulationData) { return {} } try { - return generateNewSimulationData(simulationData, { preHooks, postHooks }) + return generateNewSimulationData(simulationData, { preHooks: params.preHooks, postHooks: params.postHooks }) } catch (e) { console.log(`error`, { e, simulationData }) - return generateSimulationDataToError({ preHooks, postHooks }) + return generateSimulationDataToError({ preHooks: params.preHooks, postHooks: params.postHooks }) } }, [simulateBundle], ) - return useSWR(['tenderly-bundle-simulation', preHooks, postHooks, isOrderComplete], getNewSimulationData, { - revalidateOnFocus: false, - revalidateOnReconnect: false, - refreshWhenOffline: false, - }) + return useSWR( + [ + 'tenderly-bundle-simulation', + { + preHooks, + postHooks, + }, + ], + getNewSimulationData, + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + refreshWhenOffline: false, + }, + ) } diff --git a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts index ea6f50e751..2606308869 100644 --- a/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts +++ b/apps/cowswap-frontend/src/modules/tenderly/utils/bundleSimulation.ts @@ -1,17 +1,20 @@ -import { Erc20 } from '@cowprotocol/abis' +import { Erc20Abi } 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 { Interface } from 'ethers/lib/utils' + import { CowHook } from 'modules/hooksStore/types/hooks' import { SimulationData, SimulationInput } from '../types' +const erc20Interface = new Interface(Erc20Abi) export interface GetTransferTenderlySimulationInput { currencyAmount: string from: string receiver: string - token: Erc20 + token: string } export type TokenBuyTransferInfo = { @@ -21,8 +24,8 @@ export type TokenBuyTransferInfo = { export interface PostBundleSimulationParams { account: string chainId: SupportedChainId - tokenSell: Erc20 - tokenBuy: Erc20 + tokenSell: string + tokenBuy: string preHooks: CowHookDetails[] postHooks: CowHookDetails[] sellAmount: string @@ -71,11 +74,11 @@ export function getTransferTenderlySimulationInput({ receiver, token, }: GetTransferTenderlySimulationInput): SimulationInput { - const callData = token.interface.encodeFunctionData('transfer', [receiver, currencyAmount]) + const callData = erc20Interface.encodeFunctionData('transfer', [receiver, currencyAmount]) return { input: callData, - to: token.address, + to: token, from, } } From 4d83f0397e5fc57f21efd2332e8cb51354c46467 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Fri, 8 Nov 2024 12:57:01 -0300 Subject: [PATCH 31/31] feat: trigger extra hook simulation on order review --- .../containers/OrderHooksDetails/index.tsx | 13 ++++++++- .../containers/OrderHooksDetails/styled.tsx | 29 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx index 06d2800412..5e9a27c4f2 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/index.tsx @@ -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' @@ -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 @@ -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) @@ -42,6 +51,8 @@ export function OrderHooksDetails({ appData, children, margin }: OrderHooksDetai Hooks + {hasSomeFailedSimulation && Simulation failed} + {isValidating && } setOpen(!isOpen)}> {preHooksToDapp.length > 0 && ( diff --git a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx index 55f511bc35..dfc2f9716c 100644 --- a/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx +++ b/apps/cowswap-frontend/src/common/containers/OrderHooksDetails/styled.tsx @@ -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; @@ -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); + } + } +`