From 5b536949a630a69bf2ed587f777a7ed3c635a592 Mon Sep 17 00:00:00 2001 From: KillariDev Date: Wed, 30 Oct 2024 03:53:04 +0200 Subject: [PATCH 01/28] add support for optimism --- .../SimulationModeEthereumClientService.ts | 8 +++---- app/ts/types/visualizer-types.ts | 4 ++-- app/ts/types/wire-types.ts | 22 +++++++++++++++++-- app/ts/utils/ethereum.ts | 16 +++++++++++++- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/app/ts/simulation/services/SimulationModeEthereumClientService.ts b/app/ts/simulation/services/SimulationModeEthereumClientService.ts index a321190f..020582ee 100644 --- a/app/ts/simulation/services/SimulationModeEthereumClientService.ts +++ b/app/ts/simulation/services/SimulationModeEthereumClientService.ts @@ -1,5 +1,5 @@ import { EthereumClientService } from './EthereumClientService.js' -import { EthereumUnsignedTransaction, EthereumSignedTransactionWithBlockData, EthereumBlockTag, EthereumAddress, EthereumBlockHeader, EthereumBlockHeaderWithTransactionHashes, EthereumSignedTransaction, EthereumData, EthereumQuantity, EthereumBytes32 } from '../../types/wire-types.js' +import { EthereumUnsignedTransaction, EthereumSignedTransactionWithBlockData, EthereumBlockTag, EthereumAddress, EthereumBlockHeader, EthereumBlockHeaderWithTransactionHashes, EthereumData, EthereumQuantity, EthereumBytes32, EthereumSendableSignedTransaction } from '../../types/wire-types.js' import { addressString, bigintToUint8Array, bytes32String, calculateWeightedPercentile, dataStringWith0xStart, max, min, stringToUint8Array } from '../../utils/bigint.js' import { CANNOT_SIMULATE_OFF_LEGACY_BLOCK, ERROR_INTERCEPTOR_GAS_ESTIMATION_FAILED, ETHEREUM_LOGS_LOGGER_ADDRESS, ETHEREUM_EIP1559_BASEFEECHANGEDENOMINATOR, ETHEREUM_EIP1559_ELASTICITY_MULTIPLIER, MOCK_ADDRESS, MULTICALL3, Multicall3ABI, DEFAULT_CALL_ADDRESS, GAS_PER_BLOB, MAKE_YOU_RICH_TRANSACTION } from '../../utils/constants.js' import { Interface, TypedDataEncoder, ethers, hashMessage, keccak256, } from 'ethers' @@ -201,7 +201,7 @@ export const calculateRealizedEffectiveGasPrice = (transaction: EthereumUnsigned return min(blocksBaseFeePerGas + transaction.maxPriorityFeePerGas, transaction.maxFeePerGas) } -export const mockSignTransaction = (transaction: EthereumUnsignedTransaction) : EthereumSignedTransaction => { +export const mockSignTransaction = (transaction: EthereumUnsignedTransaction) : EthereumSendableSignedTransaction => { const unsignedTransaction = EthereumUnsignedTransactionToUnsignedTransaction(transaction) if (unsignedTransaction.type === 'legacy') { const signatureParams = { r: 0n, s: 0n, v: 0n } @@ -568,7 +568,7 @@ async function getSimulatedMockBlock(ethereumClientService: EthereumClientServic stateRoot: parentBlock.stateRoot, // TODO: this is wrong timestamp: new Date(parentBlock.timestamp.getTime() + 12 * 1000), // estimate that the next block is after 12 secs size: parentBlock.size, // TODO: this is wrong - totalDifficulty: parentBlock.totalDifficulty + parentBlock.difficulty, // The difficulty increases about the same amount as previously + totalDifficulty: (parentBlock.totalDifficulty ?? 0n) + parentBlock.difficulty, // The difficulty increases about the same amount as previously uncles: [], baseFeePerGas: getNextBaseFeePerGas(parentBlock.gasUsed, parentBlock.gasLimit, parentBlock.baseFeePerGas), transactionsRoot: parentBlock.transactionsRoot, // TODO: this is wrong @@ -915,7 +915,7 @@ export const getTokenBalancesAfter = async ( requestAbortController: AbortController | undefined, ethSimulateV1CallResults: EthSimulateV1CallResults, blockNumber: bigint, - signedTxs: EthereumSignedTransaction[] = [], + signedTxs: EthereumSendableSignedTransaction[] = [], signedMessages: readonly SignatureWithFakeSignerAddress[] = [], extraAccountOverrides: StateOverrides = {}, ) => { diff --git a/app/ts/types/visualizer-types.ts b/app/ts/types/visualizer-types.ts index 99803952..25ae784a 100644 --- a/app/ts/types/visualizer-types.ts +++ b/app/ts/types/visualizer-types.ts @@ -1,6 +1,6 @@ import * as funtypes from 'funtypes' -import { EthereumAddress, EthereumBytes32, EthereumData, EthereumQuantity, EthereumSignedTransaction, EthereumTimestamp, EthereumUnsignedTransaction, OptionalEthereumAddress } from './wire-types.js' +import { EthereumAddress, EthereumBytes32, EthereumData, EthereumQuantity, EthereumSendableSignedTransaction, EthereumTimestamp, EthereumUnsignedTransaction, OptionalEthereumAddress } from './wire-types.js' import { RenameAddressCallBack } from './user-interface-types.js' import { EthNewFilter, EthSubscribeParams, OriginalSendRequestParameters, SendRawTransactionParams, SendTransactionParams } from './JsonRpc-types.js' import { InterceptedRequest, WebsiteSocket } from '../utils/requests.js' @@ -74,7 +74,7 @@ export const ProtectorResults = funtypes.ReadonlyObject( { export type PreSimulationTransaction = funtypes.Static export const PreSimulationTransaction = funtypes.ReadonlyObject({ - signedTransaction: EthereumSignedTransaction, + signedTransaction: EthereumSendableSignedTransaction, website: Website, created: EthereumTimestamp, originalRequestParameters: funtypes.Union(SendTransactionParams, SendRawTransactionParams), diff --git a/app/ts/types/wire-types.ts b/app/ts/types/wire-types.ts index 8ccacd08..7d567cc2 100644 --- a/app/ts/types/wire-types.ts +++ b/app/ts/types/wire-types.ts @@ -334,6 +334,21 @@ const EthereumTransactionLegacySignature = funtypes.Intersect( ) ) +type EthereumSignedTransactionOptimismDeposit = funtypes.Static +const EthereumSignedTransactionOptimismDeposit = funtypes.ReadonlyObject({ + type: funtypes.Literal('0x7e').withParser(LiteralConverterParserFactory('0x7e', 'optimismDeposit' as const)), + sourceHash: EthereumBytes32, + from: EthereumAddress, + to: funtypes.Union(EthereumAddress, funtypes.Null), + mint: EthereumQuantity, + value: EthereumQuantity, + gas: EthereumQuantity, + data: EthereumInput, + hash: EthereumBytes32, + gasPrice: EthereumQuantity, + nonce: EthereumQuantity, +}) + type EthereumSignedTransactionLegacy = funtypes.Static const EthereumSignedTransactionLegacy = funtypes.Intersect( EthereumUnsignedTransactionLegacy, @@ -358,8 +373,11 @@ const EthereumSignedTransaction4844 = funtypes.Intersect( EthereumTransaction2930And1559And4844Signature, ) +export type EthereumSendableSignedTransaction = funtypes.Static +export const EthereumSendableSignedTransaction = funtypes.Union(EthereumSignedTransactionLegacy, EthereumSignedTransaction2930, EthereumSignedTransaction1559, EthereumSignedTransaction4844) + export type EthereumSignedTransaction = funtypes.Static -export const EthereumSignedTransaction = funtypes.Union(EthereumSignedTransactionLegacy, EthereumSignedTransaction2930, EthereumSignedTransaction1559, EthereumSignedTransaction4844) +export const EthereumSignedTransaction = funtypes.Union(EthereumSendableSignedTransaction, EthereumSignedTransactionOptimismDeposit) export type EthereumSignedTransactionWithBlockData = funtypes.Static export const EthereumSignedTransactionWithBlockData = funtypes.Intersect( @@ -409,7 +427,6 @@ const EthereumBlockHeaderWithoutTransactions = funtypes.Intersect( stateRoot: EthereumBytes32, timestamp: EthereumTimestamp, size: EthereumQuantity, - totalDifficulty: EthereumQuantity, uncles: funtypes.ReadonlyArray(EthereumBytes32), baseFeePerGas: funtypes.Union(EthereumQuantity, funtypes.Undefined), transactionsRoot: EthereumBytes32, @@ -420,6 +437,7 @@ const EthereumBlockHeaderWithoutTransactions = funtypes.Intersect( parentBeaconBlockRoot: EthereumBytes32, withdrawalsRoot: EthereumBytes32, // missing from old block withdrawals: funtypes.ReadonlyArray(EthereumWithdrawal), // missing from old block + totalDifficulty: EthereumQuantity, // missing from new blocks }) ) ) diff --git a/app/ts/utils/ethereum.ts b/app/ts/utils/ethereum.ts index 62b77929..ed51a72a 100644 --- a/app/ts/utils/ethereum.ts +++ b/app/ts/utils/ethereum.ts @@ -49,6 +49,18 @@ export interface IUnsignedTransaction1559 { }[] } +export interface IOptimismDepositTransaction { + readonly type: 'optimismDeposit' + readonly sourceHash: bigint + readonly from: bigint + readonly to: bigint | null + readonly mint: bigint + readonly value: bigint + readonly gas: bigint + readonly data: Uint8Array + readonly hash: bigint +} + interface IUnsignedTransaction4844 { readonly type: '4844' readonly from: bigint @@ -91,7 +103,7 @@ type ISignedTransaction1559 = IUnsignedTransaction1559 & ITransactionSignature15 type ISignedTransactionLegacy = IUnsignedTransactionLegacy & ITransactionSignatureLegacy type ISignedTransaction2930 = IUnsignedTransaction2930 & ITransactionSignature1559and2930and4844 type ISignedTransaction4844 = IUnsignedTransaction4844 & ITransactionSignature1559and2930and4844 -type ISignedTransaction = ISignedTransaction1559 | ISignedTransactionLegacy | ISignedTransaction2930 | ISignedTransaction4844 +type ISignedTransaction = ISignedTransaction1559 | ISignedTransactionLegacy | ISignedTransaction2930 | ISignedTransaction4844 | IOptimismDepositTransaction function calculateV(transaction: DistributiveOmit): bigint { if ('v' in transaction) return transaction.v @@ -241,6 +253,7 @@ export function serializeSignedTransactionToBytes(transaction: DistributiveOmit< case '2930': return new Uint8Array([1, ...rlpEncodeSigned2930TransactionPayload(transaction)]) case '1559': return new Uint8Array([2, ...rlpEncodeSigned1559TransactionPayload(transaction)]) case '4844': return new Uint8Array([2, ...rlpEncodeSigned4844TransactionPayload(transaction)]) + case 'optimismDeposit': throw new Error('Serializing optimismDeposit (0x7e) transaction is not supported') default: assertNever(transaction) } } @@ -291,6 +304,7 @@ export function EthereumSignedTransactionToSignedTransaction(transaction: Ethere ...transaction, gasLimit: transaction.gas, } + case 'optimismDeposit': return transaction default: assertNever(transaction) } } From 7b6612b4456154ba3ce73347d6725146a840c4e1 Mon Sep 17 00:00:00 2001 From: KillariDev Date: Wed, 30 Oct 2024 04:45:39 +0200 Subject: [PATCH 02/28] remove uniswapv3multicall2 --- app/ts/utils/constants.ts | 1 - app/ts/utils/tokenIdentification.ts | 41 ++++++++++++++++------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/app/ts/utils/constants.ts b/app/ts/utils/constants.ts index 8092a729..e1dda35d 100644 --- a/app/ts/utils/constants.ts +++ b/app/ts/utils/constants.ts @@ -14,7 +14,6 @@ export const Multicall3ABI = [ 'function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) payable returns (tuple(bool success, bytes returnData)[] returnData)', 'function getEthBalance(address) returns (uint256)', ] -export const UniswapV3Multicall2 = 0x5ba1e12693dc8f9c48aad8770482f4739beed696n // common 4-byte function sigs // export const ERC20_TRANSFER_FROM_4BYTES = 0x23b872dd diff --git a/app/ts/utils/tokenIdentification.ts b/app/ts/utils/tokenIdentification.ts index 301c9dd9..b3967995 100644 --- a/app/ts/utils/tokenIdentification.ts +++ b/app/ts/utils/tokenIdentification.ts @@ -1,9 +1,8 @@ import { Interface } from 'ethers' -import { Erc20ABI, Erc721ABI, MulticallABI } from './abi.js' +import { Erc20ABI, Erc721ABI } from './abi.js' import { EthereumAddress } from '../types/wire-types.js' import { IEthereumClientService } from '../simulation/services/EthereumClientService.js' -import { UniswapV3Multicall2 } from './constants.js' -import { addressString, checksummedAddress, stringToUint8Array } from './bigint.js' +import { checksummedAddress, stringToUint8Array } from './bigint.js' import { Erc1155Entry, Erc20TokenEntry, Erc721Entry } from '../types/addressBookTypes.js' type EOA = { @@ -18,12 +17,19 @@ type UnknownContract = { export type IdentifiedAddress = (EOA | Erc20TokenEntry | Erc721Entry | Erc1155Entry | UnknownContract) -async function tryAggregateMulticall(ethereumClientService: IEthereumClientService, requestAbortController: AbortController | undefined, calls: { target: string, callData: string }[]): Promise<{ success: boolean, returnData: string }[]> { - const multicallInterface = new Interface(MulticallABI) - const tryAggregate = multicallInterface.getFunction('tryAggregate') - if (tryAggregate === null) throw new Error('tryAggregate misssing from ABI') - const returnData = await ethereumClientService.call({ to: UniswapV3Multicall2, input: stringToUint8Array(multicallInterface.encodeFunctionData(tryAggregate, [false, calls])) }, 'latest', requestAbortController) - return multicallInterface.decodeFunctionResult(tryAggregate, returnData)[0] +async function tryAggregateMulticall(ethereumClientService: IEthereumClientService, requestAbortController: AbortController | undefined, calls: { targetAddress: EthereumAddress, callData: Uint8Array }[]): Promise<{ success: boolean, returnData: Uint8Array }[]> { + const results = await ethereumClientService.ethSimulateV1([{ calls: calls.map((call) => ({ + type: '1559' as const, + to: call.targetAddress, + input: call.callData + }))}], 'latest', requestAbortController) + const blockResult = results[0] + if (blockResult === undefined) throw new Error('Failed eth_simulateV1 call: did not get a block') + if (blockResult.calls.length !== calls.length) throw new Error('Failed eth_simulateV1 call: call length mismatch') + return blockResult.calls.map((call) => ({ + success: call.status === 'success', + returnData: call.returnData + })) } export async function itentifyAddressViaOnChainInformation(ethereumClientService: IEthereumClientService, requestAbortController: AbortController | undefined, address: EthereumAddress): Promise { @@ -32,16 +38,15 @@ export async function itentifyAddressViaOnChainInformation(ethereumClientService const nftInterface = new Interface(Erc721ABI) const erc20Interface = new Interface(Erc20ABI) - const target = addressString(address) - + const targetAddress = address const calls = [ - { target, callData: nftInterface.encodeFunctionData('supportsInterface', ['0x80ac58cd']) }, // Is Erc721Definition - { target, callData: nftInterface.encodeFunctionData('supportsInterface', ['0x5b5e139f']) }, // Is Erc721Metadata - { target, callData: nftInterface.encodeFunctionData('supportsInterface', ['0xd9b67a26']) }, // Is Erc1155Definition - { target, callData: erc20Interface.encodeFunctionData('name', []) }, - { target, callData: erc20Interface.encodeFunctionData('symbol', []) }, - { target, callData: erc20Interface.encodeFunctionData('decimals', []) }, - { target, callData: erc20Interface.encodeFunctionData('totalSupply', []) } + { targetAddress, callData: stringToUint8Array(nftInterface.encodeFunctionData('supportsInterface', ['0x80ac58cd'])) }, // Is Erc721Definition + { targetAddress, callData: stringToUint8Array(nftInterface.encodeFunctionData('supportsInterface', ['0x5b5e139f'])) }, // Is Erc721Metadata + { targetAddress, callData: stringToUint8Array(nftInterface.encodeFunctionData('supportsInterface', ['0xd9b67a26'])) }, // Is Erc1155Definition + { targetAddress, callData: stringToUint8Array(erc20Interface.encodeFunctionData('name', [])) }, + { targetAddress, callData: stringToUint8Array(erc20Interface.encodeFunctionData('symbol', [])) }, + { targetAddress, callData: stringToUint8Array(erc20Interface.encodeFunctionData('decimals', [])) }, + { targetAddress, callData: stringToUint8Array(erc20Interface.encodeFunctionData('totalSupply', [])) } ] try { From 96bfa6c66bc3df4093795983cd99fb05afeaa02d Mon Sep 17 00:00:00 2001 From: KillariDev Date: Wed, 30 Oct 2024 08:31:34 +0200 Subject: [PATCH 03/28] make micah poor again --- app/ts/background/windows/confirmTransaction.ts | 4 ++-- .../services/SimulationModeEthereumClientService.ts | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/ts/background/windows/confirmTransaction.ts b/app/ts/background/windows/confirmTransaction.ts index f81b0a4a..66da4841 100644 --- a/app/ts/background/windows/confirmTransaction.ts +++ b/app/ts/background/windows/confirmTransaction.ts @@ -197,7 +197,7 @@ export const formEthSendTransaction = async(ethereumClientService: EthereumClien const transactionDetails = sendTransactionParams.params[0] if (activeAddress === undefined) throw new Error('Access to active address is denied') const from = simulationMode && transactionDetails.from !== undefined ? transactionDetails.from : activeAddress - const transactionCount = getSimulatedTransactionCount(ethereumClientService, requestAbortController, simulationState, from) + const transactionCountPromise = getSimulatedTransactionCount(ethereumClientService, requestAbortController, simulationState, from) const parentBlock = await parentBlockPromise if (parentBlock === null) throw new Error('The latest block is null') if (parentBlock.baseFeePerGas === undefined) throw new Error(CANNOT_SIMULATE_OFF_LEGACY_BLOCK) @@ -206,7 +206,7 @@ export const formEthSendTransaction = async(ethereumClientService: EthereumClien type: '1559' as const, from, chainId: ethereumClientService.getChainId(), - nonce: await transactionCount, + nonce: await transactionCountPromise, maxFeePerGas: transactionDetails.maxFeePerGas !== undefined && transactionDetails.maxFeePerGas !== null ? transactionDetails.maxFeePerGas : parentBlock.baseFeePerGas * 2n + maxPriorityFeePerGas, maxPriorityFeePerGas, to: transactionDetails.to === undefined ? null : transactionDetails.to, diff --git a/app/ts/simulation/services/SimulationModeEthereumClientService.ts b/app/ts/simulation/services/SimulationModeEthereumClientService.ts index a321190f..66b20f0e 100644 --- a/app/ts/simulation/services/SimulationModeEthereumClientService.ts +++ b/app/ts/simulation/services/SimulationModeEthereumClientService.ts @@ -224,6 +224,11 @@ export const appendTransaction = async (ethereumClientService: EthereumClientSer const signed = mockSignTransaction(transaction.transaction) return simulationState === undefined ? [signed] : simulationState.simulatedTransactions.map((x) => x.preSimulationTransaction.signedTransaction).concat([signed]) } + const getMakeMeRichAddress = async () => { + if (typeof browser === 'undefined') return simulationState?.addressToMakeRich // if we are not running in browser (tests) + if (simulationState === undefined) return undefined // we are not simulation, don't make anyone rich + return await getAddressToMakeRich() + } const parentBlock = await ethereumClientService.getBlock(requestAbortController) if (parentBlock === null) throw new Error('The latest block is null') @@ -231,7 +236,7 @@ export const appendTransaction = async (ethereumClientService: EthereumClientSer if (parentBaseFeePerGas === undefined) throw new Error(CANNOT_SIMULATE_OFF_LEGACY_BLOCK) const signedMessages = getSignedMessagesWithFakeSigner(simulationState) const signedTxs = getSignedTransactions() - const addressToMakeRich = typeof browser === 'undefined' ? simulationState?.addressToMakeRich : await getAddressToMakeRich() + const addressToMakeRich = await getMakeMeRichAddress() const makeMeRich = getMakeMeRichStateOverride(addressToMakeRich) const ethSimulateV1CallResult = await ethereumClientService.simulateTransactionsAndSignatures(signedTxs, signedMessages, parentBlock.number, requestAbortController, makeMeRich) const transactionWebsiteData = { website: transaction.website, created: transaction.created, originalRequestParameters: transaction.originalRequestParameters, transactionIdentifier: transaction.transactionIdentifier } From e10ccec7106c0b1aa9b6018c636b24e61b5fc79e Mon Sep 17 00:00:00 2001 From: KillariDev Date: Sat, 2 Nov 2024 08:20:41 +0200 Subject: [PATCH 04/28] separate addressbook for each chain --- app/ts/AddressBook.tsx | 101 ++++++--- app/ts/background/background.ts | 2 +- app/ts/background/medataSearch.ts | 25 ++- app/ts/background/metadataUtils.ts | 17 +- app/ts/background/popupMessageHandlers.ts | 10 +- app/ts/background/settings.ts | 2 +- app/ts/background/storageVariables.ts | 7 +- app/ts/components/App.tsx | 76 ++++--- app/ts/components/pages/AddNewAddress.tsx | 211 +++++++++--------- .../components/pages/ConfirmTransaction.tsx | 73 ++++-- app/ts/components/pages/Home.tsx | 25 +-- app/ts/components/pages/InterceptorAccess.tsx | 58 +++-- app/ts/components/pages/SettingsView.tsx | 6 +- .../SimulationSummary.tsx | 7 +- .../subcomponents/ChainSelector.tsx | 15 +- app/ts/types/addressBookTypes.ts | 18 +- app/ts/types/interceptor-messages.ts | 7 +- app/ts/types/user-interface-types.ts | 23 +- 18 files changed, 402 insertions(+), 281 deletions(-) diff --git a/app/ts/AddressBook.tsx b/app/ts/AddressBook.tsx index 1535240c..8460fb35 100644 --- a/app/ts/AddressBook.tsx +++ b/app/ts/AddressBook.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'preact/hooks' +import { useEffect } from 'preact/hooks' import { RenameAddressCallBack } from './types/user-interface-types.js' import { GetAddressBookDataReply, MessageToPopup } from './types/interceptor-messages.js' import { AddNewAddress } from './components/pages/AddNewAddress.js' @@ -11,7 +11,9 @@ import { AddressBookEntries, AddressBookEntry } from './types/addressBookTypes.j import { ModifyAddressWindowState } from './types/visualizer-types.js' import { XMarkIcon } from './components/subcomponents/icons.js' import { DynamicScroller } from './components/subcomponents/DynamicScroller.js' -import { useSignal, useSignalEffect } from '@preact/signals' +import { useComputed, useSignal, useSignalEffect } from '@preact/signals' +import { RpcEntries, RpcEntry, RpcNetwork } from './types/rpc.js' +import { ChainSelector } from './components/subcomponents/ChainSelector.js' type Modals = { page: 'noModal' } | { page: 'addNewAddress', state: ModifyAddressWindowState } @@ -165,35 +167,54 @@ function AddressBookEntryCard({ removeEntry, renameAddressCallBack, ...entry }: type ViewFilter = { activeFilter: FilterKey searchString: string + rpcNetwork: { readonly name: string, readonly chainId: bigint } | undefined } export function AddressBook() { const addressBookEntries = useSignal([]) - const viewFilter = useSignal({ activeFilter: 'My Active Addresses', searchString: ''}) - const [modalState, setModalState] = useState({ page: 'noModal' }) - + const currentRpcNetwork = useSignal(undefined) + const rpcEntries = useSignal([]) + const viewFilter = useSignal({ activeFilter: 'My Active Addresses', searchString: '', rpcNetwork: undefined }) + const modalState = useSignal({ page: 'noModal' }) 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_addressBookEntriesChanged') { - sendQuery(viewFilter.value.activeFilter, viewFilter.value.searchString) + const chainId = currentRpcNetwork.peek()?.chainId + if (chainId !== undefined) sendQuery() + return + } + if (parsed.method === 'popup_settingsUpdated') return sendPopupMessageToBackgroundPage({ method: 'popup_requestSettings' }) + if (parsed.method === 'popup_requestSettingsReply') { + rpcEntries.value = parsed.data.rpcEntries + const prevCurrentNetwork = currentRpcNetwork.peek() + if (prevCurrentNetwork === undefined || prevCurrentNetwork.chainId === parsed.data.currentRpcNetwork.chainId) { + currentRpcNetwork.value = parsed.data.currentRpcNetwork + if (prevCurrentNetwork === undefined || viewFilter.value.rpcNetwork === undefined) { + viewFilter.value.rpcNetwork = currentRpcNetwork.value === undefined ? undefined : { name: currentRpcNetwork.value.name, chainId: currentRpcNetwork.value.chainId } + sendQuery() + } + } } if (parsed.method !== 'popup_getAddressBookDataReply') return const reply = GetAddressBookDataReply.parse(msg) - addressBookEntries.value = reply.data.entries + if(currentRpcNetwork.peek()?.chainId === reply.data.data.chainId) { + addressBookEntries.value = reply.data.entries + } } browser.runtime.onMessage.addListener(popupMessageListener) - return () => { - browser.runtime.onMessage.removeListener(popupMessageListener) - } + return () => { browser.runtime.onMessage.removeListener(popupMessageListener) } }, []) - function sendQuery(filter: FilterKey, searchString: string | undefined) { + function sendQuery() { + const filterValue = viewFilter.peek() + if (filterValue.rpcNetwork === undefined) return sendPopupMessageToBackgroundPage({ method: 'popup_getAddressBookData', data: { - filter: filter, - searchString: searchString, + chainId: filterValue.rpcNetwork.chainId, + filter: filterValue.activeFilter, + searchString: filterValue.searchString } }) } @@ -207,8 +228,8 @@ export function AddressBook() { function getNoResultsError() { const errorMessage = (viewFilter.value.searchString && viewFilter.value.searchString.trim().length > 0 ) - ? `No entries found for "${ viewFilter.value.searchString }" in ${ viewFilter.value.activeFilter }` - : `No cute dinosaurs in ${ viewFilter.value.activeFilter }` + ? `No entries found for "${ viewFilter.value.searchString }" in ${ viewFilter.value.activeFilter } on ${ viewFilter.value.rpcNetwork?.name }` + : `No cute dinosaurs in ${ viewFilter.value.activeFilter } on ${ viewFilter.value.rpcNetwork?.name }` return
{ errorMessage }
} @@ -225,7 +246,7 @@ export function AddressBook() { } } - return setModalState({ page: 'addNewAddress', state: { + modalState.value = { page: 'addNewAddress', state: { windowStateId: 'AddressBookAdd', errorState: undefined, incompleteAddressBookEntry: { @@ -241,12 +262,14 @@ export function AddressBook() { abi: undefined, useAsActiveAddress: filter === 'My Active Addresses', declarativeNetRequestBlockMode: undefined, + chainId: currentRpcNetwork.peek()?.chainId || 1n, } - } }) + } } + return } function renameAddressCallBack(entry: AddressBookEntry) { - return setModalState({ page: 'addNewAddress', state: { + modalState.value = { page: 'addNewAddress', state: { windowStateId: 'AddressBookRename', errorState: undefined, incompleteAddressBookEntry: { @@ -260,8 +283,10 @@ export function AddressBook() { ...entry, abi: 'abi' in entry ? entry.abi : undefined, address: checksummedAddress(entry.address), + chainId: entry.chainId || 1n, } - } }) + } } + return } function removeAddressBookEntry(entry: AddressBookEntry) { @@ -270,19 +295,29 @@ export function AddressBook() { data: { address: entry.address, addressBookCategory: viewFilter.value.activeFilter, + chainId: entry.chainId || 1n, } }) } - useSignalEffect(() => { - sendQuery(viewFilter.value.activeFilter, viewFilter.value.searchString) - }) + const changeCurrentRpcEntry = (entry: RpcEntry) => { + if (entry.chainId === currentRpcNetwork.peek()?.chainId) return + currentRpcNetwork.value = entry + viewFilter.value = { ...viewFilter.peek(), rpcNetwork: { name: entry.name, chainId: entry.chainId } } + sendQuery() + } + useSignalEffect(() => { sendPopupMessageToBackgroundPage({ method: 'popup_requestSettings' }) }) + if (currentRpcNetwork.value === undefined) return
+ const modifyAddressSignal = useComputed(() => modalState.value.page === 'addNewAddress' ? modalState.value.state : undefined) return (
+
+ +
-
- { modalState.page === 'addNewAddress' ? +
+ { modifyAddressSignal.value !== undefined ? setModalState({ page: 'noModal' }) } + modifyAddressWindowState = { modifyAddressSignal } + close = { () => modalState.value = { page: 'noModal' } } activeAddress = { undefined } + rpcEntries = { rpcEntries } + modifyStateCallBack = { (newState: ModifyAddressWindowState) => { + if (modalState.value.page !== 'addNewAddress') return + console.log('setstate!', newState.incompleteAddressBookEntry.chainId) + modalState.value = { page: modalState.value.page, state: newState } + } } /> : <> } - { modalState.page === 'confirmaddressBookEntryToBeRemoved' ? + { modalState.value.page === 'confirmaddressBookEntryToBeRemoved' ? setModalState({ page: 'noModal' }) } + close = { () => modalState.value = { page: 'noModal' } } renameAddressCallBack = { renameAddressCallBack } /> : <> } diff --git a/app/ts/background/background.ts b/app/ts/background/background.ts index 240885ab..9d73b03f 100644 --- a/app/ts/background/background.ts +++ b/app/ts/background/background.ts @@ -708,7 +708,7 @@ export async function popupMessageHandler( case 'popup_confirmTransactionReadyAndListening': return await updateConfirmTransactionView(simulator.ethereum) case 'popup_requestNewHomeData': return await requestNewHomeData(simulator, simulationAbortController) case 'popup_refreshHomeData': return await refreshHomeData(simulator) - case 'popup_settingsOpened': return await settingsOpened() + case 'popup_requestSettings': return await settingsOpened() case 'popup_refreshInterceptorAccessMetadata': return await interceptorAccessMetadataRefresh() case 'popup_interceptorAccessChangeAddress': return await interceptorAccessChangeAddressOrRefresh(websiteTabConnections, parsedRequest) case 'popup_interceptorAccessRefresh': return await interceptorAccessChangeAddressOrRefresh(websiteTabConnections, parsedRequest) diff --git a/app/ts/background/medataSearch.ts b/app/ts/background/medataSearch.ts index b24619b9..169ba769 100644 --- a/app/ts/background/medataSearch.ts +++ b/app/ts/background/medataSearch.ts @@ -4,7 +4,7 @@ import { tokenMetadata, contractMetadata, ContractDefinition, TokenDefinition, E import { AddressBookCategory, GetAddressBookDataFilter } from '../types/interceptor-messages.js' import { getFullLogoUri } from './metadataUtils.js' import { assertNever } from '../utils/typescript.js' -import { getUserAddressBookEntries } from './storageVariables.js' +import { getUserAddressBookEntriesForChainId } from './storageVariables.js' type PartialResult = { bestMatchLength: number, @@ -72,12 +72,12 @@ function concatArraysUniqueByAddress(addTo: readonly (T & { address: bigint } return [...addTo, ...uniqueItems] } -async function filterAddressBookDataByCategoryAndSearchString(addressBookCategory: AddressBookCategory, searchString: string | undefined): Promise { +async function filterAddressBookDataByCategoryAndSearchString(addressBookCategory: AddressBookCategory, searchString: string | undefined, chainId: bigint): Promise { const unicodeEscapeString = (input: string) => `\\u{${ input.charCodeAt(0).toString(16) }}` const trimmedSearch = searchString !== undefined && searchString.trim().length > 0 ? searchString.trim() : undefined const searchPattern = trimmedSearch ? new RegExp(`(?=(${ trimmedSearch.split('').map(unicodeEscapeString).join('.*?') }))`, 'ui') : undefined const searchingDisabled = trimmedSearch === undefined || searchPattern === undefined - const userEntries = (await getUserAddressBookEntries()).filter((entry) => entry.entrySource !== 'OnChain') + const userEntries = (await getUserAddressBookEntriesForChainId(chainId)).filter((entry) => entry.entrySource !== 'OnChain') switch(addressBookCategory) { case 'My Contacts': { const entries = userEntries.filter((entry): entry is ContactEntry => entry.type === 'contact') @@ -98,7 +98,8 @@ async function filterAddressBookDataByCategoryAndSearchString(addressBookCategor return search(entries, searchFunction) } case 'ERC1155 Tokens': { - const entries = concatArraysUniqueByAddress(userEntries.filter((entry): entry is Erc1155Entry => entry.type === 'ERC1155'), Array.from(erc1155Metadata).map(convertErc1155DefinitionToAddressBookEntry)) + const filteredUserEntries = userEntries.filter((entry): entry is Erc1155Entry => entry.type === 'ERC1155') + const entries = chainId === 1n ? concatArraysUniqueByAddress(filteredUserEntries, Array.from(erc1155Metadata).map(convertErc1155DefinitionToAddressBookEntry)) : filteredUserEntries if (searchingDisabled) return entries const searchFunction = (entry: Erc1155Entry) => ({ comparison: fuzzyCompare(searchPattern, trimmedSearch, entry.name, addressString(entry.address)), @@ -107,7 +108,8 @@ async function filterAddressBookDataByCategoryAndSearchString(addressBookCategor return search(entries, searchFunction) } case 'ERC20 Tokens': { - const entries = concatArraysUniqueByAddress(userEntries.filter((entry): entry is Erc20TokenEntry => entry.type === 'ERC20'), Array.from(tokenMetadata).map(convertTokenDefinitionToAddressBookEntry)) + const filteredUserEntries = userEntries.filter((entry): entry is Erc20TokenEntry => entry.type === 'ERC20') + const entries = chainId === 1n ? concatArraysUniqueByAddress(filteredUserEntries, Array.from(tokenMetadata).map(convertTokenDefinitionToAddressBookEntry)) : filteredUserEntries if (searchingDisabled) return entries const searchFunction = (entry: Erc20TokenEntry) => ({ comparison: fuzzyCompare(searchPattern, trimmedSearch, `${ entry.symbol} ${ entry.name}`, addressString(entry.address)), @@ -116,7 +118,8 @@ async function filterAddressBookDataByCategoryAndSearchString(addressBookCategor return search(entries, searchFunction) } case 'Non Fungible Tokens': { - const entries = concatArraysUniqueByAddress(userEntries.filter((entry): entry is Erc721Entry => entry.type === 'ERC721'), Array.from(erc721Metadata).map(convertErc721DefinitionToAddressBookEntry)) + const filteredUserEntries = userEntries.filter((entry): entry is Erc721Entry => entry.type === 'ERC721') + const entries = chainId === 1n ? concatArraysUniqueByAddress(filteredUserEntries, Array.from(erc721Metadata).map(convertErc721DefinitionToAddressBookEntry)) : filteredUserEntries if (searchingDisabled) return entries const searchFunction = (entry: Erc721Entry) => ({ comparison: fuzzyCompare(searchPattern, trimmedSearch, `${ entry.symbol} ${ entry.name}`, addressString(entry.address)), @@ -125,7 +128,8 @@ async function filterAddressBookDataByCategoryAndSearchString(addressBookCategor return search(entries, searchFunction) } case 'Other Contracts': { - const entries = concatArraysUniqueByAddress(userEntries.filter((entry): entry is ContractEntry => entry.type === 'contract'), Array.from(contractMetadata).map(convertContractDefinitionToAddressBookEntry)) + const filteredUserEntries = userEntries.filter((entry): entry is ContractEntry => entry.type === 'contract') + const entries = chainId === 1n ? concatArraysUniqueByAddress(filteredUserEntries, Array.from(contractMetadata).map(convertContractDefinitionToAddressBookEntry)) : filteredUserEntries if (searchingDisabled) return entries const searchFunction = (entry: ContractEntry) => ({ comparison: fuzzyCompare(searchPattern, trimmedSearch, `${ 'protocol' in entry && entry.protocol !== undefined ? entry.protocol : ''} ${ entry.name }`, addressString(entry.address)), @@ -138,14 +142,14 @@ async function filterAddressBookDataByCategoryAndSearchString(addressBookCategor } export async function getMetadataForAddressBookData(filter: GetAddressBookDataFilter) { - const filtered = await filterAddressBookDataByCategoryAndSearchString(filter.filter, filter.searchString) + const filtered = await filterAddressBookDataByCategoryAndSearchString(filter.filter, filter.searchString, filter.chainId) return { entries: filtered.slice(filter.startIndex, filter.maxIndex), maxDataLength: filtered.length, } } -export async function findEntryWithSymbolOrName(symbol: string | undefined, name: string | undefined): Promise { +export async function findEntryWithSymbolOrName(symbol: string | undefined, name: string | undefined, chainId: bigint): Promise { const lowerCasedName = name?.toLowerCase() const lowerCasedSymbol = symbol?.toLowerCase() @@ -163,9 +167,8 @@ export async function findEntryWithSymbolOrName(symbol: string | undefined, name const contractMetadataEntry = Array.from(contractMetadata).find((entry) => lowerCasedEqual(entry[1].name, lowerCasedName)) if (contractMetadataEntry !== undefined) return convertContractDefinitionToAddressBookEntry(contractMetadataEntry) - const userEntries = await getUserAddressBookEntries() + const userEntries = await getUserAddressBookEntriesForChainId(chainId) const userEntry = userEntries.find((entry) => ('symbol' in entry && lowerCasedEqual(entry.symbol, lowerCasedSymbol)) || lowerCasedEqual(entry.name, lowerCasedName)) if (userEntry !== undefined) return userEntry - return undefined } diff --git a/app/ts/background/metadataUtils.ts b/app/ts/background/metadataUtils.ts index 5e6e067f..b1a7d9a9 100644 --- a/app/ts/background/metadataUtils.ts +++ b/app/ts/background/metadataUtils.ts @@ -7,7 +7,7 @@ import { ENS_ADDR_REVERSE_NODE, ENS_TOKEN_WRAPPER, ETHEREUM_COIN_ICON, ETHEREUM_ import { EthereumClientService } from '../simulation/services/EthereumClientService.js' import { IdentifiedAddress, itentifyAddressViaOnChainInformation } from '../utils/tokenIdentification.js' import { assertNever } from '../utils/typescript.js' -import { addEnsLabelHash, addEnsNodeHash, addUserAddressBookEntryIfItDoesNotExist, getEnsLabelHashes, getEnsNodeHashes, getUserAddressBookEntries } from './storageVariables.js' +import { addEnsLabelHash, addEnsNodeHash, addUserAddressBookEntryIfItDoesNotExist, getEnsLabelHashes, getEnsNodeHashes, getUserAddressBookEntries, getUserAddressBookEntriesForChainId } from './storageVariables.js' import { getUniqueItemsByProperties } from '../utils/typed-arrays.js' import { getEnsReverseNodeHash, getEthereumNameServiceNameFromTokenId } from '../utils/ethereumNameService.js' import { defaultActiveAddresses } from './settings.js' @@ -48,10 +48,11 @@ async function identifyAddressWithoutNode(address: bigint, rpcEntry: RpcNetwork symbol: rpcEntry?.currencyTicker ?? 'ETH', decimals: 18n, logoUri: rpcEntry !== undefined && 'currencyLogoUri' in rpcEntry ? rpcEntry.currencyLogoUri : ETHEREUM_COIN_ICON, + chainId: rpcEntry?.chainId, } if (useLocalStorage) { - const userEntry = (await getUserAddressBookEntries()).find((entry) => entry.address === address) + const userEntry = (await getUserAddressBookEntriesForChainId(rpcEntry?.chainId || 1n)).find((entry) => entry.address === address) if (userEntry !== undefined) return userEntry } const addrString = addressString(address) @@ -62,6 +63,7 @@ async function identifyAddressWithoutNode(address: bigint, rpcEntry: RpcNetwork logoUri: addressData.logoUri ? `${ getFullLogoUri(addressData.logoUri) }` : undefined, type: 'contract', entrySource: 'DarkFloristMetadata', + chainId: rpcEntry?.chainId } const tokenData = tokenMetadata.get(addrString) @@ -71,6 +73,7 @@ async function identifyAddressWithoutNode(address: bigint, rpcEntry: RpcNetwork logoUri: tokenData.logoUri ? `${ getFullLogoUri(tokenData.logoUri) }` : undefined, type: 'ERC20', entrySource: 'DarkFloristMetadata', + chainId: rpcEntry?.chainId } const erc721TokenData = erc721Metadata.get(addrString) @@ -80,6 +83,7 @@ async function identifyAddressWithoutNode(address: bigint, rpcEntry: RpcNetwork logoUri: erc721TokenData.logoUri ? `${ getFullLogoUri(erc721TokenData.logoUri) }` : undefined, type: 'ERC721', entrySource: 'DarkFloristMetadata', + chainId: rpcEntry?.chainId } const erc1155TokenData = erc1155Metadata.get(addrString) @@ -90,6 +94,7 @@ async function identifyAddressWithoutNode(address: bigint, rpcEntry: RpcNetwork type: 'ERC1155', entrySource: 'DarkFloristMetadata', decimals: undefined, + chainId: rpcEntry?.chainId } if (address === MOCK_ADDRESS) return { @@ -98,12 +103,14 @@ async function identifyAddressWithoutNode(address: bigint, rpcEntry: RpcNetwork logoUri: '../../img/contracts/rhino.png', type: 'contact', entrySource: 'Interceptor', + chainId: rpcEntry?.chainId } if (address === 0n) return { address: address, name: '0x0 Address', type: 'contact', entrySource: 'Interceptor', + chainId: rpcEntry?.chainId } return undefined } @@ -113,6 +120,7 @@ export async function identifyAddress(ethereumClientService: EthereumClientServi if (identifiedAddress !== undefined) return identifiedAddress const addrString = addressString(address) const tokenIdentification = await itentifyAddressViaOnChainInformation(ethereumClientService, requestAbortController, address) + const chainId = ethereumClientService.getChainId() const getEntry = (tokenIdentification: IdentifiedAddress) => { switch (tokenIdentification.type) { case 'ERC20': return { @@ -122,6 +130,7 @@ export async function identifyAddress(ethereumClientService: EthereumClientServi decimals: tokenIdentification.decimals, type: 'ERC20' as const, entrySource: 'OnChain' as const, + chainId } case 'ERC1155': return { name: ethers.getAddress(addrString), @@ -130,6 +139,7 @@ export async function identifyAddress(ethereumClientService: EthereumClientServi type: 'ERC1155' as const, decimals: undefined, entrySource: 'OnChain' as const, + chainId } case 'ERC721': return { name: tokenIdentification.name, @@ -137,18 +147,21 @@ export async function identifyAddress(ethereumClientService: EthereumClientServi symbol: tokenIdentification.symbol, type: 'ERC721' as const, entrySource: 'OnChain' as const, + chainId } case 'contract': return { address, name: ethers.getAddress(addrString), type: 'contract' as const, entrySource: 'OnChain' as const, + chainId } case 'EOA': return { address, name: ethers.getAddress(addrString), type: 'contact' as const, entrySource: 'OnChain' as const, + chainId } default: assertNever(tokenIdentification) } diff --git a/app/ts/background/popupMessageHandlers.ts b/app/ts/background/popupMessageHandlers.ts index fb861db8..ce258cf1 100644 --- a/app/ts/background/popupMessageHandlers.ts +++ b/app/ts/background/popupMessageHandlers.ts @@ -90,8 +90,8 @@ export async function removeAddressBookEntry(simulator: Simulator, websiteTabCon export async function addOrModifyAddressBookEntry(simulator: Simulator, websiteTabConnections: WebsiteTabConnections, entry: AddOrEditAddressBookEntry) { await updateUserAddressBookEntries((previousContacts) => { - if (previousContacts.find((x) => x.address === entry.data.address)) { - return previousContacts.map((x) => x.address === entry.data.address ? entry.data : x) + if (previousContacts.find((previous) => previous.address === entry.data.address && (previous.chainId || 1n) === (entry.data.chainId || 1n)) ) { + return previousContacts.map((previous) => previous.address === entry.data.address && (previous.chainId || 1n) === (entry.data.chainId || 1n) ? entry.data : previous) } return previousContacts.concat([entry.data]) }) @@ -383,13 +383,15 @@ export async function settingsOpened() { const useTabsInsteadOfPopupPromise = getUseTabsInsteadOfPopup() const metamaskCompatibilityModePromise = getMetamaskCompatibilityMode() const rpcEntriesPromise = getRpcList() + const settingsPromise = getSettings() await sendPopupMessageToOpenWindows({ - method: 'popup_settingsOpenedReply' as const, + method: 'popup_requestSettingsReply' as const, data: { useTabsInsteadOfPopup: await useTabsInsteadOfPopupPromise, metamaskCompatibilityMode: await metamaskCompatibilityModePromise, rpcEntries: await rpcEntriesPromise, + currentRpcNetwork: (await settingsPromise).currentRpcNetwork } }) } @@ -465,7 +467,7 @@ export async function simulateGnosisSafeTransactionOnPass(ethereum: EthereumClie const getErrorIfAnyWithIncompleteAddressBookEntry = async (ethereum: EthereumClientService, incompleteAddressBookEntry: IncompleteAddressBookEntry) => { // check for duplicates - const duplicateEntry = await findEntryWithSymbolOrName(incompleteAddressBookEntry.symbol, incompleteAddressBookEntry.name) + const duplicateEntry = await findEntryWithSymbolOrName(incompleteAddressBookEntry.symbol, incompleteAddressBookEntry.name, incompleteAddressBookEntry.chainId) if (duplicateEntry !== undefined && duplicateEntry.address !== stringToAddress(incompleteAddressBookEntry.address)) { return `There already exists ${ duplicateEntry.type } with ${ 'symbol' in duplicateEntry ? `the symbol "${ duplicateEntry.symbol }" and` : '' } the name "${ duplicateEntry.name }".` } diff --git a/app/ts/background/settings.ts b/app/ts/background/settings.ts index 8cbac975..920b88f4 100644 --- a/app/ts/background/settings.ts +++ b/app/ts/background/settings.ts @@ -202,7 +202,7 @@ export async function importSettingsAndAddressBook(exportedSetings: ExportedSett await updateUserAddressBookEntries(() => exportedSetings.settings.addressBookEntries) } else { await updateUserAddressBookEntries((previousEntries) => { - const convertActiveAddressToAddressBookEntry = (info: ActiveAddress): AddressBookEntry => ({ ...info, type: 'contact' as const, useAsActiveAddress: true,entrySource: 'User' as const }) + const convertActiveAddressToAddressBookEntry = (info: ActiveAddress): AddressBookEntry => ({ ...info, type: 'contact' as const, useAsActiveAddress: true, entrySource: 'User' as const }) return getUniqueItemsByProperties(previousEntries.concat(exportedSetings.settings.addressInfos.map((x) => convertActiveAddressToAddressBookEntry(x))).concat(exportedSetings.settings.contacts ?? []), ['address']) }) } diff --git a/app/ts/background/storageVariables.ts b/app/ts/background/storageVariables.ts index 254780ff..92065886 100644 --- a/app/ts/background/storageVariables.ts +++ b/app/ts/background/storageVariables.ts @@ -136,7 +136,7 @@ export const removeTabState = async(tabId: number) => await removeTabStateFromSt const getTabAllStateKeys = async () => { const allStorage = Object.keys(await browser.storage.local.get()) - return allStorage.filter((entry) => entry.match(/^tabState_[0-9]+/) !== null) + return allStorage.filter((entry) => entry.match(/^tabState_[0-9]+/) !== null) } export const clearTabStates = async () => await browser.storage.local.remove(await getTabAllStateKeys()) @@ -245,6 +245,7 @@ export const getRpcNetworkForChain = async (chainId: bigint): Promise (await browserStorageLocalGet('userAddressBookEntriesV2'))?.userAddressBookEntriesV2 ?? [] +export const getUserAddressBookEntriesForChainId = async (chainId: bigint) => (await getUserAddressBookEntries()).filter((entry) => entry.chainId === chainId || (entry.chainId === undefined && chainId === 1n)) const userAddressBookEntriesSemaphore = new Semaphore(1) export async function updateUserAddressBookEntries(updateFunc: (prevState: AddressBookEntries) => AddressBookEntries) { @@ -257,7 +258,7 @@ export async function updateUserAddressBookEntries(updateFunc: (prevState: Addre export async function addUserAddressBookEntryIfItDoesNotExist(newEntry: AddressBookEntry) { await userAddressBookEntriesSemaphore.execute(async () => { const entries = await getUserAddressBookEntries() - const existingEntry = entries.find((entry) => entry.address === newEntry.address) + const existingEntry = entries.find((entry) => entry.address === newEntry.address && (entry.chainId || 1n) === (newEntry.chainId || 1n) ) if (existingEntry !== undefined) return return await browserStorageLocalSet({ userAddressBookEntriesV2: entries.concat(newEntry) }) }) @@ -295,7 +296,7 @@ export async function addEnsLabelHash(label: string) { } const transactionStackSemaphore = new Semaphore(1) -export const getTransactionStack = async () => (await browserStorageLocalGet('transactionStack'))?.transactionStack ?? { transactions: [], signedMessages: [] } +export const getTransactionStack = async () => (await browserStorageLocalGet('transactionStack'))?.transactionStack ?? { transactions: [], signedMessages: [] } export async function updateTransactionStack(updateFunc: (prevStack: TransactionStack) => TransactionStack): Promise { return await transactionStackSemaphore.execute(async () => { const prevStack = await getTransactionStack() diff --git a/app/ts/components/App.tsx b/app/ts/components/App.tsx index e9d33ca7..4fef5d04 100644 --- a/app/ts/components/App.tsx +++ b/app/ts/components/App.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'preact/hooks' import { defaultActiveAddresses } from '../background/settings.js' -import { SimulatedAndVisualizedTransaction, SimulationAndVisualisationResults, SimulationState, TokenPriceEstimate, SimulationUpdatingState, SimulationResultState, NamedTokenId } from '../types/visualizer-types.js' +import { SimulatedAndVisualizedTransaction, SimulationAndVisualisationResults, SimulationState, TokenPriceEstimate, SimulationUpdatingState, SimulationResultState, NamedTokenId, ModifyAddressWindowState } from '../types/visualizer-types.js' import { ChangeActiveAddress } from './pages/ChangeActiveAddress.js' import { Home } from './pages/Home.js' import { RpcConnectionStatus, TabIconDetails, TabState } from '../types/user-interface-types.js' @@ -27,6 +27,7 @@ import { SomeTimeAgo } from './subcomponents/SomeTimeAgo.js' import { noNewBlockForOverTwoMins } from '../background/iconHandler.js' import { humanReadableDate } from './ui-utils.js' import { EditEnsLabelHash } from './pages/EditEnsLabelHash.js' +import { ReadonlySignal, Signal, useComputed, useSignal } from '@preact/signals' type ProviderErrorsParam = { tabState: TabState | undefined @@ -40,29 +41,29 @@ function ProviderErrors({ tabState } : ProviderErrorsParam) { } type NetworkErrorParams = { - rpcConnectionStatus: RpcConnectionStatus + rpcConnectionStatus: Signal } export function NetworkErrors({ rpcConnectionStatus } : NetworkErrorParams) { - if (rpcConnectionStatus === undefined) return <> - const nextConnectionAttempt = new Date(rpcConnectionStatus.lastConnnectionAttempt.getTime() + TIME_BETWEEN_BLOCKS * 1000) - if (rpcConnectionStatus.retrying === false) return <> + if (rpcConnectionStatus.value === undefined) return <> + const nextConnectionAttempt = new Date(rpcConnectionStatus.value.lastConnnectionAttempt.getTime() + TIME_BETWEEN_BLOCKS * 1000) + if (rpcConnectionStatus.value.retrying === false) return <> return <> - { rpcConnectionStatus.isConnected === false ? + { rpcConnectionStatus.value.isConnected === false ? Unable to connect to { rpcConnectionStatus.rpcNetwork.name }. Retrying in . + <>Unable to connect to { rpcConnectionStatus.value.rpcNetwork.name }. Retrying in . }/> : <> } - { rpcConnectionStatus.latestBlock !== undefined && noNewBlockForOverTwoMins(rpcConnectionStatus) && rpcConnectionStatus.latestBlock !== null ? + { rpcConnectionStatus.value.latestBlock !== undefined && noNewBlockForOverTwoMins(rpcConnectionStatus.value) && rpcConnectionStatus.value.latestBlock !== null ? The connected RPC ({ rpcConnectionStatus.rpcNetwork.name }) seem to be stuck at block { rpcConnectionStatus.latestBlock.number } (occured on: { humanReadableDate(rpcConnectionStatus.latestBlock.timestamp) }). Retrying in . + <>The connected RPC ({ rpcConnectionStatus.value.rpcNetwork.name }) seem to be stuck at block { rpcConnectionStatus.value.latestBlock.number } (occured on: { humanReadableDate(rpcConnectionStatus.value.latestBlock.timestamp) }). Retrying in . }/> : <> } } export function App() { - const [appPage, setAppPage] = useState({ page: 'Home' }) + const appPage = useSignal({ page: 'Home' }) const [makeMeRich, setMakeMeRich] = useState(false) const [activeAddresses, setActiveAddresses] = useState(defaultActiveAddresses) const [activeSimulationAddress, setActiveSimulationAddress] = useState(undefined) @@ -71,15 +72,15 @@ export function App() { const [simVisResults, setSimVisResults] = useState(undefined) const [websiteAccess, setWebsiteAccess] = useState(undefined) const [websiteAccessAddressMetadata, setWebsiteAccessAddressMetadata] = useState([]) - const [rpcNetwork, setSelectedNetwork] = useState(undefined) + const rpcNetwork = useSignal(undefined) const [simulationMode, setSimulationMode] = useState(true) const [tabIconDetails, setTabConnection] = useState(DEFAULT_TAB_CONNECTION) const [isSettingsLoaded, setIsSettingsLoaded] = useState(false) const [currentBlockNumber, setCurrentBlockNumber] = useState(undefined) const [tabState, setTabState] = useState(undefined) - const [rpcConnectionStatus, setRpcConnectionStatus] = useState(undefined) + const rpcConnectionStatus = useSignal(undefined) const [currentTabId, setCurrentTabId] = useState(undefined) - const [rpcEntries, setRpcEntries] = useState([]) + const rpcEntries = useSignal([]) const [simulationUpdatingState, setSimulationUpdatingState] = useState(undefined) const [simulationResultState, setSimulationResultState] = useState(undefined) const [interceptorDisabled, setInterceptorDisabled] = useState(false) @@ -112,7 +113,7 @@ export function App() { async function setActiveRpcAndInformAboutIt(entry: RpcEntry) { sendPopupMessageToBackgroundPage({ method: 'popup_changeActiveRpc', data: entry }) if(!isSignerConnected()) { - setSelectedNetwork(entry) + rpcNetwork.value = entry } } @@ -145,7 +146,7 @@ export function App() { const updateHomePage = ({ data }: UpdateHomePage) => { if (data.tabId !== currentTabId && currentTabId !== undefined) return setIsSettingsLoaded((isSettingsLoaded) => { - setRpcEntries(data.rpcEntries) + rpcEntries.value = data.rpcEntries setActiveAddresses(data.activeAddresses) setCurrentTabId(data.tabId) setActiveSigningAddress(data.activeSigningAddressInThisTab) @@ -170,14 +171,14 @@ export function App() { setTabState(data.tabState) setCurrentBlockNumber(data.currentBlockNumber) setWebsiteAccessAddressMetadata(data.websiteAccessAddressMetadata) - setRpcConnectionStatus(data.rpcConnectionStatus) + rpcConnectionStatus.value = data.rpcConnectionStatus return true }) } const updateHomePageSettings = (settings: Settings, updateQuery: boolean) => { - if (updateQuery) setAppPage(settings.openedPage) + if (updateQuery) appPage.value = settings.openedPage setSimulationMode(settings.simulationMode) - setSelectedNetwork(settings.currentRpcNetwork) + rpcNetwork.value = settings.currentRpcNetwork setActiveSimulationAddress(settings.activeSimulationAddress) setUseSignersAddressAsActiveAddress(settings.useSignersAddressAsActiveAddress) setWebsiteAccess(settings.websiteAccess) @@ -197,9 +198,13 @@ export function App() { case 'popup_websiteIconChanged': return setTabConnection(parsed.data) case 'popup_new_block_arrived': { await sendPopupMessageToBackgroundPage({ method: 'popup_refreshHomeData' }) - return setRpcConnectionStatus(parsed.data.rpcConnectionStatus) + rpcConnectionStatus.value = parsed.data.rpcConnectionStatus + return + } + case 'popup_failed_to_get_block': { + rpcConnectionStatus.value = parsed.data.rpcConnectionStatus + return } - case 'popup_failed_to_get_block': return setRpcConnectionStatus(parsed.data.rpcConnectionStatus) case 'popup_update_rpc_list': return case 'popup_simulation_state_changed': return await sendPopupMessageToBackgroundPage({ method: 'popup_refreshHomeData' }) } @@ -213,12 +218,12 @@ export function App() { useEffect(() => { sendPopupMessageToBackgroundPage({ method: 'popup_refreshHomeData' }) }, []) function setAndSaveAppPage(page: Page) { - setAppPage(page) + appPage.value = page sendPopupMessageToBackgroundPage({ method: 'popup_changePage', data: page }) } async function addressPaste(address: string) { - if (appPage.page === 'AddNewAddress') return + if (appPage.value.page === 'AddNewAddress') return const trimmed = address.trim() if (!ethers.isAddress(trimmed)) return @@ -247,6 +252,7 @@ export function App() { abi: undefined, useAsActiveAddress: true, declarativeNetRequestBlockMode: undefined, + chainId: rpcConnectionStatus.peek()?.rpcNetwork.chainId || 1n, } } }) } @@ -264,6 +270,7 @@ export function App() { abi: undefined, useAsActiveAddress: false, declarativeNetRequestBlockMode: undefined, + chainId: entry.chainId || 1n, ...entry, address: checksummedAddress(entry.address), } @@ -287,6 +294,7 @@ export function App() { abi: undefined, useAsActiveAddress: true, declarativeNetRequestBlockMode: undefined, + chainId: rpcConnectionStatus.peek()?.rpcNetwork.chainId || 1n, } } }) } @@ -311,12 +319,13 @@ export function App() { setUnexpectedError(undefined) await sendPopupMessageToBackgroundPage({ method: 'popup_clearUnexpectedError' }) } + const modifyAddressSignal: ReadonlySignal = useComputed(() => appPage.value.page === 'AddNewAddress' || appPage.value.page === 'ModifyAddress' ? appPage.value.state : undefined) return (
- -
+ +
{ !isSettingsLoaded ? <> : <>