Class: Bitcoin::PSBT::Tx
- Inherits:
-
Object
- Object
- Bitcoin::PSBT::Tx
- Includes:
- HexConverter
- Defined in:
- lib/bitcoin/psbt/tx.rb
Instance Attribute Summary collapse
-
#inputs ⇒ Object
readonly
Returns the value of attribute inputs.
-
#outputs ⇒ Object
readonly
Returns the value of attribute outputs.
-
#proprietaries ⇒ Object
Returns the value of attribute proprietaries.
-
#tx ⇒ Object
Returns the value of attribute tx.
-
#unknowns ⇒ Object
Returns the value of attribute unknowns.
-
#version_number ⇒ Object
Returns the value of attribute version_number.
-
#xpubs ⇒ Object
Returns the value of attribute xpubs.
Class Method Summary collapse
-
.parse_from_base64(base64) ⇒ Bitcoin::PartiallySignedTx
parse Partially Signed Bitcoin Transaction data with Base64 format.
-
.parse_from_payload(payload) ⇒ Bitcoin::PartiallySignedTx
parse Partially Signed Bitcoin Transaction data.
Instance Method Summary collapse
-
#extract_tx ⇒ Bitcoin::Tx
extract final tx.
-
#finalize! ⇒ Bitcoin::PSBT::Tx
finalize tx.
-
#initialize(tx = nil) ⇒ Tx
constructor
A new instance of Tx.
-
#input_utxo(index) ⇒ Bitcoin::TxOut
Finds the UTXO for a given input index.
-
#merge(psbt) ⇒ Bitcoin::PartiallySignedTx
merge two PSBTs to create one PSBT.
-
#ready_to_sign? ⇒ Boolean
Check whether the signer can sign.
-
#signature_script(index) ⇒ Bitcoin::Script
get signature script of input specified by
index. -
#to_base64 ⇒ String
generate payload with Base64 format.
-
#to_file(path) ⇒ Object
Store the PSBT to a file.
- #to_h ⇒ Object
-
#to_payload ⇒ String
generate payload.
-
#update!(prev_tx, redeem_script: nil, witness_script: nil, hd_key_paths: []) ⇒ Object
update input key-value maps.
-
#version ⇒ Integer
get PSBT version.
Methods included from HexConverter
Constructor Details
Instance Attribute Details
#inputs ⇒ Object (readonly)
Returns the value of attribute inputs.
33 34 35 |
# File 'lib/bitcoin/psbt/tx.rb', line 33 def inputs @inputs end |
#outputs ⇒ Object (readonly)
Returns the value of attribute outputs.
34 35 36 |
# File 'lib/bitcoin/psbt/tx.rb', line 34 def outputs @outputs end |
#proprietaries ⇒ Object
Returns the value of attribute proprietaries.
35 36 37 |
# File 'lib/bitcoin/psbt/tx.rb', line 35 def proprietaries @proprietaries end |
#tx ⇒ Object
Returns the value of attribute tx.
31 32 33 |
# File 'lib/bitcoin/psbt/tx.rb', line 31 def tx @tx end |
#unknowns ⇒ Object
Returns the value of attribute unknowns.
36 37 38 |
# File 'lib/bitcoin/psbt/tx.rb', line 36 def unknowns @unknowns end |
#version_number ⇒ Object
Returns the value of attribute version_number.
37 38 39 |
# File 'lib/bitcoin/psbt/tx.rb', line 37 def version_number @version_number end |
#xpubs ⇒ Object
Returns the value of attribute xpubs.
32 33 34 |
# File 'lib/bitcoin/psbt/tx.rb', line 32 def xpubs @xpubs end |
Class Method Details
.parse_from_base64(base64) ⇒ Bitcoin::PartiallySignedTx
parse Partially Signed Bitcoin Transaction data with Base64 format.
51 52 53 |
# File 'lib/bitcoin/psbt/tx.rb', line 51 def self.parse_from_base64(base64) self.parse_from_payload(Base64.decode64(base64)) end |
.parse_from_payload(payload) ⇒ Bitcoin::PartiallySignedTx
parse Partially Signed Bitcoin Transaction data.
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 |
# File 'lib/bitcoin/psbt/tx.rb', line 58 def self.parse_from_payload(payload) buf = StringIO.new(payload) raise ArgumentError, 'Invalid PSBT magic bytes.' unless buf.read(4).unpack1('N') == PSBT_MAGIC_BYTES raise ArgumentError, 'Invalid PSBT separator.' unless buf.read(1).bth.to_i(16) == 0xff partial_tx = self.new found_sep = false # read global data. until buf.eof? key_len = Bitcoin.unpack_var_int_from_io(buf) if key_len == 0 found_sep = true break end key_start = buf.pos key_type = Bitcoin.unpack_var_int_from_io(buf) key = buf.read(key_len - (buf.pos - key_start)) value = buf.read(Bitcoin.unpack_var_int_from_io(buf)) case key_type when PSBT_GLOBAL_TYPES[:unsigned_tx] raise ArgumentError, 'Invalid global transaction typed key.' unless key_len == 1 raise ArgumentError, 'Duplicate Key, unsigned tx already provided.' if partial_tx.tx partial_tx.tx = Bitcoin::Tx.parse_from_payload(value, non_witness: true, strict: true) partial_tx.tx.in.each do |tx_in| raise ArgumentError, 'Unsigned tx does not have empty scriptSigs and scriptWitnesses.' if !tx_in.script_sig.empty? || !tx_in.script_witness.empty? end when PSBT_GLOBAL_TYPES[:xpub] raise ArgumentError, 'Size of key was not the expected size for the type global xpub.' unless key.size == Bitcoin::BIP32_EXTKEY_WITH_VERSION_SIZE xpub = Bitcoin::ExtPubkey.parse_from_payload(key) raise ArgumentError, Errors::Messages::INVALID_PUBLIC_KEY unless xpub.key.fully_valid_pubkey? raise ArgumentError, 'Duplicate key, global xpub already provided' if partial_tx.xpubs.any?{|x|x.xpub == xpub} info = Bitcoin::PSBT::KeyOriginInfo.parse_from_payload(value) raise ArgumentError, "global xpub's depth and the number of indexes not matched." unless xpub.depth == info.key_paths.size partial_tx.xpubs << Bitcoin::PSBT::GlobalXpub.new(xpub, info) when PSBT_GLOBAL_TYPES[:ver] raise ArgumentError, 'Invalid global version typed key.' unless key_len == 1 raise ArgumentError, 'Duplicate Key, global version already provided.' if partial_tx.version_number raise ArgumentError, 'Invalid global version value.' unless value.bytesize == 4 partial_tx.version_number = value.unpack1('V') raise ArgumentError, "An unsupported version was detected." if SUPPORT_VERSION < partial_tx.version_number when PSBT_GLOBAL_TYPES[:proprietary] proprietary = Proprietary.new(key, value) raise ArgumentError, 'Duplicate Key, key for proprietary value already provided.' if partial_tx.proprietaries.any?{|p| p.key == proprietary.key} partial_tx.proprietaries << proprietary else unknown_key = (Bitcoin.pack_var_int(key_type) + key).bth raise ArgumentError, 'Duplicate Key, key for unknown value already provided.' if partial_tx.unknowns[unknown_key] partial_tx.unknowns[unknown_key] = value end end raise ArgumentError, 'Separator is missing at the end of an output map.' unless found_sep raise ArgumentError, 'No unsigned transaction was provided.' unless partial_tx.tx # read input data. partial_tx.tx.in.each do |tx_in| break if buf.eof? input = Input.parse_from_buf(buf) partial_tx.inputs << input if input.non_witness_utxo && input.non_witness_utxo.tx_hash != tx_in.prev_hash raise ArgumentError, 'Non-witness UTXO does not match outpoint hash.' end end raise ArgumentError, 'Inputs provided does not match the number of inputs in transaction.' unless partial_tx.inputs.size == partial_tx.tx.in.size # read output data. partial_tx.tx.outputs.each do break if buf.eof? output = Output.parse_from_buf(buf) break unless output partial_tx.outputs << output end raise ArgumentError, 'Outputs provided does not match the number of outputs in transaction.' unless partial_tx.outputs.size == partial_tx.tx.out.size partial_tx end |
Instance Method Details
#extract_tx ⇒ Bitcoin::Tx
extract final tx.
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
# File 'lib/bitcoin/psbt/tx.rb', line 274 def extract_tx extract_tx = Bitcoin::Tx.parse_from_payload(tx.to_payload) inputs.each_with_index do |input, index| extract_tx.in[index].script_sig = input.final_script_sig if input.final_script_sig extract_tx.in[index].script_witness = input.final_script_witness if input.final_script_witness end # validate signature extract_tx.in.each_with_index do |tx_in, index| input = inputs[index] if input.non_witness_utxo utxo = input.non_witness_utxo.out[tx_in.out_point.index] raise "input[#{index}]'s signature is invalid.'" unless extract_tx.verify_input_sig(index, utxo.script_pubkey) else utxo = input.witness_utxo raise ArgumentError, "input[#{index}] does not have utxo." unless utxo raise "input[#{index}]'s signature is invalid.'" unless extract_tx.verify_input_sig(index, utxo.script_pubkey, amount: utxo.value) end end extract_tx end |
#finalize! ⇒ Bitcoin::PSBT::Tx
finalize tx. TODO This feature is experimental and support only multisig.
267 268 269 270 |
# File 'lib/bitcoin/psbt/tx.rb', line 267 def finalize! inputs.each {|input|input.finalize!} self end |
#input_utxo(index) ⇒ Bitcoin::TxOut
Finds the UTXO for a given input index
146 147 148 149 150 151 152 |
# File 'lib/bitcoin/psbt/tx.rb', line 146 def input_utxo(index) input = inputs[index] prevout_index = tx.in[index].out_point.index return input.non_witness_utxo.out[prevout_index] if input.non_witness_utxo return input.witness_utxo if input.witness_utxo nil end |
#merge(psbt) ⇒ Bitcoin::PartiallySignedTx
merge two PSBTs to create one PSBT. TODO This feature is experimental.
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 |
# File 'lib/bitcoin/psbt/tx.rb', line 246 def merge(psbt) raise ArgumentError, 'The argument psbt must be an instance of Bitcoin::PSBT::Tx.' unless psbt.is_a?(Bitcoin::PSBT::Tx) raise ArgumentError, 'The combined transactions are different.' unless tx == psbt.tx raise ArgumentError, 'The Partially Signed Input\'s count are different.' unless inputs.size == psbt.inputs.size raise ArgumentError, 'The Partially Signed Output\'s count are different.' unless outputs.size == psbt.outputs.size combined = Bitcoin::PSBT::Tx.new(tx) inputs.each_with_index do |i, index| combined.inputs[index] = i.merge(psbt.inputs[index]) end outputs.each_with_index do |o, index| combined.outputs[index] = o.merge(psbt.outputs[index]) end combined.unknowns = Hash[unknowns.merge(psbt.unknowns).sort] combined end |
#ready_to_sign? ⇒ Boolean
Check whether the signer can sign. Specifically, check the following.
- If a non-witness UTXO is provided, its hash must match the hash specified in the prevout
- If a witness UTXO is provided, no non-witness signature may be created
- If a redeemScript is provided, the scriptPubKey must be for that redeemScript
- If a witnessScript is provided, the scriptPubKey or the redeemScript must be for that witnessScript
225 226 227 228 |
# File 'lib/bitcoin/psbt/tx.rb', line 225 def ready_to_sign? inputs.each.with_index{|psbt_in, index|return false unless psbt_in.ready_to_sign?(input_utxo(index))} true end |
#signature_script(index) ⇒ Bitcoin::Script
get signature script of input specified by index
233 234 235 236 237 238 239 240 |
# File 'lib/bitcoin/psbt/tx.rb', line 233 def signature_script(index) i = inputs[index] if i.non_witness_utxo i.redeem_script ? i.redeem_script : i.non_witness_utxo.out[tx.in[index].out_point.index].script_pubkey else i.witness_script ? i.witness_script : i.witness_utxo end end |
#to_base64 ⇒ String
generate payload with Base64 format.
173 174 175 |
# File 'lib/bitcoin/psbt/tx.rb', line 173 def to_base64 Base64.strict_encode64(to_payload) end |
#to_file(path) ⇒ Object
Store the PSBT to a file.
179 180 181 182 183 184 |
# File 'lib/bitcoin/psbt/tx.rb', line 179 def to_file(path) raise ArgumentError, 'The file already exists' if File.exist?(path) File.open(path, 'w') do |f| f.write(to_payload) end end |
#to_h ⇒ Object
186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/bitcoin/psbt/tx.rb', line 186 def to_h { tx: tx.to_h, global_xpubs: xpubs.map(&:to_h), psbt_version: version, proprietary: proprietaries.map(&:to_h), unknown: unknowns.map {|k, v| {"#{k}": v}}, inputs: inputs.map(&:to_h), outputs: outputs.map(&:to_h) } end |
#to_payload ⇒ String
generate payload.
156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/bitcoin/psbt/tx.rb', line 156 def to_payload payload = PSBT_MAGIC_BYTES.itb << 0xff.itb payload << PSBT.serialize_to_vector(PSBT_GLOBAL_TYPES[:unsigned_tx], value: tx.to_payload) payload << xpubs.map(&:to_payload).join payload << PSBT.serialize_to_vector(PSBT_GLOBAL_TYPES[:ver], value: [version_number].pack('V')) if version_number payload << proprietaries.map(&:to_payload).join payload << unknowns.map {|k,v|Bitcoin.pack_var_int(k.htb.bytesize) << k.htb << Bitcoin.pack_var_int(v.bytesize) << v}.join payload << PSBT_SEPARATOR.itb payload << inputs.map(&:to_payload).join payload << outputs.map(&:to_payload).join payload end |
#update!(prev_tx, redeem_script: nil, witness_script: nil, hd_key_paths: []) ⇒ Object
update input key-value maps.
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 |
# File 'lib/bitcoin/psbt/tx.rb', line 203 def update!(prev_tx, redeem_script: nil, witness_script: nil, hd_key_paths: []) prev_hash = prev_tx.tx_hash tx.in.each_with_index do|tx_in, i| if tx_in.prev_hash == prev_hash utxo = prev_tx.out[tx_in.out_point.index] raise ArgumentError, 'redeem script does not match utxo.' if redeem_script && !utxo.script_pubkey.include?(redeem_script.to_hash160) raise ArgumentError, 'witness script does not match redeem script.' if redeem_script && witness_script && !redeem_script.include?(witness_script.to_sha256) inputs[i].witness_utxo = utxo if utxo.script_pubkey.witness_program? || redeem_script&.witness_program? inputs[i].non_witness_utxo = prev_tx inputs[i].redeem_script = redeem_script if redeem_script inputs[i].witness_script = witness_script if witness_script inputs[i].hd_key_paths = hd_key_paths.map(&:pubkey).zip(hd_key_paths).to_h end end end |
#version ⇒ Integer
get PSBT version
139 140 141 |
# File 'lib/bitcoin/psbt/tx.rb', line 139 def version version_number ? version_number : 0 end |