├── .gitignore ├── commitlint.config.js ├── ens_logo.png ├── test ├── end2end │ ├── README.txt │ ├── runCommand.js │ ├── listInterfaces.test.js │ ├── gas.test.js │ ├── testdata.js │ ├── reverseName.test.js │ ├── errorconditions.test.js │ ├── gasprice.test.js │ ├── address.test.js │ ├── blockchainAddress.test.js │ └── contenthash.test.js ├── middleware │ ├── web3Middleware.test.js │ └── connectionCheckMiddleware.test.js └── lib │ ├── lib.listinterface.test.js │ ├── lib.gasprice.test.js │ ├── lib.gas.test.js │ ├── lib.blockchainAddress.test.js │ ├── lib.name.test.js │ ├── lib.address.test.js │ └── lib.contenthash.test.js ├── migrations ├── 1_initial_migration.js ├── 2_deploy_ens.js └── 3_prepare_test.js ├── lib ├── ENSAddresses.js ├── ResolverInterfaces.js └── index.js ├── src ├── middleware │ ├── controllerAddressMiddleware.js │ ├── web3Middleware.js │ ├── credentialsMiddleware.js │ ├── gaspriceMiddleware.js │ ├── requiresAccountMiddleware.js │ ├── updaterMiddleware.js │ ├── connectionCheckMiddleware.js │ └── providerMiddleware.js └── commands │ ├── clearReverseName.js │ ├── clearContenthash.js │ ├── getContenthash.js │ ├── getReverseName.js │ ├── listInterfaces.js │ ├── getAddress.js │ ├── setReverseName.js │ ├── clearAddress.js │ ├── getInfo.js │ ├── sharedOptions.json │ ├── setContenthash.js │ └── setAddress.js ├── .releaserc ├── .travis.yml ├── contracts └── Migrations.sol ├── .eslintrc.json ├── unitTest └── middleware │ ├── gaspriceMiddleware.test.js │ ├── controllerAddressMiddleware.test.js │ ├── requiresAccountMiddleware.test.js │ ├── credentialsMiddleware.test.js │ └── providerMiddleware.test.js ├── ens_logo.drawio ├── bin └── ens-updater.js ├── package.json ├── truffle-config.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /node_modules/ 3 | /.env 4 | /build/ 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /ens_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TripleSpeeder/ens-updater/HEAD/ens_logo.png -------------------------------------------------------------------------------- /test/end2end/README.txt: -------------------------------------------------------------------------------- 1 | To run end-2-end tests: 2 | 3 | 1. Start ganache-cli in deterministic mode: 4 | > ganache-cli -d 5 | 6 | 2. Run tests: 7 | > truffle test 8 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require('Migrations') 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations) 5 | } 6 | -------------------------------------------------------------------------------- /lib/ENSAddresses.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Mainnet 3 | '1': '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', 4 | // Ropsten 5 | '3': '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', 6 | // Rinkeby 7 | '4': '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', 8 | // Goerli 9 | '5': '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', 10 | } -------------------------------------------------------------------------------- /lib/ResolverInterfaces.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'EIP165': '0x01ffc9a7', 3 | 'Ethereum Address': '0x3b3b57de', 4 | 'Blockchain Address': '0xf1cb7e06', 5 | 'Canonical Name': '0x691f3431', 6 | 'Content Hash': '0xbc1c58d1', 7 | 'Contract ABI': '0x2203ab56', 8 | 'Public Key': '0xc8690233', 9 | 'Text Data': '0x59d1d43c' 10 | } 11 | -------------------------------------------------------------------------------- /test/end2end/runCommand.js: -------------------------------------------------------------------------------- 1 | const execa = require('execa') 2 | 3 | 4 | module.exports.runCommand = function runCommand(command, options={}) { 5 | let childResult 6 | try { 7 | childResult = execa.commandSync(command, options) 8 | } catch(childResultError) { 9 | childResult = childResultError 10 | } 11 | return childResult 12 | } 13 | -------------------------------------------------------------------------------- /src/middleware/controllerAddressMiddleware.js: -------------------------------------------------------------------------------- 1 | const getControllerAddress = ({requiresAccount, provider, accountIndex}) => { 2 | if (requiresAccount) { 3 | const controllerAddress = provider.getAddress(accountIndex).toLowerCase() 4 | return {controllerAddress} 5 | } 6 | else { 7 | return {} 8 | } 9 | } 10 | 11 | module.exports = getControllerAddress -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/commit-analyzer", 4 | "@semantic-release/release-notes-generator", 5 | "@semantic-release/changelog", 6 | "@semantic-release/npm", 7 | ["@semantic-release/git", { 8 | "assets": ["package.json", "CHANGELOG.md"], 9 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 10 | }], 11 | "@semantic-release/github" 12 | ] 13 | } -------------------------------------------------------------------------------- /src/commands/clearReverseName.js: -------------------------------------------------------------------------------- 1 | exports.command = 'clearReverseName' 2 | 3 | exports.describe = 'Clear reverse name record of calling address' 4 | 5 | exports.builder = (yargs) => { 6 | return yargs 7 | } 8 | 9 | exports.handler = async ({updater}) => { 10 | let result = await updater.clearReverseName() 11 | console.log(result) 12 | await updater.stop() 13 | // hardwire process.exit(0) here to fix problems with dangling HDWalletProvider engine for good. 14 | process.exit(0) 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | language: node_js 3 | node_js: 10 4 | jobs: 5 | include: 6 | - name: "Test" 7 | install: 8 | - npm ci 9 | - npm install -g truffle@5.0.* 10 | - npm install -g ganache-cli 11 | before_script: 12 | - ganache-cli -d >ganache.log 2>&1 & 13 | - sleep 5 14 | - truffle migrate --network development 15 | script: 16 | - npm run test 17 | deploy: 18 | provider: script 19 | skip_cleanup: true 20 | script: 21 | - npx semantic-release 22 | -------------------------------------------------------------------------------- /src/middleware/web3Middleware.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3') 2 | 3 | const createWeb3 = async ({verbose, provider}) => { 4 | verbose && console.log('Setting up web3...') 5 | 6 | if (provider === undefined) { 7 | throw Error(`Failed to initialize web3. provider is undefined.`) 8 | } 9 | 10 | try { 11 | let web3 = new Web3(provider) 12 | return { 13 | web3, 14 | } 15 | } catch (error) { 16 | throw Error(`Failed to initialize web3 at ${provider}: ${error.message}`) 17 | } 18 | } 19 | 20 | module.exports = createWeb3 -------------------------------------------------------------------------------- /src/commands/clearContenthash.js: -------------------------------------------------------------------------------- 1 | exports.command = 'clearContenthash ' 2 | 3 | exports.describe = 'Clear contenthash record' 4 | 5 | exports.builder = (yargs) => { 6 | return yargs 7 | .positional('ensname', { 8 | description: 'ENS Name to query or update', 9 | type: 'string', 10 | }) 11 | } 12 | 13 | exports.handler = async ({updater}) => { 14 | let result = await updater.clearContenthash() 15 | console.log(result) 16 | await updater.stop() 17 | // hardwire process.exit(0) here to fix problems with dangling HDWalletProvider engine for good. 18 | process.exit(0) 19 | } 20 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.4.21 <0.6.0; 2 | 3 | contract Migrations { 4 | address public owner; 5 | uint public last_completed_migration; 6 | 7 | constructor() public { 8 | owner = msg.sender; 9 | } 10 | 11 | modifier restricted() { 12 | if (msg.sender == owner) _; 13 | } 14 | 15 | function setCompleted(uint completed) public restricted { 16 | last_completed_migration = completed; 17 | } 18 | 19 | function upgrade(address new_address) public restricted { 20 | Migrations upgraded = Migrations(new_address); 21 | upgraded.setCompleted(last_completed_migration); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/getContenthash.js: -------------------------------------------------------------------------------- 1 | exports.command = 'getContenthash ' 2 | 3 | exports.describe = 'Get contenthash record' 4 | 5 | exports.builder = (yargs) => { 6 | return yargs 7 | .positional('ensname', { 8 | description: 'ENS Name to query or update', 9 | type: 'string', 10 | }) 11 | } 12 | 13 | exports.handler = async ({updater}) => { 14 | try { 15 | let {codec, hash} = await updater.getContenthash() 16 | if (hash === undefined) { 17 | console.log('No contenthash record set') 18 | } else { 19 | console.log(`${codec}: ${hash}`) 20 | } 21 | } finally { 22 | updater.stop() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/getReverseName.js: -------------------------------------------------------------------------------- 1 | exports.command = 'getReverseName
' 2 | 3 | exports.describe = 'Get reverse name record' 4 | 5 | exports.builder = (yargs) => { 6 | return yargs 7 | .positional('address', { 8 | description: 'Ethereum address to query', 9 | type: 'string', 10 | }) 11 | } 12 | 13 | exports.handler = async ({updater, address}) => { 14 | try { 15 | let reverseName = await updater.getReverseName(address) 16 | if (reverseName === '') { 17 | console.log('No reverse name record set') 18 | } else { 19 | console.log(reverseName) 20 | } 21 | } finally { 22 | updater.stop() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly", 12 | "artifacts": "readonly", 13 | "contract": "readonly" 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2018 17 | }, 18 | "rules": { 19 | "quotes": ["error", "single", { "allowTemplateLiterals": true }], 20 | "semi": ["error", "never"], 21 | "eqeqeq": "error", 22 | "no-multi-spaces": "error", 23 | "quote-props": [ "error", "consistent" ] 24 | } 25 | } -------------------------------------------------------------------------------- /src/middleware/credentialsMiddleware.js: -------------------------------------------------------------------------------- 1 | 2 | const getCredentials = ({requiresAccount}) => { 3 | if (requiresAccount) { 4 | // use HDWalletProvider with mnemonic or private string 5 | const mnemonic = process.env.MNEMONIC 6 | const private_key = process.env.PRIVATE_KEY 7 | if ((mnemonic !== undefined) && (private_key !== undefined)) { 8 | throw Error('Got both mnemonic and private key') 9 | } 10 | if (mnemonic) { 11 | return {mnemonic} 12 | } else if (private_key) { 13 | return {private_key} 14 | } else { 15 | throw Error('Got neither mnemonic nor private key') 16 | } 17 | } else { 18 | return {} 19 | } 20 | } 21 | 22 | module.exports = getCredentials -------------------------------------------------------------------------------- /src/middleware/gaspriceMiddleware.js: -------------------------------------------------------------------------------- 1 | const Web3 = require('web3') 2 | const maxGaspriceGWei = 500 3 | 4 | const gaspriceMiddleware = ({verbose, gasPrice, requiresAccount}) => { 5 | // check limits 6 | if (gasPrice > maxGaspriceGWei) { 7 | throw Error(`Gas price too high. Maximum possible value is ${maxGaspriceGWei} gwei (provided value: ${gasPrice})`) 8 | } 9 | // convert to wei BN instance 10 | const gwei = Web3.utils.toBN(gasPrice) 11 | const wei = Web3.utils.toWei(gwei, 'gwei') 12 | 13 | // ignore gas price when command does not require account and no tx will be performed 14 | if (requiresAccount) { 15 | verbose && console.log(`Setting gas price: ${gasPrice} gwei`) 16 | } 17 | return { 18 | gasPrice: wei 19 | } 20 | } 21 | 22 | module.exports = gaspriceMiddleware -------------------------------------------------------------------------------- /src/middleware/requiresAccountMiddleware.js: -------------------------------------------------------------------------------- 1 | const requiresAccount = (argv) => { 2 | const cmdsNeedAccount = [ 3 | 'setContenthash', 4 | 'setAddress', 5 | 'clearAddress', 6 | 'clearContenthash', 7 | 'setReverseName', 8 | 'clearReverseName' 9 | ] 10 | const cmdsDontNeedAccount = [ 11 | 'getAddress', 12 | 'getContenthash', 13 | 'getInfo', 14 | 'listInterfaces', 15 | 'getReverseName' 16 | ] 17 | const command = argv._[0] 18 | const requiresAccount = cmdsNeedAccount.includes(command) 19 | const doesNotRequireAccount = cmdsDontNeedAccount.includes(command) 20 | if ((!requiresAccount) && (!doesNotRequireAccount)) { 21 | throw Error('Unknown command') 22 | } 23 | return {requiresAccount} 24 | } 25 | 26 | module.exports = requiresAccount -------------------------------------------------------------------------------- /src/commands/listInterfaces.js: -------------------------------------------------------------------------------- 1 | exports.command = 'listInterfaces ' 2 | 3 | exports.describe = 'List interfaces resolver supports' 4 | 5 | exports.builder = (yargs) => { 6 | return yargs 7 | .positional('ensname', { 8 | description: 'ENS Name to query or update', 9 | type: 'string', 10 | }) 11 | } 12 | 13 | exports.handler = async ({updater}) => { 14 | try { 15 | let interfaces = await updater.listInterfaces() 16 | if (interfaces.length) { 17 | console.log(`Resolver supports ${interfaces.length} interfaces:`) 18 | for (const i of interfaces) { 19 | console.log(` - ${i}`) 20 | } 21 | } else { 22 | console.log(`Resolver does not support any interface`) 23 | } 24 | } finally { 25 | updater.stop() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/middleware/updaterMiddleware.js: -------------------------------------------------------------------------------- 1 | const Updater = require('../../lib/index') 2 | 3 | const createUpdater = async ({web3, ensname, controllerAddress, verbose, registryAddress, dryRun, requiresAccount, estimateGas, gasPrice, gas}) => { 4 | const updater = new Updater() 5 | 6 | // Ignore 'dryRun' option for read-only commands 7 | // TODO: Prevent setting both options via yargs configuration? 8 | if (!requiresAccount) { 9 | dryRun = false 10 | } 11 | 12 | const setupOptions = { 13 | web3: web3, 14 | ensName: ensname, 15 | controllerAddress: controllerAddress, 16 | verbose: verbose, 17 | registryAddress: registryAddress, 18 | dryrun: dryRun, 19 | estimateGas: estimateGas, 20 | gasPrice: web3.utils.toBN(gasPrice), 21 | gas: gas 22 | } 23 | await updater.setup(setupOptions) 24 | return {updater} 25 | } 26 | 27 | module.exports = createUpdater -------------------------------------------------------------------------------- /unitTest/middleware/gaspriceMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert 2 | const gaspriceMiddleware = require('../../src/middleware/gaspriceMiddleware') 3 | const Web3 = require('web3') 4 | 5 | describe('gaspriceMiddleware', function() { 6 | 7 | it('should convert provided gasPrice from gwei number to wei BN', function() { 8 | const options = { 9 | gasPrice: 10 10 | } 11 | const expectedGwei = Web3.utils.toBN(10) 12 | const expected = { 13 | gasPrice: Web3.utils.toWei(expectedGwei, 'gwei'), 14 | } 15 | const result = gaspriceMiddleware(options) 16 | assert.deepEqual(result, expected) 17 | }) 18 | 19 | it('should fail when provided gasprice is too high', function() { 20 | const options = { 21 | gasPrice: '501' 22 | } 23 | assert.throws(()=>{gaspriceMiddleware(options)}, Error, /Gas price too high/) 24 | }) 25 | 26 | }) -------------------------------------------------------------------------------- /src/commands/getAddress.js: -------------------------------------------------------------------------------- 1 | const {formatsByName} = require('@ensdomains/address-encoder') 2 | 3 | exports.command = 'getAddress [coinname]' 4 | 5 | exports.describe = 'Get address record' 6 | 7 | exports.builder = (yargs) => { 8 | return yargs 9 | .positional('ensname', { 10 | description: 'ENS Name to query or update', 11 | type: 'string', 12 | }) 13 | .positional('coinname', { 14 | description: 'Blockchain/Cryptocurrency address to look up', 15 | type: 'string', 16 | choices: Object.keys(formatsByName), 17 | default: 'ETH', 18 | }) 19 | } 20 | 21 | exports.handler = async ({coinname, updater}) => { 22 | // get SLIP-0044 coinType from coinname 23 | const coinType = formatsByName[coinname].coinType 24 | try { 25 | let currentAddress = await updater.getAddress(coinType) 26 | console.log(currentAddress) 27 | } finally { 28 | updater.stop() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/setReverseName.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | exports.command = 'setReverseName ' 4 | 5 | exports.describe = 'Set reverse name record of calling address' 6 | 7 | exports.builder = (yargs) => { 8 | return yargs 9 | .positional('reverseName', { 10 | description: 'Name to set or \'stdin\' to read from stdin', 11 | type: 'string', 12 | }) 13 | } 14 | 15 | exports.handler = async ({reverseName, verbose, updater}) => { 16 | if (reverseName === 'stdin') { 17 | verbose && console.log('Reading reverse name from stdin...') 18 | reverseName = fs.readFileSync(0).toString().trim() 19 | verbose && console.log(`\t Got reverse name: ${reverseName}.`) 20 | } 21 | let result = await updater.setReverseName(reverseName) 22 | console.log(result) 23 | await updater.stop() 24 | // hardwire process.exit(0) here to fix problems with dangling HDWalletProvider engine for good. 25 | process.exit(0) 26 | } 27 | -------------------------------------------------------------------------------- /test/middleware/web3Middleware.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const chaiAsPromised = require('chai-as-promised') 3 | chai.use(chaiAsPromised) 4 | const assert = chai.assert 5 | const web3Middleware = require('../../src/middleware/web3Middleware') 6 | 7 | describe('web3Middleware', function() { 8 | 9 | it('should fail with invalid provider', function() { 10 | const options = { 11 | verbose: false, 12 | provider: undefined, 13 | } 14 | assert.isRejected( web3Middleware(options), /Failed to initialize web3./) 15 | }) 16 | 17 | it('should return web3', async function() { 18 | // Precondition: Have Ganache running on localhost:8545 19 | const ganacheProvider = 'http://localhost:8545' 20 | const options = { 21 | verbose: false, 22 | provider: ganacheProvider, 23 | } 24 | const result = await web3Middleware(options) 25 | assert.property(result, 'web3') 26 | }) 27 | 28 | }) -------------------------------------------------------------------------------- /src/commands/clearAddress.js: -------------------------------------------------------------------------------- 1 | const {formatsByName} = require('@ensdomains/address-encoder') 2 | 3 | exports.command = 'clearAddress [coinname]' 4 | 5 | exports.describe = 'Clear address record' 6 | 7 | exports.builder = (yargs) => { 8 | return yargs 9 | .positional('ensname', { 10 | description: 'ENS Name to query or update', 11 | type: 'string', 12 | }) 13 | .positional('coinname', { 14 | description: 'Blockchain/Cryptocurrency address type to clear', 15 | type: 'string', 16 | choices: Object.keys(formatsByName), 17 | default: 'ETH' 18 | }) 19 | } 20 | 21 | exports.handler = async ({coinname, updater}) => { 22 | // get SLIP-0044 coinType from coinname 23 | const coinType = formatsByName[coinname].coinType 24 | let result = await updater.clearAddress(coinType) 25 | console.log(result) 26 | await updater.stop() 27 | // hardwire process.exit(0) here to fix problems with dangling HDWalletProvider engine for good. 28 | process.exit(0) 29 | } 30 | -------------------------------------------------------------------------------- /unitTest/middleware/controllerAddressMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const sinon = require('sinon') 2 | const chai = require('chai') 3 | const assert = chai.assert 4 | const getControllerAddress = require('../../src/middleware/controllerAddressMiddleware') 5 | 6 | describe('controllerAddressMiddleware', function() { 7 | 8 | const controllerAddress = '0x123456789' 9 | const fakeProvider = { 10 | getAddress: sinon.fake.returns(controllerAddress) 11 | } 12 | 13 | it('should get controller when account is required', function() { 14 | const options = { 15 | requiresAccount: true, 16 | provider: fakeProvider, 17 | } 18 | const result = getControllerAddress(options) 19 | assert.deepEqual(result, {controllerAddress: controllerAddress}) 20 | }) 21 | 22 | it('should not get controller when no account is required', function() { 23 | const options = { 24 | requiresAccount: false, 25 | provider: fakeProvider 26 | } 27 | const result = getControllerAddress(options) 28 | assert.deepEqual(result, {}) 29 | }) 30 | 31 | }) -------------------------------------------------------------------------------- /ens_logo.drawio: -------------------------------------------------------------------------------- 1 | vVbJbtswEP0aHQtosbwcIydOUTRtULfNmRHH0jQUKVB0vHx9SYnalSYu0ho+cB63mfceSTnBOjveSpKnd4ICc3yXHp3g2vF9b+b7jvm79GQRdxlUSCKRWqwFtniGeqBF90ih6A1UQjCFeR+MBecQqx5GpBSH/rCdYP1dc5LACNjGhI3RB6QqrdClv2jxj4BJWu/szVdVT0bqwbaSIiVUHDpQcOMEaymEqlrZcQ3MsFfzUs3bvNDbJCaBq7dMCH98wv3d9uv558r7hg+H9a8z/2BXeSZsbwu2yapTzcAhRQXbnMQmPmiZnSBKVcZ05OkmKfKK9x0eQW8VFUqKp4YsT9cZ5SAxAwXSLIM8sVOl2HNq5pTRTnBl9deV6BgZWwsmZJlGsCt/Gk8koahrrvu44GBgRgqjtZnaUG2CWGQY2/aYs5oAkAqOHchyeAtCJy5PeojtDRZWT+voMLTxobWHH1gs7VijAYm1ZNKs3aqmG1a4C0QMRiICL0Y66vqUIR0KPJPHEjWUkL0SRUV7KSfDhOs2g50ZbXhBfRquLJwhpWZqlAvkqiwkjJzweqCX1aSywgBsVXet6huSITN0rsVeIkid9xfQ8kWb76fcJJb07bGsJ25tdd5Af0YegUUkfkrKzaZclAqJZ70EqX38DtaY+UHPGvM67ljDm09Yw3P/lTVmE+d7nqim3r/2R6xp0kpd5JALlB+qXSp6rxNRKCa3/zwY0KTR98l7nH9/IPJqJPL/1Xj5+h0OnF6Zx7A9hZ0b/K1CzN3R7b7408EB2ntax4x2GAsnGKsxCYwofO4/yFMs2h3ujfFawcLBhb1wV/0lCl1yDHZW9wEdLrR6ZSFFZAJqtFApalP2lM46bL8DquHt51Rw8xs= -------------------------------------------------------------------------------- /src/middleware/connectionCheckMiddleware.js: -------------------------------------------------------------------------------- 1 | const url = require('url') 2 | const connectionTester = require('connection-tester') 3 | 4 | const connectionCheck = async ({verbose, web3}) => { 5 | verbose && console.log(`Checking connectivity of web3 node at ${web3}...`) 6 | // Do initial check if there is a service running on provided endpoint. Relying on Web3/HDWalletProvider 7 | // to fail in case node is not reachable is flaky, see https://github.com/TripleSpeeder/ens-updater/issues/25 8 | let web3Url = url.parse(web3) 9 | let port = web3Url.port 10 | if (!port) { 11 | // no port specified. Derive from protocol. 12 | switch(web3Url.protocol) { 13 | case 'https:': 14 | case 'wss:': 15 | port = 443 16 | break 17 | case 'http:': 18 | case 'ws:': 19 | default: 20 | port = 80 21 | } 22 | } 23 | const nodeIsReachable = connectionTester.test(web3Url.hostname, port, 2000) 24 | if (!nodeIsReachable.success) { 25 | throw Error(`Node is not reachable at ${web3}`) 26 | } 27 | } 28 | 29 | module.exports = connectionCheck -------------------------------------------------------------------------------- /src/commands/getInfo.js: -------------------------------------------------------------------------------- 1 | exports.command = 'getInfo ' 2 | 3 | exports.describe = 'Get various info about ENS name' 4 | 5 | exports.builder = (yargs) => { 6 | return yargs 7 | .positional('ensname', { 8 | description: 'ENS Name to query or update', 9 | type: 'string', 10 | }) 11 | } 12 | 13 | exports.handler = async ({updater, ensname}) => { 14 | try { 15 | let info = await updater.getInfo() 16 | console.log(`ENS name '${ensname}:'`) 17 | console.log('==========================================================') 18 | console.log(`Controller address: \t\t${info.Controller}`) 19 | console.log(`Address: \t\t\t${info.Address}`) 20 | console.log(`Expires: \t\t\t${info.Expires}`) 21 | console.log(`Resolver contract: \t\t${info.Resolver}`) 22 | console.log(`Registrant: \t\t\t${info.Registrant ? info.Registrant : 'n/a'}`) 23 | console.log(`Registrar contract: \t\t${info.Registrar}`) 24 | console.log(`Reverse resolver contract: \t${info.ReverseResolver}`) 25 | console.log(`Reverse name: \t\t\t${info.ReverseName}`) 26 | } finally { 27 | updater.stop() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/end2end/listInterfaces.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const chai = require('chai') 3 | const assert = chai.assert 4 | const {runCommand} = require('./runCommand') 5 | 6 | 7 | contract('listInterfaces', function() { 8 | 9 | const scriptpath = 'bin/ens-updater.js' 10 | const providerstring = 'http://localhost:8545' 11 | const ensName = 'wayne.test' 12 | let registryAddress 13 | 14 | before('Get registry address', async function() { 15 | const registry = await ENSRegistry.deployed() 16 | registryAddress = registry.address 17 | }) 18 | 19 | it('Should return supported interfaces of public resolver', async function() { 20 | const command = `${scriptpath} listInterfaces ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 21 | const childResult = await runCommand(command) 22 | assert.isFalse(childResult.failed) 23 | const expected = `Resolver supports 8 interfaces: 24 | - EIP165 25 | - Ethereum Address 26 | - Blockchain Address 27 | - Canonical Name 28 | - Content Hash 29 | - Contract ABI 30 | - Public Key 31 | - Text Data` 32 | assert.equal(childResult.stdout, expected) 33 | }) 34 | 35 | }) -------------------------------------------------------------------------------- /src/commands/sharedOptions.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": { 3 | "description": "Verbose output", 4 | "alias": "v", 5 | "type": "boolean", 6 | "default": false, 7 | "demandOption": false 8 | }, 9 | "web3": { 10 | "description": "Web3 connection string", 11 | "type": "string", 12 | "demandOption": true 13 | }, 14 | "estimateGas": { 15 | "description": "Estimate required gas for transactions", 16 | "type": "boolean", 17 | "default": false, 18 | "demandOption": false 19 | }, 20 | "gasPrice": { 21 | "description": "Gas price to set for transaction (unit 'gwei'). Defaults to 10.", 22 | "type": "number", 23 | "default": 10, 24 | "demandOption": false 25 | }, 26 | "gas": { 27 | "description": "Gas to provide for transaction (omit to use automatic calculation)", 28 | "type": "number", 29 | "demandOption": false 30 | }, 31 | "dry-run": { 32 | "description": "Do not perform any real transactions", 33 | "type": "boolean", 34 | "default": false, 35 | "demandOption": false 36 | }, 37 | "accountindex": { 38 | "alias": "i", 39 | "description": "Account index. Defaults to 0", 40 | "default": 0, 41 | "type": "number" 42 | }, 43 | "registryAddress": { 44 | "description": "Optional contract address of the ENS Registry.", 45 | "type": "string", 46 | "demandOption": false 47 | } 48 | } -------------------------------------------------------------------------------- /src/commands/setContenthash.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | exports.command = 'setContenthash ' 4 | 5 | exports.describe = 'Set contenthash record' 6 | 7 | exports.builder = (yargs) => { 8 | return yargs 9 | .positional('ensname', { 10 | description: 'ENS Name to query or update', 11 | type: 'string', 12 | }) 13 | .positional('contenttype', { 14 | alias: 'type', 15 | description: 'Type of content hash to set (e.g ipfs-ns, swarm-ns, ...)', 16 | type: 'string', 17 | demandOption: true, 18 | }).positional('contenthash', { 19 | alias: 'hash', 20 | description: 'Content hash. Use \'stdin\' to read from stdin', 21 | type: 'string', 22 | demandOption: true, 23 | }) 24 | } 25 | 26 | exports.handler = async ({verbose, contenttype, contenthash, updater}) => { 27 | if (contenthash === 'stdin') { 28 | verbose && console.log('Getting contenthash from stdin...') 29 | contenthash = fs.readFileSync(0).toString().trim() 30 | verbose && console.log(`\t Got contenthash: ${contenthash}.`) 31 | } 32 | let result = await updater.setContenthash({ 33 | contentType: contenttype, 34 | contentHash: contenthash, 35 | }) 36 | console.log(result) 37 | await updater.stop() 38 | // hardwire process.exit(0) here to fix problems with dangling HDWalletProvider engine for good. 39 | process.exit(0) 40 | } 41 | -------------------------------------------------------------------------------- /src/middleware/providerMiddleware.js: -------------------------------------------------------------------------------- 1 | const HDWalletProvider = require('@truffle/hdwallet-provider') 2 | 3 | const createProvider = ({verbose, requiresAccount, accountIndex, web3, mnemonic, private_key}) => { 4 | verbose && console.log('Setting up web3 provider...') 5 | let provider 6 | if (requiresAccount) { 7 | // use HDWalletProvider with mnemonic or private string 8 | if (mnemonic && private_key) { 9 | throw Error(`Both PRIVATE_KEY and MNEMONIC are set. Can't decide which one to use.`) 10 | } 11 | if (mnemonic) { 12 | try { 13 | provider = new HDWalletProvider(mnemonic, web3, accountIndex, accountIndex+1) 14 | } catch (error) { 15 | throw Error(`Could not initialize HDWalletProvider with mnemonic: ${error}`) 16 | } 17 | } else if (private_key) { 18 | try { 19 | provider = new HDWalletProvider(private_key, web3) 20 | } catch (error) { 21 | throw Error(`Could not initialize HDWalletProvider with privatekey: ${error}`) 22 | } 23 | } else { 24 | throw Error(`No account available. Make sure to provide either PRIVATE_KEY or MNEMONIC through .env`) 25 | } 26 | } else { 27 | // just use plain connection string as provider 28 | provider = web3 29 | } 30 | 31 | return { 32 | provider, 33 | } 34 | } 35 | 36 | module.exports = createProvider -------------------------------------------------------------------------------- /test/middleware/connectionCheckMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const chaiAsPromised = require('chai-as-promised') 3 | chai.use(chaiAsPromised) 4 | const assert = chai.assert 5 | const connectionCheckMiddleware = require('../../src/middleware/connectionCheckMiddleware') 6 | 7 | describe('connectionCheckMiddleware', function() { 8 | 9 | it('should fail with invalid http provider', function() { 10 | const options = { 11 | verbose: false, 12 | web3: 'http://in.val.id:4321', 13 | } 14 | assert.isRejected( connectionCheckMiddleware(options), /Node is not reachable/) 15 | }) 16 | 17 | it('should fail with invalid websocket provider', function() { 18 | const options = { 19 | verbose: false, 20 | web3: 'ws://in.val.id:4321', 21 | } 22 | assert.isRejected( connectionCheckMiddleware(options), /Node is not reachable/) 23 | }) 24 | 25 | it('should succeed with valid http provider', function() { 26 | const options = { 27 | verbose: false, 28 | web3: 'http://127.0.0.1:8545', 29 | } 30 | assert.isFulfilled(connectionCheckMiddleware(options)) 31 | }) 32 | 33 | it('should succeed with valid websocket provider', function() { 34 | const options = { 35 | verbose: false, 36 | web3: 'ws://127.0.0.1:8545', 37 | } 38 | assert.isFulfilled(connectionCheckMiddleware(options)) 39 | }) 40 | }) -------------------------------------------------------------------------------- /src/commands/setAddress.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const {formatsByName} = require('@ensdomains/address-encoder') 3 | 4 | exports.command = 'setAddress
[coinname]' 5 | 6 | exports.describe = 'Set address record' 7 | 8 | exports.builder = (yargs) => { 9 | return yargs 10 | .positional('ensname', { 11 | description: 'ENS Name to query or update', 12 | type: 'string', 13 | }) 14 | .positional('address', { 15 | description: 'Address to set or \'stdin\' to read from stdin', 16 | type: 'string', 17 | }) 18 | .positional('coinname', { 19 | description: 'Blockchain/Cryptocurrency address belongs to', 20 | type: 'string', 21 | choices: Object.keys(formatsByName), 22 | default: 'ETH', 23 | }) 24 | } 25 | 26 | exports.handler = async ({address, coinname, verbose, updater}) => { 27 | if (address === 'stdin') { 28 | verbose && console.log('Reading address from stdin...') 29 | address = fs.readFileSync(0).toString().trim() 30 | verbose && console.log(`\t Got address: ${address}.`) 31 | } 32 | // get SLIP-0044 coinType from coinname 33 | const coinType = formatsByName[coinname].coinType 34 | let result = await updater.setAddress({ 35 | address, 36 | coinType 37 | }) 38 | console.log(result) 39 | await updater.stop() 40 | // hardwire process.exit(0) here to fix problems with dangling HDWalletProvider engine for good. 41 | process.exit(0) 42 | } 43 | -------------------------------------------------------------------------------- /unitTest/middleware/requiresAccountMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert 2 | const requiresAccount = require('../../src/middleware/requiresAccountMiddleware') 3 | 4 | describe('requiresAccountMiddleware', function() { 5 | const cmdsNeedAccount = [ 6 | 'setContenthash', 7 | 'setAddress', 8 | 'clearAddress', 9 | 'clearContenthash', 10 | 'setReverseName', 11 | 'clearReverseName' 12 | ] 13 | const cmdsDontNeedAccount = [ 14 | 'getAddress', 15 | 'getContenthash', 16 | 'getInfo', 17 | 'listInterfaces', 18 | 'getReverseName' 19 | ] 20 | 21 | it('should throw on unknown commands', function() { 22 | const testargv = { 23 | _: ['unknownCommand'] 24 | } 25 | assert.throws(() => {requiresAccount(testargv)}, Error) 26 | }) 27 | 28 | cmdsNeedAccount.forEach(function(cmd) { 29 | it(`should require account for cmd ${cmd}`, function() { 30 | const testargv = { 31 | _: [cmd] 32 | } 33 | const result = requiresAccount(testargv) 34 | assert.deepEqual(result, {requiresAccount: true}) 35 | }) 36 | }) 37 | 38 | cmdsDontNeedAccount.forEach(function(cmd) { 39 | it(`should not require account for cmd ${cmd}`, function() { 40 | const testargv = { 41 | _: [cmd] 42 | } 43 | const result = requiresAccount(testargv) 44 | assert.deepEqual(result, {requiresAccount: false}) 45 | }) 46 | }) 47 | }) -------------------------------------------------------------------------------- /test/lib/lib.listinterface.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const Updater = require('../../lib') 3 | const ResolverInterfaces = require('../../lib/ResolverInterfaces') 4 | const chai = require('chai') 5 | const chaiAsPromised = require('chai-as-promised') 6 | chai.use(chaiAsPromised) 7 | const assert = chai.assert 8 | 9 | /* global web3 */ 10 | 11 | contract('lib - listinterface functions', function(accounts) { 12 | const accountIndex = 1 13 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 14 | const tld = 'test' 15 | const label = 'wayne' 16 | const ensName = label + '.' + tld 17 | let updater 18 | let registryAddress 19 | 20 | before('Get registry address', async function () { 21 | const registry = await ENSRegistry.deployed() 22 | registryAddress = registry.address 23 | }) 24 | 25 | beforeEach('provide fresh updater instance', async function () { 26 | const updaterOptions = { 27 | web3: web3, 28 | ensName: ensName, 29 | registryAddress: registryAddress, 30 | controllerAddress: controller, 31 | verbose: false, 32 | gasPrice: web3.utils.toBN('10000000000') 33 | } 34 | updater = new Updater() 35 | await updater.setup(updaterOptions) 36 | }) 37 | 38 | it('should list supported interfaces', async function () { 39 | let requiredInterfaceNames = Object.keys(ResolverInterfaces) 40 | let supportedInterfaceNames = await updater.listInterfaces() 41 | assert.sameMembers(requiredInterfaceNames, supportedInterfaceNames) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/lib/lib.gasprice.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const Updater = require('../../lib') 3 | const chai = require('chai') 4 | const chaiAsPromised = require('chai-as-promised') 5 | chai.use(chaiAsPromised) 6 | const assert = chai.assert 7 | 8 | /* global web3 */ 9 | 10 | const accountIndex = 1 11 | const tld = 'test' 12 | const label = 'wayne' 13 | const ensName = label+'.'+tld 14 | const coinTypeETH = 60 15 | let updater 16 | 17 | contract('lib - gasprice', function(accounts) { 18 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 19 | 20 | let updaterOptions = { 21 | web3: web3, 22 | ensName: ensName, 23 | registryAddress: undefined, 24 | controllerAddress: controller, 25 | verbose: false, 26 | dryrun: false, 27 | estimateGas: false, 28 | } 29 | 30 | before('Get registry address', async function() { 31 | const registry = await ENSRegistry.deployed() 32 | updaterOptions.registryAddress = registry.address 33 | }) 34 | 35 | it ('should use provided gasprice', async function() { 36 | let gasPriceWei = web3.utils.toBN('52000000000') 37 | updater = new Updater() 38 | updaterOptions.gasPrice = gasPriceWei 39 | await updater.setup(updaterOptions) 40 | let newaddress = accounts[4] 41 | const txHash = await updater.setAddress({ 42 | address: newaddress, 43 | coinType: coinTypeETH 44 | }) 45 | // Verify the default gasprice was used during transaction 46 | const txReceipt = await web3.eth.getTransaction(txHash) 47 | const actualGasprice = web3.utils.toBN(txReceipt.gasPrice) 48 | assert.isOk( 49 | actualGasprice.eq(gasPriceWei), 50 | `Actual ${actualGasprice.toString()} - expected ${gasPriceWei.toString()}` 51 | ) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /bin/ens-updater.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('dotenv').config() 3 | const yargs = require('yargs') 4 | const requiresAccount = require('../src/middleware/requiresAccountMiddleware') 5 | const getCredentials = require('../src/middleware/credentialsMiddleware') 6 | const connectionCheck = require('../src/middleware/connectionCheckMiddleware') 7 | const createProvider = require('../src/middleware/providerMiddleware') 8 | const createWeb3 = require('../src/middleware/web3Middleware') 9 | const getControllerAddress = require('../src/middleware/controllerAddressMiddleware') 10 | const createUpdater = require('../src/middleware/updaterMiddleware') 11 | const gasPriceMiddleware = require('../src/middleware/gaspriceMiddleware') 12 | 13 | const main = () => { 14 | // noinspection BadExpressionStatementJS 15 | yargs 16 | .usage('Usage: $0 [options]') 17 | .middleware(requiresAccount) 18 | .middleware(getCredentials) 19 | .middleware(connectionCheck) 20 | .middleware(createProvider) 21 | .middleware(createWeb3) 22 | .middleware(getControllerAddress) 23 | .middleware(gasPriceMiddleware) 24 | .middleware(createUpdater) 25 | .command(require('../src/commands/getInfo')) 26 | .command(require('../src/commands/setContenthash')) 27 | .command(require('../src/commands/getContenthash')) 28 | .command(require('../src/commands/clearContenthash')) 29 | .command(require('../src/commands/setAddress')) 30 | .command(require('../src/commands/getAddress')) 31 | .command(require('../src/commands/clearAddress')) 32 | .command(require('../src/commands/setReverseName')) 33 | .command(require('../src/commands/getReverseName')) 34 | .command(require('../src/commands/clearReverseName')) 35 | .command(require('../src/commands/listInterfaces')) 36 | .demandCommand(1) 37 | .options(require('../src/commands/sharedOptions.json')) 38 | .config() 39 | .help() 40 | .alias('help', 'h') 41 | .strict() 42 | .completion() 43 | .epilog('contact: michael@m-bauer.org') 44 | .epilog('github: https://github.com/TripleSpeeder/ens-updater') 45 | .argv 46 | } 47 | 48 | main() 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@triplespeeder/ens-updater", 3 | "version": "0.0.0-development", 4 | "description": "Set ENS name records from the commandline", 5 | "homepage": "https://github.com/TripleSpeeder/ens-updater", 6 | "main": "lib/index.js", 7 | "bin": { 8 | "ens-updater": "bin/ens-updater.js" 9 | }, 10 | "scripts": { 11 | "test:unit": "mocha unitTest/**/*.test.js --exit", 12 | "test:truffle": "truffle test", 13 | "test": "npm run test:unit && npm run test:truffle", 14 | "semantic-release": "semantic-release" 15 | }, 16 | "keywords": [ 17 | "ENS", 18 | "ipfs", 19 | "ethereum" 20 | ], 21 | "author": "michael@m-bauer.org", 22 | "repository": "https://github.com/TripleSpeeder/ens-updater", 23 | "license": "MIT", 24 | "husky": { 25 | "hooks": { 26 | "pre-commit": "lint-staged", 27 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 28 | } 29 | }, 30 | "lint-staged": { 31 | "*.js": [ 32 | "./node_modules/.bin/eslint --fix", 33 | "git add" 34 | ] 35 | }, 36 | "dependencies": { 37 | "@ensdomains/address-encoder": "^0.1.3", 38 | "@ensdomains/ens": "^0.4.0", 39 | "@ensdomains/ethregistrar": "^2.0.0", 40 | "@ensdomains/resolver": "^0.2.0", 41 | "@truffle/contract": "^4.1.7", 42 | "@truffle/hdwallet-provider": "^1.0.30", 43 | "connection-tester": "^0.2.0", 44 | "content-hash": "^2.5.2", 45 | "dotenv": "^8.2.0", 46 | "eth-ens-namehash": "^2.0.8", 47 | "web3": "^1.2.5", 48 | "yargs": "^15.1.0" 49 | }, 50 | "devDependencies": { 51 | "@commitlint/cli": "^8.3.5", 52 | "@commitlint/config-conventional": "^8.3.4", 53 | "@ensdomains/buffer": "0.0.10", 54 | "@ensdomains/dnssec-oracle": "^0.1.2", 55 | "@semantic-release/changelog": "^3.0.6", 56 | "@semantic-release/git": "^7.0.18", 57 | "chai": "^4.2.0", 58 | "chai-as-promised": "^7.1.1", 59 | "eslint": "^6.8.0", 60 | "execa": "^3.4.0", 61 | "husky": "^3.1.0", 62 | "lint-staged": "^9.5.0", 63 | "mocha": "^7.0.1", 64 | "semantic-release": "^15.13.31", 65 | "sinon": "^8.1.1", 66 | "web3-utils": "^1.2.5" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /unitTest/middleware/credentialsMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert 2 | const getCredentials = require('../../src/middleware/credentialsMiddleware') 3 | 4 | describe('getCredentialsMiddleware', function() { 5 | 6 | beforeEach(function () { 7 | delete process.env['MNEMONIC'] 8 | delete process.env['PRIVATE_KEY'] 9 | }) 10 | 11 | it('should not return anything if account is not required', function() { 12 | const options = { 13 | requiresAccount: false 14 | } 15 | const expected = {} 16 | assert.deepEqual(getCredentials(options), expected) 17 | }) 18 | 19 | it('should return private_key if account is required', function() { 20 | const PRIVATE_KEY = 'PRIVATE_KEY_STRING' 21 | const options = { 22 | requiresAccount: true 23 | } 24 | process.env.PRIVATE_KEY = PRIVATE_KEY 25 | const expected = { 26 | private_key: PRIVATE_KEY 27 | } 28 | assert.deepEqual(getCredentials(options), expected) 29 | 30 | }) 31 | 32 | it('should return mnemonic if account is required', function() { 33 | const MNEMONIC = 'MNEMONIC_STRING' 34 | const options = { 35 | requiresAccount: true 36 | } 37 | process.env.MNEMONIC = MNEMONIC 38 | const expected = { 39 | mnemonic: MNEMONIC 40 | } 41 | assert.deepEqual(getCredentials(options), expected) 42 | }) 43 | 44 | it('should throw if both private_key and mnemonic are set', function() { 45 | const PRIVATE_KEY = 'PRIVATE_KEY_STRING' 46 | const MNEMONIC = 'MNEMONIC_STRING' 47 | const options = { 48 | requiresAccount: true 49 | } 50 | process.env.PRIVATE_KEY = PRIVATE_KEY 51 | process.env.MNEMONIC = MNEMONIC 52 | assert.throws(()=>{getCredentials(options)}, /Got both mnemonic and private key/) 53 | }) 54 | 55 | it('should throw if account is required but neither mnemonic or private key is set', function() { 56 | const options = { 57 | requiresAccount: true 58 | } 59 | assert.throws(()=>{getCredentials(options)}, /Got neither mnemonic nor private key/) 60 | }) 61 | }) -------------------------------------------------------------------------------- /test/lib/lib.gas.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const Updater = require('../../lib') 3 | const chai = require('chai') 4 | const chaiAsPromised = require('chai-as-promised') 5 | chai.use(chaiAsPromised) 6 | const assert = chai.assert 7 | 8 | /* global web3 */ 9 | 10 | const accountIndex = 1 11 | const tld = 'test' 12 | const label = 'wayne' 13 | const ensName = label+'.'+tld 14 | const coinTypeETH = 60 15 | let updater 16 | 17 | contract('lib - gas', function(accounts) { 18 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 19 | 20 | let updaterOptions = { 21 | web3: web3, 22 | ensName: ensName, 23 | registryAddress: undefined, 24 | controllerAddress: controller, 25 | verbose: false, 26 | dryrun: false, 27 | estimateGas: false, 28 | } 29 | 30 | before('Get registry address', async function() { 31 | const registry = await ENSRegistry.deployed() 32 | updaterOptions.registryAddress = registry.address 33 | }) 34 | 35 | it ('should use automatic gas amount calculation when not specified', async function() { 36 | let expectedGas = web3.utils.toBN('70000') 37 | updater = new Updater() 38 | await updater.setup(updaterOptions) 39 | let newaddress = accounts[4] 40 | const txHash = await updater.setAddress({ 41 | address: newaddress, 42 | coinType: coinTypeETH 43 | }) 44 | const txReceipt = await web3.eth.getTransaction(txHash) 45 | const actualGas = web3.utils.toBN(txReceipt.gas) 46 | // Allow threshhold for slightly changing gas costs 47 | let threshold = web3.utils.toBN('5000') 48 | assert.isOk( 49 | (actualGas.gte(expectedGas.sub(threshold)) && actualGas.lte(expectedGas.add(threshold))), 50 | `Actual ${actualGas.toString()} - expected ${expectedGas.toString()}` 51 | ) 52 | }) 53 | 54 | it ('should use provided gas amount', async function() { 55 | let gas = web3.utils.toBN('500005') 56 | updater = new Updater() 57 | updaterOptions.gas = gas 58 | await updater.setup(updaterOptions) 59 | let newaddress = accounts[4] 60 | const txHash = await updater.setAddress({ 61 | address: newaddress, 62 | coinType: coinTypeETH 63 | }) 64 | const txReceipt = await web3.eth.getTransaction(txHash) 65 | const actualGas = web3.utils.toBN(txReceipt.gas) 66 | assert.isOk( 67 | actualGas.eq(gas), 68 | `Actual ${actualGas.toString()} - expected ${gas.toString()}` 69 | ) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /test/lib/lib.blockchainAddress.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const {formatsByCoinType} = require('@ensdomains/address-encoder') 3 | const Updater = require('../../lib') 4 | const chai = require('chai') 5 | const chaiAsPromised = require('chai-as-promised') 6 | chai.use(chaiAsPromised) 7 | const assert = chai.assert 8 | const {blockchainAddressTestcases} = require('../end2end/testdata') 9 | 10 | /* global web3 */ 11 | 12 | const accountIndex = 1 13 | const tld = 'test' 14 | const label = 'wayne' 15 | const ensName = label+'.'+tld 16 | let updater 17 | let registryAddress 18 | 19 | contract('lib - other blockchain address functions', function(accounts) { 20 | 21 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 22 | const zeroAddress = '0x0000000000000000000000000000000000000000' 23 | 24 | before('Get registry address', async function() { 25 | const registry = await ENSRegistry.deployed() 26 | registryAddress = registry.address 27 | }) 28 | 29 | before('provide fresh updater instance', async function() { 30 | const updaterOptions = { 31 | web3: web3, 32 | ensName: ensName, 33 | registryAddress: registryAddress, 34 | controllerAddress: controller, 35 | verbose: false, 36 | dryrun: false, 37 | gasPrice: web3.utils.toBN('10000000000') 38 | } 39 | updater = new Updater() 40 | await updater.setup(updaterOptions) 41 | }) 42 | 43 | for (let coin of Object.values(formatsByCoinType)) { 44 | it(`should return zero address for ${coin.name} (coinType ${coin.coinType})`, async function () { 45 | let address = await updater.getAddress(coin.coinType) 46 | assert.strictEqual(address, zeroAddress) 47 | }) 48 | } 49 | 50 | blockchainAddressTestcases.forEach(function (coin) { 51 | coin.addresses.forEach(function(address){ 52 | it(`Should set ${coin.name} address ${address}`, async function() { 53 | await updater.setAddress({address: address, coinType: coin.cointypeIndex}) 54 | let updatedAddress = await updater.getAddress(coin.cointypeIndex) 55 | assert.strictEqual(updatedAddress, address) 56 | }) 57 | }) 58 | }) 59 | 60 | blockchainAddressTestcases.forEach(function (coin) { 61 | it(`Should clear ${coin.name} address`, async function() { 62 | await updater.clearAddress(coin.cointypeIndex) 63 | let updatedAddress = await updater.getAddress(coin.cointypeIndex) 64 | assert.strictEqual(updatedAddress, zeroAddress) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /migrations/2_deploy_ens.js: -------------------------------------------------------------------------------- 1 | const OldENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistry') 2 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 3 | const FIFSRegistrar = artifacts.require('@ensdomains/ens/FIFSRegistrar') 4 | const ReverseRegistrar = artifacts.require('@ensdomains/ens/ReverseRegistrar') 5 | const PublicResolver = artifacts.require('@ensdomains/resolver/PublicResolver') 6 | 7 | const utils = require('web3-utils') 8 | const namehash = require('eth-ens-namehash') 9 | 10 | const tld = 'test' 11 | 12 | module.exports = function(deployer, network, accounts) { 13 | // Only deploy on development network! 14 | if (network !== 'development') { 15 | return 16 | } 17 | let oldEns,ens,resolver,registrar 18 | 19 | deployer.deploy(OldENSRegistry) 20 | .then(function(oldEnsRegistryInstance) { 21 | oldEns = oldEnsRegistryInstance 22 | return deployer.deploy(ENSRegistry, oldEns.address) 23 | }) 24 | // Resolver 25 | .then(function(ensInstance) { 26 | ens = ensInstance 27 | return deployer.deploy(PublicResolver, ens.address) 28 | }) 29 | .then(function(resolverInstance) { 30 | resolver = resolverInstance 31 | return setupResolver(ens, resolver, accounts) 32 | }) 33 | // Registrar 34 | .then(function() { 35 | return deployer.deploy(FIFSRegistrar, ens.address, namehash.hash(tld)) 36 | }) 37 | .then(function(registrarInstance) { 38 | registrar = registrarInstance 39 | return setupRegistrar(ens, registrar) 40 | }) 41 | // Reverse Registrar 42 | .then(function() { 43 | return deployer.deploy(ReverseRegistrar, ens.address, resolver.address) 44 | }) 45 | .then(function(reverseRegistrarInstance) { 46 | return setupReverseRegistrar(ens, resolver, reverseRegistrarInstance, accounts) 47 | }) 48 | } 49 | 50 | async function setupResolver(ens, resolver, accounts) { 51 | const resolverNode = namehash.hash('resolver') 52 | const resolverLabel = utils.sha3('resolver') 53 | 54 | await ens.setSubnodeOwner('0x0000000000000000000000000000000000000000', resolverLabel, accounts[0]) 55 | await ens.setResolver(resolverNode, resolver.address) 56 | await resolver.setAddr(resolverNode, resolver.address) 57 | } 58 | 59 | async function setupRegistrar(ens, registrar) { 60 | await ens.setSubnodeOwner('0x0000000000000000000000000000000000000000', utils.sha3(tld), registrar.address) 61 | } 62 | 63 | async function setupReverseRegistrar(ens, resolver, reverseRegistrar, accounts) { 64 | await ens.setSubnodeOwner('0x0000000000000000000000000000000000000000', utils.sha3('reverse'), accounts[0]) 65 | await ens.setSubnodeOwner(namehash.hash('reverse'), utils.sha3('addr'), reverseRegistrar.address) 66 | } -------------------------------------------------------------------------------- /test/end2end/gas.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const chai = require('chai') 3 | const chaiAsPromised = require('chai-as-promised') 4 | chai.use(chaiAsPromised) 5 | const assert = chai.assert 6 | const {runCommand} = require('./runCommand') 7 | const {private_keys} = require('./testdata').wallet 8 | 9 | /* global web3 */ 10 | 11 | contract('gas option', function(accounts) { 12 | 13 | const controllerAccountIndex = 1 14 | const private_key = private_keys[controllerAccountIndex] 15 | const scriptpath = 'bin/ens-updater.js' 16 | const providerstring = 'http://localhost:8545' 17 | const ensName = 'wayne.test' 18 | let registryAddress 19 | 20 | before('Get registry address', async function() { 21 | const registry = await ENSRegistry.deployed() 22 | registryAddress = registry.address 23 | }) 24 | 25 | it('Should use default gas when no option set', async function() { 26 | let expectedGas = web3.utils.toBN('70000') 27 | const targetAddress = accounts[3] 28 | // set address 29 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} --web3 ${providerstring} --registryAddress ${registryAddress}` 30 | const options = {env: { PRIVATE_KEY: private_key}} 31 | let childResult = await runCommand(setAddressCmd, options) 32 | assert.isFalse(childResult.failed) 33 | const txHash = childResult.stdout 34 | assert.match(txHash, /^0x/) 35 | const transaction = await web3.eth.getTransaction(txHash) 36 | const actualGas = web3.utils.toBN(transaction.gas) 37 | // Allow threshhold for slightly changing gas costs 38 | const threshold = web3.utils.toBN('5000') 39 | assert.isOk( 40 | (actualGas.gte(expectedGas.sub(threshold)) && actualGas.lte(expectedGas.add(threshold))), 41 | `Actual ${actualGas.toString()} - expected ${expectedGas.toString()}` 42 | ) 43 | }) 44 | 45 | it('Should use provided gas amount', async function() { 46 | let gas = web3.utils.toBN('500005') 47 | const targetAddress = accounts[3] 48 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} --gas ${gas} --web3 ${providerstring} --registryAddress ${registryAddress}` 49 | const options = {env: { PRIVATE_KEY: private_key}} 50 | let childResult = await runCommand(setAddressCmd, options) 51 | assert.isFalse(childResult.failed) 52 | const txHash = childResult.stdout 53 | assert.match(txHash, /^0x/) 54 | 55 | // Verify the provided gas was used during transaction 56 | const transaction = await web3.eth.getTransaction(txHash) 57 | const actualGas = web3.utils.toBN(transaction.gas) 58 | assert.isOk( 59 | actualGas.eq(gas), 60 | `Actual ${actualGas.toString()} - expected ${gas.toString()}` 61 | ) 62 | }) 63 | }) -------------------------------------------------------------------------------- /migrations/3_prepare_test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const FIFSRegistrar = artifacts.require('@ensdomains/ens/FIFSRegistrar') 3 | const PublicResolver = artifacts.require('@ensdomains/resolver/PublicResolver') 4 | const ReverseRegistrar = artifacts.require('@ensdomains/ens/ReverseRegistrar') 5 | 6 | 7 | const utils = require('web3-utils') 8 | const namehash = require('eth-ens-namehash') 9 | 10 | const controllerAccountIndex = 1 11 | 12 | module.exports = async function(deployer, network, accounts) { 13 | // Only run this on development network! 14 | if (network !== 'development') { 15 | return 16 | } 17 | 18 | const registry = await ENSRegistry.deployed() 19 | const registrar = await FIFSRegistrar.deployed() 20 | const resolver = await PublicResolver.deployed() 21 | const reverseRegistrar = await ReverseRegistrar.deployed() 22 | 23 | await setupNames(registry, registrar, resolver, reverseRegistrar, accounts) 24 | } 25 | 26 | async function register(label, registrar, owner) { 27 | const labelHash = utils.sha3(label) 28 | await registrar.register(labelHash, owner, { from: owner }) 29 | // console.log(`Registered '${label}' owned by ${owner}.`) 30 | } 31 | 32 | async function setResolver(ensName, registry, resolverAddress, owner) { 33 | const node = namehash.hash(ensName) 34 | await registry.setResolver(node, resolverAddress, {from: owner}) 35 | // console.log(`Set resolver for node ${node} to ${resolverAddress}`) 36 | } 37 | 38 | async function setupNames(registry, registrar, resolver, reverseRegistrar, accounts) { 39 | // console.log('Using registry at ' + registry.address) 40 | 41 | // 'noresolver.test' has no resolver set 42 | await register('noresolver', registrar, accounts[controllerAccountIndex]) 43 | 44 | // 'wayne.test' has a resolver set, but no reverse name 45 | await register('wayne', registrar, accounts[controllerAccountIndex]) 46 | await setResolver('wayne.test', registry, resolver.address, accounts[1]) 47 | 48 | // 'reverse.test' is registered and resolves to accounts[2] 49 | // accounts[2] has a full reverse entry pointing back to 'reverse.test' 50 | await register('reverse', registrar, accounts[2]) 51 | await setResolver('reverse.test', registry, resolver.address, accounts[2]) 52 | await reverseRegistrar.setName('reverse.test', {from: accounts[2]}) 53 | 54 | // 'halfreverse.test' is registered and resolves to accounts[3] 55 | // accounts[3] has a partial reverse setup: Reverse resolver is set, but no name is set in resolver 56 | let defaultResolver = await reverseRegistrar.defaultResolver() 57 | await register('halfreverse', registrar, accounts[3]) 58 | await setResolver('halfreverse.test', registry, resolver.address, accounts[3]) 59 | await reverseRegistrar.claimWithResolver(accounts[3], defaultResolver, {from: accounts[3]}) 60 | 61 | // 'noreverse.test' is registered to accounts[4] but no reverse name is set 62 | await register('noreverse', registrar, accounts[4]) 63 | await setResolver('noreverse.test', registry, resolver.address, accounts[4]) 64 | } 65 | -------------------------------------------------------------------------------- /test/end2end/testdata.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | // mnemonic and private keys used by ganache in deterministic mode (-d) 4 | wallet: { 5 | mnemonic: 'myth like bonus scare over problem client lizard pioneer submit female collect', 6 | private_keys: [ 7 | '0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d', 8 | '0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1', 9 | '0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c', 10 | '0x646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913', 11 | '0xadd53f9a7e588d003326d1cbf9e4a43c061aadd9bc938c843a79e7b4fd2ad743', 12 | '0x395df67f0c2d2d9fe1ad08d1bc8b6627011959b79c53d7dd6a3536a33ab8a4fd', 13 | '0xe485d098507f54e7733a205420dfddbe58db035fa577fc294ebd14db90767a52', 14 | '0xa453611d9419d0e56f499079478fd72c37b251a94bfde4d19872c44cf65386e3', 15 | '0x829e924fdf021ba3dbbc4225edfece9aca04b929d6e75613329ca6f1d31c0bb4', 16 | '0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773', 17 | ] 18 | }, 19 | // Testcases for other blockchain addresses taken from EIP 2304 (https://eips.ethereum.org/EIPS/eip-2304) 20 | blockchainAddressTestcases: [ 21 | { 22 | name: 'Bitcoin', 23 | symbol: 'BTC', 24 | cointypeIndex: 0, 25 | addresses: [ 26 | '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', 27 | '3Ai1JZ8pdJb2ksieUV8FsxSNVJCpoPi8W6', 28 | 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4' 29 | ] 30 | }, 31 | { 32 | name: 'Litecoin', 33 | symbol: 'LTC', 34 | cointypeIndex: 2, 35 | addresses: [ 36 | 'LaMT348PWRnrqeeWArpwQPbuanpXDZGEUz', 37 | 'MQMcJhpWHYVeQArcZR3sBgyPZxxRtnH441', 38 | 'ltc1qdp7p2rpx4a2f80h7a4crvppczgg4egmv5c78w8' 39 | ] 40 | }, 41 | { 42 | name: 'Dogecoin', 43 | symbol: 'DOGE', 44 | cointypeIndex: 3, 45 | addresses: [ 46 | 'DBXu2kgc3xtvCUWFcxFE3r9hEYgmuaaCyD', 47 | 'AF8ekvSf6eiSBRspJjnfzK6d1EM6pnPq3G' 48 | ] 49 | }, 50 | { 51 | name: 'Monacoin', 52 | symbol: 'MONA', 53 | cointypeIndex: 22, 54 | addresses: ['MHxgS2XMXjeJ4if2PRRbWYcdwZPWfdwaDT'] 55 | }, 56 | { 57 | name: 'Ethereum Classic', 58 | symbol: 'ETC', 59 | cointypeIndex: 61, 60 | addresses: ['0x314159265dD8dbb310642f98f50C066173C1259b'] 61 | }, 62 | { 63 | name: 'Rootstock', 64 | symbol: 'RSK', 65 | cointypeIndex: 137, 66 | addresses: ['0x5aaEB6053f3e94c9b9a09f33669435E7ef1bEAeD'] 67 | }, 68 | { 69 | name: 'Ripple', 70 | symbol: 'XRP', 71 | cointypeIndex: 144, 72 | addresses: [ 73 | 'rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn', 74 | 'X7qvLs7gSnNoKvZzNWUT2e8st17QPY64PPe7zriLNuJszeg' 75 | ] 76 | }, 77 | { 78 | name: 'Bitcoin Cash', 79 | symbol: 'BCH', 80 | cointypeIndex: 145, 81 | addresses: [ 82 | 'bitcoincash:qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a', 83 | 'bitcoincash:ppm2qsznhks23z7629mms6s4cwef74vcwvn0h829pq' 84 | ] 85 | }, 86 | { 87 | name: 'Binance', 88 | symbol: 'BNB', 89 | cointypeIndex: 714, 90 | addresses: ['bnb1grpf0955h0ykzq3ar5nmum7y6gdfl6lxfn46h2'] 91 | } 92 | ] 93 | } -------------------------------------------------------------------------------- /unitTest/middleware/providerMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('chai').assert 2 | const HDWalletProvider = require('@truffle/hdwallet-provider') 3 | const providerMiddlerware = require('../../src/middleware/providerMiddleware') 4 | 5 | describe('providerMiddleware', function() { 6 | 7 | const VALID_MNEMONIC = 'arrest erupt vintage embrace coast shoulder pond cattle toy hello cloud nurse' 8 | const INVALID_MNEMONIC = 'some garbage text' 9 | const VALID_CONNECTIONSTRING = 'http://localhost:8545' 10 | const PRIVATE_KEY = '0xABCDEF1234567890' 11 | 12 | it('should return plain connectionstring when no account required', function() { 13 | const options = { 14 | verbose: false, 15 | accountIndex: 0, 16 | web3: VALID_CONNECTIONSTRING, 17 | requiresAccount: false 18 | } 19 | const expected = { 20 | provider: options.web3, 21 | } 22 | assert.deepEqual(providerMiddlerware(options), expected) 23 | }) 24 | 25 | it('should return HDWalletProvider when account is required (with mnemonic)', function() { 26 | const options = { 27 | verbose: false, 28 | accountIndex: 0, 29 | web3: VALID_CONNECTIONSTRING, 30 | requiresAccount: true, 31 | mnemonic: VALID_MNEMONIC 32 | } 33 | const result = providerMiddlerware(options) 34 | assert.instanceOf(result.provider, HDWalletProvider) 35 | assert.equal(result.provider.getAddresses().length, 1) 36 | result.provider.engine.stop() 37 | }) 38 | 39 | it('should fail when account is required but mnemonic is invalid', function() { 40 | const options = { 41 | verbose: false, 42 | accountIndex: 0, 43 | web3: VALID_CONNECTIONSTRING, 44 | requiresAccount: true, 45 | mnemonic: INVALID_MNEMONIC 46 | } 47 | assert.throws(()=>{providerMiddlerware(options)}, Error, /Mnemonic invalid or undefined/) 48 | }) 49 | 50 | it('should return HDWalletProvider with one address when account is required (with private key)', function() { 51 | const options = { 52 | verbose: false, 53 | accountIndex: 0, 54 | web3: VALID_CONNECTIONSTRING, 55 | requiresAccount: true, 56 | private_key: PRIVATE_KEY 57 | } 58 | const result = providerMiddlerware(options) 59 | assert.instanceOf(result.provider, HDWalletProvider) 60 | result.provider.engine.stop() 61 | }) 62 | 63 | it('should fail when account is required and neither mnemonic nor private key are provided', function() { 64 | const options = { 65 | verbose: false, 66 | accountIndex: 0, 67 | web3: 'connectionstring', 68 | requiresAccount: true, 69 | } 70 | assert.throws(()=>{providerMiddlerware(options)}, Error, /No account available./) 71 | }) 72 | 73 | it('should fail when account is required and both mnemonic and private key are provided', function() { 74 | const options = { 75 | verbose: false, 76 | accountIndex: 0, 77 | web3: VALID_CONNECTIONSTRING, 78 | requiresAccount: true, 79 | private_key: PRIVATE_KEY, 80 | mnemonic: VALID_MNEMONIC 81 | } 82 | assert.throws(()=>{providerMiddlerware(options)}, Error, /Both PRIVATE_KEY and MNEMONIC are set/) 83 | }) 84 | 85 | it('should return HDWalletProvider with requested number of accounts', function() { 86 | const accountIndex = 5 87 | const options = { 88 | verbose: false, 89 | accountIndex: accountIndex, 90 | web3: VALID_CONNECTIONSTRING, 91 | requiresAccount: true, 92 | mnemonic: VALID_MNEMONIC 93 | } 94 | const result = providerMiddlerware(options) 95 | assert.instanceOf(result.provider, HDWalletProvider) 96 | assert.equal(result.provider.getAddresses().length, accountIndex+1) 97 | result.provider.engine.stop() 98 | }) 99 | 100 | }) -------------------------------------------------------------------------------- /test/end2end/reverseName.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const chai = require('chai') 3 | const chaiAsPromised = require('chai-as-promised') 4 | chai.use(chaiAsPromised) 5 | const assert = chai.assert 6 | const {runCommand} = require('./runCommand') 7 | const {wallet} = require('./testdata') 8 | 9 | contract('get/set reverse names', function(accounts) { 10 | const controllerAccountIndex = 1 11 | const private_key = wallet.private_keys[controllerAccountIndex] 12 | const scriptpath = 'bin/ens-updater.js' 13 | const providerstring = 'http://localhost:8545' 14 | const ensName = 'wayne.test' 15 | let registryAddress 16 | 17 | before('Get registry address', async function() { 18 | const registry = await ENSRegistry.deployed() 19 | registryAddress = registry.address 20 | }) 21 | 22 | it('should have no reverse name set', async function() { 23 | const command = `${scriptpath} getReverseName ${accounts[controllerAccountIndex]} --web3 ${providerstring} --registryAddress ${registryAddress}` 24 | const childResult = await runCommand(command) 25 | assert.isFalse(childResult.failed) 26 | assert.match(childResult.stdout, /No reverse name record set/) 27 | }) 28 | 29 | it('should estimate gas for setting reverse name', async function() { 30 | const setReverseNameCmd = `${scriptpath} setReverseName ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress} --estimateGas` 31 | const options = {env: { PRIVATE_KEY: private_key}} 32 | let childResult = await runCommand(setReverseNameCmd, options) 33 | assert.isFalse(childResult.failed) 34 | assert.closeTo(parseInt(childResult.stdout), 125000, 15000) 35 | 36 | // Verify still no reverse name is set 37 | const getReverseNameCmd = `${scriptpath} getReverseName ${accounts[controllerAccountIndex]} --web3 ${providerstring} --registryAddress ${registryAddress}` 38 | childResult = await runCommand(getReverseNameCmd) 39 | assert.isFalse(childResult.failed) 40 | assert.match(childResult.stdout, /No reverse name record set/) 41 | }) 42 | 43 | it('should set reverse name', async function() { 44 | const setReverseNameCmd = `${scriptpath} setReverseName ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 45 | const options = {env: { PRIVATE_KEY: private_key}} 46 | let childResult = await runCommand(setReverseNameCmd, options) 47 | assert.isFalse(childResult.failed) 48 | assert.match(childResult.stdout, /^0x/) 49 | 50 | // Verify new reverse name is set 51 | const getReverseNameCmd = `${scriptpath} getReverseName ${accounts[controllerAccountIndex]} --web3 ${providerstring} --registryAddress ${registryAddress}` 52 | childResult = await runCommand(getReverseNameCmd) 53 | assert.isFalse(childResult.failed) 54 | assert.equal(childResult.stdout, ensName) 55 | }) 56 | 57 | it('should estimate gas for clearing reverse name', async function() { 58 | const clearReverseNameCmd = `${scriptpath} clearReverseName --web3 ${providerstring} --registryAddress ${registryAddress} --estimateGas` 59 | const options = {env: { PRIVATE_KEY: private_key}} 60 | let childResult = await runCommand(clearReverseNameCmd, options) 61 | assert.isFalse(childResult.failed) 62 | assert.closeTo(parseInt(childResult.stdout), 55000, 8000) 63 | }) 64 | 65 | it('should clear reverse name', async function() { 66 | const clearReverseNameCmd = `${scriptpath} clearReverseName --web3 ${providerstring} --registryAddress ${registryAddress}` 67 | const options = {env: { PRIVATE_KEY: private_key}} 68 | let childResult = await runCommand(clearReverseNameCmd, options) 69 | assert.isFalse(childResult.failed) 70 | assert.match(childResult.stdout, /^0x/) 71 | 72 | const command = `${scriptpath} getReverseName ${accounts[controllerAccountIndex]} --web3 ${providerstring} --registryAddress ${registryAddress}` 73 | childResult = await runCommand(command) 74 | assert.isFalse(childResult.failed) 75 | assert.match(childResult.stdout, /No reverse name record set/) 76 | }) 77 | 78 | }) -------------------------------------------------------------------------------- /test/end2end/errorconditions.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const chai = require('chai') 3 | const chaiAsPromised = require('chai-as-promised') 4 | chai.use(chaiAsPromised) 5 | const assert = chai.assert 6 | const {private_keys, mnemonic} = require('./testdata').wallet 7 | const {runCommand} = require('./runCommand') 8 | 9 | 10 | contract('errorConditions', function(accounts) { 11 | 12 | const controllerAccountIndex = 1 13 | const private_key = private_keys[controllerAccountIndex] 14 | const scriptpath = 'bin/ens-updater.js' 15 | const providerstring = 'http://localhost:8545' 16 | let registryAddress 17 | 18 | before('Get registry address', async function() { 19 | const registry = await ENSRegistry.deployed() 20 | registryAddress = registry.address 21 | }) 22 | 23 | it('Should show usage info when no commands are specified', function() { 24 | const childResult = runCommand(scriptpath) 25 | assert.isTrue(childResult.failed, 'Command should have failed') 26 | assert.match(childResult.stderr, /Usage: ens-updater.js \[options]/) 27 | }) 28 | 29 | it('Should show error message when web3 connectionstring is invalid and no account required', function() { 30 | const command = `${scriptpath} getContenthash wayne.test --web3 http://in.val.id:12345` 31 | const childResult = runCommand(command) 32 | assert.isTrue(childResult.failed, 'Command should have failed') 33 | assert.match(childResult.stderr, /Node is not reachable at/) 34 | }) 35 | 36 | it('Should show error message when web3 connectionstring is invalid and account is required', function() { 37 | const command = `${scriptpath} setAddress wayne.test 0x123 --web3 http://in.valid:12345` 38 | const options = { 39 | env: { PRIVATE_KEY: private_key } 40 | } 41 | const childResult = runCommand(command, options) 42 | assert.isTrue(childResult.failed, 'Command should have failed') 43 | assert.match(childResult.stderr, /Node is not reachable at/) 44 | }) 45 | 46 | it('Should show error message when account is required but no credentials are provided', async function() { 47 | const targetAddress = accounts[3] 48 | const command = `${scriptpath} setAddress wayne.test ${targetAddress} --web3 ${providerstring}` 49 | const childResult = runCommand(command) 50 | assert.isTrue(childResult.failed, 'Command should have failed') 51 | assert.match(childResult.stderr, /Got neither mnemonic nor private key/) 52 | }) 53 | 54 | it('Should show error message when account is required and both mnemonic and private key are provided', async function() { 55 | const targetAddress = accounts[3] 56 | const command = `${scriptpath} setAddress wayne.test ${targetAddress} --web3 ${providerstring}` 57 | const options = { 58 | env: { 59 | MNEMONIC: mnemonic, 60 | PRIVATE_KEY: private_key 61 | } 62 | } 63 | const childResult = runCommand(command, options) 64 | assert.isTrue(childResult.failed, 'Command should have failed') 65 | assert.match(childResult.stderr, /Got both mnemonic and private key/) 66 | }) 67 | 68 | it('Should show error message when provided account is not controller of ensname', async function() { 69 | const targetAddress = accounts[3] 70 | const other_private_key = private_keys[3] 71 | const command = `${scriptpath} setAddress wayne.test ${targetAddress} --web3 ${providerstring} --registryAddress ${registryAddress}` 72 | const options = { 73 | env: { PRIVATE_KEY: other_private_key } 74 | } 75 | const childResult = runCommand(command, options) 76 | assert.isTrue(childResult.failed, 'Command should have failed') 77 | assert.match(childResult.stderr, /is not controller of wayne.test./) 78 | }) 79 | 80 | it('Should show error message when resolver is required but not set', async function() { 81 | const targetAddress = accounts[3] 82 | const command = `${scriptpath} setAddress noresolver.test ${targetAddress} --web3 ${providerstring} --registryAddress ${registryAddress}` 83 | const options = { 84 | env: { PRIVATE_KEY: private_key } 85 | } 86 | const childResult = runCommand(command, options) 87 | assert.isTrue(childResult.failed, 'Command should have failed') 88 | assert.match(childResult.stderr, /No resolver set for /) 89 | }) 90 | }) -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Use this file to configure your truffle project. It's seeded with some 3 | * common settings for different networks and features like migrations, 4 | * compilation and testing. Uncomment the ones you need or modify 5 | * them to suit your project as necessary. 6 | * 7 | * More information about configuration can be found at: 8 | * 9 | * truffleframework.com/docs/advanced/configuration 10 | * 11 | * To deploy via Infura you'll need a wallet provider (like truffle-hdwallet-provider) 12 | * to sign your transactions before they're sent to a remote public node. Infura accounts 13 | * are available for free at: infura.io/register. 14 | * 15 | * You'll also need a mnemonic - the twelve word phrase the wallet uses to generate 16 | * public/private key pairs. If you're publishing your code to GitHub make sure you load this 17 | * phrase from a file you've .gitignored so it doesn't accidentally become public. 18 | * 19 | */ 20 | 21 | // const HDWalletProvider = require('truffle-hdwallet-provider'); 22 | // const infuraKey = "fj4jll3k....."; 23 | // 24 | // const fs = require('fs'); 25 | // const mnemonic = fs.readFileSync(".secret").toString().trim(); 26 | 27 | module.exports = { 28 | /** 29 | * Networks define how you connect to your ethereum client and let you set the 30 | * defaults web3 uses to send transactions. If you don't specify one truffle 31 | * will spin up a development blockchain for you on port 9545 when you 32 | * run `develop` or `test`. You can ask a truffle command to use a specific 33 | * network from the command line, e.g 34 | * 35 | * $ truffle test --network 36 | */ 37 | 38 | networks: { 39 | // Useful for testing. The `development` name is special - truffle uses it by default 40 | // if it's defined here and no other network is specified at the command line. 41 | // You should run a client (like ganache-cli, geth or parity) in a separate terminal 42 | // tab if you use this network and you must also set the `host`, `port` and `network_id` 43 | // options below to some value. 44 | // 45 | development: { 46 | host: '127.0.0.1', // Localhost (default: none) 47 | port: 8545, // Standard Ethereum port (default: none) 48 | network_id: '*', // Any network (default: none) 49 | }, 50 | 51 | main: { 52 | host: 'fullnode.dappnode', 53 | port: 8545, 54 | network_id: 1, 55 | } 56 | 57 | // Another network with more advanced options... 58 | // advanced: { 59 | // port: 8777, // Custom port 60 | // network_id: 1342, // Custom network 61 | // gas: 8500000, // Gas sent with each transaction (default: ~6700000) 62 | // gasPrice: 20000000000, // 20 gwei (in wei) (default: 100 gwei) 63 | // from:
, // Account to send txs from (default: accounts[0]) 64 | // websockets: true // Enable EventEmitter interface for web3 (default: false) 65 | // }, 66 | 67 | // Useful for deploying to a public network. 68 | // NB: It's important to wrap the provider as a function. 69 | // ropsten: { 70 | // provider: () => new HDWalletProvider(mnemonic, `https://ropsten.infura.io/v3/YOUR-PROJECT-ID`), 71 | // network_id: 3, // Ropsten's id 72 | // gas: 5500000, // Ropsten has a lower block limit than mainnet 73 | // confirmations: 2, // # of confs to wait between deployments. (default: 0) 74 | // timeoutBlocks: 200, // # of blocks before a deployment times out (minimum/default: 50) 75 | // skipDryRun: true // Skip dry run before migrations? (default: false for public nets ) 76 | // }, 77 | 78 | // Useful for private networks 79 | // private: { 80 | // provider: () => new HDWalletProvider(mnemonic, `https://network.io`), 81 | // network_id: 2111, // This network is yours, in the cloud. 82 | // production: true // Treats this network as if it was a public net. (default: false) 83 | // } 84 | }, 85 | 86 | // Set default mocha options here, use special reporters etc. 87 | mocha: { 88 | timeout: 10000 89 | }, 90 | 91 | // Configure your compilers 92 | compilers: { 93 | solc: { 94 | // version: "0.5.1", // Fetch exact version from solc-bin (default: truffle's version) 95 | // docker: true, // Use "0.5.1" you've installed locally with docker (default: false) 96 | // settings: { // See the solidity docs for advice about optimization and evmVersion 97 | // optimizer: { 98 | // enabled: false, 99 | // runs: 200 100 | // }, 101 | // evmVersion: "byzantium" 102 | // } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/end2end/gasprice.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const gasPriceOptions = require('../../src/commands/sharedOptions.json').gasPrice 3 | const chai = require('chai') 4 | const chaiAsPromised = require('chai-as-promised') 5 | chai.use(chaiAsPromised) 6 | const assert = chai.assert 7 | const {runCommand} = require('./runCommand') 8 | const {private_keys} = require('./testdata').wallet 9 | 10 | /* global web3 */ 11 | 12 | contract('gasPrice option', function(accounts) { 13 | 14 | const controllerAccountIndex = 1 15 | const private_key = private_keys[controllerAccountIndex] 16 | const scriptpath = 'bin/ens-updater.js' 17 | const providerstring = 'http://localhost:8545' 18 | const ensName = 'wayne.test' 19 | let registryAddress 20 | 21 | before('Get registry address', async function() { 22 | const registry = await ENSRegistry.deployed() 23 | registryAddress = registry.address 24 | }) 25 | 26 | it('Should use default gasPrice when no option set', async function() { 27 | const defaultGaspriceGWei = web3.utils.toBN(gasPriceOptions.default) 28 | const defaultGaspriceWei = web3.utils.toWei(defaultGaspriceGWei, 'gwei') 29 | const targetAddress = accounts[3] 30 | // set address 31 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} --web3 ${providerstring} --registryAddress ${registryAddress}` 32 | const options = {env: { PRIVATE_KEY: private_key}} 33 | let childResult = await runCommand(setAddressCmd, options) 34 | assert.isFalse(childResult.failed) 35 | const txHash = childResult.stdout 36 | assert.match(txHash, /^0x/) 37 | 38 | // Verify the default gasPrice was used during transaction 39 | const transaction = await web3.eth.getTransaction(txHash) 40 | const actualGasprice = web3.utils.toBN(transaction.gasPrice) 41 | assert.isOk( 42 | actualGasprice.eq(defaultGaspriceWei), 43 | `Actual ${actualGasprice.toString()} - expected ${defaultGaspriceWei.toString()}` 44 | ) 45 | }) 46 | 47 | it('Should use provided gasPrice', async function() { 48 | const targetAddress = accounts[3] 49 | let gasPriceWei = web3.utils.toBN('5000000000') 50 | let gasPriceGWei = web3.utils.fromWei(gasPriceWei, 'gwei') 51 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} --gasPrice ${gasPriceGWei} --web3 ${providerstring} --registryAddress ${registryAddress}` 52 | const options = {env: { PRIVATE_KEY: private_key}} 53 | let childResult = await runCommand(setAddressCmd, options) 54 | assert.isFalse(childResult.failed) 55 | const txHash = childResult.stdout 56 | assert.match(txHash, /^0x/) 57 | 58 | // Verify the provided gasPrice was used during transaction 59 | const transaction = await web3.eth.getTransaction(txHash) 60 | const actualGasprice = web3.utils.toBN(transaction.gasPrice) 61 | assert.isOk( 62 | actualGasprice.eq(gasPriceWei), 63 | `Actual ${actualGasprice.toString()} - expected ${gasPriceWei.toString()}` 64 | ) 65 | }) 66 | 67 | it('Should show error message when gasPrice is too high', async function() { 68 | // The internal limit is set to 500 gwei. Anything above this value will be considered user error and rejected. 69 | const targetAddress = accounts[3] 70 | let gasPriceWei = web3.utils.toBN('501000000000') 71 | let gasPriceGWei = web3.utils.fromWei(gasPriceWei, 'gwei') 72 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} --gasPrice ${gasPriceGWei} --web3 ${providerstring} --registryAddress ${registryAddress}` 73 | const options = {env: { PRIVATE_KEY: private_key}} 74 | let childResult = await runCommand(setAddressCmd, options) 75 | assert.isTrue(childResult.failed, 'Command should have failed') 76 | assert.match(childResult.stderr, /Gas price too high/) 77 | }) 78 | 79 | it('Should display gas price info in verbose mode', async function() { 80 | const targetAddress = accounts[3] 81 | let gasPriceWei = web3.utils.toBN('5000000000') 82 | let gasPriceGWei = web3.utils.fromWei(gasPriceWei, 'gwei') 83 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} --verbose --gasPrice ${gasPriceGWei} --web3 ${providerstring} --registryAddress ${registryAddress}` 84 | const options = {env: { PRIVATE_KEY: private_key}} 85 | let childResult = await runCommand(setAddressCmd, options) 86 | assert.isFalse(childResult.failed) 87 | assert.match(childResult.stdout, new RegExp(`Setting gas price: ${gasPriceGWei.toString()} gwei`)) 88 | }) 89 | 90 | it('Should not display gas price info in verbose mode for read-only commands', async function() { 91 | let gasPriceWei = web3.utils.toBN('5000000000') 92 | let gasPriceGWei = web3.utils.fromWei(gasPriceWei, 'gwei') 93 | const getAddressCmd = `${scriptpath} getAddress ${ensName} --verbose --gasPrice ${gasPriceGWei} --web3 ${providerstring} --registryAddress ${registryAddress}` 94 | let childResult = await runCommand(getAddressCmd) 95 | assert.isFalse(childResult.failed, 'Command should not fail') 96 | assert.notMatch(childResult.stdout, /Setting gas price:/) 97 | }) 98 | }) -------------------------------------------------------------------------------- /test/lib/lib.name.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const Updater = require('../../lib') 3 | const chai = require('chai') 4 | const chaiAsPromised = require('chai-as-promised') 5 | chai.use(chaiAsPromised) 6 | const assert = chai.assert 7 | 8 | /* global web3 */ 9 | 10 | let updater 11 | let registryAddress 12 | 13 | contract('lib - reverse name estimategas', function(accounts) { 14 | const accountIndex = 2 15 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 16 | 17 | before('Get registry address', async function() { 18 | const registry = await ENSRegistry.deployed() 19 | registryAddress = registry.address 20 | }) 21 | 22 | beforeEach('provide fresh updater instance', async function() { 23 | const updaterOptions = { 24 | web3: web3, 25 | registryAddress: registryAddress, 26 | controllerAddress: controller, 27 | verbose: false, 28 | estimateGas: true, 29 | } 30 | updater = new Updater() 31 | await updater.setup(updaterOptions) 32 | }) 33 | 34 | it ('should return gas estimate for getting reverse name', async function() { 35 | let gasEstimate = await updater.getReverseName(controller) 36 | assert.isNumber(gasEstimate) 37 | assert.isAbove(gasEstimate, 100) 38 | }) 39 | 40 | it ('should return gas estimate for setting reverse name', async function() { 41 | // update reverse name with estimateGas option set 42 | let newName = 'another.test' 43 | let gasEstimate = await updater.setReverseName(newName) 44 | assert.isNumber(gasEstimate) 45 | assert.isAbove(gasEstimate, 100) 46 | }) 47 | }) 48 | 49 | contract('lib - reverse name getter', function(accounts) { 50 | 51 | before('Get registry address', async function() { 52 | const registry = await ENSRegistry.deployed() 53 | registryAddress = registry.address 54 | }) 55 | 56 | beforeEach('provide fresh updater instance', async function() { 57 | const updaterOptions = { 58 | web3: web3, 59 | registryAddress: registryAddress, 60 | verbose: false, 61 | } 62 | updater = new Updater() 63 | await updater.setup(updaterOptions) 64 | }) 65 | 66 | it ('should fail when invalid address is provided', function() { 67 | let address = '0xsomeThing' 68 | assert.isRejected(updater.getReverseName(address)) 69 | }) 70 | 71 | it ('should handle no reverseResolver being set', async function() { 72 | let address = accounts[1] 73 | let reverseName = await updater.getReverseName(address) 74 | assert.strictEqual(reverseName, '') 75 | }) 76 | 77 | it ('should handle reverseResolver being set and name record not being set', async function() { 78 | let address = accounts[3] 79 | let reverseName = await updater.getReverseName(address) 80 | assert.strictEqual(reverseName, '') 81 | }) 82 | 83 | it ('should get reverse name record for address', async function() { 84 | let address = accounts[2] 85 | let reverseName = await updater.getReverseName(address) 86 | assert.strictEqual(reverseName, 'reverse.test') 87 | }) 88 | }) 89 | 90 | contract('lib - reverse name setter', function(accounts) { 91 | const accountIndex = 4 92 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 93 | 94 | before('Get registry address', async function() { 95 | const registry = await ENSRegistry.deployed() 96 | registryAddress = registry.address 97 | }) 98 | 99 | it ('should not set reverse name with --dry-run option', async function() { 100 | const updaterOptions = { 101 | web3: web3, 102 | registryAddress: registryAddress, 103 | controllerAddress: controller, 104 | verbose: false, 105 | dryrun: true 106 | } 107 | updater = new Updater() 108 | await updater.setup(updaterOptions) 109 | let currentName = await updater.getReverseName(controller) 110 | let newName = 'yetanother.test' 111 | await updater.setReverseName(newName) 112 | let updatedName = await updater.getReverseName(controller) 113 | assert.strictEqual(updatedName, currentName) 114 | }) 115 | 116 | it ('should set reverse name', async function() { 117 | const updaterOptions = { 118 | web3: web3, 119 | registryAddress: registryAddress, 120 | controllerAddress: controller, 121 | verbose: false, 122 | } 123 | updater = new Updater() 124 | await updater.setup(updaterOptions) 125 | let newName = 'aaanditsgone.test' 126 | await updater.setReverseName(newName) 127 | let updatedName = await updater.getReverseName(controller) 128 | assert.strictEqual(updatedName, newName) 129 | }) 130 | 131 | it ('should clear reverse name for address', async function() { 132 | const updaterOptions = { 133 | web3: web3, 134 | registryAddress: registryAddress, 135 | controllerAddress: controller, 136 | verbose: false, 137 | } 138 | updater = new Updater() 139 | await updater.setup(updaterOptions) 140 | await updater.clearReverseName() 141 | let updatedName = await updater.getReverseName(controller) 142 | assert.strictEqual(updatedName, '') 143 | }) 144 | }) 145 | -------------------------------------------------------------------------------- /test/lib/lib.address.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const Updater = require('../../lib') 3 | const chai = require('chai') 4 | const chaiAsPromised = require('chai-as-promised') 5 | chai.use(chaiAsPromised) 6 | const assert = chai.assert 7 | 8 | /* global web3 */ 9 | 10 | const accountIndex = 1 11 | const tld = 'test' 12 | const label = 'wayne' 13 | const ensName = label+'.'+tld 14 | let updater 15 | let registryAddress 16 | const coinTypeETH = 60 17 | 18 | contract('lib - address functions dry-run', function(accounts) { 19 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 20 | 21 | before('Get registry address', async function() { 22 | const registry = await ENSRegistry.deployed() 23 | registryAddress = registry.address 24 | }) 25 | 26 | beforeEach('provide fresh updater instance', async function() { 27 | const updaterOptions = { 28 | web3: web3, 29 | ensName: ensName, 30 | registryAddress: registryAddress, 31 | controllerAddress: controller, 32 | verbose: false, 33 | dryrun: true, 34 | gasPrice: web3.utils.toBN('10000000000') 35 | } 36 | updater = new Updater() 37 | await updater.setup(updaterOptions) 38 | }) 39 | 40 | it ('should fail when setting invalid address record', async function() { 41 | let newaddress = '0xsomeThing' 42 | assert.isRejected(updater.setAddress({ 43 | address: newaddress, 44 | coinType: coinTypeETH 45 | })) 46 | }) 47 | 48 | it ('should not change ETH address when dry-run option is set', async function() { 49 | let currentaddress = await updater.getAddress(coinTypeETH) 50 | let newaddress = '0xF6b7788cD280cc1065a16777f7dBD2fE782Be8f9' 51 | await updater.setAddress({ 52 | address: newaddress, 53 | coinType: coinTypeETH 54 | }) 55 | let updatedAddress = await updater.getAddress(coinTypeETH) 56 | assert.strictEqual(updatedAddress, currentaddress) 57 | }) 58 | 59 | }) 60 | 61 | contract('lib - address functions estimategas', function(accounts) { 62 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 63 | 64 | let updaterOptions = { 65 | web3: web3, 66 | ensName: ensName, 67 | registryAddress: undefined, 68 | controllerAddress: controller, 69 | verbose: false, 70 | dryrun: false, 71 | estimateGas: false, 72 | gasPrice: web3.utils.toBN('10000000000') 73 | } 74 | 75 | let updaterOptions_estimate = { 76 | web3: web3, 77 | ensName: ensName, 78 | registryAddress: undefined, 79 | controllerAddress: controller, 80 | verbose: false, 81 | dryrun: false, 82 | estimateGas: true, 83 | gasPrice: web3.utils.toBN('10000000000') 84 | } 85 | 86 | before('Get registry address', async function() { 87 | const registry = await ENSRegistry.deployed() 88 | updaterOptions.registryAddress = registry.address 89 | updaterOptions_estimate.registryAddress = registry.address 90 | }) 91 | 92 | it ('should return gas estimate for read-only method', async function() { 93 | updater = new Updater() 94 | await updater.setup(updaterOptions_estimate) 95 | let gasEstimate = await updater.getAddress(coinTypeETH) 96 | assert.isNumber(gasEstimate) 97 | assert.isAbove(gasEstimate, 100) 98 | }) 99 | 100 | it ('should return gas estimate and not change anything', async function() { 101 | updater = new Updater() 102 | await updater.setup(updaterOptions) 103 | let currentaddress = await updater.getAddress(coinTypeETH) 104 | 105 | // update address with estimateGas option set 106 | await updater.setup(updaterOptions_estimate) 107 | let newaddress = '0xF6b7788cD280cc1065a16777f7dBD2fE782Be8f9' 108 | let gasEstimate = await updater.setAddress({ 109 | address: newaddress, 110 | coinType: coinTypeETH 111 | }) 112 | assert.isNumber(gasEstimate) 113 | assert.isAbove(gasEstimate, 100) 114 | 115 | // double check nothing was changed 116 | await updater.setup(updaterOptions) 117 | let updatedAddress = await updater.getAddress(coinTypeETH) 118 | assert.strictEqual(updatedAddress, currentaddress) 119 | }) 120 | 121 | }) 122 | 123 | contract('lib - address functions', function(accounts) { 124 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 125 | 126 | before('Get registry address', async function() { 127 | const registry = await ENSRegistry.deployed() 128 | registryAddress = registry.address 129 | }) 130 | 131 | beforeEach('provide fresh updater instance', async function() { 132 | const updaterOptions = { 133 | web3: web3, 134 | ensName: ensName, 135 | registryAddress: registryAddress, 136 | controllerAddress: controller, 137 | verbose: false, 138 | dryrun: false, 139 | gasPrice: web3.utils.toBN('10000000000') 140 | } 141 | updater = new Updater() 142 | await updater.setup(updaterOptions) 143 | }) 144 | 145 | it ('should return zero address when no address is set', async function() { 146 | const zeroAddress = '0x0000000000000000000000000000000000000000' 147 | let address = await updater.getAddress(coinTypeETH) 148 | assert.strictEqual(address, zeroAddress) 149 | }) 150 | 151 | it ('should fail when setting invalid address record', async function() { 152 | let newaddress = '0xsomeThing' 153 | assert.isRejected(updater.setAddress({ 154 | address: newaddress, 155 | coinType: coinTypeETH 156 | })) 157 | }) 158 | 159 | it ('should set ETH address record with valid address', async function() { 160 | let newaddress = '0xF6b7788cD280cc1065a16777f7dBD2fE782Be8f9' 161 | await updater.setAddress({ 162 | address: newaddress, 163 | coinType: coinTypeETH 164 | }) 165 | let updatedAddress = await updater.getAddress(coinTypeETH) 166 | assert.strictEqual(updatedAddress, newaddress) 167 | }) 168 | 169 | it ('should fail when resolver is required but not set', async function() { 170 | const updaterOptions = { 171 | web3: web3, 172 | ensName: 'noresolver.test', 173 | registryAddress: registryAddress, 174 | controllerAddress: controller, 175 | verbose: false, 176 | dryrun: false, 177 | gasPrice: web3.utils.toBN('10000000000') 178 | } 179 | updater = new Updater() 180 | await updater.setup(updaterOptions) 181 | 182 | let newaddress = '0xF6b7788cD280cc1065a16777f7dBD2fE782Be8f9' 183 | assert.isRejected(updater.setAddress( 184 | { 185 | address: newaddress, 186 | coinType: coinTypeETH 187 | }), 188 | /No resolver set/, 189 | 'Should fail with No Resolver set error') 190 | }) 191 | }) 192 | -------------------------------------------------------------------------------- /test/end2end/address.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const chai = require('chai') 3 | const chaiAsPromised = require('chai-as-promised') 4 | chai.use(chaiAsPromised) 5 | const assert = chai.assert 6 | const {runCommand} = require('./runCommand') 7 | const {wallet} = require('./testdata') 8 | 9 | 10 | contract('get/set address', function(accounts) { 11 | 12 | const controllerAccountIndex = 1 13 | const private_key = wallet.private_keys[controllerAccountIndex] 14 | const scriptpath = 'bin/ens-updater.js' 15 | const providerstring = 'http://localhost:8545' 16 | const ensName = 'wayne.test' 17 | const zeroAddress = '0x0000000000000000000000000000000000000000' 18 | let registryAddress 19 | 20 | before('Get registry address', async function() { 21 | const registry = await ENSRegistry.deployed() 22 | registryAddress = registry.address 23 | }) 24 | 25 | beforeEach('Clear address', async function() { 26 | const clearAddressCmd = `${scriptpath} clearAddress ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 27 | const options = {env: { PRIVATE_KEY: private_key}} 28 | const childResult = await runCommand(clearAddressCmd, options) 29 | assert.isFalse(childResult.failed) 30 | assert.match(childResult.stdout, /^0x/) // Expected output is a transaction hash so just check for anything starting with '0x' 31 | }) 32 | 33 | after('Clear address', async function() { 34 | const command = `${scriptpath} clearAddress ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 35 | const options = {env: { PRIVATE_KEY: private_key}} 36 | const childResult = await runCommand(command, options) 37 | assert.isFalse(childResult.failed) 38 | assert.match(childResult.stdout, /^0x/) // Expected output is a transaction hash so just check for anything starting with '0x' 39 | }) 40 | 41 | it('Should fail when no resolver is set', async function() { 42 | const command = `${scriptpath} getAddress noresolver.test --web3 ${providerstring} --registryAddress ${registryAddress}` 43 | const childResult = await runCommand(command) 44 | assert.isTrue(childResult.failed) 45 | assert.match(childResult.stderr, /No resolver set/) 46 | }) 47 | 48 | it('Should not fail when no address is set', async function() { 49 | const command = `${scriptpath} getAddress ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 50 | const childResult = await runCommand(command) 51 | assert.isFalse(childResult.failed) 52 | assert.equal(childResult.stdout, zeroAddress) 53 | }) 54 | 55 | it('Should estimate gas for setting address record', async function() { 56 | const targetAddress = accounts[3] 57 | // set new address with estimategas option 58 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} --web3 ${providerstring} --registryAddress ${registryAddress} --estimateGas` 59 | const options = {env: { PRIVATE_KEY: private_key}} 60 | let childResult = await runCommand(setAddressCmd, options) 61 | assert.isFalse(childResult.failed) 62 | assert.closeTo(parseInt(childResult.stdout), 45000, 5000) 63 | 64 | // Verify still zero-address is set 65 | const getAddressCmd = `${scriptpath} getAddress ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 66 | childResult = await runCommand(getAddressCmd) 67 | assert.isFalse(childResult.failed) 68 | assert.equal(childResult.stdout, zeroAddress) 69 | }) 70 | 71 | it('Should set address record', async function() { 72 | const targetAddress = accounts[3] 73 | // set new address 74 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} --web3 ${providerstring} --registryAddress ${registryAddress}` 75 | const options = {env: { PRIVATE_KEY: private_key}} 76 | let childResult = await runCommand(setAddressCmd, options) 77 | assert.isFalse(childResult.failed) 78 | assert.match(childResult.stdout, /^0x/) 79 | 80 | // Verify new address is set 81 | const getAddressCmd = `${scriptpath} getAddress ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 82 | childResult = await runCommand(getAddressCmd) 83 | assert.isFalse(childResult.failed) 84 | assert.equal(childResult.stdout, targetAddress) 85 | }) 86 | 87 | it('Should not set address record when dry-run option is set', async function() { 88 | const targetAddress = accounts[3] 89 | // set new address with dry-run option 90 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} --web3 ${providerstring} --registryAddress ${registryAddress} --dry-run` 91 | const options = {env: { PRIVATE_KEY: private_key}} 92 | let childResult = await runCommand(setAddressCmd, options) 93 | assert.isFalse(childResult.failed) 94 | 95 | // Verify still zero-address is set 96 | const getAddressCmd = `${scriptpath} getAddress ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 97 | childResult = await runCommand(getAddressCmd) 98 | assert.isFalse(childResult.failed) 99 | assert.equal(childResult.stdout, zeroAddress) 100 | }) 101 | 102 | it('Should not clear address record when dry-run option is set', async function() { 103 | const targetAddress = accounts[4] 104 | // First set address 105 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} --web3 ${providerstring} --registryAddress ${registryAddress}` 106 | const options = {env: { PRIVATE_KEY: private_key}} 107 | let childResult = await runCommand(setAddressCmd, options) 108 | assert.isFalse(childResult.failed) 109 | assert.match(childResult.stdout, /^0x/) 110 | 111 | // clear address with dry-run option 112 | const clearAddressCmd = `${scriptpath} clearAddress ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress} --dry-run` 113 | childResult = await runCommand(clearAddressCmd, options) 114 | assert.isFalse(childResult.failed) 115 | 116 | // Now verify that still previous address is returned 117 | const getAddressCmd = `${scriptpath} getAddress ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 118 | const clearedResult = await runCommand(getAddressCmd) 119 | assert.isFalse(childResult.failed) 120 | assert.equal(clearedResult.stdout, targetAddress) 121 | }) 122 | 123 | it('Should clear address record', async function() { 124 | const targetAddress = accounts[4] 125 | // First set address 126 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} --web3 ${providerstring} --registryAddress ${registryAddress}` 127 | const options = {env: { PRIVATE_KEY: private_key}} 128 | let childResult = await runCommand(setAddressCmd, options) 129 | assert.isFalse(childResult.failed) 130 | assert.match(childResult.stdout, /^0x/) 131 | 132 | // now clear address 133 | const clearAddressCmd = `${scriptpath} clearAddress ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 134 | childResult = await runCommand(clearAddressCmd, options) 135 | assert.isFalse(childResult.failed) 136 | assert.match(childResult.stdout, /^0x/) 137 | 138 | // Now verify that zero address is returned 139 | const getAddressCmd = `${scriptpath} getAddress ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 140 | const clearedResult = await runCommand(getAddressCmd) 141 | assert.isFalse(childResult.failed) 142 | assert.equal(clearedResult.stdout, zeroAddress) 143 | }) 144 | }) -------------------------------------------------------------------------------- /test/end2end/blockchainAddress.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const chai = require('chai') 3 | const chaiAsPromised = require('chai-as-promised') 4 | chai.use(chaiAsPromised) 5 | const assert = chai.assert 6 | const {runCommand} = require('./runCommand') 7 | const {wallet, blockchainAddressTestcases} = require('./testdata') 8 | 9 | /* global web3 */ 10 | 11 | contract('get/set other blockchain address', function() { 12 | 13 | const controllerAccountIndex = 1 14 | const private_key = wallet.private_keys[controllerAccountIndex] 15 | const scriptpath = 'bin/ens-updater.js' 16 | const providerstring = 'http://localhost:8545' 17 | const ensName = 'wayne.test' 18 | const zeroAddress = '0x0000000000000000000000000000000000000000' 19 | let registryAddress 20 | 21 | before('Get registry address', async function() { 22 | const registry = await ENSRegistry.deployed() 23 | registryAddress = registry.address 24 | }) 25 | 26 | /* Tests that do not change state */ 27 | it('Should fail with unknown coin symbol', async function() { 28 | const wrongSymbol = 'NOCOIN' 29 | const command = `${scriptpath} getAddress ${ensName} ${wrongSymbol} --web3 ${providerstring} --registryAddress ${registryAddress}` 30 | // Force language en_US to get error message in English 31 | const options = {env: {LANG: 'en_US.UTF-8'}} 32 | const childResult = await runCommand(command, options) 33 | assert.isTrue(childResult.failed) 34 | assert.match(childResult.stderr, /Invalid values/) 35 | }) 36 | 37 | for (let coin of blockchainAddressTestcases.slice(3,5)) { 38 | it(`Should fail trying to get ${coin.name} address when no resolver is set`, async function() { 39 | const command = `${scriptpath} getAddress noresolver.test ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress}` 40 | const childResult = await runCommand(command) 41 | assert.isTrue(childResult.failed) 42 | assert.match(childResult.stderr, /No resolver set/) 43 | }) 44 | 45 | it(`Should not fail trying to get unset ${coin.name} address`, async function() { 46 | const command = `${scriptpath} getAddress ${ensName} ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress}` 47 | const childResult = await runCommand(command) 48 | assert.isFalse(childResult.failed) 49 | assert.strictEqual(childResult.stdout, zeroAddress) 50 | }) 51 | 52 | it(`Should estimate gas for setting ${coin.name} address record`, async function() { 53 | const targetAddress = coin.addresses[0] 54 | // set new address with estimategas option 55 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${targetAddress} ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress} --estimateGas` 56 | const options = {env: { PRIVATE_KEY: private_key}} 57 | let childResult = await runCommand(setAddressCmd, options) 58 | assert.isFalse(childResult.failed) 59 | const actualGas = web3.utils.toBN(childResult.stdout) 60 | const expectedGas = web3.utils.toBN('55000') 61 | const threshold = web3.utils.toBN('5000') 62 | assert.isOk( 63 | (actualGas.gte(expectedGas.sub(threshold)) && actualGas.lte(expectedGas.add(threshold))), 64 | `Actual ${actualGas.toString()} - expected ${expectedGas.toString()}` 65 | ) 66 | 67 | // Verify still zero-address is set 68 | const getAddressCmd = `${scriptpath} getAddress ${ensName} ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress}` 69 | childResult = await runCommand(getAddressCmd) 70 | assert.isFalse(childResult.failed) 71 | assert.strictEqual(childResult.stdout, zeroAddress) 72 | }) 73 | 74 | it(`Should not set ${coin.name} address record ${coin.addresses[0]} when dry-run option is set`, async function () { 75 | // set new address with dry-run option 76 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${coin.addresses[0]} ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress} --dry-run` 77 | const options = {env: {PRIVATE_KEY: private_key}} 78 | let childResult = await runCommand(setAddressCmd, options) 79 | assert.isFalse(childResult.failed) 80 | 81 | // Verify still zero-address is set 82 | const getAddressCmd = `${scriptpath} getAddress ${ensName} ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress}` 83 | childResult = await runCommand(getAddressCmd) 84 | assert.isFalse(childResult.failed) 85 | assert.strictEqual(childResult.stdout, zeroAddress) 86 | }) 87 | } 88 | 89 | /* Setting address records */ 90 | for (let coin of blockchainAddressTestcases) { 91 | for (let address of coin.addresses) { 92 | it(`Should set ${coin.name} address record ${address}`, async function () { 93 | // set new address 94 | const setAddressCmd = `${scriptpath} setAddress ${ensName} ${address} ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress}` 95 | const options = {env: {PRIVATE_KEY: private_key}} 96 | let childResult = await runCommand(setAddressCmd, options) 97 | assert.isFalse(childResult.failed) 98 | assert.match(childResult.stdout, /^0x/) 99 | 100 | // Verify new address is set 101 | const getAddressCmd = `${scriptpath} getAddress ${ensName} ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress}` 102 | childResult = await runCommand(getAddressCmd) 103 | assert.isFalse(childResult.failed) 104 | assert.strictEqual(childResult.stdout, address) 105 | }) 106 | } 107 | } 108 | 109 | /* Clearing address records */ 110 | for (let coin of blockchainAddressTestcases.slice(3,5)) { 111 | it(`Should not clear ${coin.name} address record when dry-run option is set`, async function () { 112 | // get current address 113 | const getAddressCmd = `${scriptpath} getAddress ${ensName} ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress}` 114 | let childResult = await runCommand(getAddressCmd) 115 | // console.log(childResult) 116 | assert.isFalse(childResult.failed) 117 | // test only makes sense if some address has been set before! 118 | assert.notStrictEqual(childResult.stdout, zeroAddress) 119 | const prevAddress = childResult.stdout 120 | 121 | // clear address with dry-run option 122 | const clearAddressCmd = `${scriptpath} clearAddress ${ensName} ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress} --dry-run` 123 | const options = {env: {PRIVATE_KEY: private_key}} 124 | childResult = await runCommand(clearAddressCmd, options) 125 | assert.isFalse(childResult.failed) 126 | 127 | // verify that still previous address is returned 128 | const clearedResult = await runCommand(getAddressCmd) 129 | assert.isFalse(childResult.failed) 130 | assert.strictEqual(clearedResult.stdout, prevAddress) 131 | }) 132 | } 133 | 134 | for (let coin of blockchainAddressTestcases) { 135 | it(`Should clear ${coin.name} address record`, async function() { 136 | const clearAddressCmd = `${scriptpath} clearAddress ${ensName} ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress}` 137 | const options = {env: {PRIVATE_KEY: private_key}} 138 | let childResult = await runCommand(clearAddressCmd, options) 139 | assert.isFalse(childResult.failed) 140 | assert.match(childResult.stdout, /^0x/) 141 | 142 | // Now verify that zero address is returned 143 | const getAddressCmd = `${scriptpath} getAddress ${ensName} ${coin.symbol} --web3 ${providerstring} --registryAddress ${registryAddress}` 144 | const clearedResult = await runCommand(getAddressCmd) 145 | assert.isFalse(childResult.failed) 146 | assert.strictEqual(clearedResult.stdout, zeroAddress) 147 | }) 148 | } 149 | }) -------------------------------------------------------------------------------- /test/end2end/contenthash.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const chai = require('chai') 3 | const chaiAsPromised = require('chai-as-promised') 4 | chai.use(chaiAsPromised) 5 | const assert = chai.assert 6 | const {runCommand} = require('./runCommand') 7 | const {wallet} = require('./testdata') 8 | 9 | 10 | contract('get/set contenthash', function() { 11 | 12 | const controllerAccountIndex = 1 13 | const private_key = wallet.private_keys[controllerAccountIndex] 14 | const scriptpath = 'bin/ens-updater.js' 15 | const providerstring = 'http://localhost:8545' 16 | const ensName = 'wayne.test' 17 | const firstCID = 'QmY7Yh4UquoXHLPFo2XbhXkhBvFoPwmQUSa92pxnxjQuPU' 18 | const noRecordResult = 'No contenthash record set' 19 | let registryAddress 20 | 21 | before('Get registry address', async function() { 22 | const registry = await ENSRegistry.deployed() 23 | registryAddress = registry.address 24 | }) 25 | 26 | beforeEach('Clear contenthash', async function() { 27 | const clearContenthashCmd = `${scriptpath} clearContenthash ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 28 | const options = {env: { PRIVATE_KEY: private_key}} 29 | const childResult = await runCommand(clearContenthashCmd, options) 30 | assert.isFalse(childResult.failed) 31 | assert.match(childResult.stdout, /^0x/) // Expected output is a transaction hash so just check for anything starting with '0x' 32 | }) 33 | 34 | after('Clear contenthash', async function() { 35 | const clearContenthashCmd = `${scriptpath} clearContenthash ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 36 | const options = {env: { PRIVATE_KEY: private_key}} 37 | const childResult = await runCommand(clearContenthashCmd, options) 38 | assert.isFalse(childResult.failed) 39 | assert.match(childResult.stdout, /^0x/) // Expected output is a transaction hash so just check for anything starting with '0x' 40 | }) 41 | 42 | it('Should fail when no resolver is set', async function() { 43 | const command = `${scriptpath} setContenthash noresolver.test ipfs-ns ${firstCID} --web3 ${providerstring} --registryAddress ${registryAddress}` 44 | const options = {env: { PRIVATE_KEY: private_key}} 45 | const childResult = await runCommand(command, options) 46 | assert.isTrue(childResult.failed) 47 | assert.match(childResult.stderr, /No resolver set/) 48 | }) 49 | 50 | it('Should not fail when no contenthash record is set', async function() { 51 | const command = `${scriptpath} getContenthash ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 52 | const childResult = await runCommand(command) 53 | assert.isFalse(childResult.failed) 54 | assert.equal(childResult.stdout, noRecordResult) 55 | }) 56 | 57 | it('Should estimate gas for setting contenthash record', async function() { 58 | const command = `${scriptpath} setContenthash ${ensName} ipfs-ns ${firstCID} --web3 ${providerstring} --registryAddress ${registryAddress} --estimateGas` 59 | const options = {env: { PRIVATE_KEY: private_key}} 60 | let childResult = await runCommand(command, options) 61 | assert.isFalse(childResult.failed) 62 | assert.closeTo(parseInt(childResult.stdout), 92000, 5000) 63 | 64 | // double-check nothing was changed during estimateGas 65 | const verifyCommand = `${scriptpath} getContenthash ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 66 | childResult = await runCommand(verifyCommand) 67 | assert.isFalse(childResult.failed) 68 | assert.equal(childResult.stdout, noRecordResult) 69 | }) 70 | 71 | it('Should estimate gas for clearing contenthash record', async function() { 72 | const command = `${scriptpath} clearContenthash ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress} --estimateGas` 73 | const options = {env: { PRIVATE_KEY: private_key}} 74 | let childResult = await runCommand(command, options) 75 | assert.isFalse(childResult.failed) 76 | assert.closeTo(parseInt(childResult.stdout), 34000, 5000) 77 | }) 78 | 79 | it('Should set contenthahs record of type ipfs-ns', async function() { 80 | // set new address 81 | const contentType = 'ipfs-ns' 82 | const command = `${scriptpath} setContenthash ${ensName} ${contentType} ${firstCID} --web3 ${providerstring} --registryAddress ${registryAddress}` 83 | const options = {env: { PRIVATE_KEY: private_key}} 84 | let childResult = await runCommand(command, options) 85 | assert.isFalse(childResult.failed) 86 | assert.match(childResult.stdout, /^0x/) 87 | 88 | // Verify new record is set 89 | const verifyCommand = `${scriptpath} getContenthash ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 90 | childResult = await runCommand(verifyCommand) 91 | assert.isFalse(childResult.failed) 92 | assert.equal(childResult.stdout, `${contentType}: ${firstCID}`) 93 | }) 94 | 95 | it('Should not set contenthash record when dry-run option is set', async function() { 96 | const contentType = 'ipfs-ns' 97 | const command = `${scriptpath} setContenthash ${ensName} ${contentType} ${firstCID} --web3 ${providerstring} --registryAddress ${registryAddress} --dry-run` 98 | const options = {env: { PRIVATE_KEY: private_key}} 99 | let childResult = await runCommand(command, options) 100 | assert.isFalse(childResult.failed) 101 | 102 | // double-check nothing was changed during dry-run 103 | const verifyCommand = `${scriptpath} getContenthash ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 104 | childResult = await runCommand(verifyCommand) 105 | assert.isFalse(childResult.failed) 106 | assert.equal(childResult.stdout, noRecordResult) 107 | }) 108 | 109 | it('Should not clear contenthash record when dry-run option is set', async function() { 110 | // First set a record 111 | const contentType = 'ipfs-ns' 112 | const command = `${scriptpath} setContenthash ${ensName} ${contentType} ${firstCID} --web3 ${providerstring} --registryAddress ${registryAddress}` 113 | const options = {env: { PRIVATE_KEY: private_key}} 114 | let childResult = await runCommand(command, options) 115 | assert.isFalse(childResult.failed) 116 | 117 | // clear address with dry-run option 118 | const clearContenthashCmd = `${scriptpath} clearContenthash ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress} --dry-run` 119 | const clearOptions = {env: { PRIVATE_KEY: private_key}} 120 | childResult = await runCommand(clearContenthashCmd, clearOptions) 121 | assert.isFalse(childResult.failed) 122 | 123 | // Now verify that still previous record is returned 124 | const verifyCommand = `${scriptpath} getContenthash ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 125 | childResult = await runCommand(verifyCommand) 126 | assert.isFalse(childResult.failed) 127 | assert.equal(childResult.stdout, `${contentType}: ${firstCID}`) 128 | }) 129 | 130 | it('Should clear contenthash record', async function() { 131 | // First set record 132 | const contentType = 'ipfs-ns' 133 | const command = `${scriptpath} setContenthash ${ensName} ${contentType} ${firstCID} --web3 ${providerstring} --registryAddress ${registryAddress}` 134 | const options = {env: { PRIVATE_KEY: private_key}} 135 | let childResult = await runCommand(command, options) 136 | assert.isFalse(childResult.failed) 137 | 138 | // now clear record 139 | const clearContenthashCmd = `${scriptpath} clearContenthash ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 140 | const clearOptions = {env: { PRIVATE_KEY: private_key}} 141 | childResult = await runCommand(clearContenthashCmd, clearOptions) 142 | assert.isFalse(childResult.failed) 143 | assert.match(childResult.stdout, /^0x/) // Expected output is a transaction hash so just check for anything starting with '0x' 144 | 145 | // Now verify record is cleared 146 | const verifyCommand = `${scriptpath} getContenthash ${ensName} --web3 ${providerstring} --registryAddress ${registryAddress}` 147 | childResult = await runCommand(verifyCommand) 148 | assert.isFalse(childResult.failed) 149 | assert.equal(childResult.stdout, noRecordResult) 150 | }) 151 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.com/TripleSpeeder/ens-updater.svg?branch=develop)](https://travis-ci.com/TripleSpeeder/ens-updater) 2 | [![npm](https://img.shields.io/npm/v/@triplespeeder/ens-updater)](https://www.npmjs.com/package/@triplespeeder/ens-updater) 3 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 4 | 5 | # ens-updater 6 | 7 | > Manage ENS names from the commandline 8 | 9 | ens-updater enables automated update of e.g. contentHash records in the Ethereum Name System. 10 | 11 | ## Table of Contents 12 | 13 | - [Overview](#overview) 14 | - [Security](#security) 15 | - [Background](#background) 16 | - [Install](#install) 17 | - [Usage](#usage) 18 | - [Testing](#testing) 19 | - [Contributing](#contributing) 20 | - [Maintainers](#maintainers) 21 | - [License](#license) 22 | - [Development support](#development-support) 23 | 24 | ## Overview 25 | ### Design goals: 26 | - provide an all-purpose cli for managing ENS names 27 | - integrate well in deployment scripts and CI environments 28 | 29 | ### Notable features: 30 | - Get/set ENS name records 31 | - For each operation verifies that the resolver contract of an ENS-name implements the required interface 32 | via EIP165 "supportsInterface" 33 | - Show interfaces a resolver implements (command "listInterfaces") 34 | - Includes "--estimateGas" option to check the required gas for a command 35 | - Bash completion support (try command "completion" to set it up) 36 | - Can read input from stdin to support piping with other tools 37 | - Options can be set via json configfile (see [config file support](#config-file-support)) 38 | 39 | ## Security 40 | In order to perform an update of an ENS record, `ens-update` needs the private key of the 41 | Ethereum account controlling the ENS name. The private key needs to be provided via environment variable or 42 | through the file `.env` in the working directory. 43 | 44 | - **NEVER share .env file with anybody** 45 | - **NEVER check .env into version control** 46 | - **NEVER publish .env in any other way** 47 | 48 | 49 | The private key can be provided either directly or through a mnemonic 50 | #### Provide the private key 51 | Example contents of `.env`: 52 | ```bash 53 | PRIVATE_KEY= 54 | ``` 55 | #### Provide the mnemonic 56 | Example contents of `.env`: 57 | ```bash 58 | MNEMONIC= 59 | ``` 60 | By default the first account will be used. If you need to use another account provide the option --accountindex. 61 | 62 | Remember - The mnemonic gives full control to **all** accounts of the according wallet! 63 | 64 | ## Background 65 | For information on the Ethereum Name System see the [ENS documentation](https://docs.ens.domains/). 66 | 67 | ## Install 68 | ``` 69 | npm install -g @triplespeeder/ens-updater 70 | ``` 71 | 72 | ## Usage 73 | The following commands are implemented: 74 | - getInfo for a quick summary 75 | - get/set contenthash 76 | - get/set address record (including eip-2304 Multichain support) 77 | - get/set reverse name records 78 | - get list of interfaces resolver supports 79 | - setup bash completion 80 | 81 | PRs to extend functionality are welcome :) 82 | 83 | ``` 84 | > ens-updater --help 85 | Usage: ens-updater [options] 86 | 87 | Commands: 88 | ens-updater.js getInfo Get various info about ENS name 89 | ens-updater.js setContenthash Set contenthash record 90 | ens-updater.js getContenthash Get contenthash record 91 | ens-updater.js clearContenthash Clear contenthash record 92 | ens-updater.js setAddress
[coinname] Set address record 93 | ens-updater.js getAddress [coinname] Get address record 94 | ens-updater.js clearAddress [coinname] Clear address record 95 | ens-updater.js setReverseName Set reverse name record of calling address 96 | ens-updater.js getReverseName
Get reverse name record of address 97 | ens-updater.js clearReverseName Clear reverse name record of calling address 98 | ens-updater.js listInterfaces List interfaces resolver supports 99 | ens-updater.js completion generate completion script 100 | 101 | Options: 102 | --version Show version number [boolean] 103 | --verbose, -v Verbose output [boolean] [default: false] 104 | --web3 Web3 connection string [string] [required] 105 | --estimateGas Estimate required gas for transactions [boolean] [default: false] 106 | --gasPrice Gas price to set for transaction (unit 'gwei'). Defaults to 10. [number] [default: 10] 107 | --gas Gas to provide for transaction (omit to use automatic calculation) [number] 108 | --dry-run Do not perform any real transactions [boolean] [default: false] 109 | --accountindex, -i Account index. Defaults to 0 [number] [default: 0] 110 | --registryAddress Optional contract address of the ENS Registry. [string] 111 | --help, -h Show help [boolean] 112 | 113 | contact: michael@m-bauer.org 114 | github: https://github.com/TripleSpeeder/ens-updater 115 | ``` 116 | 117 | #### Example 118 | On Ropsten network, set the contentHash of the name `ens-updater.eth` to the IPFS CID `QmY7Yh4UquoXHLPFo2XbhXkhBvFoPwmQUSa92pxnxjQuPU`: 119 | ``` 120 | > ens-updater setContenthash ens-updater.eth ipfs-ns QmY7Yh4UquoXHLPFo2XbhXkhBvFoPwmQUSa92pxnxjQuPU --web3 http://ropsten.dappnode:8545 --verbose 121 | Setting up web3 & HDWallet provider... 122 | Running on chain ID 3 123 | Verifying ensName owner 124 | Verifying content hash... 125 | Updating contenthash... 126 | Successfully stored new contentHash. Transaction hash: 0x0b8cdb75ff3b514c974ccd0bdef7cc3557bfab934b39caba30c38b88d375d705. 127 | Exiting... 128 | > 129 | ``` 130 | 131 | #### Reading values from stdin 132 | Setting the value "stdin" for option `contenthash` or `address` reads the contenthash/address to set from stdin. This is useful 133 | to build a chain of commands in a deploy script. 134 | 135 | For example you can use [ipfs-deploy](https://www.npmjs.com/package/ipfs-deploy) to publish a website to IPFS 136 | and directly pipe the CID returned by ipfs-deploy into ens-updater: 137 | 138 | ``` 139 | > ipfs-deploy -d dappnode | ens-updater setContenthash ens-updater.eth ipfs-ns stdin --web3 http://ropsten.dappnode:8545 --verbose 140 | Getting contenthash from stdin... 141 | Got contenthash: QmY7Yh4UquoXHLPFo2XbhXkhBvFoPwmQUSa92pxnxjQuPU. 142 | Setting up web3 & HDWallet provider... 143 | ... 144 | ``` 145 | 146 | #### Config file support 147 | All options can be provided through a json config file. The config file can be set with 148 | `--config ` option. 149 | 150 | Example config file that sets web3 connection string and custom registry address: 151 | ```json 152 | { 153 | "web3": "http://127.0.0.1:9545", 154 | "registryAddress": "0x112234455c3a32fd11230c42e7bccd4a84e02010" 155 | } 156 | ``` 157 | Usage example: 158 | ``` 159 | > ens-updater listInterfaces example.domain.eth --config myconfig.json 160 | ``` 161 | 162 | ## Testing 163 | 164 | ### Unittests 165 | Unittests are plain mocha tests located in folder "unitTests". They do not require ganache or other 166 | node to run. 167 | 168 | Execute tests with `npm run test:unit` 169 | 170 | ### Integration and end-to-end tests 171 | These tests are implemented with truffle and require a local ganache instance to run. Tests are organized in folders: 172 | - `test/lib/`: Tests of the core functionality from the `src/lib` folder 173 | - `test/middleware/`: Tests of yargs middleware that needs to interact with a live node 174 | - `test/end2end/`: Tests of the actual binary. Each test executes `ens-updater` as a childprocess and verifies the output 175 | 176 | To execute the tests: 177 | 1. Start ganache-cli in a dedicated terminal in deterministic mode: 178 | `ganache-cli -d` 179 | 2. Run truffle tests in another terminal: 180 | `npm run test:truffle` 181 | 182 | 183 | ## Contributing 184 | 185 | PRs are welcome! Have a look at the [open issues](https://github.com/TripleSpeeder/ens-updater/issues) or create a new 186 | issue if you are missing functionality. 187 | 188 | Pull requests should be against the "development" branch. 189 | 190 | ### Commits 191 | Commit messages should follow [Conventional Commits](https://www.conventionalcommits.org/) guidelines. This is 192 | also checked via git hooks and husky. 193 | 194 | Structure: 195 | ``` 196 | 197 | 198 | [optional body] 199 | 200 | [optional footer(s)] 201 | ``` 202 | 203 | Supported types: 204 | - **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) 205 | - **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) 206 | - **docs**: Documentation only changes 207 | - **feat**: A new feature 208 | - **fix**: A bug fix 209 | - **perf**: A code change that improves performance 210 | - **refactor**: A code change that neither fixes a bug nor adds a feature 211 | - **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 212 | - **test**: Adding missing tests or correcting existing tests 213 | 214 | ## Maintainers 215 | [@TripleSpeeder](https://github.com/TripleSpeeder) 216 | 217 | ## Development support 218 | If you like this project consider contributing to the gitcoin grant: **https://gitcoin.co/grants/218/ens-updater**. 219 | 220 | If you prefer direct donations please use: **ens-updater.eth** (0x8651Cf790fc894512a726A402C9CAAA3687628f0) 221 | 222 | ## License 223 | MIT 224 | 225 | © 2019 - 2020 Michael Bauer 226 | -------------------------------------------------------------------------------- /test/lib/lib.contenthash.test.js: -------------------------------------------------------------------------------- 1 | const ENSRegistry = artifacts.require('@ensdomains/ens/ENSRegistryWithFallback') 2 | const PublicResolver = artifacts.require('@ensdomains/resolver/PublicResolver') 3 | const namehash = require('eth-ens-namehash') 4 | const Updater = require('../../lib') 5 | const {decode, getCodec} = require('content-hash') 6 | const chai = require('chai') 7 | const chaiAsPromised = require('chai-as-promised') 8 | chai.use(chaiAsPromised) 9 | const assert = chai.assert 10 | 11 | /* global web3 */ 12 | 13 | const accountIndex = 1 14 | const tld = 'test' 15 | const label = 'wayne' 16 | const ensName = label+'.'+tld 17 | const firstCID = 'QmY7Yh4UquoXHLPFo2XbhXkhBvFoPwmQUSa92pxnxjQuPU' 18 | const otherCID = 'QmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D' 19 | const codec = 'ipfs-ns' 20 | const node = namehash.hash(ensName) // for querying 21 | let updater 22 | let registryAddress 23 | 24 | contract('lib - contenthash functions', function(accounts) { 25 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 26 | 27 | before('Get registry address', async function() { 28 | const registry = await ENSRegistry.deployed() 29 | registryAddress = registry.address 30 | }) 31 | 32 | beforeEach('provide fresh updater instance', async function() { 33 | const updaterOptions = { 34 | web3: web3, 35 | ensName: ensName, 36 | registryAddress: registryAddress, 37 | controllerAddress: controller, 38 | verbose: false, 39 | dryrun: false, 40 | gasPrice: web3.utils.toBN('10000000000') 41 | } 42 | updater = new Updater() 43 | await updater.setup(updaterOptions) 44 | }) 45 | 46 | it('should set IPFS hash', async function() { 47 | await updater.setContenthash({ 48 | contentType: codec, 49 | contentHash: firstCID, 50 | }) 51 | 52 | // verify that resolver returns correct hash 53 | const resolver = await PublicResolver.deployed() 54 | const currentContentHash = await resolver.contenthash(node) 55 | assert.strictEqual(getCodec(currentContentHash), codec) 56 | assert.strictEqual(decode(currentContentHash), firstCID) 57 | }) 58 | 59 | it('should get correct IPFS hash', async function() { 60 | // Get content hash directly from resolver 61 | const resolver = await PublicResolver.deployed() 62 | const currentContentHash = await resolver.contenthash(node) 63 | 64 | // Get decoded contenthash via updater 65 | const result = await updater.getContenthash(node) 66 | assert.strictEqual(result.codec, getCodec(currentContentHash)) 67 | assert.strictEqual(result.hash, decode(currentContentHash)) 68 | }) 69 | 70 | it('should replace existing IPFS hash', async function() { 71 | const resolver = await PublicResolver.deployed() 72 | 73 | // get old contentHash 74 | const oldContentHash = await resolver.contenthash(node) 75 | assert.strictEqual(getCodec(oldContentHash), codec) 76 | assert.strictEqual(decode(oldContentHash), firstCID) 77 | 78 | // update contentHash 79 | await updater.setContenthash({ 80 | contentType: codec, 81 | contentHash: otherCID, 82 | }) 83 | 84 | // verify that updater returns new hash 85 | const result = await updater.getContenthash() 86 | assert.strictEqual(result.codec, codec) 87 | assert.strictEqual(result.hash, otherCID) 88 | }) 89 | 90 | it('should clear IPFS hash', async function() { 91 | // set contentHash 92 | await updater.setContenthash({ 93 | contentType: codec, 94 | contentHash: firstCID, 95 | }) 96 | 97 | // verify that updater returns hash 98 | const result = await updater.getContenthash() 99 | assert.strictEqual(result.codec, codec) 100 | assert.strictEqual(result.hash, firstCID) 101 | 102 | // clear contentHash 103 | await updater.clearContenthash() 104 | 105 | // verify that contenthash is cleared 106 | const clearResult = await updater.getContenthash() 107 | assert.isUndefined(clearResult.codec) 108 | assert.isUndefined(clearResult.hash) 109 | }) 110 | 111 | it('should fail with unsupported content codec', async function() { 112 | assert.isRejected(updater.setContenthash({ 113 | contentType: 'YXZ', 114 | contentHash: 'someHashValue', 115 | })) 116 | }) 117 | 118 | it('should set contenthash format CIDv1 CIDs') 119 | 120 | }) 121 | 122 | contract('lib - contenthash functions dry-run', function(accounts) { 123 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 124 | 125 | before('Get registry address', async function() { 126 | const registry = await ENSRegistry.deployed() 127 | registryAddress = registry.address 128 | }) 129 | 130 | beforeEach('provide fresh updater instance', async function() { 131 | const updaterOptions = { 132 | web3: web3, 133 | ensName: ensName, 134 | registryAddress: registryAddress, 135 | controllerAddress: controller, 136 | verbose: false, 137 | dryrun: true, 138 | gasPrice: web3.utils.toBN('10000000000') 139 | } 140 | updater = new Updater() 141 | await updater.setup(updaterOptions) 142 | }) 143 | 144 | it('should not update anything when \'dryrun\' option is set', async function() { 145 | // get current contentHash 146 | const {codec: prevCodec, hash: prevHash} = await updater.getContenthash() 147 | 148 | // update contentHash with dry-run option set 149 | await updater.setContenthash({ 150 | contentType: codec, 151 | contentHash: otherCID, 152 | }) 153 | 154 | // verify that updater still returns old hash 155 | const result = await updater.getContenthash() 156 | assert.strictEqual(result.codec, prevCodec) 157 | assert.strictEqual(result.hash, prevHash) 158 | }) 159 | 160 | it('should not clear contenthash when \'dryrun\' option is set', async function() { 161 | // get current contentHash 162 | const {codec: prevCodec, hash: prevHash} = await updater.getContenthash() 163 | 164 | await updater.clearContenthash() 165 | 166 | // verify that updater still returns old hash 167 | const result = await updater.getContenthash() 168 | assert.strictEqual(result.codec, prevCodec) 169 | assert.strictEqual(result.hash, prevHash) 170 | }) 171 | 172 | it('should fail with unsupported content codec', async function() { 173 | assert.isRejected(updater.setContenthash({ 174 | contentType: 'YXZ', 175 | contentHash: 'someHashValue', 176 | })) 177 | }) 178 | 179 | }) 180 | 181 | contract('lib - contenthash functions estimateGas', function(accounts) { 182 | const controller = accounts[accountIndex].toLowerCase() // account that registers and owns ENSName 183 | 184 | let updaterOptions = { 185 | web3: web3, 186 | ensName: ensName, 187 | registryAddress: undefined, 188 | controllerAddress: controller, 189 | verbose: false, 190 | dryrun: false, 191 | estimateGas: false, 192 | gasPrice: web3.utils.toBN('10000000000') 193 | } 194 | 195 | before('Get registry address', async function() { 196 | const registry = await ENSRegistry.deployed() 197 | updaterOptions.registryAddress = registry.address 198 | }) 199 | 200 | it ('should return gas estimate for read-only method', async function() { 201 | updater = new Updater() 202 | updaterOptions.estimateGas = true 203 | await updater.setup(updaterOptions) 204 | let gasEstimate = await updater.getContenthash() 205 | assert.isNumber(gasEstimate) 206 | assert.isAbove(gasEstimate, 1000) 207 | }) 208 | 209 | it('should return gas estimate for setContenthash and not update anything', async function() { 210 | updater = new Updater() 211 | await updater.setup(updaterOptions) 212 | // get current contentHash 213 | const {codec: prevCodec, has: prevHash} = await updater.getContenthash() 214 | 215 | // update contentHash with estimateGas option set 216 | // eslint-disable-next-line require-atomic-updates 217 | updaterOptions.estimateGas = true 218 | await updater.setup(updaterOptions) 219 | const gasEstimate = await updater.setContenthash({ 220 | contentType: codec, 221 | contentHash: otherCID, 222 | }) 223 | assert.isNumber(gasEstimate) 224 | assert.isAbove(gasEstimate, 1000) 225 | 226 | // verify that updater still returns old hash 227 | // eslint-disable-next-line require-atomic-updates 228 | updaterOptions.estimateGas = false 229 | await updater.setup(updaterOptions) 230 | const result = await updater.getContenthash() 231 | assert.strictEqual(result.codec, prevCodec) 232 | assert.strictEqual(result.hash, prevHash) 233 | }) 234 | 235 | it('should return gas estimate for clearContenthash and not update anything', async function() { 236 | updater = new Updater() 237 | await updater.setup(updaterOptions) 238 | // get current contentHash 239 | const {codec: prevCodec, has: prevHash} = await updater.getContenthash() 240 | 241 | // clear contentHash with estimateGas option set 242 | // eslint-disable-next-line require-atomic-updates 243 | updaterOptions.estimateGas = true 244 | await updater.setup(updaterOptions) 245 | const gasEstimate = await updater.clearContenthash() 246 | assert.isNumber(gasEstimate) 247 | assert.isAbove(gasEstimate, 1000) 248 | 249 | // verify that updater still returns old hash 250 | // eslint-disable-next-line require-atomic-updates 251 | updaterOptions.estimateGas = false 252 | await updater.setup(updaterOptions) 253 | const result = await updater.getContenthash() 254 | assert.strictEqual(result.codec, prevCodec) 255 | assert.strictEqual(result.hash, prevHash) 256 | }) 257 | }) -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const {decode, encode, getCodec} = require('content-hash') 2 | const namehash = require('eth-ens-namehash') 3 | const contract = require('@truffle/contract') 4 | const ResolverABI = require('@ensdomains/resolver/build/contracts/Resolver.json') 5 | const RegistryABI = require('@ensdomains/ens/build/contracts/ENSRegistryWithFallback') 6 | const OldRegistryABI = require('@ensdomains/ens/build/contracts/ENSRegistry') 7 | const RegistrarABI = require('@ensdomains/ethregistrar/build/contracts/BaseRegistrarImplementation.json') 8 | const ReverseRegistrarABI = require('@ensdomains/ens/build/contracts/ReverseRegistrar') 9 | const {formatsByCoinType} = require('@ensdomains/address-encoder') 10 | const RegistryAddresses = require('./ENSAddresses') 11 | const ResolverInterfaces = require('./ResolverInterfaces') 12 | 13 | 14 | module.exports = function Updater() { 15 | 16 | // module variables 17 | let resolver, registry, registrar, reverseRegistrar, controllerAddress, verbose, node, web3, ensName, dryrun, estimateGas, gasPrice, gas, needsMigration 18 | const zeroAddress = '0x0000000000000000000000000000000000000000' 19 | const coinTypeETH = 60 20 | controllerAddress = zeroAddress 21 | 22 | // validator functions 23 | async function verifyResolver() { 24 | if (!resolver) { 25 | throw Error(`No resolver set for ${ensName}`) 26 | } 27 | } 28 | 29 | async function verifyController() { 30 | verbose && console.log('Verifying ensName controller') 31 | const controller = await getController(node) 32 | if (controller !== controllerAddress) { 33 | throw Error(`${controllerAddress} is not controller of ${ensName}. Current controller is ${controller}`) 34 | } 35 | } 36 | 37 | async function verifyInterface(interfaceName) { 38 | let EIPSupport = await resolver.supportsInterface(ResolverInterfaces['EIP165']) 39 | if (!EIPSupport) { 40 | throw Error('Resolver contract does not implement EIP165 standard') 41 | } 42 | let support = await resolver.supportsInterface(ResolverInterfaces[interfaceName]) 43 | if (!support) { 44 | throw Error(`Resolver contract does not implement required interface "${interfaceName}"`) 45 | } 46 | } 47 | 48 | async function verifyAddress(_address) { 49 | if (!web3.utils.isAddress(_address)) { 50 | throw Error(`${_address} is not a valid Ethereum address`) 51 | } 52 | } 53 | 54 | function verifyMigrationStatus() { 55 | if (needsMigration) { 56 | throw Error(`${ensName} can not be changed until it is migrated to the current registry`) 57 | } 58 | } 59 | 60 | // helper functions 61 | function getTLD(ensName) { 62 | return ensName.split('.').pop() 63 | } 64 | 65 | function getReverseLookupName(_address) { 66 | if (_address.startsWith('0x')) { 67 | _address = _address.slice(2) 68 | } 69 | return `${_address.toLowerCase()}.addr.reverse` 70 | } 71 | 72 | async function getController(_node) { 73 | try { 74 | let controller = await registry.owner(_node) 75 | return controller.toLowerCase() 76 | } catch(error) { 77 | throw Error(`Failed to get owner of node ${_node}. Error: ${error}`) 78 | } 79 | } 80 | 81 | async function getRegistrar(ensName) { 82 | verbose && console.log(`Getting Registrar...`) 83 | const registrarAddress = await registry.owner(namehash.hash(getTLD(ensName))) 84 | // TODO: I'm assuming that the registrar is the official permanent registrar. Should explicitly 85 | // check this, e.g. by comparing with well-known registrar addresses. 86 | const registrarContract = contract(RegistrarABI) 87 | registrarContract.setProvider(web3.currentProvider) 88 | return await registrarContract.at(registrarAddress) 89 | } 90 | 91 | async function getRegistrant() { 92 | // Only the first level domain is a token and supports ownerOf method. 93 | // Subdomains don't have this concept, so return "undefined" 94 | const labels = ensName.split('.') 95 | if (labels.length === 2) { 96 | const keccac256 = web3.utils.soliditySha3(labels[0]) 97 | try { 98 | return await registrar.ownerOf(keccac256) 99 | } catch (error) { 100 | verbose && console.log(`Failed to get registrant for ${ensName}. Error: ${error}`) 101 | return undefined 102 | } 103 | } else { 104 | return undefined 105 | } 106 | } 107 | 108 | async function getExpires() { 109 | const labels = ensName.split('.') 110 | if (labels.length === 2) { 111 | const keccac256 = web3.utils.soliditySha3(labels[0]) 112 | const expires = await registrar.nameExpires(keccac256) 113 | return expires 114 | } else { 115 | return 0 116 | } 117 | } 118 | 119 | async function getResolver(_registry, _node) { 120 | const resolverAddress = await _registry.resolver(_node) 121 | if (resolverAddress !== '0x0000000000000000000000000000000000000000') { 122 | const resolverContract = contract(ResolverABI) 123 | resolverContract.setProvider(web3.currentProvider) 124 | return await resolverContract.at(resolverAddress) 125 | } else { 126 | return undefined 127 | } 128 | } 129 | 130 | async function getReverseResolver(reverseNode) { 131 | const reverseResolverAddress = await registry.resolver(reverseNode) 132 | if (reverseResolverAddress === zeroAddress) { 133 | return undefined 134 | } 135 | verbose && console.log(`Getting reverse resolver...`) 136 | const reverseResolverContract = contract(ResolverABI) 137 | reverseResolverContract.setProvider(web3.currentProvider) 138 | return await reverseResolverContract.at(reverseResolverAddress) 139 | } 140 | 141 | async function getReverseNode(address) { 142 | await verifyAddress(address) 143 | const revLookupName = getReverseLookupName(address) 144 | return namehash.hash(revLookupName) 145 | } 146 | 147 | async function getReverseRegistrar(name) { 148 | const reverseNode = namehash.hash(name) 149 | const reverseRegistrarAddress = await getController(reverseNode) 150 | if (reverseRegistrarAddress === zeroAddress) { 151 | throw Error(`'${name}' has no owner -> No ReverseRegistrar is available`) 152 | } 153 | const reverseRegistrarContract = contract(ReverseRegistrarABI) 154 | reverseRegistrarContract.setProvider(web3.currentProvider) 155 | return await reverseRegistrarContract.at(reverseRegistrarAddress) 156 | } 157 | 158 | function isMethodConstant(contract, methodkey) { 159 | // check ABI if this is a constant function 160 | // Find the first method that matches the name. There might be overloaded variants, but they will 161 | // all have the same mutability, so just take first entry with [0] 162 | const method = methodkey.split('(', 1)[0] 163 | const abi = contract.abi.filter(entry => (entry.name === method))[0] 164 | return abi.constant 165 | } 166 | 167 | async function execute(contract, methodkey, ...args) { 168 | let resultType 169 | let doDryrun = dryrun 170 | 171 | if(isMethodConstant(contract, methodkey)) { 172 | // ignore dry-run option for constant methods 173 | doDryrun = false 174 | // constant methods directly return the desired result, not a transaction object 175 | resultType = 'direct' 176 | } else { 177 | // Add transaction options for non-constant methods 178 | const transactionOptions = { 179 | // Add additional options like gas parameters etc. here 180 | from: controllerAddress, 181 | gasPrice: gasPrice, 182 | } 183 | // Only set gas when explicitly provided. Truffle will do automatic calculation of required gas if 184 | // not specified. 185 | if (gas !== undefined) { 186 | transactionOptions.gas = gas 187 | } 188 | args.push(transactionOptions) 189 | resultType = 'transaction' // Desired result is a transaction hash. Will return result.tx 190 | } 191 | 192 | let func = contract.methods[methodkey] 193 | if (estimateGas) { 194 | func = func['estimateGas'] 195 | resultType = 'direct' 196 | } 197 | if (doDryrun) { 198 | func = func['call'] 199 | resultType = 'none' // dry-run will not return any meaningful result 200 | } 201 | 202 | // Interact with contract and return result 203 | const result = await func(...args) 204 | switch (resultType) { 205 | case 'direct': 206 | return result 207 | case 'none': 208 | return undefined 209 | case 'transaction': 210 | return result.tx 211 | default: 212 | throw Error('Unknown resulttype') 213 | } 214 | } 215 | 216 | // setup & command implementation 217 | async function setup(options) { 218 | dryrun = options.dryrun 219 | estimateGas = options.estimateGas 220 | verbose = options.verbose 221 | controllerAddress = options.controllerAddress 222 | web3 = options.web3 223 | gasPrice = options.gasPrice 224 | gas = options.gas 225 | ensName = options.ensName 226 | 227 | // unset dry-run option when estimateGas is set 228 | if (estimateGas) { 229 | dryrun = false 230 | } 231 | 232 | let registryAddress = options.registryAddress 233 | 234 | if (registryAddress === undefined) { 235 | // use default address based on network ID 236 | const netID = await web3.eth.net.getId() 237 | registryAddress = RegistryAddresses[netID] 238 | if (registryAddress === undefined) { 239 | throw Error(`Unknown networkID ${netID} - Please provide address of the ENS Registry (use option '--registryAddress')`) 240 | } 241 | } 242 | 243 | // get ENS registry 244 | const registryContract = contract(RegistryABI) 245 | registryContract.setProvider(web3.currentProvider) 246 | registry = await registryContract.at(registryAddress) 247 | 248 | // get reverse registrar 249 | reverseRegistrar = await getReverseRegistrar('addr.reverse') 250 | 251 | // If ensName is provided, get its registrar and resolver 252 | if (ensName) { 253 | verbose && console.log(`Getting Registry and Resolver...`) 254 | // check if name exists in new registry 255 | node = namehash.hash(ensName) 256 | if (!await registry.recordExists(node)) { 257 | const oldRegistryAddress = await registry.old() 258 | verbose && console.log(`\t${ensName} not found in current registry. Checking old registry at ${oldRegistryAddress}`) 259 | const oldRegistryContract = contract(OldRegistryABI) 260 | oldRegistryContract.setProvider(web3.currentProvider) 261 | const oldRegistry = await oldRegistryContract.at(oldRegistryAddress) 262 | resolver = await getResolver(oldRegistry, node) 263 | if (resolver) { 264 | console.log(`\n*** NOTE: ${ensName} is only existing in the old registry. It needs to be migrated! ***\n`) 265 | // keep using old registry for this session 266 | registry = oldRegistry 267 | needsMigration = true 268 | } else { 269 | console.log(`\tNo resolver found in old registry`) 270 | } 271 | } else { 272 | resolver = await getResolver(registry, node) 273 | } 274 | registrar = await getRegistrar(ensName) 275 | } 276 | } 277 | 278 | async function stop() { 279 | // Perform a clean shutdown. Especially needed for HDWalletProvider engine to prevent dangling process 280 | if (web3 && web3.currentProvider && web3.currentProvider.engine && web3.currentProvider.engine.stop) { 281 | verbose && console.log('Stopping provider engine') 282 | await web3.currentProvider.engine.stop() 283 | } 284 | if(web3 && web3.currentProvider && web3.currentProvider.connection && web3.currentProvider.connection.close){ 285 | verbose && console.log('Closing provider connection') 286 | await web3.currentProvider.connection.close() 287 | } 288 | } 289 | 290 | async function setAddress({address, coinType}) { 291 | verifyMigrationStatus() 292 | await verifyResolver() 293 | await verifyController() 294 | if (coinTypeETH === coinType) { 295 | await verifyAddress(address) 296 | await verifyInterface('Ethereum Address') 297 | verbose && console.log('Updating Ethereum address...') 298 | try { 299 | return await execute(resolver, 'setAddr(bytes32,address)', node, address) 300 | } catch(error) { 301 | throw Error(`Error performing setAddr(): ${error}`) 302 | } 303 | } else { 304 | // Is there a reasonable way to verify other blockchain addresses? 305 | await verifyInterface('Blockchain Address') 306 | verbose && console.log('Updating blockchain address...') 307 | let decodedAddress = formatsByCoinType[coinType].decoder(address) 308 | try { 309 | return await execute(resolver, 'setAddr(bytes32,uint256,bytes)', node, coinType, decodedAddress) 310 | } catch(error) { 311 | throw Error(`Error performing setAddr(): ${error}`) 312 | } 313 | } 314 | } 315 | 316 | async function clearAddress(coinType) { 317 | verifyMigrationStatus() 318 | await verifyResolver() 319 | await verifyController() 320 | const coinSymbol = formatsByCoinType[coinType].name 321 | verbose && console.log(`Clearing ${coinSymbol} address`) 322 | if (coinTypeETH === coinType) { 323 | await verifyInterface('Ethereum Address') 324 | try { 325 | let result = await execute(resolver, 'setAddr(bytes32,address)', node, zeroAddress) 326 | return result 327 | } catch (error) { 328 | throw Error(`Error clearing ${coinSymbol} address: ${error}`) 329 | } 330 | } else { 331 | await verifyInterface('Blockchain Address') 332 | try { 333 | let result = await execute(resolver, 'setAddr(bytes32,uint256,bytes)', node, coinType, []) 334 | return result 335 | } catch (error) { 336 | throw Error(`Error clearing ${coinSymbol} address: ${error}`) 337 | } 338 | } 339 | } 340 | 341 | async function getAddress(coinType) { 342 | await verifyResolver() 343 | const coinSymbol = formatsByCoinType[coinType].name 344 | verbose && console.log(`Getting ${coinSymbol} address...`) 345 | if (coinTypeETH === coinType) { 346 | await verifyInterface('Ethereum Address') 347 | try { 348 | const result = await execute(resolver, 'addr(bytes32)', node) 349 | return result 350 | } catch (error) { 351 | throw Error(`Error obtaining ${coinSymbol} address: ${error}`) 352 | } 353 | } else { 354 | await verifyInterface('Blockchain Address') 355 | try { 356 | let result = await execute(resolver, 'addr(bytes32,uint256)', node, coinType) 357 | if (result === null) { 358 | result = zeroAddress 359 | } else { 360 | result = formatsByCoinType[coinType].encoder(Buffer.from(result.slice(2), 'hex')) 361 | } 362 | return result 363 | } catch (error) { 364 | throw Error(`Error obtaining ${coinSymbol} address: ${error}`) 365 | } 366 | } 367 | } 368 | 369 | async function setContenthash({contentType, contentHash}) { 370 | verifyMigrationStatus() 371 | await verifyResolver() 372 | await verifyInterface('Content Hash') 373 | await verifyController() 374 | 375 | verbose && console.log('Verifying provided content hash...') 376 | let encodedHash 377 | try { 378 | encodedHash = '0x' + encode(contentType, contentHash) 379 | } catch(error) { 380 | throw Error(`\tError encoding ${contentType} - ${contentHash}: ${error}`) 381 | } 382 | 383 | verbose && console.log('Updating contenthash...') 384 | try { 385 | let result = await execute(resolver, 'setContenthash(bytes32,bytes)', node, encodedHash) 386 | return result 387 | } catch(error) { 388 | throw Error(`Error performing setContenthash(): ${error}`) 389 | } 390 | } 391 | 392 | async function clearContenthash() { 393 | verifyMigrationStatus() 394 | await verifyResolver() 395 | await verifyInterface('Content Hash') 396 | await verifyController() 397 | 398 | verbose && console.log('Clearing contenthash record...') 399 | try { 400 | let result = await execute(resolver, 'setContenthash(bytes32,bytes)', node, []) 401 | return result 402 | } catch(error) { 403 | throw Error(`Error performing clearContenthash(): ${error}`) 404 | } 405 | } 406 | 407 | async function getContenthash() { 408 | await verifyResolver() 409 | await verifyInterface('Content Hash') 410 | verbose && console.log('Getting content...') 411 | try { 412 | const result = await execute(resolver, 'contenthash(bytes32)', node) 413 | if ((result === null) || (result === undefined)) { 414 | return { 415 | codec: undefined, 416 | hash: undefined 417 | } 418 | } else if (estimateGas) { 419 | // result should be gas value 420 | return result 421 | } else { 422 | // result should be a real contenthash value. Decode it and return as plaintext 423 | try { 424 | const codec = getCodec(result) 425 | const hash = decode(result) 426 | return { 427 | codec, 428 | hash 429 | } 430 | } catch (decodingError) { 431 | throw Error(`Error decoding contenthash "${result}": ${decodingError}`) 432 | } 433 | } 434 | } catch (error) { 435 | throw Error(`Error getting contenthash: ${error}`) 436 | } 437 | } 438 | 439 | async function listInterfaces() { 440 | await verifyResolver() 441 | await verifyInterface('EIP165') 442 | verbose && console.log('Getting supported interfaces...') 443 | let supportedInterfaces = [] 444 | try { 445 | for (const interfaceName of Object.keys(ResolverInterfaces)) { 446 | const support = await execute(resolver, 'supportsInterface(bytes4)', ResolverInterfaces[interfaceName]) 447 | support && supportedInterfaces.push(interfaceName) 448 | } 449 | } catch(error) { 450 | throw Error(`Error querying interfaces: ${error}`) 451 | } 452 | return supportedInterfaces 453 | } 454 | 455 | async function getReverseName(address) { 456 | verbose && console.log(`Getting reverse name for ${address}...`) 457 | const reverseNode = await getReverseNode(address) 458 | const reverseResolver = await getReverseResolver(reverseNode) 459 | if (!reverseResolver) { 460 | return '' 461 | } 462 | return await execute(reverseResolver, 'name(bytes32)', reverseNode) 463 | } 464 | 465 | async function setReverseName(reverseName) { 466 | verifyMigrationStatus() 467 | verbose && console.log(`Setting reverse name ${reverseName} for address ${controllerAddress}...`) 468 | try { 469 | return await execute(reverseRegistrar, 'setName(string)', reverseName) 470 | } catch(error) { 471 | throw Error(`Error performing setReverseName(): ${error}`) 472 | } 473 | } 474 | 475 | async function clearReverseName() { 476 | verifyMigrationStatus() 477 | verbose && console.log(`Clearing reverse name for address ${controllerAddress}...`) 478 | try { 479 | return await execute(reverseRegistrar, 'setName(string)', '') 480 | } catch(error) { 481 | throw Error(`Error performing clearReverseName(): ${error}`) 482 | } 483 | } 484 | 485 | async function getInfo() { 486 | const controller = await getController(node) 487 | const registrant = await getRegistrant() 488 | const expires = await getExpires() 489 | let resolvedAddress = zeroAddress 490 | try { 491 | resolvedAddress = await getAddress(coinTypeETH) 492 | }catch(error){ 493 | // Can't obtain address, likely due to no resolver being set. Ignore. 494 | } 495 | let reverseName = undefined 496 | let reverseResolver = undefined 497 | if (resolvedAddress !== zeroAddress) { 498 | try { 499 | reverseName = await getReverseName(resolvedAddress) 500 | const reverseNode = await getReverseNode(resolvedAddress) 501 | reverseResolver = await getReverseResolver(reverseNode) 502 | }catch(error){ 503 | // Can't obtain reverse name record, likely due to no reverse 504 | // resolver being set. Ignore. 505 | } 506 | } 507 | return { 508 | Registrant: registrant ? registrant : 'n/a', 509 | Controller: controller, 510 | Resolver: resolver ? resolver.address : zeroAddress, 511 | Expires: (expires > 0) ? new Date(expires*1000) : 'n/a', 512 | Address: resolvedAddress, 513 | ReverseResolver: reverseResolver ? reverseResolver.address: zeroAddress, 514 | ReverseName: reverseName ? reverseName : 'n/a', 515 | Registrar: registrar ? registrar.address : zeroAddress 516 | } 517 | } 518 | 519 | return { 520 | setup, 521 | stop, 522 | setContenthash, 523 | getContenthash, 524 | clearContenthash, 525 | setAddress, 526 | getAddress, 527 | clearAddress, 528 | setReverseName, 529 | getReverseName, 530 | clearReverseName, 531 | listInterfaces, 532 | getInfo 533 | } 534 | } 535 | --------------------------------------------------------------------------------