Class: TxCatcher::Transaction
- Inherits:
-
Sequel::Model
- Object
- Sequel::Model
- TxCatcher::Transaction
- Defined in:
- lib/txcatcher/models/transaction.rb
Instance Attribute Summary collapse
-
#manual_rpc_request ⇒ Object
Returns the value of attribute manual_rpc_request.
Class Method Summary collapse
- .create_from_rpc(txid) ⇒ Object
- .find_or_create_from_rpc(txid) ⇒ Object
-
.update_all(transactions) ⇒ Object
Updates only those transactions that have changed.
Instance Method Summary collapse
- #after_create ⇒ Object
- #assign_transaction_attrs ⇒ Object
- #before_create ⇒ Object
- #before_validation ⇒ Object
-
#blocks_to_check_for_inclusion_if_unconfirmed(limit = ) ⇒ Object
This calculates the approximate number of blocks to check.
- #confirmations ⇒ Object
-
#force_deposit_association_on_rbf! ⇒ Object
Sometimes, even though an RBF transaction with higher fee was broadcasted, miners accept the lower-fee transaction instead.
- #input_hexes ⇒ Object
- #log_the_catch! ⇒ Object
- #output_addresses ⇒ Object
- #rbf? ⇒ Boolean
- #rbf_next_transaction ⇒ Object
- #rbf_previous_transaction ⇒ Object
- #to_json ⇒ Object
- #tx_hash ⇒ Object (also: #parse_transaction)
- #update_block_height(confirmations: nil) ⇒ Object
- #update_block_height!(confirmations: nil) ⇒ Object
-
#update_block_height_from_latest_blocks ⇒ Object
Checks the last n blocks to see if current transaction has been included in any of them, This is for cases when -txindex is not enabled and you can’t make an RPC query for a particular txid, which would be more reliable.
-
#update_block_height_from_rpc(confirmations: nil) ⇒ Object
Directly queries the RPC, fetches transaction confirmations number and calculates the block_height.
Instance Attribute Details
#manual_rpc_request ⇒ Object
Returns the value of attribute manual_rpc_request.
8 9 10 |
# File 'lib/txcatcher/models/transaction.rb', line 8 def manual_rpc_request @manual_rpc_request end |
Class Method Details
.create_from_rpc(txid) ⇒ Object
19 20 21 22 23 24 25 26 27 28 |
# File 'lib/txcatcher/models/transaction.rb', line 19 def self.create_from_rpc(txid) raise BitcoinRPC::NoTxIndexErorr, "Cannot create transaction from RPC request, (txid: #{txid}) please use -txindex and don't use pruning" unless TxCatcher.rpc_node.txindex_enabled? if tx_from_rpc = TxCatcher.rpc_node.getrawtransaction(txid, 1) tx = self.new(hex: tx_from_rpc["hex"], txid: txid) tx.manual_rpc_request = true tx.update_block_height(confirmations: tx_from_rpc["confirmations"]) tx.save tx end end |
.find_or_create_from_rpc(txid) ⇒ Object
10 11 12 13 14 15 16 17 |
# File 'lib/txcatcher/models/transaction.rb', line 10 def self.find_or_create_from_rpc(txid) if tx = self.where(txid: txid).first tx.update_block_height! tx else self.create_from_rpc(txid) end end |
.update_all(transactions) ⇒ Object
Updates only those transactions that have changed
39 40 41 42 43 |
# File 'lib/txcatcher/models/transaction.rb', line 39 def self.update_all(transactions) transactions_to_update = transactions.select { |t| !t.column_changes.empty? } transactions_to_update.each(&:save) return transactions_to_update.map(&:id) end |
Instance Method Details
#after_create ⇒ Object
64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/txcatcher/models/transaction.rb', line 64 def after_create self.deposits.each do |d| d.transaction_id = self.id if self.rbf? d.rbf_transaction_ids ||= [] d.rbf_transaction_ids.push(self.rbf_previous_transaction.id) d.rbf_transaction_ids = d.rbf_transaction_ids.uniq end d.save end self.rbf_previous_transaction&.update(rbf_next_transaction_id: self.id) self.log_the_catch! end |
#assign_transaction_attrs ⇒ Object
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/txcatcher/models/transaction.rb', line 203 def assign_transaction_attrs self.txid = self.tx_hash["txid"] unless self.txid self.block_height = self.tx_hash["block_height"] unless self.block_height # In order to be able to identify RBF - those are normally transactions with # identical inputs and outputs - we hash inputs and outputs that hash serves # as an identifier that we store in our DB and thus can search all # previous transactions which the current transaction might be an RBF transaction to. # # A few comments: # # 1. Although an RBF transaction may techinically have different outputs as per # protocol specification, it is true in most cases that outputs will also be # the same (that's how most wallets implement RBF). Thus, # we're also incorporating outputs into the hashed value. # # 2. For inputs, we're using input hexes, because pruned bitcoin-core # doesn't provide addresses. self.inputs_outputs_hash ||= Digest::SHA256.hexdigest((self.input_hexes + self.output_addresses).join("")) end |
#before_create ⇒ Object
60 61 62 |
# File 'lib/txcatcher/models/transaction.rb', line 60 def before_create self.created_at = Time.now end |
#before_validation ⇒ Object
45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/txcatcher/models/transaction.rb', line 45 def before_validation return if !self.new? || (!self.deposits.empty? && !self.rbf?) assign_transaction_attrs self.tx_hash["vout"].uniq { |out| out["n"] }.each do |out| amount = CryptoUnit.new(Config["currency"], out["value"], from_unit: :standart).to_i if out["value"] address = out["scriptPubKey"]["addresses"]&.first # Do not create a new deposit unless it actually makes sense to create one if rbf? self.rbf_previous_transaction.deposits.each { |d| self.deposits << d } elsif address && amount && amount > 0 self.deposits << Deposit.new(amount: amount, address_string: address) end end end |
#blocks_to_check_for_inclusion_if_unconfirmed(limit = ) ⇒ Object
This calculates the approximate number of blocks to check. So, for example, if transaction is less than 10 minutes old, there’s probably no reason to try and check more than 2-3 blocks back. However, to make absolute sure, we always bump up this number by 10 blocks. Over larger periods of time, the avg block per minute value should even out, so it’s probably going to be fine either way.
143 144 145 146 147 148 149 |
# File 'lib/txcatcher/models/transaction.rb', line 143 def blocks_to_check_for_inclusion_if_unconfirmed(limit=TxCatcher::Config[:max_blocks_in_memory]) return false if self.block_height created_minutes_ago = ((self.created_at - Time.now).to_i/60) blocks_to_check = (created_minutes_ago / 10).abs + 10 blocks_to_check = limit if blocks_to_check > limit blocks_to_check end |
#confirmations ⇒ Object
83 84 85 86 87 88 89 |
# File 'lib/txcatcher/models/transaction.rb', line 83 def confirmations if self.block_height TxCatcher.current_block_height - self.block_height + 1 else 0 end end |
#force_deposit_association_on_rbf! ⇒ Object
Sometimes, even though an RBF transaction with higher fee was broadcasted, miners accept the lower-fee transaction instead. However, in txcatcher database, the deposits are already associated with the latest transaction. In this case, we need to find the deposits in the DB set their transaction_id field to current transaction id.
186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/txcatcher/models/transaction.rb', line 186 def force_deposit_association_on_rbf! tx = self while tx && tx.deposits.empty? do tx = tx.rbf_next_transaction end tx.deposits.each do |d| d.rbf_transaction_ids.delete(self.id) d.rbf_transaction_ids.push(d.transaction_id) d.transaction_id = self.id d.save end end |
#input_hexes ⇒ Object
174 175 176 |
# File 'lib/txcatcher/models/transaction.rb', line 174 def input_hexes @input_hexes ||= self.tx_hash["vin"].select { |input| !input["scriptSig"].nil? }.map { |input| input["scriptSig"]["hex"] }.compact.sort end |
#log_the_catch! ⇒ Object
30 31 32 33 34 35 36 |
# File 'lib/txcatcher/models/transaction.rb', line 30 def log_the_catch! manual_rpc_request_caption = (self.manual_rpc_request ? " (fetched via a manual RPC request) " : " ") LOGGER.report "tx #{self.txid} caught#{manual_rpc_request_caption}and saved to DB (id: #{self.id}), deposits (outputs):" self.deposits.each do |d| LOGGER.report " id: #{d.id}, addr: #{d.address.address}, amount: #{CryptoUnit.new(Config["currency"], d.amount, from_unit: :primary).to_standart}" end end |
#output_addresses ⇒ Object
178 179 180 |
# File 'lib/txcatcher/models/transaction.rb', line 178 def output_addresses @output_addresses ||= self.tx_hash["vout"].map { |output| output["scriptPubKey"]["addresses"]&.join(",") }.compact.sort end |
#rbf? ⇒ Boolean
151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/txcatcher/models/transaction.rb', line 151 def rbf? return true if self.rbf_previous_transaction_id # 1. Find transactions that are like this one (inputs, outputs). previous_unmarked_transactions = Transaction.where(inputs_outputs_hash: self.inputs_outputs_hash, block_height: nil, rbf_next_transaction_id: nil) .exclude(id: self.id) .order(Sequel.desc(:created_at)).eager(:deposits).to_a.select { |t| !t.deposits.empty? } unless previous_unmarked_transactions.empty? @rbf_previous_transaction = previous_unmarked_transactions.first self.rbf_previous_transaction_id = @rbf_previous_transaction.id true else false end end |
#rbf_next_transaction ⇒ Object
170 171 172 |
# File 'lib/txcatcher/models/transaction.rb', line 170 def rbf_next_transaction @rbf_next_transaction ||= Transaction.where(id: self.rbf_next_transaction_id).first end |
#rbf_previous_transaction ⇒ Object
166 167 168 |
# File 'lib/txcatcher/models/transaction.rb', line 166 def rbf_previous_transaction @rbf_previous_transaction ||= Transaction.where(id: self.rbf_previous_transaction_id).first end |
#to_json ⇒ Object
199 200 201 |
# File 'lib/txcatcher/models/transaction.rb', line 199 def to_json self.tx_hash.merge(confirmations: self.confirmations, block_height: self.block_height).to_json end |
#tx_hash ⇒ Object Also known as: parse_transaction
78 79 80 |
# File 'lib/txcatcher/models/transaction.rb', line 78 def tx_hash @tx_hash ||= TxCatcher.rpc_node.decoderawtransaction(self.hex) end |
#update_block_height(confirmations: nil) ⇒ Object
91 92 93 94 95 96 97 98 99 |
# File 'lib/txcatcher/models/transaction.rb', line 91 def update_block_height(confirmations: nil) return false if self.block_height if TxCatcher.rpc_node.txindex_enabled? || !confirmations.nil? update_block_height_from_rpc(confirmations: confirmations) else self.update_block_height_from_latest_blocks end end |
#update_block_height!(confirmations: nil) ⇒ Object
101 102 103 104 105 |
# File 'lib/txcatcher/models/transaction.rb', line 101 def update_block_height!(confirmations: nil) return false if self.block_height self.update_block_height(confirmations: confirmations) self.save if self.column_changed?(:block_height) end |
#update_block_height_from_latest_blocks ⇒ Object
Checks the last n blocks to see if current transaction has been included in any of them, This is for cases when -txindex is not enabled and you can’t make an RPC query for a particular txid, which would be more reliable.
110 111 112 113 114 115 116 117 118 119 |
# File 'lib/txcatcher/models/transaction.rb', line 110 def update_block_height_from_latest_blocks blocks = TxCatcher.rpc_node.get_blocks(blocks_to_check_for_inclusion_if_unconfirmed) unless blocks for block in blocks.values do if block["tx"] && block["tx"].include?(self.txid) LOGGER.report "tx #{self.txid} block height updated to #{block["height"]}" self.block_height = block["height"].to_i return block["height"].to_i end end end |
#update_block_height_from_rpc(confirmations: nil) ⇒ Object
Directly queries the RPC, fetches transaction confirmations number and calculates the block_height. Of confirmations number is provided, doesn’t do the RPC request (used in Transaction.create_from_rpc).
124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/txcatcher/models/transaction.rb', line 124 def update_block_height_from_rpc(confirmations: nil) begin confirmations ||= TxCatcher.rpc_node.getrawtransaction(self.txid, 1)["confirmations"] self.block_height = confirmations && confirmations > 0 ? TxCatcher.current_block_height - confirmations + 1 : nil rescue BitcoinRPC::JSONRPCError => e if e..include?("No such mempool or blockchain transaction") && self.rbf? LOGGER.report "tx #{self.txid} is an RBF transcation with a lower fee, bitcoin RPC says it's not in the mempool anymore. No need to check for confirmations", :warn else raise e end end end |