diff --git a/src/consts/config.ts b/src/consts/config.ts index cd1e65d..82f6157 100644 --- a/src/consts/config.ts +++ b/src/consts/config.ts @@ -1,6 +1,9 @@ const isDevMode = process?.env?.NODE_ENV === 'development'; const version = process?.env?.NEXT_PUBLIC_VERSION ?? null; const explorerApiKeys = JSON.parse(process?.env?.EXPLORER_API_KEYS || '{}'); +export const TENDERLY_USER = process?.env?.TENDERLY_USER; +export const TENDERLY_PROJECT = process?.env?.TENDERLY_PROJECT; +export const TENDERLY_ACCESS_KEY = process?.env?.TENDERLY_ACCESS_KEY; interface Config { debug: boolean; diff --git a/src/features/messages/MessageDetails.tsx b/src/features/messages/MessageDetails.tsx index 422f917..a54d813 100644 --- a/src/features/messages/MessageDetails.tsx +++ b/src/features/messages/MessageDetails.tsx @@ -119,6 +119,7 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro isStatusFetching={isDeliveryStatusFetching} isPiMsg={message.isPiMsg} blur={blur} + message={message} /> {!message.isPiMsg && } diff --git a/src/features/messages/cards/GasDetailsCard.tsx b/src/features/messages/cards/GasDetailsCard.tsx index 84e293e..7eb8656 100644 --- a/src/features/messages/cards/GasDetailsCard.tsx +++ b/src/features/messages/cards/GasDetailsCard.tsx @@ -1,5 +1,4 @@ import BigNumber from 'bignumber.js'; -import { utils } from 'ethers'; import Image from 'next/image'; import { useMemo, useState } from 'react'; @@ -10,10 +9,10 @@ import { links } from '../../../consts/links'; import FuelPump from '../../../images/icons/fuel-pump.svg'; import { Message } from '../../../types'; import { BigNumberMax, fromWei } from '../../../utils/amount'; -import { logger } from '../../../utils/logger'; import { toTitleCase } from '../../../utils/string'; import { GasPayment } from '../../debugger/types'; import { useMultiProvider } from '../../providers/multiProvider'; +import { computeAvgGasPrice } from '../utils'; import { KeyValueRow } from './KeyValueRow'; @@ -165,21 +164,6 @@ function IgpPaymentsTable({ payments }: { payments: Array )} - + ); } else if (status === MessageStatus.Pending) { @@ -84,7 +89,7 @@ export function DestinationTransactionCard({ )} - + ); @@ -207,10 +212,27 @@ function DeliveryStatus({ children }: PropsWithChildren) { ); } -function CallDataModal({ debugResult }: { debugResult?: MessageDebugResult }) { +function CallDataModal({ + debugResult, + chainId, + message, +}: { + debugResult?: MessageDebugResult; + chainId: ChainId; + message: Message; +}) { const [isOpen, setIsOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [buttonText, setButtonText] = useState('Simulate call with Tenderly'); if (!debugResult?.calldataDetails) return null; const { contract, handleCalldata } = debugResult.calldataDetails; + const handleClick = async () => { + setButtonText('Simulating'); + setLoading(true); + await simulateCall({ contract, handleCalldata, chainId, message }); + setButtonText('Simulate call with Tenderly'); + setLoading(false); //using !loading is not setting the states properly and the state stays true + }; return ( <> + {loading && } ); } +async function simulateCall({ + contract, + handleCalldata, + chainId, + message, +}: { + contract: string; + handleCalldata: string; + chainId: ChainId; + message: Message; +}) { + const gasPrice = computeAvgGasPrice('wei', message.totalGasAmount, message.totalPayment); + const data: SimulateBody = { + save: true, + save_if_fails: true, + simulation_type: 'full', + network_id: chainId, + from: '0x0000000000000000000000000000000000000000', //can be any address, doesn't matter + to: contract, + input: handleCalldata, + gas: BigNumEth.from(message.totalGasAmount).toNumber(), + gas_price: Number(gasPrice?.wei), + value: 0, + }; + const resp = await fetch(`/api/simulation`, { + method: 'POST', + body: JSON.stringify(data), + }); + const respMessage = await resp.json(); + if (respMessage.success === true) { + const simulationId = respMessage.data; + window.open(`https://dashboard.tenderly.co/shared/simulation/${simulationId}`); + } else { + toast.error(respMessage.error); + } +} const helpText = { origin: 'Info about the transaction that initiated the message placement into the outbox.', diff --git a/src/features/messages/utils.ts b/src/features/messages/utils.ts index 108fd51..7cb921f 100644 --- a/src/features/messages/utils.ts +++ b/src/features/messages/utils.ts @@ -1,5 +1,9 @@ +import { BigNumber } from 'bignumber.js'; +import { utils } from 'ethers/lib/ethers'; + import { Message, MessageStub } from '../../types'; import { fromBase64, toBase64 } from '../../utils/base64'; +import { logger } from '../../utils/logger'; export function serializeMessage(msg: MessageStub | Message): string | undefined { return toBase64(msg); @@ -8,3 +12,22 @@ export function serializeMessage(msg: MessageStub | Message): string | undefined export function deserializeMessage(data: string | string[]): M | undefined { return fromBase64(data); } + +export function computeAvgGasPrice( + unit: string, + gasAmount?: BigNumber.Value, + payment?: BigNumber.Value, +) { + try { + if (!gasAmount || !payment) return null; + const gasBN = new BigNumber(gasAmount); + const paymentBN = new BigNumber(payment); + if (gasBN.isZero() || paymentBN.isZero()) return null; + const wei = paymentBN.div(gasAmount).toFixed(0); + const formatted = utils.formatUnits(wei, unit).toString(); + return { wei, formatted }; + } catch (error) { + logger.debug('Error computing avg gas price', error); + return null; + } +} diff --git a/src/pages/api/simulation.ts b/src/pages/api/simulation.ts new file mode 100644 index 0000000..b9f9df0 --- /dev/null +++ b/src/pages/api/simulation.ts @@ -0,0 +1,35 @@ +import { TENDERLY_ACCESS_KEY, TENDERLY_PROJECT, TENDERLY_USER } from '../../consts/config'; +import { failureResult, successResult } from '../../features/api/utils'; + +export default async function handler(req, res) { + const data = req.body; + if (!TENDERLY_ACCESS_KEY || !TENDERLY_PROJECT || !TENDERLY_USER) { + console.log('ENV not defined'); + res.json(failureResult('Explorer Issues')); + } + try { + const resp = await fetch( + `https://api.tenderly.co/api/v1/account/${TENDERLY_USER}/project/${TENDERLY_PROJECT}/simulate`, + { + method: 'POST', + body: data, + headers: { + 'X-Access-Key': TENDERLY_ACCESS_KEY as string, + }, + }, + ); + const simulationId = await resp.json().then((data) => data.simulation.id); + await fetch( + `https://api.tenderly.co/api/v1/account/${TENDERLY_USER}/project/${TENDERLY_PROJECT}/simulations/${simulationId}/share`, + { + method: 'POST', + headers: { + 'X-Access-Key': TENDERLY_ACCESS_KEY as string, + }, + }, + ); + res.json(successResult(simulationId)); + } catch (error) { + res.json(failureResult('Error preparing Tenderly simulation')); + } +} diff --git a/src/types.ts b/src/types.ts index d9979a1..f350667 100644 --- a/src/types.ts +++ b/src/types.ts @@ -60,3 +60,17 @@ export interface ExtendedLog extends providers.Log { from?: Address; to?: Address; } + +// Type of body for tenderly POST requests https://docs.tenderly.co/simulations-and-forks/simulation-api/using-simulation-api +export interface SimulateBody { + save: boolean; + save_if_fails: boolean; + simulation_type: string; + network_id: ChainId; + from: Address; //can be any address, doesn't matter + to: Address; + input: string; + gas: number; + gas_price: number | null; + value: number; +}