Skip to content

Instantly share code, notes, and snippets.

@yuanfeiz
Last active June 29, 2017 13:53
Show Gist options
  • Save yuanfeiz/0e3f0e05efd02259affc4c84ca0561d3 to your computer and use it in GitHub Desktop.
Save yuanfeiz/0e3f0e05efd02259affc4c84ca0561d3 to your computer and use it in GitHub Desktop.
const bluebird = require('bluebird');
const Web3 = require('web3');
const _ = require('lodash');
const bbRetry = require('bluebird-retry');
const { env } = require('../libs/env-utils');
const config = require('../config/contracts')[env];
const { add0x, buildContract } = require('../libs/eth-helper');
const web3 = new Web3(new Web3.providers.HttpProvider(config.rpcAddr));
class TransactionConfirmError extends Error {
constructor(txHash, message) {
super(message);
this.txHash = txHash;
}
}
class TransactionFailedError extends TransactionConfirmError {
constructor(txHash, gasUsed) {
super(txHash, 'Transaction failed (out of gas, thrown)');
this.gasUsed = gasUsed;
}
}
const bunyan = require('bunyan');
const log = bunyan.createLogger({
name: 'ens-service',
serializers: {
err: bunyan.stdSerializers.err,
}
});
const ENSMode = Object.freeze({
Open: 0,
Auction: 1,
Owned: 2,
Forbidden: 3,
Reveal: 4,
NotYetAvailable: 5,
});
class ENSService {
/**
* Constructor
*
* @param {*} _web3
* @param {*} _ensConfig
* @param {*} _registrarConfig
* @param {Logger} _log
*/
constructor(_web3, _ensConfig, _registrarConfig, _deedConfig, _log) {
this.web3 = _web3;
this.web3.eth = bluebird.promisifyAll(this.web3.eth);
this.ens = bluebird.promisifyAll(buildContract(_web3, _ensConfig));
this.registrar = bluebird.promisifyAll(buildContract(_web3, _registrarConfig));
this.deedConfig = _deedConfig;
this.log = _log || log;
}
getAllowedTime(domain) {
let hash = this.getDomainHash(domain);
return this.registrar
.getAllowedTimeAsync(hash)
.then((timestamp) => {
return new Date(timestamp.toNumber() * 1000);
});
}
getState(domain) {
let hash = this.getDomainHash(domain);
return this.registrar.stateAsync(hash)
.then((_state) => _.findKey(ENSMode, (v) => v === this.web3.toDecimal(_state)));
}
getDomainHash(domain) {
if (domain.endsWith('.eth')) {
domain = domain.substr(0, domain.length - 4);
}
return this.web3.sha3(domain);
}
getLaunchLength() {
if (!this._launchLength) {
this._launchLength = parseInt(this.registrar.launchLength());
}
return this._launchLength;
}
getRegistryStarted() {
if (!this._registryStarted) {
this._registryStarted = parseInt(this.registrar.registryStarted());
}
return this._registryStarted;
}
getInfo() {
if (!this._info) {
const network = parseInt(this.web3.version.network);
this._info = {
registrarAddress: this.registrar.address,
ensAddress: this.ens.address,
network: network,
networkName: network === 42 ? 'kovan' : 'unknown',
launchLength: this.getLaunchLength(),
registryStarted: this.getRegistryStarted()
};
}
return this._info;
}
async createBid(owner, hash, bid, deposit, salt) {
// check status
let state = parseInt(await this.registrar.stateAsync(hash));
bid = this.web3.toHex(this.web3.toWei(bid, 'ether'));
let sealedBid = await this.registrar.shaBidAsync(hash, owner, bid, salt);
this.log.info({ hash, owner, bid, salt, sealedBid }, 'Ready to create bid');
let txOptions = {
nonce: await this.getNonce(owner),
gas: 600000,
gasPrice: 20000000000,
to: add0x(this.registrar.address),
value: bid,
};
let data;
switch (state) {
// New auction
case ENSMode.Open:
this.log.info({ hash }, 'New auction');
data = this.registrar.startAuctionsAndBid.getData([hash], sealedBid);
break;
// Auction started, add new bid
case ENSMode.Auction:
this.log.info({ hash }, 'Auction started, add new bid');
data = this.registrar.newBid.getData(sealedBid);
break;
default:
this.log.error({ state }, 'Invalid state');
return Promise.reject(new Error('Invalid state: ' + state));
}
let rawTx = Object.assign(txOptions, { data });
const signerService = require('../services/signer');
let rawTransaction = await signerService.sign(rawTx, owner);
// Send transaction
let bidTx = await this.web3.eth
.sendRawTransactionAsync(rawTransaction)
.then(async (txHash) => {
await this.hasTxGoneThrough(txHash);
return txHash;
});
return { bidTx, state };
}
async getNonce(owner) {
return add0x(this.web3.eth.getTransactionCountAsync(owner));
}
async revealAuction(hash, bid, salt, owner) {
bid = this.web3.toHex(this.web3.toWei(bid));
this.log.info({ hash, bid, salt, owner }, 'Ready to reveal bid');
let data = this.registrar.unsealBid.getData(hash, bid, salt);
const rawTx = {
nonce: await this.getNonce(owner),
gas: 600000,
gasPrice: 20000000000,
to: add0x(this.registrar.address),
data: add0x(data)
};
const signerService = require('../services/signer');
let rawTransaction = await signerService.sign(rawTx, owner);
// Send transaction
return this.web3.eth
.sendRawTransactionAsync(rawTransaction)
.then(async (txHash) => {
await this.hasTxGoneThrough(txHash);
return txHash;
});
}
async finalizeAuction(hash, address) {
let data = this.registrar.finalizeAuction.getData(hash);
this.log.info({ hash }, 'Ready to finalize bid');
const rawTx = {
nonce: await this.getNonce(address),
gas: 600000,
gasPrice: 20000000000,
to: add0x(this.registrar.address),
data: add0x(data)
};
const signerService = require('../services/signer');
let rawTransaction = await signerService.sign(rawTx, address);
// Send transaction
return this.web3.eth
.sendRawTransactionAsync(rawTransaction)
.then(async (txHash) => {
await this.hasTxGoneThrough(txHash);
return txHash;
});
}
/**
* Get deed contract of the hash
*
* @param {string} hash
*/
async getEntry(hash) {
let h = await this.registrar.entriesAsync(hash);
return {
state: this.web3.toDecimal(h[0]),
deedAddress: h[1],
registrationDate: this.web3.toDecimal(h[2]) * 1000,
value: this.web3.toDecimal(this.web3.fromWei(h[3])),
highestBid: this.web3.toDecimal(this.web3.fromWei(h[4])),
hash
};
}
getRegistrationDate(hash) {
return this.getEntry(hash).then((entry) => entry.registrationDate);
}
getTxReceipt(txHash) {
return this.web3.eth.getTransactionReceiptAsync(txHash);
}
getTx(txHash) {
return this.web3.eth.getTransactionAsync(txHash);
}
/**
* Return the current deed of the hash
*
* @param {string} hash domain hash
*/
async getDeed(hash) {
let logger = this.log.child({ action: 'getOwner', hash });
let h = await this.getEntry(hash);
// Entry is not found
if (!h) {
let err = new Error('Entry for hash is not found');
logger.error(err);
throw err;
}
let isFinalized = true;
// Check if auction is finalized
let now = (new Date()).getTime() * 1000;
if (h.registrationDate > now) {
this.log.warn({
registration_date: h.registrationDate,
now
}, 'Getting owner when it\'s not finalized yet');
this.isFinalized = false;
}
let deed = this._buildDeed(h.deedAddress);
return bluebird.props({
creationDate: deed.creationDateAsync().then(v => this.web3.toDecimal(v) * 1000),
owner: deed.ownerAsync(),
previousOwner: deed.previousOwnerAsync(),
registrar: deed.registrarAsync(),
value: deed.valueAsync().then(v => this.web3.toDecimal(this.web3.fromWei(v))),
now,
hash,
isFinalized
});
}
/**
* Build the deed contract from address
*
* @param {string} deedAddress
*/
_buildDeed(deedAddress) {
const deedConfig = Object.assign(
{},
this.deedConfig,
{ address: deedAddress }
);
return bluebird.promisifyAll(buildContract(this.web3, deedConfig));
}
hasTxGoneThrough(txHash, _retryOptions) {
let options = _.defaults({
context: this,
max_tries: 5,
backoff: 5
}, _retryOptions);
let counter = 0;
return bbRetry(() => {
this.log.info({ attempt_cnt: ++counter, max_tries: options.max_tries }, 'Retry hasTxGoneThrough');
return this._hasTxGoneThrough(txHash);
}, options);
}
async _hasTxGoneThrough(txHash) {
try {
let receipt = await this.getTxReceipt(txHash);
if (!receipt) {
throw new TransactionConfirmError(txHash, 'Receipt is not available yet.');
}
let tx = await this.getTx(txHash);
if (!tx) {
throw new TransactionConfirmError(txHash, 'Transaction is not available yet.');
}
if (receipt.gasUsed >= tx.gas) {
// Used up all the gas provided
throw new TransactionFailedError(txHash, receipt.gasUsed);
}
this.log.info({ tx_hash: txHash }, 'Transaction is mined');
return true;
} catch (err) {
if (err instanceof TransactionConfirmError) {
this.log.error({ err, tx_hash: txHash }, 'Transaction doesn\'t gone through');
}
throw err;
}
}
watchEntryUpdates(handler) {
if (!handler) {
throw new Error('handler is missing');
}
this.log.info('Watching BidRevealed and NewBid events');
// BidReveal with status = 2() and status = 3() would
// effect the deed.
this.registrar.BidRevealed().watch(async (err, event) => {
this.log.info({ event }, 'Got BidRevealed Event');
let { hash, status } = event.args;
if (status === '2' || status === '3') {
let entry = await this.getEntry(hash);
return handler(entry);
}
});
// HashRegistered(Finalize) would change the actual price if
// bid is the only one.
this.registrar.HashRegistered().watch(async (err, event) => {
this.log.info({ event }, 'Got HashRegistered Event');
let { hash } = event.args;
let entry = await this.getEntry(hash);
return handler(entry);
});
}
stopWatchEntryUpdates() {
}
}
const ensService = new ENSService(web3, config.ens, config.registrar, config.deed, log);
module.exports = {
ensService,
ENSService,
web3,
TransactionConfirmError,
TransactionFailedError
};
const Web3 = require('web3');
const web3 = new Web3();
/**
* add0x
* @param {*} input
*/
function add0x (input) {
if (typeof(input) !== 'string') {
return input;
}
else if (input.length < 2 || input.slice(0,2) !== '0x') {
return '0x' + input;
}
else {
return input;
}
}
function fromAscii(str, padding) {
var hex = '0x';
for (var i = 0; i < str.length; i++) {
var code = str.charCodeAt(i);
var n = code.toString(16);
hex += n.length < 2 ? '0' + n : n;
}
return hex + '0'.repeat(padding*2 - hex.length + 2);
};
function toAscii(hex) {
var str = '',
i = 0,
l = hex.length;
if (hex.substring(0, 2) === '0x') {
i = 2;
}
for (; i < l; i+=2) {
var code = parseInt(hex.substr(i, 2), 16);
if (code === 0) continue; // this is added
str += String.fromCharCode(code);
}
return str;
};
/**
* buildContract
* @param {*} config
*/
function buildContract(_web3, config) {
return _web3.eth.contract(config.abi).at(config.address);
}
/**
* parseLog
*/
var SolidityEvent = require("web3/lib/web3/event.js");
function parseLog(logs, abi) {
// pattern similar to lib/web3/contract.js: addEventsToContract()
var decoders = abi.filter(function (json) {
return json.type === 'event';
}).map(function(json) {
// note first and third params required only by enocde and execute;
// so don't call those!
return new SolidityEvent(null, json, null);
});
return logs.map(function (log) {
return decoders.find(function(decoder) {
return (decoder.signature() == log.topics[0].replace("0x",""));
}).decode(log);
})
}
module.exports = {
add0x,
buildContract,
parseLog,
fromAscii,
toAscii
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment