Page MenuHomePhabricator

D9525.id32156.diff
No OneTemporary

D9525.id32156.diff

diff --git a/lib/types/ethers-types.js b/lib/types/ethers-types.js
--- a/lib/types/ethers-types.js
+++ b/lib/types/ethers-types.js
@@ -4,5 +4,6 @@
+lookupAddress: (address: string) => Promise<?string>,
+resolveName: (name: string) => Promise<?string>,
+getAvatar: (name: string) => Promise<?string>,
+ +getNetwork: () => Promise<{ +chainId: number, ... }>,
...
};
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
@@ -1,7 +1,14 @@
// @flow
import namehash from 'eth-ens-namehash';
-
+import { Contract } from 'ethers';
+import invariant from 'invariant';
+
+import {
+ resolverABI,
+ resolverAddresses,
+ type ReverseRecordsEthersSmartContract,
+} from './reverse-records.js';
import sleep from './sleep.js';
import type { EthersProvider } from '../types/ethers-types.js';
@@ -50,6 +57,9 @@
// vanilla JS class that handles querying and caching ENS for all cases.
class ENSCache {
provider: EthersProvider;
+ batchReverseResolverSmartContract: ?ReverseRecordsEthersSmartContract;
+ batchReverseResolverSmartContractPromise: Promise<ReverseRecordsEthersSmartContract>;
+
// Maps from normalized ETH address to a cache entry for its name
nameQueryCache: Map<string, ENSNameQueryCacheEntry> = new Map();
// Maps from normalized ETH name to a cache entry for its address
@@ -59,6 +69,20 @@
constructor(provider: EthersProvider) {
this.provider = provider;
+ this.batchReverseResolverSmartContractPromise = (async () => {
+ const { chainId } = await provider.getNetwork();
+ const reverseRecordsAddress = resolverAddresses[chainId];
+ invariant(
+ reverseRecordsAddress,
+ `no ReverseRecords smart contract address for chaind ID ${chainId}!`,
+ );
+ this.batchReverseResolverSmartContract = new Contract(
+ reverseRecordsAddress,
+ resolverABI,
+ provider,
+ );
+ return this.batchReverseResolverSmartContract;
+ })();
}
// Getting a name for an ETH address is referred to as "reverse resolution".
@@ -123,6 +147,119 @@
})();
}
+ getNamesForAddresses(
+ ethAddresses: $ReadOnlyArray<string>,
+ ): Promise<Array<?string>> {
+ const normalizedETHAddresses = ethAddresses.map(normalizeETHAddress);
+
+ const cacheMatches = normalizedETHAddresses.map(ethAddress =>
+ this.getCachedNameEntryForAddress(ethAddress),
+ );
+ const cacheResultsPromise = Promise.all(
+ cacheMatches.map(match =>
+ Promise.resolve(match ? match.normalizedENSName : match),
+ ),
+ );
+ if (cacheMatches.every(Boolean)) {
+ return cacheResultsPromise;
+ }
+
+ const needFetch = [];
+ for (let i = 0; i < normalizedETHAddresses.length; i++) {
+ const ethAddress = normalizedETHAddresses[i];
+ const cacheMatch = cacheMatches[i];
+ if (!cacheMatch) {
+ needFetch.push(ethAddress);
+ }
+ }
+
+ const fetchENSNamesPromise = (async () => {
+ const {
+ batchReverseResolverSmartContract,
+ batchReverseResolverSmartContractPromise,
+ } = this;
+
+ let smartContract;
+ if (batchReverseResolverSmartContract) {
+ smartContract = batchReverseResolverSmartContract;
+ } else {
+ smartContract = await batchReverseResolverSmartContractPromise;
+ }
+
+ // ReverseRecords smart contract handles checking forward resolution
+ let ensNames: $ReadOnlyArray<?string>;
+ try {
+ const raceResult = await Promise.race([
+ smartContract['getNames(address[])'](needFetch),
+ throwOnTimeout(`names for ${JSON.stringify(needFetch)}`),
+ ]);
+ invariant(
+ Array.isArray(raceResult),
+ 'ReverseRecords smart contract should return array',
+ );
+ ensNames = raceResult;
+ } catch (e) {
+ console.log(e);
+ ensNames = new Array(needFetch.length).fill(null);
+ }
+
+ const resultMap = new Map();
+ for (let i = 0; i < needFetch.length; i++) {
+ const ethAddress = needFetch[i];
+ let ensName = ensNames[i];
+ if (!ensName || ensName !== normalizeENSName(ensName)) {
+ ensName = undefined;
+ }
+ resultMap.set(ethAddress, ensName);
+ }
+
+ return resultMap;
+ })();
+
+ for (let i = 0; i < needFetch.length; i++) {
+ const normalizedETHAddress = needFetch[i];
+ const fetchENSNamePromise = (async () => {
+ const resultMap = await fetchENSNamesPromise;
+ return resultMap.get(normalizedETHAddress) ?? null;
+ })();
+ this.nameQueryCache.set(normalizedETHAddress, {
+ normalizedETHAddress,
+ expirationTime: Date.now() + queryTimeout * 2,
+ normalizedENSName: fetchENSNamePromise,
+ });
+ }
+
+ return (async () => {
+ const [resultMap, cacheResults] = await Promise.all([
+ fetchENSNamesPromise,
+ cacheResultsPromise,
+ ]);
+ for (let i = 0; i < needFetch.length; i++) {
+ const normalizedETHAddress = needFetch[i];
+ const normalizedENSName = resultMap.get(normalizedETHAddress);
+ const timeout =
+ normalizedENSName === null ? failedQueryCacheTimeout : cacheTimeout;
+ this.nameQueryCache.set(normalizedETHAddress, {
+ normalizedETHAddress,
+ expirationTime: Date.now() + timeout,
+ normalizedENSName,
+ });
+ }
+
+ const results = [];
+ for (let i = 0; i < normalizedETHAddresses.length; i++) {
+ const cachedResult = cacheResults[i];
+ if (cachedResult) {
+ results.push(cachedResult);
+ } else {
+ const normalizedETHAddress = normalizedETHAddresses[i];
+ results.push(resultMap.get(normalizedETHAddress));
+ }
+ }
+ return results;
+ })();
+ }
+
getCachedNameEntryForAddress(ethAddress: string): ?ENSNameQueryCacheEntry {
const normalizedETHAddress = normalizeETHAddress(ethAddress);
diff --git a/lib/utils/ens-cache.test.js b/lib/utils/ens-cache.test.js
--- a/lib/utils/ens-cache.test.js
+++ b/lib/utils/ens-cache.test.js
@@ -43,15 +43,22 @@
const ashoatAddr = '0x911413ef4127910d79303483f7470d095f399ca9';
const ashoatAvatar = 'https://ashoat.com/small_searching.png';
+const commalphaDotEth = 'commalpha.eth';
const commalphaEthAddr = '0x727ad7F5134C03e88087a8019b80388b22aaD24d';
const commalphaEthAvatar =
'https://gateway.ipfs.io/ipfs/Qmb6CCsr5Hvv1DKr9Yt9ucbaK8Fz9MUP1kW9NTqAJhk7o8';
+const commbetaDotEth = 'commbeta.eth';
const commbetaEthAddr = '0x07124c3b6687e78aec8f13a2312cba72a0bed387';
const commbetaEthAvatar =
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgMjAwIDIwMCIgc3R5bGU9ImJhY2tncm91bmQ6ZGFya3Zpb2xldCI+PHBhdGggZD0iTTY4LjQ0IDE0My40NEM2MS44OCAxNDMuNDQgNTYuMDQgMTQxLjg0IDUwLjkyIDEzOC42NEM0NS44IDEzNS4zNiA0MS43NiAxMzAuNjggMzguOCAxMjQuNkMzNS44NCAxMTguNTIgMzQuMzYgMTExLjIgMzQuMzYgMTAyLjY0QzM0LjM2IDk0LjE2IDM1Ljg0IDg2Ljg4IDM4LjggODAuOEM0MS44NCA3NC42NCA0NS45NiA2OS45NiA1MS4xNiA2Ni43NkM1Ni40NCA2My40OCA2Mi40OCA2MS44NCA2OS4yOCA2MS44NEM3NC40OCA2MS44NCA3OC44NCA2Mi44OCA4Mi4zNiA2NC45NkM4NS44OCA2Ni45NiA4OC43NiA2OS4xMiA5MSA3MS40NEw4NS4zNiA3Ny44QzgzLjQ0IDc1LjcyIDgxLjIgNzQgNzguNjQgNzIuNjRDNzYuMTYgNzEuMjggNzMuMDQgNzAuNiA2OS4yOCA3MC42QzY0LjQgNzAuNiA2MC4xMiA3MS45MiA1Ni40NCA3NC41NkM1Mi43NiA3Ny4xMiA0OS44OCA4MC43NiA0Ny44IDg1LjQ4QzQ1LjggOTAuMiA0NC44IDk1Ljg0IDQ0LjggMTAyLjRDNDQuOCAxMDkuMDQgNDUuNzYgMTE0Ljc2IDQ3LjY4IDExOS41NkM0OS42IDEyNC4zNiA1Mi4zNiAxMjguMDggNTUuOTYgMTMwLjcyQzU5LjU2IDEzMy4zNiA2My45MiAxMzQuNjggNjkuMDQgMTM0LjY4QzcxLjc2IDEzNC42OCA3NC4zNiAxMzQuMjggNzYuODQgMTMzLjQ4Qzc5LjMyIDEzMi42IDgxLjI4IDEzMS40NCA4Mi43MiAxMzBWMTA5LjQ4SDY3VjEwMS4ySDkxLjk2VjEzNC4zMkM4OS40OCAxMzYuODggODYuMiAxMzkuMDQgODIuMTIgMTQwLjhDNzguMTIgMTQyLjU2IDczLjU2IDE0My40NCA2OC40NCAxNDMuNDRaTTEzNS45NTMgMTQzLjQ0QzEzMC44MzMgMTQzLjQ0IDEyNi4wNzMgMTQyLjI0IDEyMS42NzMgMTM5Ljg0QzExNy4zNTMgMTM3LjQ0IDExMy44MzMgMTMzLjk2IDExMS4xMTMgMTI5LjRDMTA4LjQ3MyAxMjQuODQgMTA3LjE1MyAxMTkuMzYgMTA3LjE1MyAxMTIuOTZDMTA3LjE1MyAxMDYuNCAxMDguNDczIDEwMC44NCAxMTEuMTEzIDk2LjI4QzExMy44MzMgOTEuNzIgMTE3LjM1MyA4OC4yNCAxMjEuNjczIDg1Ljg0QzEyNi4wNzMgODMuNDQgMTMwLjgzMyA4Mi4yNCAxMzUuOTUzIDgyLjI0QzE0MS4wNzMgODIuMjQgMTQ1Ljc5MyA4My40NCAxNTAuMTEzIDg1Ljg0QzE1NC41MTMgODguMjQgMTU4LjAzMyA5MS43MiAxNjAuNjczIDk2LjI4QzE2My4zOTMgMTAwLjg0IDE2NC43NTMgMTA2LjQgMTY0Ljc1MyAxMTIuOTZDMTY0Ljc1MyAxMTkuMzYgMTYzLjM5MyAxMjQuODQgMTYwLjY3MyAxMjkuNEMxNTguMDMzIDEzMy45NiAxNTQuNTEzIDEzNy40NCAxNTAuMTEzIDEzOS44NEMxNDUuNzkzIDE0Mi4yNCAxNDEuMDczIDE0My40NCAxMzUuOTUzIDE0My40NFpNMTM1Ljk1MyAxMzUuMjhDMTM5LjcxMyAxMzUuMjggMTQyLjk5MyAxMzQuMzYgMTQ1Ljc5MyAxMzIuNTJDMTQ4LjU5MyAxMzAuNiAxNTAuNzUzIDEyNy45NiAxNTIuMjczIDEyNC42QzE1My43OTMgMTIxLjI0IDE1NC41NTMgMTE3LjM2IDE1NC41NTMgMTEyLjk2QzE1NC41NTMgMTA4LjQ4IDE1My43OTMgMTA0LjU2IDE1Mi4yNzMgMTAxLjJDMTUwLjc1MyA5Ny43NiAxNDguNTkzIDk1LjEyIDE0NS43OTMgOTMuMjhDMTQyLjk5MyA5MS4zNiAxMzkuNzEzIDkwLjQgMTM1Ljk1MyA5MC40QzEzMi4xOTMgOTAuNCAxMjguOTEzIDkxLjM2IDEyNi4xMTMgOTMuMjhDMTIzLjM5MyA5NS4xMiAxMjEuMjMzIDk3Ljc2IDExOS42MzMgMTAxLjJDMTE4LjExMyAxMDQuNTYgMTE3LjM1MyAxMDguNDggMTE3LjM1MyAxMTIuOTZDMTE3LjM1MyAxMTcuMzYgMTE4LjExMyAxMjEuMjQgMTE5LjYzMyAxMjQuNkMxMjEuMjMzIDEyNy45NiAxMjMuMzkzIDEzMC42IDEyNi4xMTMgMTMyLjUyQzEyOC45MTMgMTM0LjM2IDEzMi4xOTMgMTM1LjI4IDEzNS45NTMgMTM1LjI4Wk0xMjQuMzEzIDcxLjQ0QzEyMi4zOTMgNzEuNDQgMTIwLjc5MyA3MC44IDExOS41MTMgNjkuNTJDMTE4LjMxMyA2OC4xNiAxMTcuNzEzIDY2LjU2IDExNy43MTMgNjQuNzJDMTE3LjcxMyA2Mi44OCAxMTguMzEzIDYxLjMyIDExOS41MTMgNjAuMDRDMTIwLjc5MyA1OC42OCAxMjIuMzkzIDU4IDEyNC4zMTMgNThDMTI2LjIzMyA1OCAxMjcuNzkzIDU4LjY4IDEyOC45OTMgNjAuMDRDMTMwLjI3MyA2MS4zMiAxMzAuOTEzIDYyLjg4IDEzMC45MTMgNjQuNzJDMTMwLjkxMyA2Ni41NiAxMzAuMjczIDY4LjE2IDEyOC45OTMgNjkuNTJDMTI3Ljc5MyA3MC44IDEyNi4yMzMgNzEuNDQgMTI0LjMxMyA3MS40NFpNMTQ3LjU5MyA3MS40NEMxNDUuNjczIDcxLjQ0IDE0NC4wNzMgNzAuOCAxNDIuNzkzIDY5LjUyQzE0MS41OTMgNjguMTYgMTQwLjk5MyA2Ni41NiAxNDAuOTkzIDY0LjcyQzE0MC45OTMgNjIuODggMTQxLjU5MyA2MS4zMiAxNDIuNzkzIDYwLjA0QzE0NC4wNzMgNTguNjggMTQ1LjY3MyA1OCAxNDcuNTkzIDU4QzE0OS41MTMgNTggMTUxLjA3MyA1OC42OCAxNTIuMjczIDYwLjA0QzE1My41NTMgNjEuMzIgMTU0LjE5MyA2Mi44OCAxNTQuMTkzIDY0LjcyQzE1NC4xOTMgNjYuNTYgMTUzLjU1MyA2OC4xNiAxNTIuMjczIDY5LjUyQzE1MS4wNzMgNzAuOCAxNDkuNTEzIDcxLjQ0IDE0Ny41OTMgNzEuNDRaIiBmaWxsPSJibGFjayIgLz48dGV4dCB4PSIyMCIgeT0iMTgwIiBmaWxsPSJibGFjayI+VG9rZW4gIyAzNjI3PC90ZXh0Pjwvc3ZnPg==';
+const noENSNameAddr = '0xcF986104d869967381dFfAb3A4127bCe6a404362';
+
describe('getNameForAddress', () => {
+ beforeAll(() => {
+ ensCache.clearCache();
+ });
it('should fail to return ashoat.eth if not in cache', async () => {
if (!process.env.ALCHEMY_API_KEY) {
return;
@@ -113,7 +120,94 @@
});
});
+describe('getNamesForAddresses', () => {
+ beforeAll(() => {
+ ensCache.clearCache();
+ });
+ it('should fail to return ashoat.eth if not in cache', async () => {
+ if (!process.env.ALCHEMY_API_KEY) {
+ return;
+ }
+ const ashoatEthResult = ensCache.getCachedNameForAddress(ashoatAddr);
+ expect(ashoatEthResult).toBe(undefined);
+ });
+ it('should return ashoat.eth', async () => {
+ if (!process.env.ALCHEMY_API_KEY) {
+ return;
+ }
+ const [ashoatEthResult] = await ensCache.getNamesForAddresses([ashoatAddr]);
+ expect(ashoatEthResult).toBe(ashoatDotEth);
+ });
+ it('should return ashoat.eth if in cache', async () => {
+ if (!process.env.ALCHEMY_API_KEY) {
+ return;
+ }
+ const ashoatEthResult = ensCache.getCachedNameForAddress(ashoatAddr);
+ expect(ashoatEthResult).toBe(ashoatDotEth);
+ });
+ it('should fetch multiple at a time', async () => {
+ if (!process.env.ALCHEMY_API_KEY) {
+ return;
+ }
+ const [ashoatEthResult, commalphaEthResult, commbetaEthResult] =
+ await ensCache.getNamesForAddresses([
+ ashoatAddr,
+ commalphaEthAddr,
+ commbetaEthAddr,
+ ]);
+ expect(ashoatEthResult).toBe(ashoatDotEth);
+ expect(commalphaEthResult).toBe(commalphaDotEth);
+ expect(commbetaEthResult).toBe(commbetaDotEth);
+ });
+ it('should dedup simultaneous fetches', async () => {
+ if (!process.env.ALCHEMY_API_KEY) {
+ return;
+ }
+
+ ensCache.clearCache();
+ const timesLookupAddressCalledBefore = timesLookupAddressCalled;
+
+ const [
+ [ashoatEthResult1, commalphaEthResult1, commbetaEthResult1],
+ ashoatEthResult2,
+ commalphaEthResult2,
+ commbetaEthResult2,
+ ] = await Promise.all([
+ ensCache.getNamesForAddresses([
+ ashoatAddr,
+ commalphaEthAddr,
+ commbetaEthAddr,
+ ]),
+ ensCache.getNameForAddress(ashoatAddr),
+ ensCache.getNameForAddress(commalphaEthAddr),
+ ensCache.getNameForAddress(commbetaEthAddr),
+ ]);
+
+ const timesLookupAddressCalledAfter = timesLookupAddressCalled;
+ const timesLookupAddressCalledDuringTest =
+ timesLookupAddressCalledAfter - timesLookupAddressCalledBefore;
+ expect(timesLookupAddressCalledDuringTest).toBe(0);
+
+ expect(ashoatEthResult1).toBe(ashoatDotEth);
+ expect(commalphaEthResult1).toBe(commalphaDotEth);
+ expect(commbetaEthResult1).toBe(commbetaDotEth);
+ expect(ashoatEthResult2).toBe(ashoatDotEth);
+ expect(commalphaEthResult2).toBe(commalphaDotEth);
+ expect(commbetaEthResult2).toBe(commbetaDotEth);
+ });
+ it('should return undefined if no ENS name', async () => {
+ if (!process.env.ALCHEMY_API_KEY) {
+ return;
+ }
+ const [noNameResult] = await ensCache.getNamesForAddresses([noENSNameAddr]);
+ expect(noNameResult).toBe(undefined);
+ });
+});
+
describe('getAddressForName', () => {
+ beforeAll(() => {
+ ensCache.clearCache();
+ });
it("should fail to return ashoat.eth's address if not in cache", async () => {
if (!process.env.ALCHEMY_API_KEY) {
return;
@@ -174,6 +268,9 @@
});
describe('getAvatarURIForAddress', () => {
+ beforeAll(() => {
+ ensCache.clearCache();
+ });
it("should fail to return ashoat.eth's avatar if not in cache", async () => {
if (!process.env.ALCHEMY_API_KEY) {
return;
diff --git a/lib/utils/ens-helpers.js b/lib/utils/ens-helpers.js
--- a/lib/utils/ens-helpers.js
+++ b/lib/utils/ens-helpers.js
@@ -42,14 +42,14 @@
const ensNames = new Map();
if (needFetch.length > 0) {
- await Promise.all(
- needFetch.map(async (ethAddress: string) => {
- const ensName = await ensCache.getNameForAddress(ethAddress);
- if (ensName) {
- ensNames.set(ethAddress, ensName);
- }
- }),
- );
+ const results = await ensCache.getNamesForAddresses(needFetch);
+ for (let i = 0; i < needFetch.length; i++) {
+ const ethAddress = needFetch[i];
+ const result = results[i];
+ if (result) {
+ ensNames.set(ethAddress, result);
+ }
+ }
}
return info.map(user => {
diff --git a/lib/utils/reverse-records.js b/lib/utils/reverse-records.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/reverse-records.js
@@ -0,0 +1,45 @@
+// @flow
+
+type ABIParam = {
+ +internalType: string,
+ +name: string,
+ +type: string,
+};
+type EthereumSmartContractABI = $ReadOnlyArray<{
+ +inputs: $ReadOnlyArray<ABIParam>,
+ +stateMutability: string,
+ +type: string,
+ +name?: ?string,
+ +outputs?: ?$ReadOnlyArray<ABIParam>,
+}>;
+
+const resolverABI: EthereumSmartContractABI = [
+ {
+ inputs: [{ internalType: 'contract ENS', name: '_ens', type: 'address' }],
+ stateMutability: 'nonpayable',
+ type: 'constructor',
+ },
+ {
+ inputs: [
+ { internalType: 'address[]', name: 'addresses', type: 'address[]' },
+ ],
+ name: 'getNames',
+ outputs: [{ internalType: 'string[]', name: 'r', type: 'string[]' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+];
+
+const mainnetChainID = 1;
+const goerliChainID = 5;
+const resolverAddresses: { +[chainID: number]: string } = {
+ [mainnetChainID]: '0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C',
+ [goerliChainID]: '0x333Fc8f550043f239a2CF79aEd5e9cF4A20Eb41e',
+};
+
+export type ReverseRecordsEthersSmartContract = {
+ +'getNames(address[])': $ReadOnlyArray<string> => Promise<string[]>,
+ ...
+};
+
+export { resolverABI, resolverAddresses };

File Metadata

Mime Type
text/plain
Expires
Fri, Sep 20, 9:11 PM (21 h, 18 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2154294
Default Alt Text
D9525.id32156.diff (15 KB)

Event Timeline