/ lib / modules / ens / index.js
index.js
  1  const fs = require('../../core/fs.js');
  2  const utils = require('../../utils/utils.js');
  3  const namehash = require('eth-ens-namehash');
  4  const async = require('async');
  5  const embarkJsUtils = require('embarkjs').Utils;
  6  const reverseAddrSuffix = '.addr.reverse';
  7  const ENSFunctions = require('./ENSFunctions');
  8  
  9  const MAINNET_ID = '1';
 10  const ROPSTEN_ID = '3';
 11  const RINKEBY_ID = '4';
 12  // Price of ENS registration contract functions
 13  const ENS_GAS_PRICE = 700000;
 14  
 15  const ENS_CONTRACTS_CONFIG = {
 16    [MAINNET_ID]: {
 17      "ENSRegistry": {
 18        "address": "0x314159265dd8dbb310642f98f50c066173c1259b",
 19        "silent": true
 20      },
 21      "Resolver": {
 22        "deploy": false
 23      },
 24      "FIFSRegistrar": {
 25        "deploy": false
 26      }
 27    },
 28    [ROPSTEN_ID]: {
 29      "ENSRegistry": {
 30        "address": "0x112234455c3a32fd11230c42e7bccd4a84e02010",
 31        "silent": true
 32      },
 33      "Resolver": {
 34        "deploy": false
 35      },
 36      "FIFSRegistrar": {
 37        "deploy": false
 38      }
 39    },
 40    [RINKEBY_ID]: {
 41      "ENSRegistry": {
 42        "address": "0xe7410170f87102DF0055eB195163A03B7F2Bff4A",
 43        "silent": true
 44      },
 45      "Resolver": {
 46        "deploy": false
 47      },
 48      "FIFSRegistrar": {
 49        "deploy": false
 50      }
 51    }
 52  };
 53  
 54  class ENS {
 55    constructor(embark, _options) {
 56      this.env = embark.env;
 57      this.logger = embark.logger;
 58      this.events = embark.events;
 59      this.namesConfig = embark.config.namesystemConfig;
 60      this.enabled = false;
 61      this.registration = this.namesConfig.register || {};
 62      this.embark = embark;
 63      this.ensConfig = require('./ensContractConfigs');
 64      this.configured = false;
 65  
 66      this.events.setCommandHandler("ens:resolve", this.ensResolve.bind(this));
 67  
 68      if (this.namesConfig === {} ||
 69        this.namesConfig.enabled !== true ||
 70        this.namesConfig.available_providers.indexOf('ens') < 0) {
 71        return;
 72      }
 73      this.enabled = true;
 74      this.doSetENSProvider = this.namesConfig.provider === 'ens';
 75  
 76      this.addENSToEmbarkJS();
 77      this.registerEvents();
 78      this.registerConsoleCommands();
 79    }
 80  
 81    reset() {
 82      this.configured = false;
 83    }
 84  
 85    registerConsoleCommands() {
 86      this.embark.registerConsoleCommand((cmd, _options) => {
 87        let [cmdName, domain] = cmd.split(' ');
 88        return {
 89          match: () => cmdName === 'resolve',
 90          process: (cb) => global.EmbarkJS.Names.resolve(domain, cb)
 91        };
 92      });
 93  
 94      this.embark.registerConsoleCommand((cmd, _options) => {
 95        let [cmdName, address] = cmd.split(' ');
 96        return {
 97          match: () => cmdName === 'lookup',
 98          process: (cb) => global.EmbarkJS.Names.lookup(address, cb)
 99        };
100      });
101  
102      this.embark.registerConsoleCommand((cmd, _options) => {
103        let [cmdName, name, address] = cmd.split(' ');
104        return {
105          match: () => cmdName === 'registerSubDomain',
106          process: (cb) => global.EmbarkJS.Names.registerSubDomain(name, address, cb)
107        };
108      });
109    }
110  
111    registerEvents() {
112      this.embark.registerActionForEvent("deploy:beforeAll", this.configureContractsAndRegister.bind(this));
113      this.events.on('blockchain:reseted', this.reset.bind(this));
114      this.events.setCommandHandler("storage:ens:associate", this.associateStorageToEns.bind(this));
115    }
116  
117    setProviderAndRegisterDomains(cb = (() => {})) {
118      const self = this;
119      let config = {
120        env: self.env,
121        registration: self.registration,
122        registryAbi: self.ensConfig.ENSRegistry.abiDefinition,
123        registryAddress: self.ensConfig.ENSRegistry.deployedAddress,
124        registrarAbi: self.ensConfig.FIFSRegistrar.abiDefinition,
125        registrarAddress: self.ensConfig.FIFSRegistrar.deployedAddress,
126        resolverAbi: self.ensConfig.Resolver.abiDefinition,
127        resolverAddress: self.ensConfig.Resolver.deployedAddress
128      };
129  
130      if (self.doSetENSProvider) {
131        self.addSetProvider(config);
132      }
133  
134      self.events.request('blockchain:networkId', (networkId) => {
135        const isKnownNetwork = Boolean(ENS_CONTRACTS_CONFIG[networkId]);
136        const shouldRegisterSubdomain = self.registration && self.registration.subdomains && Object.keys(self.registration.subdomains).length;
137        if (isKnownNetwork || !shouldRegisterSubdomain) {
138          return cb();
139        }
140  
141        self.registerConfigDomains(config, cb);
142      });
143    }
144  
145    associateStorageToEns(options, cb) {
146      const self = this;
147      // Code inspired by https://github.com/monkybrain/ipfs-to-ens
148      const {name, storageHash} = options;
149  
150      if (!utils.isValidEthDomain(name)) {
151        return cb('Invalid domain name ' + name);
152      }
153  
154      let hashedName = namehash.hash(name);
155      let contentHash;
156      try {
157        contentHash = utils.hashTo32ByteHexString(storageHash);
158      } catch (e) {
159        return cb('Invalid IPFS hash');
160      }
161      // Set content
162      async.waterfall([
163        function getRegistryABI(next) {
164          self.events.request('contracts:contract', "ENSRegistry", (contract) => {
165            next(null, contract);
166          });
167        },
168        function createRegistryContract(contract, next) {
169          self.events.request("blockchain:contract:create",
170            {abi: contract.abiDefinition, address: contract.deployedAddress},
171            (resolver) => {
172              next(null, resolver);
173            });
174        },
175        function getResolverForName(registry, next) {
176          registry.methods.resolver(hashedName).call((err, resolverAddress) => {
177            if (err) {
178              return cb(err);
179            }
180            if (resolverAddress === '0x0000000000000000000000000000000000000000') {
181              return cb('Name not yet registered');
182            }
183            next(null, resolverAddress);
184          });
185        },
186        function getResolverABI(resolverAddress, next) {
187          self.events.request('contracts:contract', "Resolver", (contract) => {
188            next(null, resolverAddress, contract);
189          });
190        },
191        function createResolverContract(resolverAddress, contract, next) {
192          self.events.request("blockchain:contract:create",
193            {abi: contract.abiDefinition, address: resolverAddress},
194            (resolver) => {
195              next(null, resolver);
196            });
197        },
198        function getDefaultAccount(resolver, next) {
199          self.events.request("blockchain:defaultAccount:get", (defaultAccount) => {
200            next(null, resolver, defaultAccount);
201          });
202        },
203        function setContent(resolver, defaultAccount, next) {
204          resolver.methods.setContent(hashedName, contentHash).send({from: defaultAccount}).then((transaction) => {
205            if (transaction.status !== "0x1" && transaction.status !== "0x01" && transaction.status !== true) {
206              return next('Association failed. Status: ' + transaction.status);
207            }
208            next();
209          }).catch(next);
210        }
211      ], cb);
212    }
213  
214    registerConfigDomains(config, cb) {
215      const self = this;
216      const secureSend = embarkJsUtils.secureSend;
217  
218      self.events.request("blockchain:defaultAccount:get", (defaultAccount) => {
219        async.each(Object.keys(self.registration.subdomains), (subDomainName, eachCb) => {
220          const address = self.registration.subdomains[subDomainName];
221          const reverseNode = utils.soliditySha3(address.toLowerCase().substr(2) + reverseAddrSuffix);
222          ENSFunctions.registerSubDomain(self.ensContract, self.registrarContract, self.resolverContract, defaultAccount,
223            subDomainName, self.registration.rootDomain, reverseNode, address, self.logger, secureSend, eachCb);
224        }, cb);
225      });
226    }
227  
228    createResolverContract(config, callback) {
229      this.events.request("blockchain:contract:create", {
230        abi: config.resolverAbi,
231        address: config.resolverAddress
232      }, (resolver) => {
233        callback(null, resolver);
234      });
235    }
236  
237    registerAPI() {
238      let self = this;
239  
240      const createInternalResolverContract = function(resolverAddress, callback) {
241        self.createResolverContract({resolverAbi: self.ensConfig.Resolver.abiDefinition, resolverAddress}, callback);
242      };
243  
244      self.embark.registerAPICall(
245        'get',
246        '/embark-api/ens/resolve',
247        (req, res) => {
248          async.waterfall([
249            function(callback) {
250              ENSFunctions.resolveName(req.query.name, self.ensContract, createInternalResolverContract.bind(self), callback);
251            }
252          ], function(error, address) {
253            if (error) {
254              return res.send({error: error.message});
255            }
256            res.send({address});
257          });
258        }
259      );
260  
261      self.embark.registerAPICall(
262        'get',
263        '/embark-api/ens/lookup',
264        (req, res) => {
265          async.waterfall([
266            function(callback) {
267              ENSFunctions.lookupAddress(req.query.address, self.ensContract, utils, createInternalResolverContract.bind(self), callback);
268            }
269          ], function(error, name) {
270            if (error) {
271              return res.send({error: error || error.message});
272            }
273            res.send({name});
274          });
275        }
276      );
277  
278      self.embark.registerAPICall(
279        'post',
280        '/embark-api/ens/register',
281        (req, res) => {
282          self.events.request("blockchain:defaultAccount:get", (defaultAccount) => {
283            const secureSend = embarkJsUtils.secureSend;
284            const {subdomain, address} = req.body;
285            const reverseNode = utils.soliditySha3(address.toLowerCase().substr(2) + reverseAddrSuffix);
286            ENSFunctions.registerSubDomain(self.ensContract, self.registrarContract, self.resolverContract, defaultAccount,
287              subdomain, self.registration.rootDomain, reverseNode, address, self.logger, secureSend, (error) => {
288                if (error) {
289                  return res.send({error: error || error.message});
290                }
291                res.send({name: `${req.body.subdomain}.${self.registration.rootDomain}`, address: req.body.address});
292              });
293          });
294        }
295      );
296    }
297  
298    addENSToEmbarkJS() {
299      const self = this;
300  
301      // get namehash, import it into file
302      self.events.request("version:get:eth-ens-namehash", function(EnsNamehashVersion) {
303        let currentEnsNamehashVersion = require('../../../package.json').dependencies["eth-ens-namehash"];
304        if (EnsNamehashVersion !== currentEnsNamehashVersion) {
305          self.events.request("version:getPackageLocation", "eth-ens-namehash", EnsNamehashVersion, function(err, location) {
306            self.embark.registerImportFile("eth-ens-namehash", fs.dappPath(location));
307          });
308        }
309      });
310  
311      let code = fs.readFileSync(utils.joinPath(__dirname, 'ENSFunctions.js')).toString();
312      code += "\n" + fs.readFileSync(utils.joinPath(__dirname, 'embarkjs.js')).toString();
313      code += "\nEmbarkJS.Names.registerProvider('ens', __embarkENS);";
314  
315      this.embark.addCodeToEmbarkJS(code);
316    }
317  
318    configureContractsAndRegister(cb) {
319      const NO_REGISTRATION = 'NO_REGISTRATION';
320      const self = this;
321      if (self.configured) {
322        return cb();
323      }
324      self.events.request('blockchain:networkId', (networkId) => {
325        if (ENS_CONTRACTS_CONFIG[networkId]) {
326          self.ensConfig = utils.recursiveMerge(self.ensConfig, ENS_CONTRACTS_CONFIG[networkId]);
327        }
328  
329        async.waterfall([
330          function registry(next) {
331            self.events.request('deploy:contract', self.ensConfig.ENSRegistry, (err, _receipt) => {
332              return next(err);
333            });
334          },
335          function resolver(next) {
336            self.ensConfig.Resolver.args = [self.ensConfig.ENSRegistry.deployedAddress];
337            self.events.request('deploy:contract', self.ensConfig.Resolver, (err, _receipt) => {
338              return next(err);
339            });
340          },
341          function registrar(next) {
342            if (!self.registration || !self.registration.rootDomain) {
343              return next(NO_REGISTRATION);
344            }
345            const registryAddress = self.ensConfig.ENSRegistry.deployedAddress;
346            const rootNode = namehash.hash(self.registration.rootDomain);
347            const contract = self.ensConfig.FIFSRegistrar;
348            contract.args = [registryAddress, rootNode];
349  
350            self.events.request('deploy:contract', contract, (err, _receipt) => {
351              return next(err);
352            });
353          },
354          function registerRoot(next) {
355            let config = {
356              registryAbi: self.ensConfig.ENSRegistry.abiDefinition,
357              registryAddress: self.ensConfig.ENSRegistry.deployedAddress,
358              registrarAbi: self.ensConfig.FIFSRegistrar.abiDefinition,
359              registrarAddress: self.ensConfig.FIFSRegistrar.deployedAddress,
360              resolverAbi: self.ensConfig.Resolver.abiDefinition,
361              resolverAddress: self.ensConfig.Resolver.deployedAddress
362            };
363            async.parallel([
364              function createRegistryContract(paraCb) {
365                self.events.request("blockchain:contract:create",
366                  {abi: config.registryAbi, address: config.registryAddress},
367                  (registry) => {
368                    paraCb(null, registry);
369                  });
370              },
371              function createRegistrarContract(paraCb) {
372                self.events.request("blockchain:contract:create",
373                  {abi: config.registrarAbi, address: config.registrarAddress},
374                  (registrar) => {
375                    paraCb(null, registrar);
376                  });
377              },
378              function createResolverContract(paraCb) {
379                self.events.request("blockchain:contract:create",
380                  {abi: config.resolverAbi, address: config.resolverAddress},
381                  (resolver) => {
382                    paraCb(null, resolver);
383                  });
384              },
385              function getWeb3(paraCb) {
386                self.events.request("blockchain:get",
387                  (web3) => {
388                    paraCb(null, web3);
389                  });
390              }
391            ], (err, result) => {
392              self.ensContract = result[0];
393              self.registrarContract = result[1];
394              self.resolverContract = result[2];
395              const web3 = result[3];
396  
397              const rootNode = namehash.hash(self.registration.rootDomain);
398              var reverseNode = web3.utils.soliditySha3(web3.eth.defaultAccount.toLowerCase().substr(2) + reverseAddrSuffix);
399              self.ensContract.methods.setOwner(rootNode, web3.eth.defaultAccount).send({from: web3.eth.defaultAccount, gas: ENS_GAS_PRICE}).then(() => {
400                return self.ensContract.methods.setResolver(rootNode, config.resolverAddress).send({from: web3.eth.defaultAccount, gas: ENS_GAS_PRICE});
401              }).then(() => {
402                return self.ensContract.methods.setResolver(reverseNode, config.resolverAddress).send({from: web3.eth.defaultAccount, gas: ENS_GAS_PRICE});
403              }).then(() => {
404                return self.resolverContract.methods.setAddr(rootNode, web3.eth.defaultAccount).send({from: web3.eth.defaultAccount, gas: ENS_GAS_PRICE});
405              }).then(() => {
406                return self.resolverContract.methods.setName(reverseNode, self.registration.rootDomain).send({from: web3.eth.defaultAccount, gas: ENS_GAS_PRICE});
407              }).then((_result) => {
408                next();
409              })
410              .catch(err => {
411                self.logger.error('Error while registering the root domain');
412                if (err.message.indexOf('Transaction has been reverted by the EVM') > -1) {
413                  return next(__('Registration was rejected. Did you change the deployment account? If so, delete chains.json'));
414                }
415                next(err);
416              });
417            });
418          }
419        ], (err) => {
420          self.configured = true;
421          if (err && err !== NO_REGISTRATION) {
422            self.logger.error('Error while deploying ENS contracts');
423            self.logger.error(err.message || err);
424            return cb(err);
425          }
426          self.registerAPI();
427          self.setProviderAndRegisterDomains(cb);
428        });
429      });
430    }
431  
432    addSetProvider(config) {
433      let code = "\nEmbarkJS.Names.setProvider('ens'," + JSON.stringify(config) + ");";
434  
435      let shouldInit = (namesConfig) => {
436        return (namesConfig.provider === 'ens' && namesConfig.enabled === true);
437      };
438  
439      this.embark.addProviderInit('names', code, shouldInit);
440      this.embark.addConsoleProviderInit('names', code, shouldInit);
441    }
442  
443    ensResolve(name, cb) {
444      const self = this;
445      if (!self.enabled) {
446        return cb('ENS not enabled');
447      }
448      if(!self.configured) {
449        return cb('ENS not configured');
450      }
451      const hashedName = namehash.hash(name);
452      async.waterfall([
453        function getResolverAddress(next) {
454          self.ensContract.methods.resolver(hashedName).call((err, resolverAddress) => {
455            if (err) {
456              return next(err);
457            }
458            if(resolverAddress === '0x0000000000000000000000000000000000000000') {
459              return next('Name not yet registered');
460            }
461            next(null, resolverAddress);
462          });
463        },
464        function createResolverContract(resolverAddress, next) {
465          self.events.request("blockchain:contract:create",
466            {abi: self.ensConfig.Resolver.abiDefinition, address: resolverAddress},
467            (resolver) => {
468              next(null, resolver);
469            });
470        },
471        function resolveName(resolverContract, next) {
472          resolverContract.methods.addr(hashedName).call(next);
473        }
474      ], (err, result) => {
475        if (err) {
476          self.logger.error(__('Failed to resolve the ENS name: %s', name));
477          return cb(err);
478        }
479        cb(null, result);
480      });
481    }
482  }
483  
484  module.exports = ENS;
485  
486