1 |
|
2 |
|
3 |
|
4 |
|
5 | 'use strict'
|
6 |
|
7 | import { Address } from './address'
|
8 | import { Constants as Cst } from './constants'
|
9 | import { Bn } from './bn'
|
10 | import { HashCache } from './hash-cache'
|
11 | import { Script } from './script'
|
12 | import { SigOperations } from './sig-operations'
|
13 | import { Sig } from './sig'
|
14 | import { Struct } from './struct'
|
15 | import { Tx } from './tx'
|
16 | import { TxIn } from './tx-in'
|
17 | import { TxOut } from './tx-out'
|
18 | import { TxOutMap } from './tx-out-map'
|
19 | import { VarInt } from './var-int'
|
20 |
|
21 | const Constants = Cst.Default.TxBuilder
|
22 |
|
23 | class TxBuilder extends Struct {
|
24 | constructor (
|
25 | tx = new Tx(),
|
26 | txIns = [],
|
27 | txOuts = [],
|
28 | uTxOutMap = new TxOutMap(),
|
29 | sigOperations = new SigOperations(),
|
30 | changeScript,
|
31 | changeAmountBn,
|
32 | feeAmountBn,
|
33 | feePerKbNum = Constants.feePerKbNum,
|
34 | nLockTime = 0,
|
35 | versionBytesNum = 1,
|
36 | sigsPerInput = 1,
|
37 | dust = Constants.dust,
|
38 | dustChangeToFees = false,
|
39 | hashCache = new HashCache()
|
40 | ) {
|
41 | super({
|
42 | tx,
|
43 | txIns,
|
44 | txOuts,
|
45 | uTxOutMap,
|
46 | sigOperations,
|
47 | changeScript,
|
48 | changeAmountBn,
|
49 | feeAmountBn,
|
50 | feePerKbNum,
|
51 | nLockTime,
|
52 | versionBytesNum,
|
53 | sigsPerInput,
|
54 | dust,
|
55 | dustChangeToFees,
|
56 | hashCache
|
57 | })
|
58 | }
|
59 |
|
60 | toJSON () {
|
61 | const json = {}
|
62 | json.tx = this.tx.toHex()
|
63 | json.txIns = this.txIns.map(txIn => txIn.toHex())
|
64 | json.txOuts = this.txOuts.map(txOut => txOut.toHex())
|
65 | json.uTxOutMap = this.uTxOutMap.toJSON()
|
66 | json.sigOperations = this.sigOperations.toJSON()
|
67 | json.changeScript = this.changeScript ? this.changeScript.toHex() : undefined
|
68 | json.changeAmountBn = this.changeAmountBn ? this.changeAmountBn.toNumber() : undefined
|
69 | json.feeAmountBn = this.feeAmountBn ? this.feeAmountBn.toNumber() : undefined
|
70 | json.feePerKbNum = this.feePerKbNum
|
71 | json.sigsPerInput = this.sigsPerInput
|
72 | json.dust = this.dust
|
73 | json.dustChangeToFees = this.dustChangeToFees
|
74 | json.hashCache = this.hashCache.toJSON()
|
75 | return json
|
76 | }
|
77 |
|
78 | fromJSON (json) {
|
79 | this.tx = new Tx().fromHex(json.tx)
|
80 | this.txIns = json.txIns.map(txIn => TxIn.fromHex(txIn))
|
81 | this.txOuts = json.txOuts.map(txOut => TxOut.fromHex(txOut))
|
82 | this.uTxOutMap = new TxOutMap().fromJSON(json.uTxOutMap)
|
83 | this.sigOperations = new SigOperations().fromJSON(json.sigOperations)
|
84 | this.changeScript = json.changeScript ? new Script().fromHex(json.changeScript) : undefined
|
85 | this.changeAmountBn = json.changeAmountBn ? new Bn(json.changeAmountBn) : undefined
|
86 | this.feeAmountBn = json.feeAmountBn ? new Bn(json.feeAmountBn) : undefined
|
87 | this.feePerKbNum = json.feePerKbNum || this.feePerKbNum
|
88 | this.sigsPerInput = json.sigsPerInput || this.sigsPerInput
|
89 | this.dust = json.dust || this.dust
|
90 | this.dustChangeToFees =
|
91 | json.dustChangeToFees || this.dustChangeToFees
|
92 | this.hashCache = HashCache.fromJSON(json.hashCache)
|
93 | return this
|
94 | }
|
95 |
|
96 | setFeePerKbNum (feePerKbNum) {
|
97 | if (typeof feePerKbNum !== 'number' || feePerKbNum <= 0) {
|
98 | throw new Error('cannot set a fee of zero or less')
|
99 | }
|
100 | this.feePerKbNum = feePerKbNum
|
101 | return this
|
102 | }
|
103 |
|
104 | setChangeAddress (changeAddress) {
|
105 | this.changeScript = changeAddress.toTxOutScript()
|
106 | return this
|
107 | }
|
108 |
|
109 | setChangeScript (changeScript) {
|
110 | this.changeScript = changeScript
|
111 | return this
|
112 | }
|
113 |
|
114 | |
115 |
|
116 |
|
117 | setNLocktime (nLockTime) {
|
118 | this.nLockTime = nLockTime
|
119 | return this
|
120 | }
|
121 |
|
122 | setVersion (versionBytesNum) {
|
123 | this.versionBytesNum = versionBytesNum
|
124 | return this
|
125 | }
|
126 |
|
127 | |
128 |
|
129 |
|
130 |
|
131 |
|
132 | setDust (dust = Constants.dust) {
|
133 | this.dust = dust
|
134 | return this
|
135 | }
|
136 |
|
137 | |
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | sendDustChangeToFees (dustChangeToFees = false) {
|
146 | this.dustChangeToFees = dustChangeToFees
|
147 | return this
|
148 | }
|
149 |
|
150 | |
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 | importPartiallySignedTx (tx, uTxOutMap = this.uTxOutMap, sigOperations = this.sigOperations) {
|
159 | this.tx = tx
|
160 | this.uTxOutMap = uTxOutMap
|
161 | this.sigOperations = sigOperations
|
162 | return this
|
163 | }
|
164 |
|
165 | |
166 |
|
167 |
|
168 | inputFromScript (txHashBuf, txOutNum, txOut, script, nSequence) {
|
169 | if (
|
170 | !Buffer.isBuffer(txHashBuf) ||
|
171 | !(typeof txOutNum === 'number') ||
|
172 | !(txOut instanceof TxOut) ||
|
173 | !(script instanceof Script)
|
174 | ) {
|
175 | throw new Error('invalid one of: txHashBuf, txOutNum, txOut, script')
|
176 | }
|
177 | this.txIns.push(
|
178 | TxIn.fromProperties(txHashBuf, txOutNum, script, nSequence)
|
179 | )
|
180 | this.uTxOutMap.set(txHashBuf, txOutNum, txOut)
|
181 | return this
|
182 | }
|
183 |
|
184 | addSigOperation (txHashBuf, txOutNum, nScriptChunk, type, addressStr, nHashType) {
|
185 | this.sigOperations.addOne(txHashBuf, txOutNum, nScriptChunk, type, addressStr, nHashType)
|
186 | return this
|
187 | }
|
188 |
|
189 | |
190 |
|
191 |
|
192 |
|
193 | inputFromPubKeyHash (txHashBuf, txOutNum, txOut, pubKey, nSequence, nHashType) {
|
194 | if (
|
195 | !Buffer.isBuffer(txHashBuf) ||
|
196 | typeof txOutNum !== 'number' ||
|
197 | !(txOut instanceof TxOut)
|
198 | ) {
|
199 | throw new Error('invalid one of: txHashBuf, txOutNum, txOut')
|
200 | }
|
201 | this.txIns.push(
|
202 | new TxIn()
|
203 | .fromObject({ nSequence })
|
204 | .fromPubKeyHashTxOut(txHashBuf, txOutNum, txOut, pubKey)
|
205 | )
|
206 | this.uTxOutMap.set(txHashBuf, txOutNum, txOut)
|
207 | const addressStr = Address.fromTxOutScript(txOut.script).toString()
|
208 | this.addSigOperation(txHashBuf, txOutNum, 0, 'sig', addressStr, nHashType)
|
209 | this.addSigOperation(txHashBuf, txOutNum, 1, 'pubKey', addressStr)
|
210 | return this
|
211 | }
|
212 |
|
213 | |
214 |
|
215 |
|
216 |
|
217 | outputToAddress (valueBn, addr) {
|
218 | if (!(addr instanceof Address) || !(valueBn instanceof Bn)) {
|
219 | throw new Error('addr must be an Address, and valueBn must be a Bn')
|
220 | }
|
221 | const script = new Script().fromPubKeyHash(addr.hashBuf)
|
222 | this.outputToScript(valueBn, script)
|
223 | return this
|
224 | }
|
225 |
|
226 | |
227 |
|
228 |
|
229 |
|
230 | outputToScript (valueBn, script) {
|
231 | if (!(script instanceof Script) || !(valueBn instanceof Bn)) {
|
232 | throw new Error('script must be a Script, and valueBn must be a Bn')
|
233 | }
|
234 | const txOut = TxOut.fromProperties(valueBn, script)
|
235 | this.txOuts.push(txOut)
|
236 | return this
|
237 | }
|
238 |
|
239 | buildOutputs () {
|
240 | let outAmountBn = new Bn(0)
|
241 | this.txOuts.forEach(txOut => {
|
242 | if (txOut.valueBn.lt(this.dust) && !txOut.script.isOpReturn() && !txOut.script.isSafeDataOut()) {
|
243 | throw new Error('cannot create output lesser than dust')
|
244 | }
|
245 | outAmountBn = outAmountBn.add(txOut.valueBn)
|
246 | this.tx.addTxOut(txOut)
|
247 | })
|
248 | return outAmountBn
|
249 | }
|
250 |
|
251 | buildInputs (outAmountBn, extraInputsNum = 0) {
|
252 | let inAmountBn = new Bn(0)
|
253 | for (const txIn of this.txIns) {
|
254 | const txOut = this.uTxOutMap.get(txIn.txHashBuf, txIn.txOutNum)
|
255 | inAmountBn = inAmountBn.add(txOut.valueBn)
|
256 | this.tx.addTxIn(txIn)
|
257 | if (inAmountBn.geq(outAmountBn)) {
|
258 | if (extraInputsNum <= 0) {
|
259 | break
|
260 | }
|
261 | extraInputsNum--
|
262 | }
|
263 | }
|
264 | if (inAmountBn.lt(outAmountBn)) {
|
265 | throw new Error(
|
266 | 'not enough funds for outputs: inAmountBn ' +
|
267 | inAmountBn.toNumber() +
|
268 | ' outAmountBn ' +
|
269 | outAmountBn.toNumber()
|
270 | )
|
271 | }
|
272 | return inAmountBn
|
273 | }
|
274 |
|
275 |
|
276 |
|
277 |
|
278 | estimateSize () {
|
279 |
|
280 |
|
281 | const sigSize = 1 + 1 + 1 + 1 + 32 + 1 + 1 + 32 + 1 + 1
|
282 |
|
283 | const pubKeySize = 1 + 1 + 33
|
284 |
|
285 | let size = this.tx.toBuffer().length
|
286 |
|
287 | this.tx.txIns.forEach((txIn) => {
|
288 | const { txHashBuf, txOutNum } = txIn
|
289 | const sigOperations = this.sigOperations.get(txHashBuf, txOutNum)
|
290 | sigOperations.forEach((obj) => {
|
291 | const { nScriptChunk, type } = obj
|
292 | const script = new Script([txIn.script.chunks[nScriptChunk]])
|
293 | const scriptSize = script.toBuffer().length
|
294 | size -= scriptSize
|
295 | if (type === 'sig') {
|
296 | size += sigSize
|
297 | } else if (obj.type === 'pubKey') {
|
298 | size += pubKeySize
|
299 | } else {
|
300 | throw new Error('unsupported sig operations type')
|
301 | }
|
302 | })
|
303 | })
|
304 |
|
305 |
|
306 | size = size + 1
|
307 | return Math.round(size)
|
308 | }
|
309 |
|
310 | estimateFee (extraFeeAmount = new Bn(0)) {
|
311 |
|
312 |
|
313 |
|
314 |
|
315 | const fee = Math.ceil(this.estimateSize() / 1000 * this.feePerKbNum)
|
316 |
|
317 | return new Bn(fee).add(extraFeeAmount)
|
318 | }
|
319 |
|
320 | |
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 | build (opts = { useAllInputs: false }) {
|
329 | let minFeeAmountBn
|
330 | if (this.txIns.length <= 0) {
|
331 | throw Error('tx-builder number of inputs must be greater than 0')
|
332 | }
|
333 | if (!this.changeScript) {
|
334 | throw new Error('must specify change script to use build method')
|
335 | }
|
336 | for (
|
337 | let extraInputsNum = opts.useAllInputs ? this.txIns.length - 1 : 0;
|
338 | extraInputsNum < this.txIns.length;
|
339 | extraInputsNum++
|
340 | ) {
|
341 | this.tx = new Tx()
|
342 | const outAmountBn = this.buildOutputs()
|
343 | const changeTxOut = TxOut.fromProperties(new Bn(0), this.changeScript)
|
344 | this.tx.addTxOut(changeTxOut)
|
345 |
|
346 | let inAmountBn
|
347 | try {
|
348 | inAmountBn = this.buildInputs(outAmountBn, extraInputsNum)
|
349 | } catch (err) {
|
350 | if (err.message.includes('not enough funds for outputs')) {
|
351 | throw new Error('unable to gather enough inputs for outputs and fee')
|
352 | } else {
|
353 | throw err
|
354 | }
|
355 | }
|
356 |
|
357 |
|
358 | this.changeAmountBn = inAmountBn.sub(outAmountBn)
|
359 | changeTxOut.valueBn = this.changeAmountBn
|
360 |
|
361 | minFeeAmountBn = this.estimateFee()
|
362 | if (
|
363 | this.changeAmountBn.geq(minFeeAmountBn) &&
|
364 | this.changeAmountBn.sub(minFeeAmountBn).gt(this.dust)
|
365 | ) {
|
366 | break
|
367 | }
|
368 | }
|
369 | if (this.changeAmountBn.geq(minFeeAmountBn)) {
|
370 |
|
371 | this.feeAmountBn = minFeeAmountBn
|
372 | this.changeAmountBn = this.changeAmountBn.sub(this.feeAmountBn)
|
373 | this.tx.txOuts[this.tx.txOuts.length - 1].valueBn = this.changeAmountBn
|
374 |
|
375 | if (this.changeAmountBn.lt(this.dust)) {
|
376 | if (this.dustChangeToFees) {
|
377 |
|
378 |
|
379 | this.tx.txOuts.pop()
|
380 | this.tx.txOutsVi = VarInt.fromNumber(this.tx.txOutsVi.toNumber() - 1)
|
381 | this.feeAmountBn = this.feeAmountBn.add(this.changeAmountBn)
|
382 | this.changeAmountBn = new Bn(0)
|
383 | } else {
|
384 | throw new Error('unable to create change amount greater than dust')
|
385 | }
|
386 | }
|
387 |
|
388 | this.tx.nLockTime = this.nLockTime
|
389 | this.tx.versionBytesNum = this.versionBytesNum
|
390 | if (this.tx.txOuts.length === 0) {
|
391 | throw new Error(
|
392 | 'outputs length is zero - unable to create any outputs greater than dust'
|
393 | )
|
394 | }
|
395 | return this
|
396 | } else {
|
397 | throw new Error('unable to gather enough inputs for outputs and fee')
|
398 | }
|
399 | }
|
400 |
|
401 |
|
402 | sort () {
|
403 | this.tx.sort()
|
404 | return this
|
405 | }
|
406 |
|
407 | |
408 |
|
409 |
|
410 | static allSigsPresent (m, script) {
|
411 |
|
412 |
|
413 | let present = 0
|
414 | for (let i = 1; i < script.chunks.length - 1; i++) {
|
415 | if (script.chunks[i].buf) {
|
416 | present++
|
417 | }
|
418 | }
|
419 | return present === m
|
420 | }
|
421 |
|
422 | |
423 |
|
424 |
|
425 | static removeBlankSigs (script) {
|
426 |
|
427 |
|
428 | script = new Script(script.chunks.slice())
|
429 | for (let i = 1; i < script.chunks.length - 1; i++) {
|
430 | if (!script.chunks[i].buf) {
|
431 | script.chunks.splice(i, 1)
|
432 | }
|
433 | }
|
434 | return script
|
435 | }
|
436 |
|
437 | fillSig (nIn, nScriptChunk, sig) {
|
438 | const txIn = this.tx.txIns[nIn]
|
439 | txIn.script.chunks[nScriptChunk] = new Script().writeBuffer(
|
440 | sig.toTxFormat()
|
441 | ).chunks[0]
|
442 | txIn.scriptVi = VarInt.fromNumber(txIn.script.toBuffer().length)
|
443 | return this
|
444 | }
|
445 |
|
446 | |
447 |
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 | getSig (keyPair, nHashType = Sig.SIGHASH_ALL | Sig.SIGHASH_FORKID, nIn, subScript, flags = Tx.SCRIPT_ENABLE_SIGHASH_FORKID) {
|
455 | let valueBn
|
456 | if (
|
457 | nHashType & Sig.SIGHASH_FORKID &&
|
458 | flags & Tx.SCRIPT_ENABLE_SIGHASH_FORKID
|
459 | ) {
|
460 | const txHashBuf = this.tx.txIns[nIn].txHashBuf
|
461 | const txOutNum = this.tx.txIns[nIn].txOutNum
|
462 | const txOut = this.uTxOutMap.get(txHashBuf, txOutNum)
|
463 | if (!txOut) {
|
464 | throw new Error('for SIGHASH_FORKID must provide UTXOs')
|
465 | }
|
466 | valueBn = txOut.valueBn
|
467 | }
|
468 | return this.tx.sign(keyPair, nHashType, nIn, subScript, valueBn, flags, this.hashCache)
|
469 | }
|
470 |
|
471 | |
472 |
|
473 |
|
474 |
|
475 | asyncGetSig (keyPair, nHashType = Sig.SIGHASH_ALL | Sig.SIGHASH_FORKID, nIn, subScript, flags = Tx.SCRIPT_ENABLE_SIGHASH_FORKID) {
|
476 | let valueBn
|
477 | if (
|
478 | nHashType & Sig.SIGHASH_FORKID &&
|
479 | flags & Tx.SCRIPT_ENABLE_SIGHASH_FORKID
|
480 | ) {
|
481 | const txHashBuf = this.tx.txIns[nIn].txHashBuf
|
482 | const txOutNum = this.tx.txIns[nIn].txOutNum
|
483 | const txOut = this.uTxOutMap.get(txHashBuf, txOutNum)
|
484 | if (!txOut) {
|
485 | throw new Error('for SIGHASH_FORKID must provide UTXOs')
|
486 | }
|
487 | valueBn = txOut.valueBn
|
488 | }
|
489 | return this.tx.asyncSign(
|
490 | keyPair,
|
491 | nHashType,
|
492 | nIn,
|
493 | subScript,
|
494 | valueBn,
|
495 | flags,
|
496 | this.hashCache
|
497 | )
|
498 | }
|
499 |
|
500 | |
501 |
|
502 |
|
503 |
|
504 |
|
505 | signTxIn (nIn, keyPair, txOut, nScriptChunk, nHashType = Sig.SIGHASH_ALL | Sig.SIGHASH_FORKID, flags = Tx.SCRIPT_ENABLE_SIGHASH_FORKID) {
|
506 | const txIn = this.tx.txIns[nIn]
|
507 | const script = txIn.script
|
508 | if (nScriptChunk === undefined && script.isPubKeyHashIn()) {
|
509 | nScriptChunk = 0
|
510 | }
|
511 | if (nScriptChunk === undefined) {
|
512 | throw new Error('cannot sign unknown script type for input ' + nIn)
|
513 | }
|
514 | const txHashBuf = txIn.txHashBuf
|
515 | const txOutNum = txIn.txOutNum
|
516 | if (!txOut) {
|
517 | txOut = this.uTxOutMap.get(txHashBuf, txOutNum)
|
518 | }
|
519 | const outScript = txOut.script
|
520 | const subScript = outScript
|
521 | const sig = this.getSig(keyPair, nHashType, nIn, subScript, flags, this.hashCache)
|
522 | this.fillSig(nIn, nScriptChunk, sig)
|
523 | return this
|
524 | }
|
525 |
|
526 | |
527 |
|
528 |
|
529 |
|
530 |
|
531 | async asyncSignTxIn (nIn, keyPair, txOut, nScriptChunk, nHashType = Sig.SIGHASH_ALL | Sig.SIGHASH_FORKID, flags = Tx.SCRIPT_ENABLE_SIGHASH_FORKID) {
|
532 | const txIn = this.tx.txIns[nIn]
|
533 | const script = txIn.script
|
534 | if (nScriptChunk === undefined && script.isPubKeyHashIn()) {
|
535 | nScriptChunk = 0
|
536 | }
|
537 | if (nScriptChunk === undefined) {
|
538 | throw new Error('cannot sign unknown script type for input ' + nIn)
|
539 | }
|
540 | const txHashBuf = txIn.txHashBuf
|
541 | const txOutNum = txIn.txOutNum
|
542 | if (!txOut) {
|
543 | txOut = this.uTxOutMap.get(txHashBuf, txOutNum)
|
544 | }
|
545 | const outScript = txOut.script
|
546 | const subScript = outScript
|
547 | const sig = await this.asyncGetSig(keyPair, nHashType, nIn, subScript, flags, this.hashCache)
|
548 | this.fillSig(nIn, nScriptChunk, sig)
|
549 | return this
|
550 | }
|
551 |
|
552 | signWithKeyPairs (keyPairs) {
|
553 |
|
554 | const addressStrMap = {}
|
555 | for (const keyPair of keyPairs) {
|
556 | const addressStr = Address.fromPubKey(keyPair.pubKey).toString()
|
557 | addressStrMap[addressStr] = keyPair
|
558 | }
|
559 |
|
560 | for (const nIn in this.tx.txIns) {
|
561 | const txIn = this.tx.txIns[nIn]
|
562 |
|
563 |
|
564 | const arr = this.sigOperations.get(txIn.txHashBuf, txIn.txOutNum)
|
565 | for (const obj of arr) {
|
566 |
|
567 | const { nScriptChunk, type, addressStr, nHashType } = obj
|
568 | const keyPair = addressStrMap[addressStr]
|
569 | if (!keyPair) {
|
570 | obj.log = `cannot find keyPair for addressStr ${addressStr}`
|
571 | continue
|
572 | }
|
573 | const txOut = this.uTxOutMap.get(txIn.txHashBuf, txIn.txOutNum)
|
574 | if (type === 'sig') {
|
575 | this.signTxIn(nIn, keyPair, txOut, nScriptChunk, nHashType)
|
576 | obj.log = 'successfully inserted signature'
|
577 | } else if (type === 'pubKey') {
|
578 | txIn.script.chunks[nScriptChunk] = new Script().writeBuffer(
|
579 | keyPair.pubKey.toBuffer()
|
580 | ).chunks[0]
|
581 | txIn.setScript(txIn.script)
|
582 | obj.log = 'successfully inserted public key'
|
583 | } else {
|
584 | obj.log = `cannot perform operation of type ${type}`
|
585 | continue
|
586 | }
|
587 | }
|
588 | }
|
589 | return this
|
590 | }
|
591 | }
|
592 |
|
593 | export { TxBuilder }
|