ethereum-ens
Version:
Javascript bindings for the Ethereum Name Service
567 lines (535 loc) • 15.7 kB
JavaScript
/*
This file is part of ethereum-ens.
ethereum-ens is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ethereum-ens is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with ethereum-ens. If not, see <http://www.gnu.org/licenses/>.
*/
var CryptoJS = require('crypto-js');
var pako = require('pako');
var Promise = require('bluebird');
var textEncoding = require('text-encoding');
var TextDecoder = textEncoding.TextDecoder;
var uts46 = require('idna-uts46');
var _ = require('underscore');
var registryInterface = [
{
"constant": true,
"inputs": [
{
"name": "node",
"type": "bytes32"
}
],
"name": "resolver",
"outputs": [
{
"name": "",
"type": "address"
}
],
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "node",
"type": "bytes32"
}
],
"name": "owner",
"outputs": [
{
"name": "",
"type": "address"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "node",
"type": "bytes32"
},
{
"name": "resolver",
"type": "address"
}
],
"name": "setResolver",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "node",
"type": "bytes32"
},
{
"name": "label",
"type": "bytes32"
},
{
"name": "owner",
"type": "address"
}
],
"name": "setSubnodeOwner",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "node",
"type": "bytes32"
},
{
"name": "owner",
"type": "address"
}
],
"name": "setOwner",
"outputs": [],
"type": "function"
}
];
var resolverInterface = [
{
"constant": true,
"inputs": [
{
"name": "node",
"type": "bytes32"
}
],
"name": "addr",
"outputs": [
{
"name": "",
"type": "address"
}
],
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "node",
"type": "bytes32"
}
],
"name": "content",
"outputs": [
{
"name": "",
"type": "bytes32"
}
],
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "node",
"type": "bytes32"
}
],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "node",
"type": "bytes32"
},
{
"name": "kind",
"type": "bytes32"
}
],
"name": "has",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "node",
"type": "bytes32"
},
{
"name": "addr",
"type": "address"
}
],
"name": "setAddr",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "node",
"type": "bytes32"
},
{
"name": "hash",
"type": "bytes32"
}
],
"name": "setContent",
"outputs": [],
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "node",
"type": "bytes32"
},
{
"name": "name",
"type": "string"
}
],
"name": "setName",
"outputs": [],
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "node",
"type": "bytes32"
},
{
"name": "contentType",
"type": "uint256"
}
],
"name": "ABI",
"outputs": [
{
"name": "",
"type": "uint256"
},
{
"name": "",
"type": "bytes"
}
],
"payable": false,
"type": "function"
}
];
var registryAddresses = {
// Mainnet
"1": "0x314159265dd8dbb310642f98f50c066173c1259b",
// Ropsten
"3": "0x112234455c3a32fd11230c42e7bccd4a84e02010",
}
/**
* @class
*/
function Resolver(ens, node, contract) {
this.ens = ens;
this.node = node;
this.instancePromise = ens.registryPromise.then(function(registry) {
return registry.resolverAsync(node).then(function(address) {
if(address == "0x0000000000000000000000000000000000000000") {
return Promise.reject(ENS.NameNotFound);
}
return Promise.promisifyAll(contract.at(address));
});
});
_.each(contract.abi, function(signature) {
this[signature.name] = function() {
var args = arguments;
return this.instancePromise.then(function(instance) {
return _.partial(instance[signature.name + 'Async'], node).apply(instance, args);
}).bind(this);
}.bind(this);
}.bind(this));
}
var abiDecoders = {
1: function(data) {
data = new TextDecoder("utf-8").decode(data);
return JSON.parse(data);
},
2: function(data) {
data = pako.inflate(data, {to: 'string'});
return JSON.parse(data);
}
};
var supportedDecoders = _.reduce(_.keys(abiDecoders), function(memo, val) { return memo | val; });
/**
* resolverAddress returns the address of the resolver.
* @returns A promise for the address of the resolver.
*/
Resolver.prototype.resolverAddress = function() {
return this.instancePromise.then(function(instance) {
return instance.address;
});
}
/**
* reverseAddr looks up the reverse record for the address returned by the resolver's addr()
* @returns A promise for the Resolver for the reverse record.
*/
Resolver.prototype.reverseAddr = function() {
return this.addr().then(function(addr) {
return this.ens.reverse(addr);
}).bind(this);
}
function fromHex(x) {
if(x.startsWith("0x")) {
x = x.slice(2);
}
var ret = new Uint8Array(x.length / 2);
for(var i = 0; i < ret.length; i++) {
ret[i] = parseInt(x.slice(i * 2, i * 2 + 2), 16);
}
return ret;
};
/**
* abi returns the ABI associated with the name. Automatically looks for an ABI on the
* reverse record if none is found on the name itself.
* @param {bool} Optional. If false, do not look up the ABI on the reverse entry.
* @returns {object} A promise for the contract ABI.
*/
Resolver.prototype.abi = function(reverse) {
return this.instancePromise.then(function(instance) {
return instance.ABIAsync(this.node, supportedDecoders).then(function(result) {
if(result[0] == 0) {
if(reverse == false) return null;
return this.reverseAddr().then(function(reverse) {
return reverse.abi(false);
});
} else {
return abiDecoders[result[0]](fromHex(result[1]));
}
}.bind(this));
}.bind(this));
};
/**
* contract returns a web3 contract object. The address is that returned by this resolver's
* `addr()`, and the ABI is loaded from this resolver's `ABI()` method, or the ABI on the
* reverse record if that's not found. Returns null if no address is specified or no ABI
* was found. The returned contract object will not be promisifed or otherwise modified.
* @returns {object} A promise for the contract instance.
*/
Resolver.prototype.contract = function() {
return Promise.join(this.abi(), this.addr(), function(abi, addr) {
return this.ens.web3.eth.contract(abi).at(addr);
}.bind(this));
};
/**
* @class
*
* @description Provides an easy-to-use interface to the Ethereum Name Service.
*
* Example usage:
*
* var ENS = require('ethereum-ens');
* var Web3 = require('web3');
*
* var web3 = new Web3();
* var ens = new ENS(web3);
*
* var address = ens.resolver('foo.eth').addr().then(function(addr) { ... });
*
* Functions that require communicating with the node return promises, rather than
* using callbacks. A promise has a `then` function, which takes a callback and will
* call it when the promise is fulfilled; `then` returns another promise, so you can
* chain callbacks. For more details, see http://bluebirdjs.com/.
*
* Notably, the `resolver` method returns a resolver instance immediately; lookup of
* the resolver address is done in the background or when you first call an asynchronous
* method on the resolver.
*
* Functions that create transactions also take an optional 'options' argument;
* this has the same parameters as web3.
*
* @author Nick Johnson <nick@ethereum.org>
* @date 2016
* @license LGPL
*
* @param {object} web3 A web3 instance to use to communicate with the blockchain.
* @param {address} address Optional. The address of the ENS registry. Defaults to the public ENS registry.
*/
function ENS (web3, address) {
this.web3 = web3;
var registryContract = web3.eth.contract(registryInterface);
if(address != undefined) {
this.registryPromise = Promise.resolve(Promise.promisifyAll(registryContract.at(address)));
} else {
this.registryPromise = Promise.promisify(web3.version.getNetwork)().then(function(version) {
return Promise.promisifyAll(registryContract.at(registryAddresses[version]));
});
}
}
ENS.NameNotFound = Error("ENS name not found");
function sha3(input) {
return CryptoJS.SHA3(input, {outputLength: 256})
}
/**
* normalise namepreps a name, throwing an exception if it contains invalid characters.
* @param {string} name The name to normalise
* @returns The normalised name. Throws ENS.InvalidName if the name contains invalid characters.
*/
function normalise(name) {
return uts46.toUnicode(name, {useStd3ASCII: true, transitional: false});
}
ENS.prototype.normalise = normalise;
/**
* namehash implements ENS' name hash algorithm.
* @param {string} name The name to hash
* @returns The computed namehash, as a hex string.
*/
function namehash(name) {
name = normalise(name);
var node = CryptoJS.enc.Hex.parse('0000000000000000000000000000000000000000000000000000000000000000');
if(name && name != '') {
var labels = name.split(".");
for(var i = labels.length - 1; i >= 0; i--) {
node = sha3(node.concat(sha3(labels[i])));
}
}
return '0x' + node.toString();
}
ENS.prototype.namehash = namehash;
function parentNamehash(name) {
var dot = name.indexOf('.');
if(dot == -1) {
return ['0x' + sha3(normalise(name)), namehash('')];
} else {
return ['0x' + sha3(normalise(name.slice(0, dot))), namehash(name.slice(dot + 1))];
}
}
/**
* resolver returns a resolver object for the specified name, throwing
* ENS.NameNotFound if the name does not exist in ENS.
* Resolver objects are wrappers around web3 contract objects, with the
* first argument - always the node ID in an ENS resolver - automatically
* supplied. So, to call the `addr(node)` function on a standard resolver,
* you only have to call `addr()`. Returned objects are also 'promisified' - they
* return a Bluebird Promise object instead of taking a callback.
* @param {string} name The name to look up.
* @param {list} abi Optional. The JSON ABI definition to use for the resolver.
* if none is supplied, a default definition implementing `has`, `addr`, `name`,
* `setName` and `setAddr` is supplied.
* @returns The resolver object.
*/
ENS.prototype.resolver = function(name, abi) {
abi = abi || resolverInterface;
var node = namehash(name);
return new Resolver(this, node, this.web3.eth.contract(abi));
};
/**
* reverse returns a resolver object for the reverse resolution of the specified address,
* throwing ENS.NameNotFound if the reverse record does not exist in ENS.
* Resolver objects are wrappers around web3 contract objects, with the
* first argument - always the node ID in an ENS resolver - automatically
* supplied. So, to call the `addr(node)` function on a standard resolver,
* you only have to call `addr()`. Returned objects are also 'promisified' - they
* return a Bluebird Promise object instead of taking a callback.
* @param {string} address The address to look up.
* @param {list} abi Optional. The JSON ABI definition to use for the resolver.
* if none is supplied, a default definition implementing `has`, `addr`, `name`,
* `setName` and `setAddr` is supplied.
* @returns The resolver object.
*/
ENS.prototype.reverse = function(address, abi) {
if(address.startsWith("0x"))
address = address.slice(2);
return this.resolver(address.toLowerCase() + ".addr.reverse", abi);
};
/**
* setResolver sets the address of the resolver contract for the specified name.
* The calling account must be the owner of the name in order for this call to
* succeed.
* @param {string} name The name to update
* @param {address} address The address of the resolver
* @param {object} options An optional dict of parameters to pass to web3.
* @returns A promise that returns the transaction ID when the transaction is mined.
*/
ENS.prototype.setResolver = function(name, addr, params) {
var node = namehash(name);
return this.registryPromise.then(function(registry) {
return registry.setResolverAsync(node, addr, params);
});
}
/**
* owner returns the address of the owner of the specified name.
* @param {string} name The name to look up.
* @returns A promise returning the owner address of the specified name.
*/
ENS.prototype.owner = function(name, callback) {
var node = namehash(name);
return this.registryPromise.then(function(registry) {
return registry.ownerAsync(node);
});
}
/**
* setOwner sets the owner of the specified name. Only the owner may call
* setResolver or setSubnodeOwner. The calling account must be the current
* owner of the name in order for this call to succeed.
* @param {string} name The name to update
* @param {address} address The address of the new owner
* @param {object} options An optional dict of parameters to pass to web3.
* @returns A promise returning the transaction ID of the transaction, once mined.
*/
ENS.prototype.setOwner = function(name, addr, params) {
var node = namehash(name);
return this.registryPromise.then(function(registry) {
return registry.setOwnerAsync(node, addr, params);
});
}
/**
* setSubnodeOwner sets the owner of the specified name. The calling account
* must be the owner of the parent name in order for this call to succeed -
* for example, to call setSubnodeOwner on 'foo.bar.eth', the caller must be
* the owner of 'bar.eth'.
* @param {string} name The name to update
* @param {address} address The address of the new owner
* @param {object} options An optional dict of parameters to pass to web3.
* @returns A promise returning the transaction ID of the transaction, once mined.
*/
ENS.prototype.setSubnodeOwner = function(name, addr, params) {
var node = parentNamehash(name);
return this.registryPromise.then(function(registry) {
return registry.setSubnodeOwnerAsync(node[1], node[0], addr, params);
});
}
module.exports = ENS;