diff --git a/app/css/interceptor.css b/app/css/interceptor.css index f8e4194c..f2df2378 100644 --- a/app/css/interceptor.css +++ b/app/css/interceptor.css @@ -53,6 +53,8 @@ body { font-family: "Inter" !important; } --alpha-005: rgba(255, 255, 255, 0.05); --alpha-015: rgba(255, 255, 255, 0.15); + --big-font-size: 28px; + --normal-font-size: 14px; --modal-background-color: rgba(10, 10, 10, .7) } @@ -756,7 +758,7 @@ svg.spinner > circle { .swap-grid { grid-template-columns: auto auto; display: grid; - place-content: center; + place-content: space-between; column-gap: 0.25em; } @@ -1718,6 +1720,127 @@ header:has(form[role='search']) h1 { } } +.multiline-card { + --bg-color: #484848; + --button-color: #77738ccc; + --image-size: 2.25rem; + --min-text-width: 3ch; + --pad-x: 0; + --pad-y: 0; + --gap-x: 0.5rem; + --edge-roundness: 3px; + + font: inherit; + display: inline-grid; + grid-template-columns: [left] minmax(0, min-content) [data] minmax(0, max-content) [right]; + grid-template-rows: [top] min-content [sub] min-content [bottom]; + column-gap: var(--gap-x); + row-gap: 2px; + padding-block: var(--pad-y); + padding-inline: var(--pad-x); + background-color: var(--bg-color); + border-radius: var(--edge-roundness); + min-width: calc(var(--min-text-width) + var(--image-size) + (var(-pad-x) * 2) + var(--gap-x)); + + data { + line-height: 1em; + color: var(--text-color); + text-align: left; + min-width: var(--min-text-width); + } + + button { + font: inherit; + background: var(--bg-color); + border: 0 none; + padding: 0; + cursor: pointer; + + &:hover, &:focus { background: var(--bg-color) } + } + + > [role=img] { + grid-area: top / left / bottom / data; + align-self: center; + font-size: var(--image-size); + line-height: 1; + + & svg, img { + display: inline-block; + vertical-align: -0.15em; + } + } + + > :has(data ~ button) { + display: inline-grid; + align-items: baseline; + grid-template-columns: [left] minmax(0, min-content) [right]; + grid-template-rows: [top] min-content [bottom]; + + /* title */ + &:nth-of-type(2) { + grid-area: top / data / sub / right; + data { + font-weight: 600; + } + } + + /* subtitle */ + &:nth-of-type(3) { + grid-area: sub / data / bottom / right; + data { + font-size: 0.825em; + color: var(--disabled-text-color); + } + } + + > data { grid-area: top / left / bottom / right } + + > button { + grid-area: top / left / bottom / right; + display: inline-grid; + align-items: baseline; + grid-template-columns: minmax(0, 1fr) min-content; + background-color: var(--bg-color); + opacity: 0; + outline: none; + + &:is(.multiline-card:hover *, .multiline-card:focus-within *) { opacity: 1 } + + &:hover, &:focus { + > span { + background: white; + color: black; + + span { display: inline } + svg { display: none } + } + } + + > span { + background: var(--button-color); + color: white; + font-size: 0.8em; + font-weight: 600; + padding-inline: 0.25em; + border-radius: 2px; + text-transform: uppercase; + line-height: 1.4; + + span { + display: none; + font-size: 0.8em; + } + + svg { + display: inline-block; + vertical-align: -0.125em; + } + } + } + } +} + .tooltip { background: #222222; color: #ffffff; diff --git a/app/ts/AddressBook.tsx b/app/ts/AddressBook.tsx index 1535240c..149b2faf 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 { ChainEntry, RpcEntries } from './types/rpc.js' +import { ChainSelector } from './components/subcomponents/ChainSelector.js' type Modals = { page: 'noModal' } | { page: 'addNewAddress', state: ModifyAddressWindowState } @@ -165,38 +167,70 @@ function AddressBookEntryCard({ removeEntry, renameAddressCallBack, ...entry }: type ViewFilter = { activeFilter: FilterKey searchString: string + chain: ChainEntry | undefined +} + +type AddressBookEntriesWithFilter = { + addressBookEntries: AddressBookEntries + activeFilter: FilterKey } export function AddressBook() { - const addressBookEntries = useSignal([]) - const viewFilter = useSignal({ activeFilter: 'My Active Addresses', searchString: ''}) - const [modalState, setModalState] = useState({ page: 'noModal' }) + const addressBookEntriesWithFilter = useSignal({ addressBookEntries: [], activeFilter: 'My Active Addresses' }) + const addressBookEntries = useComputed(() => addressBookEntriesWithFilter.value.addressBookEntries || []) + const currentChain = useSignal(undefined) + const currentChainId = useComputed(() => currentChain.value?.chainId || 1n) + const rpcEntries = useSignal([]) + const viewFilter = useSignal({ activeFilter: 'My Active Addresses', searchString: '', chain: undefined }) + const modalState = useSignal({ page: 'noModal' }) + const modifyAddressSignal = useComputed(() => modalState.value.page === 'addNewAddress' ? modalState.value.state : undefined) + function sendQuery() { + const filterValue = viewFilter.value + if (filterValue.chain === undefined) return + sendPopupMessageToBackgroundPage({ method: 'popup_getAddressBookData', data: { + chainId: filterValue.chain.chainId, + filter: filterValue.activeFilter, + searchString: filterValue.searchString + } }) + } + useSignalEffect(sendQuery) useEffect(() => { - const popupMessageListener = async (msg: unknown) => { + const popupMessageListener = (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 = currentChain.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 = currentChain.peek() + if (prevCurrentNetwork === undefined || prevCurrentNetwork.chainId === parsed.data.currentRpcNetwork.chainId) { + currentChain.value = parsed.data.currentRpcNetwork + if (prevCurrentNetwork === undefined || viewFilter.value.chain === undefined) { + viewFilter.value = { ...viewFilter.value, chain: currentChain.value === undefined ? undefined : { name: currentChain.value.name, chainId: currentChain.value.chainId } } + } + } } if (parsed.method !== 'popup_getAddressBookDataReply') return const reply = GetAddressBookDataReply.parse(msg) - addressBookEntries.value = reply.data.entries + if (currentChain.peek()?.chainId === reply.data.data.chainId) { + addressBookEntriesWithFilter.value = { + addressBookEntries: reply.data.entries, + activeFilter: reply.data.data.filter, + } + } + return } + sendPopupMessageToBackgroundPage({ method: 'popup_requestSettings' }) browser.runtime.onMessage.addListener(popupMessageListener) - return () => { - browser.runtime.onMessage.removeListener(popupMessageListener) - } + return () => { browser.runtime.onMessage.removeListener(popupMessageListener) } }, []) - function sendQuery(filter: FilterKey, searchString: string | undefined) { - sendPopupMessageToBackgroundPage({ method: 'popup_getAddressBookData', data: { - filter: filter, - searchString: searchString, - } }) - } - function changeFilter(activeFilter: FilterKey) { viewFilter.value = { ...viewFilter.peek(), activeFilter } } @@ -205,10 +239,16 @@ export function AddressBook() { viewFilter.value = { ...viewFilter.peek(), searchString } } - function getNoResultsError() { + function changeCurrentChain(entry: ChainEntry) { + if (entry.chainId === currentChain.peek()?.chainId) return + currentChain.value = entry + viewFilter.value = { ...viewFilter.peek(), chain: { name: entry.name, chainId: entry.chainId } } + } + + 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.chain?.name }` + : `No cute dinosaurs in ${ viewFilter.value.activeFilter } on ${ viewFilter.value.chain?.name }` return
{ errorMessage }
} @@ -225,7 +265,7 @@ export function AddressBook() { } } - return setModalState({ page: 'addNewAddress', state: { + modalState.value = { page: 'addNewAddress', state: { windowStateId: 'AddressBookAdd', errorState: undefined, incompleteAddressBookEntry: { @@ -241,12 +281,14 @@ export function AddressBook() { abi: undefined, useAsActiveAddress: filter === 'My Active Addresses', declarativeNetRequestBlockMode: undefined, + chainId: currentChain.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 +302,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 +314,18 @@ export function AddressBook() { data: { address: entry.address, addressBookCategory: viewFilter.value.activeFilter, + chainId: entry.chainId || 1n, } }) } - - useSignalEffect(() => { - sendQuery(viewFilter.value.activeFilter, viewFilter.value.searchString) - }) - return (
+
+ +
- { addressBookEntries.value.length + { addressBookEntriesWithFilter.value.addressBookEntries.length ? ( - setModalState({ page: 'confirmaddressBookEntryToBeRemoved', addressBookEntry }) } renameAddressCallBack = { renameAddressCallBack } /> + modalState.value = { page: 'confirmaddressBookEntryToBeRemoved', addressBookEntry } } renameAddressCallBack = { renameAddressCallBack } /> ) } /> - : getNoResultsError() + : }
-
- { 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 + 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-startup.ts b/app/ts/background/background-startup.ts index 5cc21cc2..14454fd9 100644 --- a/app/ts/background/background-startup.ts +++ b/app/ts/background/background-startup.ts @@ -2,7 +2,7 @@ import 'webextension-polyfill' import { defaultRpcs, getSettings } from './settings.js' import { handleInterceptedRequest, popupMessageHandler, resetSimulatorStateFromConfig } from './background.js' import { retrieveWebsiteDetails, updateExtensionBadge, updateExtensionIcon } from './iconHandler.js' -import { clearTabStates, getPrimaryRpcForChain, getSimulationResults, removeTabState, setRpcConnectionStatus, updateTabState, updateUserAddressBookEntries } from './storageVariables.js' +import { clearTabStates, getPrimaryRpcForChain, getSimulationResults, removeTabState, setRpcConnectionStatus, updateTabState, updateUserAddressBookEntries, updateUserAddressBookEntriesV2Old } from './storageVariables.js' import { Simulator } from '../simulation/simulator.js' import { TabConnection, TabState, WebsiteTabConnections } from '../types/user-interface-types.js' import { EthereumBlockHeader } from '../types/wire-types.js' @@ -49,7 +49,7 @@ if (browser.runtime.getManifest().manifest_version === 2) { clearTabStates() } -async function migrateAddressInfoAndContacts() { +async function migrateAddressInfoAndContactsFromV1ToV2() { const userAddressBookEntries = (await browserStorageLocalGet(['userAddressBookEntries'])).userAddressBookEntries const convertOldActiveAddressToAddressBookEntry = (entry: AddressBookEntry | OldActiveAddressEntry): AddressBookEntry => { if (entry.type !== 'activeAddress') return entry @@ -58,11 +58,29 @@ async function migrateAddressInfoAndContacts() { if (userAddressBookEntries === undefined) return const updated: AddressBookEntries = userAddressBookEntries.map(convertOldActiveAddressToAddressBookEntry) if (updated.length > 0) { - await updateUserAddressBookEntries((previousEntries) => getUniqueItemsByProperties(updated.concat(previousEntries), ['address'])) + await updateUserAddressBookEntriesV2Old((previousEntries) => getUniqueItemsByProperties(updated.concat(previousEntries), ['address'])) await browserStorageLocalRemove(['userAddressBookEntries']) } } -migrateAddressInfoAndContacts() +async function migrateAddressInfoAndContactsFromV2ToV3() { + const userAddressBookEntries = (await browserStorageLocalGet(['userAddressBookEntriesV2'])).userAddressBookEntriesV2 + const convertOldActiveAddressToAddressBookEntry = (entry: AddressBookEntry): AddressBookEntry => { + if (entry.chainId !== undefined) return entry + if (entry.useAsActiveAddress === true && entry.type === 'contact') return { ...entry, chainId: 'AllChains' } + return { ...entry, chainId: 1n } + } + if (userAddressBookEntries === undefined) return + const updated: AddressBookEntries = userAddressBookEntries.map(convertOldActiveAddressToAddressBookEntry) + if (updated.length > 0) { + await updateUserAddressBookEntries((previousEntries) => getUniqueItemsByProperties(updated.concat(previousEntries), ['address', 'chainId'])) + await browserStorageLocalRemove(['userAddressBookEntriesV2']) + } +} +async function migrateAddressBook() { + await migrateAddressInfoAndContactsFromV1ToV2() + await migrateAddressInfoAndContactsFromV2ToV3() +} +migrateAddressBook() const pendingRequestLimiter = new Semaphore(40) // only allow 40 requests pending globally @@ -176,7 +194,7 @@ async function newBlockAttemptCallback(blockheader: EthereumBlockHeader, ethereu } } -async function onErrorBlockCallback(ethereumClientService: EthereumClientService) { +async function onErrorBlockCallback(ethereumClientService: EthereumClientService, error: unknown) { try { const rpcConnectionStatus = { isConnected: false, @@ -188,6 +206,7 @@ async function onErrorBlockCallback(ethereumClientService: EthereumClientService await setRpcConnectionStatus(rpcConnectionStatus) await updateExtensionBadge() await sendPopupMessageToOpenWindows({ method: 'popup_failed_to_get_block', data: { rpcConnectionStatus } }) + await handleUnexpectedError(error) } catch(error) { await handleUnexpectedError(error) } diff --git a/app/ts/background/background.ts b/app/ts/background/background.ts index 240885ab..72059cd5 100644 --- a/app/ts/background/background.ts +++ b/app/ts/background/background.ts @@ -4,7 +4,7 @@ import { Simulator, parseEvents, parseInputData, runProtectorsForTransaction } f 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, retrieveWebsiteAccess, blockOrAllowExternalRequests, removeWebsiteAccess, allowOrPreventAddressAccessForWebsite, removeWebsiteAddressAccess, forceSetGasLimitForTransaction } from './popupMessageHandlers.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, popupfetchAbiAndNameFromBlockExplorer, openWebPage, disableInterceptor, requestNewHomeData, setEnsNameForHash, simulateGnosisSafeTransactionOnPass, retrieveWebsiteAccess, blockOrAllowExternalRequests, removeWebsiteAccess, allowOrPreventAddressAccessForWebsite, removeWebsiteAddressAccess, forceSetGasLimitForTransaction } from './popupMessageHandlers.js' import { CompleteVisualizedSimulation, SimulationState, VisualizedSimulatorState, WebsiteCreatedEthereumUnsignedTransaction, WebsiteCreatedEthereumUnsignedTransactionOrFailed } from '../types/visualizer-types.js' import { WebsiteTabConnections } from '../types/user-interface-types.js' import { askForSignerAccountsFromSignerIfNotAvailable, interceptorAccessMetadataRefresh, requestAccessFromUser, updateInterceptorAccessViewWithPendingRequests } from './windows/interceptorAccess.js' @@ -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) @@ -720,7 +720,7 @@ export async function popupMessageHandler( case 'popup_simulateGovernanceContractExecution': return await simulateGovernanceContractExecutionOnPass(simulator.ethereum, simulator.tokenPriceService, parsedRequest) case 'popup_simulateGnosisSafeTransaction': return await simulateGnosisSafeTransactionOnPass(simulator.ethereum, simulator.tokenPriceService, parsedRequest.data.gnosisSafeMessage) case 'popup_changeAddOrModifyAddressWindowState': return await changeAddOrModifyAddressWindowState(simulator.ethereum, parsedRequest) - case 'popup_fetchAbiAndNameFromEtherscan': return await popupFetchAbiAndNameFromEtherscan(parsedRequest) + case 'popup_fetchAbiAndNameFromBlockExplorer': return await popupfetchAbiAndNameFromBlockExplorer(parsedRequest) case 'popup_openWebPage': return await openWebPage(parsedRequest) case 'popup_setDisableInterceptor': return await disableInterceptor(simulator, websiteTabConnections, parsedRequest) case 'popup_clearUnexpectedError': return await setLatestUnexpectedError(undefined) diff --git a/app/ts/background/medataSearch.ts b/app/ts/background/medataSearch.ts index b24619b9..13480941 100644 --- a/app/ts/background/medataSearch.ts +++ b/app/ts/background/medataSearch.ts @@ -1,10 +1,10 @@ import { addressString } from '../utils/bigint.js' -import { AddressBookEntries, AddressBookEntry, ContactEntry, ContractEntry, Erc1155Entry, Erc20TokenEntry, Erc721Entry } from '../types/addressBookTypes.js' +import { AddressBookEntries, AddressBookEntry, ChainIdWithUniversal, ContactEntry, ContractEntry, Erc1155Entry, Erc20TokenEntry, Erc721Entry } from '../types/addressBookTypes.js' import { tokenMetadata, contractMetadata, ContractDefinition, TokenDefinition, Erc721Definition, erc721Metadata, erc1155Metadata, Erc1155Definition } from '@darkflorist/address-metadata' 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, getUserAddressBookEntriesForChainIdMorePreciseFirst } 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: ChainIdWithUniversal): 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,34 +142,34 @@ 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: ChainIdWithUniversal): Promise { const lowerCasedName = name?.toLowerCase() const lowerCasedSymbol = symbol?.toLowerCase() const lowerCasedEqual = (nonLowerCased: string, lowerCased: string | undefined) => nonLowerCased.toLowerCase() === lowerCased + if (chainId === 'AllChains' || chainId === 1n) { + const tokenMetadataEntry = Array.from(tokenMetadata).find((entry) => lowerCasedEqual(entry[1].symbol, lowerCasedSymbol) || lowerCasedEqual(entry[1].name, lowerCasedName)) + if (tokenMetadataEntry !== undefined) return convertTokenDefinitionToAddressBookEntry(tokenMetadataEntry) - const tokenMetadataEntry = Array.from(tokenMetadata).find((entry) => lowerCasedEqual(entry[1].symbol, lowerCasedSymbol) || lowerCasedEqual(entry[1].name, lowerCasedName)) - if (tokenMetadataEntry !== undefined) return convertTokenDefinitionToAddressBookEntry(tokenMetadataEntry) + const erc721MetadataEntry = Array.from(erc721Metadata).find((entry) => lowerCasedEqual(entry[1].symbol, lowerCasedSymbol) || lowerCasedEqual(entry[1].name.toLowerCase(), lowerCasedName)) + if (erc721MetadataEntry !== undefined) return convertErc721DefinitionToAddressBookEntry(erc721MetadataEntry) - const erc721MetadataEntry = Array.from(erc721Metadata).find((entry) => lowerCasedEqual(entry[1].symbol, lowerCasedSymbol) || lowerCasedEqual(entry[1].name.toLowerCase(), lowerCasedName)) - if (erc721MetadataEntry !== undefined) return convertErc721DefinitionToAddressBookEntry(erc721MetadataEntry) + const erc1155MetadataEntry = Array.from(erc1155Metadata).find((entry) => lowerCasedEqual(entry[1].symbol, lowerCasedSymbol) || lowerCasedEqual(entry[1].name.toLowerCase(), lowerCasedName)) + if (erc1155MetadataEntry !== undefined) return convertErc1155DefinitionToAddressBookEntry(erc1155MetadataEntry) - const erc1155MetadataEntry = Array.from(erc1155Metadata).find((entry) => lowerCasedEqual(entry[1].symbol, lowerCasedSymbol) || lowerCasedEqual(entry[1].name.toLowerCase(), lowerCasedName)) - if (erc1155MetadataEntry !== undefined) return convertErc1155DefinitionToAddressBookEntry(erc1155MetadataEntry) - - const contractMetadataEntry = Array.from(contractMetadata).find((entry) => lowerCasedEqual(entry[1].name, lowerCasedName)) - if (contractMetadataEntry !== undefined) return convertContractDefinitionToAddressBookEntry(contractMetadataEntry) + 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 getUserAddressBookEntriesForChainIdMorePreciseFirst(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..6bc633bf 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, getUserAddressBookEntriesForChainIdMorePreciseFirst } 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 getUserAddressBookEntriesForChainIdMorePreciseFirst(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..522bd007 100644 --- a/app/ts/background/popupMessageHandlers.ts +++ b/app/ts/background/popupMessageHandlers.ts @@ -2,7 +2,7 @@ import { changeActiveAddressAndChainAndResetSimulation, changeActiveRpc, refresh import { getSettings, setUseTabsInsteadOfPopup, setMakeMeRich, setPage, setUseSignersAddressAsActiveAddress, updateWebsiteAccess, exportSettingsAndAddressBook, importSettingsAndAddressBook, getMakeMeRich, getUseTabsInsteadOfPopup, getMetamaskCompatibilityMode, setMetamaskCompatibilityMode, getPage } from './settings.js' import { getPendingTransactionsAndMessages, getCurrentTabId, getTabState, saveCurrentTabId, setRpcList, getRpcList, getPrimaryRpcForChain, getRpcConnectionStatus, updateUserAddressBookEntries, getSimulationResults, setIdsOfOpenedTabs, getIdsOfOpenedTabs, updatePendingTransactionOrMessage, getLatestUnexpectedError, addEnsLabelHash, addEnsNodeHash, updateTransactionStack } from './storageVariables.js' import { Simulator, parseEvents, parseInputData } from '../simulation/simulator.js' -import { ChangeActiveAddress, ChangeMakeMeRich, ChangePage, RemoveTransaction, RequestAccountsFromSigner, TransactionConfirmation, InterceptorAccess, ChangeInterceptorAccess, ChainChangeConfirmation, EnableSimulationMode, ChangeActiveChain, AddOrEditAddressBookEntry, GetAddressBookData, RemoveAddressBookEntry, InterceptorAccessRefresh, InterceptorAccessChangeAddress, Settings, ChangeSettings, ImportSettings, SetRpcList, UpdateHomePage, SimulateGovernanceContractExecution, ChangeAddOrModifyAddressWindowState, FetchAbiAndNameFromEtherscan, OpenWebPage, DisableInterceptor, SetEnsNameForHash, UpdateConfirmTransactionDialog, UpdateConfirmTransactionDialogPendingTransactions, SimulateExecutionReply, BlockOrAllowExternalRequests, RemoveWebsiteAccess, AllowOrPreventAddressAccessForWebsite, RemoveWebsiteAddressAccess, ForceSetGasLimitForTransaction } from '../types/interceptor-messages.js' +import { ChangeActiveAddress, ChangeMakeMeRich, ChangePage, RemoveTransaction, RequestAccountsFromSigner, TransactionConfirmation, InterceptorAccess, ChangeInterceptorAccess, ChainChangeConfirmation, EnableSimulationMode, ChangeActiveChain, AddOrEditAddressBookEntry, GetAddressBookData, RemoveAddressBookEntry, InterceptorAccessRefresh, InterceptorAccessChangeAddress, Settings, ChangeSettings, ImportSettings, SetRpcList, UpdateHomePage, SimulateGovernanceContractExecution, ChangeAddOrModifyAddressWindowState, FetchAbiAndNameFromBlockExplorer, OpenWebPage, DisableInterceptor, SetEnsNameForHash, UpdateConfirmTransactionDialog, UpdateConfirmTransactionDialogPendingTransactions, SimulateExecutionReply, BlockOrAllowExternalRequests, RemoveWebsiteAccess, AllowOrPreventAddressAccessForWebsite, RemoveWebsiteAddressAccess, ForceSetGasLimitForTransaction } from '../types/interceptor-messages.js' import { formEthSendTransaction, formSendRawTransaction, resolvePendingTransactionOrMessage, updateConfirmTransactionView, setGasLimitForTransaction } from './windows/confirmTransaction.js' import { getAddressMetadataForAccess, requestAddressChange, resolveInterceptorAccess } from './windows/interceptorAccess.js' import { resolveChainChange } from './windows/changeChain.js' @@ -18,7 +18,7 @@ import { ExportedSettings } from '../types/exportedSettingsTypes.js' import { isJSON } from '../utils/json.js' import { IncompleteAddressBookEntry } from '../types/addressBookTypes.js' import { EthereumAddress, serialize } from '../types/wire-types.js' -import { fetchAbiFromEtherscan, isValidAbi } from '../simulation/services/EtherScanAbiFetcher.js' +import { fetchAbiFromBlockExplorer, isValidAbi } from '../simulation/services/EtherScanAbiFetcher.js' import { stringToAddress } from '../utils/bigint.js' import { ethers } from 'ethers' import { getIssueWithAddressString } from '../components/ui-utils.js' @@ -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 }".` } @@ -520,11 +522,11 @@ export async function changeAddOrModifyAddressWindowState(ethereum: EthereumClie }) } -export async function popupFetchAbiAndNameFromEtherscan(parsedRequest: FetchAbiAndNameFromEtherscan) { - const etherscanReply = await fetchAbiFromEtherscan(parsedRequest.data.address) +export async function popupfetchAbiAndNameFromBlockExplorer(parsedRequest: FetchAbiAndNameFromBlockExplorer) { + const etherscanReply = await fetchAbiFromBlockExplorer(parsedRequest.data.address, parsedRequest.data.chainId) if (etherscanReply.success) { return await sendPopupMessageToOpenWindows({ - method: 'popup_fetchAbiAndNameFromEtherscanReply' as const, + method: 'popup_fetchAbiAndNameFromBlockExplorerReply' as const, data: { windowStateId: parsedRequest.data.windowStateId, success: true, @@ -535,7 +537,7 @@ export async function popupFetchAbiAndNameFromEtherscan(parsedRequest: FetchAbiA }) } return await sendPopupMessageToOpenWindows({ - method: 'popup_fetchAbiAndNameFromEtherscanReply' as const, + method: 'popup_fetchAbiAndNameFromBlockExplorerReply' as const, data: { windowStateId: parsedRequest.data.windowStateId, success: false, diff --git a/app/ts/background/settings.ts b/app/ts/background/settings.ts index 8cbac975..770f095e 100644 --- a/app/ts/background/settings.ts +++ b/app/ts/background/settings.ts @@ -4,7 +4,7 @@ import { Settings } from '../types/interceptor-messages.js' 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 { BlockExplorer, RpcNetwork } from '../types/rpc.js' import { browserStorageLocalGet, browserStorageLocalSafeParseGet, browserStorageLocalSet } from '../utils/storageUtils.js' import { getUserAddressBookEntries, updateUserAddressBookEntries } from './storageVariables.js' import { getUniqueItemsByProperties } from '../utils/typed-arrays.js' @@ -18,6 +18,7 @@ export const defaultActiveAddresses: AddressBookEntries = [ address: 0xd8da6bf26964af9d7eed9e03e53415d37aa96045n, askForAddressAccess: false, useAsActiveAddress: true, + chainId: 'AllChains', }, { type: 'contact' as const, @@ -26,6 +27,7 @@ export const defaultActiveAddresses: AddressBookEntries = [ address: MOCK_PRIVATE_KEYS_ADDRESS, askForAddressAccess: false, useAsActiveAddress: true, + chainId: 'AllChains', } ] @@ -89,6 +91,19 @@ const wethForChainId = new Map([ ['42161', 0x82af49447d8a07e3bd95bd0d56f35241523fbab1n], // Arbitrum ]) +const defaultBlockExplorer = new Map([ + ['1', { apiUrl: 'https://api.etherscan.io/api', apiKey: 'PSW8C433Q667DVEX5BCRMGNAH9FSGFZ7Q8' } ], + ['17000', { apiUrl: 'https://api-holesky.etherscan.io/api', apiKey: 'PSW8C433Q667DVEX5BCRMGNAH9FSGFZ7Q8' }], + ['11155111', { apiUrl: 'https://api-sepolia.etherscan.io/api', apiKey: 'PSW8C433Q667DVEX5BCRMGNAH9FSGFZ7Q8' }], + ['10', { apiUrl: 'https://api-optimistic.etherscan.io/api', apiKey: '4E726IGJ2FAU4IDHZ1TJF5HA9JZ1YKRFK9' }], + ['420', { apiUrl: 'https://api-goerli-optimistic.etherscan.io/api', apiKey: '4E726IGJ2FAU4IDHZ1TJF5HA9JZ1YKRFK9' }], + ['8453', { apiUrl: 'https://api.basescan.org/api', apiKey: 'HHH4UCPI43IYIJGP9MV16Q5REIRSDTAACA' }], + ['84532', { apiUrl: 'https://api-sepolia.basescan.org/api', apiKey: 'HHH4UCPI43IYIJGP9MV16Q5REIRSDTAACA' }], + ['42161', { apiUrl: 'https://api.arbiscan.io/api', apiKey: 'DDP8M43XJYSRBMB8RJGTJ2CW3M8K73CIY6' }], +]) + +export const getDefaultBlockExplorer = (chainId: bigint): BlockExplorer | undefined => defaultBlockExplorer.get(chainId.toString()) + export const getWethForChainId = (chainId: bigint) => wethForChainId.get(chainId.toString()) export async function getSettings() : Promise { @@ -202,7 +217,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 964bdbe9..130118da 100644 --- a/app/ts/background/storageVariables.ts +++ b/app/ts/background/storageVariables.ts @@ -5,7 +5,7 @@ import { PartialIdsOfOpenedTabs, TabStateItems, browserStorageLocalGet, browserS import { CompleteVisualizedSimulation, EthereumSubscriptionsAndFilters, TransactionStack } from '../types/visualizer-types.js' import { defaultRpcs } from './settings.js' import { UniqueRequestIdentifier, doesUniqueRequestIdentifiersMatch } from '../utils/requests.js' -import { AddressBookEntries, AddressBookEntry } from '../types/addressBookTypes.js' +import { AddressBookEntries, AddressBookEntry, ChainIdWithUniversal } from '../types/addressBookTypes.js' import { SignerName } from '../types/signerTypes.js' import { PendingAccessRequests, PendingTransactionOrSignableMessage } from '../types/accessRequest.js' import { RpcEntries, RpcNetwork } from '../types/rpc.js' @@ -244,12 +244,30 @@ export const getRpcNetworkForChain = async (chainId: bigint): Promise (await browserStorageLocalGet('userAddressBookEntriesV2'))?.userAddressBookEntriesV2 ?? [] +export const getUserAddressBookEntries = async () => (await browserStorageLocalGet('userAddressBookEntriesV3'))?.userAddressBookEntriesV3 ?? [] +export const getUserAddressBookEntriesForChainId = async (chainId: ChainIdWithUniversal) => (await getUserAddressBookEntries()).filter((entry) => entry.chainId === chainId || (entry.chainId === undefined && chainId === 1n) || entry.chainId === 'AllChains') +export const getUserAddressBookEntriesForChainIdMorePreciseFirst = async (chainId: ChainIdWithUniversal) => { + const entries = (await getUserAddressBookEntries()).filter((entry) => entry.chainId === chainId || (entry.chainId === undefined && chainId === 1n) || entry.chainId === 'AllChains') + // sort more precise entries first (one with accurate chain id) + entries.sort((x, y) => { + if (typeof x.chainId === 'bigint' && typeof y.chainId !== 'bigint') return -1 + if (typeof x.chainId !== 'bigint' && typeof y.chainId === 'bigint') return 1 + return 0 + }) + return entries +} const userAddressBookEntriesSemaphore = new Semaphore(1) export async function updateUserAddressBookEntries(updateFunc: (prevState: AddressBookEntries) => AddressBookEntries) { await userAddressBookEntriesSemaphore.execute(async () => { const entries = await getUserAddressBookEntries() + return await browserStorageLocalSet({ userAddressBookEntriesV3: updateFunc(entries) }) + }) +} + +export async function updateUserAddressBookEntriesV2Old(updateFunc: (prevState: AddressBookEntries) => AddressBookEntries) { + await userAddressBookEntriesSemaphore.execute(async () => { + const entries = (await browserStorageLocalGet('userAddressBookEntriesV2'))?.userAddressBookEntriesV2 ?? [] return await browserStorageLocalSet({ userAddressBookEntriesV2: updateFunc(entries) }) }) } @@ -257,9 +275,9 @@ 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) }) + return await browserStorageLocalSet({ userAddressBookEntriesV3: entries.concat(newEntry) }) }) } 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 ? <> : <>