diff --git a/lib/utils/ens-cache.js b/lib/utils/ens-cache.js --- a/lib/utils/ens-cache.js +++ b/lib/utils/ens-cache.js @@ -16,6 +16,7 @@ export type EthersProvider = { +lookupAddress: (address: string) => Promise, +resolveName: (name: string) => Promise, + +getAvatar: (name: string) => Promise, ... }; type ENSNameQueryCacheEntry = { @@ -30,6 +31,11 @@ +expirationTime: number, +normalizedETHAddress: ?string | Promise, }; +type ENSAvatarQueryCacheEntry = { + +normalizedETHAddress: string, + +expirationTime: number, + +avatarURI: ?string | Promise, +}; const normalizeETHAddress = (ethAddress: string) => ethAddress.toLowerCase(); @@ -49,10 +55,12 @@ // vanilla JS class that handles querying and caching ENS for all cases. class ENSCache { provider: EthersProvider; - // Maps from normalized ETH address to a cache entry for that address + // Maps from normalized ETH address to a cache entry for its name nameQueryCache: Map = new Map(); - // Maps from normalized ETH name to a cache entry for that name + // Maps from normalized ETH name to a cache entry for its address addressQueryCache: Map = new Map(); + // Maps from normalized ETH address to a cache entry for its avatar + avatarQueryCache: Map = new Map(); constructor(provider: EthersProvider) { this.provider = provider; @@ -232,9 +240,92 @@ return normalizedETHAddress; } + getAvatarURIForAddress(ethAddress: string): Promise { + const normalizedETHAddress = normalizeETHAddress(ethAddress); + + const cacheResult = + this.getCachedAvatarEntryForAddress(normalizedETHAddress); + if (cacheResult) { + return Promise.resolve(cacheResult.avatarURI); + } + + const fetchENSAvatarPromise = (async () => { + const ensName = await this.getNameForAddress(normalizedETHAddress); + if (!ensName) { + return ensName; + } + let ensAvatar; + try { + ensAvatar = await Promise.race([ + this.provider.getAvatar(ensName), + throwOnTimeout(`${normalizedETHAddress}'s avatar`), + ]); + } catch (e) { + console.log(e); + return null; + } + if (!ensAvatar) { + return undefined; + } + return ensAvatar; + })(); + + this.avatarQueryCache.set(normalizedETHAddress, { + normalizedETHAddress, + expirationTime: Date.now() + queryTimeout * 4, + avatarURI: fetchENSAvatarPromise, + }); + + return (async () => { + const avatarURI = await fetchENSAvatarPromise; + const timeout = + avatarURI === null ? failedQueryCacheTimeout : cacheTimeout; + this.avatarQueryCache.set(normalizedETHAddress, { + normalizedETHAddress, + expirationTime: Date.now() + timeout, + avatarURI, + }); + return avatarURI; + })(); + } + + getCachedAvatarEntryForAddress( + ethAddress: string, + ): ?ENSAvatarQueryCacheEntry { + const normalizedETHAddress = normalizeETHAddress(ethAddress); + + const cacheResult = this.avatarQueryCache.get(normalizedETHAddress); + if (!cacheResult) { + return undefined; + } + + const { expirationTime } = cacheResult; + if (expirationTime <= Date.now()) { + this.avatarQueryCache.delete(normalizedETHAddress); + return undefined; + } + + return cacheResult; + } + + getCachedAvatarURIForAddress(ethAddress: string): ?string { + const cacheResult = this.getCachedAvatarEntryForAddress(ethAddress); + if (!cacheResult) { + return undefined; + } + + const { avatarURI } = cacheResult; + if (typeof avatarURI !== 'string') { + return undefined; + } + + return avatarURI; + } + clearCache(): void { this.nameQueryCache = new Map(); this.addressQueryCache = new Map(); + this.avatarQueryCache = new Map(); } }