Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ module.exports = (config) => {
'./index.js': ['webpack'],
'./test.spec.js': ['webpack'],
},
client: {
mocha: {
timeout: 60000
}
},
webpack: {
resolve: {
fallback: {
Expand Down
52 changes: 38 additions & 14 deletions lib/transaction/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ Transaction.FEE_PER_KB = 1000;
Transaction.CHANGE_OUTPUT_MAX_SIZE = 20 + 4 + 34 + 4;
Transaction.MAXIMUM_EXTRA_SIZE = 4 + 9 + 9 + 4;

Transaction.MINIMUM_FEE = 255;

Transaction.TYPES = registeredTransactionTypes;

Transaction.CURRENT_VERSION = CURRENT_VERSION;
Expand Down Expand Up @@ -832,6 +834,21 @@ Transaction.prototype.feePerKb = function (amount) {
return this;
};

/**
* Manually set the minimum fee's for this transaction. Beware that this resets all the signatures
* for inputs (in further versions, SIGHASH_SINGLE or SIGHASH_NONE signatures will not
* be reset).
*
* @param {number} amount satoshis per KB to be sent
* @return {Transaction} this, for chaining
*/
Transaction.prototype.minimumFee = function (amount) {
$.checkArgument(_.isNumber(amount), 'amount must be a number');
this._minimumFee = amount;
this._updateChangeOutput();
Comment on lines +837 to +848

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: JSDoc for minimumFee is wrong (copy-pasted from feePerKb)

The JSDoc says @param {number} amount satoshis per KB to be sent and the prose says Manually set the minimum fee's (stray apostrophe). The minimum fee is a flat duff amount, not satoshis-per-KB.

Suggested change
/**
* Manually set the minimum fee's for this transaction. Beware that this resets all the signatures
* for inputs (in further versions, SIGHASH_SINGLE or SIGHASH_NONE signatures will not
* be reset).
*
* @param {number} amount satoshis per KB to be sent
* @return {Transaction} this, for chaining
*/
Transaction.prototype.minimumFee = function (amount) {
$.checkArgument(_.isNumber(amount), 'amount must be a number');
this._minimumFee = amount;
this._updateChangeOutput();
/**
* Manually set the minimum fee for this transaction (in duffs). Beware that this
* resets all the signatures for inputs (in further versions, SIGHASH_SINGLE or
* SIGHASH_NONE signatures will not be reset).
*
* @param {number} amount minimum fee in duffs
* @return {Transaction} this, for chaining
*/
Transaction.prototype.minimumFee = function (amount) {
$.checkArgument(_.isNumber(amount), 'amount must be a number');
this._minimumFee = amount;
this._updateChangeOutput();
return this;
};

source: ['claude']

return this;
};
Comment on lines +837 to +850

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: New public Transaction.prototype.minimumFee setter has no direct test coverage

The PR adds a new public setter minimumFee(amount) but no new test exercises it — all rely on the default Transaction.MINIMUM_FEE = 255. Combined with the NaN bug in _estimateFee flagged above, this means the setter's two intended branches (floor and surcharge) are completely unverified. Add at least one test that calls .minimumFee(custom) and asserts both (a) the floor (fee falls below custom → returns custom) and (b) the surcharge branch (fee ≥ custom with sufficient available).

source: ['claude']

Comment on lines +845 to +850

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Declare the new minimum-fee API for TypeScript consumers

The PR introduces the public minimumFee() method and MINIMUM_FEE constant without adding them to typings/transaction/Transaction.d.ts. Type-checking new Transaction(undefined).minimumFee(500) and Transaction.MINIMUM_FEE produces TS2339 for both, preventing TypeScript consumers from using the new feature through the package declarations. Add the chainable method and static constant declarations, together with test-d coverage. The existing type tests pass because they do not exercise either addition.

source: ['claude']


/* Output management */

/**
Expand Down Expand Up @@ -1046,7 +1063,7 @@ Transaction.prototype.getFee = function () {
Transaction.prototype._estimateFee = function () {
var estimatedSize = this._estimateSize();
var available = this._getUnspentValue();
return Transaction._estimateFee(estimatedSize, available, this._feePerKb);
return Transaction._estimateFee(estimatedSize, available, this._feePerKb, this._minimumFee);
};

Transaction.prototype._getUnspentValue = function () {
Expand All @@ -1059,23 +1076,30 @@ Transaction.prototype._clearSignatures = function () {
});
};

Transaction._estimateFee = function (size, amountAvailable, feePerKb) {
var fee = Math.ceil(size / 1000) * (feePerKb || Transaction.FEE_PER_KB);
if (amountAvailable > fee) {
size += Transaction.CHANGE_OUTPUT_MAX_SIZE;
Transaction._estimateFee = function (size, amountAvailable, feePerKb, minimumFee) {
var feePerByte = (feePerKb || Transaction.FEE_PER_KB) / 1000;
var fee = size * feePerByte;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Round the calculated fee to whole duffs

Multiplying the byte count by feePerKb / 1000 can produce fractional duffs for an integer fee rate. A three-input P2PKH transaction using .feePerKb(1200) has a 488-byte estimate before change, producing a fee of 585.6 duffs. Calling .change(address) then throws "Output satoshis is not a natural number" because _updateChangeOutput() subtracts that fractional fee from the available integer amount. Round the calculated fee upward so output amounts remain integral and the requested rate is preserved.

Suggested change
var fee = size * feePerByte;
var fee = Math.ceil(size * feePerByte);

source: ['claude']

if(fee<(minimumFee || Transaction.MINIMUM_FEE)){
return (minimumFee || Transaction.MINIMUM_FEE)
}
if (amountAvailable > fee + minimumFee) {
fee += (minimumFee || Transaction.MINIMUM_FEE);
Comment on lines +1085 to +1086

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Apply minimumFee consistently as a lower bound

This branch adds the minimum fee to a size-based fee that already meets the minimum. It also treats an omitted minimum differently from an explicitly supplied default because fee + undefined is NaN. For example, _estimateFee(500, 100000, 1000) returns 500, while passing 255 as the fourth argument returns 755. In a reproduced two-input P2PKH transaction, explicitly setting .minimumFee(Transaction.MINIMUM_FEE) changes the actual fee from 340 to 595 duffs even though the floor remains 255 and was already satisfied. Normalize the optional minimum once and apply it as a lower bound, not an additional balance-dependent charge.

source: ['claude']

}
Comment on lines +1085 to 1087

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: fee + minimumFee evaluates to NaN in the default path — surcharge branch never fires

Transaction._estimateFee receives minimumFee as the raw this._minimumFee (line 1066), which is undefined for any transaction that has not explicitly called .minimumFee(...). At line 1085, amountAvailable > fee + minimumFee becomes amountAvailable > NaN, which is always false, so the fee += (minimumFee || MINIMUM_FEE) surcharge is silently skipped in the default code path. The branch only works for callers that explicitly call the new setter, producing different fees depending on whether the setter was used. Apply the (minimumFee || Transaction.MINIMUM_FEE) fallback consistently to both the comparison and the assignment (or hoist it into a local).

Suggested change
if (amountAvailable > fee + minimumFee) {
fee += (minimumFee || Transaction.MINIMUM_FEE);
}
Transaction._estimateFee = function (size, amountAvailable, feePerKb, minimumFee) {
var minFee = minimumFee || Transaction.MINIMUM_FEE;
var feePerByte = (feePerKb || Transaction.FEE_PER_KB) / 1000;
var fee = Math.ceil(size * feePerByte);
if (fee < minFee) {
return minFee;
}
if (amountAvailable > fee + minFee) {
fee += minFee;
}
return fee;
};

source: ['claude', 'codex']

return Math.ceil(size / 1000) * (feePerKb || Transaction.FEE_PER_KB);
return fee;
Comment on lines +1080 to +1088

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Per-byte formula returns fractional duffs and will throw downstream

feePerByte = (feePerKb || FEE_PER_KB) / 1000; fee = size * feePerByte is fractional whenever feePerKb is not a multiple of 1000 (e.g., feePerKb(1234) or feePerKb(1500)). The result is passed to _updateChangeOutput (line 1015) which builds new Output({ satoshis: changeAmount = available - fee }). The Output setter calls $.checkArgument(JSUtil.isNaturalNumber(num)) (lib/transaction/output.js) and throws 'Output satoshis is not a natural number'. The previous algorithm always returned an integer via Math.ceil(size/1000) * feePerKb. Wrap with Math.ceil (or Math.round) to preserve the integer-satoshi contract.

Suggested change
var feePerByte = (feePerKb || Transaction.FEE_PER_KB) / 1000;
var fee = size * feePerByte;
if(fee<(minimumFee || Transaction.MINIMUM_FEE)){
return (minimumFee || Transaction.MINIMUM_FEE)
}
if (amountAvailable > fee + minimumFee) {
fee += (minimumFee || Transaction.MINIMUM_FEE);
}
return Math.ceil(size / 1000) * (feePerKb || Transaction.FEE_PER_KB);
return fee;
Transaction._estimateFee = function (size, amountAvailable, feePerKb, minimumFee) {
var minFee = minimumFee || Transaction.MINIMUM_FEE;
var feePerByte = (feePerKb || Transaction.FEE_PER_KB) / 1000;
var fee = Math.ceil(size * feePerByte);
if (fee < minFee) {
return minFee;
}
if (amountAvailable > fee + minFee) {
fee += minFee;
}
return fee;
};

source: ['claude', 'codex']

};

Transaction.prototype._estimateSize = function () {
var result = Transaction.MAXIMUM_EXTRA_SIZE;
_.each(this.inputs, function (input) {
result += input._estimateSize();
});
_.each(this.outputs, function (output) {
result += output.script.toBuffer().length + 9;
});
return result;
// string format HEX -> 2 symbols == 1 byte
var serializedSize = this.toString().length / 2;
if ( !this.isFullySigned() ) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Do not require signature verification to estimate transaction size

Transaction.isFullySigned() throws UnableToVerifySignature for generic Input instances, including custom scripts and inputs parsed from serialized transactions. Calling it from _estimateSize() introduces that requirement into the fee checks performed by serialize(), even when disableIsFullySigned is explicitly true. A reproduced transaction spending an OP_TRUE UTXO with previous-output information throws here when serialized with that option; the same transaction serializes successfully when using the base revision's fee and size estimators. Estimate size without requiring signature-verification support so the existing serialization option remains effective.

source: ['claude']

// every signed input contains some additional bytes
serializedSize += this.inputs.length * PublicKeyHashInput.SCRIPT_MAX_SIZE;
Comment on lines +1094 to +1096

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Estimate signature growth using each input's script type

The new estimator replaces input-specific size estimates with a fixed 107-byte P2PKH allowance for every unsigned input. Supported multisig inputs can require more signature bytes: after clearSignatures() installs a 2-of-3 P2SH redeem script, the input still needs two signatures, whose combined size exceeds this allowance. Replaying the updated final fixture in test/data/tx_creation.json produces exactly its expected serialization, but the transaction pays only 586 duffs for 696 bytes at the default 1,000 duffs/kB rate. This signature-growth underestimate is separate from the omitted change output. Use script-type-aware estimates that account for bytes already serialized and any script-length varint growth.

source: ['claude']

}
if ( this._missingChange() ) {
serializedSize += Transaction.CHANGE_OUTPUT_MAX_SIZE;
}
Comment on lines +1098 to +1100

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Include the pending change output before allocating change

_missingChange() checks whether a change address is configured, not whether the transaction currently contains its change output. During _updateChangeOutput(), the change script is already configured and the previous change output has been removed, so this condition skips budgeting for the forthcoming output. A reproduced two-input P2PKH transaction pays 340 duffs but serializes to 373 bytes after signing, below the default 1,000 duffs/kB rate. Its subsequent getFee() call returns 373 rather than the actual 340, so the new getFee() assertions do not detect the underpayment. Include the prospective change output before allocating change and test the actual fee using inputAmount - outputAmount.

source: ['claude']


return serializedSize;
};
Comment on lines 1091 to 1103

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: Hard-coded PublicKeyHashInput.SCRIPT_MAX_SIZE undersizes non-P2PKH unsigned inputs

_estimateSize now adds this.inputs.length * PublicKeyHashInput.SCRIPT_MAX_SIZE (107 bytes) for every unsigned input. The previous implementation summed polymorphic input._estimateSize(). PublicKeyInput.SCRIPT_MAX_SIZE is 73 (lib/transaction/input/publickey.js:103), MultiSigInput._estimateSize at lib/transaction/input/multisig.js:252 and MultiSigScriptHashInput._estimateSize at lib/transaction/input/multisigscripthash.js:206 return larger threshold-dependent values (a 2-of-3 P2SH input is ~256+ bytes). For unsigned multisig/P2SH transactions, fees are now significantly under-estimated, which produces under-funded transactions after signing. Restore the per-input polymorphic dispatch.

Suggested change
Transaction.prototype._estimateSize = function () {
var result = Transaction.MAXIMUM_EXTRA_SIZE;
_.each(this.inputs, function (input) {
result += input._estimateSize();
});
_.each(this.outputs, function (output) {
result += output.script.toBuffer().length + 9;
});
return result;
// string format HEX -> 2 symbols == 1 byte
var serializedSize = this.toString().length / 2;
if ( !this.isFullySigned() ) {
// every signed input contains some additional bytes
serializedSize += this.inputs.length * PublicKeyHashInput.SCRIPT_MAX_SIZE;
}
if ( this._missingChange() ) {
serializedSize += Transaction.CHANGE_OUTPUT_MAX_SIZE;
}
return serializedSize;
};
Transaction.prototype._estimateSize = function () {
// string format HEX -> 2 symbols == 1 byte
var serializedSize = this.toString().length / 2;
if (!this.isFullySigned()) {
_.each(this.inputs, function (input) {
serializedSize += input._estimateSize();
});
}
if (this._missingChange()) {
serializedSize += Transaction.CHANGE_OUTPUT_MAX_SIZE;
}
return serializedSize;
};

source: ['claude', 'codex']

Comment on lines 1091 to 1103

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: isFullySigned() is semantically partial-signed; gate misfires for mixed inputs

Transaction.prototype.isFullySigned (line 1286) uses _.some(...), returning true as soon as ANY input is signed. The new _estimateSize gates the per-input script-size buffer on !isFullySigned(). A transaction with one signed input and N-1 unsigned inputs will skip the buffer entirely, under-estimating fees. Iterate per input and add the buffer only for inputs where input.isFullySigned() is false (this naturally also fixes the polymorphic-size issue above). The underlying isFullySigned semantics quirk is pre-existing and out of scope to rename here.

source: ['claude']

Comment on lines 1091 to 1103

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: _missingChange() doesn't reflect actual-output state inside _updateChangeOutput

_missingChange() returns !this._changeScript. Inside _updateChangeOutput (line 1005), the change output is removed via _removeOutput(this._changeIndex) at line 1011 while _changeScript is still set, then getFee() is called. _estimateSize reads _missingChange() === false and skips CHANGE_OUTPUT_MAX_SIZE, even though toString() does not currently contain the change output. The estimate is short by ~62 bytes. The gate likely wants to check whether the change output is currently in this.outputs (e.g., _.isUndefined(this._changeIndex)), not whether a change script has been configured.

source: ['claude']

Comment on lines 1091 to 1103

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: _estimateSize triggers full toString() on every mutator, creating O(N²) work

Each from/to/fee/feePerKb/change/minimumFee call invokes _updateChangeOutput → getFee → _estimateFee → _estimateSize, which now fully serializes the transaction via this.toString(). Building a tx with N inputs and M outputs incrementally becomes O((N+M)²). The previous implementation summed per-component max sizes in O(N+M). The 30-input tests added here pass, but larger downstream callers will feel this. Prefer summing component sizes (matching the polymorphic-input fix above) or cache toString() per dirty-state.

source: ['claude']

Comment on lines +1079 to 1103

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Inconsistent formatting in new _estimateFee / _estimateSize

if(fee<(minimumFee || Transaction.MINIMUM_FEE)){ lacks spaces around the operator and after if/{; the early-return line lacks a trailing semicolon. _estimateSize uses if ( ... ) with extra inner spaces. Both diverge from the surrounding style used throughout the file. Run npm run lint and apply fixes.

source: ['claude']


Transaction.prototype._removeOutput = function (index) {
Expand Down
2 changes: 1 addition & 1 deletion test/data/tx_creation.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,6 @@
"sign",
["XJoAwsbx9EqCp7PTMAWbhQsjFHDuQEaEJwcNS96F3ZKuPM4y8ntu"],
"serialize",
"010000000220c24f763536edb05ce8df2a4816d971be4f20b58451d71589db434aca98bfaf00000000fdfd000048304502210088ddd40f1b58603b4ee75d21226528426ee4d9c9e88b960bf80081dcdf1a9d5a02202f368ce8bd9c66b9ec17f92569ae5dca2ef541e34bf632f8487009a58a1d1925014730440220704d7b99bb5333dd6a3e8b09fc5fa4fb9c891ce03cab47e902dfce5aeaf8724a0220402e08c44d088e4e5448574c2c10d7bc36d7ba1eb5de61dccb08786628738a19014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffffa0644cd1606e081c59eb65fe69d4a83a3a822da423bc392c91712fb77a192edc00000000fdfd00004830450221008610924a2e48fd0f0be9b8dd5017f1d478be6101ee8ea69e0cdca6bcfbfd4c21022012da689506940bb20fcf9fe590d7687180b49904b82525c331bfe1965965c2f601473044022062b1f55d1afaaa94c2f27eec3bd5ea20f8a19afe09387655b7c9f7e03749f690022077904d8490ef149b8a097aa23ebc384cabe7c9b4f6b137e672793e176a1cf684014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffff03f04902000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af387007102000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af38763b204000000000017a9146c8d8b04c6a1e664b1ec20ec932760760c97688e8700000000"
"010000000220c24f763536edb05ce8df2a4816d971be4f20b58451d71589db434aca98bfaf00000000fdfe0000483045022100cf4007c6c85f5dc78afa0a1433267df95230745c055bdf727e3e5d96b1b97c070220449391b5522cafc9765286069e1c7b586ead5cc885cba46cf04a7ee1a87a886e01483045022100dc711e292f80a26f1836d50ace019d2b82841aa9209c31a0c7d7c78e7fce9b6502206dfd33a16f8566ebe54f3fbc467135a43ecaf968668f9c83cce57ec12312f2d9014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffffa0644cd1606e081c59eb65fe69d4a83a3a822da423bc392c91712fb77a192edc00000000fc0047304402201ea8024482d974efa29f8b56b62471714d5b73e4fce9da3353ecbeb338091f0c022070fc267ec8ebf62b8b2cd2fdf763888c062744c586ed7ea1b79314727d7abc120147304402200b0e08d20084090fff0749311e6a9a4a66735a22b6eefebff0060140f60ad7c50220181d8dcfef0c1508cfb08eb36223ca9249927442d9d956039f3330aa6b543fc6014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffff03f04902000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af387007102000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af38701b404000000000017a9146c8d8b04c6a1e664b1ec20ec932760760c97688e8700000000"
]
]
115 changes: 111 additions & 4 deletions test/transaction/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,113 @@ describe('Transaction', function () {
);
});


describe('transaction fee calculation test', function () {
var minimalFee = 255;

var minimalUTXO = {
"satoshis": 5460 + 255,
"script": Script.buildPublicKeyHashOut('yfGjFr9Cu8AZYYrdeiRRNtLktWcJetxKv4').toString(),
"txid": '88d78d6afaa06bbe5943152757305338eba27cc1f3e84acb1a31ab17f26c038d',
outputIndex: 0,
address: "yfGjFr9Cu8AZYYrdeiRRNtLktWcJetxKv4"
}

var simpleUTXO = {
...minimalUTXO,
"satoshis": 1000000000,
}

var privateKey = PrivateKey.fromWIF(
'cTtLHt4mv6zuJytSnM7Vd6NLxyNauYLMxD818sBC8PJ1UPiVTRSs'
)

it('should return correct minimal fee amount with 1 input and 1 output', () => {
var tx = new Transaction()
.from(minimalUTXO)
.to('yeB49jSYYp3786GQr7eKFwLNdEqonBf6hm', 5460)
.change('yeB49jSYYp3786GQr7eKFwLNdEqonBf6hm')

tx.getFee().should.equal(minimalFee);

tx.sign(privateKey);

tx.isFullySigned().should.equal(true);

tx.getFee().should.equal(minimalFee);

should.equal(tx.getChangeOutput(), null);
});

it('should return correct minimal fee amount with 30 input and 1 output', () => {
var tx = new Transaction()

const privateKeys = []

for (let i=0; i < 30; i++){
tx.from(simpleUTXO)
privateKeys.push(privateKey)
}

tx
.to('yeB49jSYYp3786GQr7eKFwLNdEqonBf6hm', 5460)
.change('yeB49jSYYp3786GQr7eKFwLNdEqonBf6hm')
.sign(privateKeys);

tx.isFullySigned().should.equal(true);

tx.getFee().should.gte(tx.toString().length/2);
});

it('should return correct minimal fee amount with 30 input and 30 outputs', () => {
var tx = new Transaction()

const privateKeys = []

for (let i=0; i < 30; i++){
tx.from(simpleUTXO)
privateKeys.push(privateKey)
}

for (let i=0; i < 30; i++){
tx.to('yeB49jSYYp3786GQr7eKFwLNdEqonBf6hm', 5460)
}

tx
.change('yeB49jSYYp3786GQr7eKFwLNdEqonBf6hm')
.sign(privateKeys);

tx.isFullySigned().should.equal(true);

tx.getFee().should.gte(tx.toString().length/2);
});

it('should return correct minimal fee amount with 30 input and 30 outputs with custom fee per KB', () => {
var tx = new Transaction()
.feePerKb(2000)

const privateKeys = []

for (let i=0; i < 30; i++){
tx.from(simpleUTXO)
privateKeys.push(privateKey)
}

for (let i=0; i < 30; i++){
tx.to('yeB49jSYYp3786GQr7eKFwLNdEqonBf6hm', 5460)
}

tx
.change('yeB49jSYYp3786GQr7eKFwLNdEqonBf6hm')
.sign(privateKeys);

tx.isFullySigned().should.equal(true);

tx.getFee().should.gte(tx.toString().length);
});
})


describe('transaction creation test vector', function () {
this.timeout(5000);
var index = 0;
Expand Down Expand Up @@ -466,7 +573,7 @@ describe('Transaction', function () {
.change(changeAddress)
.sign(privateKey);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].satoshis.should.equal(49000);
transaction.outputs[1].satoshis.should.equal(49745);
transaction.outputs[1].script
.toString()
.should.equal(Script.fromAddress(changeAddress).toString());
Expand Down Expand Up @@ -540,7 +647,7 @@ describe('Transaction', function () {
.sign(privateKey);
transaction._estimateSize().should.be.within(1000, 1999);
transaction.outputs.length.should.equal(2);
transaction.outputs[1].satoshis.should.equal(34000);
transaction.outputs[1].satoshis.should.equal(37808);
});
it('if satoshis are invalid', function () {
var transaction = new Transaction()
Expand Down Expand Up @@ -1126,7 +1233,7 @@ describe('Transaction', function () {
.change(changeAddress)
.to(toAddress, 10000);
transaction.inputAmount.should.equal(100000000);
transaction.outputAmount.should.equal(99999000);
transaction.outputAmount.should.equal(99999745);
});
it('returns correct values for coinjoin transaction', function () {
// see livenet tx c16467eea05f1f30d50ed6dbc06a38539d9bb15110e4b7dc6653046a3678a718
Expand Down Expand Up @@ -1216,7 +1323,7 @@ describe('Transaction', function () {
tx.outputs.length.should.equal(2);
tx.outputs[0].satoshis.should.equal(10000000);
tx.outputs[0].script.toAddress().toString().should.equal(toAddress);
tx.outputs[1].satoshis.should.equal(89999000);
tx.outputs[1].satoshis.should.equal(89999745);
tx.outputs[1].script.toAddress().toString().should.equal(changeAddress);
});
});
Expand Down