ethereum-bridge
Version:
Ethereum logs listener to connect Oraclize onchain events to the Oraclize offchain engine
1,166 lines (1,066 loc) • 56 kB
JavaScript
#!/usr/bin/env node
var semver = require('semver')
checkVersion()
var readline = require('readline')
var i18n = require('i18n')
var schedule = require('node-schedule')
var bridgeUtil = require('./lib/bridge-util')
var BridgeCliParse = require('./lib/bridge-cli-parse')
var bridgeCore = require('./lib/bridge-core')
var BridgeAccount = require('./lib/bridge-account')
var BlockchainInterface = require('./lib/blockchain-interface')
var BridgeLogManager = require('./lib/bridge-log-manager')
var bridgeHttp = require('./lib/bridge-http')
var BridgeStats = require('./lib/bridge-stats')
var BridgeDbManagerLib = require('./lib/bridge-db-manager')
var BridgeDbManager = BridgeDbManagerLib.BridgeDbManager
var BridgeLogEvents = BridgeLogManager.events
var AddressWatcher = require('./lib/address-watcher')
var winston = require('winston')
var colors = bridgeUtil.colors
var async = require('async')
var callbackQueue = async.queue(__callbackWrapper, 2)
var logsQueue = async.queue(logsWrapper, 1)
var fs = require('fs')
var path = require('path')
var asyncLoop = require('node-async-loop')
var moment = require('moment')
var OracleInstance = bridgeCore.OracleInstance
var activeOracleInstance
var dbConfig = {
'driver': 'tingodb',
'database': toFullPath('./database/tingodb/')
}
i18n.configure({
defaultLocale: 'ethereum',
updateFiles: false,
objectNotation: true,
directory: toFullPath('./config/text/')
})
var BLOCKCHAIN_NAME = i18n.__('blockchain_name')
var BLOCKCHAIN_ABBRV = i18n.__('blockchain_abbrv')
var BLOCKCHAIN_BASE_UNIT = i18n.__('base_unit')
var BRIDGE_NAME = i18n.__('bridge_name')
var BRIDGE_VERSION = require('./package.json').version
if (!fs.existsSync(dbConfig.database)) {
fs.mkdirSync(dbConfig.database)
}
dbConfig['BRIDGE_VERSION'] = BRIDGE_VERSION
BridgeDbManager(dbConfig)
var CallbackTx = BridgeDbManager().CallbackTx
var BridgeCache = BridgeDbManager().cache
var mode = 'active'
var keyFilePath = toFullPath('./config/instance/keys.json')
var configFilePath = ''
var isTestRpc = false
var pricingInfo = []
var basePrice = 0
var officialOar = []
var currentInstance = 'latest'
var bridgeObj = {'BRIDGE_NAME': BRIDGE_NAME, 'BRIDGE_VERSION': BRIDGE_VERSION}
var cliOptions = {
'BLOCKCHAIN_ABBRV': BLOCKCHAIN_ABBRV,
'BLOCKCHAIN_BASE_UNIT': BLOCKCHAIN_BASE_UNIT,
'KEY_FILE_PATH': keyFilePath,
'BRIDGE_NAME': BRIDGE_NAME,
'DEFAULT_NODE': 'localhost:8545',
'BRIDGE_VERSION': BRIDGE_VERSION
}
BridgeCliParse(cliOptions)
var cliConfiguration = BridgeCliParse().getConfiguration()
BridgeLogManager.passCliConf(cliConfiguration)
console.log('Please wait...')
var logger = new (winston.Logger)({
levels: bridgeUtil.getLogLevels(),
transports: [
new (winston.transports.Console)({
colorize: true,
level: bridgeUtil.parseLogLevel(cliConfiguration.loglevel),
timestamp: function () {
return moment().toISOString()
},
formatter: function (options) {
var multiLineFmt = (k, val) =>
(typeof val === 'string' && val.split('\n').length > 1) ? val.split('\n') : val;
return '[' + colors.grey(options.timestamp()) + '] ' + bridgeUtil.colorize(options.level.toUpperCase()) + ' ' + (options.message ? options.message : '') +
(options.meta && Object.keys(options.meta).length ? '\n\t' + JSON.stringify(options.meta, multiLineFmt, 4) : '')
}
}),
new (winston.transports.File)({
filename: cliConfiguration.logFilePath,
level: 'debug'
})
]
})
logger.debug('parsed options', cliConfiguration)
if (cliConfiguration.dev) {
logger.warn('--dev mode active, contract myid checks and pending queries are skipped, use this only when testing, not in production')
}
if (cliConfiguration.broadcast) {
mode = 'broadcast'
try {
var privateKeyContent = fs.readFileSync(keyFilePath)
if (JSON.parse(privateKeyContent.toString()).length > 0) {
if (!cliConfiguration.account) cliConfiguration.account = 0
} else if (cliConfiguration.account) {
cliConfiguration.new = true
if (cliConfiguration['non-interactive'] === false) {
logger.error('no account', cliConfiguration.account, 'found in your keys.json file, automatically removing the -a option...')
cliConfiguration.account = null
}
}
} catch (err) {
if (err.code === 'ENOENT') {
cliConfiguration.new = true
if (cliConfiguration['no-hints'] === false) logger.warn('keys.json not found, creating the new file', keyFilePath)
try {
bridgeUtil.saveJsonFile(keyFilePath, JSON.stringify([]))
} catch (e) {
logger.error('failed to save the key file', e)
}
}
}
}
logger.info('you are running ' + BRIDGE_NAME, '- version: ' + BRIDGE_VERSION)
logger.info('saving logs to:', cliConfiguration.logFilePath)
var oraclizeConfiguration = {
'context_name': bridgeUtil.getContext({'prefix': BLOCKCHAIN_ABBRV, 'random': true}),
'latest_block_number': -1,
'oar': cliConfiguration.oar,
'node': {
'main': cliConfiguration.defaultnode,
'backup': []
},
'contracts': {
'connector': {
'binary': toFullPath('./contracts/binary/oraclizeConnector.binary'),
'abi': toFullPath('./contracts/abi/oraclizeConnector.json'),
'source': toFullPath('./contracts/ethereum-api/connectors/oraclizeConnector.sol')
},
'oar': {
'binary': toFullPath('./contracts/binary/addressResolver.binary'),
'abi': toFullPath('./contracts/abi/addressResolver.json'),
'source': toFullPath('./contracts/ethereum-api/connectors/addressResolver.sol')
}
},
'deterministic_oar': !cliConfiguration['disable-deterministic-oar'],
'deploy_gas': parseInt(cliConfiguration.defaultGas),
'account': cliConfiguration.account,
'mode': mode,
'key_file': keyFilePath,
'gas_price': parseInt(cliConfiguration.gasprice)
}
if (cliConfiguration.abiconn || cliConfiguration.abioar) {
if (cliConfiguration.abiconn) oraclizeConfiguration.contracts.connector.abi = toFullPath(cliConfiguration.abiconn)
if (cliConfiguration.abioar) oraclizeConfiguration.contracts.oar.abi = toFullPath(cliConfiguration.abioar)
}
if (cliConfiguration.instance) {
var instanceToLoad = cliConfiguration.instance
var instances = getInstances()
logger.debug('instances found', instances)
if (instances.length === 0) throw new Error('no instance files found')
if (instanceToLoad !== 'latest' && instanceToLoad.indexOf('.json') === -1) {
instanceToLoad += '.json'
}
if (instances.indexOf(instanceToLoad) > -1) {
importConfigFile(instanceToLoad)
} else if (instanceToLoad === 'latest') {
instanceToLoad = instances[instances.length - 1]
importConfigFile(instanceToLoad)
} else {
logger.error(instanceToLoad + ' not found in ./config/instance/')
}
currentInstance = instanceToLoad
} else if (!cliConfiguration.oar) {
if (cliConfiguration.new && cliConfiguration.account) throw new Error("--new flag doesn't require the -a flag")
if (cliConfiguration.new && cliConfiguration.broadcast) {
bridgeUtil.generateNewAddress(keyFilePath, function (err, res) {
if (err) throw new Error(err)
logger.info('New address generated', res.address, 'at position: ' + res.account_position)
oraclizeConfiguration.account = res.account_position
startUpLog(true)
})
} else if (cliConfiguration.new && !cliConfiguration.broadcast) throw new Error('--new flag requires --broadcast mode')
else startUpLog(true)
} else {
if (cliConfiguration.new) throw new Error('cannot generate a new address if contracts are already deployed, please remove the --new flag')
startUpLog(false, oraclizeConfiguration)
}
function getInstances () {
var instances = fs.readdirSync(toFullPath('./config/instance/'))
var instanceKeyIndex = instances.indexOf('keys.json')
if (instanceKeyIndex > -1) {
instances.splice(instanceKeyIndex, 1)
}
var keepFile = instances.indexOf('.keep')
if (keepFile > -1) {
instances.splice(keepFile, 1)
}
return instances
}
function toFullPath (filePath) {
return path.join(__dirname, filePath)
}
function oracleFromConfig (config) {
try {
if ((pricingInfo.length > 0 && typeof config.onchain_config === 'undefined') || cliConfiguration['remote-price'] === true) {
config.onchain_config = {}
config.onchain_config.pricing = pricingInfo
config.onchain_config.base_price = basePrice
}
config.gas_price = cliConfiguration.gasprice
logger.debug('configuration file', config)
activeOracleInstance = new OracleInstance(config)
checkNodeConnection()
activeOracleInstance.isValidOracleInstance()
userWarning()
logger.info('OAR found:', activeOracleInstance.oar)
logger.info('connector found:', activeOracleInstance.connector)
logger.info('callback address:', activeOracleInstance.account)
if (cliConfiguration['update-ds'] === true) {
logger.info('updating datasource pricing...')
activeOracleInstance.multiAddDSource(activeOracleInstance.connector, config.onchain_config.pricing, function (err, res) {
if (err) logger.error('multiAddDSource error', err)
else logger.info('multiAddDSource correctly updated')
})
}
if (!cliConfiguration.newconn) runLog()
else {
if (cliConfiguration['non-interactive'] === true) throw new Error('new connector is not available in non-interactive mode')
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question('Are you sure you want to generate a new connector and update the Address Resolver? [Y/n]: ', function (answ) {
answ = answ.toLowerCase()
if (answ.match(/y/)) {
rl.close()
console.log('Please wait...')
async.waterfall([
function (callback) {
activeOracleInstance.deployConnector(callback)
},
function setPricing (result, callback) {
logger.info('successfully deployed a new connector', result.connector)
oraclizeConfiguration.connector = result.connector
if (cliConfiguration['disable-price'] === true || pricingInfo.length === 0) {
logger.warn('skipping pricing update...')
callback(null, null)
} else {
logger.info('updating connector pricing...')
activeOracleInstance.setPricing(result.connector, callback)
}
},
function updateState (result, callback) {
logger.debug('pricing update result', result)
if (cliConfiguration['connector-state']) {
var contractState = JSON.parse(fs.readFileSync(cliConfiguration['connector-state']).toString())
var addressListProof = Object.keys(contractState['addr_proofType'])
var proofList = []
for (var i = 0; i < addressListProof.length; i++) {
proofList.push(contractState['addr_proofType'][addressListProof[i]])
}
if (proofList.length !== addressListProof.length) throw new Error('Address list and proof list doesn\'t match')
var addressListGasPrice = Object.keys(contractState['addr_gasPrice'])
var gasPriceList = []
for (var i = 0; i < addressListGasPrice.length; i++) {
gasPriceList.push(contractState['addr_gasPrice'][addressListGasPrice[i]])
}
if (gasPriceList.length !== addressListGasPrice.length) throw new Error('Address list and gasPrice list doesn\'t match')
var addressListProofChunk = bridgeUtil.createGroupedArray(addressListProof, 100)
var proofListChunk = bridgeUtil.createGroupedArray(proofList, 100)
for (var i = 0; i < addressListProofChunk.length; i++) {
var addresses = addressListProofChunk[i]
addressListProofChunk[i] = []
addressListProofChunk[i][0] = addresses
addressListProofChunk[i][1] = proofListChunk[i]
}
asyncLoop(addressListProofChunk, function (chunk, next) {
activeOracleInstance.updateProofMapping(activeOracleInstance.connector, chunk[0], chunk[1], function (err, result) {
if (err) return next(err)
return next(null)
})
}, function (err) {
if (err) logger.error('loop error', err)
else logger.info('proof mapping updated')
})
var addressListGasPriceChunk = bridgeUtil.createGroupedArray(addressListGasPrice, 100)
var gasPriceListChunk = bridgeUtil.createGroupedArray(gasPriceList, 100)
for (var i = 0; i < addressListGasPriceChunk.length; i++) {
var addresses = addressListGasPriceChunk[i]
addressListGasPriceChunk[i] = []
addressListGasPriceChunk[i][0] = addresses
addressListGasPriceChunk[i][1] = gasPriceListChunk[i]
}
asyncLoop(addressListGasPriceChunk, function (chunk, next) {
activeOracleInstance.updateGasPriceMapping(activeOracleInstance.connector, chunk[0], chunk[1], function (err, result) {
if (err) return next(err)
return next(null)
})
}, function (err) {
if (err) logger.error('loop error', err)
else return callback(null, null)
})
} else return callback(null, null)
},
function updateOar (result, callback) {
logger.debug('script result', result)
logger.info('gasPrice mapping updated')
activeOracleInstance.setAddr(activeOracleInstance.oar, activeOracleInstance.connector, callback)
}], function (err, res) {
if (err) throw new Error(err)
if (res.success === true) {
logger.info('successfully updated the Address Resolver')
bridgeUtil.saveJsonFile(configFilePath, oraclizeConfiguration)
runLog()
}
})
} else throw new Error('exiting no authorization given, please remove the --newconn flag')
})
}
} catch (e) {
logger.error(e.message)
if (e.message.match(/JSON RPC response: undefined/)) nodeError()
else throw new Error(e)
}
}
function processPendingQueries (oar, connector, cbAddress) {
oar = oar || activeOracleInstance.oar
connector = connector || activeOracleInstance.connector
cbAddress = cbAddress || activeOracleInstance.account
logger.info('fetching pending queries from database with oar:', oar, 'and callback address:', cbAddress)
BridgeDbManager().getPendingQueries(oar, cbAddress, function (err, pendingQueries) {
if (err) logger.error('fetching queries error', err)
else {
logger.info('found a total of', pendingQueries.length, 'pending queries')
if (pendingQueries.length === 0) return
if (cliConfiguration.skipQueries === true) {
logger.warn('skipping all pending queries')
return
} else if (cliConfiguration.resumeQueries === true) {
logger.warn('forcing the resume of all pending queries')
}
asyncLoop(pendingQueries, function (thisPendingQuery, next) {
if (thisPendingQuery.callback_error === true && cliConfiguration.skipQueries !== true) {
logger.warn('skipping', thisPendingQuery.contract_myid, 'because of __callback tx error')
return next(null)
} else if (thisPendingQuery.retry_number < 3 || cliConfiguration.resumeQueries) {
var targetUnix = parseInt(thisPendingQuery.target_timestamp)
var queryTimeDiff = targetUnix < moment().unix() ? 0 : targetUnix
logger.info('re-processing query', {'contract_address:': thisPendingQuery.contract_address, 'contact_myid': thisPendingQuery.contract_myid, 'http_myid': thisPendingQuery.http_myid})
if (queryTimeDiff <= 0) {
checkQueryStatus(thisPendingQuery)
} else {
var targetDate = moment(targetUnix, 'X').toDate()
processQueryInFuture(targetDate, thisPendingQuery)
}
} else {
logger.warn('skipping', thisPendingQuery.contract_myid, 'query, exceeded 3 retries')
return next(null)
}
setTimeout(function () {
next(null)
}, 1000)
}, function (err) {
if (err) logger.error('Pending query error', err)
})
}
})
}
function importConfigFile (instanceToLoad) {
configFilePath = toFullPath('./config/instance/' + instanceToLoad)
loadConfigFile(configFilePath)
if (cliConfiguration['no-hints'] === false) logger.info('using ' + instanceToLoad + ' oracle configuration file')
}
function loadConfigFile (file) {
var configFile = bridgeUtil.loadLocalJson(file)
if (typeof configFile.mode !== 'undefined' && typeof configFile.account !== 'undefined' && typeof configFile.oar !== 'undefined' && typeof configFile.node !== 'undefined') {
oraclizeConfiguration = configFile
mode = configFile.mode
cliConfiguration.defaultnode = configFile.node.main
startUpLog(false, configFile)
} else return logger.error(file + ' configuration is not valid')
}
function startUpLog (newInstance, configFile) {
logger.info('using', mode, 'mode')
logger.info('Connecting to ' + BLOCKCHAIN_ABBRV + ' node ' + cliConfiguration.defaultnode)
checkBridgeVersion(function (err, res) {
if (err) { /* skip error */ }
try {
if (newInstance === true) deployOraclize()
else if (newInstance === false && typeof configFile !== 'undefined') {
oracleFromConfig(configFile)
} else throw new Error('failed to deploy/load oracle')
} catch (e) {
logger.error(e)
}
})
}
function userWarning () {
if (cliConfiguration['no-hints'] === false) logger.warn('Using', activeOracleInstance.account, 'to query contracts on your blockchain, make sure it is unlocked and do not use the same address to deploy your contracts')
}
function checkNodeConnection () {
if (!BlockchainInterface().isConnected()) nodeError()
else {
if (cliConfiguration['enable-stats'] === true) BridgeStats(logger)
var nodeType = BlockchainInterface().version.node
isTestRpc = !!nodeType.match(/TestRPC/i)
logger.info('connected to node type', nodeType)
try {
for (var i = officialOar.length - 1; i >= 0; i--) {
var oarCode = bridgeCore.ethUtil.addHexPrefix(BlockchainInterface().inter.getCode(officialOar[i], 'latest'))
if (oarCode === null || oarCode === '0x' || oarCode === '0x0') officialOar.splice(i, 1)
}
} catch (e) {
officialOar = []
}
}
}
function getHostAndPort (nodeHttp) {
var nodeSplit = nodeHttp.substring(nodeHttp.indexOf('://') + 3).split(':')
var portSplit = nodeSplit[1] || '8545'
var hostSplit = nodeSplit[0] || '127.0.0.1'
if (hostSplit === 'localhost') hostSplit = '127.0.0.1'
return [hostSplit, portSplit]
}
function nodeError () {
var nodeinfo = getHostAndPort(cliConfiguration.defaultnode)
var hostSplit = nodeinfo[0]
var portSplit = nodeinfo[1]
var startString = i18n.__('connection_failed_tip')
startString = startString ? startString.replace(/@HOST/g, hostSplit).replace(/@PORT/g, portSplit).replace(/'/g, '"') : ''
return logger.error(cliConfiguration.defaultnode + ' ' + BLOCKCHAIN_NAME + ' node not found, are you sure is it running?\n ' + startString)
}
function checkBridgeVersion (callback) {
bridgeHttp.getPlatformInfo(bridgeObj, function (error, body) {
logger.debug('check bridge version body result', body)
if (error) return callback(error, null)
try {
var checkOutdated = bridgeUtil.checkIfOutdated(bridgeObj, body)
if (checkOutdated === false) return callback(new Error('Bridge name not found'), null)
var distribution = body.result.distributions[BRIDGE_NAME.toLowerCase()]
if (typeof distribution.motd !== 'undefined' && distribution.motd.length > 0) logger.info('\n========================\n', distribution.motd, '\n========================')
if (checkOutdated.outdated === true) {
var latestVersion = checkOutdated.version
logger.warn('\n************************************************************************\nA NEW VERSION OF THIS TOOL HAS BEEN DETECTED\nIT IS HIGHLY RECOMMENDED THAT YOU ALWAYS RUN THE LATEST VERSION, PLEASE UPGRADE TO ' + BRIDGE_NAME.toUpperCase() + ' ' + latestVersion + '\n************************************************************************\n')
}
if (typeof body.result.pricing !== 'undefined' && typeof body.result.quotes !== 'undefined') {
basePrice = body.result.quotes[BLOCKCHAIN_ABBRV.toUpperCase()]
var datasources = body.result.pricing.datasources
var proofPricing = body.result.pricing.proofs
for (var i = 0; i < datasources.length; i++) {
var thisDatasource = datasources[i]
for (var j = 0; j < thisDatasource.proof_types.length; j++) {
var units = thisDatasource.units + proofPricing[thisDatasource.proof_types[j]].units
pricingInfo.push({'name': thisDatasource.name, 'proof': thisDatasource.proof_types[j], 'units': units})
pricingInfo.push({'name': thisDatasource.name, 'proof': thisDatasource.proof_types[j] + 1, 'units': units})
}
}
}
if (typeof body.result.deployments !== 'undefined' && BLOCKCHAIN_NAME in body.result.deployments) {
var deployments = body.result.deployments[BLOCKCHAIN_NAME]
Object.keys(deployments).forEach(function (key) {
officialOar.push(deployments[key]['addressResolver'])
})
}
return callback(null, true)
} catch (e) {
return callback(e, null)
}
})
}
function deployOraclize () {
try {
if (pricingInfo.length > 0) {
oraclizeConfiguration.onchain_config = {}
oraclizeConfiguration.onchain_config.pricing = pricingInfo
oraclizeConfiguration.onchain_config.base_price = basePrice
}
activeOracleInstance = new OracleInstance(oraclizeConfiguration)
checkNodeConnection()
userWarning()
} catch (e) {
if (e.message.match(/JSON RPC response: undefined/)) return nodeError()
else throw new Error(e)
}
async.waterfall([
function (callback) {
var accountBalance = activeOracleInstance.checkAccountBalance()
var amountToPay = 500000000000000000 - accountBalance
if (amountToPay > 0) {
logger.warn(activeOracleInstance.account, 'doesn\'t have enough funds to cover transaction costs, please send at least ' + parseFloat(amountToPay / 1e19) + ' ' + BLOCKCHAIN_BASE_UNIT)
if (isTestRpc && cliConfiguration['non-interactive'] === false) {
// node is TestRPC
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
})
rl.question('Authorize the bridge to move funds automatically from your node? [Y/n]: ', function (answ) {
answ = answ.toLowerCase()
if (answ.match(/y/)) {
var userAccount = ''
rl.question('Please choose the unlocked account index number in your node: ', function (answ) {
if (answ >= 0) {
userAccount = BlockchainInterface().inter.accounts[answ]
if (typeof (userAccount) === 'undefined') {
rl.close()
throw new Error('Account at index number: ' + answ + ' not found')
}
rl.question('send ' + parseFloat(amountToPay / 1e19) + ' ' + BLOCKCHAIN_BASE_UNIT + ' from account ' + userAccount + ' (index n.: ' + answ + ') to ' + activeOracleInstance.account + ' ? [Y/n]: ', function (answ) {
answ = answ.toLowerCase()
if (answ.match(/y/)) {
BlockchainInterface().inter.sendTransaction({'from': userAccount, 'to': activeOracleInstance.account, 'value': amountToPay})
rl.close()
} else {
console.log('No authorization given, waiting for funds...')
rl.close()
}
})
} else {
console.log('Negative account index not allowed')
rl.close()
}
})
} else {
console.log('No authorization given, waiting for funds...')
rl.close()
}
})
}
var checkBalance = setInterval(function () {
var balance = activeOracleInstance.checkAccountBalance()
var amountToPay = 500000000000000000 - balance
if (amountToPay <= 0) {
logger.info('received funds')
logger.debug('account balance', balance)
clearInterval(checkBalance)
if (typeof rl !== 'undefined') rl.close()
callback(null, true)
}
}, 10000)
} else callback(null, true)
},
function deployConnector (result, callback) {
logger.info('deploying the oraclize connector contract...')
activeOracleInstance.deployConnector(callback)
},
function deployOAR (result, callback) {
logger.info('connector deployed to:', result.connector)
logger.debug('connector deployment result', result)
if (cliConfiguration['disable-deterministic-oar'] === false && BlockchainInterface().getAccountNonce(BridgeAccount().getTempAddress()) === 0) logger.info('deploying the address resolver with a deterministic address...')
else {
if (cliConfiguration['no-hints'] === false) logger.warn('deterministic OAR disabled/not available, please update your contract with the new custom address generated')
logger.info('deploying the address resolver contract...')
}
activeOracleInstance.deployOAR(callback)
},
function setPricing (result, callback) {
oraclizeConfiguration.oar = result.oar
logger.info('address resolver (OAR) deployed to:', oraclizeConfiguration.oar)
logger.debug('OAR deployment result', result)
if (cliConfiguration['disable-price'] === true || pricingInfo.length === 0) {
logger.warn('skipping pricing update...')
callback(null, null)
} else {
logger.info('updating connector pricing...')
activeOracleInstance.setPricing(activeOracleInstance.connector, callback)
}
}
], function (err, result) {
if (err) throw new Error(err)
logger.info('successfully deployed all contracts')
logger.debug('pricing update result', result)
oraclizeConfiguration.connector = activeOracleInstance.connector
oraclizeConfiguration.account = activeOracleInstance.account
logger.debug('new oracle inline configuration', oraclizeConfiguration)
var oraclizeInstanceNewName = 'oracle_instance_' + moment().format('YYYYMMDDTHHmmss') + '.json'
configFilePath = toFullPath('./config/instance/' + oraclizeInstanceNewName)
currentInstance = oraclizeInstanceNewName
try {
bridgeUtil.saveJsonFile(configFilePath, oraclizeConfiguration)
if (cliConfiguration['no-hints'] === false) logger.info('instance configuration file saved to ' + configFilePath)
} catch (err) {
if (cliConfiguration['no-hints'] === false) logger.error('instance configuration file ' + configFilePath + ' not saved', err)
}
runLog()
})
}
function checkVersion () {
var prVersion = process.version
if (semver.lt(semver.clean(prVersion), '5.0.0')) {
console.error('Not compatible with ' + prVersion + ' of nodejs, please use at least v5.0.0')
console.log('exiting...')
process.exit(1)
}
// only check that Node isn't below v5. Appears to work fine with v8+
/*} else if (prVersion.substr(1, 1) > 7) {
console.error('Not compatible with ' + prVersion + ' of nodejs, please use v6.9.1 or a lower version')
console.log('exiting...')
process.exit(1)
}*/
}
function runLog () {
if (officialOar.length === 1 && cliConfiguration['no-hints'] === false) logger.info('an "official" Oraclize address resolver was found on your blockchain:', officialOar[0], 'you can use that instead and quit the bridge')
var checksumOar = bridgeCore.ethUtil.toChecksumAddress(activeOracleInstance.oar)
if (checksumOar === '0x6f485C8BF6fc43eA212E93BBF8ce046C7f1cb475' && !isTestRpc) logger.info('you are using a deterministic OAR, you don\'t need to update your contract')
else console.log('\nPlease add this line to your contract constructor:\n\n' + 'OAR = OraclizeAddrResolverI(' + checksumOar + ');\n')
logger.debug('starting the bridge log manager...')
BridgeLogManager = BridgeLogManager.init()
var latestBlockMemory = activeOracleInstance.latestBlockNumber
logger.debug('latest block seen (config file)', latestBlockMemory)
// listen for latest events
listenToLogs()
if (isTestRpc && !cliConfiguration.dev && cliConfiguration['no-hints'] === false) {
logger.warn('re-org block listen is disabled while using TestRPC')
logger.warn('if you are running a test suit with Truffle and TestRPC or your chain is reset often please use the --dev mode')
}
if (cliConfiguration.dev || cliConfiguration['disable-reorg'] === true) logger.warn('re-org block listen is disabled')
else reorgListen()
logger.info('Listening @ ' + activeOracleInstance.connector + ' (Oraclize Connector)\n')
if (cliConfiguration['price-usd']) {
var priceInUsd = bridgeUtil.normalizePrice(1 / cliConfiguration['price-usd'])
activeOracleInstance.setBasePrice(activeOracleInstance.connector, priceInUsd, function (err, res) {
if (err) return logger.error('update price error', err)
else logger.info('base price updated to', priceInUsd, BLOCKCHAIN_BASE_UNIT)
})
}
if ((cliConfiguration['price-update-interval'] && !cliConfiguration['price-usd']) || cliConfiguration['random-ds-update-interval']) fetchPlatform()
keepNodeAlive()
console.log('(Ctrl+C to exit)\n')
if (!isTestRpc && !cliConfiguration.dev && latestBlockMemory !== -1) {
latestBlockMemory += 1
var latestBlockTemp = BlockchainInterface().inter.blockNumber
if (latestBlockTemp > latestBlockMemory) {
logger.info('latest block seen:', latestBlockMemory, '- processing', (latestBlockTemp - latestBlockMemory), 'new blocks')
BridgeLogManager.fetchLogsByBlock(latestBlockMemory, latestBlockTemp)
}
}
activeOracleInstance.latestBlockNumber = BlockchainInterface().inter.blockNumber
if (cliConfiguration['oar'] || cliConfiguration['instance']) processPendingQueries()
if (typeof cliConfiguration.blockRangeResume !== 'undefined' && cliConfiguration.blockRangeResume.length === 2) {
setTimeout(function () {
logger.info('resuming logs from block range:', JSON.stringify(cliConfiguration.blockRangeResume))
BridgeLogManager.fetchLogsByBlock(parseInt(cliConfiguration.blockRangeResume[0]), parseInt(cliConfiguration.blockRangeResume[1]))
}, 5000)
}
if (!isTestRpc && !cliConfiguration.dev) checkCallbackTxs()
AddressWatcher({'address': activeOracleInstance.account, 'logger': logger, 'balance_limit': 10000000000000000})
AddressWatcher().init()
}
function fetchPlatform () {
var usdInterval = parseInt(cliConfiguration['price-update-interval'])
var randomDsInterval = parseInt(cliConfiguration['random-ds-update-interval'])
var intervalArr = []
if (!isNaN(usdInterval)) { intervalArr.push(usdInterval) }
if (!isNaN(randomDsInterval)) { intervalArr.push(randomDsInterval) }
var seconds = Math.min(...intervalArr)
if (isNaN(seconds) || seconds <= 1) return
setInterval(function () {
bridgeHttp.getPlatformInfo(bridgeObj, function (error, body) {
if (error) return
logger.debug('fetch platform result', body)
BridgeCache.set('platform_info', body.result, 0)
})
}, seconds * 1000)
setTimeout(function () {
if (cliConfiguration['random-ds-update-interval']) randDsHashUpdater(cliConfiguration['random-ds-update-interval'])
if (cliConfiguration['price-update-interval']) priceUpdater(cliConfiguration['price-update-interval'])
}, 3100)
}
function randDsHashUpdater (seconds) {
setInterval(function () {
if (BlockchainInterface().isConnected() && bridgeUtil.isSyncing(BlockchainInterface().inter.syncing) === false) {
try {
var body = BridgeCache.get('platform_info')
if (typeof body.datasources === 'undefined') return
var hashList = body.datasources.random.sessionPubKeysHash
var hashListInCache = BridgeCache.get('sessionPubKeysHash')
logger.debug('hash list cache', hashListInCache, 'hash list from API', hashList)
if (bridgeUtil.compareObject(hashListInCache, hashList)) return
BridgeCache.set('sessionPubKeysHash', hashList, 0)
activeOracleInstance.updateRandDsHash(activeOracleInstance.connector, hashList, function (err, res) {
if (err) return logger.error('update random ds hash error', err)
else logger.info('random datasource hash list updated to:', hashList)
})
} catch (e) {
logger.error('random ds hash failed to update', e)
}
}
}, seconds * 1000)
}
function priceUpdater (seconds) {
setInterval(function () {
if (BlockchainInterface().isConnected() && bridgeUtil.isSyncing(BlockchainInterface().inter.syncing) === false) {
try {
var body = BridgeCache.get('platform_info')
if (typeof body.quotes === 'undefined') return
var priceInUsd = body.quotes.ETH
if (BridgeCache.get('baseprice') === priceInUsd) return
BridgeCache.set('baseprice', priceInUsd, 0)
activeOracleInstance.setBasePrice(activeOracleInstance.connector, priceInUsd, function (err, res) {
if (err) return logger.error('update price error', err)
else logger.info('base price updated to', priceInUsd, BLOCKCHAIN_BASE_UNIT)
})
} catch (e) {
logger.error('baseprice failed to update', e)
}
}
}, seconds * 1000)
}
function processQueryInFuture (date, query) {
logger.info('checking HTTP query ' + query.http_myid + ' status on ' + date)
schedule.scheduleJob(date, function () {
checkQueryStatus(query)
})
}
function reorgListen () {
try {
if (isTestRpc) return
var startingBlock = BlockchainInterface().inter.blockNumber
var initialBlock = startingBlock - (cliConfiguration.confirmations * 2)
var prevBlock = -1
var latestBlock = -1
logger.debug('running reorgListen')
setInterval(function () {
try {
if (initialBlock < 0) initialBlock = 0
logger.debug('reorg block target: ' + initialBlock)
latestBlock = BlockchainInterface().inter.blockNumber
if (prevBlock === -1) prevBlock = latestBlock
if (latestBlock > prevBlock && prevBlock !== latestBlock && (latestBlock - initialBlock) > cliConfiguration.confirmations) {
if (!BlockchainInterface().isConnected()) return
prevBlock = latestBlock
logger.debug('reorg, fetching logs from-to: ' + initialBlock)
BridgeLogManager.fetchLogsByBlock(initialBlock, initialBlock)
initialBlock += 1
}
} catch (e) {
logger.error('reorgListen error', e)
manageErrors(e)
}
}, 30000)
} catch (e) {
logger.error('reorgListen error', e)
manageErrors(e)
}
}
function listenToLogs () {
// listen to events
BridgeLogEvents.on('new-log', function (log) {
if (typeof log !== 'undefined') logsQueue.push(log)
})
BridgeLogEvents.on('log-err', function (err) {
if (typeof err !== 'undefined') manageErrors(err)
})
BridgeLogManager.watchEvents()
}
function keepNodeAlive () {
setInterval(function () {}, (1000 * 60) * 60)
}
function logsWrapper (data, callback) {
manageLog(data)
setTimeout(function () {
callback()
}, 250)
}
function manageLog (data) {
try {
logger.debug('manageLog, raw log:', data)
if (typeof data === 'undefined') return logger.error('log error ' + data)
var contractMyid = data.args['cid'] || data['parsed_logs']['contract_myid']
BridgeDbManager().isAlreadyProcessed(contractMyid, function (err, isProcessed) {
if (err) isProcessed = false
logger.debug('mangeLog isProcessed: ' + isProcessed)
if (cliConfiguration.dev === true) return handleLog(data)
if (isProcessed === false) {
if (activeOracleInstance.isOracleEvent(data)) {
logger.debug('cache content', BridgeCache.get(contractMyid))
if (cliConfiguration.dev !== true && BridgeCache.get(contractMyid) === true) return
BridgeCache.set(contractMyid, true)
logger.debug('cache content after', BridgeCache.get(contractMyid))
if (typeof data.removed !== 'undefined' && data.removed === true) return logger.error('this log was removed because of orphaned block, rejected tx or re-org, skipping...')
if ((typeof data.malformed !== 'undefined' && data.malformed === true) || typeof data.parsed_log === 'undefined') return logger.error('malformed log, skipping...')
handleLog(data)
} else {
logger.warn('log with contract myid:', contractMyid, 'was triggered, but is not recognized as an oracle event, skipping event...')
}
} else logger.warn('log with contract myid:', contractMyid, 'was triggered, but it was already seen before, skipping event...')
})
} catch (e) {
logger.error('manageLog error', e)
}
}
function handleLog (log) {
try {
logger.info('new ' + log.event + ' event', log)
var contractMyid = log['parsed_log']['contract_myid']
var blockNumber = BlockchainInterface().inter.blockNumber
if (blockNumber > activeOracleInstance.latestBlockNumber) activeOracleInstance.latestBlockNumber = blockNumber
var eventTx = log['transactionHash']
var blockHashTx = log['blockHash']
var contractAddress = log['parsed_log']['contract_address']
var datasource = log['parsed_log']['datasource']
var formula = log['parsed_log']['formula']
var time = log['parsed_log']['timestamp']
var gasLimit = log['parsed_log']['gaslimit']
var proofType = log['parsed_log']['proofType']
var gasPrice = log['parsed_log']['gasPrice'] || null
var unixTime = moment().unix()
if (!bridgeUtil.isValidTime(time, unixTime)) return logger.error('the query is too far in the future, skipping log...')
var query = {
'when': time,
'datasource': datasource,
'query': formula,
'id2': bridgeCore.ethUtil.stripHexPrefix(contractMyid),
'proof_type': bridgeUtil.toInt(proofType),
'context': {
'name': oraclizeConfiguration.context_name,
'protocol': BLOCKCHAIN_ABBRV.toLowerCase(),
'type': 'blockchain',
'relative_timestamp': log['block_timestamp']
}
}
createQuery(query, function (data) {
if (typeof data !== 'object' || typeof data.result === 'undefined' || typeof data.result.id === 'undefined') return logger.error('no HTTP myid found, skipping log...')
var httpMyid = data.result.id
logger.info('new HTTP query created, id: ' + httpMyid)
var queryCheckUnixTime = bridgeUtil.getQueryUnixTime(time, unixTime)
var newQueryObj = {
'target_timestamp': queryCheckUnixTime,
'oar': activeOracleInstance.oar,
'connector': activeOracleInstance.connector,
'cbAddress': activeOracleInstance.account,
'http_myid': httpMyid,
'contract_myid': contractMyid,
'query_delay': time,
'query_arg': JSON.stringify(formula),
'query_datasource': datasource,
'contract_address': contractAddress,
'event_tx': eventTx,
'block_tx_hash': blockHashTx,
'proof_type': proofType,
'gas_limit': gasLimit,
'gas_price': gasPrice
}
BridgeDbManager().createDbQuery(newQueryObj, function (err, res) {
if (err !== null) logger.error('query db create error', err)
if (queryCheckUnixTime <= 0) {
logger.info('checking HTTP query ' + httpMyid + ' status in 0 seconds')
var queryObj = {
'http_myid': httpMyid,
'contract_myid': contractMyid,
'contract_address': contractAddress,
'proof_type': proofType,
'gas_limit': gasLimit,
'gas_price': gasPrice
}
checkQueryStatus(queryObj)
} else {
var targetDate = moment(queryCheckUnixTime, 'X').toDate()
processQueryInFuture(targetDate, {
'active': true,
'callback_complete': false,
'retry_number': 0,
'target_timestamp': queryCheckUnixTime,
'oar': activeOracleInstance.oar,
'connector': activeOracleInstance.connector,
'cbAddress': activeOracleInstance.account,
'http_myid': httpMyid,
'contract_myid': contractMyid,
'query_delay': time,
'query_arg': JSON.stringify(formula),
'query_datasource': datasource,
'contract_address': contractAddress,
'event_tx': eventTx,
'block_tx_hash': blockHashTx,
'proof_type': proofType,
'gas_limit': gasLimit,
'gas_price': gasPrice
})
}
})
})
} catch (e) {
logger.error('handle log error ', e)
}
}
function checkQueryStatus (queryObj) {
var myid = queryObj.http_myid
var myIdInitial = queryObj.contract_myid
var contractAddress = queryObj.contract_address
var proofType = queryObj.proof_type
var gasLimit = queryObj.gas_limit
var forceQuery = queryObj.force_query || false
if (typeof myid === 'undefined') return logger.error('checkQueryStatus error, http myid provided is invalid')
logger.info('checking HTTP query', myid, 'status every 5 seconds...')
var interval = setInterval(function () {
queryStatus(myid, function (data) {
logger.info(myid, 'HTTP query result: ', data)
if (typeof data !== 'object' || typeof data.result === 'undefined') {
clearInterval(interval)
return logger.error('HTTP query status error')
}
if (typeof data.result.active === 'undefined' || typeof data.result.bridge_request_error !== 'undefined') return
if (data.result.active === true) return
var dataProof = null
var queryComplObj = {
'contract_myid': myIdInitial,
'proof_type': proofType,
'contract_address': contractAddress,
'gas_limit': gasLimit,
'gas_price': queryObj.gas_price,
'force_query': forceQuery
}
if (bridgeUtil.checkErrors(data) === true) {
logger.error('HTTP query error', bridgeUtil.getQueryError(data))
clearInterval(interval)
var dataResult = null
var proofResult = null
if ('checks' in data.result) {
var lastQueryCheck = data.result.checks[data.result.checks.length - 1]
var queryResultWithError = lastQueryCheck.results[lastQueryCheck.results.length - 1]
var queryProofWithError = data.result.checks[data.result.checks.length - 1]['proofs'][0]
if (queryResultWithError !== null) dataResult = queryResultWithError
if (queryProofWithError !== null) proofResult = bridgeUtil.getProof(queryProofWithError, proofType)
}
queryComplObj.result = dataResult
queryComplObj.proof_content = proofResult
queryComplete(queryComplObj)
return
}
if (!('checks' in data.result)) return
else clearInterval(interval)
var lastCheck = data.result.checks[data.result.checks.length - 1]
var queryResult = lastCheck.results[lastCheck.results.length - 1]
var dataRes = queryResult
if (bridgeUtil.containsProof(proofType)) {
dataProof = bridgeUtil.getProof(data.result.checks[data.result.checks.length - 1]['proofs'][0], proofType)
}
queryComplObj.result = dataRes
queryComplObj.proof_content = dataProof
queryComplete(queryComplObj)
})
}, 5000)
}
function queryComplete (queryComplObj) {
try {
var result = queryComplObj.result
var proof = queryComplObj.proof_content
var contractAddr = queryComplObj.contract_address
var proofType = queryComplObj.proof_type
var myid = queryComplObj.contract_myid
var gasLimit = queryComplObj.gas_limit
var gasPrice = queryComplObj.gas_price
if (typeof gasLimit === 'undefined' || typeof myid === 'undefined' || typeof contractAddr === 'undefined' || typeof proofType === 'undefined') {
return queryCompleteErrors('queryComplete error, __callback arguments are empty')
}
checkCallbackTx(myid, function (findErr, alreadyCalled) {
if (findErr !== null) return queryCompleteErrors(findErr)
if (alreadyCalled === true) return logger.error('queryComplete error, __callback for contract myid', myid, 'was already called before, skipping...')
var callbackObj = {
'myid': myid,
'result': result,
'proof': proof,
'proof_type': proofType,
'contract_address': bridgeCore.ethUtil.addHexPrefix(contractAddr),
'gas_limit': gasLimit,
'gas_price': gasPrice
}
if (BridgeCache.get(callbackObj.myid + '__callback') === true) return
var ttlTx = cliConfiguration.dev === true ? 1 : 100
BridgeCache.set(callbackObj.myid + '__callback', true, ttlTx)
logger.info('sending __callback tx...', {'contract_myid': callbackObj.myid, 'contract_address': callbackObj.contract_address})
// Without concurrency, this sometimes never seems to reach the queue
callbackQueue.push(callbackObj, function (err) {
if (err) logger.error('Callback queue error', err)
})
})
} catch (e) {
logger.error('queryComplete error ', e)
}
}
function __callbackWrapper (callbackObj, cb) {
logger.debug('__callbackWrapper object:', callbackObj)
activeOracleInstance.__callback(callbackObj, function (err, contract) {
if (err) {
updateQuery(callbackObj, null, err, cb)
return logger.error('callback tx error, contract myid: ' + callbackObj.myid, err)
}
logger.info('contract ' + callbackObj.contract_address + ' __callback tx sent, transaction hash:', contract.transactionHash, callbackObj)
updateQuery(callbackObj, contract, null, cb)
})
}
function checkCallbackTx (myid, callback) {
if (cliConfiguration.dev === true) return callback(null, false)
BridgeDbManager().checkCallbackQueryStatus(myid, function (err, findObject) {
if (err) return callback(err, null)
logger.debug('checkCallbackTx findObject content:', findObject)
var queryObj = findObject.query
var callbackObj = findObject.callback
if (typeof queryObj.callback_complete === 'undefined') return callback(new Error('queryComplete error, query with contract myid ' + myid), null)
if (callbackObj !== null) {
if (callbackObj.tx_hash !== null) var txContent = BlockchainInterface().inter.getTransactionReceipt(callbackObj.tx_hash)
if (typeof callbackObj.tx_confirmed !== 'undefined') {
if (callbackObj.tx_confirmed === false && txContent !== null) return callback(null, true)
else return callback(null, false)
}
} else if (queryObj.callback_complete === true) return callback(null, true)
else return callback(null, false)
/* else {
var eventTx = BlockchainInterface().inter.getTransaction(queryObj.event_tx)
if (eventTx === null || eventTx.blockHash === null || eventTx.blockHash !== queryObj.block_tx_hash) return callback(new Error('queryComplete error, query with contract myid ' + myid + ' mismatch with block hash stored'), null)
return callback(null, false)
} */
})
}
function manageErrors (err) {
if (typeof err !== 'undefined' && typeof err.message !== 'undefined' && err.message.match(/Invalid JSON RPC response/)) {
// retry after 1 minute
logger.warn('JSON RPC error, trying to re-connect every 30 seconds')
var nodeStatusCheck = setInterval(function () {
try {
var nodeStatus = BlockchainInterface().inter.blockNumber
if (nodeStatus > 0) {
clearInterval(nodeStatusCheck)
logger.info('json-rpc is now available')
// fetch 'lost' queries
if (activeOracleInstance.latestBlockNumber < nodeStatus) {
logger.info('trying to recover "lost" queries...')
BridgeLogManager.fetchLogsByBlock(activeOracleInstance.latestBlockNumber, nodeStatus)
}
}
} catch (e) {
if (e.message.match(/Invalid JSON RPC response/)) {
logger.error('json rpc is not available')
}
}
}, 30000)
} else logger.error(err)
}
function queryCompleteErrors (err) {
logger.error(err)
if (err) {
manageErrors(err)
}
}
function updateQuery (callbackInfo, contract, errors, callback) {
logger.debug('update query content:', callbackInfo, contract)
var dataDbUpdate = {}
if (errors !== null) {
dataDbUpdate = {'query_active': false, 'callback_complete': false, 'retry_number': 3}
if (!errors.message.match(/Invalid JSON RPC response/)) dataDbUpdate.callback_error = true
} else {
dataDbUpdate = {'query_active': false, 'callback_complete': true}
}
if (cliConfiguration.dev === true) return callback()
var dbUpdateObj = {
'query': dataDbUpdate,
'callback': {}
}
dbUpdateObj['callback'] = {
'timestamp_db': moment().format('x'),
'oar': activeOracleInstance.oar,
'cbAddress': activeOracleInstance.account,
'connector': activeOracleInstance.connector,
'contract_myid': callbackInfo.myid,
'result': callbackInfo.res