diff --git a/app/css/interceptor.css b/app/css/interceptor.css index f79cd900..723bca5e 100644 --- a/app/css/interceptor.css +++ b/app/css/interceptor.css @@ -53,7 +53,10 @@ body { font-family: "Inter" !important; } --alpha-005: rgba(255, 255, 255, 0.05); --alpha-015: rgba(255, 255, 255, 0.15); - --modal-background-color: rgba(10, 10, 10, .7) + --modal-background-color: rgba(10, 10, 10, .7); + + --big-font-size: 28px; + --normal-font-size: 14px; } button:where(:not(.btn)) { diff --git a/app/inpage/ts/document_start.ts b/app/inpage/ts/document_start.ts index dc841af9..a4d00066 100644 --- a/app/inpage/ts/document_start.ts +++ b/app/inpage/ts/document_start.ts @@ -7,7 +7,12 @@ function injectScript(_content: string) { if (error !== null && error !== undefined && error.message !== undefined) throw new Error(error.message) } - function listenInContentScript(conectionName: string | undefined) { + function listenContentScript(connectionName: string | undefined) { + const checkAndThrowRuntimeLastError = () => { + const error: browser.runtime._LastError | undefined | null = browser.runtime.lastError // firefox return `null` on no errors + if (error !== null && error !== undefined && error.message !== undefined) throw new Error(error.message) + } + /** * this script executed within the context of the active tab when the user clicks the extension bar button * this script serves as a _very thin_ proxy between the page scripts (dapp) and the extension, simply forwarding messages between the two @@ -21,13 +26,14 @@ function injectScript(_content: string) { globalThis.crypto.getRandomValues(arr) return `0x${ Array.from(arr, dec2hex).join('') }` } - const connectionNameNotUndefined = conectionName === undefined ? generateId(40) : conectionName - - const extensionPort = browser.runtime.connect({ name: connectionNameNotUndefined }) + const connectionNameNotUndefined = connectionName === undefined ? generateId(40) : connectionName + let pageHidden = false + let extensionPort: browser.runtime.Port | undefined = undefined // forward all message events to the background script, which will then filter and process them // biome-ignore lint/suspicious/noExplicitAny: MessageEvent default signature - const listener = (messageEvent: MessageEvent) => { + globalThis.addEventListener('message', (messageEvent: MessageEvent) => { + if (extensionPort === undefined) return try { // we only want the data element, if it exists, and postMessage will fail if it can't clone the object fully (and it cannot clone a MessageEvent) if (!('data' in messageEvent)) return @@ -39,35 +45,59 @@ function injectScript(_content: string) { // this error happens when the extension is refreshed and the page cannot reach The Interceptor anymore return } + if (error.message?.includes('User denied')) return // user denied signature } extensionPort.postMessage({ data: { interceptorRequest: true, usingInterceptorWithoutSigner: false, requestId: -1, method: 'InterceptorError', params: [error] } }) throw error } + }) + + const connect = () => { + if (extensionPort) extensionPort.disconnect() + extensionPort = browser.runtime.connect({ name: connectionNameNotUndefined }) + + // forward all messages we get from the background script to the window so the page script can filter and process them + extensionPort.onMessage.addListener(messageEvent => { + if (typeof messageEvent !== 'object' || messageEvent === null || !('interceptorApproved' in messageEvent)) { + console.error('Malformed message:') + console.error(messageEvent) + if (extensionPort === undefined) return + extensionPort.postMessage({ data: { interceptorRequest: true, usingInterceptorWithoutSigner: false, requestId: -1, method: 'InterceptorError', params: [messageEvent] } }) + return + } + try { + globalThis.postMessage(messageEvent, '*') + checkAndThrowRuntimeLastError() + } catch (error) { + console.error(error) + } + }) + + extensionPort.onDisconnect.addListener(() => { pageHidden = true }) } - globalThis.addEventListener('message', listener) + connect() - // forward all messages we get from the background script to the window so the page script can filter and process them - extensionPort.onMessage.addListener(response => { + // https://web.dev/articles/bfcache + const bfCachePageShow = () => { try { - globalThis.postMessage(response, '*') checkAndThrowRuntimeLastError() - } catch (error) { - console.error(error) - } - }) - - extensionPort.onDisconnect.addListener(() => { - try { - globalThis.removeEventListener('message', listener) - listenInContentScript(connectionNameNotUndefined) + if (pageHidden) connect() checkAndThrowRuntimeLastError() - } catch (error) { + pageHidden = false + } catch (error: unknown) { console.error(error) } - }) + } + globalThis.addEventListener('pageshow', () => bfCachePageShow(), false) + globalThis.addEventListener('pagehide', () => { pageHidden = true }, false) + + try { + checkAndThrowRuntimeLastError() + } catch (error: unknown) { + console.error(error) + } } - try { const container = document.head || document.documentElement const scriptTag = document.createElement('script') @@ -75,7 +105,7 @@ function injectScript(_content: string) { scriptTag.src = browser.runtime.getURL('inpage/js/inpage.js') container.insertBefore(scriptTag, container.children[1]) container.removeChild(scriptTag) - listenInContentScript(undefined) + listenContentScript(undefined) checkAndThrowRuntimeLastError() } catch (error) { console.error('Interceptor: Provider injection failed.', error) diff --git a/app/inpage/ts/inpage.ts b/app/inpage/ts/inpage.ts index 4cb70ed7..4323b62a 100644 --- a/app/inpage/ts/inpage.ts +++ b/app/inpage/ts/inpage.ts @@ -81,7 +81,7 @@ type InterceptedRequestForwardWithError = InterceptedRequestBase & { } } -type InterceptedRequestForwardToSigner = InterceptedRequestBase & { readonly type: 'forwardToSigner' } +type InterceptedRequestForwardToSigner = InterceptedRequestBase & { readonly type: 'forwardToSigner', readonly replyWithSignersReply?: true } type InterceptedRequestForward = InterceptedRequestForwardWithResult | InterceptedRequestForwardWithError | InterceptedRequestForwardToSigner @@ -496,6 +496,17 @@ class InterceptorMessageListener { return } + private parseRpcError = (maybeErrorObject: unknown) => { + if (typeof maybeErrorObject !== 'object' || maybeErrorObject === null) return new EthereumJsonRpcError(METAMASK_ERROR_BLANKET_ERROR, 'Unexpected thrown value.', { rawError: maybeErrorObject } ) + if ('code' in maybeErrorObject + && maybeErrorObject.code !== undefined && typeof maybeErrorObject.code === 'number' + && 'message' in maybeErrorObject && maybeErrorObject.message !== undefined && typeof maybeErrorObject.message === 'string' + ) { + return new EthereumJsonRpcError(maybeErrorObject.code, maybeErrorObject.message, 'data' in maybeErrorObject && typeof maybeErrorObject.data === 'object' && maybeErrorObject.data !== null ? maybeErrorObject.data : undefined) + } + return new EthereumJsonRpcError(METAMASK_ERROR_BLANKET_ERROR, 'Unexpected thrown value.', maybeErrorObject ) + } + public readonly onMessage = async (messageEvent: unknown) => { this.checkIfCoinbaseInjectionMessageAndInject(messageEvent) if ( @@ -553,24 +564,30 @@ class InterceptorMessageListener { const sendToSignerWithCatchError = async () => { try { const reply = await signerRequest({ method: forwardRequest.method, params: 'params' in forwardRequest ? forwardRequest.params : [] }) - return { success: true, forwardRequest, reply } + return { success: true as const, forwardRequest, reply } } catch(error: unknown) { - return { success: false, forwardRequest, error } + return { success: false as const, forwardRequest, error } } } const signerReply = await sendToSignerWithCatchError() try { + if ('replyWithSignersReply' in forwardRequest) { + if (signerReply.success) { + await this.handleReplyRequest({ + requestId: forwardRequest.requestId, + interceptorApproved: true, + method: forwardRequest.method, + type: 'result', + result: signerReply.reply, + }) + return + } + return pendingRequest.reject(this.parseRpcError(signerReply.error)) + } await this.sendMessageToBackgroundPage({ method: 'signer_reply', params: [ signerReply ] }) } catch(error: unknown) { if (error instanceof Error) return pendingRequest.reject(error) - if (typeof error === 'object' && error !== null - && 'code' in error && error.code !== undefined && typeof error.code === 'number' - && 'message' in error && error.message !== undefined && typeof error.message === 'string' - ) { - return pendingRequest.reject(new EthereumJsonRpcError(error.code, error.message, 'data' in error && typeof error.data === 'object' && error.data !== null ? error.data : undefined)) - } - // if the signer we are connected threw something besides an Error, wrap it up in an error - pendingRequest.reject(new EthereumJsonRpcError(METAMASK_ERROR_BLANKET_ERROR, 'Unexpected thrown value.', { error })) + return pendingRequest.reject(this.parseRpcError(error)) } } catch(error: unknown) { console.error(messageEvent) @@ -581,14 +598,7 @@ class InterceptorMessageListener { const pendingRequest = this.outstandingRequests.get(requestId) if (pendingRequest === undefined) throw new Error('Request did not exist anymore') if (error instanceof Error) return pendingRequest.reject(error) - if (typeof error === 'object' && error !== null - && 'code' in error && error.code !== undefined && typeof error.code === 'number' - && 'message' in error && error.message !== undefined && typeof error.message === 'string' - ) { - return pendingRequest.reject(new EthereumJsonRpcError(error.code, error.message, 'data' in error && typeof error.data === 'object' && error.data !== null ? error.data : undefined)) - } - // if the signer we are connected threw something besides an Error, wrap it up in an error - pendingRequest.reject(new EthereumJsonRpcError(METAMASK_ERROR_BLANKET_ERROR, 'Unexpected thrown value.', { error })) + return pendingRequest.reject(this.parseRpcError(error)) } } diff --git a/app/inpage/ts/listenContentScript.ts b/app/inpage/ts/listenContentScript.ts index e49d7f02..28bf306b 100644 --- a/app/inpage/ts/listenContentScript.ts +++ b/app/inpage/ts/listenContentScript.ts @@ -1,4 +1,4 @@ -function listenContentScript(conectionName: string | undefined) { +function listenContentScript(connectionName: string | undefined) { const checkAndThrowRuntimeLastError = () => { const error: browser.runtime._LastError | undefined | null = browser.runtime.lastError // firefox return `null` on no errors if (error !== null && error !== undefined && error.message !== undefined) throw new Error(error.message) @@ -17,12 +17,14 @@ function listenContentScript(conectionName: string | undefined) { globalThis.crypto.getRandomValues(arr) return `0x${ Array.from(arr, dec2hex).join('') }` } - const connectionNameNotUndefined = conectionName === undefined ? generateId(40) : conectionName - const extensionPort = browser.runtime.connect({ name: connectionNameNotUndefined }) + const connectionNameNotUndefined = connectionName === undefined ? generateId(40) : connectionName + let pageHidden = false + let extensionPort: browser.runtime.Port | undefined = undefined // forward all message events to the background script, which will then filter and process them // biome-ignore lint/suspicious/noExplicitAny: MessageEvent default signature - const listener = (messageEvent: MessageEvent) => { + globalThis.addEventListener('message', (messageEvent: MessageEvent) => { + if (extensionPort === undefined) return try { // we only want the data element, if it exists, and postMessage will fail if it can't clone the object fully (and it cannot clone a MessageEvent) if (!('data' in messageEvent)) return @@ -39,34 +41,47 @@ function listenContentScript(conectionName: string | undefined) { extensionPort.postMessage({ data: { interceptorRequest: true, usingInterceptorWithoutSigner: false, requestId: -1, method: 'InterceptorError', params: [error] } }) throw error } + }) + + const connect = () => { + if (extensionPort) extensionPort.disconnect() + extensionPort = browser.runtime.connect({ name: connectionNameNotUndefined }) + + // forward all messages we get from the background script to the window so the page script can filter and process them + extensionPort.onMessage.addListener(messageEvent => { + if (typeof messageEvent !== 'object' || messageEvent === null || !('interceptorApproved' in messageEvent)) { + console.error('Malformed message:') + console.error(messageEvent) + if (extensionPort === undefined) return + extensionPort.postMessage({ data: { interceptorRequest: true, usingInterceptorWithoutSigner: false, requestId: -1, method: 'InterceptorError', params: [messageEvent] } }) + return + } + try { + globalThis.postMessage(messageEvent, '*') + checkAndThrowRuntimeLastError() + } catch (error) { + console.error(error) + } + }) + + extensionPort.onDisconnect.addListener(() => { pageHidden = true }) } - globalThis.addEventListener('message', listener) + connect() - // forward all messages we get from the background script to the window so the page script can filter and process them - extensionPort.onMessage.addListener(messageEvent => { - if (typeof messageEvent !== 'object' || messageEvent === null || !('interceptorApproved' in messageEvent)) { - console.error('Malformed message:') - console.error(messageEvent) - extensionPort.postMessage({ data: { interceptorRequest: true, usingInterceptorWithoutSigner: false, requestId: -1, method: 'InterceptorError', params: [messageEvent] } }) - return - } + // https://web.dev/articles/bfcache + const bfCachePageShow = () => { try { - globalThis.postMessage(messageEvent, '*') checkAndThrowRuntimeLastError() - } catch (error) { - console.error(error) - } - }) - - extensionPort.onDisconnect.addListener(() => { - try { - globalThis.removeEventListener('message', listener) - listenContentScript(connectionNameNotUndefined) + if (pageHidden) connect() checkAndThrowRuntimeLastError() + pageHidden = false } catch (error: unknown) { console.error(error) } - }) + } + globalThis.addEventListener('pageshow', () => bfCachePageShow(), false) + globalThis.addEventListener('pagehide', () => { pageHidden = true }, false) + try { checkAndThrowRuntimeLastError() } catch (error: unknown) { diff --git a/app/ts/background/background-startup.ts b/app/ts/background/background-startup.ts index 2954bc68..f326ba45 100644 --- a/app/ts/background/background-startup.ts +++ b/app/ts/background/background-startup.ts @@ -31,8 +31,13 @@ const catchAllErrorsAndCall = async (func: () => Promise) => { await func() checkAndThrowRuntimeLastError() } catch(error: unknown) { - console.error(error) if (error instanceof Error && error.message.startsWith('No tab with id')) return + if (error instanceof Error && error.message?.includes('the message channel is closed')) { + // ignore bfcache error. It means that the page is hibernating and we cannot communicate with it anymore. We get a normal disconnect about it. + // https://developer.chrome.com/blog/bfcache-extension-messaging-changes + return + } + console.error(error) handleUnexpectedError(error) } } @@ -69,13 +74,8 @@ async function onContentScriptConnected(simulator: Simulator, port: browser.runt const websitePromise = (async () => ({ websiteOrigin, ...await retrieveWebsiteDetails(socket.tabId) }))() const tabConnection = websiteTabConnections.get(socket.tabId) - const newConnection = { - port: port, - socket: socket, - websiteOrigin: websiteOrigin, - approved: false, - wantsToConnect: false, - } + const newConnection = { port, socket, websiteOrigin, approved: false, wantsToConnect: false } + port.onDisconnect.addListener(() => { catchAllErrorsAndCall(async () => { const tabConnection = websiteTabConnections.get(socket.tabId) @@ -85,6 +85,18 @@ async function onContentScriptConnected(simulator: Simulator, port: browser.runt websiteTabConnections.delete(socket.tabId) } }) + try { + checkAndThrowRuntimeLastError() + } catch (error) { + if (error instanceof Error) { + if (error.message?.includes('the message channel is closed')) { + // ignore bfcache error. It means that the page is hibernating and we cannot communicate with it anymore. We get a normal disconnect about it. + // https://developer.chrome.com/blog/bfcache-extension-messaging-changes + return + } + } + throw error + } }) port.onMessage.addListener((payload) => { @@ -142,6 +154,7 @@ async function newBlockAttemptCallback(blockheader: EthereumBlockHeader, ethereu lastConnnectionAttempt: new Date(), latestBlock: blockheader, rpcNetwork: ethereumClientService.getRpcEntry(), + retrying: ethereumClientService.isBlockPolling(), } await setRpcConnectionStatus(rpcConnectionStatus) await updateExtensionBadge() @@ -168,6 +181,7 @@ async function onErrorBlockCallback(ethereumClientService: EthereumClientService lastConnnectionAttempt: new Date(), latestBlock: ethereumClientService.getCachedBlock(), rpcNetwork: ethereumClientService.getRpcEntry(), + retrying: ethereumClientService.isBlockPolling(), } await setRpcConnectionStatus(rpcConnectionStatus) await updateExtensionBadge() @@ -196,7 +210,6 @@ async function startup() { browser.runtime.onConnect.addListener(async (port) => await catchAllErrorsAndCall(() => onContentScriptConnected(simulator, port, websiteTabConnections))) browser.runtime.onMessage.addListener(async (message: unknown) => await catchAllErrorsAndCall(async () => popupMessageHandler(websiteTabConnections, simulator, message, await getSettings()))) - const recursiveCheckIfInterceptorShouldSleep = async () => { await catchAllErrorsAndCall(async () => checkIfInterceptorShouldSleep(simulator.ethereum)) setTimeout(recursiveCheckIfInterceptorShouldSleep, 1000) diff --git a/app/ts/background/background.ts b/app/ts/background/background.ts index 7c16b145..631810d1 100644 --- a/app/ts/background/background.ts +++ b/app/ts/background/background.ts @@ -1,7 +1,7 @@ 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, getTransactionStack, setLatestUnexpectedError, updateSimulationResults, updateSimulationResultsWithCallBack, updateTransactionStack } from './storageVariables.js' +import { getSimulationResults, getTabState, getTransactionStack, promoteRpcAsPrimary, setLatestUnexpectedError, updateSimulationResults, updateSimulationResultsWithCallBack, updateTransactionStack } from './storageVariables.js' import { changeSimulationMode, getSettings, 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, simulateGnosisSafeTransactionOnPass } from './popupMessageHandlers.js' @@ -425,7 +425,7 @@ async function handleRPCRequest( console.warn(maybeParsedRequest.fullError) const maybePartiallyParsedRequest = SupportedEthereumJsonRpcRequestMethods.safeParse(request) // the method is some method that we are not supporting, forward it to the wallet if signer is available - if (maybePartiallyParsedRequest.success === false && forwardToSigner) return { type: 'forwardToSigner' as const, unknownMethod: true, ...request } + if (maybePartiallyParsedRequest.success === false && forwardToSigner) return { type: 'forwardToSigner' as const, replyWithSignersReply: true, ...request } return { type: 'result' as const, method: request.method, @@ -436,6 +436,10 @@ async function handleRPCRequest( } } } + if (settings.currentRpcNetwork.httpsRpc === undefined && forwardToSigner) { + // we are using network that is not supported by us + return { type: 'forwardToSigner' as const, replyWithSignersReply: true, ...request } + } const parsedRequest = maybeParsedRequest.value makeSureInterceptorIsNotSleeping(simulator.ethereum) switch (parsedRequest.method) { @@ -566,6 +570,7 @@ export async function changeActiveRpc(simulator: Simulator, websiteTabConnection }) sendMessageToApprovedWebsitePorts(websiteTabConnections, { method: 'request_signer_to_wallet_switchEthereumChain', result: rpcNetwork.chainId }) await sendPopupMessageToOpenWindows({ method: 'popup_settingsUpdated', data: await getSettings() }) + await promoteRpcAsPrimary(rpcNetwork) } function getProviderHandler(method: string) { diff --git a/app/ts/background/iconHandler.ts b/app/ts/background/iconHandler.ts index be7df148..b4ca2da5 100644 --- a/app/ts/background/iconHandler.ts +++ b/app/ts/background/iconHandler.ts @@ -36,7 +36,7 @@ export async function updateExtensionIcon(websiteTabConnections: WebsiteTabConne return setIcon(ICON_ACCESS_DENIED, `The access to ${ activeAddress.name } for ${ websiteOrigin } has been DENIED!`) } if (settings.simulationMode) return setIcon(ICON_SIMULATING, 'The Interceptor simulates your sent transactions.') - if (settings.currentRpcNetwork.httpsRpc === undefined) return setIcon(ICON_SIGNING_NOT_SUPPORTED, 'Interceptor is on an unsupported network and simulation mode is disabled.') + if (settings.currentRpcNetwork.httpsRpc === undefined) return setIcon(ICON_SIGNING_NOT_SUPPORTED, `The Interceptor is disabled while it's on an unsupported network`) const tabState = await getTabState(tabId) return setIcon(ICON_SIGNING, `The Interceptor forwards your transactions to ${ getPrettySignerName(tabState.signerName) } once sent.`) } @@ -47,7 +47,7 @@ export function noNewBlockForOverTwoMins(connectionStatus: RpcConnectionStatus) export async function updateExtensionBadge() { const connectionStatus = await getRpcConnectionStatus() - if ((connectionStatus?.isConnected === false || noNewBlockForOverTwoMins(connectionStatus)) && connectionStatus) { + if (connectionStatus?.isConnected === false || noNewBlockForOverTwoMins(connectionStatus) && connectionStatus && connectionStatus.retrying) { const nextConnectionAttempt = new Date(connectionStatus.lastConnnectionAttempt.getTime() + TIME_BETWEEN_BLOCKS * 1000) if (nextConnectionAttempt.getTime() - new Date().getTime() > 0) { await setExtensionBadgeBackgroundColor({ color: WARNING_COLOR }) diff --git a/app/ts/background/messageSending.ts b/app/ts/background/messageSending.ts index f9520a6c..3ef5e2fe 100644 --- a/app/ts/background/messageSending.ts +++ b/app/ts/background/messageSending.ts @@ -1,11 +1,12 @@ import { InterceptedRequestForward, InterceptorMessageToInpage, SubscriptionReplyOrCallBack } from "../types/interceptor-messages.js" -import { WebsiteSocket } from "../utils/requests.js" +import { WebsiteSocket, checkAndPrintRuntimeLastError } from "../utils/requests.js" import { WebsiteTabConnections } from "../types/user-interface-types.js" import { websiteSocketToString } from "./backgroundUtils.js" import { serialize } from "../types/wire-types.js" function postMessageToPortIfConnected(port: browser.runtime.Port, message: InterceptorMessageToInpage) { try { + checkAndPrintRuntimeLastError() port.postMessage(serialize(InterceptorMessageToInpage, message) as Object) } catch (error) { if (error instanceof Error) { @@ -18,6 +19,7 @@ function postMessageToPortIfConnected(port: browser.runtime.Port, message: Inter } throw error } + checkAndPrintRuntimeLastError() } export function replyToInterceptedRequest(websiteTabConnections: WebsiteTabConnections, message: InterceptedRequestForward) { diff --git a/app/ts/background/metadataUtils.ts b/app/ts/background/metadataUtils.ts index 680423ce..8b2c678f 100644 --- a/app/ts/background/metadataUtils.ts +++ b/app/ts/background/metadataUtils.ts @@ -209,10 +209,13 @@ export const extractEnsEvents = (events: readonly EnrichedEthereumEventWithMetad } export async function retrieveEnsNodeHashes(ethereumClientService: EthereumClientService, events: EnrichedEthereumEvents, addressBookEntriesToMatchReverseResolutions: readonly AddressBookEntry[]) { - const hashes = events.map((event) => 'logInformation' in event && 'node' in event.logInformation ? event.logInformation.node : undefined).filter((maybeNodeHash): maybeNodeHash is bigint => maybeNodeHash !== undefined) - const deduplicatedHashes = Array.from(new Set(hashes)) + const hashes = new Set() + for (const event of events) { + if (!('logInformation' in event && 'node' in event.logInformation)) continue + hashes.add(event.logInformation.node) + } const reverseEnsLabelHashes = addressBookEntriesToMatchReverseResolutions.map((addressBookEntry) => getEnsReverseNodeHash(addressBookEntry.address)) - return await Promise.all(deduplicatedHashes.map((hash) => getAndCacheEnsNodeHash(ethereumClientService, hash, reverseEnsLabelHashes))) + return Promise.all([...hashes].map((hash) => getAndCacheEnsNodeHash(ethereumClientService, hash, reverseEnsLabelHashes))) } export async function retrieveEnsLabelHashes(events: EnrichedEthereumEvents, addressBookEntriesToMatchReverseResolutions: readonly AddressBookEntry[]) { diff --git a/app/ts/background/popupMessageHandlers.ts b/app/ts/background/popupMessageHandlers.ts index 577b5b35..b4c74e56 100644 --- a/app/ts/background/popupMessageHandlers.ts +++ b/app/ts/background/popupMessageHandlers.ts @@ -325,13 +325,11 @@ export const openNewTab = async (tabName: 'settingsView' | 'addressBook') => { export async function requestNewHomeData(simulator: Simulator, requestAbortController: AbortController | undefined) { const settings = await getSettings() - simulator.ethereum.setBlockPolling(true) // wakes up the RPC block querying if it was sleeping if (settings.simulationMode) await updateSimulationMetadata(simulator.ethereum, requestAbortController) await refreshHomeData(simulator) } export async function refreshHomeData(simulator: Simulator) { - makeSureInterceptorIsNotSleeping(simulator.ethereum) const settingsPromise = getSettings() const makeMeRichPromise = getMakeMeRich() const rpcConnectionStatusPromise = getRpcConnectionStatus() @@ -343,6 +341,7 @@ export async function refreshHomeData(simulator: Simulator) { const tabId = await getLastKnownCurrentTabId() const tabState = tabId === undefined ? await getTabState(-1) : await getTabState(tabId) const settings = await settingsPromise + if (settings.currentRpcNetwork.httpsRpc !== undefined) makeSureInterceptorIsNotSleeping(simulator.ethereum) const websiteOrigin = tabState.website?.websiteOrigin const interceptorDisabled = websiteOrigin === undefined ? false : settings.websiteAccess.find((entry) => entry.website.websiteOrigin === websiteOrigin && entry.interceptorDisabled === true) !== undefined const updatedPage: UpdateHomePage = { diff --git a/app/ts/background/providerMessageHandlers.ts b/app/ts/background/providerMessageHandlers.ts index c8730375..08c02e13 100644 --- a/app/ts/background/providerMessageHandlers.ts +++ b/app/ts/background/providerMessageHandlers.ts @@ -49,9 +49,8 @@ export async function ethAccountsReply(simulator: Simulator, websiteTabConnectio async function changeSignerChain(simulator: Simulator, websiteTabConnections: WebsiteTabConnections, port: browser.runtime.Port, signerChain: bigint, approval: ApprovalState, _activeAddress: bigint | undefined) { if (approval !== 'hasAccess') return if (port.sender?.tab?.id === undefined) return - if ((await getTabState(port.sender.tab.id)).signerChain === signerChain) return - await updateTabState(port.sender.tab.id, (previousState: TabState) => modifyObject(previousState, { signerChain })) - + const oldSignerChain = (await getTabState(port.sender.tab.id)).signerChain + if (oldSignerChain !== signerChain) await updateTabState(port.sender.tab.id, (previousState: TabState) => modifyObject(previousState, { signerChain })) // update active address if we are using signers address const settings = await getSettings() if ((settings.useSignersAddressAsActiveAddress || !settings.simulationMode) && settings.currentRpcNetwork.chainId !== signerChain) { @@ -60,7 +59,7 @@ async function changeSignerChain(simulator: Simulator, websiteTabConnections: We rpcNetwork: await getRpcNetworkForChain(signerChain), }) } - sendPopupMessageToOpenWindows({ method: 'popup_chain_update' }) + if (oldSignerChain !== signerChain) sendPopupMessageToOpenWindows({ method: 'popup_chain_update' }) } export async function signerChainChanged(simulator: Simulator, websiteTabConnections: WebsiteTabConnections, port: browser.runtime.Port, request: ProviderMessage, approval: ApprovalState, activeAddress: bigint | undefined) { diff --git a/app/ts/background/settings.ts b/app/ts/background/settings.ts index 459efb7e..8cbac975 100644 --- a/app/ts/background/settings.ts +++ b/app/ts/background/settings.ts @@ -5,7 +5,7 @@ import { Semaphore } from '../utils/semaphore.js' import { EthereumAddress } from '../types/wire-types.js' import { WebsiteAccessArray } from '../types/websiteAccessTypes.js' import { RpcNetwork } from '../types/rpc.js' -import { browserStorageLocalGet, browserStorageLocalSet } from '../utils/storageUtils.js' +import { browserStorageLocalGet, browserStorageLocalSafeParseGet, browserStorageLocalSet } from '../utils/storageUtils.js' import { getUserAddressBookEntries, updateUserAddressBookEntries } from './storageVariables.js' import { getUniqueItemsByProperties } from '../utils/typed-arrays.js' import { AddressBookEntries, AddressBookEntry } from '../types/addressBookTypes.js' @@ -92,21 +92,22 @@ const wethForChainId = new Map([ export const getWethForChainId = (chainId: bigint) => wethForChainId.get(chainId.toString()) export async function getSettings() : Promise { - const results = await browserStorageLocalGet([ + const resultsPromise = browserStorageLocalGet([ 'activeSimulationAddress', 'openedPageV2', 'useSignersAddressAsActiveAddress', 'websiteAccess', - 'currentRpcNetwork', 'simulationMode', ]) + const currentRpcNetwork = await browserStorageLocalSafeParseGet('currentRpcNetwork') + const results = await resultsPromise if (defaultRpcs[0] === undefined || defaultActiveAddresses[0] === undefined) throw new Error('default rpc or default address was missing') return { activeSimulationAddress: 'activeSimulationAddress' in results ? results.activeSimulationAddress : defaultActiveAddresses[0].address, openedPage: results.openedPageV2 ?? { page: 'Home' }, useSignersAddressAsActiveAddress: results.useSignersAddressAsActiveAddress ?? false, websiteAccess: results.websiteAccess ?? [], - currentRpcNetwork: results.currentRpcNetwork !== undefined ? results.currentRpcNetwork : defaultRpcs[0], + currentRpcNetwork: currentRpcNetwork?.currentRpcNetwork !== undefined ? currentRpcNetwork.currentRpcNetwork : defaultRpcs[0], simulationMode: results.simulationMode ?? true, } } diff --git a/app/ts/background/sleeping.ts b/app/ts/background/sleeping.ts index 0de26bbd..3b12b9f5 100644 --- a/app/ts/background/sleeping.ts +++ b/app/ts/background/sleeping.ts @@ -1,13 +1,26 @@ import { EthereumClientService } from "../simulation/services/EthereumClientService.js" import { TIME_BETWEEN_BLOCKS } from "../utils/constants.js" -import { getInterceptorStartSleepingTimestamp, setInterceptorStartSleepingTimestamp } from "./storageVariables.js" +import { modifyObject } from "../utils/typescript.js" +import { sendPopupMessageToOpenWindows } from "./backgroundUtils.js" +import { updateExtensionBadge } from "./iconHandler.js" +import { getInterceptorStartSleepingTimestamp, getRpcConnectionStatus, setInterceptorStartSleepingTimestamp, setRpcConnectionStatus } from "./storageVariables.js" import { isConfirmTransactionFocused } from "./windows/confirmTransaction.js" -export const makeSureInterceptorIsNotSleeping = (ethereumClientService: EthereumClientService) => { +const updateConnectionStatusRetry = async (ethereumClientService: EthereumClientService) => { + const status = await getRpcConnectionStatus() + if (status === undefined) return + const rpcConnectionStatus = modifyObject(status, { retrying: ethereumClientService.isBlockPolling() }) + await setRpcConnectionStatus(rpcConnectionStatus) + await updateExtensionBadge() + await sendPopupMessageToOpenWindows({ method: 'popup_failed_to_get_block', data: { rpcConnectionStatus } }) +} + +export const makeSureInterceptorIsNotSleeping = async (ethereumClientService: EthereumClientService) => { setInterceptorStartSleepingTimestamp(Date.now() + TIME_BETWEEN_BLOCKS * 2 * 1000) if (!ethereumClientService.isBlockPolling()) { console.info('The Interceptor woke up! ⏰') ethereumClientService.setBlockPolling(true) + await updateConnectionStatusRetry(ethereumClientService) } } @@ -21,5 +34,6 @@ export const checkIfInterceptorShouldSleep = async (ethereumClientService: Ether if (startSleping < Date.now() && ethereumClientService.isBlockPolling()) { console.info('The Interceptor started to sleep 😴') ethereumClientService.setBlockPolling(false) + await updateConnectionStatusRetry(ethereumClientService) } } diff --git a/app/ts/background/storageVariables.ts b/app/ts/background/storageVariables.ts index 185f6631..5bda8c3e 100644 --- a/app/ts/background/storageVariables.ts +++ b/app/ts/background/storageVariables.ts @@ -14,6 +14,7 @@ import { UnexpectedErrorOccured } from '../types/interceptor-messages.js' import { namehash } from 'ethers' import { bytesToUnsigned } from '../utils/bigint.js' import { keccak_256 } from '@noble/hashes/sha3' +import { modifyObject } from '../utils/typescript.js' export const getIdsOfOpenedTabs = async () => (await browserStorageLocalGet('idsOfOpenedTabs'))?.idsOfOpenedTabs ?? { settingsView: undefined, addressBook: undefined} export const setIdsOfOpenedTabs = async (ids: PartialIdsOfOpenedTabs) => await browserStorageLocalSet({ idsOfOpenedTabs: { ...await getIdsOfOpenedTabs(), ...ids } }) @@ -213,14 +214,25 @@ export const setInterceptorStartSleepingTimestamp = async(interceptorStartSleepi export const getInterceptorStartSleepingTimestamp = async () => (await browserStorageLocalGet('interceptorStartSleepingTimestamp'))?.interceptorStartSleepingTimestamp ?? 0 +export const promoteRpcAsPrimary = async (rpcNetwork: RpcNetwork) => { + if (rpcNetwork.primary) return + const rpcs = await getRpcList() + await setRpcList(rpcs.map((rpc) => rpc.chainId === rpcNetwork.chainId ? modifyObject(rpc, { primary: rpc.httpsRpc === rpcNetwork.httpsRpc }) : rpc)) +} + export const getPrimaryRpcForChain = async (chainId: bigint) => { const rpcs = await getRpcList() - return rpcs.find((rpc) => rpc.chainId === chainId && rpc.primary) + const primary = rpcs.find((rpc) => rpc.chainId === chainId && rpc.primary) + if (primary) return primary + + // no primary was found, try to find what ever we have for that chain id + const nonPrimary = rpcs.find((rpc) => rpc.chainId === chainId) + if (nonPrimary) return nonPrimary + return undefined } export const getRpcNetworkForChain = async (chainId: bigint): Promise => { - const rpcs = await getRpcList() - const rpc = rpcs.find((rpc) => rpc.chainId === chainId && rpc.primary) + const rpc = await getPrimaryRpcForChain(chainId) if (rpc !== undefined) return rpc return { chainId: chainId, @@ -228,6 +240,8 @@ export const getRpcNetworkForChain = async (chainId: bigint): Promise (await browserStorageLocalGet('userAddressBookEntriesV2'))?.userAddressBookEntriesV2 ?? [] diff --git a/app/ts/components/App.tsx b/app/ts/components/App.tsx index e373aab0..547e31a3 100644 --- a/app/ts/components/App.tsx +++ b/app/ts/components/App.tsx @@ -23,7 +23,6 @@ import { VisualizedPersonalSignRequest } from '../types/personal-message-definit import { RpcEntries, RpcEntry, RpcNetwork } from '../types/rpc.js' import { ErrorComponent, UnexpectedError } from './subcomponents/Error.js' import { SignersLogoName } from './subcomponents/signers.js' -import { useSignal } from '@preact/signals' import { SomeTimeAgo } from './subcomponents/SomeTimeAgo.js' import { noNewBlockForOverTwoMins } from '../background/iconHandler.js' import { humanReadableDate } from './ui-utils.js' @@ -47,14 +46,14 @@ type NetworkErrorParams = { export function NetworkErrors({ rpcConnectionStatus } : NetworkErrorParams) { if (rpcConnectionStatus === undefined) return <> const nextConnectionAttempt = new Date(rpcConnectionStatus.lastConnnectionAttempt.getTime() + TIME_BETWEEN_BLOCKS * 1000) - const retrying = useSignal((nextConnectionAttempt.getTime() - new Date().getTime()) > 0) + if (rpcConnectionStatus.retrying === false) return <> return <> - { rpcConnectionStatus.isConnected === false && retrying.value ? + { rpcConnectionStatus.isConnected === false ? Unable to connect to { rpcConnectionStatus.rpcNetwork.name }. Retrying in . }/> : <> } - { rpcConnectionStatus.latestBlock !== undefined && noNewBlockForOverTwoMins(rpcConnectionStatus) && retrying.value ? + { rpcConnectionStatus.latestBlock !== undefined && noNewBlockForOverTwoMins(rpcConnectionStatus) ? The connected RPC ({ rpcConnectionStatus.rpcNetwork.name }) seem to be stuck at block { rpcConnectionStatus.latestBlock.number } (occured on: { humanReadableDate(rpcConnectionStatus.latestBlock.timestamp) }). Retrying in . }/> diff --git a/app/ts/components/pages/Home.tsx b/app/ts/components/pages/Home.tsx index a2d8d4df..494925de 100644 --- a/app/ts/components/pages/Home.tsx +++ b/app/ts/components/pages/Home.tsx @@ -283,7 +283,7 @@ export function Home(param: HomeParams) { return <> { rpcNetwork.httpsRpc === undefined ? - + : <> }
-

+

Vote { interpretSupport(inputParams.support) } {`for proposal: ${ inputParams.proposalId } `}

diff --git a/app/ts/components/subcomponents/coins.tsx b/app/ts/components/subcomponents/coins.tsx index c191dcff..ca0ac1d9 100644 --- a/app/ts/components/subcomponents/coins.tsx +++ b/app/ts/components/subcomponents/coins.tsx @@ -7,7 +7,7 @@ import { JSX } from 'preact/jsx-runtime' import { useEffect } from 'preact/hooks' import { Erc1155Entry, Erc20TokenEntry, Erc721Entry } from '../../types/addressBookTypes.js' import { RenameAddressCallBack } from '../../types/user-interface-types.js' -import { BIG_FONT_SIZE, ETHEREUM_COIN_ICON, ETHEREUM_LOGS_LOGGER_ADDRESS, NORMAL_FONT_SIZE } from '../../utils/constants.js' +import { ETHEREUM_COIN_ICON, ETHEREUM_LOGS_LOGGER_ADDRESS } from '../../utils/constants.js' import { RpcNetwork } from '../../types/rpc.js' import { Blockie } from './SVGBlockie.js' import { AbbreviatedValue } from './AbbreviatedValue.js' @@ -46,7 +46,7 @@ export function EtherAmount(param: EtherAmountParams) { 'text-overflow': 'ellipsis', color: 'var(--text-color)', ...(param.style === undefined ? {} : param.style), - 'font-size': param.fontSize === 'big' ? BIG_FONT_SIZE : NORMAL_FONT_SIZE + 'font-size': param.fontSize === 'big' ? 'var(--big-font-size)' : 'var(--normal-font-size)' } return <> @@ -71,7 +71,7 @@ export function EtherSymbol(param: EtherSymbolParams) { overflow: 'hidden', 'text-overflow': 'ellipsis', ...(param.style === undefined ? {} : param.style), - 'font-size': param.fontSize === 'big' ? BIG_FONT_SIZE : NORMAL_FONT_SIZE + 'font-size': param.fontSize === 'big' ? 'var(--big-font-size)' : 'var(--normal-font-size)' } const etheName = param.useFullTokenName ? param.rpcNetwork.currencyName : param.rpcNetwork.currencyTicker @@ -145,7 +145,7 @@ export function TokenSymbol(param: TokenSymbolParams) { color: 'var(--text-color)', ...(param.style === undefined ? {} : param.style), ...unTrusted ? { color: 'var(--warning-color)' } : {}, - 'font-size': param.fontSize === 'big' ? BIG_FONT_SIZE : NORMAL_FONT_SIZE + 'font-size': param.fontSize === 'big' ? 'var(--big-font-size)' : 'var(--normal-font-size)' } const name = param.useFullTokenName ? param.tokenEntry.name : param.tokenEntry.symbol @@ -191,7 +191,7 @@ export function TokenAmount(param: TokenAmountParams) { display: 'inline-flex', 'align-items': 'center', ...(param.style === undefined ? {} : param.style), - 'font-size': param.fontSize === 'big' ? BIG_FONT_SIZE : NORMAL_FONT_SIZE + 'font-size': param.fontSize === 'big' ? 'var(--big-font-size)' : 'var(--normal-font-size)' } if (!('decimals' in param.tokenEntry) || param.tokenEntry.decimals === undefined) { @@ -262,7 +262,7 @@ export function AllApproval(param: AllApprovalParams ) { const style = { color: 'var(--text-color)', ...(param.style === undefined ? {} : param.style), - 'font-size': param.fontSize === 'big' ? BIG_FONT_SIZE : NORMAL_FONT_SIZE + 'font-size': param.fontSize === 'big' ? 'var(--big-font-size)' : 'var(--normal-font-size)' } if (!param.allApprovalAdded) return

NONE

return

ALL

diff --git a/app/ts/simulation/protectors/sendToNonContactAddress.ts b/app/ts/simulation/protectors/sendToNonContactAddress.ts index ddb68db4..1f8439d2 100644 --- a/app/ts/simulation/protectors/sendToNonContactAddress.ts +++ b/app/ts/simulation/protectors/sendToNonContactAddress.ts @@ -8,7 +8,7 @@ export async function sendToNonContact(transaction: EthereumUnsignedTransaction, async function checkSendToAddress(to: EthereumAddress) { const sendingTo = await identifyAddress(ethereum, requestAbortController, to) if (sendingTo.entrySource !== 'OnChain') return - return `You are about to send funds to "${ sendingTo.name }", which is not in your addressbook. Please add the address to addressbook to dismiss this error in the future.` + return `This transaction sends funds to "${ sendingTo.name }", which is not in the addressbook. Please add the address to addressbook to dismiss this error in the future.` } const transferInfo = parseTransaction(transaction) diff --git a/app/ts/types/ethSimulate-types.ts b/app/ts/types/ethSimulate-types.ts index 65845d26..dac69c10 100644 --- a/app/ts/types/ethSimulate-types.ts +++ b/app/ts/types/ethSimulate-types.ts @@ -56,8 +56,8 @@ export const BlockCalls = funtypes.Intersect( }) ) -export type ethSimulateV1ParamObject = funtypes.Static -const ethSimulateV1ParamObject = funtypes.ReadonlyObject({ +export type EthSimulateV1ParamObject = funtypes.Static +const EthSimulateV1ParamObject = funtypes.ReadonlyObject({ blockStateCalls: funtypes.ReadonlyArray(BlockCalls), traceTransfers: funtypes.Boolean, validation: funtypes.Boolean, @@ -66,7 +66,7 @@ const ethSimulateV1ParamObject = funtypes.ReadonlyObject({ export type EthSimulateV1Params = funtypes.Static export const EthSimulateV1Params = funtypes.ReadonlyObject({ method: funtypes.Literal('eth_simulateV1'), - params: funtypes.ReadonlyTuple(ethSimulateV1ParamObject, EthereumBlockTag), + params: funtypes.ReadonlyTuple(EthSimulateV1ParamObject, EthereumBlockTag), }) export type EthereumEvent = funtypes.Static diff --git a/app/ts/types/interceptor-messages.ts b/app/ts/types/interceptor-messages.ts index a6c99684..73443b2b 100644 --- a/app/ts/types/interceptor-messages.ts +++ b/app/ts/types/interceptor-messages.ts @@ -117,11 +117,11 @@ const ForwardToWallet = funtypes.Intersect( // forward directly to wallet funtypes.Union(SendRawTransactionParams, SendTransactionParams, PersonalSignParams, SignTypedDataParams, OldSignTypedDataParams, WalletAddEthereumChain, EthGetStorageAtParams), ) -type UnknownMethodForward = funtypes.Static -const UnknownMethodForward = funtypes.Intersect( +type ReplyWithSignersReplyForward = funtypes.Static +const ReplyWithSignersReplyForward = funtypes.Intersect( funtypes.ReadonlyObject({ type: funtypes.Literal('forwardToSigner'), - unknownMethod: funtypes.Literal(true), + replyWithSignersReply: funtypes.Literal(true), method: funtypes.String, }), funtypes.Partial({ @@ -133,7 +133,7 @@ export type RPCReply = funtypes.Static export const RPCReply = funtypes.Union( NonForwardingRPCRequestReturnValue, ForwardToWallet, - UnknownMethodForward, + ReplyWithSignersReplyForward, funtypes.ReadonlyObject({ type: funtypes.Literal('doNotReply') }), ) @@ -337,7 +337,7 @@ export const ConnectedToSigner = funtypes.ReadonlyObject({ type SignerReplyForwardRequest = funtypes.Static const SignerReplyForwardRequest = funtypes.Intersect( funtypes.ReadonlyObject({ requestId: funtypes.Number }), - funtypes.Union(ForwardToWallet, UnknownMethodForward) + funtypes.Union(ForwardToWallet, ReplyWithSignersReplyForward) ) export type SignerReply = funtypes.Static diff --git a/app/ts/types/rpc.ts b/app/ts/types/rpc.ts index 0f8530ca..17427814 100644 --- a/app/ts/types/rpc.ts +++ b/app/ts/types/rpc.ts @@ -29,6 +29,8 @@ export const RpcNetwork = funtypes.Union( name: funtypes.String, currencyName: funtypes.Literal('Ether?'), currencyTicker: funtypes.Literal('ETH?'), + primary: funtypes.Literal(false), + minimized: funtypes.Literal(true), }) ) diff --git a/app/ts/types/user-interface-types.ts b/app/ts/types/user-interface-types.ts index 878c3c01..87dac51e 100644 --- a/app/ts/types/user-interface-types.ts +++ b/app/ts/types/user-interface-types.ts @@ -164,6 +164,7 @@ export const RpcConnectionStatus = funtypes.Union(funtypes.Undefined, funtypes.R lastConnnectionAttempt: EthereumTimestamp, rpcNetwork: RpcNetwork, latestBlock: funtypes.Union(funtypes.Undefined, EthereumBlockHeader), + retrying: funtypes.Boolean, })) export type PendingChainChangeConfirmationPromise = funtypes.Static diff --git a/app/ts/utils/constants.ts b/app/ts/utils/constants.ts index e738f892..dbce3929 100644 --- a/app/ts/utils/constants.ts +++ b/app/ts/utils/constants.ts @@ -172,9 +172,6 @@ export const PRIMARY_COLOR = '#58a5b3' export const CANNOT_SIMULATE_OFF_LEGACY_BLOCK = 'Cannot simulate off a legacy block' -export const BIG_FONT_SIZE = '28px' -export const NORMAL_FONT_SIZE = '14px' - export const NEW_BLOCK_ABORT = 'New Block Abort' export const MAKE_YOU_RICH_TRANSACTION = { diff --git a/app/ts/utils/storageUtils.ts b/app/ts/utils/storageUtils.ts index 3a302ffa..722806ce 100644 --- a/app/ts/utils/storageUtils.ts +++ b/app/ts/utils/storageUtils.ts @@ -117,6 +117,12 @@ export async function browserStorageLocalSet2(items: LocalStorageItems2) { export async function browserStorageLocalGet(keys: LocalStorageKey | LocalStorageKey[]): Promise { return LocalStorageItems.parse(await browser.storage.local.get(Array.isArray(keys) ? keys : [keys])) } +export async function browserStorageLocalSafeParseGet(keys: LocalStorageKey | LocalStorageKey[]): Promise { + const parsed = LocalStorageItems.safeParse(await browser.storage.local.get(Array.isArray(keys) ? keys : [keys])) + if (parsed.success) return parsed.value + return undefined +} + export async function browserStorageLocalRemove(keys: LocalStorageKey | LocalStorageKey[]) { return await browser.storage.local.remove(Array.isArray(keys) ? keys : [keys]) }