1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | import * as bip32 from 'bip32';
|
15 | import * as Bluebird from 'bluebird';
|
16 | import * as utxolib from '@bitgo/utxo-lib';
|
17 | import * as _ from 'lodash';
|
18 | import { VirtualSizes } from '@bitgo/unspents';
|
19 | import { getAddressP2PKH, getNetwork } from './bitcoin';
|
20 | import debugLib = require('debug');
|
21 | const debug = debugLib('bitgo:v1:txb');
|
22 | import { common } from '@bitgo/sdk-core';
|
23 | import { sanitizeLegacyPath } from '@bitgo/sdk-api';
|
24 |
|
25 | interface BaseOutput {
|
26 | amount: number;
|
27 | travelInfo?: any;
|
28 | }
|
29 |
|
30 | interface AddressOutput extends BaseOutput {
|
31 | address: string;
|
32 | }
|
33 |
|
34 | interface ScriptOutput extends BaseOutput {
|
35 | script: Buffer;
|
36 | }
|
37 |
|
38 | type Output = AddressOutput | ScriptOutput;
|
39 |
|
40 | interface BitGoUnspent {
|
41 | value: number;
|
42 | tx_hash: Buffer;
|
43 | tx_output_n: number;
|
44 | }
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 | exports.createTransaction = function (params) {
|
66 | const minConfirms = params.minConfirms || 0;
|
67 | const validate = params.validate === undefined ? true : params.validate;
|
68 | let recipients: { address: string; amount: number; script?: string; travelInfo?: any; }[] = [];
|
69 | let opReturns: { message: string; amount: number; }[] = [];
|
70 | let extraChangeAmounts: number[] = [];
|
71 | let estTxSize: number;
|
72 | let travelInfos;
|
73 |
|
74 |
|
75 | if (!_.isObject(params.wallet) ||
|
76 | (params.fee && !_.isNumber(params.fee)) ||
|
77 | (params.feeRate && !_.isNumber(params.feeRate)) ||
|
78 | !_.isInteger(minConfirms) ||
|
79 | (params.forceChangeAtEnd && !_.isBoolean(params.forceChangeAtEnd)) ||
|
80 | (params.changeAddress && !_.isString(params.changeAddress)) ||
|
81 | (params.noSplitChange && !_.isBoolean(params.noSplitChange)) ||
|
82 | (params.targetWalletUnspents && !_.isInteger(params.targetWalletUnspents)) ||
|
83 | (validate && !_.isBoolean(validate)) ||
|
84 | (params.enforceMinConfirmsForChange && !_.isBoolean(params.enforceMinConfirmsForChange)) ||
|
85 | (params.minUnspentSize && !_.isNumber(params.minUnspentSize)) ||
|
86 | (params.maxFeeRate && !_.isNumber(params.maxFeeRate)) ||
|
87 |
|
88 | (params.unspents && (!Array.isArray(params.unspents) || params.unspents.length < 1)) ||
|
89 | (params.feeTxConfirmTarget && !_.isInteger(params.feeTxConfirmTarget)) ||
|
90 | (params.instant && !_.isBoolean(params.instant)) ||
|
91 | (params.bitgoFee && !_.isObject(params.bitgoFee)) ||
|
92 | (params.unspentsFetchParams && !_.isObject(params.unspentsFetchParams))
|
93 | ) {
|
94 | throw new Error('invalid argument');
|
95 | }
|
96 |
|
97 | const bitgo = params.wallet.bitgo;
|
98 | const constants = bitgo.getConstants();
|
99 | const network = getNetwork(common.Environments[bitgo.getEnv()].network);
|
100 |
|
101 |
|
102 |
|
103 | let feeSingleKeySourceAddress;
|
104 | let feeSingleKeyInputAmount = 0;
|
105 | if (params.feeSingleKeySourceAddress) {
|
106 | try {
|
107 | utxolib.address.fromBase58Check(params.feeSingleKeySourceAddress, network);
|
108 | feeSingleKeySourceAddress = params.feeSingleKeySourceAddress;
|
109 | } catch (e) {
|
110 | throw new Error('invalid bitcoin address: ' + params.feeSingleKeySourceAddress);
|
111 | }
|
112 | }
|
113 |
|
114 | if (params.feeSingleKeyWIF) {
|
115 | const feeSingleKey = utxolib.ECPair.fromWIF(params.feeSingleKeyWIF, network as utxolib.BitcoinJSNetwork);
|
116 | feeSingleKeySourceAddress = getAddressP2PKH(feeSingleKey);
|
117 |
|
118 | if (params.feeSingleKeySourceAddress &&
|
119 | params.feeSingleKeySourceAddress !== feeSingleKeySourceAddress) {
|
120 | throw new Error('feeSingleKeySourceAddress: ' + params.feeSingleKeySourceAddress +
|
121 | ' did not correspond to address of feeSingleKeyWIF: ' + feeSingleKeySourceAddress);
|
122 | }
|
123 | }
|
124 |
|
125 | if (!_.isObject(params.recipients)) {
|
126 | throw new Error('recipients must be array of { address: abc, amount: 100000 } objects');
|
127 | }
|
128 |
|
129 | let feeParamsDefined = 0;
|
130 | if (!_.isUndefined(params.fee)) {
|
131 | feeParamsDefined++;
|
132 | }
|
133 |
|
134 | if (!_.isUndefined(params.feeRate)) {
|
135 | feeParamsDefined++;
|
136 | }
|
137 |
|
138 | if (!_.isUndefined(params.feeTxConfirmTarget)) {
|
139 | feeParamsDefined++;
|
140 | }
|
141 |
|
142 | if (feeParamsDefined > 1) {
|
143 | throw new Error('cannot specify more than one of fee, feeRate and feeTxConfirmTarget');
|
144 | }
|
145 |
|
146 | if (_.isUndefined(params.maxFeeRate)) {
|
147 | params.maxFeeRate = constants.maxFeeRate;
|
148 | }
|
149 |
|
150 |
|
151 | if (!(params.recipients instanceof Array)) {
|
152 | recipients = [];
|
153 | Object.keys(params.recipients).forEach(function (destinationAddress) {
|
154 | const amount = params.recipients[destinationAddress];
|
155 | recipients.push({ address: destinationAddress, amount: amount });
|
156 | });
|
157 | } else {
|
158 | recipients = params.recipients;
|
159 | }
|
160 |
|
161 | if (params.opReturns) {
|
162 | if (!(params.opReturns instanceof Array)) {
|
163 | opReturns = [];
|
164 | Object.keys(params.opReturns).forEach(function (message) {
|
165 | const amount = params.opReturns[message];
|
166 | opReturns.push({ message, amount });
|
167 | });
|
168 | } else {
|
169 | opReturns = params.opReturns;
|
170 | }
|
171 | }
|
172 |
|
173 | if (recipients.length === 0 && opReturns.length === 0) {
|
174 | throw new Error('must have at least one recipient');
|
175 | }
|
176 |
|
177 | let fee = params.fee;
|
178 | let feeRate = params.feeRate;
|
179 |
|
180 |
|
181 | const shouldComputeBestFee = (_.isUndefined(fee));
|
182 |
|
183 | let totalOutputAmount = 0;
|
184 |
|
185 | recipients.forEach(function (recipient) {
|
186 | if (_.isString(recipient.address)) {
|
187 | try {
|
188 | utxolib.address.fromBase58Check(recipient.address, network);
|
189 | } catch (e) {
|
190 | throw new Error('invalid bitcoin address: ' + recipient.address);
|
191 | }
|
192 | if (!!recipient.script) {
|
193 |
|
194 | if (utxolib.address.toOutputScript(recipient.address, network).toString('hex') !== recipient.script) {
|
195 | throw new Error('both script and address provided but they did not match: ' + recipient.address + ' ' + recipient.script);
|
196 | }
|
197 | }
|
198 | }
|
199 | if (!_.isInteger(recipient.amount) || recipient.amount < 0) {
|
200 | throw new Error('invalid amount for ' + recipient.address + ': ' + recipient.amount);
|
201 | }
|
202 | totalOutputAmount += recipient.amount;
|
203 | });
|
204 |
|
205 | opReturns.forEach(function (opReturn) {
|
206 | totalOutputAmount += opReturn.amount;
|
207 | });
|
208 |
|
209 | let bitgoFeeInfo = params.bitgoFee;
|
210 | if (bitgoFeeInfo &&
|
211 | (!_.isInteger(bitgoFeeInfo.amount) || !_.isString(bitgoFeeInfo.address))) {
|
212 | throw new Error('invalid bitgoFeeInfo');
|
213 | }
|
214 |
|
215 |
|
216 | let totalAmount = totalOutputAmount + (fee || 0);
|
217 |
|
218 |
|
219 | let unspents;
|
220 |
|
221 |
|
222 | let totalUnspentsCount;
|
223 |
|
224 |
|
225 | let fetchedUnspentsCount;
|
226 |
|
227 |
|
228 | let zeroConfUnspentTxIds;
|
229 |
|
230 |
|
231 | let inputAmount;
|
232 |
|
233 | let changeOutputs: Output[] = [];
|
234 |
|
235 |
|
236 | let transaction = utxolib.bitgo.createTransactionBuilderForNetwork(network);
|
237 |
|
238 | const getBitGoFee = function () {
|
239 | return Bluebird.try(function () {
|
240 | if (bitgoFeeInfo) {
|
241 | return;
|
242 | }
|
243 | return params.wallet.getBitGoFee({ amount: totalOutputAmount, instant: params.instant })
|
244 | .then(function (result) {
|
245 | if (result && result.fee > 0) {
|
246 | bitgoFeeInfo = {
|
247 | amount: result.fee,
|
248 | };
|
249 | }
|
250 | });
|
251 | })
|
252 | .then(function () {
|
253 | if (bitgoFeeInfo && bitgoFeeInfo.amount > 0) {
|
254 | totalAmount += bitgoFeeInfo.amount;
|
255 | }
|
256 | });
|
257 | };
|
258 |
|
259 | const getBitGoFeeAddress = function () {
|
260 | return Bluebird.try(function () {
|
261 |
|
262 | if (!bitgoFeeInfo || bitgoFeeInfo.address) {
|
263 | return;
|
264 | }
|
265 | return bitgo.getBitGoFeeAddress()
|
266 | .then(function (result) {
|
267 | bitgoFeeInfo.address = result.address;
|
268 | });
|
269 | });
|
270 | };
|
271 |
|
272 |
|
273 |
|
274 | const getDynamicFeeRateEstimate = function () {
|
275 | if (params.feeTxConfirmTarget || !feeParamsDefined) {
|
276 | return bitgo.estimateFee({
|
277 | numBlocks: params.feeTxConfirmTarget,
|
278 | maxFee: params.maxFeeRate,
|
279 | inputs: zeroConfUnspentTxIds,
|
280 | txSize: estTxSize,
|
281 | cpfpAware: true,
|
282 | })
|
283 | .then(function (result) {
|
284 | const estimatedFeeRate = result.cpfpFeePerKb;
|
285 | const minimum = params.instant ? Math.max(constants.minFeeRate, constants.minInstantFeeRate) : constants.minFeeRate;
|
286 |
|
287 |
|
288 | const padding = 5000;
|
289 | if (estimatedFeeRate < minimum) {
|
290 | console.log(new Date() + ': Error when estimating fee for send from ' + params.wallet.id() + ', it was too low - ' + estimatedFeeRate);
|
291 | feeRate = minimum + padding;
|
292 | } else if (estimatedFeeRate > params.maxFeeRate) {
|
293 | feeRate = params.maxFeeRate - padding;
|
294 | } else {
|
295 | feeRate = estimatedFeeRate;
|
296 | }
|
297 | return feeRate;
|
298 | })
|
299 | .catch(function (e) {
|
300 |
|
301 | if (_.includes(e.message, 'invalid txSize')) {
|
302 | return Bluebird.reject(e);
|
303 | } else {
|
304 |
|
305 | feeRate = constants.fallbackFeeRate;
|
306 | console.log('Error estimating fee for send from ' + params.wallet.id() + ': ' + e.message);
|
307 | return Bluebird.resolve();
|
308 | }
|
309 | });
|
310 | }
|
311 | };
|
312 |
|
313 |
|
314 |
|
315 | const getUnspents = function () {
|
316 |
|
317 | if (params.unspents) {
|
318 | unspents = params.unspents;
|
319 | return;
|
320 | }
|
321 |
|
322 |
|
323 | const options = _.merge({}, params.unspentsFetchParams || {}, {
|
324 | target: totalAmount,
|
325 | minSize: params.minUnspentSize || 0,
|
326 | instant: params.instant,
|
327 | targetWalletUnspents: params.targetWalletUnspents,
|
328 | });
|
329 | if (params.instant) {
|
330 | options.instant = params.instant;
|
331 | }
|
332 |
|
333 | return params.wallet.unspentsPaged(options)
|
334 | .then(function (results) {
|
335 | totalUnspentsCount = results.total;
|
336 | fetchedUnspentsCount = results.count;
|
337 | unspents = results.unspents.filter(function (u) {
|
338 | const confirms = u.confirmations || 0;
|
339 | if (!params.enforceMinConfirmsForChange && u.isChange) {
|
340 | return true;
|
341 | }
|
342 | return confirms >= minConfirms;
|
343 | });
|
344 |
|
345 |
|
346 | if (unspents.length === 0) {
|
347 | throw Error('0 unspents available for transaction creation');
|
348 | }
|
349 |
|
350 |
|
351 | zeroConfUnspentTxIds = _(results.unspents).filter(function (u) {
|
352 | return !u.confirmations;
|
353 | }).map(function (u) {
|
354 | return u.tx_hash + ':' + u.tx_output_n;
|
355 | }).value();
|
356 | if (_.isEmpty(zeroConfUnspentTxIds)) {
|
357 |
|
358 |
|
359 | zeroConfUnspentTxIds = undefined;
|
360 | }
|
361 |
|
362 |
|
363 | if (!params.noSplitChange && params.splitChangeSize !== 0) {
|
364 | extraChangeAmounts = results.extraChangeAmounts || [];
|
365 | }
|
366 | });
|
367 | };
|
368 |
|
369 |
|
370 | let feeSingleKeyUnspents: BitGoUnspent[] = [];
|
371 | const getUnspentsForSingleKey = function () {
|
372 | if (feeSingleKeySourceAddress) {
|
373 | let feeTarget = 0.01e8;
|
374 | if (params.instant) {
|
375 | feeTarget += totalAmount * 0.001;
|
376 | }
|
377 | return bitgo.get(bitgo.url('/address/' + feeSingleKeySourceAddress + '/unspents?target=' + feeTarget))
|
378 | .then(function (response) {
|
379 | if (response.body.total <= 0) {
|
380 | throw new Error('No unspents available in single key fee source');
|
381 | }
|
382 | feeSingleKeyUnspents = response.body.unspents;
|
383 | });
|
384 | }
|
385 | };
|
386 |
|
387 | let minerFeeInfo: any = {};
|
388 | let txInfo: any = {};
|
389 |
|
390 |
|
391 |
|
392 | let feeSingleKeyUnspentsUsed: BitGoUnspent[] = [];
|
393 |
|
394 | const collectInputs = function () {
|
395 | if (!unspents.length) {
|
396 | throw new Error('no unspents available on wallet');
|
397 | }
|
398 | inputAmount = 0;
|
399 |
|
400 |
|
401 | return Bluebird.try(function () {
|
402 |
|
403 | if (_.isNumber(params.feeRate) || _.isNumber(params.originalFeeRate)) {
|
404 | return (!_.isUndefined(params.feeRate) ? params.feeRate : params.originalFeeRate);
|
405 | } else {
|
406 | return bitgo.estimateFee({
|
407 | numBlocks: params.feeTxConfirmTarget,
|
408 | maxFee: params.maxFeeRate,
|
409 | })
|
410 | .then(function (feeRateEstimate) {
|
411 | return feeRateEstimate.feePerKb;
|
412 | });
|
413 | }
|
414 | }).then(function (feeRate) {
|
415 |
|
416 | let minInputValue = 0;
|
417 | if (_.isInteger(params.minUnspentSize)) {
|
418 | minInputValue = params.minUnspentSize;
|
419 | }
|
420 |
|
421 | let prunedUnspentCount = 0;
|
422 | const originalUnspentCount = unspents.length;
|
423 | unspents = _.filter(unspents, function (unspent) {
|
424 | const isSegwitInput = !!unspent.witnessScript;
|
425 | const currentInputSize = isSegwitInput ? VirtualSizes.txP2shP2wshInputSize : VirtualSizes.txP2shInputSize;
|
426 | const feeBasedMinInputValue = (feeRate * currentInputSize) / 1000;
|
427 | const currentMinInputValue = Math.max(minInputValue, feeBasedMinInputValue);
|
428 | if (currentMinInputValue > unspent.value) {
|
429 |
|
430 | const pruneDetails = {
|
431 | generalMinInputValue: minInputValue,
|
432 | feeBasedMinInputValue,
|
433 | currentMinInputValue,
|
434 | feeRate,
|
435 | inputSize: currentInputSize,
|
436 | unspent: unspent,
|
437 | };
|
438 | console.log(`pruning unspent: ${JSON.stringify(pruneDetails, null, 4)}`);
|
439 | prunedUnspentCount++;
|
440 | return false;
|
441 | }
|
442 | return true;
|
443 | });
|
444 |
|
445 | if (prunedUnspentCount > 0) {
|
446 | console.log(`pruned ${prunedUnspentCount} out of ${originalUnspentCount} unspents`);
|
447 | }
|
448 |
|
449 | if (unspents.length === 0) {
|
450 | throw new Error('insufficient funds');
|
451 | }
|
452 | let segwitInputCount = 0;
|
453 | unspents.every(function (unspent) {
|
454 | if (unspent.witnessScript) {
|
455 | segwitInputCount++;
|
456 | }
|
457 | inputAmount += unspent.value;
|
458 | transaction.addInput(unspent.tx_hash, unspent.tx_output_n, 0xffffffff);
|
459 |
|
460 | return (inputAmount < (feeSingleKeySourceAddress ? totalOutputAmount : totalAmount));
|
461 | });
|
462 |
|
463 |
|
464 | if (feeSingleKeySourceAddress) {
|
465 |
|
466 | feeSingleKeyInputAmount = 0;
|
467 | feeSingleKeyUnspentsUsed = [];
|
468 | feeSingleKeyUnspents.every(function (unspent) {
|
469 | feeSingleKeyInputAmount += unspent.value;
|
470 | inputAmount += unspent.value;
|
471 | transaction.addInput(unspent.tx_hash, unspent.tx_output_n);
|
472 | feeSingleKeyUnspentsUsed.push(unspent);
|
473 |
|
474 | return (feeSingleKeyInputAmount < (fee + (bitgoFeeInfo ? bitgoFeeInfo.amount : 0)));
|
475 | });
|
476 | }
|
477 |
|
478 | txInfo = {
|
479 | nP2shInputs: transaction.tx.ins.length - (feeSingleKeySourceAddress ? 1 : 0) - segwitInputCount,
|
480 | nP2shP2wshInputs: segwitInputCount,
|
481 | nP2pkhInputs: feeSingleKeySourceAddress ? 1 : 0,
|
482 | nOutputs: (
|
483 | recipients.length + 1 +
|
484 | extraChangeAmounts.length +
|
485 | (bitgoFeeInfo && bitgoFeeInfo.amount > 0 ? 1 : 0) +
|
486 | (feeSingleKeySourceAddress ? 1 : 0)
|
487 | ),
|
488 | };
|
489 |
|
490 | estTxSize = estimateTransactionSize({
|
491 | nP2shInputs: txInfo.nP2shInputs,
|
492 | nP2shP2wshInputs: txInfo.nP2shP2wshInputs,
|
493 | nP2pkhInputs: txInfo.nP2pkhInputs,
|
494 | nOutputs: txInfo.nOutputs,
|
495 | });
|
496 | }).then(getDynamicFeeRateEstimate)
|
497 | .then(function () {
|
498 | minerFeeInfo = exports.calculateMinerFeeInfo({
|
499 | bitgo: params.wallet.bitgo,
|
500 | feeRate: feeRate,
|
501 | nP2shInputs: txInfo.nP2shInputs,
|
502 | nP2shP2wshInputs: txInfo.nP2shP2wshInputs,
|
503 | nP2pkhInputs: txInfo.nP2pkhInputs,
|
504 | nOutputs: txInfo.nOutputs,
|
505 | });
|
506 |
|
507 | if (shouldComputeBestFee) {
|
508 | const approximateFee = minerFeeInfo.fee;
|
509 | const shouldRecurse = _.isUndefined(fee) || approximateFee > fee;
|
510 | fee = approximateFee;
|
511 |
|
512 | totalAmount = fee + totalOutputAmount;
|
513 | if (bitgoFeeInfo) {
|
514 | totalAmount += bitgoFeeInfo.amount;
|
515 | }
|
516 | if (shouldRecurse) {
|
517 |
|
518 | inputAmount = 0;
|
519 | transaction = utxolib.bitgo.createTransactionBuilderForNetwork(network);
|
520 | return collectInputs();
|
521 | }
|
522 | }
|
523 |
|
524 | const totalFee = fee + (bitgoFeeInfo ? bitgoFeeInfo.amount : 0);
|
525 |
|
526 | if (feeSingleKeySourceAddress) {
|
527 | const summedSingleKeyUnspents = _.sumBy(feeSingleKeyUnspents, 'value');
|
528 | if (totalFee > summedSingleKeyUnspents) {
|
529 | const err: any = new Error('Insufficient fee amount available in single key fee source: ' + summedSingleKeyUnspents);
|
530 | err.result = {
|
531 | fee: fee,
|
532 | feeRate: feeRate,
|
533 | estimatedSize: minerFeeInfo.size,
|
534 | available: inputAmount,
|
535 | bitgoFee: bitgoFeeInfo,
|
536 | txInfo: txInfo,
|
537 | };
|
538 | return Bluebird.reject(err);
|
539 | }
|
540 | }
|
541 |
|
542 | if (inputAmount < (feeSingleKeySourceAddress ? totalOutputAmount : totalAmount)) {
|
543 |
|
544 |
|
545 |
|
546 |
|
547 |
|
548 |
|
549 |
|
550 | let err;
|
551 | if (totalUnspentsCount === fetchedUnspentsCount) {
|
552 |
|
553 | err = new Error('Insufficient funds');
|
554 | } else {
|
555 |
|
556 | err = new Error(`Transaction size too large due to too many unspents. Can send only ${inputAmount} satoshis in this transaction`);
|
557 | }
|
558 | err.result = {
|
559 | fee: fee,
|
560 | feeRate: feeRate,
|
561 | estimatedSize: minerFeeInfo.size,
|
562 | available: inputAmount,
|
563 | bitgoFee: bitgoFeeInfo,
|
564 | txInfo: txInfo,
|
565 | };
|
566 | return Bluebird.reject(err);
|
567 | }
|
568 | });
|
569 | };
|
570 |
|
571 |
|
572 | const collectOutputs = function () {
|
573 | if (minerFeeInfo.size >= 90000) {
|
574 | throw new Error('transaction too large: estimated size ' + minerFeeInfo.size + ' bytes');
|
575 | }
|
576 |
|
577 | const outputs: Output[] = [];
|
578 |
|
579 | recipients.forEach(function (recipient) {
|
580 | let script;
|
581 | if (_.isString(recipient.address)) {
|
582 | script = utxolib.address.toOutputScript(recipient.address, network);
|
583 | } else if (_.isObject(recipient.script)) {
|
584 | script = recipient.script;
|
585 | } else {
|
586 | throw new Error('neither recipient address nor script was provided');
|
587 | }
|
588 |
|
589 |
|
590 | let travelInfo;
|
591 | if (!_.isEmpty(recipient.travelInfo)) {
|
592 | travelInfo = recipient.travelInfo;
|
593 |
|
594 | bitgo.travelRule().validateTravelInfo(travelInfo);
|
595 | }
|
596 |
|
597 | outputs.push({
|
598 | script: script,
|
599 | amount: recipient.amount,
|
600 | travelInfo: travelInfo,
|
601 | });
|
602 | });
|
603 |
|
604 | opReturns.forEach(function ({ message, amount }) {
|
605 | const script = utxolib.script.fromASM('OP_RETURN ' + Buffer.from(message).toString('hex'));
|
606 | outputs.push({ script, amount });
|
607 | });
|
608 |
|
609 | const getChangeOutputs = function (changeAmount: number): Output[] | Bluebird<Output[]> {
|
610 | if (changeAmount < 0) {
|
611 | throw new Error('negative change amount: ' + changeAmount);
|
612 | }
|
613 |
|
614 | const result: Output[] = [];
|
615 |
|
616 | if (feeSingleKeySourceAddress) {
|
617 | const feeSingleKeyWalletChangeAmount = feeSingleKeyInputAmount - (fee + (bitgoFeeInfo ? bitgoFeeInfo.amount : 0));
|
618 | if (feeSingleKeyWalletChangeAmount >= constants.minOutputSize) {
|
619 | result.push({ address: feeSingleKeySourceAddress, amount: feeSingleKeyWalletChangeAmount });
|
620 | changeAmount = changeAmount - feeSingleKeyWalletChangeAmount;
|
621 | }
|
622 | }
|
623 |
|
624 | if (changeAmount < constants.minOutputSize) {
|
625 |
|
626 | return result;
|
627 | }
|
628 |
|
629 | if (params.wallet.type() === 'safe') {
|
630 | return params.wallet.addresses()
|
631 | .then(function (response) {
|
632 | result.push({ address: response.addresses[0].address, amount: changeAmount });
|
633 | return result;
|
634 | });
|
635 | }
|
636 |
|
637 | let extraChangeTotal = _.sum(extraChangeAmounts);
|
638 |
|
639 | if (extraChangeTotal > changeAmount) {
|
640 | extraChangeAmounts = [];
|
641 | extraChangeTotal = 0;
|
642 | }
|
643 |
|
644 |
|
645 | const allChangeAmounts = extraChangeAmounts.slice(0);
|
646 | allChangeAmounts.push(changeAmount - extraChangeTotal);
|
647 |
|
648 |
|
649 | const addChangeOutputs = function (): Output[] | Bluebird<Output[]> {
|
650 | const thisAmount = allChangeAmounts.shift();
|
651 | if (!thisAmount) {
|
652 | return result;
|
653 | }
|
654 | return Bluebird.try(function () {
|
655 | if (params.changeAddress) {
|
656 |
|
657 | return params.changeAddress;
|
658 | } else {
|
659 |
|
660 |
|
661 | const changeChain = params.wallet.getChangeChain(params);
|
662 | return params.wallet.createAddress({ chain: changeChain, validate: validate })
|
663 | .then(function (result) {
|
664 | return result.address;
|
665 | });
|
666 | }
|
667 | })
|
668 | .then(function (address) {
|
669 | result.push({ address: address, amount: thisAmount });
|
670 | return addChangeOutputs();
|
671 | });
|
672 | };
|
673 |
|
674 | return addChangeOutputs();
|
675 | };
|
676 |
|
677 |
|
678 | return Bluebird.try(function () {
|
679 | return getChangeOutputs(inputAmount - totalAmount);
|
680 | })
|
681 | .then(function (result) {
|
682 | changeOutputs = result;
|
683 | const extraOutputs = changeOutputs.concat([]);
|
684 | if (bitgoFeeInfo && bitgoFeeInfo.amount > 0) {
|
685 | extraOutputs.push(bitgoFeeInfo);
|
686 | }
|
687 | extraOutputs.forEach(function (output) {
|
688 | if ((output as AddressOutput).address) {
|
689 | (output as ScriptOutput).script =
|
690 | utxolib.address.toOutputScript((output as AddressOutput).address, network);
|
691 | }
|
692 |
|
693 |
|
694 | const outputIndex = params.forceChangeAtEnd ? outputs.length : _.random(0, outputs.length);
|
695 | outputs.splice(outputIndex, 0, output);
|
696 | });
|
697 |
|
698 |
|
699 | outputs.forEach(function (output) {
|
700 | transaction.addOutput((output as ScriptOutput).script, output.amount);
|
701 | });
|
702 |
|
703 | travelInfos = _(outputs).map(function (output, index) {
|
704 | const result = output.travelInfo;
|
705 | if (!result) {
|
706 | return undefined;
|
707 | }
|
708 | result.outputIndex = index;
|
709 | return result;
|
710 | })
|
711 | .filter()
|
712 | .value();
|
713 | });
|
714 | };
|
715 |
|
716 |
|
717 | const serialize = function () {
|
718 |
|
719 | const pickedUnspents: any = _.map(unspents, function (unspent) {
|
720 | return _.pick(unspent, ['chainPath', 'redeemScript', 'instant', 'witnessScript', 'script', 'value']);
|
721 | });
|
722 | const prunedUnspents = _.slice(pickedUnspents, 0, transaction.tx.ins.length - feeSingleKeyUnspentsUsed.length);
|
723 | _.each(feeSingleKeyUnspentsUsed, function (feeUnspent) {
|
724 | prunedUnspents.push({ redeemScript: false, chainPath: false });
|
725 | });
|
726 | const result: any = {
|
727 | transactionHex: transaction.buildIncomplete().toHex(),
|
728 | unspents: prunedUnspents,
|
729 | fee: fee,
|
730 | changeAddresses: changeOutputs.map(function (co) {
|
731 | return _.pick(co, ['address', 'path', 'amount']);
|
732 | }),
|
733 | walletId: params.wallet.id(),
|
734 | walletKeychains: params.wallet.keychains,
|
735 | feeRate: feeRate,
|
736 | instant: params.instant,
|
737 | bitgoFee: bitgoFeeInfo,
|
738 | estimatedSize: minerFeeInfo.size,
|
739 | txInfo: txInfo,
|
740 | travelInfos: travelInfos,
|
741 | };
|
742 |
|
743 |
|
744 | if (result.instant && bitgoFeeInfo) {
|
745 | result.instantFee = _.pick(bitgoFeeInfo, ['amount', 'address']);
|
746 | }
|
747 |
|
748 | return result;
|
749 | };
|
750 |
|
751 | return Bluebird.try(function () {
|
752 | return getBitGoFee();
|
753 | })
|
754 | .then(function () {
|
755 | return Bluebird.all([getBitGoFeeAddress(), getUnspents(), getUnspentsForSingleKey()]);
|
756 | })
|
757 | .then(collectInputs)
|
758 | .then(collectOutputs)
|
759 | .then(serialize);
|
760 | };
|
761 |
|
762 |
|
763 |
|
764 |
|
765 |
|
766 |
|
767 |
|
768 |
|
769 |
|
770 |
|
771 |
|
772 |
|
773 |
|
774 | const estimateTransactionSize = function (params) {
|
775 | if (!_.isInteger(params.nP2shInputs) || params.nP2shInputs < 0) {
|
776 | throw new Error('expecting positive nP2shInputs');
|
777 | }
|
778 | if (!_.isInteger(params.nP2pkhInputs) || params.nP2pkhInputs < 0) {
|
779 | throw new Error('expecting positive nP2pkhInputs to be numeric');
|
780 | }
|
781 | if (!_.isInteger(params.nP2shP2wshInputs) || params.nP2shP2wshInputs < 0) {
|
782 | throw new Error('expecting positive nP2shP2wshInputs to be numeric');
|
783 | }
|
784 | if ((params.nP2shInputs + params.nP2shP2wshInputs) < 1) {
|
785 | throw new Error('expecting at least one nP2shInputs or nP2shP2wshInputs');
|
786 | }
|
787 | if (!_.isInteger(params.nOutputs) || params.nOutputs < 1) {
|
788 | throw new Error('expecting positive nOutputs');
|
789 | }
|
790 |
|
791 |
|
792 | const estimatedSize = VirtualSizes.txP2shInputSize * params.nP2shInputs +
|
793 | VirtualSizes.txP2shP2wshInputSize * (params.nP2shP2wshInputs || 0) +
|
794 | VirtualSizes.txP2pkhInputSizeUncompressedKey * (params.nP2pkhInputs || 0) +
|
795 | VirtualSizes.txP2pkhOutputSize * params.nOutputs +
|
796 |
|
797 | VirtualSizes.txOverheadSize + (params.nP2shP2wshInputs > 0 ? 1 : 0);
|
798 |
|
799 | return estimatedSize;
|
800 | };
|
801 |
|
802 |
|
803 |
|
804 |
|
805 |
|
806 |
|
807 |
|
808 |
|
809 |
|
810 |
|
811 |
|
812 |
|
813 |
|
814 |
|
815 |
|
816 |
|
817 |
|
818 |
|
819 | exports.calculateMinerFeeInfo = function (params) {
|
820 | const feeRateToUse = params.feeRate || params.bitgo.getConstants().fallbackFeeRate;
|
821 | const estimatedSize = estimateTransactionSize(params);
|
822 |
|
823 | return {
|
824 | size: estimatedSize,
|
825 | fee: Math.ceil(estimatedSize * feeRateToUse / 1000),
|
826 | feeRate: feeRateToUse,
|
827 | };
|
828 | };
|
829 |
|
830 |
|
831 |
|
832 |
|
833 |
|
834 |
|
835 |
|
836 |
|
837 |
|
838 |
|
839 |
|
840 |
|
841 |
|
842 |
|
843 |
|
844 |
|
845 |
|
846 | exports.signTransaction = function (params) {
|
847 | let keychain = params.keychain;
|
848 |
|
849 | const validate = (params.validate === undefined) ? true : params.validate;
|
850 | let privKey;
|
851 | if (!_.isString(params.transactionHex)) {
|
852 | throw new Error('expecting the transaction hex as a string');
|
853 | }
|
854 | if (!Array.isArray(params.unspents)) {
|
855 | throw new Error('expecting the unspents array');
|
856 | }
|
857 | if (!_.isBoolean(validate)) {
|
858 | throw new Error('expecting validate to be a boolean');
|
859 | }
|
860 | let network = getNetwork();
|
861 | const enableBCH = (_.isBoolean(params.forceBCH) && params.forceBCH === true);
|
862 |
|
863 | if (!_.isObject(keychain) || !_.isString((keychain as any).xprv)) {
|
864 | if (_.isString(params.signingKey)) {
|
865 | privKey = utxolib.ECPair.fromWIF(params.signingKey, network as utxolib.BitcoinJSNetwork);
|
866 | keychain = undefined;
|
867 | } else {
|
868 | throw new Error('expecting the keychain object with xprv');
|
869 | }
|
870 | }
|
871 |
|
872 | let feeSingleKey;
|
873 | if (params.feeSingleKeyWIF) {
|
874 | feeSingleKey = utxolib.ECPair.fromWIF(params.feeSingleKeyWIF, network as utxolib.BitcoinJSNetwork);
|
875 | }
|
876 |
|
877 | debug('Network: %O', network);
|
878 |
|
879 | if (enableBCH) {
|
880 | debug('Enabling BCH…');
|
881 | network = utxolib.networks.bitcoincash;
|
882 | debug('New network: %O', network);
|
883 | }
|
884 |
|
885 | const transaction = utxolib.bitgo.createTransactionFromHex(params.transactionHex, network);
|
886 | if (transaction.ins.length !== params.unspents.length) {
|
887 | throw new Error('length of unspents array should equal to the number of transaction inputs');
|
888 | }
|
889 |
|
890 |
|
891 | const isUtxoTx = _.isObject(transaction) && Array.isArray((transaction as any).ins);
|
892 | const areValidUnspents = _.isObject(params) && Array.isArray((params as any).unspents);
|
893 | if (isUtxoTx && areValidUnspents) {
|
894 |
|
895 | const inputValues = _.map((params as any).unspents, (u => _.pick(u, 'value')));
|
896 | transaction.ins.map((currentItem, index) => _.extend(currentItem, inputValues[index]));
|
897 | }
|
898 |
|
899 | let rootExtKey;
|
900 | if (keychain) {
|
901 | rootExtKey = bip32.fromBase58(keychain.xprv);
|
902 | }
|
903 |
|
904 | const txb = utxolib.bitgo.createTransactionBuilderFromTransaction(transaction);
|
905 |
|
906 | for (let index = 0; index < txb.tx.ins.length; ++index) {
|
907 | const currentUnspent = params.unspents[index];
|
908 | if (currentUnspent.redeemScript === false) {
|
909 |
|
910 | if (!feeSingleKey) {
|
911 | throw new Error('single key address used in input but feeSingleKeyWIF not provided');
|
912 | }
|
913 |
|
914 | if (enableBCH) {
|
915 | feeSingleKey.network = network;
|
916 | }
|
917 |
|
918 | txb.sign(index, feeSingleKey);
|
919 | continue;
|
920 | }
|
921 |
|
922 | if (currentUnspent.witnessScript && enableBCH) {
|
923 | throw new Error('BCH does not support segwit inputs');
|
924 | }
|
925 |
|
926 | const chainPath = currentUnspent.chainPath;
|
927 | if (rootExtKey) {
|
928 | const { walletSubPath = '/0/0' } = keychain;
|
929 | const path = sanitizeLegacyPath(keychain.path + walletSubPath + chainPath);
|
930 | privKey = rootExtKey.derivePath(path);
|
931 | }
|
932 |
|
933 | privKey.network = network;
|
934 |
|
935 |
|
936 |
|
937 |
|
938 | const subscript = Buffer.from(currentUnspent.redeemScript, 'hex');
|
939 | currentUnspent.validationScript = subscript;
|
940 |
|
941 |
|
942 |
|
943 |
|
944 | try {
|
945 | const witnessScript = currentUnspent.witnessScript ? Buffer.from(currentUnspent.witnessScript, 'hex') : undefined;
|
946 | const sigHash = utxolib.bitgo.getDefaultSigHash(network);
|
947 | txb.sign(index, privKey, subscript, sigHash, currentUnspent.value, witnessScript);
|
948 | } catch (e) {
|
949 |
|
950 | e.result = {
|
951 | unspent: currentUnspent,
|
952 | };
|
953 | e.message = `Failed to sign input #${index} - ${e.message} - ${JSON.stringify(e.result, null, 4)} - \n${e.stack}`;
|
954 | debug('input sign failed: %s', e.message);
|
955 | return Bluebird.reject(e);
|
956 | }
|
957 | }
|
958 |
|
959 | const partialTransaction = txb.buildIncomplete();
|
960 |
|
961 | if (validate) {
|
962 | partialTransaction.ins.forEach((input, index) => {
|
963 | const signatureCount = utxolib.bitgo.getSignatureVerifications(
|
964 | partialTransaction, index, params.unspents[index].value
|
965 | ).filter(v => v.signedBy !== undefined).length;
|
966 | if (signatureCount < 1) {
|
967 | throw new Error('expected at least one valid signature');
|
968 | }
|
969 | if (params.fullLocalSigning && signatureCount < 2) {
|
970 | throw new Error('fullLocalSigning set: expected at least two valid signatures');
|
971 | }
|
972 | });
|
973 | }
|
974 |
|
975 | return Bluebird.resolve({
|
976 | transactionHex: partialTransaction.toHex(),
|
977 | });
|
978 | };
|