diff --git a/src/fee_bump_transaction.js b/src/fee_bump_transaction.js index ea91d8b1..42288072 100644 --- a/src/fee_bump_transaction.js +++ b/src/fee_bump_transaction.js @@ -46,7 +46,7 @@ export class FeeBumpTransaction extends TransactionBase { const innerTxEnvelope = xdr.TransactionEnvelope.envelopeTypeTx( tx.innerTx().v1() ); - this._feeSource = encodeMuxedAccountToAddress(this.tx.feeSource()); + this._feeSource = encodeMuxedAccountToAddress(this._tx.feeSource()); this._innerTransaction = new Transaction( innerTxEnvelope, networkPassphrase @@ -89,7 +89,7 @@ export class FeeBumpTransaction extends TransactionBase { signatureBase() { const taggedTransaction = new xdr.TransactionSignaturePayloadTaggedTransaction.envelopeTypeTxFeeBump( - this.tx + this._tx ); const txSignature = new xdr.TransactionSignaturePayload({ @@ -106,7 +106,7 @@ export class FeeBumpTransaction extends TransactionBase { */ toEnvelope() { const envelope = new xdr.FeeBumpTransactionEnvelope({ - tx: xdr.FeeBumpTransaction.fromXDR(this.tx.toXDR()), // make a copy of the tx + tx: xdr.FeeBumpTransaction.fromXDR(this._tx.toXDR()), // make a copy of the tx signatures: this.signatures.slice() // make a copy of the signatures }); diff --git a/src/memo.js b/src/memo.js index d9d0bfa5..84150072 100644 --- a/src/memo.js +++ b/src/memo.js @@ -102,6 +102,13 @@ export class Memo { throw error; } + // Reject anything that isn't plain decimal digits (no scientific notation, + // no decimals, no signs). BigNumber accepts formats like "1e18" and "1.0" + // which pass numeric checks but crash BigInt()/UnsignedHyper.fromString(). + if (!/^\d+$/.test(value)) { + throw error; + } + let number; try { number = new BigNumber(value); @@ -109,26 +116,6 @@ export class Memo { throw error; } - // Infinity - if (!number.isFinite()) { - throw error; - } - - // NaN - if (number.isNaN()) { - throw error; - } - - // Negative - if (number.isNegative()) { - throw error; - } - - // Decimal - if (!number.isInteger()) { - throw error; - } - // Exceeds uint64 max (2^64 - 1) if (number.isGreaterThan('18446744073709551615')) { throw error; diff --git a/src/transaction.js b/src/transaction.js index 47d63255..4abec66a 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -62,11 +62,11 @@ export class Transaction extends TransactionBase { switch (this._envelopeType) { case xdr.EnvelopeType.envelopeTypeTxV0(): this._source = StrKey.encodeEd25519PublicKey( - this.tx.sourceAccountEd25519() + this._tx.sourceAccountEd25519() ); break; default: - this._source = encodeMuxedAccountToAddress(this.tx.sourceAccount()); + this._source = encodeMuxedAccountToAddress(this._tx.sourceAccount()); break; } @@ -256,7 +256,7 @@ export class Transaction extends TransactionBase { * @returns {Buffer} */ signatureBase() { - let tx = this.tx; + let tx = this._tx; // Backwards Compatibility: Use ENVELOPE_TYPE_TX to sign ENVELOPE_TYPE_TX_V0 // we need a Transaction to generate the signature base @@ -288,7 +288,7 @@ export class Transaction extends TransactionBase { * @returns {xdr.TransactionEnvelope} */ toEnvelope() { - const rawTx = this.tx.toXDR(); + const rawTx = this._tx.toXDR(); const signatures = this.signatures.slice(); // make a copy of the signatures let envelope; diff --git a/src/transaction_base.js b/src/transaction_base.js index acda6af7..f92a3608 100644 --- a/src/transaction_base.js +++ b/src/transaction_base.js @@ -32,7 +32,11 @@ export class TransactionBase { } get tx() { - return this._tx; + // Return a defensive copy to prevent mutation of the internal XDR object + // after construction. Signing and serialization use this._tx directly, + // so exposing the live reference could let callers alter signed bytes + // without changing the cached display fields (fee, source, memo, etc.). + return this._tx.constructor.fromXDR(this._tx.toXDR()); } set tx(value) { diff --git a/test/unit/memo_test.js b/test/unit/memo_test.js index dbb69816..549d4c92 100644 --- a/test/unit/memo_test.js +++ b/test/unit/memo_test.js @@ -134,6 +134,10 @@ describe('Memo.id()', function () { expect(() => StellarBase.Memo.id('-1')).to.throw(/Expects a uint64/); // decimal expect(() => StellarBase.Memo.id('1.5')).to.throw(/Expects a uint64/); + // trailing decimal zero (BigNumber accepts but must be rejected) + expect(() => StellarBase.Memo.id('1.0')).to.throw(/Expects a uint64/); + // scientific notation (BigNumber accepts but BigInt()/UnsignedHyper crashes) + expect(() => StellarBase.Memo.id('1e18')).to.throw(/Expects a uint64/); // overflow: 2^64 silently became 0 before this fix expect(() => StellarBase.Memo.id('18446744073709551616')).to.throw( /Expects a uint64/ diff --git a/test/unit/transaction_test.js b/test/unit/transaction_test.js index 8e0e7d09..3cc0fbc7 100644 --- a/test/unit/transaction_test.js +++ b/test/unit/transaction_test.js @@ -79,6 +79,13 @@ describe('Transaction', function () { const envelope = transaction.toEnvelope().value(); envelope.tx().fee(StellarBase.xdr.Int64.fromString('300')); + expect(transaction.tx.fee().toString()).to.equal('100'); + }); + it('tx getter returns a defensive copy that does not affect internal XDR', function () { + const transaction = this.transaction; + const txCopy = transaction.tx; + txCopy.fee(StellarBase.xdr.Int64.fromString('999')); + expect(transaction.tx.fee().toString()).to.equal('100'); }); });