ripplelib
Version:
A JavaScript API for interacting with Ripple in Node.js and the browser
1,727 lines (1,415 loc) • 46 kB
JavaScript
;
var util = require('util');
var lodash = require('lodash');
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 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 = undefined;
this._build_path = false;
this._maxFee = remoteExists ? this.remote.max_fee : undefined;
this.state = 'unsubmitted';
this.finalized = false;
this.previousSigningHash = undefined;
this.submitIndex = undefined;
this.canonical = remoteExists ? this.remote.canonical_signing : true;
this.submittedIDs = [];
this.attempts = 0;
this.submissions = 0;
this.responses = 0;
this._maxSignerNum = 8;
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);
// This is currently a constant in rippled known as the "base reference"
// https://wiki.ripple.com/Transaction_Fee#Base_Fees
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,
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,
asfDefaultRipple: 8
}
};
Transaction.MEMO_TYPES = {};
/* eslint-disable max-len */
// URL characters per RFC 3986
Transaction.MEMO_REGEX = /^[0-9a-zA-Z-\.\_\~\:\/\?\#\[\]\@\!\$\&\'\(\)\*\+\,\;\=\%]+$/;
/* eslint-enable max-len */
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.getType = 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 undefined;
}
var account = account_ || this.tx_json.Account;
return this.remote.account(account)._transactionManager;
};
/**
* Get keypair for signing.
*
* @param [String] account
*/
Transaction.prototype.getKey =
Transaction.prototype.getKeyPair =
Transaction.prototype._accountKeyPair = function(account_) {
if (!this.remote || this._secret) {
return this.generateKey();
}
var account = account_ || this.tx_json.Account;
return this.remote.getKey(account);
}
Transaction.prototype.generateKey =
Transaction.prototype.generateKeyPair = function(secret) {
secret = secret || this._secret;
try {
return Seed.from_json(secret).get_key();
} catch(e) {
throw new Error('Invalid Secret!');
}
}
/**
* Get transaction secret
*
* @param [String] account
*/
Transaction.prototype.getSecret = Transaction.prototype._accountSecret = function (account_) {
if (!this.remote) {
return undefined;
}
var 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 undefined;
}
var servers = this.remote._servers;
var fees = [];
for (var i = 0; i < servers.length; i++) {
var server = servers[i];
if (server.isConnected()) {
fees.push(Number(server._computeFee(this._getFeeUnits())));
}
}
switch (fees.length) {
case 0:
return undefined;
case 1:
return String(fees[0]);
}
fees.sort(function ascending(a, b) {
if (a > b) {
return 1;
} else if (a < b) {
return -1;
}
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;
}
}
if (typeof this.tx_json.SigningPubKey === 'undefined') {
if (this._multiSign) {
this.tx_json.SigningPubKey = '';
} else {
try {
var key = this.getKey();
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) {
var fee = this._computeFee();
if (!fee) {
this.emit('error', new RippleError('tejUnconnected'));
return false;
}
if (Number(fee) > this._maxFee) fee = String(this._maxFee);
if (this._multiSign) {
var num = this._signerNum || 2; //default to 2-signatures
this.tx_json.Fee = String(Number(fee) * (num + 1));
} else {
this.tx_json.Fee = fee;
}
}
}
// 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 (testnet) {
return this.hash(testnet ? 'HASH_TX_SIGN_TESTNET' : 'HASH_TX_SIGN');
};
Transaction.prototype.hash = function (prefix_, asUINT256, serialized) {
var prefix = undefined;
if (typeof prefix_ !== 'string') {
prefix = hashprefixes.HASH_TX_ID;
} else if (!hashprefixes.hasOwnProperty(prefix_)) {
throw new Error('Unknown hashing prefix requested: ' + prefix_);
} else {
prefix = hashprefixes[prefix_];
}
var hash = (serialized || this.serialize()).hash(prefix);
return asUINT256 ? hash : hash.to_hex();
};
Transaction.prototype.sign = function (testnet) {
if (this._multiSign) return this;
var prev_sig = this.tx_json.TxnSignature;
delete this.tx_json.TxnSignature;
var hash = this.signingHash(testnet);
// 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 = this.getKey();
var sig = key.sign(hash);
var hex = sjcl.codec.hex.fromBits(sig).toUpperCase();
this.tx_json.TxnSignature = hex;
this.previousSigningHash = hash;
return this;
};
Transaction.prototype.multiSigningHash = function(account) {
var prev_sig = this.tx_json.Signers;
delete this.tx_json.Signers;
var prefix = hashprefixes['HASH_TX_MULTISIGN'];
var suffix = UInt160.from_json(account).to_bytes();
var hash = this.serialize().hash(prefix, suffix);
this.tx_json.Signers = prev_sig;
return hash.to_hex();
};
Transaction.prototype.getSignatureFor = function(account) {
var signer = {
Signer : {
Account : account,
SigningPubKey : '',
TxnSignature : ''
}
}
var prev_sig = this.tx_json.Signers;
delete this.tx_json.Signers;
var hash = this.multiSigningHash(account);
var key = this.getKey(account);
var sig = key.sign(hash, 0);
var hex = sjcl.codec.hex.fromBits(sig).toUpperCase();
signer.Signer.SigningPubKey = key.to_hex_pub();
signer.Signer.TxnSignature = hex;
this.tx_json.Signers = prev_sig;
return signer;
}
Transaction.prototype.multiSignFor = function(account) {
this.addSignature(this.getSignatureFor(account));
return this;
}
Transaction.prototype.addSignature = function(signer) {
if (! Array.isArray(this.tx_json.Signers)) {
this.tx_json.Signers = [];
}
this.tx_json.Signers.push(signer);
if (this.tx_json.Signers.length > this._maxSignerNum) {
throw new Error('Maximum Number of Signatures Exceeded.');
}
this.sortSigners();
return this;
}
Transaction.prototype.sortSigners = function() {
if (! Array.isArray(this.tx_json.Signers)) return;
this.tx_json.Signers.sort(function(a,b){
return UInt160.from_json(a.Signer.Account)._value.greaterEquals(UInt160.from_json(b.Signer.Account)._value);
});
return this;
}
Transaction.prototype.clearSignatures = function() {
this.tx_json.Signers = []
return this;
}
Transaction.prototype.setMultiSign = function(sigNum) {
this._multiSign = true;
if (typeof sigNum === 'number') this._signerNum = sigNum;
return this;
}
Transaction.prototype.isMultiSign = function() {
return this._multiSign;
}
/**
* Add an ID to cached list of submitted IDs
*
* @param {String} transaction id
* @api private
*/
Transaction.prototype.addId = function (id) {
if (!lodash.contains(this.submittedIDs, id)) {
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 cachedTransactionID = lodash.detect(this.submittedIDs, function (id) {
return cache.hasOwnProperty(id);
});
return cache[cachedTransactionID];
};
/**
* Set client ID. This is an identifier specified by the user of the API to
* identify a transaction in the event of a disconnect. It is not currently
* persisted in the transaction itself, but used offline for identification.
* In applications that require high reliability, client-specified ID should
* be persisted such that one could map it to submitted transactions. Use
* .summary() for a consistent transaction summary output for persisitng. In
* the future, this ID may be stored in the transaction itself (in the ledger)
*
* @param {String} id
*/
Transaction.prototype.setClientID = Transaction.prototype.clientID = function (id) {
if (typeof id === 'string') {
this._clientID = id;
}
return this;
};
/**
* Set LastLedgerSequence as the absolute last ledger sequence the transaction
* is valid for. LastLedgerSequence is set automatically if not set using this
* method
*
* @param {Number} ledger index
*/
Transaction.prototype.setLastLedgerSequence = Transaction.prototype.setLastLedger = Transaction.prototype.lastLedger = function (sequence) {
this._setUInt32('LastLedgerSequence', sequence);
this._setLastLedger = true;
return this;
};
/**
* Set max fee. The maximum fee to be paid during high load condition.
* Specified fee must be >= 0.
*
* @param {Number} fee The proposed fee
*/
Transaction.prototype.setMaxFee = 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;
};
/**
* Set secret If the secret has been set with Remote.setSecret, it does not
* need to be provided
*
* @param {String} secret
*/
Transaction.prototype.setSecret = Transaction.prototype.secret = function (secret) {
if (typeof secret === 'string') {
this._secret = secret;
}
return this;
};
Transaction.prototype.setType = function (type) {
if (lodash.isUndefined(Transaction.formats, type)) {
throw new Error('TransactionType must be a valid transaction type');
}
this.tx_json.TransactionType = type;
return this;
};
Transaction.prototype._setUInt32 = function (name, value, options_) {
var options = lodash.merge({}, options_);
var isValidUInt32 = typeof value === 'number' && value >= 0 && value < Math.pow(256, 4);
if (!isValidUInt32) {
throw new Error(name + ' must be a valid UInt32');
}
if (!lodash.isUndefined(options.min_value) && value < options.min_value) {
throw new Error(name + ' must be >= ' + options.min_value);
}
this.tx_json[name] = value;
return this;
};
/**
* Set SourceTag
*
* @param {Number} source tag
*/
Transaction.prototype.setSourceTag = Transaction.prototype.sourceTag = function (tag) {
return this._setUInt32('SourceTag', tag);
};
Transaction.prototype._setAccount = function (name, value) {
var uInt160 = UInt160.from_json(value);
if (!uInt160.is_valid()) {
throw new Error(name + ' must be a valid account');
}
this.tx_json[name] = uInt160.to_json();
return this;
};
Transaction.prototype.setAccount = function (account) {
return this._setAccount('Account', account);
};
Transaction.prototype._setAmount = function (name, amount, options_) {
var options = lodash.merge({ no_native: false }, options_);
var parsedAmount = Amount.from_json(amount);
if (parsedAmount.is_negative()) {
throw new Error(name + ' value must be non-negative');
}
var isNative = parsedAmount.currency().is_native();
if (isNative && options.no_native) {
throw new Error(name + ' must be a non-native amount');
}
if (!(isNative || parsedAmount.currency().is_valid())) {
throw new Error(name + ' must have a valid currency');
}
if (!(isNative || parsedAmount.issuer().is_valid())) {
throw new Error(name + ' must have a valid issuer');
}
this.tx_json[name] = parsedAmount.to_json();
return this;
};
Transaction.prototype._setHash256 = function (name, value, options_) {
if (typeof value !== 'string') {
throw new Error(name + ' must be a valid Hash256');
}
var options = lodash.merge({ pad: false }, options_);
var hash256 = value;
if (options.pad) {
while (hash256.length < 64) {
hash256 += '0';
}
}
if (!/^[0-9A-Fa-f]{64}$/.test(hash256)) {
throw new Error(name + ' must be a valid Hash256');
}
this.tx_json[name] = hash256;
return this;
};
Transaction.prototype.setAccountTxnID = Transaction.prototype.accountTxnID = function (id) {
return this._setHash256('AccountTxnID', id);
};
/**
* 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 === undefined) {
return this;
}
if (typeof flags === 'number') {
this.tx_json.Flags = flags;
return this;
}
var transaction_flags = Transaction.flags[this.getType()] || {};
var flag_set = Array.isArray(flags) ? flags : [].slice.call(arguments);
for (var i = 0, l = flag_set.length; i < l; i++) {
var flag = flag_set[i];
if (transaction_flags.hasOwnProperty(flag)) {
this.tx_json.Flags += transaction_flags[flag];
} else {
// XXX Should throw?
this.emit('error', new RippleError('tejInvalidFlag'));
return this;
}
}
return this;
};
/**
* Add a Memo to transaction.
*
* @param [String] memoType
* - describes what the data represents, must contain valid URL characters
* @param [String] memoFormat
* - describes what format the data is in, MIME type, must contain valid URL
* - characters
* @param [String] memoData
* - data for the memo, can be any JS object. Any object other than string will
* be stringified (JSON) for transport
*/
Transaction.prototype.addMemo = function (options_) {
var options = undefined;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
} else {
options = {
memoType: arguments[0],
memoFormat: arguments[1],
memoData: arguments[2]
};
}
var memo = {};
var memoRegex = Transaction.MEMO_REGEX;
var memoType = options.memoType;
var memoFormat = options.memoFormat;
var memoData = options.memoData;
if (memoType) {
if (!(lodash.isString(memoType) && memoRegex.test(memoType))) {
throw new Error('MemoType must be a string containing only valid URL characters');
}
if (Transaction.MEMO_TYPES[memoType]) {
// XXX Maybe in the future we want a schema validator for
// memo types
memoType = Transaction.MEMO_TYPES[memoType];
}
memo.MemoType = utils.convertStringToHex(memoType);
}
if (memoFormat) {
if (!(lodash.isString(memoFormat) && memoRegex.test(memoFormat))) {
throw new Error('MemoFormat must be a string containing only valid URL characters');
}
memo.MemoFormat = utils.convertStringToHex(memoFormat);
}
if (memoData) {
if (typeof memoData !== 'string') {
if (lodash.isString(memoFormat) && memoFormat.toLowerCase() == 'json') {
try {
memoData = JSON.stringify(memoData);
} catch (e) {
throw new Error('MemoFormat json with invalid JSON in MemoData field');
}
} else {
throw new Error('MemoData can only be a JSON object with a valid json MemoFormat');
}
}
if (lodash.isString(memoFormat) && memoFormat.toLowerCase() == 'hex') {
// no need to convert data that's already in hex
if (/^[0-9a-fA-F]+$/.test(memoData)){
memo.MemoData = memoData;
} else {
throw new Error('MemoFormat hex with invalid Hex String in MemoData field');
}
} else {
memo.MemoData = utils.convertStringToHex(memoData);
}
}
this.tx_json.Memos = (this.tx_json.Memos || []).concat({ Memo: memo });
return this;
};
/**
* Construct an 'AccountSet' transaction
*
* Note that bit flags can be set using the .setFlags() method but for
* 'AccountSet' transactions there is an additional way to modify AccountRoot
* flags. The values available for the SetFlag and ClearFlag are as follows:
*
* asfRequireDest: Require a destination tag
* asfRequireAuth: Authorization is required to extend trust
* asfDisallowXRP: XRP should not be sent to this account
* asfDisableMaster: Disallow use of the master key
* asfNoFreeze: Permanently give up the ability to freeze individual
* trust lines. This flag can never be cleared.
* asfGlobalFreeze: Freeze all assets issued by this account
*
* @param [String] set flag
* @param [String] clear flag
*/
Transaction.prototype.accountSet = function (options_) {
var options = undefined;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
if (lodash.isUndefined(options.account)) {
options.account = options.src;
}
if (lodash.isUndefined(options.set_flag)) {
options.set_flag = options.set;
}
if (lodash.isUndefined(options.clear_flag)) {
options.clear_flag = options.clear;
}
} else {
options = {
account: arguments[0],
set_flag: arguments[1],
clear_flag: arguments[2]
};
}
this.setType('AccountSet');
this.setAccount(options.account);
if (!lodash.isUndefined(options.set_flag)) {
this.setSetFlag(options.set_flag);
}
if (!lodash.isUndefined(options.clear_flag)) {
this.setClearFlag(options.clear_flag);
}
return this;
};
Transaction.prototype.setAccountSetFlag = function (name, value) {
// if (this.getType() !== 'AccountSet') {
// throw new Error('TransactionType must be AccountSet to use ' + name);
// }
var accountSetFlags = Transaction.set_clear_flags.AccountSet;
var flagValue = value;
if (typeof flagValue === 'string') {
flagValue = /^asf/.test(flagValue) ? accountSetFlags[flagValue] : accountSetFlags['asf' + flagValue];
}
if (!lodash.contains(lodash.values(accountSetFlags), flagValue)) {
throw new Error(name + ' must be a valid AccountSet flag');
}
this.tx_json[name] = flagValue;
return this;
};
Transaction.prototype.setSetFlag = function (flag) {
return this.setAccountSetFlag('SetFlag', flag);
};
Transaction.prototype.setClearFlag = function (flag) {
return this.setAccountSetFlag('ClearFlag', flag);
};
/**
* Set TransferRate for AccountSet
*
* @param {Number} transfer rate
*/
Transaction.prototype.setTransferRate = Transaction.prototype.transferRate = function (rate) {
/* eslint-disable max-len */
// if (this.getType() !== 'AccountSet') {
// throw new Error('TransactionType must be AccountSet to use TransferRate');
// }
/* eslint-enable max-len */
var transferRate = rate;
if (transferRate === 0) {
// Clear TransferRate
this.tx_json.TransferRate = transferRate;
return this;
}
// if (rate >= 1 && rate < 2) {
// transferRate *= 1e9;
// }
return this._setUInt32('TransferRate', transferRate, { min_value: 1e9 });
};
/**
* Construct a 'SetRegularKey' transaction
*
* If the RegularKey is set, the private key that corresponds to it can be
* used to sign transactions instead of the master key
*
* The RegularKey must be a valid Ripple Address, or a Hash160 of the public
* key corresponding to the new private signing key.
*
* @param {String} account
* @param {String} regular key
*/
Transaction.prototype.setRegularKey = function (options_) {
var options = undefined;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
if (lodash.isUndefined(options.account)) {
options.account = options.src;
}
} else {
options = {
account: arguments[0],
regular_key: arguments[1]
};
}
this.setType('SetRegularKey');
this.setAccount(options.account);
if (!lodash.isUndefined(options.regular_key)) {
this._setAccount('RegularKey', options.regular_key);
}
return this;
};
/**
* Construct a 'SignerListSet' transaction
*
* @param {String} account
* @param {Array} signers object in the form {address, weight}
* @param {Number} quorum
*/
Transaction.prototype.signerListSet = function (options_) {
var options = undefined;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
if (lodash.isUndefined(options.account)) {
options.account = options.src;
}
} else {
options = {
account: arguments[0],
signers: arguments[1],
quorum: arguments[2]
};
}
this.setType('SignerListSet');
this.setAccount(options.account);
function formatSignerEntry (signer) {
var uInt160 = UInt160.from_json(signer.address);
if (!uInt160.is_valid()) {
throw new Error('signer.address must be a valid account');
}
var value = signer.weight;
var isValidUInt32 = typeof value === 'number' && value >= 0 && value < Math.pow(256, 4);
if (!isValidUInt32) {
throw new Error('signer.weight must be a valid UInt32');
}
return {
SignerEntry: {
Account: uInt160.to_json(),
SignerWeight: value
}
}
}
if (!lodash.isUndefined(options.signers) && options.quorum) {
if (options.signers.length > 8) throw new Error('signer members must not more 8.');
this.tx_json.SignerEntries = lodash.map(options.signers, formatSignerEntry);
}
if (!lodash.isUndefined(options.quorum)) {
this._setUInt32('SignerQuorum', options.quorum);
}
return this;
};
/**
* Construct a 'TrustSet' transaction
*
* @param {String} account
* @param [Amount] limit
* @param [Number] quality in
* @param [Number] quality out
*/
Transaction.prototype.trustSet = Transaction.prototype.rippleLineSet = function (options_) {
var options = undefined;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
if (lodash.isUndefined(options.account)) {
options.account = options.src;
}
} else {
options = {
account: arguments[0],
limit: arguments[1],
quality_in: arguments[2],
quality_out: arguments[3]
};
}
this.setType('TrustSet');
this.setAccount(options.account);
if (!lodash.isUndefined(options.limit)) {
this.setLimit(options.limit);
}
if (!lodash.isUndefined(options.quality_in)) {
this.setQualityIn(options.quality_in);
}
if (!lodash.isUndefined(options.quality_out)) {
this.setQualityOut(options.quality_out);
}
// XXX Throw an error if nothing is set.
return this;
};
Transaction.prototype.setLimit = function (amount) {
// if (this.getType() !== 'TrustSet') {
// throw new Error('TransactionType must be TrustSet to use LimitAmount');
// }
return this._setAmount('LimitAmount', amount, { no_native: true });
};
Transaction.prototype.setQualityIn = function (quality) {
// if (this.getType() !== 'TrustSet') {
// throw new Error('TransactionType must be TrustSet to use QualityIn');
// }
return this._setUInt32('QualityIn', quality);
};
Transaction.prototype.setQualityOut = function (quality) {
// if (this.getType() !== 'TrustSet') {
// throw new Error('TransactionType must be TrustSet to use QualityOut');
// }
return this._setUInt32('QualityOut', quality);
};
/**
* Construct a 'Payment' transaction
*
* Relevant setters:
* - setPaths()
* - setBuildPath()
* - addPath()
* - setSourceTag()
* - setDestinationTag()
* - setSendMax()
* - setFlags()
*
* @param {String} source account
* @param {String} destination account
* @param {Amount} payment amount
*/
Transaction.prototype.payment = function (options_) {
var options = undefined;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
if (lodash.isUndefined(options.account)) {
options.account = options.src || options.from;
}
if (lodash.isUndefined(options.destination)) {
options.destination = options.dst || options.to;
}
} else {
options = {
account: arguments[0],
destination: arguments[1],
amount: arguments[2]
};
}
this.setType('Payment');
this.setAccount(options.account);
this.setDestination(options.destination);
this.setAmount(options.amount);
return this;
};
/**
* Construct a 'EscrowCreate' transaction
* @param {String} source account
* @param {String} destination account
* @param {Amount} payment amount
* @param {String} condition
* @param [Number|Date] cancel after
* @param [Number|Date] finish after
*/
Transaction.prototype.escrowCreate = function (options_) {
var options = undefined;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
if (lodash.isUndefined(options.account)) {
options.account = options.src || options.from;
}
if (lodash.isUndefined(options.destination)) {
options.destination = options.dst || options.to;
}
} else {
options = {
account: arguments[0],
destination: arguments[1],
amount: arguments[2],
condition: arguments[3],
cancel_after: arguments[4],
finish_after: arguments[5],
};
}
this.setType('EscrowCreate');
this.setAccount(options.account);
this.setDestination(options.destination);
this.setAmount(options.amount);
var dtag = options.destination_tag || options.dtag;
if (!lodash.isUndefined(dtag)) {
this._.setDestinationTag(dtag);
}
if (!lodash.isUndefined(options.condition)) {
this.tx_json.Condition = options.condition;
}
if (!lodash.isUndefined(options.cancel_after)) {
this._setTime('CancelAfter', options.cancel_after);
}
if (!lodash.isUndefined(options.finish_after)) {
this._setTime('FinishAfter', options.finish_after);
}
return this;
};
/**
* Construct a 'EscrowFinish' transaction
* @param {String} account
* @param {String} owner account of the escrow
* @param [Number] sequence of the txn that create the escrow
* @param {String} condition
* @param {String} fulfillment
*/
Transaction.prototype.escrowFinish = function (options_) {
var options = undefined;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
if (lodash.isUndefined(options.offer_sequence)) {
options.offer_sequence = options.sequence || options.seq;
}
} else {
options = {
account: arguments[0],
owner: arguments[1],
offer_sequence: arguments[2],
condition: arguments[3],
fulfillment: arguments[4],
};
}
this.setType('EscrowFinish');
this.setAccount(options.account);
this.setOwner(options.owner);
this.setOfferSequence(options.offer_sequence);
if (!lodash.isUndefined(options.condition)) {
this.tx_json.Condition = options.condition;
}
if (!lodash.isUndefined(options.fulfillment)) {
this.tx_json.Fulfillment = options.fulfillment;
}
return this;
};
/**
* Construct a 'EscrowCancel' transaction
* @param {String} account
* @param {String} owner account of the escrow
* @param [Number] sequence of the txn that create the escrow
*/
Transaction.prototype.escrowCancel = function (options_) {
var options = undefined;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
if (lodash.isUndefined(options.offer_sequence)) {
options.offer_sequence = options.sequence || options.seq;
}
} else {
options = {
account: arguments[0],
owner: arguments[1],
offer_sequence: arguments[2],
};
}
this.setType('EscrowCancel');
this.setAccount(options.account);
this.setOwner(options.owner);
this.setOfferSequence(options.offer_sequence);
return this;
};
Transaction.prototype.setOwner = function (account) {
return this._setAccount('Owner', account);
};
Transaction.prototype.setAmount = function (amount) {
// if (this.getType() !== 'Payment') {
// throw new Error('TransactionType must be Payment to use SendMax');
// }
return this._setAmount('Amount', amount);
};
Transaction.prototype.setDestination = function (destination) {
// if (this.getType() !== 'Payment') {
// throw new Error('TransactionType must be Payment to use Destination');
// }
return this._setAccount('Destination', destination);
};
/**
* Set SendMax for Payment
*
* @param {String|Object} send max amount
*/
Transaction.prototype.setSendMax = Transaction.prototype.sendMax = function (send_max) {
// if (this.getType() !== 'Payment') {
// throw new Error('TransactionType must be Payment to use SendMax');
// }
return this._setAmount('SendMax', send_max);
};
/**
* 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) {
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
*
* @param {Array} path
*/
Transaction.prototype.addPath = Transaction.prototype.pathAdd = function (path) {
if (!Array.isArray(path)) {
throw new Error('Path must be an array');
}
// Path must not be empty array
if (path.length === 0) {
return this;
}
this.tx_json.Paths = this.tx_json.Paths || [];
this.tx_json.Paths.push(Transaction._rewritePath(path));
return this;
};
/**
* Set paths for Payment transaction
*
* @param {Array} paths
*/
Transaction.prototype.setPaths = Transaction.prototype.paths = function (paths) {
if (!Array.isArray(paths)) {
throw new Error('Paths must be an array');
}
// if (this.getType() !== 'Payment') {
// throw new Error('TransactionType must be Payment to use Paths');
// }
this.tx_json.Paths = [];
paths.forEach(this.addPath, this);
return this;
};
/**
* Set build_path to have server blindly construct a path for Payment
*
* "blindly" because the sender has no idea of the actual cost must be less
* than send max.
*
* @param {Boolean} build path
*/
Transaction.prototype.setBuildPath = Transaction.prototype.buildPath = function (build) {
// if (this.getType() !== 'Payment') {
// throw new Error('TransactionType must be Payment to use build_path');
// }
this._build_path = build === undefined || build;
return this;
};
/**
* Set DestinationTag for Payment transaction
*
* @param {Number} destination tag
*/
Transaction.prototype.setDestinationTag = Transaction.prototype.destinationTag = function (tag) {
// if (this.getType() !== 'Payment') {
// throw new Error('TransactionType must be Payment to use DestinationTag');
// }
return this._setUInt32('DestinationTag', tag);
};
/**
* Set InvoiceID for Payment transaction
*
* @param {String} id
*/
Transaction.prototype.setInvoiceID = Transaction.prototype.invoiceID = function (id) {
// if (this.getType() !== 'Payment') {
// throw new Error('TransactionType must be Payment to use InvoiceID');
// }
return this._setHash256('InvoiceID', id, { pad: true });
};
/**
* Construct an 'OfferCreate transaction
*
* @param {String} account
* @param {Amount} taker pays amount
* @param {Amount} taker gets amount
* @param [Number|Date] expiration
* @param [Number] sequence of an existing offer to replace
*/
Transaction.prototype.offerCreate = function (options_) {
var options = undefined;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
if (lodash.isUndefined(options.account)) {
options.account = options.src;
}
if (lodash.isUndefined(options.taker_pays)) {
options.taker_pays = options.buy;
}
if (lodash.isUndefined(options.taker_gets)) {
options.taker_gets = options.sell;
}
if (lodash.isUndefined(options.offer_sequence)) {
options.offer_sequence = options.cancel_sequence || options.sequence;
}
} else {
options = {
account: arguments[0],
taker_pays: arguments[1],
taker_gets: arguments[2],
expiration: arguments[3],
offer_sequence: arguments[4]
};
}
this.setType('OfferCreate');
this.setAccount(options.account);
this.setTakerGets(options.taker_gets);
this.setTakerPays(options.taker_pays);
if (!lodash.isUndefined(options.expiration)) {
this.setExpiration(options.expiration);
}
if (!lodash.isUndefined(options.offer_sequence)) {
this.setOfferSequence(options.offer_sequence);
}
return this;
};
Transaction.prototype.setTakerGets = function (amount) {
// if (this.getType() !== 'OfferCreate') {
// throw new Error('TransactionType must be OfferCreate to use TakerGets');
// }
return this._setAmount('TakerGets', amount);
};
Transaction.prototype.setTakerPays = function (amount) {
// if (this.getType() !== 'OfferCreate') {
// throw new Error('TransactionType must be OfferCreate to use TakerPays');
// }
return this._setAmount('TakerPays', amount);
};
Transaction.prototype.setExpiration = function (expiration) {
return this._setTime('Expiration', expiration);
};
Transaction.prototype._setTime = function (fieldname, time) {
return this._setUInt32(fieldname, utils.time.toRipple(time));
};
Transaction.prototype.setOfferSequence = function (offerSequence) {
/* eslint-disable max-len */
// if (!/^Offer(Cancel|Create)$/.test(this.getType())) {
// throw new Error(
// 'TransactionType must be OfferCreate or OfferCancel to use OfferSequence'
// );
// }
/* eslint-enable max-len */
return this._setUInt32('OfferSequence', offerSequence);
};
/**
* Construct an 'OfferCancel' transaction
*
* @param {String} account
* @param [Number] sequence of an existing offer
*/
Transaction.prototype.offerCancel = function (options_) {
var options = undefined;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
if (lodash.isUndefined(options.account)) {
options.account = options.src;
}
if (lodash.isUndefined(options.offer_sequence)) {
options.offer_sequence = options.sequence || options.cancel_sequence;
}
} else {
options = {
account: arguments[0],
offer_sequence: arguments[1]
};
}
this.setType('OfferCancel');
this.setAccount(options.account);
this.setOfferSequence(options.offer_sequence);
return this;
};
/**
* Submit transaction to the network
*
* @param [Function] callback
*/
Transaction.prototype.submit = function (callback) {
var self = this;
this.callback = typeof callback === 'function' ? callback : function () {};
this._errorHandler = function transactionError(error_, message) {
var error = error_;
if (!(error instanceof RippleError)) {
error = new RippleError(error, message);
}
self.callback(error);
};
this._successHandler = function transactionSuccess(message) {
self.callback(null, message);
};
if (!this.remote) {
this.emit('error', new Error('No remote found'));
return this;
}
/* eslint-disable max-len */
// if (this.state !== 'unsubmitted') {
// this.emit('error', new Error('Attempt to submit transaction more than once'));
// return;
// }
/* eslint-enable max-len */
this.getManager().submit(this);
return this;
};
Transaction.prototype.abort = function () {
if (!this.finalized) {
this.emit('error', new RippleError('tejAbort', 'Transaction aborted'));
}
return this;
};
/**
* Return summary object containing important information for persistence
*
* @return {Object} transaction summary
*/
Transaction.prototype.getSummary = Transaction.prototype.summary = function () {
var txSummary = {
tx_json: this.tx_json,
clientID: this._clientID,
submittedIDs: this.submittedIDs,
submissionAttempts: this.attempts,
submitIndex: this.submitIndex,
initialSubmitIndex: this.initialSubmitIndex,
lastLedgerSequence: this.lastLedgerSequence,
state: this.state,
finalized: this.finalized
};
if (this.result) {
var transaction_hash = this.result.tx_json ? this.result.tx_json.hash : undefined;
txSummary.result = {
engine_result: this.result.engine_result,
engine_result_message: this.result.engine_result_message,
ledger_hash: this.result.ledger_hash,
ledger_index: this.result.ledger_index,
transaction_hash: transaction_hash
};
}
return txSummary;
};
exports.Transaction = Transaction;
// vim:sw=2:sts=2:ts=8:et