# Transaction output (aka "tx out") is a value with rules attached in form of a script. # To spend money one need to choose a transaction output and provide an appropriate # input which makes the script execute with success. module BTC class TransactionOutput # Core attributes. # Value of output in satoshis. attr_accessor :value # BTC::Script defining redemption rules for this output (aka scriptPubKey or pk_script) attr_accessor :script # Derived attributes. # Serialized binary form of the output (payload) attr_reader :data # Dictionary representation of transaction ready to be encoded in JSON, PropertyList etc. attr_reader :dictionary # Optional attributes. # These are not derived from tx data, but attached externally (e.g. via external APIs). # 'index', 'confirmations' and 'transaction_hash' are optional attributes updated in certain context. # E.g. when loading unspent outputs from Chain.com, all these attributes will be set. # index and transaction_hash are kept up to date when output is added/removed from the transaction. # Reference to the owning transaction. It is set on tx.add_output() and # reset to nil on tx.remove_all_outputs. Default is nil. attr_accessor :transaction # Identifier of the transaction. Default is nil. attr_accessor :transaction_hash # Transaction ID. Always in sync with transaction_hash. Default is nil. attr_accessor :transaction_id # Index of this output in its transaction. Default is nil (unknown). attr_accessor :index # Binary hash of the block at which transaction was included. # If not confirmed or not available, equals nil. attr_accessor :block_hash attr_accessor :block_id # 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. # Default value is derived from `transaction` if possible or equals nil. attr_accessor :block_height # Time of the block at which tx was included (::Time instance or nil). # Default value is derived from `transaction` if possible or equals nil. attr_accessor :block_time # Number of confirmations. # Default value is derived from `transaction` if possible or equals nil. attr_accessor :confirmations # If available, returns whether this output is spent (true or false). # Default is nil. # See also `spent_confirmations`. attr_accessor :spent # If this transaction is spent, contains number of confirmations of the spending transaction. # Returns nil if not available or output is not spent. # Returns 0 if spending transaction is unconfirmed. attr_accessor :spent_confirmations def initialize(data: nil, stream: nil, dictionary: nil, value: -1, script: BTC::Script.new, # optional attributes transaction: nil, transaction_hash: nil, transaction_id: nil, index: nil, block_hash: nil, block_id: nil, block_height: nil, block_time: nil, confirmations: nil, spent: nil, spent_confirmations: nil) if stream || data init_with_stream(stream || StringIO.new(data)) elsif dictionary init_with_dictionary(dictionary) else @value = value || 0 @script = script || BTC::Script.new end @transaction = transaction @transaction_hash = transaction_hash @transaction_hash = BTC.hash_from_id(transaction_id) if transaction_id @index = index @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 @spent = spent @spent_confirmations = spent_confirmations end def init_with_stream(stream) if stream.eof? raise ArgumentError, "Can't parse transaction output from stream because it is already closed." end # Read value if !(@value = BTC::WireFormat.read_int64le(stream: stream).first) raise ArgumentError, "Failed to read output value from stream." end # Read script if !(scriptdata = BTC::WireFormat.read_string(stream: stream).first) raise ArgumentError, "Failed to read output script data from stream." end @script = BTC::Script.new(data: scriptdata) end def init_with_dictionary(dict) @value = 0 if amount_string = dict["value"] @value = CurrencyFormatter.btc_long_formatter.number_from_string(amount_string) if !@value raise ArgumentError, "Failed to parse bitcoin amount from dictionary 'value': #{amount_string.inspect}" end end @script = nil if dict["scriptPubKey"] && dict["scriptPubKey"].is_a?(Hash) if hex = dict["scriptPubKey"]["hex"] @script = Script.new(data: BTC.from_hex(hex)) if !@script raise ArgumentError, "Failed to parse script from scriptPubKey.hex" end end end end def data data = "".b data << BTC::WireFormat.encode_int64le(self.value) data << BTC::WireFormat.encode_string(self.script.data) data end def dictionary { "value" => CurrencyFormatter.btc_long_formatter.string_from_number(self.value), "scriptPubKey" => { "asm" => self.script.to_s, "hex" => BTC.to_hex(self.script.data) } } end def transaction=(tx) @transaction = tx @transaction_hash = nil @outpoint = nil end def index=(i) @index = i @outpoint = nil end def transaction_hash return @transaction_hash if @transaction_hash return @transaction.transaction_hash if @transaction return nil end def transaction_hash=(txhash) @transaction_hash = txhash @outpoint = nil end def transaction_id=(txid) self.transaction_hash = BTC.hash_from_id(txid) end def transaction_id BTC.id_from_hash(self.transaction_hash) end def outpoint return @outpoint if @outpoint if transaction_hash && index @outpoint = Outpoint.new(transaction_hash: transaction_hash, index: index) end @outpoint end def outpoint_id outpoint.outpoint_id end def block_id BTC.id_from_hash(self.block_hash) end def block_id=(block_id) self.block_hash = BTC.hash_from_id(block_id) end def block_hash return @block_hash if @block_hash return @transaction.block_hash if @transaction return nil end def block_height return @block_height if @block_height return @transaction.block_height if @transaction return nil end def block_time return @block_time if @block_time return @transaction.block_time if @transaction return nil end def confirmations return @confirmations if @confirmations return @transaction.confirmations if @transaction return nil end # Returns `true` if this transaction output contains an Open Assets marker. # Does not perform expensive validation. # Use this method to quickly filter out non-asset transactions. def open_assets_marker? self.script.open_assets_marker? end def dust?(relay_fee_rate = Transaction::DEFAULT_RELAY_FEE_RATE) return self.value < self.dust_limit(relay_fee_rate) end def dust_limit(relay_fee_rate = Transaction::DEFAULT_RELAY_FEE_RATE) # "Dust" is defined in terms of Transaction::DEFAULT_RELAY_FEE_RATE, # which has units satoshis-per-kilobyte. # If you'd pay more than 1/3 in fees # to spend something, then we consider it dust. # A typical txout is 34 bytes big, and will # need a TransactionInput of at least 148 bytes to spend: # so dust is a txout less than 546 satoshis (3*(34+148)) # with default relay_fee_rate. size = self.data.bytesize + 148 return 3*Transaction.compute_fee(size, fee_rate: relay_fee_rate) end def to_h self.dictionary end def to_s BTC.to_hex(self.data) end def ==(other) return true if super(other) return true if data == other.data return false end alias_method :eql?, :== # Makes a deep copy of a transaction output def dup TransactionOutput.new(value: @value, script: @script.dup, transaction: @transaction, transaction_hash: @transaction_hash, index: @index, block_hash: @block_hash, block_height: @block_height, block_time: @block_time, confirmations: @confirmations, spent: @spent, spent_confirmations: @spent_confirmations) end def inspect(style = :full) %{#<#{self.class.name} value:#{CurrencyFormatter.btc_long_formatter.string_from_number(self.value)}} + %{ script:#{self.script.to_s.inspect}>} end end end