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 all 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
18 changes: 1 addition & 17 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,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';

Expand Down Expand Up @@ -165,21 +164,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
70 changes: 66 additions & 4 deletions src/features/messages/cards/TransactionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
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 +43,7 @@ export function DestinationTransactionCard({
isStatusFetching,
isPiMsg,
blur,
message,
}: {
chainId: ChainId;
status: MessageStatus;
Expand All @@ -48,6 +52,7 @@ export function DestinationTransactionCard({
isStatusFetching: boolean;
isPiMsg?: boolean;
blur: boolean;
message: Message;
}) {
let content: ReactNode;
if (transaction) {
Expand All @@ -70,7 +75,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 +89,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 +212,27 @@ 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 +258,51 @@ 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">
Copy link
Collaborator

Choose a reason for hiding this comment

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

Show only when not loading (see feedback on discord).
Please also reduce text size: text-sm

{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
23 changes: 23 additions & 0 deletions src/features/messages/utils.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -8,3 +12,22 @@ 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;
}
}
35 changes: 35 additions & 0 deletions src/pages/api/simulation.ts
Original file line number Diff line number Diff line change
@@ -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');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use logger instead of console, I believe this will raise a linter warning

Copy link
Collaborator

Choose a reason for hiding this comment

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

Change log message to something more descriptive: Tenderly key, project, or user not defined in env

res.json(failureResult('Explorer Issues'));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Change to 'Tenderly credentials missing'

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