• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

blockchain / My-Wallet-V3 / 2378

pending completion
2378

push

travis-ci

web-flow
Merge pull request #351 from blockchain/state-cleanup

Prevent state issues caused by mutability in payment.js

979 of 1531 branches covered (63.95%)

15 of 15 new or added lines in 2 files covered. (100.0%)

2852 of 3661 relevant lines covered (77.9%)

603.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.47
/src/transaction.js
1
'use strict';
2

3
var assert = require('assert');
221✔
4
var Bitcoin = require('bitcoinjs-lib');
221✔
5
var Helpers = require('./helpers');
221✔
6
var Buffer = require('buffer').Buffer;
221✔
7
var constants = require('./constants');
221✔
8

9
// Error messages that can be seen by the user should take the form of:
10
// {error: "NOT_GOOD", some_param: 1}
11
// Error messages that should only appear during development can be any string.
12

13
var Transaction = function (payment, emitter) {
221✔
14
  var unspentOutputs = payment.selectedCoins;
11✔
15
  var toAddresses = payment.to;
11✔
16
  var amounts = payment.amounts;
11✔
17
  var fee = payment.finalFee;
11✔
18
  var changeAddress = payment.change;
11✔
19
  var BITCOIN_DUST = constants.getNetwork().dustThreshold;
11✔
20

21
  if (!Array.isArray(toAddresses) && toAddresses != null) { toAddresses = [toAddresses]; }
11✔
22
  if (!Array.isArray(amounts) && amounts != null) { amounts = [amounts]; }
11✔
23

24
  assert(toAddresses && toAddresses.length, 'Missing destination address');
11✔
25
  assert(amounts && amounts.length, 'Missing amount to pay');
11✔
26

27
  if (payment.blockchainFee && payment.blockchainAddress) {
11!
28
    amounts = amounts.concat(payment.blockchainFee);
×
29
    toAddresses = toAddresses.concat(payment.blockchainAddress);
×
30
  }
31

32
  this.emitter = emitter;
11✔
33
  this.amount = amounts.reduce(Helpers.add, 0);
11✔
34
  this.addressesOfInputs = [];
11✔
35
  this.privateKeys = null;
11✔
36
  this.addressesOfNeededPrivateKeys = [];
11✔
37
  this.pathsOfNeededPrivateKeys = [];
11✔
38

39
  assert(toAddresses.length === amounts.length, 'The number of destination addresses and destination amounts should be the same.');
11✔
40
  assert(this.amount >= BITCOIN_DUST, {error: 'BELOW_DUST_THRESHOLD', amount: this.amount, threshold: BITCOIN_DUST});
11✔
41
  assert(unspentOutputs && unspentOutputs.length > 0, {error: 'NO_UNSPENT_OUTPUTS'});
10✔
42
  var transaction = new Bitcoin.TransactionBuilder(constants.getNetwork());
9✔
43
  // add all outputs
44
  function addOutput (e, i) { transaction.addOutput(toAddresses[i], amounts[i]); }
10✔
45
  toAddresses.map(addOutput);
9✔
46

47
  // add all inputs
48
  var total = 0;
9✔
49
  for (var i = 0; i < unspentOutputs.length; i++) {
9✔
50
    var output = unspentOutputs[i];
9✔
51
    total = total + output.value;
9✔
52
    var transactionHashBuffer = Buffer(output.hash, 'hex');
9✔
53
    transaction.addInput(Array.prototype.reverse.call(transactionHashBuffer), output.index);
9✔
54

55
    // Generate address from output script and add to private list so we can check if the private keys match the inputs later
56
    var scriptBuffer = Buffer(output.script, 'hex');
9✔
57
    assert.notEqual(Bitcoin.script.classifyOutput(scriptBuffer), 'nonstandard', {error: 'STRANGE_SCRIPT'});
9✔
58
    var address = Bitcoin.address.fromOutputScript(scriptBuffer, constants.getNetwork()).toString();
9✔
59
    assert(address, {error: 'CANNOT_DECODE_OUTPUT_ADDRESS', tx_hash: output.tx_hash});
9✔
60
    this.addressesOfInputs.push(address);
9✔
61

62
    // Add to list of needed private keys
63
    if (output.xpub) {
9✔
64
      this.pathsOfNeededPrivateKeys.push(output.xpub.path);
1✔
65
    } else {
66
      this.addressesOfNeededPrivateKeys.push(address);
8✔
67
    }
68
  }
69
  // Consume the change if it would create a very small none standard output
70
  var changeAmount = total - this.amount - fee;
9✔
71
  if (changeAmount >= BITCOIN_DUST) {
9✔
72
    assert(changeAddress, 'No change address specified');
8✔
73
    transaction.addOutput(changeAddress, changeAmount);
8✔
74
  }
75

76
  this.transaction = transaction;
9✔
77
};
78

