var util = require('util'); var assert = require('assert'); var EventEmitter = require('events').EventEmitter; var utils = require('./utils'); var sjcl = require('./utils').sjcl; var Amount = require('./amount').Amount; var Currency = require('./amount').Currency; var UInt160 = require('./amount').UInt160; var Seed = require('./seed').Seed; var SerializedObject = require('./serializedobject').SerializedObject; var RippleError = require('./rippleerror').RippleError; var hashprefixes = require('./hashprefixes'); var config = require('./config'); var log = require('./log').internal.sub('transaction'); /** * @constructor Transaction * * Notes: * All transactions including those with local and malformed errors may be * forwarded anyway. * * A malicous server can: * - may or may not forward * - give any result * + it may declare something correct as incorrect or something incorrect * as correct * + it may not communicate with the rest of the network */ function Transaction(remote) { EventEmitter.call(this); var self = this; var remoteExists = (typeof remote === 'object'); this.remote = remote; this.tx_json = { Flags: 0 }; this._secret = void(0); this._build_path = false; this._maxFee = remoteExists ? this.remote.max_fee : void(0); this.state = 'unsubmitted'; this.finalized = false; this.previousSigningHash = void(0); this.submitIndex = void(0); this.canonical = remoteExists ? this.remote.canonical_signing : true; this.submittedIDs = [ ]; this.attempts = 0; this.submissions = 0; this.responses = 0; this.once('success', function(message) { // Transaction definitively succeeded self.setState('validated'); self.finalize(message); if (self._successHandler) { self._successHandler(message); } }); this.once('error', function(message) { // Transaction definitively failed self.setState('failed'); self.finalize(message); if (self._errorHandler) { self._errorHandler(message); } }); this.once('submitted', function() { // Transaction was submitted to the network self.setState('submitted'); }); this.once('proposed', function() { // Transaction was submitted successfully to the network self.setState('pending'); }); }; util.inherits(Transaction, EventEmitter); // XXX This needs to be determined from the network. Transaction.fee_units = { default: 10 }; Transaction.flags = { // Universal flags can apply to any transaction type Universal: { FullyCanonicalSig: 0x80000000 }, AccountSet: { RequireDestTag: 0x00010000, OptionalDestTag: 0x00020000, RequireAuth: 0x00040000, OptionalAuth: 0x00080000, DisallowXRP: 0x00100000, AllowXRP: 0x00200000 }, TrustSet: { SetAuth: 0x00010000, NoRipple: 0x00020000, SetNoRipple: 0x00020000, ClearNoRipple: 0x00040000, SetFreeze: 0x00100000, ClearFreeze: 0x00200000 }, OfferCreate: { Passive: 0x00010000, ImmediateOrCancel: 0x00020000, FillOrKill: 0x00040000, Sell: 0x00080000 }, Payment: { NoRippleDirect: 0x00010000, PartialPayment: 0x00020000, LimitQuality: 0x00040000 } }; // The following are integer (as opposed to bit) flags // that can be set for particular transactions in the // SetFlag or ClearFlag field Transaction.set_clear_flags = { AccountSet: { asfRequireDest: 1, asfRequireAuth: 2, asfDisallowXRP: 3, asfDisableMaster: 4, asfAccountTxnID: 5, asfNoFreeze: 6, asfGlobalFreeze: 7 } }; Transaction.MEMO_TYPES = { }; Transaction.ASCII_REGEX = /^[\x00-\x7F]*$/; Transaction.formats = require('./binformat').tx; Transaction.prototype.consts = { telLOCAL_ERROR: -399, temMALFORMED: -299, tefFAILURE: -199, terRETRY: -99, tesSUCCESS: 0, tecCLAIMED: 100 }; Transaction.prototype.isTelLocal = function(ter) { return ter >= this.consts.telLOCAL_ERROR && ter < this.consts.temMALFORMED; }; Transaction.prototype.isTemMalformed = function(ter) { return ter >= this.consts.temMALFORMED && ter < this.consts.tefFAILURE; }; Transaction.prototype.isTefFailure = function(ter) { return ter >= this.consts.tefFAILURE && ter < this.consts.terRETRY; }; Transaction.prototype.isTerRetry = function(ter) { return ter >= this.consts.terRETRY && ter < this.consts.tesSUCCESS; }; Transaction.prototype.isTepSuccess = function(ter) { return ter >= this.consts.tesSUCCESS; }; Transaction.prototype.isTecClaimed = function(ter) { return ter >= this.consts.tecCLAIMED; }; Transaction.prototype.isRejected = function(ter) { return this.isTelLocal(ter) || this.isTemMalformed(ter) || this.isTefFailure(ter); }; Transaction.from_json = function(j) { return (new Transaction()).parseJson(j); }; Transaction.prototype.parseJson = function(v) { this.tx_json = v; return this; }; /** * Set state on the condition that the state is different * * @param {String} state */ Transaction.prototype.setState = function(state) { if (this.state !== state) { this.state = state; this.emit('state', state); } }; /** * Finalize transaction. This will prevent future activity * * @param {Object} message * @api private */ Transaction.prototype.finalize = function(message) { this.finalized = true; if (this.result) { this.result.ledger_index = message.ledger_index; this.result.ledger_hash = message.ledger_hash; } else { this.result = message; this.result.tx_json = this.tx_json; } this.emit('cleanup'); this.emit('final', message); if (this.remote && this.remote.trace) { log.info('transaction finalized:', this.tx_json, this.getManager()._pending.getLength()); } return this; }; /** * Get transaction Account * * @return {Account} */ Transaction.prototype.getAccount = function() { return this.tx_json.Account; }; /** * Get TransactionType * * @return {String} */ Transaction.prototype.getTransactionType = function() { return this.tx_json.TransactionType; }; /** * Get transaction TransactionManager * * @param [String] account * @return {TransactionManager] */ Transaction.prototype.getManager = function(account) { if (!this.remote) { return void(0); } if (!account) { account = this.tx_json.Account; } return this.remote.account(account)._transactionManager; }; /** * Get transaction secret * * @param [String] account */ Transaction.prototype.getSecret = Transaction.prototype._accountSecret = function(account) { if (!this.remote) { return void(0); } if (!account) { account = this.tx_json.Account; } return this.remote.secrets[account]; }; /** * Returns the number of fee units this transaction will cost. * * Each Ripple transaction based on its type and makeup costs a certain number * of fee units. The fee units are calculated on a per-server basis based on the * current load on both the network and the server. * * @see https://ripple.com/wiki/Transaction_Fee * * @return {Number} Number of fee units for this transaction. */ Transaction.prototype._getFeeUnits = Transaction.prototype.feeUnits = function() { return Transaction.fee_units['default']; }; /** * Compute median server fee * * @return {String} median fee */ Transaction.prototype._computeFee = function() { if (!this.remote) { return void(0); } var servers = this.remote._servers; var fees = [ ]; for (var i=0; i b) { return 1; } else if (a < b) { return -1; } else { return 0; } }); var midInd = Math.floor(fees.length / 2); var median = fees.length % 2 === 0 ? Math.floor(0.5 + (fees[midInd] + fees[midInd - 1]) / 2) : fees[midInd]; return String(median); }; /** * Attempts to complete the transaction for submission. * * This function seeks to fill out certain fields, such as Fee and * SigningPubKey, which can be determined by the library based on network * information and other fields. * * @return {Boolean|Transaction} If succeeded, return transaction. Otherwise * return `false` */ Transaction.prototype.complete = function() { if (this.remote) { if (!this.remote.trusted && !this.remote.local_signing) { this.emit('error', new RippleError( 'tejServerUntrusted', 'Attempt to give secret to untrusted server')); return false; } } // Try to auto-fill the secret if (!this._secret && !(this._secret = this.getSecret())) { this.emit('error', new RippleError('tejSecretUnknown', 'Missing secret')); return false; } if (typeof this.tx_json.SigningPubKey === 'undefined') { try { var seed = Seed.from_json(this._secret); var key = seed.get_key(this.tx_json.Account); this.tx_json.SigningPubKey = key.to_hex_pub(); } catch(e) { this.emit('error', new RippleError( 'tejSecretInvalid', 'Invalid secret')); return false; } } // If the Fee hasn't been set, one needs to be computed by // an assigned server if (this.remote && typeof this.tx_json.Fee === 'undefined') { if (this.remote.local_fee || !this.remote.trusted) { if (!(this.tx_json.Fee = this._computeFee())) { this.emit('error', new RippleError('tejUnconnected')); return false; } } } if (Number(this.tx_json.Fee) > this._maxFee) { this.emit('error', new RippleError( 'tejMaxFeeExceeded', 'Max fee exceeded')); return false; } // Set canonical flag - this enables canonicalized signature checking if (this.remote && this.remote.local_signing && this.canonical) { this.tx_json.Flags |= Transaction.flags.Universal.FullyCanonicalSig; // JavaScript converts operands to 32-bit signed ints before doing bitwise // operations. We need to convert it back to an unsigned int. this.tx_json.Flags = this.tx_json.Flags >>> 0; } return this.tx_json; }; Transaction.prototype.serialize = function() { return SerializedObject.from_json(this.tx_json); }; Transaction.prototype.signingHash = function() { return this.hash(config.testnet ? 'HASH_TX_SIGN_TESTNET' : 'HASH_TX_SIGN'); }; Transaction.prototype.hash = function(prefix, asUINT256, serialized) { if (typeof prefix === 'string') { if (typeof hashprefixes[prefix] === 'undefined') { throw new Error('Unknown hashing prefix requested.'); } prefix = hashprefixes[prefix]; } else if (!prefix) { prefix = hashprefixes.HASH_TX_ID; } var hash = (serialized || this.serialize()).hash(prefix); return asUINT256 ? hash : hash.to_hex(); }; Transaction.prototype.sign = function() { var seed = Seed.from_json(this._secret); var prev_sig = this.tx_json.TxnSignature; delete this.tx_json.TxnSignature; var hash = this.signingHash(); // If the hash is the same, we can re-use the previous signature if (prev_sig && hash === this.previousSigningHash) { this.tx_json.TxnSignature = prev_sig; return this; } var key = seed.get_key(this.tx_json.Account); var sig = key.sign(hash, 0); var hex = sjcl.codec.hex.fromBits(sig).toUpperCase(); this.tx_json.TxnSignature = hex; this.previousSigningHash = hash; return this; }; /** * Add an ID to cached list of submitted IDs * * @param {String} transaction id * @api private */ Transaction.prototype.addId = function(id) { if (this.submittedIDs.indexOf(id) === -1) { this.submittedIDs.unshift(id); } }; /** * Find ID within cached received (validated) IDs. If this transaction has * an ID that is within the cache, it has been seen validated, so return the * received message * * @param {Object} cache * @return {Object} message * @api private */ Transaction.prototype.findId = function(cache) { var result; for (var i=0; i= 0. * * @param {Number} fee The proposed fee */ Transaction.prototype.maxFee = function(fee) { if (typeof fee === 'number' && fee >= 0) { this._setMaxFee = true; this._maxFee = fee; } return this; }; /* * Set the fee user will pay to the network for submitting this transaction. * Specified fee must be >= 0. * * @param {Number} fee The proposed fee * * @returns {Transaction} calling instance for chaining */ Transaction.prototype.setFixedFee = function(fee) { if (typeof fee === 'number' && fee >= 0) { this._setFixedFee = true; this.tx_json.Fee = String(fee); } return this; }; /** * Filter invalid properties from path objects in a path array * * Valid properties are: * - account * - currency * - issuer * - type_hex * * @param {Array} path * @return {Array} filtered path */ Transaction._rewritePath = function(path) { if (!Array.isArray(path)) { throw new Error('Path must be an array'); } var newPath = path.map(function(node) { var newNode = { }; if (node.hasOwnProperty('account')) { newNode.account = UInt160.json_rewrite(node.account); } if (node.hasOwnProperty('issuer')) { newNode.issuer = UInt160.json_rewrite(node.issuer); } if (node.hasOwnProperty('currency')) { newNode.currency = Currency.json_rewrite(node.currency); } if (node.hasOwnProperty('type_hex')) { newNode.type_hex = node.type_hex; } return newNode; }); return newPath; }; /** * Add a path for Payment transaction * * XXX Abort if not a Payment * * @param {Array} path */ Transaction.prototype.addPath = Transaction.prototype.pathAdd = function(path) { if (Array.isArray(path)) { this.tx_json.Paths = this.tx_json.Paths || [ ]; this.tx_json.Paths.push(Transaction._rewritePath(path)); } return this; }; /** * Set paths for Payment transaction * * XXX Abort if not a Payment * * @param {Array} paths */ Transaction.prototype.setPaths = Transaction.prototype.paths = function(paths) { if (Array.isArray(paths)) { for (var i=0, l=paths.length; i= 1e9) { this.tx_json.TransferRate = rate; } return this; }; /** * Set Flags. You may specify flags as a number, as the string name of the * flag, or as an array of strings. * * setFlags(Transaction.flags.AccountSet.RequireDestTag) * setFlags('RequireDestTag') * setFlags('RequireDestTag', 'RequireAuth') * setFlags([ 'RequireDestTag', 'RequireAuth' ]) * * @param {Number|String|Array} flags */ Transaction.prototype.setFlags = function(flags) { if (flags === void(0)) { return this; } if (typeof flags === 'number') { this.tx_json.Flags = flags; return this; } var transaction_flags = Transaction.flags[this.getTransactionType()] || { }; var flag_set = Array.isArray(flags) ? flags : [].slice.call(arguments); for (var i=0, l=flag_set.length; i