diff --git a/src/core/transaction.js b/src/core/transaction.js index 3685c17f..e36d9196 100644 --- a/src/core/transaction.js +++ b/src/core/transaction.js @@ -45,6 +45,7 @@ function Transaction(remote) { ? this.remote.automatic_resubmission : true; this._maxFee = remoteExists ? this.remote.max_fee : undefined; + this._lastLedgerOffset = remoteExists ? this.remote.last_ledger_offset : 3; this.state = 'unsubmitted'; this.finalized = false; this.previousSigningHash = undefined; @@ -582,19 +583,30 @@ Transaction.prototype.clientID = function(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.setLastLedgerSequenceOffset = function(offset) { + this._lastLedgerOffset = offset; +}; -Transaction.prototype.setLastLedgerSequence = +Transaction.prototype.getLastLedgerSequenceOffset = function() { + return this._lastLedgerOffset; +}; + +Transaction.prototype.lastLedger = Transaction.prototype.setLastLedger = -Transaction.prototype.lastLedger = function(sequence) { - this._setUInt32('LastLedgerSequence', sequence); +Transaction.prototype.setLastLedgerSequence = function(sequence) { + if (!_.isUndefined(sequence)) { + this._setUInt32('LastLedgerSequence', sequence); + } else { + // Autofill LastLedgerSequence + assert(this.remote, 'Unable to set LastLedgerSequence, missing Remote'); + + this._setUInt32('LastLedgerSequence', + this.remote.getLedgerSequence() + 1 + + this.getLastLedgerSequenceOffset()); + } + this._setLastLedger = true; + return this; }; @@ -1479,7 +1491,7 @@ Transaction.prototype.summary = function() { submissionAttempts: this.attempts, submitIndex: this.submitIndex, initialSubmitIndex: this.initialSubmitIndex, - lastLedgerSequence: this.lastLedgerSequence, + lastLedgerSequence: this.tx_json.LastLedgerSequence, state: this.state, finalized: this.finalized }; @@ -1616,30 +1628,44 @@ Transaction.prototype.setSigners = function(signers) { 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 = []; + if (_.isUndefined(this.tx_json.Signers)) { + this.tx_json.Signers = []; } - this.multi_signers.push({Signer: signer}); + this.tx_json.Signers.push({Signer: signer}); - this.multi_signers.sort((a, b) => { + this.tx_json.Signers.sort((a, b) => { return UInt160.from_json(a.Signer.Account) .cmp(UInt160.from_json(b.Signer.Account)); }); + + return this; }; Transaction.prototype.hasMultiSigners = function() { - return !_.isEmpty(this.multi_signers); + return !_.isEmpty(this.tx_json.Signers); }; + Transaction.prototype.getMultiSigners = function() { - return this.multi_signers; + return this.tx_json.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); + if (_.isUndefined(this.tx_json.LastLedgerSequence)) { + // Auto-fill LastLedgerSequence + this.setLastLedgerSequence(); + } + + const cleanedJson = _.omit(this.tx_json, [ + 'SigningPubKey', + 'Signers', + 'TxnSignature' + ]); + + const signingTx = Transaction.from_json(cleanedJson); signingTx.remote = this.remote; signingTx.setSigningPubKey(''); signingTx.setCanonicalFlag(); diff --git a/src/core/transactionmanager.js b/src/core/transactionmanager.js index dfcbd2bd..3406e30f 100644 --- a/src/core/transactionmanager.js +++ b/src/core/transactionmanager.js @@ -27,7 +27,6 @@ function TransactionManager(account) { this._maxFee = this._remote.max_fee; this._maxAttempts = this._remote.max_attempts; this._submissionTimeout = this._remote.submission_timeout; - this._lastLedgerOffset = this._remote.last_ledger_offset; this._pending = new PendingQueue(); this._account.on('transaction-outbound', function(res) { @@ -521,17 +520,6 @@ 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(); @@ -541,6 +529,10 @@ TransactionManager.prototype._prepareRequest = function(tx) { const hash = tx.hash(null, null, serialized); tx.addId(hash); } else { + if (tx.hasMultiSigners()) { + submitRequest.message.command = 'submit_multisigned'; + } + // ND: `build_path` is completely ignored when doing local signing as // `Paths` is a component of the signed blob, the `tx_blob` is signed, // sealed and delivered, and the txn unmodified. @@ -580,6 +572,11 @@ TransactionManager.prototype._request = function(tx) { return; } + if (Number(tx.tx_json.Fee) > tx._maxFee) { + tx.emit('error', new RippleError('tejMaxFeeExceeded')); + return; + } + if (remote.trace) { log.info('submit transaction:', tx.tx_json); } @@ -684,24 +681,12 @@ TransactionManager.prototype._request = function(tx) { } } - tx.submitIndex = this._remote._ledger_current_index; + tx.submitIndex = this._remote.getLedgerSequence() + 1; if (tx.attempts === 0) { tx.initialSubmitIndex = tx.submitIndex; } - if (!tx._setLastLedger && !tx.hasMultiSigners()) { - // Honor LastLedgerSequence set with tx.lastLedger() - tx.tx_json.LastLedgerSequence = tx.initialSubmitIndex - + this._lastLedgerOffset; - } - - tx.lastLedgerSequence = tx.tx_json.LastLedgerSequence; - - if (remote.local_signing) { - tx.sign(); - } - const submitRequest = this._prepareRequest(tx); submitRequest.once('error', submitted); submitRequest.once('success', submitted); @@ -743,8 +728,13 @@ TransactionManager.prototype.submit = function(tx) { tx.setSequence(this._nextSequence++); } + if (_.isUndefined(tx.tx_json.LastLedgerSequence)) { + tx.setLastLedgerSequence(); + } + if (tx.hasMultiSigners()) { tx.setResubmittable(false); + tx.setSigningPubKey(''); } tx.once('cleanup', function() { diff --git a/test/transaction-manager-test.js b/test/transaction-manager-test.js index 841eee37..7a083eec 100644 --- a/test/transaction-manager-test.js +++ b/test/transaction-manager-test.js @@ -518,7 +518,9 @@ describe('TransactionManager', function() { break; } }); + /* eslint-disable no-unused-vars */ rippled.once('request_submit', function(m, req) { + /* eslint-enable no-unused-vars */ req.sendJSON(lodash.extend({}, LEDGER, { ledger_index: transaction.tx_json.LastLedgerSequence + 1 })); @@ -574,7 +576,9 @@ describe('TransactionManager', function() { req.sendResponse(SUBMIT_TEF_RESPONSE, {id: m.id}); }); + /* eslint-disable no-unused-vars */ rippled.once('request_submit', function(m, req) { + /* eslint-enable no-unused-vars */ transaction.once('resubmitted', function() { receivedResubmitted = true; req.sendJSON(lodash.extend({}, LEDGER, { @@ -634,7 +638,9 @@ describe('TransactionManager', function() { req.sendResponse(SUBMIT_TEL_RESPONSE, {id: m.id}); }); + /* eslint-disable no-unused-vars */ rippled.once('request_submit', function(m, req) { + /* eslint-enable no-unused-vars */ transaction.once('resubmitted', function() { receivedResubmitted = true; req.sendJSON(lodash.extend({}, LEDGER, { @@ -690,7 +696,7 @@ describe('TransactionManager', function() { assert.strictEqual(summary.submissionAttempts, 0); assert.strictEqual(summary.submitIndex, undefined); assert.strictEqual(summary.initialSubmitIndex, undefined); - assert.strictEqual(summary.lastLedgerSequence, undefined); + assert.strictEqual(summary.lastLedgerSequence, remote.getLedgerSequence() + 1 + Remote.DEFAULTS.last_ledger_offset); assert.strictEqual(summary.state, 'failed'); assert.strictEqual(summary.finalized, true); assert.deepEqual(summary.result, { @@ -799,7 +805,9 @@ describe('TransactionManager', function() { req.sendResponse(SUBMIT_TEL_RESPONSE, {id: m.id}); }); + /* eslint-disable no-unused-vars */ rippled.once('request_submit', function(m, req) { + /* eslint-enable no-unused-vars */ transaction.once('resubmitted', function() { receivedResubmitted = true; }); @@ -857,6 +865,7 @@ describe('TransactionManager', function() { receivedSubmitted = true; }); + /* eslint-disable no-unused-vars */ rippled.on('request_submit', function(m, req) { assert.strictEqual(m.tx_blob, SerializedObject.from_json( transaction.tx_json).to_hex()); @@ -867,7 +876,9 @@ describe('TransactionManager', function() { req.sendResponse(SUBMIT_TOO_BUSY_ERROR, {id: m.id}); }); + /* eslint-disable no-unused-vars */ rippled.once('request_submit', function(m, req) { + /* eslint-enable no-unused-vars */ transaction.once('resubmitted', function() { receivedResubmitted = true; req.sendJSON(lodash.extend({}, LEDGER, { diff --git a/test/transaction-test.js b/test/transaction-test.js index 94b9d65f..f1865501 100644 --- a/test/transaction-test.js +++ b/test/transaction-test.js @@ -983,9 +983,6 @@ describe('Transaction', function() { it('Set LastLedgerSequence', function() { const transaction = new Transaction(); - assert.throws(function() { - transaction.lastLedger('a'); - }, /Error: LastLedgerSequence must be a valid UInt32/); assert.throws(function() { transaction.setLastLedgerSequence('a'); }, /Error: LastLedgerSequence must be a valid UInt32/); @@ -1945,6 +1942,8 @@ describe('Transaction', function() { it('Submit transaction', function(done) { const remote = new Remote(); + remote._ledger_current_index = 1; + const transaction = new Transaction(remote).accountSet('r36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe'); assert.strictEqual(transaction.callback, undefined); @@ -1989,6 +1988,8 @@ describe('Transaction', function() { it('Submit transaction - submission error', function(done) { const remote = new Remote(); + remote._ledger_current_index = 1; + const transaction = new Transaction(remote).accountSet('r36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe'); const account = remote.addAccount('r36xtKNKR43SeXnGn7kN4r4JdQzcrkqpWe'); @@ -2107,82 +2108,6 @@ describe('Transaction', function() { }); }); - 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} - ]); - }); - it('Construct SuspendedPaymentCreate transaction', function() { const transaction = new Transaction().suspendedPaymentCreate({ account: 'rsLEU1TPdCJPPysqhWYw9jD97xtG5WqSJm', @@ -2294,4 +2219,101 @@ describe('Transaction', function() { OfferSequence: 1234 }); }); + + 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, + LastLedgerSequence: 1 + }); + + const multiSigningJson = transaction.getMultiSigningJson(); + const t1 = Transaction.from_json(multiSigningJson); + + const a1 = 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK'; + const a2 = 'rH4KEcG9dEwGwpn6AyoWK9cZPLL4RLSmWW'; + + const s1 = t1.multiSign(a1, 'alice'); + assert.deepEqual(s1, { + Account: 'rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK', + TxnSignature: '3045022100DB13DC794DDFA1E27D099CDBFC7DB5B1EE892AD1725B0CEEE97D8B1C4C2055C7022030B3372C96D08106594B3CF8CDF88E05CC6260C51954F02387289CB69B839D7A', + SigningPubKey: '0388935426E0D08083314842EDFBB2D517BD47699F9A4527318A8E10468C97C052' + + }); + + const s2 = t1.multiSign(a2, 'bob'); + assert.deepEqual(s2, { + Account: 'rH4KEcG9dEwGwpn6AyoWK9cZPLL4RLSmWW', + TxnSignature: '304402207A22109088069C5ABE3E961C2F85B2B8111C5666C869E8BA3F2A57C2ECEA7FC402205F9D87FB42266CC498FCE9B4904955D0E6D5F44D092596F5DE3E25843F6D10AB', + SigningPubKey: '02691AC5AE1C4C333AE5DF8A93BDC495F0EEBFC6DB0DA7EB6EF808F3AFC006E3FE' + + }); + + transaction.addMultiSigner(s1); + transaction.addMultiSigner(s2); + + assert.deepEqual(transaction.getMultiSigners(), [ + {Signer: s2}, + {Signer: s1} + ]); + }); + + it('Multisign -- missing LastLedgerSequence', function() { + const transaction = Transaction.from_json({ + Account: 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn', + Sequence: 1, + Fee: '100', + TransactionType: 'AccountSet', + Flags: 0 + }); + + assert.throws(function() { + transaction.getMultiSigningJson(); + }); + }); });