diff --git a/app/css/interceptor.css b/app/css/interceptor.css index 91b857d4..f50cf86e 100644 --- a/app/css/interceptor.css +++ b/app/css/interceptor.css @@ -62,7 +62,7 @@ button:where(:not(.btn)):hover { background-color: var(--highlighted-primary-color); } -button:not(.btn):disabled, button[disabled]) { +button:not(.btn):disabled, button:not(.btn)[disabled] { background-color: var(--disabled-primary-color); border-color: transparent; } diff --git a/app/ts/background/background.ts b/app/ts/background/background.ts index 95c5d7ec..0b8e8daa 100644 --- a/app/ts/background/background.ts +++ b/app/ts/background/background.ts @@ -1,20 +1,20 @@ -import { InpageScriptRequest, PopupMessage, RPCReply, Settings } from '../types/interceptor-messages.js' +import { InpageScriptRequest, PopupMessage, RPCReply, Settings, SimulateExecutionReplyData } from '../types/interceptor-messages.js' import 'webextension-polyfill' import { Simulator, parseEvents, parseInputData, runProtectorsForTransaction } from '../simulation/simulator.js' import { getSimulationResults, getTabState, setLatestUnexpectedError, updateSimulationResults, updateSimulationResultsWithCallBack } from './storageVariables.js' import { changeSimulationMode, getSettings, getMakeMeRich, getWethForChainId } from './settings.js' import { blockNumber, call, chainId, estimateGas, gasPrice, getAccounts, getBalance, getBlockByNumber, getCode, getLogs, getPermissions, getSimulationStack, getTransactionByHash, getTransactionCount, getTransactionReceipt, netVersion, personalSign, sendTransaction, subscribe, switchEthereumChain, unsubscribe, web3ClientVersion, getBlockByHash, feeHistory, installNewFilter, uninstallNewFilter, getFilterChanges, getFilterLogs, handleIterceptorError } from './simulationModeHanders.js' -import { changeActiveAddress, changeMakeMeRich, changePage, confirmDialog, refreshSimulation, removeTransactionOrSignedMessage, requestAccountsFromSigner, refreshPopupConfirmTransactionSimulation, confirmRequestAccess, changeInterceptorAccess, changeChainDialog, popupChangeActiveRpc, enableSimulationMode, addOrModifyAddressBookEntry, getAddressBookData, removeAddressBookEntry, refreshHomeData, interceptorAccessChangeAddressOrRefresh, refreshPopupConfirmTransactionMetadata, changeSettings, importSettings, exportSettings, setNewRpcList, simulateGovernanceContractExecutionOnPass, openNewTab, settingsOpened, changeAddOrModifyAddressWindowState, popupFetchAbiAndNameFromEtherscan, openWebPage, disableInterceptor, requestNewHomeData, setEnsNameForHash } from './popupMessageHandlers.js' -import { CompleteVisualizedSimulation, ProtectorResults, SimulationState, VisualizedSimulatorState, WebsiteCreatedEthereumUnsignedTransactionOrFailed } from '../types/visualizer-types.js' +import { changeActiveAddress, changeMakeMeRich, changePage, confirmDialog, refreshSimulation, removeTransactionOrSignedMessage, requestAccountsFromSigner, refreshPopupConfirmTransactionSimulation, confirmRequestAccess, changeInterceptorAccess, changeChainDialog, popupChangeActiveRpc, enableSimulationMode, addOrModifyAddressBookEntry, getAddressBookData, removeAddressBookEntry, refreshHomeData, interceptorAccessChangeAddressOrRefresh, refreshPopupConfirmTransactionMetadata, changeSettings, importSettings, exportSettings, setNewRpcList, simulateGovernanceContractExecutionOnPass, openNewTab, settingsOpened, changeAddOrModifyAddressWindowState, popupFetchAbiAndNameFromEtherscan, openWebPage, disableInterceptor, requestNewHomeData, setEnsNameForHash, simulateGnosisSafeTransactionOnPass } from './popupMessageHandlers.js' +import { CompleteVisualizedSimulation, ProtectorResults, SimulationState, VisualizedSimulatorState, WebsiteCreatedEthereumUnsignedTransaction, WebsiteCreatedEthereumUnsignedTransactionOrFailed } from '../types/visualizer-types.js' import { WebsiteTabConnections } from '../types/user-interface-types.js' import { askForSignerAccountsFromSignerIfNotAvailable, interceptorAccessMetadataRefresh, requestAccessFromUser, updateInterceptorAccessViewWithPendingRequests } from './windows/interceptorAccess.js' import { FourByteExplanations, METAMASK_ERROR_FAILED_TO_PARSE_REQUEST, METAMASK_ERROR_NOT_AUTHORIZED, METAMASK_ERROR_NOT_CONNECTED_TO_CHAIN, ERROR_INTERCEPTOR_DISABLED, NEW_BLOCK_ABORT, ETHEREUM_LOGS_LOGGER_ADDRESS } from '../utils/constants.js' import { sendActiveAccountChangeToApprovedWebsitePorts, sendMessageToApprovedWebsitePorts, updateWebsiteApprovalAccesses, verifyAccess } from './accessManagement.js' import { getActiveAddressEntry, getAddressBookEntriesForVisualiser, identifyAddress, nameTokenIds, retrieveEnsLabelHashes, retrieveEnsNodeHashes } from './metadataUtils.js' import { getActiveAddress, sendPopupMessageToOpenWindows } from './backgroundUtils.js' -import { assertNever, assertUnreachable, modifyObject } from '../utils/typescript.js' +import { DistributiveOmit, assertNever, assertUnreachable, modifyObject } from '../utils/typescript.js' import { EthereumClientService } from '../simulation/services/EthereumClientService.js' -import { appendTransaction, calculateGasPrice, copySimulationState, getEmptySimulationStateWithRichAddress, getNonceFixedSimulatedTransactions, getTokenBalancesAfter, getWebsiteCreatedEthereumUnsignedTransactions, mockSignTransaction, setSimulationTransactionsAndSignedMessages } from '../simulation/services/SimulationModeEthereumClientService.js' +import { appendTransaction, calculateGasPrice, copySimulationState, getEmptySimulationStateWithRichAddress, getNonceFixedSimulatedTransactions, getTokenBalancesAfter, getWebsiteCreatedEthereumUnsignedTransactions, mockSignTransaction, setSimulationTransactionsAndSignedMessages, simulateEstimateGas } from '../simulation/services/SimulationModeEthereumClientService.js' import { Semaphore } from '../utils/semaphore.js' import { JsonRpcResponseError, handleUnexpectedError, isFailedToFetchError, isNewBlockAbort } from '../utils/errors.js' import { formSimulatedAndVisualizedTransaction } from '../components/formVisualizerResults.js' @@ -39,6 +39,7 @@ import { makeSureInterceptorIsNotSleeping } from './sleeping.js' import { decodeEthereumError } from '../utils/errorDecoding.js' import { estimateEthereumPricesForTokens } from '../simulation/priceEstimator.js' import { EnrichedEthereumEvents, EnrichedEthereumInputData } from '../types/EnrichedEthereumData.js' +import { VisualizedPersonalSignRequestSafeTx } from '../types/personal-message-definitions.js' async function updateMetadataForSimulation(simulationState: SimulationState, ethereum: EthereumClientService, requestAbortController: AbortController | undefined, eventsForEachTransaction: readonly EnrichedEthereumEvents[], inputData: readonly EnrichedEthereumInputData[], protectorResults: readonly ProtectorResults[]) { const settingsPromise = getSettings() @@ -60,8 +61,8 @@ async function updateMetadataForSimulation(simulationState: SimulationState, eth } } -export const simulateGovernanceContractExecution = async (pendingTransaction: PendingTransaction, ethereum: EthereumClientService) => { - const returnError = (text: string) => ({ success: false as const, error: { type: 'Other' as const, message: text } }) +export const simulateGovernanceContractExecution = async (pendingTransaction: PendingTransaction, ethereum: EthereumClientService): Promise> => { + const returnError = (errorMessage: string) => ({ success: false as const, errorType: 'Other' as const, errorMessage }) try { // identifies compound governane call and performs simulation if the vote passes if (pendingTransaction.transactionOrMessageCreationStatus !== 'Simulated') return returnError('Still simulating the voting transaction') @@ -85,7 +86,7 @@ export const simulateGovernanceContractExecution = async (pendingTransaction: Pe if (pendingTransaction.transactionToSimulate.transaction.to === null) return returnError('The transaction creates a contract instead of casting a vote') const params = governanceContractInterface.decodeFunctionData(voteFunction, dataStringWith0xStart(pendingTransaction.transactionToSimulate.transaction.input)) const addr = await identifyAddress(ethereum, undefined, pendingTransaction.transactionToSimulate.transaction.to) - if (!('abi' in addr) || addr.abi === undefined) return { success: false as const, error: { type: 'MissingAbi' as const, message: 'ABi for the governance contract is missing', addressBookEntry: addr } } + if (!('abi' in addr) || addr.abi === undefined) return { success: false as const, errorType: 'MissingAbi' as const, errorMessage: 'ABi for the governance contract is missing', errorAddressBookEntry: addr } const contractExecutionResult = await simulateCompoundGovernanceExecution(ethereum, addr, params[0]) if (contractExecutionResult === undefined) return returnError('Failed to simulate governance execution') const parentBlock = await ethereum.getBlock(undefined) @@ -122,6 +123,40 @@ export const simulateGovernanceContractExecution = async (pendingTransaction: Pe } } +export const simulateGnosisSafeMetaTransaction = async (gnosisSafeMessage: VisualizedPersonalSignRequestSafeTx, simulationState: SimulationState | undefined, ethereumClientService: EthereumClientService): Promise> => { + const returnError = (errorMessage: string) => ({ success: false as const, errorType: 'Other' as const, errorMessage }) + try { + const transactionWithoutGas = { + value: gnosisSafeMessage.message.message.value, + to: gnosisSafeMessage.to.address, + maxPriorityFeePerGas: 0n, + maxFeePerGas: 0n, + input: gnosisSafeMessage.parsedMessageData.input, + type: '1559' as const, + from: gnosisSafeMessage.verifyingContract.address, + nonce: 0n, + chainId: ethereumClientService.getChainId(), + } + const gasLimit = gnosisSafeMessage.message.message.baseGas !== 0n ? { gas: gnosisSafeMessage.message.message.baseGas } : await simulateEstimateGas(ethereumClientService, undefined, simulationState, transactionWithoutGas) + if ('error' in gasLimit) return returnError(gasLimit.error.message) + const transaction = { ...transactionWithoutGas, gas: gasLimit.gas } + const metaTransaction: WebsiteCreatedEthereumUnsignedTransaction = { + website: gnosisSafeMessage.website, + created: new Date(), + originalRequestParameters: { method: 'eth_sendTransaction', params: [transaction] }, + transactionIdentifier: gnosisSafeMessage.messageIdentifier, + success: true, + transaction, + } + const simulationStateAfterGnosisSafeMetaTransaction = await appendTransaction(ethereumClientService, undefined, simulationState, metaTransaction) + return { success: true as const, result: await visualizeSimulatorState(simulationStateAfterGnosisSafeMetaTransaction, ethereumClientService, undefined) } + } catch(error) { + console.warn(error) + if (error instanceof Error) return returnError(error.message) + return returnError('Unknown error occured') + } +} + async function visualizeSimulatorState(simulationState: SimulationState, ethereum: EthereumClientService, requestAbortController: AbortController | undefined): Promise { const transactions = getWebsiteCreatedEthereumUnsignedTransactions(simulationState.simulatedTransactions) const eventsForEachTransactionPromise = Promise.all(simulationState.simulatedTransactions.map(async (simulatedTransaction) => simulatedTransaction.ethSimulateV1CallResult.status === 'failure' ? [] : await parseEvents(simulatedTransaction.ethSimulateV1CallResult.logs, ethereum, requestAbortController))) @@ -613,6 +648,7 @@ export async function popupMessageHandler( case 'popup_get_export_settings': return await exportSettings() case 'popup_set_rpc_list': return await setNewRpcList(simulator, parsedRequest, settings) case 'popup_simulateGovernanceContractExecution': return await simulateGovernanceContractExecutionOnPass(simulator.ethereum, parsedRequest) + case 'popup_simulateGnosisSafeTransaction': return await simulateGnosisSafeTransactionOnPass(simulator.ethereum, parsedRequest) case 'popup_changeAddOrModifyAddressWindowState': return await changeAddOrModifyAddressWindowState(simulator.ethereum, parsedRequest) case 'popup_fetchAbiAndNameFromEtherscan': return await popupFetchAbiAndNameFromEtherscan(parsedRequest) case 'popup_openWebPage': return await openWebPage(parsedRequest) diff --git a/app/ts/background/popupMessageHandlers.ts b/app/ts/background/popupMessageHandlers.ts index b6faf483..af0adb2e 100644 --- a/app/ts/background/popupMessageHandlers.ts +++ b/app/ts/background/popupMessageHandlers.ts @@ -1,8 +1,8 @@ -import { changeActiveAddressAndChainAndResetSimulation, changeActiveRpc, refreshConfirmTransactionSimulation, updateSimulationState, updateSimulationMetadata, simulateGovernanceContractExecution, resetSimulatorStateFromConfig } from './background.js' +import { changeActiveAddressAndChainAndResetSimulation, changeActiveRpc, refreshConfirmTransactionSimulation, updateSimulationState, updateSimulationMetadata, simulateGovernanceContractExecution, resetSimulatorStateFromConfig, simulateGnosisSafeMetaTransaction } from './background.js' import { getSettings, setUseTabsInsteadOfPopup, setMakeMeRich, setPage, setUseSignersAddressAsActiveAddress, updateWebsiteAccess, exportSettingsAndAddressBook, importSettingsAndAddressBook, getMakeMeRich, getUseTabsInsteadOfPopup, getMetamaskCompatibilityMode, setMetamaskCompatibilityMode, getPage } from './settings.js' import { getPendingTransactionsAndMessages, getCurrentTabId, getTabState, saveCurrentTabId, setRpcList, getRpcList, getPrimaryRpcForChain, getRpcConnectionStatus, updateUserAddressBookEntries, getSimulationResults, setIdsOfOpenedTabs, getIdsOfOpenedTabs, updatePendingTransactionOrMessage, getLatestUnexpectedError, addEnsLabelHash, addEnsNodeHash } from './storageVariables.js' import { Simulator, parseEvents, parseInputData } from '../simulation/simulator.js' -import { ChangeActiveAddress, ChangeMakeMeRich, ChangePage, RemoveTransaction, RequestAccountsFromSigner, TransactionConfirmation, InterceptorAccess, ChangeInterceptorAccess, ChainChangeConfirmation, EnableSimulationMode, ChangeActiveChain, AddOrEditAddressBookEntry, GetAddressBookData, RemoveAddressBookEntry, InterceptorAccessRefresh, InterceptorAccessChangeAddress, Settings, RefreshConfirmTransactionMetadata, ChangeSettings, ImportSettings, SetRpcList, UpdateHomePage, SimulateGovernanceContractExecutionReply, SimulateGovernanceContractExecution, ChangeAddOrModifyAddressWindowState, FetchAbiAndNameFromEtherscan, OpenWebPage, DisableInterceptor, SetEnsNameForHash, UpdateConfirmTransactionDialog, UpdateConfirmTransactionDialogPendingTransactions } from '../types/interceptor-messages.js' +import { ChangeActiveAddress, ChangeMakeMeRich, ChangePage, RemoveTransaction, RequestAccountsFromSigner, TransactionConfirmation, InterceptorAccess, ChangeInterceptorAccess, ChainChangeConfirmation, EnableSimulationMode, ChangeActiveChain, AddOrEditAddressBookEntry, GetAddressBookData, RemoveAddressBookEntry, InterceptorAccessRefresh, InterceptorAccessChangeAddress, Settings, RefreshConfirmTransactionMetadata, ChangeSettings, ImportSettings, SetRpcList, UpdateHomePage, SimulateGovernanceContractExecution, ChangeAddOrModifyAddressWindowState, FetchAbiAndNameFromEtherscan, OpenWebPage, DisableInterceptor, SetEnsNameForHash, UpdateConfirmTransactionDialog, UpdateConfirmTransactionDialogPendingTransactions, SimulateGnosisSafeTransaction, SimulateExecutionReply } from '../types/interceptor-messages.js' import { formEthSendTransaction, formSendRawTransaction, resolvePendingTransactionOrMessage, updateConfirmTransactionView } from './windows/confirmTransaction.js' import { getAddressMetadataForAccess, requestAddressChange, resolveInterceptorAccess } from './windows/interceptorAccess.js' import { resolveChainChange } from './windows/changeChain.js' @@ -383,9 +383,18 @@ export async function simulateGovernanceContractExecutionOnPass(ethereum: Ethere const transaction = pendingTransactions.find((tx) => tx.type === 'Transaction' && tx.transactionIdentifier === request.data.transactionIdentifier) if (transaction === undefined || transaction.type !== 'Transaction') throw new Error(`Could not find transactionIdentifier: ${ request.data.transactionIdentifier }`) const governanceContractExecutionVisualisation = await simulateGovernanceContractExecution(transaction, ethereum) - return await sendPopupMessageToOpenWindows(serialize(SimulateGovernanceContractExecutionReply, { - method: 'popup_simulateGovernanceContractExecutionReply' as const, - data: { ...governanceContractExecutionVisualisation, transactionIdentifier: request.data.transactionIdentifier } + return await sendPopupMessageToOpenWindows(serialize(SimulateExecutionReply, { + method: 'popup_simulateExecutionReply' as const, + data: { ...governanceContractExecutionVisualisation, transactionOrMessageIdentifier: request.data.transactionIdentifier } + })) +} + +export async function simulateGnosisSafeTransactionOnPass(ethereum: EthereumClientService, request: SimulateGnosisSafeTransaction) { + const simulationResults = await getSimulationResults() + const gnosisTransactionExecutionVisualisation = await simulateGnosisSafeMetaTransaction(request.data.gnosisSafeMessage, simulationResults.simulationState, ethereum) + return await sendPopupMessageToOpenWindows(serialize(SimulateExecutionReply, { + method: 'popup_simulateExecutionReply' as const, + data: { ...gnosisTransactionExecutionVisualisation, transactionOrMessageIdentifier: request.data.gnosisSafeMessage.messageIdentifier } })) } diff --git a/app/ts/background/windows/confirmTransaction.ts b/app/ts/background/windows/confirmTransaction.ts index 82b95236..4565d48b 100644 --- a/app/ts/background/windows/confirmTransaction.ts +++ b/app/ts/background/windows/confirmTransaction.ts @@ -317,7 +317,7 @@ export async function openConfirmTransactionDialogForTransaction( const openedDialog = await getPendingTransactionWindow(simulator, websiteTabConnections) if (openedDialog === undefined) throw new Error('Failed to get pending transaction window!') - const pendingTransaction = { + const pendingTransaction = { type: 'Transaction' as const, popupOrTabId: openedDialog, originalRequestParameters: transactionParams, diff --git a/app/ts/components/pages/ConfirmTransaction.tsx b/app/ts/components/pages/ConfirmTransaction.tsx index 47954fe9..33d9028f 100644 --- a/app/ts/components/pages/ConfirmTransaction.tsx +++ b/app/ts/components/pages/ConfirmTransaction.tsx @@ -536,7 +536,7 @@ export function ConfirmTransaction() { setActiveAddressAndInformAboutIt = { undefined } modifyAddressWindowState = { modalState.state } close = { () => { setModalState({ page: 'noModal' }) } } - activeAddress = { undefined } + activeAddress = { currentPendingTransactionOrSignableMessage?.activeAddress } /> : <> } @@ -568,7 +568,7 @@ export function ConfirmTransaction() { setActiveAddressAndInformAboutIt = { undefined } modifyAddressWindowState = { modalState.state } close = { () => { setModalState({ page: 'noModal' }) } } - activeAddress = { undefined } + activeAddress = { currentPendingTransactionOrSignableMessage?.activeAddress } /> : <> } @@ -615,6 +615,7 @@ export function ConfirmTransaction() { renameAddressCallBack = { renameAddressCallBack } removeTransactionOrSignedMessage = { undefined } numberOfUnderTransactions = { underTransactions.length } + editEnsNamedHashCallBack = { editEnsNamedHashCallBack } /> } diff --git a/app/ts/components/pages/PersonalSign.tsx b/app/ts/components/pages/PersonalSign.tsx index da80b751..4d1739fb 100644 --- a/app/ts/components/pages/PersonalSign.tsx +++ b/app/ts/components/pages/PersonalSign.tsx @@ -16,12 +16,15 @@ import { EnrichedEIP712, EnrichedEIP712Message, TypeEnrichedEIP712MessageRecord import { TransactionCreated } from '../simulationExplaining/SimulationSummary.js' import { EnrichedSolidityTypeComponent } from '../subcomponents/solidityType.js' import { ParsedInputData, QuarantineReasons } from '../simulationExplaining/Transactions.js' +import { GnosisSafeVisualizer } from '../simulationExplaining/customExplainers/GnosisSafeVisualizer.js' +import { EditEnsNamedHashCallBack } from '../subcomponents/ens.js' type SignatureCardParams = { visualizedPersonalSignRequest: VisualizedPersonalSignRequest renameAddressCallBack: RenameAddressCallBack removeTransactionOrSignedMessage: ((transactionOrMessageIdentifier: TransactionOrMessageIdentifier) => void) | undefined - numberOfUnderTransactions: number, + numberOfUnderTransactions: number + editEnsNamedHashCallBack: EditEnsNamedHashCallBack } type SignatureHeaderParams = { @@ -38,7 +41,7 @@ export function identifySignature(data: VisualizedPersonalSignRequest) { signingAction: 'Sign Opensea order', } case 'SafeTx': return { - title: 'Arbitary Gnosis Safe message', + title: 'Gnosis Safe message', rejectAction: 'Reject Gnosis Safe message', simulationAction: 'Simulate Gnosis Safe message', signingAction: 'Sign Gnosis Safe message', @@ -108,6 +111,7 @@ export function SignatureHeader(params: SignatureHeaderParams) { type SignRequestParams = { visualizedPersonalSignRequest: VisualizedPersonalSignRequest renameAddressCallBack: RenameAddressCallBack + editEnsNamedHashCallBack: EditEnsNamedHashCallBack } const decodeMessage = (message: string) => { @@ -115,7 +119,7 @@ const decodeMessage = (message: string) => { return message } -function SignRequest({ visualizedPersonalSignRequest, renameAddressCallBack }: SignRequestParams) { +function SignRequest({ visualizedPersonalSignRequest, renameAddressCallBack, editEnsNamedHashCallBack }: SignRequestParams) { switch (visualizedPersonalSignRequest.type) { case 'NotParsed': { if (visualizedPersonalSignRequest.method === 'personal_sign') { @@ -134,8 +138,9 @@ function SignRequest({ visualizedPersonalSignRequest, renameAddressCallBack }: S

{ visualizedPersonalSignRequest.message }

} - case 'SafeTx': return @@ -187,8 +192,14 @@ function SignRequest({ visualizedPersonalSignRequest, renameAddressCallBack }: S } } -function SafeTx({ visualizedPersonalSignRequestSafeTx, renameAddressCallBack }: { visualizedPersonalSignRequestSafeTx: VisualizedPersonalSignRequestSafeTx, renameAddressCallBack: RenameAddressCallBack }) { +function SafeTx({ visualizedPersonalSignRequestSafeTx, renameAddressCallBack, activeAddress, editEnsNamedHashCallBack }: { visualizedPersonalSignRequestSafeTx: VisualizedPersonalSignRequestSafeTx, renameAddressCallBack: RenameAddressCallBack, activeAddress: bigint, editEnsNamedHashCallBack: EditEnsNamedHashCallBack }) { return <> + { visualizedPersonalSignRequestSafeTx.message.domain.chainId !== undefined ? <> diff --git a/app/ts/components/simulationExplaining/Transactions.tsx b/app/ts/components/simulationExplaining/Transactions.tsx index 5acd5a7e..1f5ad639 100644 --- a/app/ts/components/simulationExplaining/Transactions.tsx +++ b/app/ts/components/simulationExplaining/Transactions.tsx @@ -191,6 +191,7 @@ export function TransactionsAndSignedMessages(param: TransactionsAndSignedMessag visualizedPersonalSignRequest = { simTx } renameAddressCallBack = { param.renameAddressCallBack } removeTransactionOrSignedMessage = { param.removeTransactionOrSignedMessage } + editEnsNamedHashCallBack = { param.editEnsNamedHashCallBack } numberOfUnderTransactions = { 0 } /> : <> diff --git a/app/ts/components/simulationExplaining/customExplainers/GnosisSafeVisualizer.tsx b/app/ts/components/simulationExplaining/customExplainers/GnosisSafeVisualizer.tsx new file mode 100644 index 00000000..c34779b0 --- /dev/null +++ b/app/ts/components/simulationExplaining/customExplainers/GnosisSafeVisualizer.tsx @@ -0,0 +1,131 @@ +import { sendPopupMessageToBackgroundPage } from '../../../background/backgroundUtils.js' +import { MessageToPopup, SimulateExecutionReply } from '../../../types/interceptor-messages.js' +import { VisualizedPersonalSignRequestSafeTx } from '../../../types/personal-message-definitions.js' +import { RenameAddressCallBack, RpcConnectionStatus } from '../../../types/user-interface-types.js' +import { ErrorComponent } from '../../subcomponents/Error.js' +import { EditEnsNamedHashCallBack } from '../../subcomponents/ens.js' +import { Transaction } from '../Transactions.js' +import { useEffect, useState } from 'preact/hooks' + +type ShowSuccessOrFailureParams = { + gnosisSafeMessage: VisualizedPersonalSignRequestSafeTx + currentBlockNumber: undefined | bigint + rpcConnectionStatus: RpcConnectionStatus + activeAddress: bigint + simulateExecutionReply: SimulateExecutionReply | undefined + renameAddressCallBack: RenameAddressCallBack + editEnsNamedHashCallBack: EditEnsNamedHashCallBack +} + +const requestToSimulate = (gnosisSafeMessage: VisualizedPersonalSignRequestSafeTx) => sendPopupMessageToBackgroundPage({ method: 'popup_simulateGnosisSafeTransaction', data: { gnosisSafeMessage } }) + + +const ShowSuccessOrFailure = ({ simulateExecutionReply, activeAddress, renameAddressCallBack, editEnsNamedHashCallBack, gnosisSafeMessage }: ShowSuccessOrFailureParams) => { + if (simulateExecutionReply === undefined) { + return
+ +
+ } + + if (simulateExecutionReply.data.success === false) { + return
+ +
+ } + console.log(simulateExecutionReply.data.result.simulatedAndVisualizedTransactions) + const simTx = simulateExecutionReply.data.result.simulatedAndVisualizedTransactions.at(-1) + if (simTx === undefined) return <> + return
+ +
+} + +type GnosisSafeVisualizerParams = { + gnosisSafeMessage: VisualizedPersonalSignRequestSafeTx + activeAddress: bigint + renameAddressCallBack: RenameAddressCallBack + editEnsNamedHashCallBack: EditEnsNamedHashCallBack +} + +export function GnosisSafeVisualizer(param: GnosisSafeVisualizerParams) { + const [currentBlockNumber, setCurrentBlockNumber] = useState(undefined) + const [rpcConnectionStatus, setRpcConnectionStatus] = useState(undefined) + const [simulateExecutionReply, setSimulateExecutionReply] = useState(undefined) + + const [activeAddress, setActiveAddress] = useState(undefined) + + useEffect(() => { + const popupMessageListener = async (msg: unknown) => { + const maybeParsed = MessageToPopup.safeParse(msg) + if (!maybeParsed.success) return // not a message we are interested in + const parsed = maybeParsed.value + if (parsed.method === 'popup_new_block_arrived') { + setRpcConnectionStatus(parsed.data.rpcConnectionStatus) + return setCurrentBlockNumber(parsed.data.rpcConnectionStatus?.latestBlock?.number) + } + if (parsed.method !== 'popup_simulateExecutionReply') return + const reply = SimulateExecutionReply.parse(parsed) + if (reply.data.transactionOrMessageIdentifier !== param.gnosisSafeMessage.messageIdentifier) return + return setSimulateExecutionReply(reply) + } + browser.runtime.onMessage.addListener(popupMessageListener) + return () => browser.runtime.onMessage.removeListener(popupMessageListener) + }) + + useEffect(() => { + setActiveAddress(param.activeAddress) + setSimulateExecutionReply(undefined) + }, [param.activeAddress, param.gnosisSafeMessage.messageIdentifier]) + + if (activeAddress === undefined) return <> + return <> +
+ +
+

Simulation of this transaction should the multisig approve the transaction:

+
+
+ { simulateExecutionReply === undefined ? <> : + + } +
+
+
+
+ +
+ +} diff --git a/app/ts/components/simulationExplaining/customExplainers/GovernanceVoteVisualizer.tsx b/app/ts/components/simulationExplaining/customExplainers/GovernanceVoteVisualizer.tsx index 6df39cc8..af1eb3e8 100644 --- a/app/ts/components/simulationExplaining/customExplainers/GovernanceVoteVisualizer.tsx +++ b/app/ts/components/simulationExplaining/customExplainers/GovernanceVoteVisualizer.tsx @@ -1,6 +1,6 @@ import { sendPopupMessageToBackgroundPage } from '../../../background/backgroundUtils.js' import { AddressBookEntry } from '../../../types/addressBookTypes.js' -import { MessageToPopup, GovernanceVoteInputParameters, SimulateGovernanceContractExecutionReply } from '../../../types/interceptor-messages.js' +import { MessageToPopup, GovernanceVoteInputParameters, SimulateExecutionReply } from '../../../types/interceptor-messages.js' import { RenameAddressCallBack, RpcConnectionStatus } from '../../../types/user-interface-types.js' import { SimulatedAndVisualizedTransaction } from '../../../types/visualizer-types.js' import { EthereumQuantity } from '../../../types/wire-types.js' @@ -79,16 +79,16 @@ type ShowSuccessOrFailureParams = { currentBlockNumber: undefined | bigint rpcConnectionStatus: RpcConnectionStatus activeAddress: bigint - simulateGovernanceContractExecutionReply: SimulateGovernanceContractExecutionReply | undefined + simulateExecutionReply: SimulateExecutionReply | undefined renameAddressCallBack: RenameAddressCallBack editEnsNamedHashCallBack: EditEnsNamedHashCallBack } const simulateGovernanceVote = (transactionIdentifier: EthereumQuantity) => sendPopupMessageToBackgroundPage({ method: 'popup_simulateGovernanceContractExecution', data: { transactionIdentifier } }) -const ShowSuccessOrFailure = ({ simulateGovernanceContractExecutionReply, simTx, activeAddress, renameAddressCallBack, editEnsNamedHashCallBack }: ShowSuccessOrFailureParams) => { +const ShowSuccessOrFailure = ({ simulateExecutionReply, simTx, activeAddress, renameAddressCallBack, editEnsNamedHashCallBack }: ShowSuccessOrFailureParams) => { const missingAbiText = 'The governance contract is missing an ABI. Add an ABI to simulate execution of this proposal.' - if (simulateGovernanceContractExecutionReply === undefined) { + if (simulateExecutionReply === undefined) { return
{ simTx.transaction.to !== undefined && 'abi' in simTx.transaction.to && simTx.transaction.to.abi !== undefined ?
} - if (simulateGovernanceContractExecutionReply.data.success === false) { + if (simulateExecutionReply.data.success === false) { return
- { simulateGovernanceContractExecutionReply.data.error.type === 'MissingAbi' ? : } + /> : }
} - const govSimTx = simulateGovernanceContractExecutionReply.data.result.simulatedAndVisualizedTransactions.at(-1) + const govSimTx = simulateExecutionReply.data.result.simulatedAndVisualizedTransactions.at(-1) if (govSimTx === undefined) return <> return
} @@ -152,7 +152,7 @@ type GovernanceVoteVisualizerParams = { export function GovernanceVoteVisualizer(param: GovernanceVoteVisualizerParams) { const [currentBlockNumber, setCurrentBlockNumber] = useState(undefined) const [rpcConnectionStatus, setRpcConnectionStatus] = useState(undefined) - const [simulateGovernanceContractExecutionReply, setSimulateGovernanceContractExecutionReply] = useState(undefined) + const [simulateExecutionReply, setSimulateExecutionReply] = useState(undefined) const [governanceVoteInputParameters, setGovernanceVoteInputParameters] = useState(undefined) const [simTx, setSimTx] = useState(undefined) @@ -167,10 +167,10 @@ export function GovernanceVoteVisualizer(param: GovernanceVoteVisualizerParams) setRpcConnectionStatus(parsed.data.rpcConnectionStatus) return setCurrentBlockNumber(parsed.data.rpcConnectionStatus?.latestBlock?.number) } - if (parsed.method !== 'popup_simulateGovernanceContractExecutionReply') return - const reply = SimulateGovernanceContractExecutionReply.parse(parsed) - if (reply.data.transactionIdentifier !== param.simTx.transactionIdentifier) return - return setSimulateGovernanceContractExecutionReply(SimulateGovernanceContractExecutionReply.parse(parsed)) + if (parsed.method !== 'popup_simulateExecutionReply') return + const reply = SimulateExecutionReply.parse(parsed) + if (reply.data.transactionOrMessageIdentifier !== param.simTx.transactionIdentifier) return + return setSimulateExecutionReply(reply) } browser.runtime.onMessage.addListener(popupMessageListener) return () => browser.runtime.onMessage.removeListener(popupMessageListener) @@ -180,7 +180,7 @@ export function GovernanceVoteVisualizer(param: GovernanceVoteVisualizerParams) setGovernanceVoteInputParameters(param.governanceVoteInputParameters) setSimTx(param.simTx) setActiveAddress(param.activeAddress) - setSimulateGovernanceContractExecutionReply(undefined) + setSimulateExecutionReply(undefined) }, [param.simTx.transactionIdentifier]) if (governanceVoteInputParameters === undefined || simTx === undefined || activeAddress === undefined) return <> @@ -193,7 +193,7 @@ export function GovernanceVoteVisualizer(param: GovernanceVoteVisualizerParams)

Simulation of this proposal's outcome should the vote pass:

- { simulateGovernanceContractExecutionReply === undefined ? <> : + { simulateExecutionReply === undefined ? <> : }
@@ -204,7 +204,7 @@ export function GovernanceVoteVisualizer(param: GovernanceVoteVisualizerParams) -const PartiallyParsedSimulateGovernanceContractExecutionReply = funtypes.ReadonlyObject({ - method: funtypes.Literal('popup_simulateGovernanceContractExecutionReply'), +type PartiallyParsedSimulateExecutionReply = funtypes.Static +const PartiallyParsedSimulateExecutionReply = funtypes.ReadonlyObject({ + method: funtypes.Literal('popup_simulateExecutionReply'), data: funtypes.Unknown, }).asReadonly() export type GovernanceVoteInputParameters = funtypes.Static export const GovernanceVoteInputParameters = funtypes.ReadonlyObject({ proposalId: EthereumQuantity, - support: funtypes.Union(funtypes.Boolean, EthereumQuantity), + support: funtypes.Union(funtypes.Boolean, EthereumQuantity), reason: funtypes.Union(funtypes.Undefined, funtypes.String), params: funtypes.Union(funtypes.Undefined, EthereumData), signature: funtypes.Union(funtypes.Undefined, EthereumData), voter: funtypes.Union(funtypes.Undefined, EthereumAddress), }) -export type SimulateGovernanceContractExecutionReply = funtypes.Static -export const SimulateGovernanceContractExecutionReply = funtypes.ReadonlyObject({ - method: funtypes.Literal('popup_simulateGovernanceContractExecutionReply'), - data: funtypes.Union( - funtypes.ReadonlyObject({ - transactionIdentifier: EthereumQuantity, - success: funtypes.Literal(false), - error: funtypes.Union( - funtypes.ReadonlyObject({ - type: funtypes.Literal('MissingAbi'), - message: funtypes.String, - addressBookEntry: AddressBookEntry, - }), - ) - }), - funtypes.ReadonlyObject({ - transactionIdentifier: EthereumQuantity, - success: funtypes.Literal(false), - error: funtypes.Union( - funtypes.ReadonlyObject({ - type: funtypes.Literal('Other'), - message: funtypes.String, - }), - ) - }), - funtypes.ReadonlyObject({ - transactionIdentifier: EthereumQuantity, - success: funtypes.Literal(true), - result: funtypes.ReadonlyObject({ - namedTokenIds: funtypes.ReadonlyArray(NamedTokenId), - addressBookEntries: funtypes.ReadonlyArray(AddressBookEntry), - simulatedAndVisualizedTransactions: funtypes.ReadonlyArray(SimulatedAndVisualizedTransaction), - visualizedPersonalSignRequests: funtypes.ReadonlyArray(VisualizedPersonalSignRequest), - tokenPrices: funtypes.ReadonlyArray(TokenPriceEstimate), - eventsForEachTransaction: funtypes.ReadonlyArray(funtypes.ReadonlyArray(EnrichedEthereumEvent)), - parsedInputData: funtypes.ReadonlyArray(EnrichedEthereumInputData), - protectors: funtypes.ReadonlyArray(ProtectorResults), - simulationState: funtypes.Union(SimulationState), - }) +export type SimulateExecutionReplyData = funtypes.Static +export const SimulateExecutionReplyData = funtypes.Union( + funtypes.ReadonlyObject({ + success: funtypes.Literal(false), + errorType: funtypes.Literal('Other'), + transactionOrMessageIdentifier: EthereumQuantity, + errorMessage: funtypes.String, + }), + funtypes.ReadonlyObject({ + success: funtypes.Literal(false), + errorType: funtypes.Literal('MissingAbi'), + transactionOrMessageIdentifier: EthereumQuantity, + errorMessage: funtypes.String, + errorAddressBookEntry: AddressBookEntry, + }), + funtypes.ReadonlyObject({ + success: funtypes.Literal(true), + transactionOrMessageIdentifier: EthereumQuantity, + result: funtypes.ReadonlyObject({ + namedTokenIds: funtypes.ReadonlyArray(NamedTokenId), + addressBookEntries: funtypes.ReadonlyArray(AddressBookEntry), + simulatedAndVisualizedTransactions: funtypes.ReadonlyArray(SimulatedAndVisualizedTransaction), + visualizedPersonalSignRequests: funtypes.ReadonlyArray(VisualizedPersonalSignRequest), + tokenPrices: funtypes.ReadonlyArray(TokenPriceEstimate), + eventsForEachTransaction: funtypes.ReadonlyArray(funtypes.ReadonlyArray(EnrichedEthereumEvent)), + parsedInputData: funtypes.ReadonlyArray(EnrichedEthereumInputData), + protectors: funtypes.ReadonlyArray(ProtectorResults), + simulationState: funtypes.Union(SimulationState), }) - ) + }) +) + +export type SimulateExecutionReply = funtypes.Static +export const SimulateExecutionReply = funtypes.ReadonlyObject({ + method: funtypes.Literal('popup_simulateExecutionReply'), + data: SimulateExecutionReplyData }).asReadonly() export type SimulateGovernanceContractExecution = funtypes.Static @@ -666,6 +661,13 @@ export const SimulateGovernanceContractExecution = funtypes.ReadonlyObject({ method: funtypes.Literal('popup_simulateGovernanceContractExecution'), data: funtypes.ReadonlyObject({ transactionIdentifier: EthereumQuantity }) }) +export type SimulateGnosisSafeTransaction = funtypes.Static +export const SimulateGnosisSafeTransaction = funtypes.ReadonlyObject({ + method: funtypes.Literal('popup_simulateGnosisSafeTransaction'), + data: funtypes.ReadonlyObject({ + gnosisSafeMessage: VisualizedPersonalSignRequestSafeTx, + }) +}) type SettingsOpenedReply = funtypes.Static const SettingsOpenedReply = funtypes.ReadonlyObject({ @@ -796,6 +798,7 @@ export const PopupMessage = funtypes.Union( funtypes.ReadonlyObject({ method: funtypes.Literal('popup_import_settings'), data: funtypes.ReadonlyObject({ fileContents: funtypes.String }) }), funtypes.ReadonlyObject({ method: funtypes.Literal('popup_get_export_settings') }), SimulateGovernanceContractExecution, + SimulateGnosisSafeTransaction, funtypes.ReadonlyObject({ method: funtypes.Literal('popup_settingsOpened') }), ChangeSettings, SetRpcList, @@ -823,7 +826,7 @@ export const MessageToPopup = funtypes.Union( UpdateRPCList, SimulationUpdateStartedOrEnded, PartialUpdateHomePage, - PartiallyParsedSimulateGovernanceContractExecutionReply, + PartiallyParsedSimulateExecutionReply, SettingsOpenedReply, PopupAddOrModifyAddressWindowStateInfomation, FetchAbiAndNameFromEtherscanReply, diff --git a/app/ts/types/personal-message-definitions.ts b/app/ts/types/personal-message-definitions.ts index 7ec54ba8..aecb1381 100644 --- a/app/ts/types/personal-message-definitions.ts +++ b/app/ts/types/personal-message-definitions.ts @@ -176,18 +176,18 @@ const SeaPortSingleConsideration = funtypes.ReadonlyObject({ export type OpenSeaOrderMessage = funtypes.Static export const OpenSeaOrderMessage = funtypes.ReadonlyObject({ - offerer: EthereumAddress, - offer: funtypes.ReadonlyArray(SeaPortSingleOffer), - consideration: funtypes.ReadonlyArray(SeaPortSingleConsideration), - startTime: NonHexBigInt, - endTime: NonHexBigInt, - orderType: SeaPortOrderType, - zone: EthereumAddress, - zoneHash: EthereumBytes32, - salt: NonHexBigInt, - conduitKey: EthereumBytes32, - totalOriginalConsiderationItems: NonHexBigInt, - counter: NonHexBigInt, + offerer: EthereumAddress, + offer: funtypes.ReadonlyArray(SeaPortSingleOffer), + consideration: funtypes.ReadonlyArray(SeaPortSingleConsideration), + startTime: NonHexBigInt, + endTime: NonHexBigInt, + orderType: SeaPortOrderType, + zone: EthereumAddress, + zoneHash: EthereumBytes32, + salt: NonHexBigInt, + conduitKey: EthereumBytes32, + totalOriginalConsiderationItems: NonHexBigInt, + counter: NonHexBigInt, }) type OpenSeaOrder = funtypes.Static @@ -337,18 +337,18 @@ export const SeaPortSingleConsiderationWithAddressBookEntries = funtypes.Readon export type OpenSeaOrderMessageWithAddressBookEntries = funtypes.Static export const OpenSeaOrderMessageWithAddressBookEntries = funtypes.ReadonlyObject({ - offerer: AddressBookEntry, - offer: funtypes.ReadonlyArray(SeaPortSingleOfferWithAddressBookEntries), - consideration: funtypes.ReadonlyArray(SeaPortSingleConsiderationWithAddressBookEntries), - startTime: NonHexBigInt, - endTime: NonHexBigInt, - orderType: SeaPortOrderType, - zone: AddressBookEntry, - zoneHash: EthereumBytes32, - salt: NonHexBigInt, - conduitKey: EthereumBytes32, - totalOriginalConsiderationItems: NonHexBigInt, - counter: NonHexBigInt, + offerer: AddressBookEntry, + offer: funtypes.ReadonlyArray(SeaPortSingleOfferWithAddressBookEntries), + consideration: funtypes.ReadonlyArray(SeaPortSingleConsiderationWithAddressBookEntries), + startTime: NonHexBigInt, + endTime: NonHexBigInt, + orderType: SeaPortOrderType, + zone: AddressBookEntry, + zoneHash: EthereumBytes32, + salt: NonHexBigInt, + conduitKey: EthereumBytes32, + totalOriginalConsiderationItems: NonHexBigInt, + counter: NonHexBigInt, }) type PersonalSignRequestBase = funtypes.Static @@ -435,43 +435,43 @@ export type SafeTx = funtypes.Static export const SafeTx = funtypes.ReadonlyObject({ types: funtypes.ReadonlyObject({ SafeTx: funtypes.ReadonlyTuple( - funtypes.ReadonlyObject({ name: funtypes.Literal('to'), type: funtypes.Literal('address') }), - funtypes.ReadonlyObject({ name: funtypes.Literal('value'), type: funtypes.Literal('uint256') }), - funtypes.ReadonlyObject({ name: funtypes.Literal('data'), type: funtypes.Literal('bytes') }), - funtypes.ReadonlyObject({ name: funtypes.Literal('operation'), type: funtypes.Literal('uint8') }), - funtypes.ReadonlyObject({ name: funtypes.Literal('safeTxGas'), type: funtypes.Literal('uint256') }), - funtypes.ReadonlyObject({ name: funtypes.Literal('baseGas'), type: funtypes.Literal('uint256') }), - funtypes.ReadonlyObject({ name: funtypes.Literal('gasPrice'), type: funtypes.Literal('uint256') }), - funtypes.ReadonlyObject({ name: funtypes.Literal('gasToken'), type: funtypes.Literal('address') }), - funtypes.ReadonlyObject({ name: funtypes.Literal('refundReceiver'), type: funtypes.Literal('address') }), - funtypes.ReadonlyObject({ name: funtypes.Literal('nonce'), type: funtypes.Literal('uint256') }) + funtypes.ReadonlyObject({ name: funtypes.Literal('to'), type: funtypes.Literal('address') }), + funtypes.ReadonlyObject({ name: funtypes.Literal('value'), type: funtypes.Literal('uint256') }), + funtypes.ReadonlyObject({ name: funtypes.Literal('data'), type: funtypes.Literal('bytes') }), + funtypes.ReadonlyObject({ name: funtypes.Literal('operation'), type: funtypes.Literal('uint8') }), + funtypes.ReadonlyObject({ name: funtypes.Literal('safeTxGas'), type: funtypes.Literal('uint256') }), + funtypes.ReadonlyObject({ name: funtypes.Literal('baseGas'), type: funtypes.Literal('uint256') }), + funtypes.ReadonlyObject({ name: funtypes.Literal('gasPrice'), type: funtypes.Literal('uint256') }), + funtypes.ReadonlyObject({ name: funtypes.Literal('gasToken'), type: funtypes.Literal('address') }), + funtypes.ReadonlyObject({ name: funtypes.Literal('refundReceiver'), type: funtypes.Literal('address') }), + funtypes.ReadonlyObject({ name: funtypes.Literal('nonce'), type: funtypes.Literal('uint256') }) ), - EIP712Domain: funtypes.ReadonlyTuple( + EIP712Domain: funtypes.ReadonlyTuple( funtypes.Partial({ name: funtypes.Literal('chainId'), type: funtypes.Literal('uint256') }), funtypes.ReadonlyObject({ name: funtypes.Literal('verifyingContract'), type: funtypes.Literal('address') }) ), - }), - primaryType: funtypes.Literal('SafeTx'), - domain: funtypes.Intersect( + }), + primaryType: funtypes.Literal('SafeTx'), + domain: funtypes.Intersect( funtypes.Partial({ chainId: funtypes.Union(EthereumQuantity, NonHexBigInt) }), funtypes.ReadonlyObject({ - verifyingContract: EthereumAddress, + verifyingContract: EthereumAddress, }) ), - message: funtypes.ReadonlyObject({ - to: EthereumAddress, - value: NonHexBigInt, - data: EthereumInput, - operation: NonHexBigInt, - safeTxGas: NonHexBigInt, - baseGas: NonHexBigInt, - gasPrice: NonHexBigInt, - gasToken: EthereumAddress, - refundReceiver: EthereumAddress, - nonce: NonHexBigInt, - }) + message: funtypes.ReadonlyObject({ + to: EthereumAddress, + value: NonHexBigInt, + data: EthereumInput, + operation: NonHexBigInt, + safeTxGas: NonHexBigInt, + baseGas: NonHexBigInt, + gasPrice: NonHexBigInt, + gasToken: EthereumAddress, + refundReceiver: EthereumAddress, + nonce: NonHexBigInt, + }) }) export type VisualizedPersonalSignRequestSafeTx = funtypes.Static