Skip to content

Feat/fix fee calculations #312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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();
return this;
};

/* 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;
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.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._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