From 80980208d5b444b96a5fb66a35fbb413385dd25f Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Fri, 1 Sep 2023 15:33:56 +0400 Subject: [PATCH 1/4] feat: tenderly enactment simulation --- .env.sample | 5 + assets/tenderly.com.svg.react | 1 + examples/sample.env-develop.env | 5 + examples/sample.env-production.env | 5 + examples/sample.env-staging.env | 5 + .../ui/DashboardVote/DashboardVote.tsx | 14 +- modules/shared/metrics/index.ts | 7 +- modules/shared/metrics/responseTime.ts | 8 + modules/shared/utils/sanitize.ts | 6 +- .../votes/hooks/useVoteDetailsFormatted.ts | 14 ++ modules/votes/ui/VoteDetails/VoteDetails.tsx | 6 +- modules/votes/ui/VoteForm/VoteForm.tsx | 42 +++++- modules/votes/ui/VoteForm/useFormVoteInfo.ts | 7 + .../votes/ui/VoteForm/useFormVoteSubmit.ts | 28 ++-- .../ui/VoteSimulation/VoteSimulation.tsx | 114 +++++++++++++++ .../ui/VoteSimulation/VoteSimulationStyle.ts | 9 ++ modules/votes/ui/VoteSimulation/index.ts | 1 + .../ui/VoteSimulation/tenderlyFetchers.ts | 58 ++++++++ .../votes/ui/VoteSimulation/tenderlyUtils.ts | 102 +++++++++++++ modules/votes/ui/VoteSimulation/type.ts | 137 ++++++++++++++++++ .../votes/utils/getVoteDetailsFormatted.ts | 7 + next.config.mjs | 7 + pages/api/tenderly/[[...route]].ts | 76 ++++++++++ 23 files changed, 630 insertions(+), 34 deletions(-) create mode 100644 assets/tenderly.com.svg.react create mode 100644 modules/votes/hooks/useVoteDetailsFormatted.ts create mode 100644 modules/votes/ui/VoteSimulation/VoteSimulation.tsx create mode 100644 modules/votes/ui/VoteSimulation/VoteSimulationStyle.ts create mode 100644 modules/votes/ui/VoteSimulation/index.ts create mode 100644 modules/votes/ui/VoteSimulation/tenderlyFetchers.ts create mode 100644 modules/votes/ui/VoteSimulation/tenderlyUtils.ts create mode 100644 modules/votes/ui/VoteSimulation/type.ts create mode 100644 pages/api/tenderly/[[...route]].ts diff --git a/.env.sample b/.env.sample index c55c15f3..80b6e1c8 100644 --- a/.env.sample +++ b/.env.sample @@ -21,3 +21,8 @@ CSP_REPORT_ONLY= # api endpoint for reporting csp violations CSP_REPORT_URI= + +# tenderly credentials +TENDERLY_USER= +TENDERLY_PROJECT= +TENDERLY_ACCESS_KEY= diff --git a/assets/tenderly.com.svg.react b/assets/tenderly.com.svg.react new file mode 100644 index 00000000..6f88a12a --- /dev/null +++ b/assets/tenderly.com.svg.react @@ -0,0 +1 @@ + diff --git a/examples/sample.env-develop.env b/examples/sample.env-develop.env index 027d7d16..a02093d6 100644 --- a/examples/sample.env-develop.env +++ b/examples/sample.env-develop.env @@ -21,3 +21,8 @@ CSP_REPORT_ONLY= # api endpoint for reporting csp violations CSP_REPORT_URI= + +# tenderly credentials +TENDERLY_USER= +TENDERLY_PROJECT= +TENDERLY_ACCESS_KEY= diff --git a/examples/sample.env-production.env b/examples/sample.env-production.env index 638ab3b3..dbe14ecd 100644 --- a/examples/sample.env-production.env +++ b/examples/sample.env-production.env @@ -21,3 +21,8 @@ CSP_REPORT_ONLY= # api endpoint for reporting csp violations CSP_REPORT_URI= + +# tenderly credentials +TENDERLY_USER= +TENDERLY_PROJECT= +TENDERLY_ACCESS_KEY= diff --git a/examples/sample.env-staging.env b/examples/sample.env-staging.env index 638ab3b3..dbe14ecd 100644 --- a/examples/sample.env-staging.env +++ b/examples/sample.env-staging.env @@ -21,3 +21,8 @@ CSP_REPORT_ONLY= # api endpoint for reporting csp violations CSP_REPORT_URI= + +# tenderly credentials +TENDERLY_USER= +TENDERLY_PROJECT= +TENDERLY_ACCESS_KEY= diff --git a/modules/dashboard/ui/DashboardVote/DashboardVote.tsx b/modules/dashboard/ui/DashboardVote/DashboardVote.tsx index 58450a2e..0360207f 100644 --- a/modules/dashboard/ui/DashboardVote/DashboardVote.tsx +++ b/modules/dashboard/ui/DashboardVote/DashboardVote.tsx @@ -1,5 +1,6 @@ import { useCallback } from 'react' import { useVotePassedCallback } from 'modules/votes/hooks/useVotePassedCallback' +import { useVoteDetailsFormatted } from 'modules/votes/hooks/useVoteDetailsFormatted' import Link from 'next/link' import { VoteStatusBanner } from 'modules/votes/ui/VoteStatusBanner' @@ -17,9 +18,6 @@ import { import type { StartVoteEventObject } from 'generated/AragonVotingAbi' import { Vote, VoteStatus } from 'modules/votes/types' -import { weiToNum } from 'modules/blockChain/utils/parseWei' -import { getVoteDetailsFormatted } from 'modules/votes/utils/getVoteDetailsFormatted' -import { formatFloatPct } from 'modules/shared/utils/formatFloatPct' import * as urls from 'modules/network/utils/urls' type Props = { @@ -44,12 +42,13 @@ export function DashboardVote({ const { nayPct, yeaPct, - yeaPctOfTotalSupply, nayPctOfTotalSupplyFormatted, yeaPctOfTotalSupplyFormatted, + neededToQuorum, + neededToQuorumFormatted, startDate, endDate, - } = getVoteDetailsFormatted({ vote, voteTime }) + } = useVoteDetailsFormatted({ vote, voteTime }) const handlePass = useCallback(() => { // TODO: @@ -72,11 +71,6 @@ export function DashboardVote({ onPass: handlePass, }) - const neededToQuorum = weiToNum(vote.minAcceptQuorum) - yeaPctOfTotalSupply - const neededToQuorumFormatted = formatFloatPct(neededToQuorum, { - floor: true, - }).toFixed(2) - const isEnded = status === VoteStatus.Rejected || status === VoteStatus.Executed diff --git a/modules/shared/metrics/index.ts b/modules/shared/metrics/index.ts index a9fb69f6..ddf0c595 100644 --- a/modules/shared/metrics/index.ts +++ b/modules/shared/metrics/index.ts @@ -3,7 +3,11 @@ import { buildInfo } from './buildInfo' import { chainInfo } from './chainInfo' import { METRICS_PREFIX } from './constants' import { contractInfo } from './contractInfo' -import { rpcResponseTime, etherscanResponseTime } from './responseTime' +import { + rpcResponseTime, + etherscanResponseTime, + tenderlyResponseTime, +} from './responseTime' const registry = new Registry() @@ -13,6 +17,7 @@ if (process.env.NODE_ENV === 'production') { registry.registerMetric(contractInfo) registry.registerMetric(rpcResponseTime) registry.registerMetric(etherscanResponseTime) + registry.registerMetric(tenderlyResponseTime) collectDefaultMetrics({ prefix: METRICS_PREFIX, register: registry }) } diff --git a/modules/shared/metrics/responseTime.ts b/modules/shared/metrics/responseTime.ts index 2da28e7b..9aafdaac 100644 --- a/modules/shared/metrics/responseTime.ts +++ b/modules/shared/metrics/responseTime.ts @@ -16,3 +16,11 @@ export const etherscanResponseTime = new Histogram({ labelNames: ['chainId'], registers: [], }) + +export const tenderlyResponseTime = new Histogram({ + name: METRICS_PREFIX + 'tenderly_response', + help: 'Tenderly response times', + labelNames: [], + buckets: [0.1, 0.2, 0.3, 0.6, 1, 1.5, 2, 5], + registers: [], +}) diff --git a/modules/shared/utils/sanitize.ts b/modules/shared/utils/sanitize.ts index d19958db..6d9a47a1 100644 --- a/modules/shared/utils/sanitize.ts +++ b/modules/shared/utils/sanitize.ts @@ -1,12 +1,16 @@ import getConfig from 'next/config' const { serverRuntimeConfig } = getConfig() -const { infuraApiKey, alchemyApiKey, etherscanApiKey } = serverRuntimeConfig +const { infuraApiKey, alchemyApiKey, etherscanApiKey, tendrerlyAccessKey } = + serverRuntimeConfig const SECRETS = { INFURA_API_KEY: infuraApiKey ? new RegExp(infuraApiKey, 'ig') : null, ALCHEMY_API_KEY: alchemyApiKey ? new RegExp(alchemyApiKey, 'ig') : null, ETHERSCAN_API_KEY: etherscanApiKey ? new RegExp(etherscanApiKey, 'ig') : null, + TENDERLY_API_KEY: tendrerlyAccessKey + ? new RegExp(tendrerlyAccessKey, 'ig') + : null, SANITIZED_HEX: new RegExp('0x[a-fA-F0-9]+', 'ig'), ENS_ADDRESS: new RegExp('[a-zA-Z.]+\\.eth', 'gi'), } diff --git a/modules/votes/hooks/useVoteDetailsFormatted.ts b/modules/votes/hooks/useVoteDetailsFormatted.ts new file mode 100644 index 00000000..8dbb60fe --- /dev/null +++ b/modules/votes/hooks/useVoteDetailsFormatted.ts @@ -0,0 +1,14 @@ +import { useMemo } from 'react' +import { getVoteDetailsFormatted } from '../utils/getVoteDetailsFormatted' + +type Args = Partial[0]> + +export const useVoteDetailsFormatted = ({ vote, voteTime }: Args) => { + return useMemo( + () => + vote && voteTime + ? getVoteDetailsFormatted({ vote, voteTime }) + : undefined, + [vote, voteTime], + ) +} diff --git a/modules/votes/ui/VoteDetails/VoteDetails.tsx b/modules/votes/ui/VoteDetails/VoteDetails.tsx index 2f1620a3..7cd5b51a 100644 --- a/modules/votes/ui/VoteDetails/VoteDetails.tsx +++ b/modules/votes/ui/VoteDetails/VoteDetails.tsx @@ -20,7 +20,7 @@ import { VoteMetadataDescription } from '../VoteMetadataDescription' import { Vote, VoteStatus } from 'modules/votes/types' import { weiToNum } from 'modules/blockChain/utils/parseWei' import { formatNumber } from 'modules/shared/utils/formatNumber' -import { getVoteDetailsFormatted } from 'modules/votes/utils/getVoteDetailsFormatted' +import type { getVoteDetailsFormatted } from 'modules/votes/utils/getVoteDetailsFormatted' type Props = { vote: Vote @@ -32,6 +32,7 @@ type Props = { metadata?: string isEnded: boolean executedTxHash?: string + voteDetailsFormatted: ReturnType } export function VoteDetails({ @@ -44,6 +45,7 @@ export function VoteDetails({ metadata, isEnded, executedTxHash, + voteDetailsFormatted, }: Props) { const { totalSupplyFormatted, @@ -55,7 +57,7 @@ export function VoteDetails({ yeaPctOfTotalSupplyFormatted, startDate, endDate, - } = getVoteDetailsFormatted({ vote, voteTime }) + } = voteDetailsFormatted return ( <> diff --git a/modules/votes/ui/VoteForm/VoteForm.tsx b/modules/votes/ui/VoteForm/VoteForm.tsx index df3f6ec0..5e495dc3 100644 --- a/modules/votes/ui/VoteForm/VoteForm.tsx +++ b/modules/votes/ui/VoteForm/VoteForm.tsx @@ -14,8 +14,11 @@ import { VoteFormVoterState } from '../VoteFormVoterState' import { VoteVotersList } from '../VoteVotersList' import { Desc, ClearButton } from './VoteFormStyle' import { FetchErrorBanner } from 'modules/shared/ui/Common/FetchErrorBanner' +import { VoteSimulation } from '../VoteSimulation' +import { DetailsBoxWrap } from '../VoteDetails/VoteDetailsStyle' import { VoteStatus } from 'modules/votes/types' +import { isVoteEnactable } from 'modules/votes/utils/isVoteEnactable' type Props = { voteId?: string @@ -39,14 +42,21 @@ export function VoteForm({ voteId }: Props) { eventsVoted, eventExecuteVote, status, + voteDetailsFormatted, } = useFormVoteInfo({ voteId }) const { clearVoteId } = useVotePrompt() - const { txVote, txEnact, handleVote, handleEnact, isSubmitting } = - useFormVoteSubmit({ - voteId, - onFinish: doRevalidate, - }) + const { + txVote, + txEnact, + handleVote, + populateEnact, + handleEnact, + isSubmitting, + } = useFormVoteSubmit({ + voteId, + onFinish: doRevalidate, + }) useVotePassedCallback({ startDate, @@ -67,6 +77,15 @@ export function VoteForm({ voteId }: Props) { const isEmpty = !voteId const isNotFound = swrVote.error?.reason === 'VOTING_NO_VOTE' const isFound = !isEmpty && !isNotFound && !isLoading && vote && status + const isEnactmentStillPossible = + vote && + voteDetailsFormatted && + isVoteEnactable(vote) && + !isEnded && + !( + status !== VoteStatus.ActiveMain && + voteDetailsFormatted.neededToQuorum > 0 + ) return ( @@ -118,8 +137,21 @@ export function VoteForm({ voteId }: Props) { creator={eventStart?.creator} metadata={eventStart?.metadata} executedTxHash={eventExecuteVote?.event.transactionHash} + voteDetailsFormatted={voteDetailsFormatted!} /> + {isEnactmentStillPossible && ( + + + + )} + {!isWalletConnected && } {isWalletConnected && ( diff --git a/modules/votes/ui/VoteForm/useFormVoteInfo.ts b/modules/votes/ui/VoteForm/useFormVoteInfo.ts index eeda94a2..546697f5 100644 --- a/modules/votes/ui/VoteForm/useFormVoteInfo.ts +++ b/modules/votes/ui/VoteForm/useFormVoteInfo.ts @@ -4,6 +4,7 @@ import { useCallback, useEffect } from 'react' import { useSWR } from 'modules/network/hooks/useSwr' import { useWeb3 } from 'modules/blockChain/hooks/useWeb3' import { useConfig } from 'modules/config/hooks/useConfig' +import { useVoteDetailsFormatted } from 'modules/votes/hooks/useVoteDetailsFormatted' import { ContractVoting, @@ -125,6 +126,11 @@ export function useFormVoteInfo({ voteId }: Args) { } }, [doRevalidate, contractVoting, voteId]) + const voteDetailsFormatted = useVoteDetailsFormatted({ + vote, + voteTime, + }) + return { swrVote, vote, @@ -142,5 +148,6 @@ export function useFormVoteInfo({ voteId }: Args) { eventsVoted: swrVote.data?.eventsVoted, eventExecuteVote: swrVote.data?.eventExecuteVote, status: swrVote.data?.status, + voteDetailsFormatted, } } diff --git a/modules/votes/ui/VoteForm/useFormVoteSubmit.ts b/modules/votes/ui/VoteForm/useFormVoteSubmit.ts index d3abf804..b84ea58a 100644 --- a/modules/votes/ui/VoteForm/useFormVoteSubmit.ts +++ b/modules/votes/ui/VoteForm/useFormVoteSubmit.ts @@ -68,20 +68,17 @@ export function useFormVoteSubmit({ voteId, onFinish }: Args) { [voteId, txVote], ) - const populateEnact = useCallback( - async (args: { voteId: string }) => { - const gasLimit = await estimateGasFallback( - contractVoting.estimateGas.executeVote(args.voteId), - 2000000, - ) - const tx = await contractVoting.populateTransaction.executeVote( - args.voteId, - { gasLimit }, - ) - return tx - }, - [contractVoting], - ) + const populateEnact = useCallback(async () => { + if (!voteId) throw new Error('voteId is required') + const gasLimit = await estimateGasFallback( + contractVoting.estimateGas.executeVote(voteId), + 2000000, + ) + const tx = await contractVoting.populateTransaction.executeVote(voteId, { + gasLimit, + }) + return tx + }, [voteId, contractVoting]) const txEnact = useTransactionSender(populateEnact, { onError: handleError, onFinish: handleFinish, @@ -91,7 +88,7 @@ export function useFormVoteSubmit({ voteId, onFinish }: Args) { if (!voteId) return try { setSubmitting('enact') - await txEnact.send({ voteId }) + await txEnact.send() } catch (err) { console.error(err) setSubmitting(false) @@ -104,5 +101,6 @@ export function useFormVoteSubmit({ voteId, onFinish }: Args) { handleVote, handleEnact, isSubmitting, + populateEnact, } } diff --git a/modules/votes/ui/VoteSimulation/VoteSimulation.tsx b/modules/votes/ui/VoteSimulation/VoteSimulation.tsx new file mode 100644 index 00000000..ad0236e0 --- /dev/null +++ b/modules/votes/ui/VoteSimulation/VoteSimulation.tsx @@ -0,0 +1,114 @@ +import { useCallback, useMemo, useState } from 'react' +import { useWeb3 } from 'modules/blockChain/hooks/useWeb3' +import { useSWR } from 'modules/network/hooks/useSwr' + +import { ButtonIcon, ToastError } from '@lidofinance/lido-ui' +import { TenderlyIcon } from './VoteSimulationStyle' + +import { + getForkSimulationUrl, + sumulateVoteEnactmentOnFork, +} from './tenderlyUtils' +import { fetchForkTransactionsList, fetchForksList } from './tenderlyFetchers' +import { getErrorMessage } from 'modules/shared/utils/getErrorMessage' +import { openWindow } from 'modules/shared/utils/openWindow' +import type { Vote } from 'modules/votes/types' +import type { PopulatedTransaction } from 'ethers' + +type Props = { + voteId: string + vote: Vote + voteTime?: number + objectionPhaseTime?: number + populateEnact: () => Promise +} + +export const VoteSimulation = ({ + voteId, + vote, + voteTime, + objectionPhaseTime, + populateEnact, +}: Props) => { + const { chainId } = useWeb3() + const [simulationIdState, setSimulationId] = useState( + undefined, + ) + const [isPendingSimulation, setPendingSimulation] = useState(false) + + const { data: enactTxPopulated, initialLoading: isLoadingTx } = useSWR( + ['populated-enact-tx', voteId], + populateEnact, + ) + + const { data: simulationsList, initialLoading: isLoadingList } = useSWR( + 'tenderly-simulations-list', + async () => { + const { simulation_forks: forks } = await fetchForksList() + const transactionsRaw = await Promise.all( + forks.map(fork => fetchForkTransactionsList(fork.id)), + ) + const transactions = transactionsRaw.reduce( + (prev, curr) => [...prev, ...curr.fork_transactions], + [], + ) + return transactions + }, + ) + + const simulationPreexisted = useMemo(() => { + if (!simulationsList || !enactTxPopulated) return null + return simulationsList.find( + s => + s.status === true && + s.to.toLowerCase() === enactTxPopulated.to?.toLowerCase() && + s.input === enactTxPopulated.data, + ) + }, [simulationsList, enactTxPopulated]) + + const isLoading = isLoadingList || isLoadingTx || isPendingSimulation + const simulationId = simulationPreexisted?.id ?? simulationIdState + + const runSimulation = useCallback(async () => { + if (!enactTxPopulated || !voteTime || !objectionPhaseTime) return + setPendingSimulation(true) + try { + const id = await sumulateVoteEnactmentOnFork({ + chainId, + voteId, + vote, + voteTime, + objectionPhaseTime, + }) + setSimulationId(id) + return id + } catch (error) { + console.error(error) + ToastError(getErrorMessage(error), {}) + } finally { + setPendingSimulation(false) + } + }, [chainId, enactTxPopulated, vote, voteId, voteTime, objectionPhaseTime]) + + const handleClick = useCallback(async () => { + if (isLoading || !enactTxPopulated) return + let id = simulationId + if (!id) { + id = await runSimulation() + } + if (id) openWindow(getForkSimulationUrl(id)) + }, [isLoading, enactTxPopulated, simulationId, runSimulation]) + + return ( + } + color="secondary" + variant="translucent" + fullwidth + loading={isLoading} + onClick={handleClick} + > + {simulationId ? 'Show' : 'Run'} Tenderly enactment simulation + + ) +} diff --git a/modules/votes/ui/VoteSimulation/VoteSimulationStyle.ts b/modules/votes/ui/VoteSimulation/VoteSimulationStyle.ts new file mode 100644 index 00000000..42efb8be --- /dev/null +++ b/modules/votes/ui/VoteSimulation/VoteSimulationStyle.ts @@ -0,0 +1,9 @@ +import styled from 'styled-components' +import TenderlySvg from 'assets/tenderly.com.svg.react' + +export const TenderlyIcon = styled(TenderlySvg).attrs({ + width: 24, + height: 24, +})` + margin-right: ${({ theme }) => theme.spaceMap.sm}px; +` diff --git a/modules/votes/ui/VoteSimulation/index.ts b/modules/votes/ui/VoteSimulation/index.ts new file mode 100644 index 00000000..5b1e019a --- /dev/null +++ b/modules/votes/ui/VoteSimulation/index.ts @@ -0,0 +1 @@ +export * from './VoteSimulation' diff --git a/modules/votes/ui/VoteSimulation/tenderlyFetchers.ts b/modules/votes/ui/VoteSimulation/tenderlyFetchers.ts new file mode 100644 index 00000000..dfc96457 --- /dev/null +++ b/modules/votes/ui/VoteSimulation/tenderlyFetchers.ts @@ -0,0 +1,58 @@ +import { CHAINS } from '@lido-sdk/constants' +import { + TenderlyForkResponse, + TenderlyResponseForksList, + TenderlyResponseForkTransactionsList, +} from './type' + +export const TENDERLY_API_URL = '/api/tenderly' + +export const getForkSimulationUrl = (id: string) => + `https://dashboard.tenderly.co/shared/fork/simulation/${id}` + +export const fetchForksList = async (): Promise => { + const res = await fetch(`${TENDERLY_API_URL}/forks`, { + headers: { + 'Content-Type': 'application/json', + }, + }) + return res.json() +} + +export const fetchForkTransactionsList = async ( + forkId: string, +): Promise => { + const res = await fetch(`${TENDERLY_API_URL}/fork/${forkId}/transactions`, { + headers: { + 'Content-Type': 'application/json', + }, + }) + return res.json() +} + +export const requestSimulationShare = async ( + forkId: string, + simulationId: string, +) => { + await fetch( + `${TENDERLY_API_URL}/fork/${forkId}/transaction/${simulationId}/share`, + { + method: 'POST', + }, + ) +} + +export const requestNetworkFork = async (args: { + chainId: CHAINS +}): Promise => { + const res = await fetch(`${TENDERLY_API_URL}/fork`, { + method: 'POST', + body: JSON.stringify({ + network_id: String(args.chainId), + }), + headers: { + 'Content-Type': 'application/json', + }, + }) + return res.json() +} diff --git a/modules/votes/ui/VoteSimulation/tenderlyUtils.ts b/modules/votes/ui/VoteSimulation/tenderlyUtils.ts new file mode 100644 index 00000000..eda4ef6b --- /dev/null +++ b/modules/votes/ui/VoteSimulation/tenderlyUtils.ts @@ -0,0 +1,102 @@ +import * as ethers from 'ethers' +import { CHAINS } from '@lido-sdk/constants' +import { ContractVoting } from 'modules/blockChain/contracts' +import { + fetchForkTransactionsList, + requestNetworkFork, + requestSimulationShare, +} from './tenderlyFetchers' +import { estimateGasFallback } from 'modules/shared/utils/estimateGasFallback' +import { Vote } from 'modules/votes/types' + +const TREASURY_ADDRESS: Partial> = { + [CHAINS.Mainnet]: '0x3e40D73EB977Dc6a537aF587D48316feE66E9C8c', + [CHAINS.Goerli]: '0x4333218072D5d7008546737786663c38B4D561A4', +} as const + +export const getForkSimulationUrl = (id: string) => + `https://dashboard.tenderly.co/shared/fork/simulation/${id}` + +export const createNetworkFork = async (args: { chainId: CHAINS }) => { + const fork = await requestNetworkFork(args) + const { id: forkId } = fork.simulation_fork + const rpcUrl = `https://rpc.tenderly.co/fork/${forkId}` + const forkProvider = new ethers.providers.JsonRpcProvider(rpcUrl) + return { + forkId, + forkProvider, + } +} + +export const sumulateVoteEnactmentOnFork = async ({ + chainId, + voteId, + vote, + voteTime, + objectionPhaseTime, +}: { + chainId: CHAINS + voteId: string + vote: Vote + voteTime: number + objectionPhaseTime: number +}) => { + const { forkId, forkProvider } = await createNetworkFork({ chainId }) + + const treasuryAddress = TREASURY_ADDRESS[chainId]! + const treasurySigner = forkProvider.getSigner(treasuryAddress) + const forkedVotingContract = ContractVoting.connect({ + chainId, + library: treasurySigner, + }) + + const { timestamp } = await forkProvider.getBlock('latest') + const startDate = vote.startDate.toNumber() + const objectionEndDate = startDate + (voteTime - objectionPhaseTime) + const endDate = startDate + voteTime + + if (timestamp < objectionEndDate) { + const gasLimit = await estimateGasFallback( + forkedVotingContract.estimateGas.vote(voteId, true, false), + 2000000, + ) + + const voteTx = await forkedVotingContract.populateTransaction.vote( + voteId, + true, + false, + { + gasLimit, + }, + ) + + await treasurySigner.sendTransaction({ + ...voteTx, + from: treasuryAddress, + }) + } + + if (timestamp < endDate) { + await forkProvider.send('evm_increaseTime', [ + ethers.utils.hexValue(endDate - timestamp), + ]) + await forkProvider.send('evm_increaseBlocks', [ethers.utils.hexValue(1)]) + } + + const gasLimit = await estimateGasFallback( + forkedVotingContract.estimateGas.executeVote(voteId), + 2000000, + ) + + const tx = await forkedVotingContract.executeVote(voteId, { gasLimit }) + + const { fork_transactions } = await fetchForkTransactionsList(forkId) + + const { id: txTenderlyId } = fork_transactions.find( + forkTx => forkTx.hash === tx.hash, + )! + + await requestSimulationShare(forkId, txTenderlyId) + + return txTenderlyId +} diff --git a/modules/votes/ui/VoteSimulation/type.ts b/modules/votes/ui/VoteSimulation/type.ts new file mode 100644 index 00000000..92859c32 --- /dev/null +++ b/modules/votes/ui/VoteSimulation/type.ts @@ -0,0 +1,137 @@ +export type TenderlyBlockHeader = { + baseFeePerGas: string + difficulty: string + extraData: string + gasLimit: string + gasUsed: string + hash: string + logsBloom: string + miner: string + mixHash: string + nonce: string + number: string + parentHash: string + receiptsRoot: string + sha3Uncles: string + size: string + stateRoot: string + timestamp: string + totalDifficulty: string + transactions: unknown + transactionsRoot: string + uncles: null | any +} + +export type TenderlyReceipt = { + blockHash: string + blockNumber: string + contractAddress: null | any + cumulativeGasUsed: string + effectiveGasPrice: string + from: string + gasUsed: string + logs: any[] + logsBloom: string + status: string + to: string + transactionHash: string + transactionIndex: string + type: string +} + +export type TenderlyStateObject = { + address: string + data: { balance: string } +} + +export type TenderlyFork = { + accounts: Record + block_number: number + chain_config: { + berlin_block: number + byzantium_block: number + chain_id: number + clique: { period: number; epoch: number } + constantinople_block: number + dao_fork_support: boolean + eip_150_block: number + eip_150_hash: string + eip_155_block: number + eip_158_block: number + homestead_block: number + istanbul_block: number + london_block: number + petersburg_block: number + polygon: { + heimdall_url: string + sprint_length_changelog: null | any + state_sync: { + do_not_skip: boolean + event_records_amount_override: null | any + } + } + shanghai_time: number + type: string + } + created_at: string + current_block_number: number + fork_config: null | any + id: string + network_id: string + project_id: string + shared: false + transaction_index: number +} + +export type TenderlyTransaction = { + access_list: null | any + alias: string + block_hash: string + block_header: TenderlyBlockHeader + block_number: number + branch_root: boolean + created_at: string + deposit_tx: boolean + description: string + fork_height: number + fork_id: string + from: string + gas: number + gas_price: string + hash: string + id: string + immutable: boolean + input: string + internal: boolean + kind: string + l1_block_number: number + l1_message_sender: string + l1_timestamp: number + network_id: string + nonce: number + origin: string + parent_id: string + project_id: string + receipt: TenderlyReceipt + shared: boolean + state_objects: TenderlyStateObject[] + status: boolean + system_tx: boolean + timestamp: string + to: string + transaction_index: number + value: string +} + +export type TenderlyResponseForksList = { + simulation_forks: TenderlyFork[] +} + +export type TenderlyResponseForkTransactionsList = { + fork_transactions: TenderlyTransaction[] +} + +export type TenderlyForkResponse = { + root_transaction: TenderlyTransaction + simulation_fork: TenderlyFork +} diff --git a/modules/votes/utils/getVoteDetailsFormatted.ts b/modules/votes/utils/getVoteDetailsFormatted.ts index 39d17218..ab3edcae 100644 --- a/modules/votes/utils/getVoteDetailsFormatted.ts +++ b/modules/votes/utils/getVoteDetailsFormatted.ts @@ -31,6 +31,11 @@ export function getVoteDetailsFormatted({ vote, voteTime }: Args) { const startDate = vote.startDate.toNumber() const endDate = startDate + voteTime + const neededToQuorum = weiToNum(vote.minAcceptQuorum) - yeaPctOfTotalSupply + const neededToQuorumFormatted = formatFloatPct(neededToQuorum, { + floor: true, + }).toFixed(2) + return { totalSupply, totalSupplyFormatted, @@ -42,6 +47,8 @@ export function getVoteDetailsFormatted({ vote, voteTime }: Args) { yeaPctOfTotalSupply, nayPctOfTotalSupplyFormatted, yeaPctOfTotalSupplyFormatted, + neededToQuorum, + neededToQuorumFormatted, startDate, endDate, } diff --git a/next.config.mjs b/next.config.mjs index aa558408..6b3536ed 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -13,6 +13,10 @@ const cspReportUri = process.env.CSP_REPORT_URI const ipfsMode = process.env.IPFS_MODE const walletconnectProjectId = process.env.WALLETCONNECT_PROJECT_ID +const tenderlyUser = process.env.TENDERLY_USER +const tenderlyProject = process.env.TENDERLY_PROJECT +const tendrerlyAccessKey = process.env.TENDERLY_ACCESS_KEY + export default { basePath, webpack5: true, @@ -117,6 +121,9 @@ export default { cspTrustedHosts, cspReportOnly, cspReportUri, + tenderlyUser, + tenderlyProject, + tendrerlyAccessKey, }, publicRuntimeConfig: { defaultChain, diff --git a/pages/api/tenderly/[[...route]].ts b/pages/api/tenderly/[[...route]].ts new file mode 100644 index 00000000..523e642d --- /dev/null +++ b/pages/api/tenderly/[[...route]].ts @@ -0,0 +1,76 @@ +import isEmpty from 'lodash/isEmpty' +import clone from 'just-clone' +import getConfig from 'next/config' +import { logger } from 'modules/shared/utils/log' +import { tenderlyResponseTime } from 'modules/shared/metrics/responseTime' +import type { NextApiRequest, NextApiResponse } from 'next' + +const { serverRuntimeConfig } = getConfig() +const { tenderlyUser, tenderlyProject, tendrerlyAccessKey } = + serverRuntimeConfig + +const getTenderlyApiUrl = (user: string, project: string) => + `https://api.tenderly.co/api/v1/account/${user}/project/${project}` + +export default async function tenderly( + req: NextApiRequest, + res: NextApiResponse, +) { + const requestInfo = { + type: 'API request', + path: 'tenderly', + body: clone(req.body), + query: clone(req.query), + method: req.method, + stage: 'INCOMING', + } + + logger.info('Incoming request to tenderly', requestInfo) + + try { + const contentType = req.headers['content-type'] + const jsonMode = contentType === 'application/json' + + const route = + typeof req.query.route === 'object' + ? (req.query.route as string[]).join('/') + : req.query.route + + const url = + getTenderlyApiUrl(tenderlyUser, tenderlyProject) + + (route ? `/${route}` : '') + + const end = tenderlyResponseTime.startTimer() + const requested = await fetch(url, { + method: req.method, + ...(!isEmpty(req.body) ? { body: JSON.stringify(req.body) } : {}), + headers: { + 'X-Access-Key': tendrerlyAccessKey, + ...(jsonMode ? { 'Content-Type': 'application/json' } : {}), + }, + }) + end() + + res.status(requested.status) + + if (jsonMode) { + const responded = await requested.json() + res.json(responded) + } else { + res.end() + } + + logger.info('Request to tenderly successfully fullfilled', { + ...requestInfo, + stage: 'FULFILLED', + }) + } catch (error) { + logger.error( + error instanceof Error ? error.message : 'Something went wrong', + error, + ) + res.status(500).send({ error: 'Something went wrong!' }) + } + + res.end() +} From 29e4bae7c55cd2e030b24fab21b8546932a0f884 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Fri, 1 Sep 2023 15:56:31 +0400 Subject: [PATCH 2/4] fix: logger updated --- pages/api/tenderly/[[...route]].ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pages/api/tenderly/[[...route]].ts b/pages/api/tenderly/[[...route]].ts index 523e642d..a591d57c 100644 --- a/pages/api/tenderly/[[...route]].ts +++ b/pages/api/tenderly/[[...route]].ts @@ -1,7 +1,6 @@ import isEmpty from 'lodash/isEmpty' import clone from 'just-clone' import getConfig from 'next/config' -import { logger } from 'modules/shared/utils/log' import { tenderlyResponseTime } from 'modules/shared/metrics/responseTime' import type { NextApiRequest, NextApiResponse } from 'next' @@ -25,7 +24,7 @@ export default async function tenderly( stage: 'INCOMING', } - logger.info('Incoming request to tenderly', requestInfo) + console.info('Incoming request to tenderly', requestInfo) try { const contentType = req.headers['content-type'] @@ -60,12 +59,12 @@ export default async function tenderly( res.end() } - logger.info('Request to tenderly successfully fullfilled', { + console.info('Request to tenderly successfully fullfilled', { ...requestInfo, stage: 'FULFILLED', }) } catch (error) { - logger.error( + console.error( error instanceof Error ? error.message : 'Something went wrong', error, ) From c7023d0698cbdcde8c3e06c26cede568920ce30e Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Fri, 1 Sep 2023 16:00:28 +0400 Subject: [PATCH 3/4] fix: tenderly icon size --- assets/tenderly.com.svg.react | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/tenderly.com.svg.react b/assets/tenderly.com.svg.react index 6f88a12a..c6f9ad10 100644 --- a/assets/tenderly.com.svg.react +++ b/assets/tenderly.com.svg.react @@ -1 +1 @@ - + From e15c1ba8d9e25a518c4b2dcb6aab56f433ba34a7 Mon Sep 17 00:00:00 2001 From: Dmitrii Podlesnyi Date: Fri, 1 Sep 2023 16:02:28 +0400 Subject: [PATCH 4/4] fix: vote details formatted type --- modules/dashboard/ui/DashboardVote/DashboardVote.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/dashboard/ui/DashboardVote/DashboardVote.tsx b/modules/dashboard/ui/DashboardVote/DashboardVote.tsx index 4fd4ceca..9d77a5a0 100644 --- a/modules/dashboard/ui/DashboardVote/DashboardVote.tsx +++ b/modules/dashboard/ui/DashboardVote/DashboardVote.tsx @@ -48,7 +48,7 @@ export function DashboardVote({ neededToQuorumFormatted, startDate, endDate, - } = useVoteDetailsFormatted({ vote, voteTime }) + } = useVoteDetailsFormatted({ vote, voteTime })! // we are sure that we have non-undefined `vote` and `voteTime` here const handlePass = useCallback(() => { // TODO: