Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F3398867
D9525.id32156.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
15 KB
Referenced Files
None
Subscribers
None
D9525.id32156.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Tue, Dec 3, 12:49 AM (20 h, 51 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2609524
Default Alt Text
D9525.id32156.diff (15 KB)
Attached To
Mode
D9525: [lib] Batch ENS queries using ReverseRecords smart contract
Attached
Detach File
Event Timeline
Log In to Comment