Merge pull request #545 from wltsmrz/add-multisign

Add multisign helpers
This commit is contained in:
sublimator
2015-09-13 15:10:19 +07:00
8 changed files with 439 additions and 104 deletions

View File

@@ -1,6 +1,8 @@
'use strict';
/*eslint no-multi-spaces:0,space-in-brackets:0,key-spacing:0,comma-spacing:0*/
/*eslint-disable max-len,spaced-comment,array-bracket-spacing,key-spacing*/
/*eslint-disable no-multi-spaces,comma-spacing*/
/*eslint-disable no-multi-spaces:0,space-in-brackets:0,key-spacing:0,comma-spacing:0*/
/**
* Data type map.
@@ -52,7 +54,8 @@ const FIELDS_MAP = exports.fields = {
// Common types
1: { // Int16
1: 'LedgerEntryType',
2: 'TransactionType'
2: 'TransactionType',
3: 'SignerWeight'
},
2: { // Int32
2: 'Flags',
@@ -87,7 +90,9 @@ const FIELDS_MAP = exports.fields = {
31: 'ReserveBase',
32: 'ReserveIncrement',
33: 'SetFlag',
34: 'ClearFlag'
34: 'ClearFlag',
35: 'SignerQuorum',
38: 'SignerListID'
},
3: { // Int64
1: 'IndexNext',
@@ -166,13 +171,15 @@ const FIELDS_MAP = exports.fields = {
7: 'FinalFields',
8: 'NewFields',
9: 'TemplateEntry',
10: 'Memo'
10: 'Memo',
11: 'SignerEntry',
16: 'Signer'
},
15: { // Array
1: undefined, // end of Array
2: 'SigningAccounts',
3: 'TxnSignatures',
4: 'Signatures',
3: 'Signers',
4: 'SignerEntries',
5: 'Template',
6: 'Necessary',
7: 'Sufficient',
@@ -202,7 +209,7 @@ const FIELDS_MAP = exports.fields = {
}
};
let INVERSE_FIELDS_MAP = exports.fieldsInverseMap = { };
const INVERSE_FIELDS_MAP = exports.fieldsInverseMap = { };
Object.keys(FIELDS_MAP).forEach(function(k1) {
Object.keys(FIELDS_MAP[k1]).forEach(function(k2) {
@@ -211,8 +218,8 @@ Object.keys(FIELDS_MAP).forEach(function(k1) {
});
const REQUIRED = exports.REQUIRED = 0,
OPTIONAL = exports.OPTIONAL = 1,
DEFAULT = exports.DEFAULT = 2;
OPTIONAL = exports.OPTIONAL = 1,
DEFAULT = exports.DEFAULT = 2;
const base = [
[ 'TransactionType' , REQUIRED ],
@@ -226,7 +233,8 @@ const base = [
[ 'SigningPubKey' , REQUIRED ],
[ 'TxnSignature' , OPTIONAL ],
[ 'AccountTxnID' , OPTIONAL ],
[ 'Memos' , OPTIONAL ]
[ 'Memos' , OPTIONAL ],
[ 'Signers' , OPTIONAL ]
];
exports.tx = {
@@ -296,6 +304,10 @@ exports.tx = {
]),
TicketCancel: [11].concat(base, [
[ 'TicketID' , REQUIRED ]
]),
SignerListSet: [12].concat(base, [
['SignerQuorum', REQUIRED],
['SignerEntries', OPTIONAL]
])
};
@@ -396,7 +408,15 @@ exports.ledger = {
['LedgerIndex', OPTIONAL],
['Balance', REQUIRED],
['LowLimit', REQUIRED],
['HighLimit', REQUIRED]])
['HighLimit', REQUIRED]]),
SignerList: [83].concat(sleBase,[
['OwnerNode', REQUIRED],
['SignerQuorum', REQUIRED],
['SignerEntries', REQUIRED],
['SignerListID', REQUIRED],
['PreviousTxnID', REQUIRED],
['PreviousTxnLgrSeq', REQUIRED]
])
};
exports.metadata = [

View File

@@ -30,6 +30,8 @@ exports.HASH_LEAF_NODE = 0x4D4C4E00; // 'MLN'
exports.HASH_TX_SIGN = 0x53545800; // 'STX'
// inner transaction to sign (TESTNET)
exports.HASH_TX_SIGN_TESTNET = 0x73747800; // 'stx'
// inner transaction to multisign
exports.HASH_TX_MULTISIGN = 0x534D5400; // 'SMT'
Object.keys(exports).forEach(function(k) {
exports[k + '_BYTES'] = toBytes(exports[k]);

View File

@@ -24,7 +24,8 @@ exports._test = {
Log: require('./log'),
PathFind: require('./pathfind').PathFind,
TransactionManager: require('./transactionmanager').TransactionManager,
RangeSet: require('./rangeset').RangeSet
RangeSet: require('./rangeset').RangeSet,
HashPrefixes: require('./hashprefixes')
};
exports.types = require('./serializedtypes');

View File

@@ -2275,7 +2275,8 @@ Remote.prototype.createTransaction = function(type, options = {}) {
TrustSet: transaction.trustSet,
OfferCreate: transaction.offerCreate,
OfferCancel: transaction.offerCancel,
SetRegularKey: transaction.setRegularKey
SetRegularKey: transaction.setRegularKey,
SignerListSet: transaction.setSignerList
};
const transactionConstructor = constructorMap[type];

View File

@@ -1,7 +1,8 @@
'use strict';
const assert = require('assert');
const util = require('util');
const lodash = require('lodash');
const _ = require('lodash');
const EventEmitter = require('events').EventEmitter;
const utils = require('./utils');
const sjclcodec = require('sjcl-codec');
@@ -9,6 +10,7 @@ const Amount = require('./amount').Amount;
const Currency = require('./amount').Currency;
const UInt160 = require('./amount').UInt160;
const Seed = require('./seed').Seed;
const KeyPair = require('ripple-keypairs').KeyPair;
const SerializedObject = require('./serializedobject').SerializedObject;
const RippleError = require('./rippleerror').RippleError;
const hashprefixes = require('./hashprefixes');
@@ -199,7 +201,7 @@ Transaction.from_json = function(j) {
Transaction.prototype.setJson =
Transaction.prototype.parseJson = function(v) {
this.tx_json = v;
this.tx_json = _.merge({}, v);
return this;
};
@@ -358,7 +360,6 @@ Transaction.prototype._computeFee = function() {
});
const midInd = Math.floor(fees.length / 2);
const median = fees.length % 2 === 0
? Math.floor(0.5 + (fees[midInd] + fees[midInd - 1]) / 2)
: fees[midInd];
@@ -377,56 +378,86 @@ Transaction.prototype._computeFee = function() {
* return `false`
*/
Transaction.prototype.err = function(error, errorMessage) {
this.emit('error', new RippleError(error, errorMessage));
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;
}
// Auto-fill the secret
this._secret = this._secret || this.getSecret();
if (_.isUndefined(this._secret)) {
return this.err('tejSecretUnknown', 'Missing secret');
}
if (!this._secret) {
this._secret = this.getSecret();
if (this.remote && !(this.remote.local_signing || this.remote.trusted)) {
return this.err(
'tejServerUntrusted',
'Attempt to give secret to untrusted server');
}
// Try to auto-fill the secret
if (!this._secret) {
this.emit('error', new RippleError('tejSecretUnknown', 'Missing secret'));
return false;
}
if (typeof this.tx_json.SigningPubKey === 'undefined') {
if (_.isUndefined(this.tx_json.SigningPubKey)) {
try {
const seed = Seed.from_json(this._secret);
const key = seed.get_key();
this.tx_json.SigningPubKey = key.pubKeyHex();
} catch(e) {
this.emit('error', new RippleError(
'tejSecretInvalid', 'Invalid secret'));
return false;
this.setSigningPubKey(this.getSigningPubKey());
} catch (e) {
return this.err('tejSecretInvalid', 'Invalid secret');
}
}
// 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) {
this.tx_json.Fee = this._computeFee();
if (!this.tx_json.Fee) {
this.emit('error', new RippleError('tejUnconnected'));
return false;
// Auto-fill transaction Fee
if (_.isUndefined(this.tx_json.Fee)) {
if (this.remote && (this.remote.local_fee || !this.remote.trusted)) {
const computedFee = this._computeFee();
if (!computedFee) {
// Unable to compute fee due to no connected servers
return this.err('tejUnconnected');
}
this.tx_json.Fee = computedFee;
}
}
if (Number(this.tx_json.Fee) > this._maxFee) {
this.emit('error', new RippleError(
'tejMaxFeeExceeded', 'Max fee exceeded'));
return false;
return this.err('tejMaxFeeExceeded', 'Max fee exceeded');
}
// Set canonical flag - this enables canonicalized signature checking
this.setCanonicalFlag();
return this.tx_json;
};
Transaction.prototype.getKeyPair = function(secret_) {
if (this._keyPair) {
return this._keyPair;
}
const secret = secret_ || this._secret;
assert(secret, 'Secret missing');
const keyPair = Seed.from_json(secret).get_key();
this._keyPair = keyPair;
return keyPair;
};
Transaction.prototype.getSigningPubKey = function(secret) {
return this.getKeyPair(secret).pubKeyHex();
};
Transaction.prototype.setSigningPubKey = function(key) {
if (_.isString(key)) {
this.tx_json.SigningPubKey = key;
} else if (key instanceof KeyPair) {
this.tx_json.SigningPubKey = key.pubKeyHex();
}
return this;
};
Transaction.prototype.setCanonicalFlag = function() {
if (this.remote && this.remote.local_signing && this.canonical) {
this.tx_json.Flags |= Transaction.flags.Universal.FullyCanonicalSig;
@@ -435,7 +466,7 @@ Transaction.prototype.complete = function() {
this.tx_json.Flags = this.tx_json.Flags >>> 0;
}
return this.tx_json;
return this;
};
Transaction.prototype.serialize = function() {
@@ -453,6 +484,14 @@ Transaction.prototype.signingData = function() {
return so;
};
Transaction.prototype.multiSigningData = function(account) {
const so = new SerializedObject();
so.append(hashprefixes.HASH_TX_MULTISIGN_BYTES);
so.parse_json(this.tx_json);
so.append(UInt160.from_json(account).to_bytes());
return so;
};
Transaction.prototype.hash = function(prefix_, asUINT256, serialized) {
let prefix;
@@ -470,7 +509,11 @@ Transaction.prototype.hash = function(prefix_, asUINT256, serialized) {
};
Transaction.prototype.sign = function() {
const seed = Seed.from_json(this._secret);
if (this.hasMultiSigners()) {
return this;
}
const keyPair = this.getKeyPair();
const prev_sig = this.tx_json.TxnSignature;
delete this.tx_json.TxnSignature;
@@ -483,9 +526,7 @@ Transaction.prototype.sign = function() {
return this;
}
const key = seed.get_key();
const hex = key.signHex(this.signingData().buffer);
this.tx_json.TxnSignature = hex;
this.tx_json.TxnSignature = keyPair.signHex(this.signingData().buffer);
this.previousSigningHash = hash;
return this;
@@ -499,7 +540,7 @@ Transaction.prototype.sign = function() {
*/
Transaction.prototype.addId = function(id) {
if (!lodash.contains(this.submittedIDs, id)) {
if (!_.contains(this.submittedIDs, id)) {
this.submittedIDs.unshift(id);
}
};
@@ -515,7 +556,7 @@ Transaction.prototype.addId = function(id) {
*/
Transaction.prototype.findId = function(cache) {
const cachedTransactionID = lodash.detect(this.submittedIDs, function(id) {
const cachedTransactionID = _.detect(this.submittedIDs, function(id) {
return cache.hasOwnProperty(id);
});
return cache[cachedTransactionID];
@@ -582,9 +623,24 @@ Transaction.prototype.maxFee = function(fee) {
* @returns {Transaction} calling instance for chaining
*/
Transaction.prototype.setFixedFee = function(fee) {
if (typeof fee === 'number' && fee >= 0) {
this._setFixedFee = true;
return this.setFee(fee, {fixed: true});
};
Transaction.prototype.setFee = function(fee, options = {}) {
if (_.isNumber(fee) && fee >= 0) {
this.tx_json.Fee = String(fee);
if (options.fixed) {
this._setFixedFee = true;
}
}
return this;
};
Transaction.prototype.setSequence = function(sequence) {
if (_.isNumber(sequence)) {
this._setUInt32('Sequence', sequence);
this._setSequence = true;
}
return this;
@@ -606,7 +662,7 @@ Transaction.prototype.secret = function(secret) {
};
Transaction.prototype.setType = function(type) {
if (lodash.isUndefined(Transaction.formats, type)) {
if (_.isUndefined(Transaction.formats, type)) {
throw new Error('TransactionType must be a valid transaction type');
}
@@ -616,14 +672,14 @@ Transaction.prototype.setType = function(type) {
};
Transaction.prototype._setUInt32 = function(name, value, options_) {
const options = lodash.merge({}, options_);
const options = _.merge({}, options_);
const 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) {
if (!_.isUndefined(options.min_value) && value < options.min_value) {
throw new Error(name + ' must be >= ' + options.min_value);
}
@@ -660,7 +716,7 @@ Transaction.prototype.setAccount = function(account) {
};
Transaction.prototype._setAmount = function(name, amount, options_) {
const options = lodash.merge({no_native: false}, options_);
const options = _.merge({no_native: false}, options_);
const parsedAmount = Amount.from_json(amount);
if (parsedAmount.is_negative()) {
@@ -689,7 +745,7 @@ Transaction.prototype._setHash256 = function(name, value, options_) {
throw new Error(name + ' must be a valid Hash256');
}
const options = lodash.merge({pad: false}, options_);
const options = _.merge({pad: false}, options_);
let hash256 = value;
if (options.pad) {
@@ -769,7 +825,7 @@ Transaction.prototype.addMemo = function(options_) {
let options;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
options = _.merge({}, options_);
} else {
options = {
memoType: arguments[0],
@@ -790,7 +846,7 @@ Transaction.prototype.addMemo = function(options_) {
let memoData = options.memoData;
if (memoType) {
if (!(lodash.isString(memoType) && memoRegex.test(memoType))) {
if (!(_.isString(memoType) && memoRegex.test(memoType))) {
throw new Error(
'MemoType must be a string containing only valid URL characters');
}
@@ -803,7 +859,7 @@ Transaction.prototype.addMemo = function(options_) {
}
if (memoFormat) {
if (!(lodash.isString(memoFormat) && memoRegex.test(memoFormat))) {
if (!(_.isString(memoFormat) && memoRegex.test(memoFormat))) {
throw new Error(
'MemoFormat must be a string containing only valid URL characters');
}
@@ -857,15 +913,15 @@ Transaction.prototype.accountSet = function(options_) {
let options;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
options = _.merge({}, options_);
if (lodash.isUndefined(options.account)) {
if (_.isUndefined(options.account)) {
options.account = options.src;
}
if (lodash.isUndefined(options.set_flag)) {
if (_.isUndefined(options.set_flag)) {
options.set_flag = options.set;
}
if (lodash.isUndefined(options.clear_flag)) {
if (_.isUndefined(options.clear_flag)) {
options.clear_flag = options.clear;
}
} else {
@@ -879,10 +935,10 @@ Transaction.prototype.accountSet = function(options_) {
this.setType('AccountSet');
this.setAccount(options.account);
if (!lodash.isUndefined(options.set_flag)) {
if (!_.isUndefined(options.set_flag)) {
this.setSetFlag(options.set_flag);
}
if (!lodash.isUndefined(options.clear_flag)) {
if (!_.isUndefined(options.clear_flag)) {
this.setClearFlag(options.clear_flag);
}
@@ -899,7 +955,7 @@ Transaction.prototype.setAccountSetFlag = function(name, value) {
: accountSetFlags['asf' + flagValue];
}
if (!lodash.contains(lodash.values(accountSetFlags), flagValue)) {
if (!_.contains(_.values(accountSetFlags), flagValue)) {
throw new Error(name + ' must be a valid AccountSet flag');
}
@@ -956,9 +1012,9 @@ Transaction.prototype.setRegularKey = function(options_) {
let options;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
options = _.merge({}, options_);
if (lodash.isUndefined(options.account)) {
if (_.isUndefined(options.account)) {
options.account = options.src;
}
} else {
@@ -971,7 +1027,7 @@ Transaction.prototype.setRegularKey = function(options_) {
this.setType('SetRegularKey');
this.setAccount(options.account);
if (!lodash.isUndefined(options.regular_key)) {
if (!_.isUndefined(options.regular_key)) {
this._setAccount('RegularKey', options.regular_key);
}
@@ -992,9 +1048,9 @@ Transaction.prototype.rippleLineSet = function(options_) {
let options;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
options = _.merge({}, options_);
if (lodash.isUndefined(options.account)) {
if (_.isUndefined(options.account)) {
options.account = options.src;
}
} else {
@@ -1009,13 +1065,13 @@ Transaction.prototype.rippleLineSet = function(options_) {
this.setType('TrustSet');
this.setAccount(options.account);
if (!lodash.isUndefined(options.limit)) {
if (!_.isUndefined(options.limit)) {
this.setLimit(options.limit);
}
if (!lodash.isUndefined(options.quality_in)) {
if (!_.isUndefined(options.quality_in)) {
this.setQualityIn(options.quality_in);
}
if (!lodash.isUndefined(options.quality_out)) {
if (!_.isUndefined(options.quality_out)) {
this.setQualityOut(options.quality_out);
}
@@ -1057,12 +1113,12 @@ Transaction.prototype.payment = function(options_) {
let options;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
options = _.merge({}, options_);
if (lodash.isUndefined(options.account)) {
if (_.isUndefined(options.account)) {
options.account = options.src || options.from;
}
if (lodash.isUndefined(options.destination)) {
if (_.isUndefined(options.destination)) {
options.destination = options.dst || options.to;
}
} else {
@@ -1241,18 +1297,18 @@ Transaction.prototype.offerCreate = function(options_) {
let options;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
options = _.merge({}, options_);
if (lodash.isUndefined(options.account)) {
if (_.isUndefined(options.account)) {
options.account = options.src;
}
if (lodash.isUndefined(options.taker_pays)) {
if (_.isUndefined(options.taker_pays)) {
options.taker_pays = options.buy;
}
if (lodash.isUndefined(options.taker_gets)) {
if (_.isUndefined(options.taker_gets)) {
options.taker_gets = options.sell;
}
if (lodash.isUndefined(options.offer_sequence)) {
if (_.isUndefined(options.offer_sequence)) {
options.offer_sequence = options.cancel_sequence || options.sequence;
}
} else {
@@ -1270,10 +1326,10 @@ Transaction.prototype.offerCreate = function(options_) {
this.setTakerGets(options.taker_gets);
this.setTakerPays(options.taker_pays);
if (!lodash.isUndefined(options.expiration)) {
if (!_.isUndefined(options.expiration)) {
this.setExpiration(options.expiration);
}
if (!lodash.isUndefined(options.offer_sequence)) {
if (!_.isUndefined(options.offer_sequence)) {
this.setOfferSequence(options.offer_sequence);
}
@@ -1311,12 +1367,12 @@ Transaction.prototype.offerCancel = function(options_) {
let options;
if (typeof options_ === 'object') {
options = lodash.merge({}, options_);
options = _.merge({}, options_);
if (lodash.isUndefined(options.account)) {
if (_.isUndefined(options.account)) {
options.account = options.src;
}
if (lodash.isUndefined(options.offer_sequence)) {
if (_.isUndefined(options.offer_sequence)) {
options.offer_sequence = options.sequence || options.cancel_sequence;
}
} else {
@@ -1333,16 +1389,48 @@ Transaction.prototype.offerCancel = function(options_) {
return this;
};
Transaction._prepareSignerEntry = function(signer) {
const {account, weight} = signer;
assert(UInt160.is_valid(account), 'Signer account invalid');
assert(_.isNumber(weight), 'Signer weight missing');
assert(weight > 0 && weight <= 65535, 'Signer weight must be 1-65535');
return {
SignerEntry: {
Account: account,
SignerWeight: weight
}
};
};
Transaction.prototype.setSignerList = function(options = {}) {
this.setType('SignerListSet');
this.setAccount(options.account);
this.setSignerQuorum(options.signerQuorum);
if (!_.isEmpty(options.signers)) {
this.tx_json.SignerEntries =
options.signers.map(Transaction._prepareSignerEntry);
}
return this;
};
Transaction.prototype.setSignerQuorum = function(quorum) {
this._setUInt32('SignerQuorum', quorum);
};
/**
* Submit transaction to the network
*
* @param [Function] callback
*/
Transaction.prototype.submit = function(callback) {
Transaction.prototype.submit = function(callback = function() {}) {
const self = this;
this.callback = (typeof callback === 'function') ? callback : function() {};
this.callback = callback;
this._errorHandler = function transactionError(error_, message) {
let error = error_;
@@ -1413,6 +1501,59 @@ Transaction.prototype.summary = function() {
return txSummary;
};
exports.Transaction = Transaction;
Transaction.prototype.setSigners = function(signers) {
if (_.isArray(signers)) {
this.tx_json.Signers = signers;
}
// vim:sw=2:sts=2:ts=8:et
return this;
};
Transaction.prototype.addMultiSigner = function(signer) {
assert(UInt160.is_valid(signer.Account), 'Signer must have a valid Account');
if (_.isUndefined(this.multi_signers)) {
this.multi_signers = [];
}
this.multi_signers.push({Signer: signer});
this.multi_signers.sort((a, b) => {
return UInt160.from_json(a.Signer.Account)
.cmp(UInt160.from_json(b.Signer.Account));
});
};
Transaction.prototype.hasMultiSigners = function() {
return !_.isEmpty(this.multi_signers);
};
Transaction.prototype.getMultiSigners = function() {
return this.multi_signers;
};
Transaction.prototype.getMultiSigningJson = function() {
assert(this.tx_json.Sequence, 'Sequence must be set before multi-signing');
assert(this.tx_json.Fee, 'Fee must be set before multi-signing');
const signingTx = Transaction.from_json(this.tx_json);
signingTx.remote = this.remote;
signingTx.setSigningPubKey('');
signingTx.setCanonicalFlag();
return signingTx.tx_json;
};
Transaction.prototype.multiSign = function(account, secret) {
const signingData = this.multiSigningData(account);
const keyPair = Seed.from_json(secret).get_key();
const signer = {
Account: account,
TxnSignature: keyPair.signHex(signingData.buffer),
SigningPubKey: keyPair.pubKeyHex()
};
return signer;
};
exports.Transaction = Transaction;

View File

@@ -1,5 +1,6 @@
'use strict';
const _ = require('lodash');
const util = require('util');
const assert = require('assert');
const async = require('async');
@@ -520,6 +521,17 @@ TransactionManager.prototype._resubmit = function(ledgers_, pending_) {
TransactionManager.prototype._prepareRequest = function(tx) {
const submitRequest = this._remote.requestSubmit();
if (tx.hasMultiSigners()) {
tx.setSigningPubKey('');
if (this._remote.local_signing) {
tx.setSigners(tx.getMultiSigners());
} else {
submitRequest.message.command = 'submit_multisigned';
submitRequest.message.Signers = tx.getMultiSigners();
}
}
if (this._remote.local_signing) {
tx.sign();
@@ -678,7 +690,7 @@ TransactionManager.prototype._request = function(tx) {
tx.initialSubmitIndex = tx.submitIndex;
}
if (!tx._setLastLedger) {
if (!tx._setLastLedger && !tx.hasMultiSigners()) {
// Honor LastLedgerSequence set with tx.lastLedger()
tx.tx_json.LastLedgerSequence = tx.initialSubmitIndex
+ this._lastLedgerOffset;
@@ -726,9 +738,13 @@ TransactionManager.prototype.submit = function(tx) {
return;
}
if (typeof tx.tx_json.Sequence !== 'number') {
if (!_.isNumber(tx.tx_json.Sequence)) {
// Honor manually-set sequences
tx.tx_json.Sequence = this._nextSequence++;
tx.setSequence(this._nextSequence++);
}
if (tx.hasMultiSigners()) {
tx.setResubmittable(false);
}
tx.once('cleanup', function() {

View File

@@ -2057,4 +2057,42 @@ describe('Remote', function() {
RegularKey: TX_JSON.Destination
});
});
it('Construct SignerListSet transaction', function() {
const tx = remote.createTransaction('SignerListSet', {
account: 'rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm',
signerQuorum: 3,
signers: [
{
account: 'rH4KEcG9dEwGwpn6AyoWK9cZPLL4RLSmWW',
weight: 1
},
{
account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK',
weight: 2
}
]
});
assert(tx instanceof Transaction);
assert.deepEqual(tx.tx_json, {
Flags: 0,
TransactionType: 'SignerListSet',
Account: 'rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm',
SignerQuorum: 3,
SignerEntries: [
{
SignerEntry: {
Account: 'rH4KEcG9dEwGwpn6AyoWK9cZPLL4RLSmWW',
SignerWeight: 1
}
},
{
SignerEntry: {
Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK',
SignerWeight: 2
}
}
]
});
});
});

View File

@@ -4,6 +4,7 @@
const assert = require('assert');
const lodash = require('lodash');
const ripple = require('ripple-lib');
const Transaction = require('ripple-lib').Transaction;
const TransactionQueue = require('ripple-lib').TransactionQueue;
const Remote = require('ripple-lib').Remote;
@@ -412,6 +413,7 @@ describe('Transaction', function() {
const remote = new Remote();
const transaction = new Transaction(remote);
transaction.setSecret('shK5kH88MZPaCpCNmzmzpLY58xKS9');
remote.trusted = false;
remote.local_signing = false;
@@ -445,7 +447,6 @@ describe('Transaction', function() {
remote.trusted = true;
remote.local_signing = true;
transaction.SigningPubKey = undefined;
transaction.tx_json.Account = 'rMWwx3Ma16HnqSd4H6saPisihX9aKpXxHJ';
transaction._secret = 'sh2pTicynUEG46jjR4EoexHcQEoijX';
transaction.once('error', function(err) {
@@ -571,8 +572,8 @@ describe('Transaction', function() {
tx.complete();
tx.sign();
assert.strictEqual(tx_json.SigningPubKey, expectedPub);
assert.strictEqual(tx_json.TxnSignature, expectedSig);
assert.strictEqual(tx.tx_json.SigningPubKey, expectedPub);
assert.strictEqual(tx.tx_json.TxnSignature, expectedSig);
});
});
@@ -1903,6 +1904,45 @@ describe('Transaction', function() {
});
});
it('Construct SignerListSet transaction', function() {
const transaction = new Transaction().setSignerList({
account: 'rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm',
signerQuorum: 3,
signers: [
{
account: 'rH4KEcG9dEwGwpn6AyoWK9cZPLL4RLSmWW',
weight: 1
},
{
account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK',
weight: 2
}
]
});
assert(transaction instanceof Transaction);
assert.deepEqual(transaction.tx_json, {
Flags: 0,
TransactionType: 'SignerListSet',
Account: 'rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm',
SignerQuorum: 3,
SignerEntries: [
{
SignerEntry: {
Account: 'rH4KEcG9dEwGwpn6AyoWK9cZPLL4RLSmWW',
SignerWeight: 1
}
},
{
SignerEntry: {
Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK',
SignerWeight: 2
}
}
]
});
});
it('Submit transaction', function(done) {
const remote = new Remote();
const transaction = new Transaction(remote).accountSet('r36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe');
@@ -2066,4 +2106,80 @@ describe('Transaction', function() {
queue.remove(tx);
});
});
it('Add multisigner', function() {
const transaction = new Transaction();
const s1 = {
Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK',
TxnSignature: '304402203020865BDC995431325C371E6A3CE89BFC40597D9CFAF77DBB16E9D159824EA402203645A6462A6DCEC7B5D0811882DC54CEA66258A227A2762BE6EFCD9EB62C27BF',
SigningPubKey: '02691AC5AE1C4C333AE5DF8A93BDC495F0EEBFC6DB0DA7EB6EF808F3AFC006E3FE'
};
const s2 = {
Account: 'rH4KEcG9dEwGwpn6AyoWK9cZPLL4RLSmWW',
TxnSignature: '30450221009C84E455DC199A7DB4B800D68C92269D60972E8850AFC0D50B1AE6B08BBB02EA02206FA93A560BE96844DF7D96D07F6400EF9534A32FBA352DD10E855DA8923A3AF8',
SigningPubKey: '028949021029D5CC87E78BCF053AFEC0CAFD15108EC119EAAFEC466F5C095407BF'
};
transaction.addMultiSigner(s1);
transaction.addMultiSigner(s2);
assert.deepEqual(transaction.getMultiSigners(), [
{Signer: s2}, {Signer: s1}]);
});
it('Get multisign data', function() {
const transaction = Transaction.from_json({
Account: 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn',
Sequence: 1,
Fee: '100',
TransactionType: 'AccountSet',
Flags: 0
});
transaction.setSigningPubKey('');
const a1 = 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK';
const d1 = transaction.multiSigningData(a1);
const tbytes = ripple.SerializedObject.from_json(
lodash.merge(transaction.tx_json, {SigningPubKey: ''})).buffer;
const abytes = ripple.UInt160.from_json(a1).to_bytes();
const prefix = require('ripple-lib')._test.HashPrefixes.HASH_TX_MULTISIGN_BYTES;
assert.deepEqual(d1.buffer, prefix.concat(tbytes, abytes));
});
it('Multisign', function() {
const transaction = Transaction.from_json({
Account: 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn',
Sequence: 1,
Fee: '100',
TransactionType: 'AccountSet',
Flags: 0
});
const multiSigningJson = transaction.getMultiSigningJson();
const t1 = Transaction.from_json(multiSigningJson);
const a1 = 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK';
const a2 = 'rH4KEcG9dEwGwpn6AyoWK9cZPLL4RLSmWW';
const s1 = t1.multiSign(a1, 'alice');
assert.strictEqual(s1.Account, a1);
assert.strictEqual(s1.SigningPubKey, '0388935426E0D08083314842EDFBB2D517BD47699F9A4527318A8E10468C97C052');
assert.strictEqual(s1.TxnSignature, '30440220611256E46B2946152695FFEF34D5C71BB3AE569C3D919A270BFBCA9ADF260D9202202FAE24FC8A575FE3265A6D7CFA596094A7950E0011706431A11C2A9ABEF60B3B');
const s2 = t1.multiSign(a2, 'bob');
assert.strictEqual(s2.Account, a2);
assert.strictEqual(s2.SigningPubKey, '02691AC5AE1C4C333AE5DF8A93BDC495F0EEBFC6DB0DA7EB6EF808F3AFC006E3FE');
assert.strictEqual(s2.TxnSignature, '3044022067F769BE0A4CC2B4F26E7B52B366F861FED02DA0F564F98B44009C8181A9655702206D882919139DF8E9D7F2FC1DD54D8B4FEAC40203349AE21519FD388925A4DE83');
transaction.addMultiSigner(s1);
transaction.addMultiSigner(s2);
assert.deepEqual(transaction.getMultiSigners(), [
{Signer: s2},
{Signer: s1}
]);
});
});