UNPKG

iota.lib.js

Version:
368 lines (291 loc) 9.28 kB
var async = require("async"); var errors = require("../errors/requestErrors"); function xmlHttpRequest() { if (typeof XMLHttpRequest !== 'undefined') { return new XMLHttpRequest(); } pretendingNotToRequire = require; var module = 'xmlhttprequest'; var request = pretendingNotToRequire(module).XMLHttpRequest; return new request(); } function makeRequest(provider, tokenOrUsername, password) { this.provider = provider || "http://localhost:14265"; this.tokenOrUsername = tokenOrUsername; this.password = password || false; this.timeout = -1; } /** * Set the request timeout (-1 for no timeout) * * @method setTimeout * @param {integer} timeout **/ makeRequest.prototype.setApiTimeout = function(timeout) { this.timeout = timeout; } /** * Change the HTTP provider * * @method setProvider * @param {String} provider **/ makeRequest.prototype.setProvider = function(provider) { this.provider = provider || "http://localhost:14265"; } /** * creates an XMLHttpRequest Object * * @method open * * @returns {object} request **/ makeRequest.prototype.open = function() { var request = xmlHttpRequest(); request.open('POST', this.provider, true); request.setRequestHeader('Content-Type','application/json'); request.setRequestHeader('X-IOTA-API-Version', '1'); if (this.password) { request.setRequestHeader('Authorization', 'Basic ' + Buffer.from(this.tokenOrUsername + ':' + this.password).toString('base64')); } else if (this.tokenOrUsername) { //request.withCredentials = true; request.setRequestHeader('Authorization', 'token ' + this.tokenOrUsername); } return request; } /** * sends an http request to a specified host * * @method send * @param {object} command * @param {function} callback **/ makeRequest.prototype.send = function(command, callback) { var self = this; var request = this.open(); var timeout = this.timeout; var settled = false; var aborted = false; var timer; var timeoutError = JSON.stringify({ error: 'Request timed out.' }); if (timeout > 0) { if (request.timeout === undefined) { // timeout is not implemented in nodejs xhr timer = setTimeout(function() { if (request.readyState !== 4) { aborted = true; request.abort(); } }, timeout); } else { // in browsers, xmr has timeout property request.timeout = timeout; } } request.onreadystatechange = function() { if (request.readyState === 4) { if (settled) { return; } if (!aborted) { if (timer) { clearTimeout(timer); } return self.prepareResult(request.responseText.length ? request.responseText : timeoutError, command.command, callback); } settled = true; self.prepareResult(timeoutError, command.command, callback); } } try { request.send(JSON.stringify(command)); } catch(error) { return callback(errors.invalidResponse(error)); } } /** * sends a batched http request to a specified host * supports findTransactions, getBalances, getInclusionStates & getTrytes commands * * @method batchedSend * @param {object} command * @param {function} callback **/ makeRequest.prototype.batchedSend = function (command, keys, batchSize, callback) { var self = this var requestStack = [] keys.forEach(function (key) { // Create a copy to avoid mutating original params var clone = command[key].slice() while (clone.length) { var batch = clone.splice(0, batchSize) var params = {} Object.keys(command).forEach(function (k) { if (k === key || keys.indexOf(k) === -1) { params[k] = k === key ? batch : command[k] } }) requestStack.push(params) } }) async.mapSeries(requestStack, function (command, cb) { self.send(command, function (err, res) { if (err) { return cb(err) } cb(null, res) }) }, function (err, res) { if (err) { return callback(err) } switch (command.command) { case 'getBalances': var balances = res.reduce(function (a, b) { return a.concat(b.balances) }, []) res = res.sort(function (a, b) { return a.milestoneIndex - b.milestoneIndex }).shift() res.balances = balances callback(null, res) break case 'findTransactions': var seenTxs = new Set() if (keys.length === 1) { return callback(null, res.reduce(function (a, b) { return a.concat(b) }, []).filter(function (hash) { var seen = seenTxs.has(hash) if (!seen) { seenTxs.add(hash) return true } return false })) } var keysToTxFields = { 'bundles': 'bundle', 'addresses': 'address', 'hashes': 'hash', 'tags': 'tag' } callback(null, res.map(function (batch) { return batch.filter(function (tx) { return keys.every(function (key) { return requestStack.some(function (command) { return command.hasOwnProperty(key) && command[key].findIndex(function (value) { return value === tx[keysToTxFields[key]] }) !== -1 }) }) }) }).reduce(function (a, b) { return a.concat(b) }, []).filter(function (tx) { if (!seenTxs.has(tx.hash)) { seenTxs.add(tx.hash) return true } return false })) break default: callback(null, res.reduce(function (a, b) { return a.concat(b) }, [])) } }) } /** * sends an http request to a specified host * * @method sandboxSend * @param {object} command * @param {function} callback **/ makeRequest.prototype.sandboxSend = function(job, callback) { // Check every 15 seconds if the job finished or not // If failed, return error var newInterval = setInterval(function() { var request = xmlHttpRequest(); request.onreadystatechange = function() { if (request.readyState === 4) { var result; // Prepare the result, check that it's JSON try { result = JSON.parse(request.responseText); } catch(e) { return callback(errors.invalidResponse(e)); } if (result.status === "FINISHED") { var attachedTrytes = result.attachToTangleResponse.trytes; clearInterval(newInterval); return callback(null, attachedTrytes); } else if (result.status === "FAILED") { clearInterval(newInterval); return callback(new Error("Sandbox transaction processing failed. Please retry.")) } } } try { request.open('GET', job, true); request.send(JSON.stringify()); } catch(error) { return callback(new Error("No connection to Sandbox, failed with job: ", job)); } }, 5000) } /** * prepares the returned values from the request * * @method prepareResult * @param {string} result * @param {function} callback **/ makeRequest.prototype.prepareResult = function(result, requestCommand, callback) { // Result map of the commands we want to format var resultMap = { 'getNeighbors' : 'neighbors', 'addNeighbors' : 'addedNeighbors', 'removeNeighbors' : 'removedNeighbors', 'getTips' : 'hashes', 'findTransactions' : 'hashes', 'getTrytes' : 'trytes', 'getInclusionStates' : 'states', 'attachToTangle' : 'trytes', 'wereAddressesSpentFrom': 'states' } var error; try { result = JSON.parse(result); } catch(e) { error = errors.invalidResponse(result); result = null; } // // TODO: PREPARE ERROR MESSAGES BETTER // if (!error && result.error) { error = errors.requestError(result.error); result = null; } if (!error && result.exception) { error = errors.requestError(result.exception); result = null; } // If correct result and we want to prepare the result if (result && resultMap.hasOwnProperty(requestCommand)) { // If the response is from the sandbox, don't prepare the result if (requestCommand === 'attachToTangle' && result.hasOwnProperty('id')) { result = result; } else { result = result[resultMap[requestCommand]]; } } return callback(error, result); } module.exports = makeRequest;