Skip to content

Commit 106e00e

Browse files
committed
Merge pull request #244 from dcousens/txbuilder
Transaction Builder
2 parents ec3ed87 + 22f8c8a commit 106e00e

10 files changed

+730
-49
lines changed

src/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module.exports = {
1313
Script: require('./script'),
1414
scripts: require('./scripts'),
1515
Transaction: require('./transaction'),
16+
TransactionBuilder: require('./transaction_builder'),
1617
networks: require('./networks'),
1718
Wallet: require('./wallet')
1819
}

src/transaction.js

+26-12
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,17 @@ Transaction.prototype.toHex = function() {
162162
* hashType, serializes and finally hashes the result. This hash can then be
163163
* used to sign the transaction input in question.
164164
*/
165-
Transaction.prototype.hashForSignature = function(prevOutScript, inIndex, hashType) {
165+
Transaction.prototype.hashForSignature = function(inIndex, prevOutScript, hashType) {
166+
// FIXME: remove in 2.x.y
167+
if (arguments[0] instanceof Script) {
168+
console.warn('hashForSignature(prevOutScript, inIndex, ...) has been deprecated. Use hashForSignature(inIndex, prevOutScript, ...)')
169+
170+
// swap the arguments (must be stored in tmp, arguments is special)
171+
var tmp = arguments[0]
172+
inIndex = arguments[1]
173+
prevOutScript = tmp
174+
}
175+
166176
assert(inIndex >= 0, 'Invalid vin index')
167177
assert(inIndex < this.ins.length, 'Invalid vin index')
168178
assert(prevOutScript instanceof Script, 'Invalid Script object')
@@ -296,35 +306,39 @@ Transaction.fromHex = function(hex) {
296306
return Transaction.fromBuffer(new Buffer(hex, 'hex'))
297307
}
298308

299-
/**
300-
* Signs a pubKeyHash output at some index with the given key
301-
*/
309+
Transaction.prototype.setInputScript = function(index, script) {
310+
this.ins[index].script = script
311+
}
312+
313+
// FIXME: remove in 2.x.y
302314
Transaction.prototype.sign = function(index, privKey, hashType) {
315+
console.warn("Transaction.prototype.sign is deprecated. Use TransactionBuilder instead.")
316+
303317
var prevOutScript = privKey.pub.getAddress().toOutputScript()
304318
var signature = this.signInput(index, prevOutScript, privKey, hashType)
305319

306-
// FIXME: Assumed prior TX was pay-to-pubkey-hash
307320
var scriptSig = scripts.pubKeyHashInput(signature, privKey.pub)
308321
this.setInputScript(index, scriptSig)
309322
}
310323

324+
// FIXME: remove in 2.x.y
311325
Transaction.prototype.signInput = function(index, prevOutScript, privKey, hashType) {
326+
console.warn("Transaction.prototype.signInput is deprecated. Use TransactionBuilder instead.")
327+
312328
hashType = hashType || Transaction.SIGHASH_ALL
313329

314-
var hash = this.hashForSignature(prevOutScript, index, hashType)
330+
var hash = this.hashForSignature(index, prevOutScript, hashType)
315331
var signature = privKey.sign(hash)
316332

317333
return signature.toScriptSignature(hashType)
318334
}
319335

320-
Transaction.prototype.setInputScript = function(index, script) {
321-
this.ins[index].script = script
322-
}
323-
324-
// FIXME: could be validateInput(index, prevTxOut, pub)
336+
// FIXME: remove in 2.x.y
325337
Transaction.prototype.validateInput = function(index, prevOutScript, pubKey, buffer) {
338+
console.warn("Transaction.prototype.validateInput is deprecated. Use TransactionBuilder instead.")
339+
326340
var parsed = ECSignature.parseScriptSignature(buffer)
327-
var hash = this.hashForSignature(prevOutScript, index, parsed.hashType)
341+
var hash = this.hashForSignature(index, prevOutScript, parsed.hashType)
328342

329343
return pubKey.verify(hash, parsed.signature)
330344
}

src/transaction_builder.js

+260
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
var assert = require('assert')
2+
var scripts = require('./scripts')
3+
4+
var ECPubKey = require('./ecpubkey')
5+
var ECSignature = require('./ecsignature')
6+
var Script = require('./script')
7+
var Transaction = require('./transaction')
8+
9+
function TransactionBuilder() {
10+
this.prevOutMap = {}
11+
this.prevOutScripts = {}
12+
this.prevOutTypes = {}
13+
14+
this.signatures = []
15+
this.tx = new Transaction()
16+
}
17+
18+
// Static constructors
19+
TransactionBuilder.fromTransaction = function(transaction) {
20+
var txb = new TransactionBuilder()
21+
22+
// Extract/add inputs
23+
transaction.ins.forEach(function(txin) {
24+
txb.addInput(txin.hash, txin.index, txin.sequence)
25+
})
26+
27+
// Extract/add outputs
28+
transaction.outs.forEach(function(txout) {
29+
txb.addOutput(txout.script, txout.value)
30+
})
31+
32+
// Extract/add signatures
33+
transaction.ins.forEach(function(txin) {
34+
// Ignore empty scripts
35+
if (txin.script.buffer.length === 0) return
36+
37+
var redeemScript
38+
var scriptSig = txin.script
39+
var scriptType = scripts.classifyInput(scriptSig)
40+
41+
// Re-classify if P2SH
42+
if (scriptType === 'scripthash') {
43+
redeemScript = Script.fromBuffer(scriptSig.chunks.slice(-1)[0])
44+
scriptSig = Script.fromChunks(scriptSig.chunks.slice(0, -1))
45+
46+
scriptType = scripts.classifyInput(scriptSig)
47+
assert.equal(scripts.classifyOutput(redeemScript), scriptType, 'Non-matching scriptSig and scriptPubKey in input')
48+
}
49+
50+
// Extract hashType, pubKeys and signatures
51+
var hashType, pubKeys, signatures
52+
53+
switch (scriptType) {
54+
case 'pubkeyhash':
55+
var parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0])
56+
var pubKey = ECPubKey.fromBuffer(scriptSig.chunks[1])
57+
58+
hashType = parsed.hashType
59+
pubKeys = [pubKey]
60+
signatures = [parsed.signature]
61+
62+
break
63+
64+
case 'multisig':
65+
var scriptSigs = scriptSig.chunks.slice(1) // ignore OP_0
66+
var parsed = scriptSigs.map(function(scriptSig) {
67+
return ECSignature.parseScriptSignature(scriptSig)
68+
})
69+
70+
hashType = parsed[0].hashType
71+
pubKeys = []
72+
signatures = parsed.map(function(p) { return p.signature })
73+
74+
break
75+
76+
case 'pubkey':
77+
var parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0])
78+
79+
hashType = parsed.hashType
80+
pubKeys = []
81+
signatures = [parsed.signature]
82+
83+
break
84+
85+
default:
86+
assert(false, scriptType + ' not supported')
87+
}
88+
89+
txb.signatures[txin.index] = {
90+
hashType: hashType,
91+
pubKeys: pubKeys,
92+
redeemScript: redeemScript,
93+
scriptType: scriptType,
94+
signatures: signatures
95+
}
96+
})
97+
98+
return txb
99+
}
100+
101+
// Operations
102+
TransactionBuilder.prototype.addInput = function(prevTx, index, sequence, prevOutScript) {
103+
var prevOutHash
104+
105+
if (typeof prevTx === 'string') {
106+
prevOutHash = new Buffer(prevTx, 'hex')
107+
108+
// TxId hex is big-endian, we want little-endian hash
109+
Array.prototype.reverse.call(prevOutHash)
110+
111+
} else if (prevTx instanceof Transaction) {
112+
prevOutHash = prevTx.getHash()
113+
prevOutScript = prevTx.outs[index].script
114+
115+
} else {
116+
prevOutHash = prevTx
117+
118+
}
119+
120+
var prevOutType
121+
if (prevOutScript !== undefined) {
122+
prevOutType = scripts.classifyOutput(prevOutScript)
123+
124+
assert.notEqual(prevOutType, 'nonstandard', 'PrevOutScript not supported (nonstandard)')
125+
}
126+
127+
assert(this.signatures.every(function(input) {
128+
return input.hashType & Transaction.SIGHASH_ANYONECANPAY
129+
}), 'No, this would invalidate signatures')
130+
131+
var prevOut = prevOutHash.toString('hex') + ':' + index
132+
assert(!(prevOut in this.prevOutMap), 'Transaction is already an input')
133+
134+
var vout = this.tx.addInput(prevOutHash, index, sequence)
135+
this.prevOutMap[prevOut] = true
136+
this.prevOutScripts[vout] = prevOutScript
137+
this.prevOutTypes[vout] = prevOutType
138+
139+
return vout
140+
}
141+
142+
TransactionBuilder.prototype.addOutput = function(scriptPubKey, value) {
143+
assert(this.signatures.every(function(signature) {
144+
return (signature.hashType & 0x1f) === Transaction.SIGHASH_SINGLE
145+
}), 'No, this would invalidate signatures')
146+
147+
return this.tx.addOutput(scriptPubKey, value)
148+
}
149+
150+
TransactionBuilder.prototype.build = function() {
151+
return this.__build(false)
152+
}
153+
154+
TransactionBuilder.prototype.buildIncomplete = function() {
155+
return this.__build(true)
156+
}
157+
158+
TransactionBuilder.prototype.__build = function(allowIncomplete) {
159+
if (!allowIncomplete) {
160+
assert(this.tx.ins.length > 0, 'Transaction has no inputs')
161+
assert(this.tx.outs.length > 0, 'Transaction has no outputs')
162+
assert(this.signatures.length > 0, 'Transaction has no signatures')
163+
assert.equal(this.signatures.length, this.tx.ins.length, 'Transaction is missing signatures')
164+
}
165+
166+
var tx = this.tx.clone()
167+
168+
// Create script signatures from signature meta-data
169+
this.signatures.forEach(function(input, index) {
170+
var scriptSig
171+
var scriptType = input.scriptType
172+
173+
var signatures = input.signatures.map(function(signature) {
174+
return signature.toScriptSignature(input.hashType)
175+
})
176+
177+
switch (scriptType) {
178+
case 'pubkeyhash':
179+
var signature = signatures[0]
180+
var pubKey = input.pubKeys[0]
181+
scriptSig = scripts.pubKeyHashInput(signature, pubKey)
182+
183+
break
184+
185+
case 'multisig':
186+
var redeemScript = allowIncomplete ? undefined : input.redeemScript
187+
scriptSig = scripts.multisigInput(signatures, redeemScript)
188+
189+
break
190+
191+
case 'pubkey':
192+
var signature = signatures[0]
193+
scriptSig = scripts.pubKeyInput(signature)
194+
195+
break
196+
197+
default:
198+
assert(false, scriptType + ' not supported')
199+
}
200+
201+
if (input.redeemScript) {
202+
scriptSig = scripts.scriptHashInput(scriptSig, input.redeemScript)
203+
}
204+
205+
tx.setInputScript(index, scriptSig)
206+
})
207+
208+
return tx
209+
}
210+
211+
TransactionBuilder.prototype.sign = function(index, privKey, redeemScript, hashType) {
212+
assert(this.tx.ins.length >= index, 'No input at index: ' + index)
213+
hashType = hashType || Transaction.SIGHASH_ALL
214+
215+
var prevOutScript = this.prevOutScripts[index]
216+
var prevOutType = this.prevOutTypes[index]
217+
218+
var scriptType, hash
219+
if (redeemScript) {
220+
prevOutScript = prevOutScript || scripts.scriptHashOutput(redeemScript.getHash())
221+
prevOutType = prevOutType || 'scripthash'
222+
223+
assert.equal(prevOutType, 'scripthash', 'PrevOutScript must be P2SH')
224+
225+
scriptType = scripts.classifyOutput(redeemScript)
226+
227+
assert.notEqual(scriptType, 'scripthash', 'RedeemScript can\'t be P2SH')
228+
assert.notEqual(scriptType, 'nonstandard', 'RedeemScript not supported (nonstandard)')
229+
230+
hash = this.tx.hashForSignature(index, redeemScript, hashType)
231+
232+
} else {
233+
prevOutScript = prevOutScript || privKey.pub.getAddress().toOutputScript()
234+
scriptType = prevOutType || 'pubkeyhash'
235+
236+
assert.notEqual(scriptType, 'scripthash', 'PrevOutScript requires redeemScript')
237+
238+
hash = this.tx.hashForSignature(index, prevOutScript, hashType)
239+
}
240+
241+
if (!(index in this.signatures)) {
242+
this.signatures[index] = {
243+
hashType: hashType,
244+
pubKeys: [],
245+
redeemScript: redeemScript,
246+
scriptType: scriptType,
247+
signatures: []
248+
}
249+
}
250+
251+
var input = this.signatures[index]
252+
assert.equal(input.hashType, hashType, 'Inconsistent hashType')
253+
assert.deepEqual(input.redeemScript, redeemScript, 'Inconsistent redeemScript')
254+
255+
var signature = privKey.sign(hash)
256+
input.pubKeys.push(privKey.pub)
257+
input.signatures.push(signature)
258+
}
259+
260+
module.exports = TransactionBuilder

0 commit comments

Comments
 (0)