This repository has been archived by the owner on Mar 7, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 284
/
transaction.js
195 lines (151 loc) · 6.58 KB
/
transaction.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
'use strict';
var assert = require('assert');
var Bitcoin = require('bitcoinjs-lib');
var randomBytes = require('randombytes');
var Helpers = require('./helpers');
var MyWallet = require('./wallet');
var Buffer = require('buffer').Buffer;
var Transaction = function (unspentOutputs, toAddresses, amounts, fee, changeAddress, listener) {
if (!Array.isArray(toAddresses) && toAddresses != null) {toAddresses = [toAddresses];}
if (!Array.isArray(amounts) && amounts != null) {amounts = [amounts];}
var network = Bitcoin.networks.bitcoin;
assert(toAddresses, 'Missing destination address');
assert(amounts, 'Missing amount to pay');
this.amount = amounts.reduce(function(a, b) {return a + b;},0);
this.listener = listener;
this.addressesOfInputs = [];
this.privateKeys = null;
this.addressesOfNeededPrivateKeys = [];
this.pathsOfNeededPrivateKeys = [];
this.fee = 0; // final used fee
var BITCOIN_DUST = 5460;
var forcedFee = (typeof(fee) == "number") ? fee : null;
assert(toAddresses.length == amounts.length, 'The number of destiny addresses and destiny amounts should be the same.');
assert(this.amount > BITCOIN_DUST, this.amount + ' must be above dust threshold (' + BITCOIN_DUST + ' Satoshis)');
assert(unspentOutputs && unspentOutputs.length > 0, 'Missing coins to spend');
var transaction = new Bitcoin.Transaction();
// add all outputs
function addOutput(e, i) {transaction.addOutput(toAddresses[i],amounts[i]);}
toAddresses.map(addOutput);
// Choose inputs
var unspent = sortUnspentOutputs(unspentOutputs);
var accum = 0;
var subTotal = 0;
var nIns = 0;
var nOuts = toAddresses.length + 1; // assumed one change output
for (var i = 0; i < unspent.length; i++) {
var output = unspent[i];
transaction.addInput(output.hash, output.index);
nIns += 1;
this.fee = Helpers.isNumber(forcedFee) ? forcedFee : Helpers.guessFee(nIns, nOuts, MyWallet.wallet.fee_per_kb);
// Generate address from output script and add to private list so we can check if the private keys match the inputs later
var script = Bitcoin.Script.fromHex(output.script);
assert.notEqual(Bitcoin.scripts.classifyOutput(script), 'nonstandard', 'Strange Script');
var address = Bitcoin.Address.fromOutputScript(script).toString();
assert(address, 'Unable to decode output address from transaction hash ' + output.tx_hash);
this.addressesOfInputs.push(address);
// Add to list of needed private keys
if (output.xpub) {
this.pathsOfNeededPrivateKeys.push(output.xpub.path);
}
else {
this.addressesOfNeededPrivateKeys.push(address);
}
accum += output.value;
subTotal = this.amount + this.fee;
if (accum >= subTotal) {
var change = accum - subTotal;
// Consume the change if it would create a very small none standard output
if (change > network.dustThreshold) {
assert(changeAddress, 'No change address specified');
transaction.addOutput(changeAddress, change);
}
break;
}
}
if (accum < subTotal) {
throw { error: 500, message: 'Insufficient funds. Value Needed ' + subTotal / 100000000 + 'BTC' +'. Available amount ' + accum / 100000000 + 'BTC'};
}
this.transaction = transaction;
};
Transaction.prototype.addPrivateKeys = function(privateKeys) {
assert.equal(privateKeys.length, this.addressesOfInputs.length, 'Number of private keys needs to match inputs');
for (var i = 0; i < privateKeys.length; i++) {
assert.equal(this.addressesOfInputs[i], privateKeys[i].pub.getAddress().toBase58Check(), 'Private key does not match bitcoin address ' + this.addressesOfInputs[i] + '!=' + privateKeys[i].pub.getAddress().toBase58Check());
}
this.privateKeys = privateKeys;
};
/**
* Shuffles the outputs of a transaction so that they receive and change
* addresses are in random order.
*/
Transaction.prototype.randomizeOutputs = function () {
function randomNumberBetweenZeroAnd(i) {
assert(i < Math.pow(2, 16), 'Cannot shuffle more outputs than one transaction can handle');
var randArray = randomBytes(2);
var rand = randArray[0] << 8 | randArray[1];
return rand%i;
}
function shuffle(o){
for(var j, x, i = o.length; i > 1; j = randomNumberBetweenZeroAnd(i), x = o[--i], o[i] = o[j], o[j] = x);
return o;
}
shuffle(this.transaction.outs);
};
/**
* BIP69: Sort outputs lexicographycally
*/
Transaction.prototype.sortBIP69 = function (){
var compareInputs = function(a, b) {
var hasha = new Buffer(a[0].hash);
var hashb = new Buffer(b[0].hash);
var x = [].reverse.call(hasha);
var y = [].reverse.call(hashb);
return x.compare(y) || a[0].index - b[0].index
};
var compareOutputs = function(a, b) {
return (a.value - b.value) || (a.script.buffer).compare(b.script.buffer)
};
var mix = Helpers.zip3(this.transaction.ins, this.privateKeys, this.addressesOfInputs);
mix.sort(compareInputs);
this.transaction.ins = mix.map(function(a){return a[0];});
this.privateKeys = mix.map(function(a){return a[1];});
this.addressesOfInputs = mix.map(function(a){return a[2];});
this.transaction.outs.sort(compareOutputs);
};
/**
* Sign the transaction
* @return {Object} Signed transaction
*/
Transaction.prototype.sign = function() {
assert(this.privateKeys, 'Need private keys to sign transaction');
assert.equal(this.privateKeys.length, this.transaction.ins.length, 'Number of private keys needs to match inputs');
for (var i = 0; i < this.privateKeys.length; i++) {
assert.equal(this.addressesOfInputs[i], this.privateKeys[i].pub.getAddress().toBase58Check(), 'Private key does not match bitcoin address ' + this.addressesOfInputs[i] + '!=' + this.privateKeys[i].pub.getAddress().toBase58Check());
}
var listener = this.listener;
listener && typeof(listener.on_begin_signing) === 'function' && listener.on_begin_signing();
var transaction = this.transaction;
for (var i = 0; i < transaction.ins.length; i++) {
listener && typeof(listener.on_sign_progress) === 'function' && listener.on_sign_progress(i+1);
var key = this.privateKeys[i];
transaction.sign(i, key);
assert(transaction.ins[i].script, 'Error creating input script');
}
listener && typeof(listener.on_finish_signing) === 'function' && listener.on_finish_signing();
return transaction;
};
function sortUnspentOutputs(unspentOutputs) {
var unspent = [];
for (var key in unspentOutputs) {
var output = unspentOutputs[key];
if (!output.pending) {
unspent.push(output);
}
}
unspent.sort(function(o1, o2){
return o2.value - o1.value;
});
return unspent;
}
module.exports = Transaction;