diff --git a/app/css/interceptor.css b/app/css/interceptor.css index 1da2f738..f50cf86e 100644 --- a/app/css/interceptor.css +++ b/app/css/interceptor.css @@ -1,6 +1,19 @@ @charset "utf-8"; +@font-face { + font-family: "Atkinson"; + src: url("../fonts/Atkinson-Hyperlegible-Regular-102a.woff2") format("woff2"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "Inter"; + src: url("../fonts/Inter-VariableFont.ttf") format("ttf"); +} + html { font-size: 100%; } +body { font-family: "Inter" } :root { --bg-color: #303030; @@ -41,6 +54,7 @@ html { font-size: 100%; } } button:where(:not(.btn)) { + font-family: "Inter"; background-color: var(--primary-color); } @@ -48,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; } @@ -248,6 +262,7 @@ li { } .card-header { + column-gap: 1rem; background-color: var(--card-header-bg-color); } @@ -328,6 +343,39 @@ li { flex-direction: column; } +.button.is-reveal:after { + content: ''; + border-style: solid; + border-width: 2px 2px 0 0; + display: inline-block; + height: 0.6em; + width: 0.6em; + position: relative; + top: 6%; + transform: rotate(135deg); + transform-origin: 66% 33%; + transition: transform 150ms; + flex-shrink: 0; +} + +.dropdown.is-active > .dropdown-trigger > button:after { + transform: rotate(-45deg); +} + +.dropdown-trigger > button.is-danger:before { + content: '!'; + width: 1.1em; + height: 1.1em; + font-size: 0.9em; + background: white; + color: var(--negative-color); + flex: 0 0 auto; + align-items: center; + display: flex; + justify-content: center; + border-radius: 100%; +} + .dropdown-content { background-color: var(--card-content-bg-color); box-shadow: 0px 8px 16px 0px rgb(0 0 0 / 20%); @@ -335,6 +383,9 @@ li { .dropdown-item { color: var(--text-color); + background: transparent; + border: initial; + cursor: pointer; } a.dropdown-item.is-active, button.dropdown-item.is-active { @@ -342,9 +393,6 @@ a.dropdown-item.is-active, button.dropdown-item.is-active { } .card-header-icon { - padding: 0; - padding-left: 0.75rem; - padding-right: 0.75rem; background-color: transparent; } @@ -1106,3 +1154,14 @@ summary:where(details[open] > *) { place-content: center; justify-items: center; } + +.text-legible { + font-family: 'Atkinson'; +} + +.truncate { + display: block; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} diff --git a/app/fonts/Atkinson-Hyperlegible-Regular-102a.woff2 b/app/fonts/Atkinson-Hyperlegible-Regular-102a.woff2 new file mode 100644 index 00000000..99b3c6f5 Binary files /dev/null and b/app/fonts/Atkinson-Hyperlegible-Regular-102a.woff2 differ diff --git a/app/fonts/Inter-VariableFont.ttf b/app/fonts/Inter-VariableFont.ttf new file mode 100644 index 00000000..e7247087 Binary files /dev/null and b/app/fonts/Inter-VariableFont.ttf differ diff --git a/app/img/warning-sign-white.svg b/app/img/warning-sign-white.svg deleted file mode 100644 index 7fcf89ae..00000000 --- a/app/img/warning-sign-white.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/ts/background/background.ts b/app/ts/background/background.ts index 13d64221..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, EnrichedEthereumEvents, EnrichedEthereumInputData, 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' @@ -38,6 +38,8 @@ import { connectedToSigner, ethAccountsReply, signerChainChanged, signerReply, w 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() @@ -59,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') @@ -84,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) @@ -121,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))) @@ -612,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/metadataUtils.ts b/app/ts/background/metadataUtils.ts index 4f44b74f..bd764357 100644 --- a/app/ts/background/metadataUtils.ts +++ b/app/ts/background/metadataUtils.ts @@ -1,6 +1,6 @@ import { addressString, addressStringWithout0x, bytesToUnsigned, checksummedAddress } from '../utils/bigint.js' import { AddressBookEntries, AddressBookEntry } from '../types/addressBookTypes.js' -import { EnrichedEthereumEventWithMetadata, EnrichedEthereumEvents, EnrichedEthereumInputData, EnsEvent, NamedTokenId, SimulationState, TokenEvent, TokenVisualizerResultWithMetadata } from '../types/visualizer-types.js' +import { NamedTokenId, SimulationState } from '../types/visualizer-types.js' import { tokenMetadata, contractMetadata, erc721Metadata, erc1155Metadata } from '@darkflorist/address-metadata' import { ethers } from 'ethers' import { ENS_ADDR_REVERSE_NODE, ENS_TOKEN_WRAPPER, ETHEREUM_COIN_ICON, ETHEREUM_LOGS_LOGGER_ADDRESS, MOCK_ADDRESS } from '../utils/constants.js' @@ -16,6 +16,7 @@ import { EthereumBytes32 } from '../types/wire-types.js' import { ENSNameHashes } from '../types/ens.js' import { keccak_256 } from '@noble/hashes/sha3' const LOGO_URI_PREFIX = '../vendor/@darkflorist/address-metadata' +import { EnrichedEthereumEventWithMetadata, EnrichedEthereumEvents, EnrichedEthereumInputData, EnsEvent, SolidityVariable, TokenEvent, TokenVisualizerResultWithMetadata } from '../types/EnrichedEthereumData.js' const pathJoin = (parts: string[], sep = '/') => parts.join(sep).replace(new RegExp(sep + '{1,}', 'g'), sep) @@ -157,13 +158,17 @@ export async function identifyAddress(ethereumClientService: EthereumClientServi return entry } -export async function getAddressBookEntriesForVisualiser(ethereumClientService: EthereumClientService, requestAbortController: AbortController | undefined, events: EnrichedEthereumEvents, inputData: readonly EnrichedEthereumInputData[], simulationState: SimulationState): Promise { - const eventAndTransactionArguments = [...events.flatMap((event) => event.type !== 'NonParsed' ? event.args : []), ...inputData.flatMap((event) => event.type !== 'NonParsed' ? event.args : [])] - const addressesInEventsAndInputData = eventAndTransactionArguments.flatMap((argumentVariable) => { +export const getAddressesForSolidityTypes = (variables: readonly SolidityVariable[]) => { + return variables.map((argumentVariable) => { if (argumentVariable.typeValue.type === 'address') return argumentVariable.typeValue.value if (argumentVariable.typeValue.type === 'address[]') return argumentVariable.typeValue.value return undefined }).filter((address): address is bigint => address !== undefined) +} + +export async function getAddressBookEntriesForVisualiser(ethereumClientService: EthereumClientService, requestAbortController: AbortController | undefined, events: EnrichedEthereumEvents, inputData: readonly EnrichedEthereumInputData[], simulationState: SimulationState): Promise { + const eventAndTransactionArguments = [...events.flatMap((event) => event.type !== 'NonParsed' ? event.args : []), ...inputData.flatMap((event) => event.type !== 'NonParsed' ? event.args : [])] + const addressesInEventsAndInputData = getAddressesForSolidityTypes(eventAndTransactionArguments) const addressesToFetchMetadata = [...addressesInEventsAndInputData, ...events.map((event) => event.address)] for (const tx of simulationState.simulatedTransactions) { 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/background/windows/personalSign.ts b/app/ts/background/windows/personalSign.ts index 0d6da6fb..e30d55ac 100644 --- a/app/ts/background/windows/personalSign.ts +++ b/app/ts/background/windows/personalSign.ts @@ -4,11 +4,12 @@ import { OpenSeaOrderMessage, PersonalSignRequestIdentifiedEIP712Message, Visual import { assertNever } from '../../utils/typescript.js' import { extractEIP712Message, validateEIP712Types } from '../../utils/eip712Parsing.js' import { getRpcNetworkForChain, getTabState } from '../storageVariables.js' -import { identifyAddress } from '../metadataUtils.js' +import { getAddressesForSolidityTypes, identifyAddress } from '../metadataUtils.js' import { AddressBookEntry } from '../../types/addressBookTypes.js' import { SignedMessageTransaction } from '../../types/visualizer-types.js' import { RpcNetwork } from '../../types/rpc.js' import { getChainName } from '../../utils/constants.js' +import { parseInputData } from '../../simulation/simulator.js' async function addMetadataToOpenSeaOrder(ethereumClientService: EthereumClientService, requestAbortController: AbortController | undefined, openSeaOrder: OpenSeaOrderMessage) { return { @@ -123,20 +124,29 @@ export async function craftPersonalSignPopupMessage(ethereumClientService: Ether rawMessage: stringifyJSONWithBigInts(parsed, 4), } } - case 'SafeTx': return { - method: originalParams.originalRequestParameters.method, - ...basicParams, - rpcNetwork: parsed.domain.chainId !== undefined && rpcNetwork.chainId !== parsed.domain.chainId ? await getRpcNetworkForChain(parsed.domain.chainId) : rpcNetwork, - type: 'SafeTx' as const, - message: parsed, - account, - to: await identifyAddress(ethereumClientService, requestAbortController, parsed.message.to), - gasToken: await identifyAddress(ethereumClientService, requestAbortController, parsed.message.gasToken), - refundReceiver: await identifyAddress(ethereumClientService, requestAbortController, parsed.message.refundReceiver), - verifyingContract: await identifyAddress(ethereumClientService, requestAbortController, parsed.domain.verifyingContract), - quarantine: false, - quarantineReasons: [], - rawMessage: stringifyJSONWithBigInts(parsed, 4), + case 'SafeTx': { + const addresses = { + to: await identifyAddress(ethereumClientService, requestAbortController, parsed.message.to), + gasToken: await identifyAddress(ethereumClientService, requestAbortController, parsed.message.gasToken), + refundReceiver: await identifyAddress(ethereumClientService, requestAbortController, parsed.message.refundReceiver), + verifyingContract: await identifyAddress(ethereumClientService, requestAbortController, parsed.domain.verifyingContract), + } + const parsedMessageData = await parseInputData({ to: parsed.message.to, value: 0n, input: parsed.message.data }, ethereumClientService, requestAbortController) + const addressesInEventsAndInputData = getAddressesForSolidityTypes(parsedMessageData.type === 'Parsed' ? parsedMessageData.args : []) + return { + method: originalParams.originalRequestParameters.method, + ...basicParams, + rpcNetwork: parsed.domain.chainId !== undefined && rpcNetwork.chainId !== parsed.domain.chainId ? await getRpcNetworkForChain(parsed.domain.chainId) : rpcNetwork, + type: 'SafeTx' as const, + message: parsed, + account, + ...addresses, + quarantine: false, + quarantineReasons: [], + rawMessage: stringifyJSONWithBigInts(parsed, 4), + parsedMessageData, + parsedMessageDataAddressBookEntries: await Promise.all(addressesInEventsAndInputData.map((address) => identifyAddress(ethereumClientService, requestAbortController, address))) + } } case 'OrderComponents': return { method: originalParams.originalRequestParameters.method, diff --git a/app/ts/components/formVisualizerResults.ts b/app/ts/components/formVisualizerResults.ts index fcfa377f..8c842960 100644 --- a/app/ts/components/formVisualizerResults.ts +++ b/app/ts/components/formVisualizerResults.ts @@ -1,10 +1,11 @@ import { addressString, dataStringWith0xStart } from '../utils/bigint.js' -import { EnrichedEthereumEventWithMetadata, EnrichedEthereumEvents, EnrichedEthereumInputData, NamedTokenId, ParsedEnsEvent, ProtectorResults, SimulatedAndVisualizedTransaction, SimulationState } from '../types/visualizer-types.js' +import { NamedTokenId, ProtectorResults, SimulatedAndVisualizedTransaction, SimulationState } from '../types/visualizer-types.js' import { AddressBookEntry } from '../types/addressBookTypes.js' import { Interface } from 'ethers' import { decodeEthereumError } from '../utils/errorDecoding.js' import { MaybeENSLabelHashes, MaybeENSNameHashes } from '../types/ens.js' import { assertNever } from '../utils/typescript.js' +import { EnrichedEthereumEventWithMetadata, EnrichedEthereumEvents, EnrichedEthereumInputData, ParsedEnsEvent } from '../types/EnrichedEthereumData.js' const enrichEnsEvent = (event: ParsedEnsEvent, ens: { ensNameHashes: MaybeENSNameHashes, ensLabelHashes: MaybeENSLabelHashes }, addressMetaData: Map) => { const getNameHash = (node: bigint) => ens.ensNameHashes.find((nameHash) => nameHash.nameHash === node) ?? { nameHash: node, name: undefined } 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/Home.tsx b/app/ts/components/pages/Home.tsx index cb90c077..f5cdf809 100644 --- a/app/ts/components/pages/Home.tsx +++ b/app/ts/components/pages/Home.tsx @@ -39,16 +39,14 @@ function SignerExplanation(param: SignerExplanationParams) { function FirstCardHeader(param: FirstCardParams) { return <> -
-
- - - - - +
+
+ + +
-
-
+
+
-
+
{ param.changeActiveRpc(entry) } }/>
@@ -93,17 +91,17 @@ function InterceptorDisabledButton({ disableInterceptorToggle, interceptorDisabl function FirstCard(param: FirstCardParams) { if (param.tabState?.signerName === 'NoSigner' && param.simulationMode === false) { return <> -
+
-
+ } return <> -
+
{ param.useSignersAddressAsActiveAddress || !param.simulationMode ? @@ -140,7 +138,7 @@ function FirstCard(param: FirstCardParams) {
}
-
+ 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 ? <> @@ -226,10 +237,12 @@ function SafeTx({ visualizedPersonalSignRequestSafeTx, renameAddressCallBack }: }/> -

Raw transaction input:

+

Gnosis Safe meta transaction input:

{ dataStringWith0xStart(visualizedPersonalSignRequestSafeTx.message.message.data) }

+

Parsed Gnosis Safe meta transaction:

+ { visualizedPersonalSignRequestSafeTx.parsedMessageData?.type !== 'Parsed' ?

No ABI available

: } } diff --git a/app/ts/components/simulationExplaining/SimulationSummary.tsx b/app/ts/components/simulationExplaining/SimulationSummary.tsx index 5b168175..30c6a9a4 100644 --- a/app/ts/components/simulationExplaining/SimulationSummary.tsx +++ b/app/ts/components/simulationExplaining/SimulationSummary.tsx @@ -1,6 +1,6 @@ import { Erc1155TokenBalanceChange, Erc721and1155OperatorChange, LogSummarizer, SummaryOutcome } from '../../simulation/services/LogSummarizer.js' import { RenameAddressCallBack, RpcConnectionStatus } from '../../types/user-interface-types.js' -import { Erc721TokenApprovalChange, SimulatedAndVisualizedTransaction, SimulationAndVisualisationResults, ERC20TokenApprovalChange, Erc20TokenBalanceChange, TransactionWithAddressBookEntries, NamedTokenId, EnrichedEthereumInputData } from '../../types/visualizer-types.js' +import { Erc721TokenApprovalChange, SimulatedAndVisualizedTransaction, SimulationAndVisualisationResults, ERC20TokenApprovalChange, Erc20TokenBalanceChange, TransactionWithAddressBookEntries, NamedTokenId } from '../../types/visualizer-types.js' import { BigAddress, SmallAddress, WebsiteOriginText } from '../subcomponents/address.js' import { Ether, EtherAmount, EtherSymbol, TokenWithAmount, TokenAmount, TokenPrice, TokenSymbol, TokenOrEth } from '../subcomponents/coins.js' import { NonTokenLogAnalysis, ParsedInputData, TokenLogAnalysis } from './Transactions.js' @@ -17,6 +17,7 @@ import { AddressBookEntry, Erc1155Entry, Erc20TokenEntry, Erc721Entry } from '.. import { Website } from '../../types/websiteAccessTypes.js' import { extractTokenEvents } from '../../background/metadataUtils.js' import { EditEnsNamedHashCallBack } from '../subcomponents/ens.js' +import { EnrichedEthereumInputData } from '../../types/EnrichedEthereumData.js' type Erc20BalanceChangeParams = { erc20TokenBalanceChanges: Erc20TokenBalanceChange[] @@ -723,7 +724,7 @@ export function SimulationSummary(param: SimulationSummaryParams) { type RawTransactionDetailsCardParams = { addressMetaData: readonly AddressBookEntry[] - parsedInputData: EnrichedEthereumInputData | undefined + parsedInputData: EnrichedEthereumInputData transaction: TransactionWithAddressBookEntries renameAddressCallBack: RenameAddressCallBack gasSpent: bigint @@ -762,8 +763,8 @@ export function RawTransactionDetailsCard({ transaction, renameAddressCallBack, - - + + { transaction.type !== '1559' ? <> @@ -782,7 +783,7 @@ export function RawTransactionDetailsCard({ transaction, renameAddressCallBack,

{ dataStringWith0xStart(transaction.input) }

Parsed transaction input:

- { parsedInputData?.type !== 'Parsed' ? <> : } + { parsedInputData?.type !== 'Parsed' ?

No ABI available

: } } diff --git a/app/ts/components/simulationExplaining/SwapTransactions.tsx b/app/ts/components/simulationExplaining/SwapTransactions.tsx index 4e4d814e..4863da30 100644 --- a/app/ts/components/simulationExplaining/SwapTransactions.tsx +++ b/app/ts/components/simulationExplaining/SwapTransactions.tsx @@ -1,4 +1,4 @@ -import { SimulatedAndVisualizedTransaction, TokenVisualizerResultWithMetadata } from '../../types/visualizer-types.js' +import { SimulatedAndVisualizedTransaction } from '../../types/visualizer-types.js' import * as funtypes from 'funtypes' import { EthereumQuantity } from '../../types/wire-types.js' import { addressString } from '../../utils/bigint.js' @@ -7,6 +7,7 @@ import { AddressBookEntry, Erc1155Entry, Erc20TokenEntry, Erc721Entry } from '.. import { assertNever, getWithDefault } from '../../utils/typescript.js' import { RenameAddressCallBack } from '../../types/user-interface-types.js' import { extractTokenEvents } from '../../background/metadataUtils.js' +import { TokenVisualizerResultWithMetadata } from '../../types/EnrichedEthereumData.js' type BeforeAfterBalance = funtypes.Static const BeforeAfterBalance = funtypes.ReadonlyObject({ diff --git a/app/ts/components/simulationExplaining/Transactions.tsx b/app/ts/components/simulationExplaining/Transactions.tsx index 82f2902d..1f5ad639 100644 --- a/app/ts/components/simulationExplaining/Transactions.tsx +++ b/app/ts/components/simulationExplaining/Transactions.tsx @@ -1,4 +1,4 @@ -import { EnrichedEthereumEventWithMetadata, EnrichedEthereumInputData, SimulatedAndVisualizedTransaction, SimulationAndVisualisationResults, TokenVisualizerResultWithMetadata, TransactionVisualizationParameters } from '../../types/visualizer-types.js' +import { SimulatedAndVisualizedTransaction, SimulationAndVisualisationResults, TransactionVisualizationParameters } from '../../types/visualizer-types.js' import { SmallAddress } from '../subcomponents/address.js' import { TokenSymbol, TokenAmount, AllApproval } from '../subcomponents/coins.js' import { LogAnalysisParams, NonLogAnalysisParams, RenameAddressCallBack } from '../../types/user-interface-types.js' @@ -24,6 +24,7 @@ import { ProxyTokenTransferVisualisation } from './customExplainers/ProxySendVis import { extractTokenEvents } from '../../background/metadataUtils.js' import { EditEnsNamedHashCallBack, EnsNamedHashComponent } from '../subcomponents/ens.js' import { insertBetweenElements } from '../subcomponents/misc.js' +import { EnrichedEthereumEventWithMetadata, EnrichedEthereumInputData, TokenVisualizerResultWithMetadata } from '../../types/EnrichedEthereumData.js' function isPositiveEvent(visResult: TokenVisualizerResultWithMetadata, ourAddressInReferenceFrame: bigint) { if (visResult.type === 'ERC20') { @@ -190,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/CatchAllVisualizer.tsx b/app/ts/components/simulationExplaining/customExplainers/CatchAllVisualizer.tsx index 8ea6b564..0f8c1329 100644 --- a/app/ts/components/simulationExplaining/customExplainers/CatchAllVisualizer.tsx +++ b/app/ts/components/simulationExplaining/customExplainers/CatchAllVisualizer.tsx @@ -1,6 +1,6 @@ import { TransactionImportanceBlockParams } from '../Transactions.js' import { Erc1155OperatorChange, Erc20ApprovalChanges, Erc721OperatorChange, Erc721TokenIdApprovalChanges, Erc721or1155OperatorChanges } from '../SimulationSummary.js' -import { Erc721TokenApprovalChange, ERC20TokenApprovalChange, TokenVisualizerErc20Event, TokenVisualizerErc721Event, TokenVisualizerResultWithMetadata, TokenVisualizerNFTAllApprovalEvent } from '../../../types/visualizer-types.js' +import { Erc721TokenApprovalChange, ERC20TokenApprovalChange } from '../../../types/visualizer-types.js' import { TokenSymbol, TokenAmount } from '../../subcomponents/coins.js' import { RenameAddressCallBack } from '../../../types/user-interface-types.js' import { SmallAddress } from '../../subcomponents/address.js' @@ -9,6 +9,7 @@ import { getDeployedContractAddress } from '../../../simulation/services/Simulat import { addressString } from '../../../utils/bigint.js' import { extractEnsEvents, extractTokenEvents } from '../../../background/metadataUtils.js' import { EnsEventsExplainer } from './EnsEventExplainer.js' +import { TokenVisualizerErc20Event, TokenVisualizerErc721Event, TokenVisualizerNFTAllApprovalEvent, TokenVisualizerResultWithMetadata } from '../../../types/EnrichedEthereumData.js' type SendOrReceiveTokensImportanceBoxParams = { sending: boolean, @@ -85,16 +86,16 @@ export function CatchAllVisualizer(param: TransactionImportanceBlockParams) { const ensEvents = extractEnsEvents(param.simTx.events) const sendingTokenResults = tokenResults.filter((x) => x.from.address === msgSender) const receivingTokenResults = tokenResults.filter((x) => x.to.address === msgSender) - const erc20TokenApprovalChanges: ERC20TokenApprovalChange[] = sendingTokenResults.filter((x): x is TokenVisualizerErc20Event => x.isApproval && x.type === 'ERC20').map((entry) => { + const erc20TokenApprovalChanges: ERC20TokenApprovalChange[] = sendingTokenResults.filter((x): x is TokenVisualizerErc20Event => x.isApproval && x.type === 'ERC20').map((entry) => { return { ...entry.token, approvals: [ {...entry.to, change: entry.amount } ] } }) - const operatorChanges: (Erc721OperatorChange | Erc1155OperatorChange)[] = sendingTokenResults.filter((x): x is TokenVisualizerNFTAllApprovalEvent => x.type === 'NFT All approval').map((entry) => { + const operatorChanges: (Erc721OperatorChange | Erc1155OperatorChange)[] = sendingTokenResults.filter((x): x is TokenVisualizerNFTAllApprovalEvent => x.type === 'NFT All approval').map((entry) => { return { ...entry.token, operator: 'allApprovalAdded' in entry && entry.allApprovalAdded ? entry.to : undefined } }) // token address, tokenId, approved address - const tokenIdApprovalChanges: Erc721TokenApprovalChange[] = sendingTokenResults.filter((x): x is TokenVisualizerErc721Event => 'tokenId' in x && x.isApproval).map((entry) => { + const tokenIdApprovalChanges: Erc721TokenApprovalChange[] = sendingTokenResults.filter((x): x is TokenVisualizerErc721Event => 'tokenId' in x && x.isApproval).map((entry) => { return { tokenEntry: entry.token, tokenId: entry.tokenId, approvedEntry: entry.to } }) diff --git a/app/ts/components/simulationExplaining/customExplainers/EnsEventExplainer.tsx b/app/ts/components/simulationExplaining/customExplainers/EnsEventExplainer.tsx index 2fefc0fd..b3bad0f3 100644 --- a/app/ts/components/simulationExplaining/customExplainers/EnsEventExplainer.tsx +++ b/app/ts/components/simulationExplaining/customExplainers/EnsEventExplainer.tsx @@ -1,6 +1,6 @@ +import { EnsEvent } from '../../../types/EnrichedEthereumData.js' import { RpcNetwork } from '../../../types/rpc.js' import { RenameAddressCallBack } from '../../../types/user-interface-types.js' -import { EnsEvent } from '../../../types/visualizer-types.js' import { dataStringWith0xStart } from '../../../utils/bigint.js' import { assertNever } from '../../../utils/typescript.js' import { SmallAddress } from '../../subcomponents/address.js' 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) (null); - clickOutsideAlerter(wrapperRef, () => setIsOpen(false)); + const wrapperRef = useRef(null) + clickOutsideAlerter(wrapperRef, () => setIsOpen(false)) function changeRpc(entry: RpcEntry) { params.changeRpc(entry) setIsOpen(false) } - return
- -
} +const AddressTitle = ({ content, useLegibleFont }: { content: string, useLegibleFont?: boolean }) => { + return

{ content }

+} + +const AddressSubTitle = ({ content }: { content?: string }) => { + if (!content) return <> + return

{ content }

+} + type ActiveAddressParams = { readonly activeAddress: AddressBookEntry | undefined readonly disableButton: boolean @@ -163,7 +169,7 @@ export function SmallAddress(params: SmallAddressParams) { -

{ params.addressBookEntry.name }

+

{ params.addressBookEntry.name }