Last active
June 29, 2017 13:53
-
-
Save yuanfeiz/0e3f0e05efd02259affc4c84ca0561d3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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