diff --git a/keyserver/package.json b/keyserver/package.json --- a/keyserver/package.json +++ b/keyserver/package.json @@ -89,6 +89,7 @@ "tcomb": "^3.2.29", "twin-bcrypt": "^2.1.1", "uuid": "^3.4.0", + "viem": "^2.9.5", "web": "0.0.1", "web-push": "^3.5.0", "ws": "^8.13.0" diff --git a/keyserver/src/utils/ens-cache.js b/keyserver/src/utils/ens-cache.js --- a/keyserver/src/utils/ens-cache.js +++ b/keyserver/src/utils/ens-cache.js @@ -8,6 +8,8 @@ getENSNames as baseGetENSNames, type GetENSNames, } from 'lib/utils/ens-helpers.js'; +import { ENSWrapper } from 'lib/utils/ens-wrapper.js'; +import { getAlchemyMainnetViemClientWithENSContracts } from 'lib/utils/viem-utils.js'; type AlchemyConfig = { +key: string }; type BaseUserInfo = { +username?: ?string, ... }; @@ -22,8 +24,10 @@ if (!alchemyKey) { return; } - const provider = new AlchemyProvider('mainnet', alchemyKey); - const ensCache = new ENSCache(provider); + const viemClient = getAlchemyMainnetViemClientWithENSContracts(alchemyKey); + const ethersProvider = new AlchemyProvider('mainnet', alchemyKey); + const ensWrapper = new ENSWrapper(viemClient, ethersProvider); + const ensCache = new ENSCache(ensWrapper); getENSNames = (users: $ReadOnlyArray): Promise => baseGetENSNames(ensCache, users); } diff --git a/lib/components/ens-cache-provider.react.js b/lib/components/ens-cache-provider.react.js --- a/lib/components/ens-cache-provider.react.js +++ b/lib/components/ens-cache-provider.react.js @@ -8,6 +8,8 @@ getENSNames as baseGetENSNames, type GetENSNames, } from '../utils/ens-helpers.js'; +import { ENSWrapper } from '../utils/ens-wrapper.js'; +import { getAlchemyMainnetViemClientWithENSContracts } from '../utils/viem-utils.js'; type BaseUserInfo = { +username?: ?string, ... }; @@ -23,21 +25,24 @@ React.createContext(defaultContext); type Props = { - +provider: ?EthersProvider, + +ethersProvider: ?EthersProvider, + +alchemyKey: ?string, +children: React.Node, }; function ENSCacheProvider(props: Props): React.Node { - const { provider, children } = props; + const { ethersProvider, alchemyKey, children } = props; const context = React.useMemo(() => { - if (!provider) { + if (!ethersProvider) { return defaultContext; } - const ensCache = new ENSCache(provider); + const viemClient = getAlchemyMainnetViemClientWithENSContracts(alchemyKey); + const ensWrapper = new ENSWrapper(viemClient, ethersProvider); + const ensCache = new ENSCache(ensWrapper); const getENSNames: GetENSNames = ( users: $ReadOnlyArray, ): Promise => baseGetENSNames(ensCache, users); return { ensCache, getENSNames }; - }, [provider]); + }, [ethersProvider, alchemyKey]); return ( {children} diff --git a/lib/flow-typed/npm/@ensdomains/ensjs_vx.x.x.js b/lib/flow-typed/npm/@ensdomains/ensjs_vx.x.x.js new file mode 100644 --- /dev/null +++ b/lib/flow-typed/npm/@ensdomains/ensjs_vx.x.x.js @@ -0,0 +1,62 @@ +// flow-typed signature: 3c7fa5a7cb8e52f2d81c14972c6d689f +// flow-typed version: <>/@ensdomains/ensjs_v4.0.1/flow_v0.202.1 + +declare module '@ensdomains/ensjs' { + import type { ViemChain } from 'viem'; + + declare export function addEnsContracts(chain: ViemChain): ViemChain; +} + +declare module '@ensdomains/ensjs/public' { + import type { ViemClient } from 'viem'; + + declare export type BatchHandle = { + ... + }; + + declare export type GetNameResult = { + +match: boolean, + +name: ?string, + +reverseResolverAddress: ?string, + +resolverAddress: ?string, + ... + }; + declare export var getName: + & { + +batch: ({ +address: string, ... }) => BatchHandle, + ... + } + & (ViemClient, { +address: string, ... }) => Promise; + + declare export type GetOwnerResult = { + +owner: ?string, + +registrant: ?string, + +ownershipLevel: ?string, + ... + }; + declare export var getOwner: + & { + +batch: ({ +name: string, ... }) => BatchHandle, + ... + } + & (ViemClient, { +name: string, ... }) => Promise; + + declare export type GetRecordsResult = { + +texts: $ReadOnlyArray<{ + +key: string, + +value: string, + }>, + ... + }; + declare export var getRecords: + & { + +batch: ({ +name: string, +texts: $ReadOnlyArray, ... }) => BatchHandle, + ... + } + & (ViemClient, { +name: string, +texts: $ReadOnlyArray, ... }) => Promise; + + declare export function batch( + client: ViemClient, + ...batchCalls: $ReadOnlyArray> + ): Promise>; +} diff --git a/lib/flow-typed/npm/viem_vx.x.x.js b/lib/flow-typed/npm/viem_vx.x.x.js new file mode 100644 --- /dev/null +++ b/lib/flow-typed/npm/viem_vx.x.x.js @@ -0,0 +1,32 @@ +// flow-typed signature: 585e2883477434dc7cfd534337e85dce +// flow-typed version: <>/viem_v2.9.5/flow_v0.202.1 + +declare module 'viem' { + + declare export type ViemTransport = { ... }; + + declare export function http(url?: string): ViemTransport; + + declare export type ViemChain = { ... }; + + declare export type ViemCreateClientParams = { + +chain: ViemChain, + +transport: ViemTransport, + ... + }; + + declare export type ViemClient = { + ... + }; + + declare export function createClient( + params: ViemCreateClientParams, + ): ViemClient; +} + +declare module 'view/chains' { + import type { ViemChain } from 'viem'; + + declare export var mainnet: ViemChain; + declare export var sepolia: ViemChain; +} diff --git a/lib/package.json b/lib/package.json --- a/lib/package.json +++ b/lib/package.json @@ -36,6 +36,7 @@ }, "dependencies": { "@commapp/olm": "0.2.1", + "@ensdomains/ensjs": "^4.0.1", "@rainbow-me/rainbowkit": "^2.0.7", "base-64": "^0.1.0", "dateformat": "^3.0.3", 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,20 +1,13 @@ // @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 { ENSWrapper } from './ens-wrapper.js'; import sleep from './sleep.js'; -import type { EthersProvider } from '../types/ethers-types.js'; const cacheTimeout = 24 * 60 * 60 * 1000; // one day const failedQueryCacheTimeout = 5 * 60 * 1000; // five minutes -const queryTimeout = 10 * 1000; // ten seconds +const queryTimeout = 30 * 1000; // ten seconds async function throwOnTimeout(identifier: string) { await sleep(queryTimeout); @@ -56,9 +49,7 @@ // 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; - batchReverseResolverSmartContract: ?ReverseRecordsEthersSmartContract; - batchReverseResolverSmartContractPromise: Promise; + ensWrapper: ENSWrapper; // Maps from normalized ETH address to a cache entry for its name nameQueryCache: Map = new Map(); @@ -67,22 +58,8 @@ // Maps from normalized ETH address to a cache entry for its avatar avatarQueryCache: Map = new Map(); - constructor(provider: EthersProvider) { - this.provider = provider; - this.batchReverseResolverSmartContractPromise = (async () => { - const { chainId } = await provider.getNetwork(); - const reverseRecordsAddress = resolverAddresses[chainId]; - if (reverseRecordsAddress) { - this.batchReverseResolverSmartContract = new Contract( - reverseRecordsAddress, - resolverABI, - provider, - ); - } else { - this.batchReverseResolverSmartContract = null; - } - return this.batchReverseResolverSmartContract; - })(); + constructor(ensWrapper: ENSWrapper) { + this.ensWrapper = ensWrapper; } // Getting a name for an ETH address is referred to as "reverse resolution". @@ -109,7 +86,7 @@ let ensName; try { ensName = await Promise.race([ - this.provider.lookupAddress(normalizedETHAddress), + this.ensWrapper.getNameForAddress(normalizedETHAddress), throwOnTimeout(`${normalizedETHAddress}'s name`), ]); } catch (e) { @@ -174,39 +151,16 @@ } const fetchENSNamesPromise = (async () => { - const { - batchReverseResolverSmartContract, - batchReverseResolverSmartContractPromise, - } = this; - - let smartContract; - if (batchReverseResolverSmartContract) { - smartContract = batchReverseResolverSmartContract; - } else if (batchReverseResolverSmartContract !== null) { - smartContract = await batchReverseResolverSmartContractPromise; - } - - // ReverseRecords smart contract handles checking forward resolution let ensNames: $ReadOnlyArray; - if (smartContract) { - 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); - } - } else { - ensNames = await Promise.all( - needFetch.map(ethAddress => this.getNameForAddress(ethAddress)), - ); + try { + const raceResult = await Promise.race([ + this.ensWrapper.getNamesForAddresses(needFetch), + throwOnTimeout(`names for ${JSON.stringify(needFetch)}`), + ]); + ensNames = raceResult; + } catch (e) { + console.log(e); + ensNames = new Array(needFetch.length).fill(null); } const resultMap = new Map(); @@ -315,7 +269,7 @@ let ethAddress; try { ethAddress = await Promise.race([ - this.provider.resolveName(normalizedENSName), + this.ensWrapper.getAddressForName(normalizedENSName), throwOnTimeout(`${normalizedENSName}'s address`), ]); } catch (e) { @@ -398,7 +352,7 @@ let ensAvatar; try { ensAvatar = await Promise.race([ - this.provider.getAvatar(ensName), + this.ensWrapper.getAvatarURIForName(ensName), throwOnTimeout(`${normalizedETHAddress}'s avatar`), ]); } catch (e) { 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 @@ -1,32 +1,74 @@ // @flow +import { addEnsContracts } from '@ensdomains/ensjs'; import { AlchemyProvider } from 'ethers'; +import { createClient } from 'viem'; +// eslint-disable-next-line import/extensions +import { mainnet, sepolia } from 'viem/chains'; import { ENSCache } from './ens-cache.js'; +import { ENSWrapper } from './ens-wrapper.js'; +import { + getAlchemyMainnetViemTransport, + getAlchemySepoliaViemTransport, +} from './viem-utils.js'; -const provider = new AlchemyProvider('sepolia', 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); -}; - -const baseResolveName = provider.resolveName.bind(provider); -let timesResolveNameCalled = 0; -provider.resolveName = (ensName: string) => { - timesResolveNameCalled++; - return baseResolveName(ensName); -}; - -const baseGetAvatar = provider.getAvatar.bind(provider); -let timesGetAvatarCalled = 0; -provider.getAvatar = (ethAddress: string) => { - timesGetAvatarCalled++; - return baseGetAvatar(ethAddress); -}; +const sepoliaEthersProvider = new AlchemyProvider( + 'sepolia', + process.env.ALCHEMY_API_KEY, +); + +const sepoliaViemTransport = getAlchemySepoliaViemTransport( + process.env.ALCHEMY_API_KEY, +); +const sepoliaViemClient = createClient({ + chain: addEnsContracts(sepolia), + transport: sepoliaViemTransport, +}); + +const baseSepoliaENSWrapper = new ENSWrapper( + sepoliaViemClient, + sepoliaEthersProvider, +); + +let timesGetAddressForNameCalled = 0; +let timesGetNameForAddressCalled = 0; +let timesGetAvatarURIForNameCalled = 0; +const sepoliaENSWrapper: ENSWrapper = ({ + ...baseSepoliaENSWrapper, + getAddressForName: (ethAddress: string) => { + timesGetAddressForNameCalled++; + return baseSepoliaENSWrapper.getAddressForName(ethAddress); + }, + getNameForAddress: (ensName: string) => { + timesGetNameForAddressCalled++; + return baseSepoliaENSWrapper.getNameForAddress(ensName); + }, + getAvatarURIForName: (ensName: string) => { + timesGetAvatarURIForNameCalled++; + return baseSepoliaENSWrapper.getAvatarURIForName(ensName); + }, +}: any); +const ensCache = new ENSCache(sepoliaENSWrapper); + +const mainnetEthersProvider = new AlchemyProvider( + 'mainnet', + process.env.ALCHEMY_API_KEY, +); + +const mainnetViemTransport = getAlchemyMainnetViemTransport( + process.env.ALCHEMY_API_KEY, +); +const mainnetViemClient = createClient({ + chain: addEnsContracts(mainnet), + transport: mainnetViemTransport, +}); + +const mainnetENSWrapper = new ENSWrapper( + mainnetViemClient, + mainnetEthersProvider, +); +const mainnetENSCache = new ENSCache(mainnetENSWrapper); if (!process.env.ALCHEMY_API_KEY) { // Test only works if we can query blockchain @@ -52,9 +94,13 @@ const noENSNameAddr = '0xcF986104d869967381dFfAb3A4127bCe6a404362'; +const nfthreatBaseName = 'nfthreat.base.eth'; +const nfthreatBaseAddr = '0x598C91d70e16177defB34CbA95C2f80F551A4ccB'; + describe('getNameForAddress', () => { beforeAll(() => { ensCache.clearCache(); + mainnetENSCache.clearCache(); }); it('should fail to return ashoat.eth if not in cache', async () => { if (!process.env.ALCHEMY_API_KEY) { @@ -70,6 +116,14 @@ const ashoatEthResult = await ensCache.getNameForAddress(ashoatAddr); expect(ashoatEthResult).toBe(ashoatDotEth); }); + it('should return nfthreat.base.eth', async () => { + if (!process.env.ALCHEMY_API_KEY) { + return; + } + const nfthreatBaseResult = + await mainnetENSCache.getNameForAddress(nfthreatBaseAddr); + expect(nfthreatBaseResult).toBe(nfthreatBaseName); + }, 10000); it('should return ashoat.eth if in cache', async () => { if (!process.env.ALCHEMY_API_KEY) { return; @@ -81,12 +135,12 @@ if (!process.env.ALCHEMY_API_KEY) { return; } - const timesLookupAddressCalledBefore = timesLookupAddressCalled; + const timesLookupAddressCalledBefore = timesGetAddressForNameCalled; const ashoatEthResult = await ensCache.getNameForAddress( ashoatAddr.toUpperCase(), ); expect(ashoatEthResult).toBe(ashoatDotEth); - expect(timesLookupAddressCalled).toBe(timesLookupAddressCalledBefore); + expect(timesGetAddressForNameCalled).toBe(timesLookupAddressCalledBefore); }); it('should dedup simultaneous fetches', async () => { if (!process.env.ALCHEMY_API_KEY) { @@ -94,14 +148,16 @@ } ensCache.clearCache(); - const timesLookupAddressCalledBeforeSingleFetch = timesLookupAddressCalled; + const timesLookupAddressCalledBeforeSingleFetch = + timesGetAddressForNameCalled; const ashoatEthResult1 = await ensCache.getNameForAddress(ashoatAddr); expect(ashoatEthResult1).toBe(ashoatDotEth); const timesLookupAddressCalledForSingleFetch = - timesLookupAddressCalled - timesLookupAddressCalledBeforeSingleFetch; + timesGetAddressForNameCalled - timesLookupAddressCalledBeforeSingleFetch; ensCache.clearCache(); - const timesLookupAddressCalledBeforeDoubleFetch = timesLookupAddressCalled; + const timesLookupAddressCalledBeforeDoubleFetch = + timesGetAddressForNameCalled; const [ashoatEthResult2, ashoatEthResult3] = await Promise.all([ ensCache.getNameForAddress(ashoatAddr), ensCache.getNameForAddress(ashoatAddr.toUpperCase()), @@ -109,7 +165,7 @@ expect(ashoatEthResult2).toBe(ashoatDotEth); expect(ashoatEthResult3).toBe(ashoatDotEth); const timesLookupAddressCalledForDoubleFetch = - timesLookupAddressCalled - timesLookupAddressCalledBeforeDoubleFetch; + timesGetAddressForNameCalled - timesLookupAddressCalledBeforeDoubleFetch; expect(timesLookupAddressCalledForDoubleFetch).toBe( timesLookupAddressCalledForSingleFetch, @@ -120,6 +176,7 @@ describe('getNamesForAddresses', () => { beforeAll(() => { ensCache.clearCache(); + mainnetENSCache.clearCache(); }); it('should fail to return ashoat.eth if not in cache', async () => { if (!process.env.ALCHEMY_API_KEY) { @@ -135,6 +192,15 @@ const [ashoatEthResult] = await ensCache.getNamesForAddresses([ashoatAddr]); expect(ashoatEthResult).toBe(ashoatDotEth); }); + it('should return nfthreat.base.eth', async () => { + if (!process.env.ALCHEMY_API_KEY) { + return; + } + const [nfthreatBaseResult] = await mainnetENSCache.getNamesForAddresses([ + nfthreatBaseAddr, + ]); + expect(nfthreatBaseResult).toBe(nfthreatBaseName); + }, 10000); it('should return ashoat.eth if in cache', async () => { if (!process.env.ALCHEMY_API_KEY) { return; @@ -162,7 +228,7 @@ } ensCache.clearCache(); - const timesLookupAddressCalledBefore = timesLookupAddressCalled; + const timesLookupAddressCalledBefore = timesGetAddressForNameCalled; const [ [ashoatEthResult1, commalphaEthResult1, commbetaEthResult1], @@ -180,17 +246,11 @@ ensCache.getNameForAddress(commbetaEthAddr), ]); - const timesLookupAddressCalledAfter = timesLookupAddressCalled; + const timesLookupAddressCalledAfter = timesGetAddressForNameCalled; const timesLookupAddressCalledDuringTest = timesLookupAddressCalledAfter - timesLookupAddressCalledBefore; - // These tests are run on the Sepolia testnet, where the ReverseRecords - // smart contract is not deployed. As a result, we end up needing to call - // the lookupAddress method (single lookup) once for each address. On - // mainnet (outside of these tests) this is 0, since the ReverseRecords - // smart contract lets us batch up our requests, and avoid calling - // lookupAddress entirely. - expect(timesLookupAddressCalledDuringTest).toBe(3); + expect(timesLookupAddressCalledDuringTest).toBe(0); expect(ashoatEthResult1).toBe(ashoatDotEth); expect(commalphaEthResult1).toBe(commalphaDotEth); @@ -211,6 +271,7 @@ describe('getAddressForName', () => { beforeAll(() => { ensCache.clearCache(); + mainnetENSCache.clearCache(); }); it("should fail to return ashoat.eth's address if not in cache", async () => { if (!process.env.ALCHEMY_API_KEY) { @@ -237,10 +298,10 @@ if (!process.env.ALCHEMY_API_KEY) { return; } - const timesResolveNameCalledBefore = timesResolveNameCalled; + const timesResolveNameCalledBefore = timesGetNameForAddressCalled; const ashoatAddrResult = await ensCache.getAddressForName(ashoatDotEth); expect(ashoatAddrResult).toBe(ashoatAddr); - expect(timesResolveNameCalled).toBe(timesResolveNameCalledBefore); + expect(timesGetNameForAddressCalled).toBe(timesResolveNameCalledBefore); }); it('should dedup simultaneous fetches', async () => { if (!process.env.ALCHEMY_API_KEY) { @@ -248,14 +309,16 @@ } ensCache.clearCache(); - const timesResolveNameCalledBeforeSingleFetch = timesResolveNameCalled; + const timesResolveNameCalledBeforeSingleFetch = + timesGetNameForAddressCalled; const ashoatAddrResult1 = await ensCache.getAddressForName(ashoatDotEth); expect(ashoatAddrResult1).toBe(ashoatAddr); const timesResolveNameCalledForSingleFetch = - timesResolveNameCalled - timesResolveNameCalledBeforeSingleFetch; + timesGetNameForAddressCalled - timesResolveNameCalledBeforeSingleFetch; ensCache.clearCache(); - const timesResolveNameCalledBeforeDoubleFetch = timesResolveNameCalled; + const timesResolveNameCalledBeforeDoubleFetch = + timesGetNameForAddressCalled; const [ashoatAddrResult2, ashoatAddrResult3] = await Promise.all([ ensCache.getAddressForName(ashoatDotEth), ensCache.getAddressForName(ashoatDotEth), @@ -263,7 +326,7 @@ expect(ashoatAddrResult2).toBe(ashoatAddr); expect(ashoatAddrResult3).toBe(ashoatAddr); const timesResolveNamesCalledForDoubleFetch = - timesResolveNameCalled - timesResolveNameCalledBeforeDoubleFetch; + timesGetNameForAddressCalled - timesResolveNameCalledBeforeDoubleFetch; expect(timesResolveNamesCalledForDoubleFetch).toBe( timesResolveNameCalledForSingleFetch, @@ -303,11 +366,11 @@ if (!process.env.ALCHEMY_API_KEY) { return; } - const timesGetAvatarCalledBefore = timesGetAvatarCalled; + const timesGetAvatarCalledBefore = timesGetAvatarURIForNameCalled; const ashoatAvatarResult = await ensCache.getAvatarURIForAddress(ashoatAddr); expect(ashoatAvatarResult).toBe(ashoatAvatar); - expect(timesGetAvatarCalled).toBe(timesGetAvatarCalledBefore); + expect(timesGetAvatarURIForNameCalled).toBe(timesGetAvatarCalledBefore); }); it('should dedup simultaneous fetches', async () => { if (!process.env.ALCHEMY_API_KEY) { @@ -315,15 +378,17 @@ } ensCache.clearCache(); - const timesGetAvatarCalledBeforeSingleFetch = timesGetAvatarCalled; + const timesGetAvatarCalledBeforeSingleFetch = + timesGetAvatarURIForNameCalled; const ashoatAvatarResult1 = await ensCache.getAvatarURIForAddress(ashoatAddr); expect(ashoatAvatarResult1).toBe(ashoatAvatar); const timesGetAvatarCalledForSingleFetch = - timesGetAvatarCalled - timesGetAvatarCalledBeforeSingleFetch; + timesGetAvatarURIForNameCalled - timesGetAvatarCalledBeforeSingleFetch; ensCache.clearCache(); - const timesGetAvatarCalledBeforeDoubleFetch = timesGetAvatarCalled; + const timesGetAvatarCalledBeforeDoubleFetch = + timesGetAvatarURIForNameCalled; const [ashoatAvatarResult2, ashoatAvatarResult3] = await Promise.all([ ensCache.getAvatarURIForAddress(ashoatAddr), ensCache.getAvatarURIForAddress(ashoatAddr), @@ -331,7 +396,7 @@ expect(ashoatAvatarResult2).toBe(ashoatAvatar); expect(ashoatAvatarResult3).toBe(ashoatAvatar); const timesGetAvatarCalledForDoubleFetch = - timesGetAvatarCalled - timesGetAvatarCalledBeforeDoubleFetch; + timesGetAvatarURIForNameCalled - timesGetAvatarCalledBeforeDoubleFetch; expect(timesGetAvatarCalledForDoubleFetch).toBe( timesGetAvatarCalledForSingleFetch, diff --git a/lib/utils/ens-wrapper.js b/lib/utils/ens-wrapper.js new file mode 100644 --- /dev/null +++ b/lib/utils/ens-wrapper.js @@ -0,0 +1,49 @@ +// @flow + +import { + batch, + getName, + type GetNameResult, + getOwner, + // eslint-disable-next-line import/extensions +} from '@ensdomains/ensjs/public'; +import type { ViemClient } from 'viem'; + +import type { EthersProvider } from '../types/ethers-types.js'; + +class ENSWrapper { + viemClient: ViemClient; + ethersProvider: EthersProvider; + + constructor(viemClient: ViemClient, ethersProvider: EthersProvider) { + this.viemClient = viemClient; + this.ethersProvider = ethersProvider; + } + + getNameForAddress: string => Promise = async ethAddress => { + const result = await getName(this.viemClient, { address: ethAddress }); + return result ? result.name : undefined; + }; + + getNamesForAddresses: ($ReadOnlyArray) => Promise> = + async ethAddresses => { + const results = await batch( + this.viemClient, + ...ethAddresses.map(address => getName.batch({ address })), + ); + return results.map(result => (result ? result.name : undefined)); + }; + + getAddressForName: string => Promise = async ensName => { + const result = await getOwner(this.viemClient, { name: ensName }); + return result ? result.owner : undefined; + }; + + // @ensdomains/ensjs doesn't handle resolving ipfs and eip155 URIs to HTTP + // URIs, so we use Ethers.js instead + getAvatarURIForName: string => Promise = async ensName => { + return await this.ethersProvider.getAvatar(ensName); + }; +} + +export { ENSWrapper }; diff --git a/lib/utils/reverse-records.js b/lib/utils/reverse-records.js deleted file mode 100644 --- a/lib/utils/reverse-records.js +++ /dev/null @@ -1,45 +0,0 @@ -// @flow - -type ABIParam = { - +internalType: string, - +name: string, - +type: string, -}; -type EthereumSmartContractABI = $ReadOnlyArray<{ - +inputs: $ReadOnlyArray, - +stateMutability: string, - +type: string, - +name?: ?string, - +outputs?: ?$ReadOnlyArray, -}>; - -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) => Promise, - ... -}; - -export { resolverABI, resolverAddresses }; diff --git a/lib/utils/viem-utils.js b/lib/utils/viem-utils.js new file mode 100644 --- /dev/null +++ b/lib/utils/viem-utils.js @@ -0,0 +1,32 @@ +// @flow + +import { addEnsContracts } from '@ensdomains/ensjs'; +import { createClient, http, type ViemTransport, type ViemClient } from 'viem'; +// eslint-disable-next-line import/extensions +import { mainnet } from 'viem/chains'; + +function getAlchemyMainnetViemTransport(alchemyKey: ?string): ViemTransport { + const key = alchemyKey ?? 'demo'; + return http(`https://eth-mainnet.g.alchemy.com/v2/${key}`); +} + +function getAlchemySepoliaViemTransport(alchemyKey: ?string): ViemTransport { + const key = alchemyKey ?? 'demo'; + return http(`https://eth-sepolia.g.alchemy.com/v2/${key}`); +} + +function getAlchemyMainnetViemClientWithENSContracts( + alchemyKey: ?string, +): ViemClient { + const viemTransport = getAlchemyMainnetViemTransport(alchemyKey); + return createClient({ + chain: addEnsContracts(mainnet), + transport: viemTransport, + }); +} + +export { + getAlchemyMainnetViemTransport, + getAlchemySepoliaViemTransport, + getAlchemyMainnetViemClientWithENSContracts, +}; diff --git a/lib/utils/wagmi-utils.js b/lib/utils/wagmi-utils.js --- a/lib/utils/wagmi-utils.js +++ b/lib/utils/wagmi-utils.js @@ -67,7 +67,10 @@ function AlchemyENSCacheProvider(props: Props): React.Node { const { children } = props; return ( - + {children} ); diff --git a/native/package.json b/native/package.json --- a/native/package.json +++ b/native/package.json @@ -87,6 +87,7 @@ "expo-media-library": "~15.0.0", "expo-secure-store": "~12.0.0", "expo-splash-screen": "~0.17.4", + "fastestsmallesttextencoderdecoder": "^1.0.22", "ffmpeg-kit-react-native": "^6.0.2", "find-root": "^1.1.0", "invariant": "^2.2.4", diff --git a/native/root.react.js b/native/root.react.js --- a/native/root.react.js +++ b/native/root.react.js @@ -103,7 +103,7 @@ import { useLoadCommFonts } from './themes/fonts.js'; import { DarkTheme, LightTheme } from './themes/navigation.js'; import ThemeHandler from './themes/theme-handler.react.js'; -import { provider } from './utils/ethers-utils.js'; +import { alchemyKey, ethersProvider } from './utils/ethers-utils.js'; import { neynarKey } from './utils/neynar-utils.js'; // Add custom items to expo-dev-menu @@ -353,7 +353,10 @@ initialMetrics={initialWindowMetrics} > - +