79
Transaction.prototype.addPrivateKeys = function (privateKeys) {
221✔
80
  assert.equal(privateKeys.length, this.addressesOfInputs.length, 'Number of private keys needs to match inputs');
3✔
81

82
  for (var i = 0; i < privateKeys.length; i++) {
3✔
83
    assert.equal(this.addressesOfInputs[i], privateKeys[i].getAddress(), 'Private key does not match bitcoin address ' + this.addressesOfInputs[i] + '!=' + privateKeys[i].getAddress() + ' while adding private key for input ' + i);
3✔
84
  }
85

86
  this.privateKeys = privateKeys;
3✔
87
};
88

89
/**
90
 * BIP69: Sort outputs lexicographycally
91
 */
92

93
Transaction.prototype.sortBIP69 = function () {
221✔
94
  var compareInputs = function (a, b) {
2✔
95
    var hasha = new Buffer(a[0].hash);
48✔
96
    var hashb = new Buffer(b[0].hash);
48✔
97
    var x = [].reverse.call(hasha);
48✔
98
    var y = [].reverse.call(hashb);
48✔
99
    return x.compare(y) || a[0].index - b[0].index;
48✔
100
  };
101

102
  var compareOutputs = function (a, b) {
2✔
103
    return (a.value - b.value) || (a.script).compare(b.script);
2✔
104
  };
105
  var mix = Helpers.zip3(this.transaction.tx.ins, this.privateKeys, this.addressesOfInputs);
2✔
106
  mix.sort(compareInputs);
2✔
107
  this.transaction.tx.ins = mix.map(function (a) { return a[0]; });
19✔
108
  this.privateKeys = mix.map(function (a) { return a[1]; });
19✔
109
  this.addressesOfInputs = mix.map(function (a) { return a[2]; });
19✔
110
  this.transaction.tx.outs.sort(compareOutputs);
2✔
111
};
112
/**
113
 * Sign the transaction
114
 * @return {Object} Signed transaction
115
 */
116
Transaction.prototype.sign = function () {
221✔
117
  assert(this.privateKeys, 'Need private keys to sign transaction');
2✔
118

119
  assert.equal(this.privateKeys.length, this.transaction.inputs.length, 'Number of private keys needs to match inputs');
2✔
120

121
  for (var i = 0; i < this.privateKeys.length; i++) {
2✔
122
    assert.equal(this.addressesOfInputs[i], this.privateKeys[i].getAddress(), 'Private key does not match bitcoin address ' + this.addressesOfInputs[i] + '!=' + this.privateKeys[i].getAddress() + ' while signing input ' + i);
2✔
123
  }
124

125
  this.emitter.emit('on_begin_signing');
2✔
126

127
  var transaction = this.transaction;
2✔
128

129
  for (var ii = 0; ii < transaction.inputs.length; ii++) {
2✔
130
    this.emitter.emit('on_sign_progress', ii + 1);
2✔
131
    var key = this.privateKeys[ii];
2✔
132
    transaction.sign(ii, key);
2✔
133
    assert(transaction.inputs[ii].scriptType === 'pubkeyhash', 'Error creating input script');
2✔
134
  }
135

136
  this.emitter.emit('on_finish_signing');
2✔
137
  return transaction;
2✔
138
};
139

