From 3ac32e995c067d64fc08e97ff1ca55454fb03fb6 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Wed, 2 Oct 2024 11:06:00 -0300 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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, }),