Page MenuHomePhabricator

D6264.diff
No OneTemporary

D6264.diff

diff --git a/.github/workflows/eslint_flow_jest.yml b/.github/workflows/eslint_flow_jest.yml
--- a/.github/workflows/eslint_flow_jest.yml
+++ b/.github/workflows/eslint_flow_jest.yml
@@ -47,6 +47,8 @@
- name: '[lib] test'
working-directory: ./lib
+ env:
+ ALCHEMY_API_KEY: ${{secrets.ALCHEMY_API_KEY}}
run: yarn test
- name: '[keyserver] test'
diff --git a/lib/package.json b/lib/package.json
--- a/lib/package.json
+++ b/lib/package.json
@@ -27,6 +27,8 @@
"dependencies": {
"dateformat": "^3.0.3",
"emoji-regex": "^9.2.0",
+ "eth-ens-namehash": "^2.0.8",
+ "ethers": "^5.7.2",
"fast-json-stable-stringify": "^2.0.0",
"file-type": "^12.3.0",
"invariant": "^2.2.4",
diff --git a/lib/utils/ens-cache.js b/lib/utils/ens-cache.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/ens-cache.js
@@ -0,0 +1,81 @@
+// @flow
+
+import namehash from 'eth-ens-namehash';
+
+const cacheTimeout = 24 * 60 * 60 * 1000; // one day
+
+type EthersProvider = {
+ +lookupAddress: (address: string) => Promise<?string>,
+ ...
+};
+type ENSNameQueryCacheEntry = {
+ // We normalize ETH addresses to lowercase characters
+ +normalizedETHAddress: string,
+ +cacheInsertionTime: number,
+ // We normalize ENS names using eth-ens-namehash
+ +normalizedENSName: ?string,
+};
+
+// We have a need for querying ENS names from both clients as well as from
+// keyserver code. On the client side, we could use wagmi's caching behavior,
+// but that doesn't work for keyserver since it's React-specific. To keep
+// caching behavior consistent across platforms, we instead introduce this
+// 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
+ nameQueryCache: Map<string, ENSNameQueryCacheEntry> = new Map();
+
+ constructor(provider: EthersProvider) {
+ this.provider = provider;
+ }
+
+ // Getting a name for an ETH address is referred to as "reverse resolution".
+ // 1. Since any address can set a reverse resolution to an arbitrary ENS name
+ // (without permission from the owner), this function will also perform a
+ // "forward resolution" to confirm that the owner of the ENS name has
+ // mapped it to this address.
+ // 2. We only consider an ENS name valid if it's equal to its normalized
+ // version via eth-ens-namehash. This is to protect against homograph
+ // attacks. See https://docs.ens.domains/dapp-developer-guide/resolving-names#reverse-resolution
+ // If we fail to find an ENS name for an address, fail to confirm a matching
+ // forward resolution, or if the ENS name does not equal its normalized
+ // version, we will return undefined.
+ async getNameForAddress(ethAddress: string): Promise<?string> {
+ const normalizedETHAddress = ethAddress.toLowerCase();
+
+ const cacheResult = this.nameQueryCache.get(normalizedETHAddress);
+ if (cacheResult) {
+ const { cacheInsertionTime, normalizedENSName } = cacheResult;
+ if (cacheInsertionTime + cacheTimeout > Date.now()) {
+ return normalizedENSName;
+ } else {
+ this.nameQueryCache.delete(normalizedETHAddress);
+ }
+ }
+
+ const cacheAndReturnResult = (result: ?string) => {
+ this.nameQueryCache.set(normalizedETHAddress, {
+ normalizedETHAddress,
+ cacheInsertionTime: Date.now(),
+ normalizedENSName: result,
+ });
+ return result;
+ };
+
+ // ethers.js handles checking forward resolution (point 1 above) for us
+ const ensName = await this.provider.lookupAddress(normalizedETHAddress);
+ if (!ensName) {
+ return cacheAndReturnResult(undefined);
+ }
+
+ const normalizedENSName = namehash.normalize(ensName);
+ if (normalizedENSName !== ensName) {
+ return cacheAndReturnResult(undefined);
+ }
+
+ return cacheAndReturnResult(normalizedENSName);
+ }
+}
+
+export { ENSCache };
diff --git a/lib/utils/ens-cache.test.js b/lib/utils/ens-cache.test.js
new file mode 100644
--- /dev/null
+++ b/lib/utils/ens-cache.test.js
@@ -0,0 +1,49 @@
+// @flow
+
+import { ethers } from 'ethers';
+
+import { ENSCache } from './ens-cache';
+
+const provider = new ethers.providers.AlchemyProvider(
+ 'mainnet',
+ process.env.ALCHEMY_API_KEY,
+);
+const ensCache = new ENSCache(provider);
+
+const baseLookupAddress = provider.lookupAddress.bind(provider);
+let timesLookupAddressCalled = 0;
+provider.lookupAddress = (ethAddress: string) => {
+ timesLookupAddressCalled++;
+ return baseLookupAddress(ethAddress);
+};
+
+if (!process.env.ALCHEMY_API_KEY) {
+ // Test only works if we can query blockchain
+ console.log(
+ 'skipped running ENSCache tests because of missing ALCHEMY_API_KEY ' +
+ 'environmental variable',
+ );
+}
+
+describe('getNameForAddress', () => {
+ it('should return ashoat.eth', async () => {
+ if (!process.env.ALCHEMY_API_KEY) {
+ return;
+ }
+ const obviouslyAshoatEth = await ensCache.getNameForAddress(
+ '0x911413ef4127910d79303483f7470d095f399ca9',
+ );
+ expect(obviouslyAshoatEth).toBe('ashoat.eth');
+ });
+ it('should have ashoat.eth cached', async () => {
+ if (!process.env.ALCHEMY_API_KEY) {
+ return;
+ }
+ const timesLookupAddressCalledBefore = timesLookupAddressCalled;
+ const obviouslyAshoatEth = await ensCache.getNameForAddress(
+ '0x911413ef4127910d79303483f7470d095f399ca9',
+ );
+ expect(obviouslyAshoatEth).toBe('ashoat.eth');
+ expect(timesLookupAddressCalled).toBe(timesLookupAddressCalledBefore);
+ });
+});
diff --git a/yarn.lock b/yarn.lock
--- a/yarn.lock
+++ b/yarn.lock
@@ -10373,6 +10373,14 @@
pify "^3.0.0"
safe-event-emitter "^1.0.1"
+eth-ens-namehash@^2.0.8:
+ version "2.0.8"
+ resolved "https://registry.yarnpkg.com/eth-ens-namehash/-/eth-ens-namehash-2.0.8.tgz#229ac46eca86d52e0c991e7cb2aef83ff0f68bcf"
+ integrity sha512-VWEI1+KJfz4Km//dadyvBBoBeSQ0MHTXPvr8UIXiLW6IanxvAV+DmlZAijZwAyggqGUfwQBeHf7tc9wzc1piSw==
+ dependencies:
+ idna-uts46-hx "^2.3.1"
+ js-sha3 "^0.5.7"
+
eth-json-rpc-filters@4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/eth-json-rpc-filters/-/eth-json-rpc-filters-4.2.2.tgz#eb35e1dfe9357ace8a8908e7daee80b2cd60a10d"
@@ -12749,6 +12757,13 @@
dependencies:
harmony-reflect "^1.4.6"
+idna-uts46-hx@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/idna-uts46-hx/-/idna-uts46-hx-2.3.1.tgz#a1dc5c4df37eee522bf66d969cc980e00e8711f9"
+ integrity sha512-PWoF9Keq6laYdIRwwCdhTPl60xRqAloYNMQLiyUnG42VjT53oW07BXIRM+NK7eQjzXjAk2gUvX9caRxlnF9TAA==
+ dependencies:
+ punycode "2.1.0"
+
ieee754@^1.1.13, ieee754@^1.1.4, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -14288,6 +14303,11 @@
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==
+js-sha3@^0.5.7:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.7.tgz#0d4ffd8002d5333aabaf4a23eed2f6374c9f28e7"
+ integrity sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g==
+
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -18423,6 +18443,11 @@
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
+punycode@2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
+ integrity sha512-Yxz2kRwT90aPiWEMHVYnEf4+rhwF1tBmmZ4KepCP+Wkium9JxtWnUm1nqGwpiAHr/tnTSeHqr3wb++jgSkXjhA==
+
punycode@^1.2.4:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"

File Metadata

Mime Type
text/plain
Expires
Fri, Dec 20, 12:28 PM (15 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2677730
Default Alt Text
D6264.diff (8 KB)

Event Timeline