140
Transaction.inputCost = function (feePerKb) {
221✔
141
  return Math.ceil(feePerKb * 0.148);
199✔
142
};
143
Transaction.guessSize = function (nInputs, nOutputs) {
221✔
144
  if (nInputs < 1 || nOutputs < 1) { return 0; }
210✔
145
  return (nInputs * 148 + nOutputs * 34 + 10);
170✔
146
};
147

148
Transaction.guessFee = function (nInputs, nOutputs, feePerKb) {
221✔
149
  var sizeBytes = Transaction.guessSize(nInputs, nOutputs);
199✔
150
  return Math.ceil(feePerKb * (sizeBytes / 1000));
199✔
151
};
152

153
Transaction.filterUsableCoins = function (coins, feePerKb) {
221✔
154
  if (!Array.isArray(coins)) return [];
199✔
155
  var icost = Transaction.inputCost(feePerKb);
198✔
156
  return coins.filter(function (c) { return c.value >= icost; });
298✔
157
};
158

159
Transaction.maxAvailableAmount = function (usableCoins, feePerKb) {
221✔
160
  var len = usableCoins.length;
149✔
161
  var fee = Transaction.guessFee(len, 2, feePerKb);
149✔
162
  return {'amount': usableCoins.reduce(function (a, e) { a = a + e.value; return a; }, 0) - fee, 'fee': fee};
228✔
163
};
164

165
Transaction.sumOfCoins = function (coins) {
221✔
166
  return coins.reduce(function (a, e) { a = a + e.value; return a; }, 0);
42✔
167
};
168

169
Transaction.selectCoins = function (usableCoins, amounts, fee, isAbsoluteFee) {
221✔
170
  var amount = amounts.reduce(Helpers.add, 0);
60✔
171
  var nouts = amounts.length;
60✔
172
  var sorted = usableCoins.sort(function (a, b) { return b.value - a.value; });
60✔
173
  var len = sorted.length;
60✔
174
  var sel = [];
60✔
175
  var accAm = 0;
60✔
176
  var accFee = 0;
60✔
177

178
  if (isAbsoluteFee) {
60✔
179
    for (var i = 0; i < len; i++) {
3✔
180
      var coin = sorted[i];
3✔
181
      accAm = accAm + coin.value;
3✔
182
      sel.push(coin);
3✔
183
      if (accAm >= fee + amount) { return {'coins': sel, 'fee': fee}; }
3✔
184
    }
185
  } else {
186
    for (var ii = 0; ii < len; ii++) {
57✔
187
      var coin2 = sorted[ii];
49✔
188
      accAm = accAm + coin2.value;
49✔
189
      accFee = Transaction.guessFee(ii + 1, nouts + 1, fee);
49✔
190
      sel.push(coin2);
49✔
191
      if (accAm >= accFee + amount) { return {'coins': sel, 'fee': accFee}; }
49✔
192
    }
193
  }
194
  return {'coins': [], 'fee': 0};
16✔
195
};
196

197
Transaction.confirmationEstimation = function (absoluteFees, fee) {
221✔
198
  var len = absoluteFees.length;
13✔
199
  for (var i = 0; i < len; i++) {
13✔
200
    if (absoluteFees[i] > 0 && fee >= absoluteFees[i]) {
56✔
201
      return i + 1;
8✔
202
    } else {
203
      if (absoluteFees[i] > 0 && (i + 1 === len)) { return Infinity; }
48✔
204
    }
205
  }
206
  return null;
3✔
207
};
208
module.exports = Transaction;
221✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc