Class: BTC::Transaction

Inherits:
Object
  • Object
show all
Defined in:
lib/btcruby/transaction.rb

Constant Summary collapse

CURRENT_VERSION =
1
DEFAULT_FEE_RATE =

satoshis per 1000 bytes

10_000
DEFAULT_RELAY_FEE_RATE =

satoshis per 1000 bytes

1000

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(hex: nil, data: nil, stream: nil, dictionary: nil, version: CURRENT_VERSION, inputs: [], outputs: [], lock_time: 0, block_hash: nil, block_id: nil, block_height: nil, block_time: nil, confirmations: nil, fee: nil, inputs_amount: nil) ⇒ Transaction

Initializes transaction with its attributes. Every attribute has a valid default value.



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
# File 'lib/btcruby/transaction.rb', line 82

def initialize(hex: nil,
               data: nil,
               stream: nil,
               dictionary: nil,
               version: CURRENT_VERSION,
               inputs: [],
               outputs: [],
               lock_time: 0,

               # optional attributes
               block_hash: nil,
               block_id: nil,
               block_height: nil,
               block_time: nil,
               confirmations: nil,
               fee: nil,
               inputs_amount: nil)

  data = BTC.from_hex(hex) if hex
  stream = StringIO.new(data) if data
  if stream
    init_with_stream(stream)
  elsif dictionary
    init_with_dictionary(dictionary)
  else
    init_with_components(version: version, inputs: inputs, outputs: outputs, lock_time: lock_time)
  end

  @block_hash = block_hash
  @block_hash = BTC.hash_from_id(block_id) if block_id
  @block_height = block_height
  @block_time = block_time
  @confirmations = confirmations
  @fee = fee
  @inputs_amount = inputs_amount
end

Instance Attribute Details

#block_hashObject

Binary hash of the block at which transaction was included. If not confirmed or not available, equals nil.



49
50
51
# File 'lib/btcruby/transaction.rb', line 49

def block_hash
  @block_hash
end

#block_heightObject

Height of the block at which transaction was included. If not confirmed equals -1. Note: block_height might not be provided by some APIs while confirmations may be.



58
59
60
# File 'lib/btcruby/transaction.rb', line 58

def block_height
  @block_height
end

#block_idObject

Hex-encoded block ID. If not confirmed or not available, equals nil.



53
54
55
# File 'lib/btcruby/transaction.rb', line 53

def block_id
  @block_id
end

#block_timeObject

Time of the block at which tx was included (::Time instance or nil).



61
62
63
# File 'lib/btcruby/transaction.rb', line 61

def block_time
  @block_time
end

#confirmationsObject

Number of confirmations for this transaction (depth in the blockchan). 0 stands for unconfirmed mempool transaction. Default is nil ("no info").



65
66
67
# File 'lib/btcruby/transaction.rb', line 65

def confirmations
  @confirmations
end

#dataObject (readonly)

Binary representation of the transaction in wire format (aka payload).



38
39
40
# File 'lib/btcruby/transaction.rb', line 38

def data
  @data
end

#dictionaryObject (readonly)

Dictionary representation of transaction ready to be encoded in JSON, PropertyList etc.



41
42
43
# File 'lib/btcruby/transaction.rb', line 41

def dictionary
  @dictionary
end

#feeObject

If available, returns mining fee paid by this transaction. If set, inputs_amount is updated as (outputs_amount + fee). Default is nil.



70
71
72
# File 'lib/btcruby/transaction.rb', line 70

def fee
  @fee
end

#inputsObject

List of TransactionInputs. See also #add_input and #remove_all_inputs.



18
19
20
# File 'lib/btcruby/transaction.rb', line 18

def inputs
  @inputs
end

#inputs_amountObject

If available, returns total amount of all inputs. If set, fee is updated as (inputs_amount - outputs_amount). Default is nil.



75
76
77
# File 'lib/btcruby/transaction.rb', line 75

def inputs_amount
  @inputs_amount
end

#lock_timeObject

Lock time. Either a block height or a unix timestamp. Default is 0.



25
26
27
# File 'lib/btcruby/transaction.rb', line 25

def lock_time
  @lock_time
end

#outputsObject

List of TransactionOutputs. See also #add_output and #remove_all_outputs.



21
22
23
# File 'lib/btcruby/transaction.rb', line 21

def outputs
  @outputs
end

#outputs_amountObject (readonly)

Total amount on all outputs (not including fees). Always available since outputs contain their amounts.



79
80
81
# File 'lib/btcruby/transaction.rb', line 79

def outputs_amount
  @outputs_amount
end

#transaction_hashObject (readonly)

32-byte transaction hash



31
32
33
# File 'lib/btcruby/transaction.rb', line 31

def transaction_hash
  @transaction_hash
end

#transaction_idObject (readonly)

Hexadecimal transaction ID with bytes reversed. Used by Chain.com, Blockchain.info, Blockr.io.



35
36
37
# File 'lib/btcruby/transaction.rb', line 35

def transaction_id
  @transaction_id
end

#versionObject

Version of the transaction. Default is CURRENT_VERSION.



15
16
17
# File 'lib/btcruby/transaction.rb', line 15

def version
  @version
end

Class Method Details

.compute_fee(size, fee_rate: DEFAULT_FEE_RATE) ⇒ Object

Compute a fee for a transaction of a given size with a specified per-KB fee rate. By default uses built-in DEFAULT_FEE_RATE. Makes sure that whole number of fee_rate amounts are paid.



430
431
432
433
434
435
# File 'lib/btcruby/transaction.rb', line 430

def self.compute_fee(size, fee_rate: DEFAULT_FEE_RATE)
  return 0 if fee_rate <= 0
  fee = fee_rate*size/1000 # according to Bitcoin Core as of March 15, 2015.
  fee = fee_rate if fee == 0
  fee
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?



472
473
474
475
# File 'lib/btcruby/transaction.rb', line 472

def ==(other)
  return false if other == nil
  self.data == other.data
end

#add_input(txin) ⇒ Object

Adds another input to the transaction.

Raises:

  • (ArgumentError)


208
209
210
211
212
213
214
215
216
217
# File 'lib/btcruby/transaction.rb', line 208

def add_input(txin)
  raise ArgumentError, "Input is missing" if !txin
  if !(txin.transaction == nil || txin.transaction == self)
    raise ArgumentError, "Can't add an input to a transaction when it references another transaction" # sanity check
  end
  txin.transaction = self
  txin.index = @inputs.size
  @inputs << txin
  self
end

#add_output(txout) ⇒ Object

Raises:

  • (ArgumentError)


219
220
221
222
223
224
225
226
227
228
# File 'lib/btcruby/transaction.rb', line 219

def add_output(txout)
  raise ArgumentError, "Output is missing" if !txout
  if !(txout.transaction == nil || txout.transaction == self)
    raise ArgumentError, "Can't add an output to a transaction when it references another transaction" # sanity check
  end
  txout.transaction = self
  txout.index = @outputs.size
  @outputs << txout
  self
end

#coinbase?Boolean

Returns true if this transaction is a coinbase transaction.

Returns:

  • (Boolean)


178
179
180
# File 'lib/btcruby/transaction.rb', line 178

def coinbase?
  self.inputs.size == 1 && self.inputs[0].coinbase?
end

#compute_fee(fee_rate: DEFAULT_FEE_RATE) ⇒ Object

Compute a fee for this transaction with a specified per-KB fee rate. By default uses built-in DEFAULT_FEE_RATE.



439
440
441
# File 'lib/btcruby/transaction.rb', line 439

def compute_fee(fee_rate: DEFAULT_FEE_RATE)
  self.class.compute_fee(self.data.bytesize, fee_rate: fee_rate)
end

#dupObject

Makes a deep copy of a transaction (all inputs and outputs are copied too).



459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/btcruby/transaction.rb', line 459

def dup
  Transaction.new(version: @version,
                   inputs: (@inputs || []).map{|txin|txin.dup},
                  outputs: (@outputs || []).map{|txout|txout.dup},
                lock_time: @lock_time,
               block_hash: @block_hash,
             block_height: @block_height,
               block_time: @block_time,
            confirmations: @confirmations,
                      fee: @fee,
            inputs_amount: @inputs_amount)
end

#init_with_components(version: CURRENT_VERSION, inputs: [], outputs: [], lock_time: 0) ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/btcruby/transaction.rb', line 119

def init_with_components(version: CURRENT_VERSION,  inputs: [],  outputs: [], lock_time: 0)
  @version   = version   || CURRENT_VERSION
  @inputs    = inputs    || []
  @outputs   = outputs   || []
  @lock_time = lock_time || 0
  @inputs.each_with_index do |txin, i|
    txin.transaction = self
    txin.index = i
  end
  @outputs.each_with_index do |txout, i|
    txout.transaction = self
    txout.index = i
  end
end

#init_with_dictionary(dict) ⇒ Object



167
168
169
170
171
172
173
174
175
# File 'lib/btcruby/transaction.rb', line 167

def init_with_dictionary(dict)
  version = dict["ver"] || CURRENT_VERSION
  lock_time = dict["lock_time"] || 0

  txins  = dict["in"].map { |i| TransactionInput.new(dictionary: i) }
  txouts = dict["out"].map {|o| TransactionOutput.new(dictionary: o) }

  init_with_components(version: version, inputs: txins, outputs: txouts, lock_time: lock_time)
end

#init_with_stream(stream) ⇒ Object

Raises:

  • (ArgumentError)


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
# File 'lib/btcruby/transaction.rb', line 134

def init_with_stream(stream)
  raise ArgumentError, "Stream is missing" if !stream
  if stream.eof?
    raise ArgumentError, "Can't parse transaction from stream because it is already closed."
  end

  if !(version = BTC::WireFormat.read_int32le(stream: stream).first)
    raise ArgumentError, "Failed to read version prefix from the stream."
  end

  if !(inputs_count = BTC::WireFormat.read_varint(stream: stream).first)
    raise ArgumentError, "Failed to read inputs count from the stream."
  end

  txins = (0...inputs_count).map do
    TransactionInput.new(stream: stream)
  end

  if !(outputs_count = BTC::WireFormat.read_varint(stream: stream).first)
    raise ArgumentError, "Failed to read outputs count from the stream."
  end

  txouts = (0...outputs_count).map do
    TransactionOutput.new(stream: stream)
  end

  if !(lock_time = BTC::WireFormat.read_uint32le(stream: stream).first)
    raise ArgumentError, "Failed to read lock_time from the stream."
  end

  init_with_components(version: version, inputs: txins, outputs: txouts, lock_time: lock_time)
end

#inspectObject



478
479
480
481
482
483
484
485
# File 'lib/btcruby/transaction.rb', line 478

def inspect
  %{#<#{self.class.name}:#{transaction_id}} +
  %{ v#{version}} +
  (lock_time > 0 ? %{ lock_time:#{lock_time} #{lock_time > LOCKTIME_THRESHOLD ? 'sec' : 'block'}} : "") +
  %{ inputs:[#{inputs.map{|i|i.inspect(:light)}.join(", ")}]} +
  %{ outputs:[#{outputs.map{|o|o.inspect(:light)}.join(", ")}]} +
  %{>}
end

#open_assets_transaction?Boolean

Returns true if this transaction contains an Open Assets marker. Does not perform expensive validation. Use this method to quickly filter out non-asset transactions.

Returns:

  • (Boolean)


185
186
187
# File 'lib/btcruby/transaction.rb', line 185

def open_assets_transaction?
  self.outputs.any? {|txout| txout.script.open_assets_marker? }
end

#remove_all_inputsObject



230
231
232
233
234
235
236
237
238
# File 'lib/btcruby/transaction.rb', line 230

def remove_all_inputs
  return if !@inputs
  @inputs.each do |txin|
    txin.transaction = nil
    txin.index = nil
  end
  @inputs = []
  self
end

#remove_all_outputsObject



240
241
242
243
244
245
246
247
248
# File 'lib/btcruby/transaction.rb', line 240

def remove_all_outputs
  return if !@outputs
  @outputs.each do |txout|
    txout.transaction = nil
    txout.index = nil
  end
  @outputs = []
  self
end

#signature_hash(input_index: nil, output_script: nil, hash_type: BTC::SIGHASH_ALL, version: 0, amount: 0) ⇒ Object

Hash for signing a transaction. You should specify an input index, output script of the previous transaction for that input, and an optional hash type (default is SIGHASH_ALL).

Raises:

  • (ArgumentError)


335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/btcruby/transaction.rb', line 335

def signature_hash(input_index: nil, output_script: nil, hash_type: BTC::SIGHASH_ALL, version: 0, amount: 0)

  raise ArgumentError, "Should specify input_index in Transaction#signature_hash." if !input_index
  raise ArgumentError, "Should specify output_script in Transaction#signature_hash." if !output_script
  raise ArgumentError, "Should specify hash_type in Transaction#signature_hash." if !hash_type

  # Create a temporary copy of the transaction to apply modifications to it.
  tx = self.dup

  # Note: BitcoinQT returns a 256-bit little-endian number 1 in such case,
  # but it does not matter because it would crash before that in CScriptCheck::operator()().
  # We normally won't enter this condition if script machine is instantiated
  # with transaction and input index, but it's better to check anyway.
  if (input_index >= tx.inputs.size)
    raise ArgumentError, "Input index is out of bounds for transaction: #{input_index} >= #{tx.inputs.size}"
  end

  # In case concatenating two scripts ends up with two codeseparators,
  # or an extra one at the end, this prevents all those possible incompatibilities.
  # Note: this normally never happens because there is no use for OP_CODESEPARATOR.
  # But we have to do that cleanup anyway to not break on rare transaction that use that for lulz.
  # Also: we modify the same subscript which is used several times for multisig check,
  # but that's what BitcoinQT does as well.
  output_script.delete_opcode(BTC::OP_CODESEPARATOR)

  # Blank out other inputs' signature scripts
  # and replace our input script with a subscript (which is typically a full
  # output script from the previous transaction).
  tx.inputs.each do |txin|
    txin.signature_script = BTC::Script.new
  end
  tx.inputs[input_index].signature_script = output_script

  # Blank out some of the outputs depending on BTCSignatureHashType
  # Default is SIGHASH_ALL - all inputs and outputs are signed.
  if (hash_type & BTC::SIGHASH_OUTPUT_MASK) == BTC::SIGHASH_NONE
    # Wildcard payee - we can pay anywhere.
    tx.remove_all_outputs

    # Blank out others' input sequence numbers to let others update transaction at will.
    tx.inputs.each_with_index do |txin, i|
      if i != input_index
        tx.inputs[i].sequence = 0
      end
    end

  # Single mode assumes we sign an output at the same index as an input.
  # Outputs before the one we need are blanked out. All outputs after are simply removed.
  elsif (hash_type & BTC::SIGHASH_OUTPUT_MASK) == BTC::SIGHASH_SINGLE
    # Only lock-in the txout payee at same index as txin.
    output_index = input_index;

    # If output_index is out of bounds, BitcoinQT is returning a 256-bit little-endian 0x01 instead of failing with error.
    # We should do the same to stay compatible.
    if output_index >= tx.outputs.size
      return "\x01" + "\x00"*31
    end

    # All outputs before the one we need are blanked out. All outputs after are simply removed.
    # This is equivalent to replacing outputs with (i-1) empty outputs and a i-th original one.
    my_output = tx.outputs[output_index]
    tx.remove_all_outputs
    (0...output_index).each do |i|
      tx.add_output(BTC::TransactionOutput.new)
    end
    tx.add_output(my_output)

    # Blank out others' input sequence numbers to let others update transaction at will.
    tx.inputs.each_with_index do |txin, i|
      if i != input_index
        txin.sequence = 0
      end
    end
  end # if hashtype is none or single

  # Blank out other inputs completely. This is not recommended for open transactions.
  if (hash_type & BTC::SIGHASH_ANYONECANPAY) != 0
    input = tx.inputs[input_index]
    tx.remove_all_inputs
    tx.add_input(input)
  end

  # Important: we have to hash transaction together with its hash type.
  # Hash type is appended as a little endian uint32 unlike 1-byte suffix of the signature.
  data = tx.data + BTC::WireFormat.encode_uint32le(hash_type)
  hash = BTC.hash256(data)
  # puts ""
  # puts "SIGHASH[#{self.transaction_id}, input #{input_index}, hashtype 0x#{hash_type.to_s(16)}]: hash = #{BTC.id_from_hash(hash)}; tx = " + tx.inspect
  # puts ""
  return hash
end

#to_hObject

Returns dictionary representation of the transaction.



444
445
446
# File 'lib/btcruby/transaction.rb', line 444

def to_h
  self.dictionary
end

#to_hexObject

Returns hex representation of the transaction.



454
455
456
# File 'lib/btcruby/transaction.rb', line 454

def to_hex
  BTC.to_hex(self.data)
end

#to_sObject

Returns hex representation of the transaction.



449
450
451
# File 'lib/btcruby/transaction.rb', line 449

def to_s
  to_hex
end