UNPKG

iota.lib.js

Version:
1,671 lines (1,250 loc) 61.6 kB
var apiCommands = require('./apiCommands') var errors = require('../errors/inputErrors'); var inputValidator = require('../utils/inputValidator'); var HMAC = require("../crypto/hmac/hmac"); var Converter = require("../crypto/converter/converter"); var Signing = require("../crypto/signing/signing"); var Bundle = require("../crypto/bundle/bundle"); var Utils = require("../utils/utils"); var async = require("async"); 'use strict'; var nullHashTrytes = (new Array(244).join('9')); /** * Making API requests, including generalized wrapper functions **/ function api(provider, isSandbox) { this._makeRequest = provider; this.sandbox = isSandbox; } /** * Set the request timeout (-1 for no timeout) * * @method setTimeout * @param {int} timeout **/ api.prototype.setApiTimeout = function(timeout) { this._makeRequest.setApiTimeout(timeout); } /** * General function that makes an HTTP request to the local node * * @method sendCommand * @param {object} command * @param {function} callback * @returns {object} success **/ api.prototype.sendCommand = function(command, callback) { var commandsToBatch = ['findTransactions', 'getBalances', 'getInclusionStates', 'getTrytes'] var commandKeys = ['addresses', 'bundles', 'hashes', 'tags', 'transactions', 'approvees'] var batchSize = 1000 if (commandsToBatch.indexOf(command.command) > -1) { var keysToBatch = Object.keys(command) .filter(function (key) { return commandKeys.indexOf(key) > -1 && command[key].length > batchSize }) if (keysToBatch.length) { return this._makeRequest.batchedSend(command, keysToBatch, batchSize, callback) } } return this._makeRequest.send(command, callback); } /** * @method attachToTangle * @param {string} trunkTransaction * @param {string} branchTransaction * @param {integer} minWeightMagnitude * @param {array} trytes * @returns {function} callback * @returns {object} success **/ api.prototype.attachToTangle = function(trunkTransaction, branchTransaction, minWeightMagnitude, trytes, callback) { // inputValidator: Check if correct hash if (!inputValidator.isHash(trunkTransaction)) { return callback(errors.invalidTrunkOrBranch(trunkTransaction)); } // inputValidator: Check if correct hash if (!inputValidator.isHash(branchTransaction)) { return callback(errors.invalidTrunkOrBranch(branchTransaction)); } // inputValidator: Check if int if (!inputValidator.isValue(minWeightMagnitude)) { return callback(errors.notInt()); } // inputValidator: Check if array of trytes if (!inputValidator.isArrayOfTrytes(trytes)) { return callback(errors.invalidTrytes()); } var command = apiCommands.attachToTangle(trunkTransaction, branchTransaction, minWeightMagnitude, trytes) return this.sendCommand(command, callback) } /** * @method findTransactions * @param {object} searchValues * @returns {function} callback * @returns {object} success **/ api.prototype.findTransactions = function(searchValues, callback) { // If not an object, return error if (!inputValidator.isObject(searchValues)) { return callback(errors.invalidKey()); } // Get search key from input object var searchKeys = Object.keys(searchValues); var availableKeys = ['bundles', 'addresses', 'tags', 'approvees']; var keyError = false; searchKeys.forEach(function(key) { if (availableKeys.indexOf(key) === -1) { keyError = errors.invalidKey(); return } if (key === 'addresses') { searchValues.addresses = searchValues.addresses.map(function(address) { return Utils.noChecksum(address) }); } var hashes = searchValues[key]; // If tags, append to 27 trytes if (key === 'tags') { searchValues.tags = hashes.map(function(hash) { // Simple padding to 27 trytes while (hash.length < 27) { hash += '9'; } // validate hash if (!inputValidator.isTrytes(hash, 27)) { keyError = errors.invalidTrytes(); return } return hash; }) } else { // Check if correct array of hashes if (!inputValidator.isArrayOfHashes(hashes)) { keyError = errors.invalidTrytes(); return } } }) // If invalid key found, return if (keyError) { callback(keyError); return } var command = apiCommands.findTransactions(searchValues); return this.sendCommand(command, callback) } /** * @method getBalances * @param {array} addresses * @param {int} threshold * @param {array} [tips] * @returns {function} callback * @returns {object} success **/ api.prototype.getBalances = function(addresses, threshold, tips, callback) { var missingTips = arguments.length === 3 && Object.prototype.toString.call(tips) === "[object Function]"; var actualCallback; var actualTips; // Check if tips are provided if (missingTips) { actualCallback = tips; actualTips = []; } else { actualCallback = callback; actualTips = tips; } // Check if correct transaction hashes and tips if (!inputValidator.isArrayOfHashes(addresses) || !inputValidator.isArrayOfHashes(actualTips)) { return actualCallback(errors.invalidTrytes()); } var command = apiCommands.getBalances(addresses.map(function(address) { return Utils.noChecksum(address) }), threshold, actualTips); return this.sendCommand(command, actualCallback) } /** * @method getInclusionStates * @param {array} transactions * @param {array} tips * @returns {function} callback * @returns {object} success **/ api.prototype.getInclusionStates = function(transactions, tips, callback) { // Check if correct transaction hashes if (!inputValidator.isArrayOfHashes(transactions)) { return callback(errors.invalidTrytes()); } // Check if correct tips if (!inputValidator.isArrayOfHashes(tips)) { return callback(errors.invalidTrytes()); } var command = apiCommands.getInclusionStates(transactions, tips); return this.sendCommand(command, callback) } /** * @method getNodeInfo * @returns {function} callback * @returns {object} success **/ api.prototype.getNodeInfo = function(callback) { var command = apiCommands.getNodeInfo(); return this.sendCommand(command, callback) } /** * @method getNeighbors * @returns {function} callback * @returns {object} success **/ api.prototype.getNeighbors = function(callback) { var command = apiCommands.getNeighbors(); return this.sendCommand(command, callback) } /** * @method addNeighbors * @param {Array} uris List of URI's * @returns {function} callback * @returns {object} success **/ api.prototype.addNeighbors = function(uris, callback) { // Validate URIs for (var i = 0; i < uris.length; i++) { if (!inputValidator.isUri(uris[i])) return callback(errors.invalidUri(uris[i])); } var command = apiCommands.addNeighbors(uris); return this.sendCommand(command, callback) } /** * @method removeNeighbors * @param {Array} uris List of URI's * @returns {function} callback * @returns {object} success **/ api.prototype.removeNeighbors = function(uris, callback) { // Validate URIs for (var i = 0; i < uris.length; i++) { if (!inputValidator.isUri(uris[i])) return callback(errors.invalidUri(uris[i])); } var command = apiCommands.removeNeighbors(uris); return this.sendCommand(command, callback) } /** * @method getTips * @returns {function} callback * @returns {object} success **/ api.prototype.getTips = function(callback) { var command = apiCommands.getTips(); return this.sendCommand(command, callback) } var MAX_DEPTH = 15 var REFERENCE_TRANSACTION_TOO_OLD = 'reference transaction is too old' /** * @method getTransactionsToApprove * @param {int} depth * @param {string|object} [options] - Reference transaction hash or options object * @param {string} [options.reference] - Reference transaction hash * @param {number} [options.adjustDepth=false] - Flag to re-adjust depth, if original is too small * @param {number} [options.maxDepth=15] - Max depth * @returns {function} callback * @returns {object} success **/ api.prototype.getTransactionsToApprove = function(depth, options, callback) { var self = this if (typeof arguments[1] === 'function') { callback = options options = {} } var reference = typeof arguments[1] === 'string' ? options : options.reference var maxDepth = options.maxDepth || MAX_DEPTH var adjustDepth = options.adjustDepth || false // Check if correct depth if (!inputValidator.isValue(depth)) { return callback(errors.invalidInputs()); } var command = apiCommands.getTransactionsToApprove(depth, reference); return this.sendCommand(command, function (err, tips) { if (adjustDepth && err && err.message.indexOf(REFERENCE_TRANSACTION_TOO_OLD) > -1 && ++depth <= maxDepth) { return self.getTransactionsToApprove(depth, { reference: reference, adjustDepth: adjustDepth, maxDepth: maxDepth }, callback) } callback(err, tips) }) } /** * @method getTrytes * @param {array} hashes * @returns {function} callback * @returns {object} success **/ api.prototype.getTrytes = function(hashes, callback) { if (!inputValidator.isArrayOfHashes(hashes)) { return callback(errors.invalidTrytes()); } var command = apiCommands.getTrytes(hashes); return this.sendCommand(command, callback) } /** * @method interruptAttachingToTangle * @returns {function} callback * @returns {object} success **/ api.prototype.interruptAttachingToTangle = function(callback) { var command = apiCommands.interruptAttachingToTangle(); return this.sendCommand(command, callback) } /** * @method broadcastTransactions * @param {array} trytes * @returns {function} callback * @returns {object} success **/ api.prototype.broadcastTransactions = function(trytes, callback) { if (!inputValidator.isArrayOfAttachedTrytes(trytes)) { return callback(errors.invalidAttachedTrytes()); } var command = apiCommands.broadcastTransactions(trytes); return this.sendCommand(command, callback) } /** * @method storeTransactions * @param {array} trytes * @returns {function} callback * @returns {object} success **/ api.prototype.storeTransactions = function(trytes, callback) { if (!inputValidator.isArrayOfAttachedTrytes(trytes)) { return callback(errors.invalidAttachedTrytes()); } var command = apiCommands.storeTransactions(trytes); return this.sendCommand(command, callback) } /************************************* WRAPPER AND CUSTOM FUNCTIONS **************************************/ /** * Wrapper function for getTrytes and transactionObjects * gets the trytes and transaction object from a list of transaction hashes * * @method getTransactionsObjects * @param {array} hashes * @returns {function} callback * @returns {object} success **/ api.prototype.getTransactionsObjects = function(hashes, callback) { // If not array of hashes, return error if (!inputValidator.isArrayOfHashes(hashes)) { return callback(errors.invalidInputs()); } // get the trytes of the transaction hashes this.getTrytes(hashes, function(error, trytes) { if (error) return callback(error); var transactionObjects = []; // call transactionObjects for each trytes trytes.forEach(function(thisTrytes, index) { // If no trytes returned, simply push null as placeholder if (!thisTrytes) { transactionObjects.push(null); } else { transactionObjects.push(Utils.transactionObject(thisTrytes, hashes[index])); } }) return callback(null, transactionObjects); }) } /** * Wrapper function for findTransactions, getTrytes and transactionObjects * Returns the transactionObject of a transaction hash. The input can be a valid * findTransactions input * * @method getTransactionsObjects * @param {object} input * @returns {function} callback * @returns {object} success **/ api.prototype.findTransactionObjects = function(input, callback) { var self = this; self.findTransactions(input, function(error, transactions) { if (error) return callback(error); // get the transaction objects of the transactions self.getTransactionsObjects(transactions, callback); }) } /** * Wrapper function for getNodeInfo and getInclusionStates * * @method getLatestInclusion * @param {array} hashes * @returns {function} callback * @returns {object} success **/ api.prototype.getLatestInclusion = function(hashes, callback) { var self = this; self.getNodeInfo(function(e, nodeInfo) { if (e) return callback(e); var latestMilestone = nodeInfo.latestSolidSubtangleMilestone; return self.getInclusionStates(hashes, Array(latestMilestone), callback); }) } /** * Broadcasts and stores transaction trytes * * @method storeAndBroadcast * @param {array} trytes * @returns {function} callback * @returns {object} success **/ api.prototype.storeAndBroadcast = function(trytes, callback) { var self = this; self.storeTransactions(trytes, function(error, success) { if (error) return callback(error); // If no error return self.broadcastTransactions(trytes, callback) }) } /** * Gets transactions to approve, attaches to Tangle, broadcasts and stores * * @method sendTrytes * @param {array} trytes * @param {int} depth * @param {int} minWeightMagnitude * @param {object} options * @param {function} callback * @returns {object} analyzed Transaction objects **/ api.prototype.sendTrytes = function(trytes, depth, minWeightMagnitude, options, callback) { var self = this; // If no options provided, switch arguments if (arguments.length === 4 && Object.prototype.toString.call(options) === "[object Function]") { callback = options; options = {}; } // Check if correct depth and minWeightMagnitude if (!inputValidator.isValue(depth) || !inputValidator.isValue(minWeightMagnitude)) { return callback(errors.invalidInputs()); } // Get branch and trunk self.getTransactionsToApprove(depth, options, function(error, toApprove) { if (error) { return callback(error) } // attach to tangle - do pow self.attachToTangle(toApprove.trunkTransaction, toApprove.branchTransaction, minWeightMagnitude, trytes, function(error, attached) { if (error) { return callback(error) } // If the user is connected to the sandbox, we have to monitor the POW queue // to check if the POW job was completed if (self.sandbox) { var job = self.sandbox + '/jobs/' + attached.id; // Do the Sandbox send function self._makeRequest.sandboxSend(job, function(e, attachedTrytes) { if (e) { return callback(e); } self.storeAndBroadcast(attachedTrytes, function(error, success) { if (error) { return callback(error); } var finalTxs = []; attachedTrytes.forEach(function(trytes) { finalTxs.push(Utils.transactionObject(trytes)); }) return callback(null, finalTxs); }) }) } else { // Broadcast and store tx self.storeAndBroadcast(attached, function(error, success) { if (error) { return callback(error); } var finalTxs = []; attached.forEach(function(trytes) { finalTxs.push(Utils.transactionObject(trytes)); }) return callback(null, finalTxs); }) } }) }) } /** * Prepares Transfer, gets transactions to approve * attaches to Tangle, broadcasts and stores * * @method sendTransfer * @param {string | array} seed * @param {int} depth * @param {int} minWeightMagnitude * @param {array} transfers * @param {object} options * @property {array} inputs List of inputs used for funding the transfer * @property {string} address if defined, this address wil be used for sending the remainder value to * @param {function} callback * @returns {object} analyzed Transaction objects **/ api.prototype.sendTransfer = function(seed, depth, minWeightMagnitude, transfers, options, callback) { var self = this; // Validity check for number of arguments if (arguments.length < 5) { return callback(new Error("Invalid number of arguments")); } // If no options provided, switch arguments if (arguments.length === 5 && Object.prototype.toString.call(options) === "[object Function]") { callback = options; options = {}; } // Check if correct depth and minWeightMagnitude if (!inputValidator.isValue(depth) || !inputValidator.isValue(minWeightMagnitude)) { return callback(errors.invalidInputs()); } self.prepareTransfers(seed, transfers, options, function(error, trytes) { if (error) { return callback(error) } self.sendTrytes(trytes, depth, minWeightMagnitude, options, callback); }) } /** * Promotes a transaction by adding spam on top of it. * Will promote {maximum} transfers on top of the current one with {delay} interval. * * @Param {string} tail * @param {int} depth * @param {int} minWeightMagnitude * @param {array} transfer * @param {object} params * @param callback * * @returns {array} transaction objects */ api.prototype.promoteTransaction = function(tail, depth, minWeightMagnitude, transfer, params, callback) { var self = this; if (!params) params = {} if (!inputValidator.isHash(tail)) { return callback(errors.invalidTrytes()); } self.isPromotable(tail, { rejectWithReason: true }).then(function (isPromotable) { if (params.interrupt === true || (typeof(params.interrupt) === 'function' && params.interrupt())) return callback(null, tail); self.sendTransfer(transfer[0].address, depth, minWeightMagnitude, transfer, { reference: tail, adjustDepth: true, maxDepth: params.maxDepth }, function(err, res) { if (err == null && params.delay > 0) { setTimeout (function() { self.promoteTransaction(tail, depth, minWeightMagnitude, transfer, params, callback); }, params.delay); } else { return callback(err, res); } }); }).catch(function (err) { callback(err) }) } /** * Replays a transfer by doing Proof of Work again * * @method replayBundle * @param {string} tail * @param {int} depth * @param {int} minWeightMagnitude * @param {function} callback * @returns {object} analyzed Transaction objects **/ api.prototype.replayBundle = function(tail, depth, minWeightMagnitude, callback) { var self = this; // Check if correct tail hash if (!inputValidator.isHash(tail)) { return callback(errors.invalidTrytes()); } // Check if correct depth and minWeightMagnitude if (!inputValidator.isValue(depth) || !inputValidator.isValue(minWeightMagnitude)) { return callback(errors.invalidInputs()); } self.getBundle(tail, function(error, bundle) { if (error) return callback(error); // Get the trytes of all the bundle objects var bundleTrytes = []; bundle.forEach(function(bundleTx) { bundleTrytes.push(Utils.transactionTrytes(bundleTx)); }) return self.sendTrytes(bundleTrytes.reverse(), depth, minWeightMagnitude, callback); }) } /** * Re-Broadcasts a transfer * * @method broadcastBundle * @param {string} tail * @param {function} callback * @returns {object} analyzed Transaction objects **/ api.prototype.broadcastBundle = function(tail, callback) { var self = this; // Check if correct tail hash if (!inputValidator.isHash(tail)) { return callback(errors.invalidTrytes()); } self.getBundle(tail, function(error, bundle) { if (error) return callback(error); // Get the trytes of all the bundle objects var bundleTrytes = []; bundle.forEach(function(bundleTx) { bundleTrytes.push(Utils.transactionTrytes(bundleTx)); }) return self.broadcastTransactions(bundleTrytes.reverse(), callback); }) } /** * Generates a new address * * @method newAddress * @param {string | array} seed * @param {int} index * @param {int} security Security level of the private key * @param {bool} checksum * @returns {string} address Transaction objects **/ api.prototype._newAddress = function(seed, index, security, checksum) { var key = Signing.key(typeof seed === "string" ? Converter.trits(seed) : seed, index, security); var digests = Signing.digests(key); var addressTrits = Signing.address(digests); var address = Converter.trytes(addressTrits) if (checksum) { address = Utils.addChecksum(address); } return address; } /** * Generates a new address either deterministically or index-based * * @method getNewAddress * @param {string | array} seed * @param {object} options * @property {int} index Key index to start search from * @property {bool} checksum add 9-tryte checksum * @property {int} total Total number of addresses to return * @property {int} security Security level to be used for the private key / address. Can be 1, 2 or 3 * @property {bool} returnAll return all searched addresses * @param {function} callback * @returns {string | array} address List of addresses **/ api.prototype.getNewAddress = function(seed, options, callback) { var self = this; // If no options provided, switch arguments if (arguments.length === 2 && Object.prototype.toString.call(options) === "[object Function]") { callback = options; options = {}; } // validate the seed if (!inputValidator.isTrytes(seed) && !inputValidator.isTritArray(seed)) { return callback(errors.invalidSeed()); } // default index value var index = 0; if ('index' in options) { index = options.index; // validate the index option if (!inputValidator.isValue(index) || index < 0) { return callback(errors.invalidIndex()); } } var checksum = options.checksum || false; var total = options.total || null; // If no user defined security, use the standard value of 2 var security = 2; if ('security' in options) { security = options.security; // validate the security option if (!inputValidator.isValue(security) || security < 1 || security > 3) { return callback(errors.invalidSecurity()); } } var allAddresses = []; // Case 1: total // // If total number of addresses to generate is supplied, simply generate // and return the list of all addresses if (total) { // Increase index with each iteration for (var i = 0; i < total; i++, index++) { var address = self._newAddress(seed, index, security, checksum); allAddresses.push(address); } return callback(null, allAddresses); } // Case 2: no total provided // // Continue calling wasAddressSpenFrom & findTransactions to see if address was already created // if null, return list of addresses // else { async.doWhilst(function(callback) { // Iteratee function var newAddress = self._newAddress(seed, index, security, checksum) if (options.returnAll) { allAddresses.push(newAddress) } // Increase the index index += 1 self.wereAddressesSpentFrom(newAddress, function (err, res) { if (err) { return callback(err) } // Validity check if (res[0]) { callback(null, newAddress, true) } else { // Check for txs if address isn't spent self.findTransactions({'addresses': [newAddress]}, function (err, transactions) { if (err) { return callback(err) } callback(err, newAddress, transactions.length > 0) }) } }) }, function (address, isUsed) { return isUsed }, function(err, address) { // Final callback if (err) { return callback(err); } else { // If returnAll, return list of allAddresses // else return the last address that was generated var addressToReturn = options.returnAll ? allAddresses : address; return callback(null, addressToReturn); } }) } } /** * Gets the inputs of a seed * * @method getInputs * @param {string | array} seed * @param {object} options * @property {int} start Starting key index * @property {int} end Ending key index * @property {int} threshold Min balance required * @property {int} security secuirty level of private key / seed * @param {function} callback **/ api.prototype.getInputs = function(seed, options, callback) { var self = this; // If no options provided, switch arguments if (arguments.length === 2 && Object.prototype.toString.call(options) === "[object Function]") { callback = options; options = {}; } // validate the seed if (!inputValidator.isTrytes(seed) && !inputValidator.isTritArray(seed)) { return callback(errors.invalidSeed()); } var start = options.start || 0; var end = options.end || null; var threshold = options.threshold || null; // If no user defined security, use the standard value of 2 var security = options.security || 2; // If start value bigger than end, return error // or if difference between end and start is bigger than 500 keys if (options.end && (start > end || end > (start + 500))) { return callback(new Error("Invalid inputs provided")) } // Case 1: start and end // // If start and end is defined by the user, simply iterate through the keys // and call getBalances if (end) { var allAddresses = []; for (var i = start; i < end; i++) { var address = self._newAddress(seed, i, security, false); allAddresses.push(address); } getBalanceAndFormat(allAddresses); } // Case 2: iterate till threshold || end // // Either start from index: 0 or start (if defined) until threshold is reached. // Calls getNewAddress and deterministically generates and returns all addresses // We then do getBalance, format the output and return it else { self.getNewAddress(seed, {'index': start, 'returnAll': true, 'security': security}, function(error, addresses) { if (error) { return callback(error); } else { getBalanceAndFormat(addresses); } }) } // Calls getBalances and formats the output // returns the final inputsObject then function getBalanceAndFormat(addresses) { self.getBalances(addresses, 100, function(error, balances) { if (error) { return callback(error); } else { var inputsObject = { 'inputs': [], 'totalBalance': 0 } // If threshold defined, keep track of whether reached or not // else set default to true var thresholdReached = threshold ? false : true; for (var i = 0; i < addresses.length; i++) { var balance = parseInt(balances.balances[i]); if (balance > 0) { var newEntry = { 'address': addresses[i], 'balance': balance, 'keyIndex': start + i, 'security': security } // Add entry to inputs inputsObject.inputs.push(newEntry); // Increase totalBalance of all aggregated inputs inputsObject.totalBalance += balance; if (threshold && inputsObject.totalBalance >= threshold) { thresholdReached = true; break; } } } if (thresholdReached) { return callback(null, inputsObject); } else { return callback(new Error("Not enough balance")); } } }) } } /** * Prepares transfer by generating bundle, finding and signing inputs * * @method prepareTransfers * @param {string | array} seed * @param {object} transfers * @param {object} options * @property {array} inputs Inputs used for signing. Needs to have correct security, keyIndex and address value * @property {string} address Remainder address * @property {int} security security level to be used for getting inputs and addresses * @property {string} hmacKey HMAC key used for attaching an HMAC * @param {function} callback * @returns {array} trytes Returns bundle trytes **/ api.prototype.prepareTransfers = function(seed, transfers, options, callback) { var self = this; var addHMAC = false; var addedHMAC = false; // If no options provided, switch arguments if (arguments.length === 3 && Object.prototype.toString.call(options) === "[object Function]") { callback = options; options = {}; } // validate the seed if (!inputValidator.isTrytes(seed) && !inputValidator.isTritArray(seed)) { return callback(errors.invalidSeed()); } if (options.hasOwnProperty('hmacKey') && options.hmacKey) { if(!inputValidator.isTrytes(options.hmacKey)) { return callback(errors.invalidTrytes()); } addHMAC = true; } // If message or tag is not supplied, provide it // Also remove the checksum of the address if it's there after validating it transfers.forEach(function(thisTransfer) { thisTransfer.message = thisTransfer.message ? thisTransfer.message : ''; thisTransfer.obsoleteTag = thisTransfer.tag ? thisTransfer.tag : (thisTransfer.obsoleteTag ? thisTransfer.obsoleteTag : ''); if (addHMAC && thisTransfer.value > 0) { thisTransfer.message = nullHashTrytes + thisTransfer.message; addedHMAC = true; } // If address with checksum, validate it if (thisTransfer.address.length === 90) { if (!Utils.isValidChecksum(thisTransfer.address)) { return callback(errors.invalidChecksum(thisTransfer.address)); } } thisTransfer.address = Utils.noChecksum(thisTransfer.address); }) // Input validation of transfers object if (!inputValidator.isTransfersArray(transfers)) { return callback(errors.invalidTransfers()); } // If inputs provided, validate the format if (options.inputs && !inputValidator.isInputs(options.inputs)) { return callback(errors.invalidInputs()); } var remainderAddress = options.address || null; var chosenInputs = options.inputs || []; var security = options.security || 2; // Create a new bundle var bundle = new Bundle(); var totalValue = 0; var signatureFragments = []; var tag; // // Iterate over all transfers, get totalValue // and prepare the signatureFragments, message and tag // for (var i = 0; i < transfers.length; i++) { var signatureMessageLength = 1; // If message longer than 2187 trytes, increase signatureMessageLength (add 2nd transaction) if (transfers[i].message.length > 2187) { // Get total length, message / maxLength (2187 trytes) signatureMessageLength += Math.floor(transfers[i].message.length / 2187); var msgCopy = transfers[i].message; // While there is still a message, copy it while (msgCopy) { var fragment = msgCopy.slice(0, 2187); msgCopy = msgCopy.slice(2187, msgCopy.length); // Pad remainder of fragment for (var j = 0; fragment.length < 2187; j++) { fragment += '9'; } signatureFragments.push(fragment); } } else { // Else, get single fragment with 2187 of 9's trytes var fragment = ''; if (transfers[i].message) { fragment = transfers[i].message.slice(0, 2187) } for (var j = 0; fragment.length < 2187; j++) { fragment += '9'; } signatureFragments.push(fragment); } // get current timestamp in seconds var timestamp = Math.floor(Date.now() / 1000); // If no tag defined, get 27 tryte tag. tag = transfers[i].obsoleteTag ? transfers[i].obsoleteTag : '999999999999999999999999999'; // Pad for required 27 tryte length for (var j = 0; tag.length < 27; j++) { tag += '9'; } // Add first entries to the bundle // Slice the address in case the user provided a checksummed one bundle.addEntry(signatureMessageLength, transfers[i].address, transfers[i].value, tag, timestamp) // Sum up total value totalValue += parseInt(transfers[i].value); } // Get inputs if we are sending tokens if (totalValue) { // Case 1: user provided inputs // // Validate the inputs by calling getBalances if (options.inputs) { // Get list if addresses of the provided inputs var inputsAddresses = []; options.inputs.forEach(function(inputEl) { inputsAddresses.push(inputEl.address); }) self.getBalances(inputsAddresses, 100, function(error, balances) { if (error) return callback(error); var confirmedInputs = []; var totalBalance = 0; for (var i = 0; i < balances.balances.length; i++) { var thisBalance = parseInt(balances.balances[i]); // If input has balance, add it to confirmedInputs if (thisBalance > 0) { totalBalance += thisBalance; var inputEl = options.inputs[i]; inputEl.balance = thisBalance; confirmedInputs.push(inputEl); // if we've already reached the intended input value, break out of loop if (totalBalance >= totalValue) { break; } } } // Return not enough balance error if (totalValue > totalBalance) { return callback(new Error("Not enough balance")); } addRemainder(confirmedInputs); }); } // Case 2: Get inputs deterministically // // If no inputs provided, derive the addresses from the seed and // confirm that the inputs exceed the threshold else { self.getInputs(seed, { 'threshold': totalValue, 'security': security }, function(error, inputs) { // If inputs with enough balance if (!error) { addRemainder(inputs.inputs); } else { return callback(error); } }) } } else { // If no input required, don't sign and simply finalize the bundle bundle.finalize(); bundle.addTrytes(signatureFragments); var bundleTrytes = [] bundle.bundle.forEach(function(tx) { bundleTrytes.push(Utils.transactionTrytes(tx)) }) return callback(null, bundleTrytes.reverse()); } function addRemainder(inputs) { var totalTransferValue = totalValue; for (var i = 0; i < inputs.length; i++) { var thisBalance = inputs[i].balance; var toSubtract = 0 - thisBalance; var timestamp = Math.floor(Date.now() / 1000); var address = Utils.noChecksum(inputs[i].address); // Add input as bundle entry bundle.addEntry(inputs[i].security, address, toSubtract, tag, timestamp); // If there is a remainder value // Add extra output to send remaining funds to if (thisBalance >= totalTransferValue) { var remainder = thisBalance - totalTransferValue; // If user has provided remainder address // Use it to send remaining funds to if (remainder > 0 && remainderAddress) { // Remainder bundle entry bundle.addEntry(1, remainderAddress, remainder, tag, timestamp); // Final function for signing inputs signInputsAndReturn(inputs); } else if (remainder > 0) { var startIndex = 0; for(var k = 0; k < inputs.length; k++) { startIndex = Math.max(inputs[k].keyIndex, startIndex); } startIndex++; // Generate a new Address by calling getNewAddress self.getNewAddress(seed, {'index': startIndex, 'security': security}, function(error, address) { if (error) return callback(error) var timestamp = Math.floor(Date.now() / 1000); // Remainder bundle entry bundle.addEntry(1, address, remainder, tag, timestamp); // Final function for signing inputs signInputsAndReturn(inputs); }) } else { // If there is no remainder, do not add transaction to bundle // simply sign and return signInputsAndReturn(inputs); } // If multiple inputs provided, subtract the totalTransferValue by // the inputs balance } else { totalTransferValue -= thisBalance; } } } function signInputsAndReturn(inputs) { bundle.finalize(); bundle.addTrytes(signatureFragments); // SIGNING OF INPUTS // // Here we do the actual signing of the inputs // Iterate over all bundle transactions, find the inputs // Get the corresponding private key and calculate the signatureFragment for (var i = 0; i < bundle.bundle.length; i++) { if (bundle.bundle[i].value < 0) { var thisAddress = bundle.bundle[i].address; // Get the corresponding keyIndex and security of the address var keyIndex; var keySecurity; for (var k = 0; k < inputs.length; k++) { if (inputs[k].address === thisAddress) { keyIndex = inputs[k].keyIndex; keySecurity = inputs[k].security ? inputs[k].security : security; break; } } var bundleHash = bundle.bundle[i].bundle; // Get corresponding private key of address var key = Signing.key(typeof seed === "string" ? Converter.trits(seed) : seed, keyIndex, keySecurity); // Get the normalized bundle hash var normalizedBundleHash = bundle.normalizedBundle(bundleHash); var normalizedBundleFragments = []; // Split hash into 3 fragments for (var l = 0; l < 3; l++) { normalizedBundleFragments[l] = normalizedBundleHash.slice(l * 27, (l + 1) * 27); } // First 6561 trits for the firstFragment var firstFragment = key.slice(0, 6561); // First bundle fragment uses the first 27 trytes var firstBundleFragment = normalizedBundleFragments[0]; // Calculate the new signatureFragment with the first bundle fragment var firstSignedFragment = Signing.signatureFragment(firstBundleFragment, firstFragment); // Convert signature to trytes and assign the new signatureFragment bundle.bundle[i].signatureMessageFragment = Converter.trytes(firstSignedFragment); // if user chooses higher than 27-tryte security // for each security level, add an additional signature for (var j = 1; j < keySecurity; j++) { // Because the signature is > 2187 trytes, we need to // find the subsequent transaction to add the remainder of the signature // Same address as well as value = 0 (as we already spent the input) if (bundle.bundle[i + j].address === thisAddress && bundle.bundle[i + j].value === 0) { // Use the next 6561 trits var nextFragment = key.slice(6561 * j, (j + 1) * 6561); var nextBundleFragment = normalizedBundleFragments[j]; // Calculate the new signature var nextSignedFragment = Signing.signatureFragment(nextBundleFragment, nextFragment); // Convert signature to trytes and assign it again to this bundle entry bundle.bundle[i + j].signatureMessageFragment = Converter.trytes(nextSignedFragment); } } } } if(addedHMAC) { var hmac = new HMAC(options.hmacKey); hmac.addHMAC(bundle); } var bundleTrytes = [] // Convert all bundle entries into trytes bundle.bundle.forEach(function(tx) { bundleTrytes.push(Utils.transactionTrytes(tx)) }) return callback(null, bundleTrytes.reverse()); } } /** * Basically traverse the Bundle by going down the trunkTransactions until * the bundle hash of the transaction is no longer the same. In case the input * transaction hash is not a tail, we return an error. * * @method traverseBundle * @param {string} trunkTx Hash of a trunk or a tail transaction of a bundle * @param {string} bundleHash * @param {array} bundle List of bundles to be populated * @returns {array} bundle Transaction objects **/ api.prototype.traverseBundle = function(trunkTx, bundleHash, bundle, callback) { var self = this; // Get trytes of transaction hash self.getTrytes(Array(trunkTx), function(error, trytesList) { if (error) return callback(error); var trytes = trytesList[0] if (!trytes) return callback(new Error("Bundle transactions not visible")) // get the transaction object var txObject = Utils.transactionObject(trytes); if (!txObject) return callback(new Error("Invalid trytes, could not create object")); // If first transaction to search is not a tail, return error if (!bundleHash && txObject.currentIndex !== 0) { return callback(new Error("Invalid tail transaction supplied.")); } // If no bundle hash, define it if (!bundleHash) { bundleHash = txObject.bundle; } // If different bundle hash, return with bundle if (bundleHash !== txObject.bundle) { return callback(null, bundle); } // If only one bundle element, return if (txObject.lastIndex === 0 && txObject.currentIndex === 0) { return callback(null, Array(txObject)); } // Define new trunkTransaction for search var trunkTx = txObject.trunkTransaction; // Add transaction object to bundle bundle.push(txObject); // Continue traversing with new trunkTx return self.traverseBundle(trunkTx, bundleHash, bundle, callback); }) } /** * Gets the associated bundle transactions of a single transaction * Does validation of signatures, total sum as well as bundle order * * @method getBundle * @param {string} transaction Hash of a tail transaction * @returns {list} bundle Transaction objects **/ api.prototype.getBundle = function(transaction, callback) { var self = this; // inputValidator: Check if correct hash if (!inputValidator.isHash(transaction)) { return callback(errors.invalidInputs(transaction)); } // Initiate traverseBundle self.traverseBundle(transaction, null, Array(), function(error, bundle) { if (error) return callback(error); if (!Utils.isBundle(bundle)) { return callback(new Error("Invalid Bundle provided")) } else { // Return bundle element return callback(null, bundle); } }) } /** * Internal function to get the formatted bundles of a list of addresses * * @method _bundlesFromAddresses * @param {list} addresses List of addresses * @param {bool} inclusionStates * @returns {list} bundles Transaction objects **/ api.prototype._bundlesFromAddresses = function(addresses, inclusionStates, callback) { var self = this; // call wrapper function to get txs associated with addresses self.findTransactionObjects({'addresses': addresses}, function(error, transactionObjects) { if (error) return callback(error); // set of tail transactions var tailTransactions = new Set(); var nonTailBundleHashes = new Set(); transactionObjects.forEach(function(thisTransaction) { // Sort tail and nonTails if (thisTransaction.currentIndex === 0) { tailTransactions.add(thisTransaction.hash); } else { nonTailBundleHashes.add(thisTransaction.bundle) } }) // Get tail transactions for each nonTail via the bundle hash self.findTransactionObjects({'bundles': Array.from(nonTailBundleHashes)}, function(error, bundleObjects) { if (error) return callback(error); bundleObjects.forEach(function(thisTransaction) { if (thisTransaction.currentIndex === 0) { tailTransactions.add(thisTransaction.hash); } }) var finalBundles = []; var tailTxArray = Array.from(tailTransactions); // If inclusionStates, get the confirmation status // of the tail transactions, and thus the bundles async.waterfall([ // // 1. Function // function(cb) { if (inclusionStates) { self.getLatestInclusion(tailTxArray, function(error, states) { // If error, return it to original caller if (error) return callback(error); cb(null, states); }) } else { cb(null, []); } }, // // 2. Function // function(tailTxStates, cb) { // Map each tail transaction to the getBundle function // format the returned bundles and add inclusion states if necessary async.mapSeries(tailTxArray, function(tailTx, cb2) { self.getBundle(tailTx, function(error, bundle) { // If error returned from getBundle, simply ignore it // because the bundle was most likely incorrect if (!error) { // If inclusion states, add to each bund