diff --git a/README.md b/README.md index f83c556..fa4e923 100644 --- a/README.md +++ b/README.md @@ -16,20 +16,22 @@ npm install imperviousinc/handover ``` Your Infura credentials can be passed to the plugin in the same way(s) as all -other `hsd` configuraiton parameters: +other `hsd` configuration parameters: Command line: ``` hsd \ --plugins=handover \ + --handover-provider=infura --handover-infura-projectid=<...> \ --handover-infura-projectsecret=<...> -``` +``` Environment variables: ``` +export HSD_HANDOVER_PROVIDER=infura export HSD_HANDOVER_INFURA_PROJECTID=<...> export HSD_HANDOVER_INFURA_PROJECTSECRET=<...> hsd --plugins handover @@ -40,6 +42,7 @@ Configuration file: `~/.hsd/hsd.conf`: ``` +handover-provider: infura handover-infura-projectid: <...> handover-infura-projectsecret: <...> plugins: handover @@ -98,6 +101,42 @@ $ node '096365727469666965640662616461737300000100010000ea600004b8495201' ``` +## Using a local Ethereum provider + +To use a local Etherum node over JSON-RPC instead of Infura, use these configuration options instead. + +* `handover-jsonrpc-ens-address` is an ethereum address for the ENS registry to use when resolving '.eth' requests. +* `handover-jsonrpc-url` (optional) specify the jsonrpc connection url. If not provided, will use the ethersjs default. + +Command line: + +``` +hsd \ + --plugins=handover \ + --handover-provider=jsonrpc \ + --handover-jsonrpc-url=<...> \ + --handover-jsonrpc-ens-address=<...> +``` + +Environment variables: + +``` +export HSD_HANDOVER_INFURA_PROJECTID=<...> +export HSD_HANDOVER_INFURA_PROJECTSECRET=<...> +hsd --plugins handover +``` + +Configuration file: + +`~/.hsd/hsd.conf`: + +``` +handover-provider: jsonrpc +handover-jsonrpc-url: <...> +handover-jsonrpc-ens-address: <...> +plugins: handover +``` + ## Explanation When `hsd` is run with this plugin, a middleware function is added to the HNS root @@ -122,8 +161,8 @@ request with the full query string (i.e. `certified.badass`). There's still a lot "TODO": -Local Ethereum provider: Currently the plugin relies on the Infura API. However, -it is trivial to add an option to use a local Ethereum full node instead. +More config options: Support running against live testnets, without a '.ens' resolver +(to simplify testing), etc. Cache: The Ethereum interface should cache "resolver" and "registry" contract objects instead of requesting them from the Ethereum provider on each query. diff --git a/lib/ethereum.js b/lib/ethereum.js index 06e6425..3dbcb79 100644 --- a/lib/ethereum.js +++ b/lib/ethereum.js @@ -3,7 +3,7 @@ const ethers = require('ethers'); const {encoding, wire} = require('bns'); -const ENS_ADDRESS = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'; +const MAINNET_ENS_ADDRESS = '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e'; const ENS_ABI = [ 'function setOwner(bytes32 node, address owner) external @500000', 'function setSubnodeOwner(bytes32 node, bytes32 label, address owner) external @500000', @@ -26,14 +26,48 @@ const RESOLVER_ABI = [ 'function hasDNSRecords(bytes32 node, bytes32 name) view returns (bool)' ]; +/** + * Ethereum configuration options + * + * @typedef {{ + * provider: 'infura'| 'jsonrpc', + * jsonRpcUrl?: string | ethers.ethers.utils.ConnectionInfo, + * network?: ethers.ethers.providers.Networkish, + * localhostEnsAddress?: string, + * projectId?: string, + * projectSecret?: string, + * }} Options + */ + class Ethereum { + /** + * @param {Options} options + */ constructor(options) { this.keccak256 = ethers.utils.keccak256; this.namehash = ethers.utils.namehash; - this.infura = new ethers.providers.InfuraProvider('homestead', options); + /** @type { ethers.providers.Provider & ethers.providers.EnsProvider } */ + let provider; + /** @type { string } */ + let ensAddress; + + switch (options.provider) { + case 'infura': + provider = new ethers.providers.InfuraProvider('homestead', options); + ensAddress = MAINNET_ENS_ADDRESS; + break; + case 'jsonrpc': + provider = new ethers.providers.JsonRpcProvider(options.jsonRpcUrl) + ensAddress = options.localhostEnsAddress; + break; + default: + throw new Error("no provider specified for handover"); + } + + this.provider = provider; - this.ensRegistry = new ethers.Contract(ENS_ADDRESS, ENS_ABI, this.infura); + this.ensRegistry = new ethers.Contract(ensAddress, ENS_ABI, this.provider); this.ensResolver = null; } @@ -48,20 +82,20 @@ class Ethereum { // TODO: cache these async getResolverFromRegistry(name, registry) { const resolverAddr = await registry.resolver(this.namehash(name)); - return new ethers.Contract(resolverAddr, RESOLVER_ABI, this.infura); + return new ethers.Contract(resolverAddr, RESOLVER_ABI, this.provider); } getAbstractEnsRegistry(address) { - return new ethers.Contract(address, ENS_ABI, this.infura); + return new ethers.Contract(address, ENS_ABI, this.provider); } async resolveEnsAddress(name) { - return this.infura.resolveName(name); + return this.provider.resolveName(name); } // https://eips.ethereum.org/EIPS/eip-634 async resolveEnsText(name, key) { - const nameResolver = await this.infura.getResolver(name); + const nameResolver = await this.provider.getResolver(name); if (!nameResolver) return null; diff --git a/lib/handover.js b/lib/handover.js index a703f44..d469f51 100644 --- a/lib/handover.js +++ b/lib/handover.js @@ -11,16 +11,38 @@ const Ethereum = require('./ethereum'); const plugin = exports; +/** + * @typedef { import("hsd/lib/node").FullNode } FullNode + * @typedef { import("bcfg/lib/config")} Config + */ + class Plugin { + /** + * @param { FullNode } node + */ constructor(node) { this.ready = false; this.node = node; this.ns = node.ns; this.logger = node.logger.context('handover'); + /** @type { Config } */ + const config = node.config; + + const provider = config.str('handover-provider'); + if (provider !== 'infura' && provider != 'jsonrpc') { + throw new Error(`Configured with invalid provider type: ${provider}`); + } + + // For now, just set all the possible options from config, missing ones + // aren't a problem for the unused providers. this.ethereum = new Ethereum({ - projectId: node.config.str('handover-infura-projectid'), - projectSecret: node.config.str('handover-infura-projectsecret') + provider, + projectId: config.str('handover-infura-projectid'), + projectSecret: config.str('handover-infura-projectsecret'), + jsonRpcUrl: config.str('handover-jsonrpc-url'), + localhostEnsAddress: config.str('handover-jsonrpc-ens-address'), + network: config.str('handover-network'), }); // Plugin can not operate if node doesn't have DNS resolvers @@ -97,7 +119,7 @@ class Plugin { // Look up an alternate (forked) ENS contract by the Ethereum // address specified in the NS record, and query it for // the user's original request - if (rr.data.ns.slice(-6) === '._eth.') { + if (rr.data.ns.endsWith('._eth.')) { hasEnsReferral = true; // If the recursive is being minimal, don't look up the name. diff --git a/package-lock.json b/package-lock.json index ee3dab0..84d8c9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -380,6 +380,12 @@ "@ethersproject/strings": "^5.0.8" } }, + "@types/node": { + "version": "14.14.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.43.tgz", + "integrity": "sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ==", + "dev": true + }, "aes-js": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", diff --git a/package.json b/package.json index 812aa81..320bd0e 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "bns": "~0.14.0" }, "devDependencies": { + "@types/node": "^14.14.41", "bmocha": "^2.1.5" } }