diff --git a/background/main.ts b/background/main.ts index 259a0c8cd1..f373040cc4 100644 --- a/background/main.ts +++ b/background/main.ts @@ -47,6 +47,7 @@ import { deleteAccount, loadAccount, updateAccountBalance, + updateAccountLocalName, updateAccountName, updateENSAvatar, } from "./redux-slices/accounts" @@ -959,6 +960,19 @@ export default class Main extends BaseService { this.store.dispatch(updateAccountName({ ...addressOnNetwork, name })) } ) + this.nameService.emitter.on( + "resolvedLocalName", + async ({ + from: { + addressOnNetwork: { address }, // ignore network + }, + resolved: { + nameOnNetwork: { name }, + }, + }) => { + this.store.dispatch(updateAccountLocalName({ address, name })) + } + ) this.nameService.emitter.on( "resolvedAvatar", async ({ from: { addressOnNetwork }, resolved: { avatar } }) => { diff --git a/background/redux-slices/accounts.ts b/background/redux-slices/accounts.ts index 329451038a..718997b0d8 100644 --- a/background/redux-slices/accounts.ts +++ b/background/redux-slices/accounts.ts @@ -16,7 +16,7 @@ import { import { DomainName, HexString, URI } from "../types" import { normalizeEVMAddress } from "../lib/utils" import { AccountSigner } from "../services/signing" -import { TEST_NETWORK_BY_CHAIN_ID } from "../constants" +import { NETWORK_BY_CHAIN_ID, TEST_NETWORK_BY_CHAIN_ID } from "../constants" import { convertFixedPoint } from "../lib/fixed-point" /** @@ -350,8 +350,6 @@ const accountSlice = createSlice({ immerState.accountsData.evm[network.chainID] ??= {} const baseAccountData = getOrCreateAccountData( - // TODO Figure out the best way to handle default name assignment - // TODO across networks. immerState, normalizedAddress, network @@ -362,6 +360,35 @@ const accountSlice = createSlice({ ens: { ...baseAccountData.ens, name }, } }, + updateAccountLocalName: ( + immerState, + { + payload: { address, name }, + }: { payload: { address: string; name: DomainName } } + ) => { + const normalizedAddress = normalizeEVMAddress(address) + + Object.keys(immerState.accountsData.evm).forEach((chainID) => { + if ( + immerState.accountsData.evm[chainID]?.[normalizedAddress] === + undefined + ) { + return + } + + const network = NETWORK_BY_CHAIN_ID[chainID] + const baseAccountData = getOrCreateAccountData( + immerState, + normalizedAddress, + network + ) + + immerState.accountsData.evm[chainID][normalizedAddress] = { + ...baseAccountData, + ens: { ...baseAccountData.ens, name }, + } + }) + }, updateENSAvatar: ( immerState, { @@ -402,6 +429,7 @@ export const { loadAccount, updateAccountBalance, updateAccountName, + updateAccountLocalName, updateENSAvatar, } = accountSlice.actions diff --git a/background/services/name/index.ts b/background/services/name/index.ts index 20f726278b..b6a6207fb8 100644 --- a/background/services/name/index.ts +++ b/background/services/name/index.ts @@ -57,6 +57,7 @@ type ResolvedAvatarRecord = { type Events = ServiceLifecycleEvents & { resolvedAddress: ResolvedAddressRecord resolvedName: ResolvedNameRecord + resolvedLocalName: ResolvedNameRecord resolvedAvatar: ResolvedAvatarRecord } @@ -130,7 +131,6 @@ export default class NameService extends BaseService { preferenceService.emitter.on( "addressBookEntryModified", async ({ network, address }) => { - this.clearNameCacheEntry(network.chainID, address) await this.lookUpName({ network, address }) } ) @@ -214,6 +214,52 @@ export default class NameService extends BaseService { ): Promise { const { address, network } = normalizeAddressOnNetwork(addressOnNetwork) + const workingResolvers = this.resolvers.filter((resolver) => + resolver.canAttemptNameResolution({ address, network }) + ) + + // check local address book + const localResolvers = [...workingResolvers].filter( + (resolver) => + resolver.type === "tally-address-book" || + resolver.type === "tally-known-contracts" + ) + + const localResolution = ( + await Promise.allSettled( + localResolvers.map(async (resolver) => ({ + type: resolver.type, + resolved: await resolver.lookUpNameForAddress({ address, network }), + })) + ) + ) + .filter(isFulfilledPromise) + .find(({ value: { resolved } }) => resolved !== undefined)?.value + + if ( + typeof localResolution !== "undefined" && + typeof localResolution.resolved !== "undefined" + ) { + const { type: system, resolved: nameOnNetwork } = localResolution + + const nameRecord = { + from: { addressOnNetwork: { address, network } }, + resolved: { + nameOnNetwork, + // TODO Read this from the name service; for now, this avoids infinite + // TODO resolution loops. + expiresAt: Date.now() + MINIMUM_RECORD_EXPIRY, + }, + system, + } as const + + // Emit local names without a network and update all address-network pairs in Redux + this.emitter.emit("resolvedLocalName", nameRecord) + + return nameRecord + } + + // If there is no local name then look deeper if (!this.cachedResolvedNames[network.family][network.chainID]) { this.cachedResolvedNames[network.family][network.chainID] = {} } @@ -231,24 +277,15 @@ export default class NameService extends BaseService { } } - const workingResolvers = this.resolvers.filter((resolver) => - resolver.canAttemptNameResolution({ address, network }) - ) - - const localResolvers = [...workingResolvers].filter( - (resolver) => - resolver.type === "tally-address-book" || - resolver.type === "tally-known-contracts" - ) const remoteResolvers = [...workingResolvers].filter( (resolver) => resolver.type !== "tally-address-book" && resolver.type !== "tally-known-contracts" ) - let firstMatchingResolution = ( + const remoteResolution = ( await Promise.allSettled( - localResolvers.map(async (resolver) => ({ + remoteResolvers.map(async (resolver) => ({ type: resolver.type, resolved: await resolver.lookUpNameForAddress({ address, network }), })) @@ -257,28 +294,14 @@ export default class NameService extends BaseService { .filter(isFulfilledPromise) .find(({ value: { resolved } }) => resolved !== undefined)?.value - if (!firstMatchingResolution) { - firstMatchingResolution = ( - await Promise.allSettled( - remoteResolvers.map(async (resolver) => ({ - type: resolver.type, - resolved: await resolver.lookUpNameForAddress({ address, network }), - })) - ) - ) - .filter(isFulfilledPromise) - .find(({ value: { resolved } }) => resolved !== undefined)?.value - } - if ( - firstMatchingResolution === undefined || - firstMatchingResolution.resolved === undefined + remoteResolution === undefined || + remoteResolution.resolved === undefined ) { return undefined } - const { type: resolverType, resolved: nameOnNetwork } = - firstMatchingResolution + const { type: system, resolved: nameOnNetwork } = remoteResolution const nameRecord = { from: { addressOnNetwork: { address, network } }, @@ -288,7 +311,7 @@ export default class NameService extends BaseService { // TODO resolution loops. expiresAt: Date.now() + MINIMUM_RECORD_EXPIRY, }, - system: resolverType, + system, } as const const cachedNameOnNetwork = cachedResolvedNameRecord?.resolved.nameOnNetwork @@ -304,12 +327,6 @@ export default class NameService extends BaseService { return nameRecord } - clearNameCacheEntry(chainId: string, address: HexString): void { - if (this.cachedResolvedNames.EVM[chainId]?.[address] !== undefined) { - delete this.cachedResolvedNames.EVM[chainId][address] - } - } - async lookUpAvatar( addressOnNetwork: AddressOnNetwork ): Promise { diff --git a/background/services/preferences/index.ts b/background/services/preferences/index.ts index f3a0d8eaad..e497639ace 100644 --- a/background/services/preferences/index.ts +++ b/background/services/preferences/index.ts @@ -16,17 +16,17 @@ import { HexString } from "../../types" import { AccountSignerSettings } from "../../ui" import { AccountSignerWithId } from "../../signing" -type AddressBookEntry = { +type ContractAddressBookEntry = { network: EVMNetwork address: HexString name: string } -type InMemoryAddressBook = AddressBookEntry[] - -const sameAddressBookEntry = (a: AddressOnNetwork, b: AddressOnNetwork) => - normalizeEVMAddress(a.address) === normalizeEVMAddress(b.address) && - sameNetwork(a.network, b.network) +type CustomAddressBookEntry = { + // no network to make custom names global + address: HexString + name: string +} const BUILT_IN_CONTRACTS = [ { @@ -96,8 +96,8 @@ interface Events extends ServiceLifecycleEvents { preferencesChanges: Preferences initializeDefaultWallet: boolean initializeSelectedAccount: AddressOnNetwork + addressBookEntryModified: AddressOnNetwork & { name: string } updateAnalyticsPreferences: AnalyticsPreferences - addressBookEntryModified: AddressBookEntry updatedSignerSettings: AccountSignerSettings[] } @@ -106,9 +106,9 @@ interface Events extends ServiceLifecycleEvents { * event when preferences change. */ export default class PreferenceService extends BaseService { - private knownContracts: InMemoryAddressBook = BUILT_IN_CONTRACTS + private knownContracts: ContractAddressBookEntry[] = BUILT_IN_CONTRACTS - private addressBook: InMemoryAddressBook = [] + private addressBook: CustomAddressBookEntry[] = [] /* * Create a new PreferenceService. The service isn't initialized until @@ -148,38 +148,51 @@ export default class PreferenceService extends BaseService { // TODO Implement the following 6 methods as something stored in the database and user-manageable. // TODO Track account names in the UI in the address book. - addOrEditNameInAddressBook(newEntry: AddressBookEntry): void { - const correspondingEntryIndex = this.addressBook.findIndex((entry) => - sameAddressBookEntry(newEntry, entry) + addOrEditNameInAddressBook({ + address, + network, + name, + }: { + network: EVMNetwork + address: HexString + name: string + }): void { + const newEntry = { + address: normalizeEVMAddress(address), + name, + } + const correspondingEntryIndex = this.addressBook.findIndex( + (entry) => + normalizeEVMAddress(newEntry.address) === + normalizeEVMAddress(entry.address) ) if (correspondingEntryIndex !== -1) { this.addressBook[correspondingEntryIndex] = newEntry } else { - this.addressBook.push({ - network: newEntry.network, - name: newEntry.name, - address: normalizeEVMAddress(newEntry.address), - }) + this.addressBook.push(newEntry) } - this.emitter.emit("addressBookEntryModified", newEntry) + this.emitter.emit("addressBookEntryModified", { ...newEntry, network }) } lookUpAddressForName({ name, network, }: NameOnNetwork): AddressOnNetwork | undefined { - return this.addressBook.find( - ({ name: entryName, network: entryNetwork }) => - sameNetwork(network, entryNetwork) && name === entryName + const entry = this.addressBook.find( + ({ name: entryName }) => name === entryName ) + return entry ? { address: entry.address, network } : undefined } - lookUpNameForAddress( - addressOnNetwork: AddressOnNetwork - ): NameOnNetwork | undefined { - return this.addressBook.find((addressBookEntry) => - sameAddressBookEntry(addressBookEntry, addressOnNetwork) + lookUpNameForAddress({ + address, + network, + }: AddressOnNetwork): NameOnNetwork | undefined { + const entry = this.addressBook.find( + ({ address: entryAddress }) => + normalizeEVMAddress(entryAddress) === normalizeEVMAddress(address) ) + return entry ? { name: entry.name, network } : undefined } async lookUpAddressForContractName({ diff --git a/ui/components/AccountItem/AccountItemEditName.tsx b/ui/components/AccountItem/AccountItemEditName.tsx index f5ff47299c..dc75f8d69d 100644 --- a/ui/components/AccountItem/AccountItemEditName.tsx +++ b/ui/components/AccountItem/AccountItemEditName.tsx @@ -126,8 +126,6 @@ export default function AccountItemEditName({ margin-top: 0px; } .details { - display: flex; - flex-direction: column; line-height: 24px; font-size: 16px; margin-top: 21px;