Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding tenderly simulation #49

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/consts/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const isDevMode = process?.env?.NODE_ENV === 'development';
jmrossy marked this conversation as resolved.
Show resolved Hide resolved
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;
Expand Down
1 change: 1 addition & 0 deletions src/features/messages/MessageDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export function MessageDetails({ messageId, message: messageFromUrlParams }: Pro
isStatusFetching={isDeliveryStatusFetching}
isPiMsg={message.isPiMsg}
blur={blur}
message={message}
/>
{!message.isPiMsg && <TimelineCard message={message} blur={blur} />}
<ContentDetailsCard message={message} blur={blur} />
Expand Down
19 changes: 1 addition & 18 deletions src/features/messages/cards/GasDetailsCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import BigNumber from 'bignumber.js';
import { utils } from 'ethers';
import Image from 'next/image';
import { useMemo, useState } from 'react';

Expand All @@ -10,11 +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';

interface Props {
Expand Down Expand Up @@ -165,21 +163,6 @@ function IgpPaymentsTable({ payments }: { payments: Array<GasPayment & { contrac
);
}

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;
}
}

const style = {
th: 'p-1 md:p-2 text-sm text-gray-500 font-normal text-left border border-gray-200 rounded',
td: 'p-1 md:p-2 text-xs md:text-sm text-gray-700 text-left border border-gray-200 rounded',
Expand Down
60 changes: 54 additions & 6 deletions src/features/messages/cards/TransactionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { BigNumber as BigNumEth } from 'ethers';
import { PropsWithChildren, ReactNode, useState } from 'react';

import { toast } from 'react-toastify';
import { Spinner } from '../../../components/animations/Spinner';
import { ChainLogo } from '../../../components/icons/ChainLogo';
import { HelpIcon } from '../../../components/icons/HelpIcon';
import { Card } from '../../../components/layout/Card';
import { Modal } from '../../../components/layout/Modal';
import { links } from '../../../consts/links';
import { MessageStatus, MessageTx } from '../../../types';
import { Message, MessageStatus, MessageTx, SimulateBody } from '../../../types';
import { getDateTimeString, getHumanReadableTimeString } from '../../../utils/time';
import { getChainDisplayName } from '../../chains/utils';
import { debugStatusToDesc } from '../../debugger/strings';
import { MessageDebugResult } from '../../debugger/types';
import { useMultiProvider } from '../../providers/multiProvider';

import { computeAvgGasPrice } from '../utils';
import { LabelAndCodeBlock } from './CodeBlock';
import { KeyValueRow } from './KeyValueRow';

Expand Down Expand Up @@ -40,6 +41,7 @@ export function DestinationTransactionCard({
isStatusFetching,
isPiMsg,
blur,
message
}: {
chainId: ChainId;
status: MessageStatus;
Expand All @@ -48,6 +50,7 @@ export function DestinationTransactionCard({
isStatusFetching: boolean;
isPiMsg?: boolean;
blur: boolean;
message:Message;
}) {
let content: ReactNode;
if (transaction) {
Expand All @@ -70,7 +73,7 @@ export function DestinationTransactionCard({
{debugResult.description}
</div>
)}
<CallDataModal debugResult={debugResult} />
<CallDataModal debugResult={debugResult} chainId={chainId} message={message} />
</DeliveryStatus>
);
} else if (status === MessageStatus.Pending) {
Expand All @@ -84,7 +87,7 @@ export function DestinationTransactionCard({
</div>
)}
<Spinner classes="my-4 scale-75" />
<CallDataModal debugResult={debugResult} />
<CallDataModal debugResult={debugResult} chainId={chainId} message={message}/>
</div>
</DeliveryStatus>
);
Expand Down Expand Up @@ -207,10 +210,19 @@ function DeliveryStatus({ children }: PropsWithChildren<unknown>) {
);
}

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');
jmrossy marked this conversation as resolved.
Show resolved Hide resolved
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 (
<>
<button onClick={() => setIsOpen(true)} className={`mt-5 ${styles.textLink}`}>
Expand All @@ -236,11 +248,47 @@ function CallDataModal({ debugResult }: { debugResult?: MessageDebugResult }) {
</p>
<LabelAndCodeBlock label="Recipient contract address:" value={contract} />
<LabelAndCodeBlock label="Handle function input calldata:" value={handleCalldata} />
<button onClick={handleClick}
disabled={loading}
className='underline text-blue-400'
>
{buttonText}
</button>
{loading && <Spinner classes="mt-4 scale-75 self-center" />}
</div>
</Modal>
</>
);
}
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.',
Expand Down
18 changes: 18 additions & 0 deletions src/features/messages/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
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);
Expand All @@ -8,3 +11,18 @@ export function serializeMessage(msg: MessageStub | Message): string | undefined
export function deserializeMessage<M extends MessageStub>(data: string | string[]): M | undefined {
return fromBase64<M>(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;
}
}
36 changes: 36 additions & 0 deletions src/pages/api/simulation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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
jmrossy marked this conversation as resolved.
Show resolved Hide resolved
if(!TENDERLY_ACCESS_KEY || !TENDERLY_PROJECT || !TENDERLY_USER){
console.log("ENV not defined")
res.json(failureResult("Explorer Issues"))
return null
}
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("Could not simulate"))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please change to "Error preparing Tenderly simluation"

}
}
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
jmrossy marked this conversation as resolved.
Show resolved Hide resolved
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,
}
Loading