txcatcher 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d15212b3559d579704e262dc02ed9250a01e957c5b336a573f4a3499dd3bb00
4
- data.tar.gz: f0f740d31b4e35e04d1fa65e664eb32fe73b1b1c3665f9e825bbb4640120096b
3
+ metadata.gz: b03f8177e761f95c4e004bdee74aa320bb19495799a2752a0fc0cf476840025a
4
+ data.tar.gz: 2604d5b41d3d7f8c750c91d06abfaa453bc7d15326ad96484cada3f3bd543d4c
5
5
  SHA512:
6
- metadata.gz: 3fea87d4db4cf75cb92b9a86d96a52e430aa7fe219a2f636f1bab6cb9f3c344e11612bfb87bfcc3148e515769738cc92c37ec604be13e8c1c16299b0ffa845a2
7
- data.tar.gz: 1257311dae7a0a84bce3057a69798a26e63b37e0aa4ee361e5aa44c13a6b1e3317c621710186d6089cca9bf30e67fb7b9488f2ec0b0f60b33a7a726500bcae2c
6
+ metadata.gz: 4cde58e8673c4b62e4acde2cb541fb1dd5e1a4d1fda9b07d10b259ebed15c3191508098561e3cdead7e49858ff3473458d25c01ed95791581e5f8012106babda
7
+ data.tar.gz: 12aabc4580072213a7010cb9c7d271aaf77b6829a03a2157420850f7ec1ecb1a7e627f37cea1f74eb64084a984c61c5586ba19d3ae97384ede4c36ef4ce0ca52
data/Gemfile.lock CHANGED
@@ -17,10 +17,7 @@ GEM
17
17
  aws-sigv4 (~> 1.1)
18
18
  aws-sigv4 (1.1.0)
19
19
  aws-eventstream (~> 1.0, >= 1.0.2)
20
- builder (3.2.3)
21
20
  crypto-unit (0.3.4)
22
- descendants_tracker (0.0.4)
23
- thread_safe (~> 0.3, >= 0.3.1)
24
21
  diff-lcs (1.3)
25
22
  einhorn (0.7.4)
26
23
  em-synchrony (1.0.6)
@@ -36,13 +33,6 @@ GEM
36
33
  ffi-rzmq-core (>= 1.0.7)
37
34
  ffi-rzmq-core (1.0.7)
38
35
  ffi
39
- git (1.5.0)
40
- github_api (0.18.2)
41
- addressable (~> 2.4)
42
- descendants_tracker (~> 0.0.4)
43
- faraday (~> 0.8)
44
- hashie (~> 3.5, >= 3.5.2)
45
- oauth2 (~> 1.0)
46
36
  goliath (1.0.6)
47
37
  async-rack
48
38
  einhorn
@@ -55,35 +45,11 @@ GEM
55
45
  rack (>= 1.2.2)
56
46
  rack-contrib
57
47
  rack-respond_to
58
- hashie (3.6.0)
59
- highline (2.0.2)
60
48
  http_parser.rb (0.6.0)
61
- jeweler (2.1.1)
62
- builder
63
- bundler (>= 1.0)
64
- git (>= 1.2.5)
65
- github_api
66
- highline (>= 1.6.15)
67
- nokogiri (>= 1.5.10)
68
- rake
69
- rdoc
70
- semver
71
49
  jmespath (1.4.0)
72
- jwt (2.2.1)
73
50
  log4r (1.1.10)
74
- mini_portile2 (2.4.0)
75
51
  multi_json (1.13.1)
76
- multi_xml (0.6.0)
77
52
  multipart-post (2.1.1)
78
- nokogiri (1.10.3)
79
- mini_portile2 (~> 2.4.0)
80
- oauth2 (1.4.1)
81
- faraday (>= 0.8, < 0.16.0)
82
- jwt (>= 1.0, < 3.0)
83
- multi_json (~> 1.3)
84
- multi_xml (~> 0.5)
85
- rack (>= 1.2, < 3)
86
- pg (1.1.4)
87
53
  public_suffix (3.1.1)
88
54
  rack (1.6.11)
89
55
  rack-accept-media-types (0.9)
@@ -91,8 +57,6 @@ GEM
91
57
  rack (~> 1.4)
92
58
  rack-respond_to (0.9.8)
93
59
  rack-accept-media-types (>= 0.6)
94
- rake (12.3.3)
95
- rdoc (6.1.1)
96
60
  rspec (3.8.0)
97
61
  rspec-core (~> 3.8.0)
98
62
  rspec-expectations (~> 3.8.0)
@@ -106,12 +70,10 @@ GEM
106
70
  diff-lcs (>= 1.2.0, < 2.0)
107
71
  rspec-support (~> 3.8.0)
108
72
  rspec-support (3.8.2)
109
- semver (1.0.1)
110
73
  sentry-raven (2.11.0)
111
74
  faraday (>= 0.7.6, < 1.0)
112
75
  sequel (5.23.0)
113
76
  sqlite3 (1.4.1)
114
- thread_safe (0.3.6)
115
77
 
116
78
  PLATFORMS
117
79
  ruby
@@ -124,8 +86,6 @@ DEPENDENCIES
124
86
  faraday
125
87
  ffi-rzmq
126
88
  goliath
127
- jeweler
128
- pg
129
89
  rack
130
90
  rspec
131
91
  sentry-raven
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.3
1
+ 0.2.4
@@ -28,18 +28,40 @@ class BitcoinRPC
28
28
  end
29
29
 
30
30
  def get_block_transactions(block_height)
31
- TxCatcher::LOGGER.report "--- checking transactions in block #{block_height}"
32
31
  block_hash = self.getblockhash(block_height)
33
32
  TxCatcher.rpc_node.getblock(block_hash)
34
33
  end
35
34
 
36
- def get_blocks(limit=100)
37
- blocks = []
35
+ def get_blocks(limit=TxCatcher::Config[:max_blocks_in_memory])
36
+ # We cache blocks we get from RPC to avoid repetetive requests
37
+ # which are very slow.
38
+ @blocks ||= {}
39
+
40
+ blocks_removed = []
41
+ @blocks.delete_if do |height,hash|
42
+ if height < TxCatcher.current_block_height-TxCatcher::Config[:max_blocks_in_memory]
43
+ blocks_removed << height
44
+ true
45
+ end
46
+ end
47
+ TxCatcher::LOGGER.report(
48
+ "--- removing blocks\n#{blocks_removed.join(", ")}\nfrom cache, they're below the config " +
49
+ "setting of #{TxCatcher::Config[:max_blocks_in_memory]}"
50
+ ) unless blocks_removed.empty?
51
+
52
+ blocks_cached = []
38
53
  limit.times do |i|
39
54
  height = TxCatcher.current_block_height - i
40
- blocks << get_block_transactions(height)
55
+ unless @blocks[height]
56
+ blocks_cached << height
57
+ @blocks[height] = get_block_transactions(height)
58
+ end
41
59
  end
42
- blocks
60
+ TxCatcher::LOGGER.report(
61
+ "--- loading (from RPC) and caching transactions in blocks " +
62
+ blocks_cached.join(", ")
63
+ ) unless blocks_cached.empty?
64
+ @blocks
43
65
  end
44
66
 
45
67
  class JSONRPCError < RuntimeError
@@ -2,15 +2,71 @@ module TxCatcher
2
2
 
3
3
  class Catcher
4
4
 
5
- attr_accessor :name, :sockets
5
+ attr_accessor :break_all_loops
6
+ attr_reader :name, :queue, :zeromq_threads, :queue_threads, :sockets
6
7
 
7
- def initialize(name:, socket: "ipc:///tmp/")
8
- @queue = {}
9
- @sockets = {}
8
+ def initialize(name:, socket_prefix: "ipc:///tmp/", init_threads: true)
9
+ @socket_prefix = socket_prefix
10
+ @name = name
11
+ @queue = {}
12
+ @sockets = {}
13
+ @zeromq_threads = []
14
+ @queue_threads = []
10
15
 
11
- {'rawtx' => "#{socket}#{name}.rawtx", 'hashblock' => "#{socket}#{name}.hashblock"}.each do |channel, address|
12
- LOGGER.report "Start listening on #{name} #{channel}... (#{address})"
13
- listen_to_zeromq_message(channel: channel, address: address)
16
+ ['rawtx', 'hashblock'].each do |channel|
17
+ @queue_threads << Thread.new { listen_to_action_queues(channel) }
18
+ @zeromq_threads << Thread.new { listen_to_zeromq_channels(channel) }
19
+ end
20
+ end
21
+
22
+ def close_all_connections
23
+ @break_all_loops = true
24
+ (@zeromq_threads + @queue_threads).each { |t| t.kill }
25
+ @sockets.each { |k,v| v[:object].close }
26
+ end
27
+
28
+ # Responsible for actions after the message from ZeroMQ is parsed,
29
+ # typically it's writing data to DB through the models. We start it
30
+ # before we start listening to any messages from ZeroMQ.
31
+ def listen_to_action_queues(channel)
32
+ @queue[channel] = Queue.new
33
+ until @break_all_loops
34
+ LOGGER.report "in #{channel} queue: #{@queue[channel].size}" if Config["logger"]["log_queue_info"]
35
+ if @queue[channel].empty?
36
+ sleep 1
37
+ else
38
+ begin
39
+ @queue[channel].pop.call
40
+ rescue Sequel::ValidationFailed => e
41
+ LOGGER.report e, :warn, timestamp: true
42
+ rescue Exception => e
43
+ LOGGER.report e, :error, timestamp: true
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ # Now we can start receiving messages from ZeroMQ.
50
+ # On every received message we call a handler method, which parses it
51
+ # appropriately (each ZeroMQ channel has its own handler method) and then
52
+ # adds additional tasks, such as writing to the DB, in the queue.
53
+ # They queue itself is handled in the thread created above.
54
+ def listen_to_zeromq_channels(channel)
55
+ address = "#{@socket_prefix}#{@name}.#{channel}"
56
+ LOGGER.report "Start listening on #{@name} #{channel}... (#{address})"
57
+ context = ZMQ::Context.new
58
+ socket = context.socket(ZMQ::SUB)
59
+ socket.setsockopt(ZMQ::SUBSCRIBE, channel)
60
+ socket.connect(address)
61
+ @sockets[channel] = { object: socket }
62
+ until @break_all_loops do
63
+ message = []
64
+ socket.recv_strings(message)
65
+ if message[1]
66
+ message_hex = hexlify(message[1]).downcase
67
+ @sockets[channel][:last_message] = message_hex
68
+ send("handle_#{channel}", "#{message_hex}")
69
+ end
14
70
  end
15
71
  end
16
72
 
@@ -24,55 +80,6 @@ module TxCatcher
24
80
  a.join
25
81
  end
26
82
 
27
- def listen_to_zeromq_message(channel:, address:)
28
- @queue[channel] = Queue.new
29
-
30
- # This thread is responsible for actions after the message from ZeroMQ is parsed,
31
- # typically it's writing data to DB through the models. We start it
32
- # before we start listening to any messages from ZeroMQ.
33
- queue_thread = Thread.new do
34
- loop do
35
- LOGGER.report "in #{channel} queue: #{@queue[channel].size}" if Config["logger"]["log_queue_info"]
36
- if @queue[channel].empty?
37
- sleep 1
38
- else
39
- begin
40
- @queue[channel].pop.call
41
- rescue Sequel::ValidationFailed => e
42
- LOGGER.report e, :warn, timestamp: true
43
- rescue Exception => e
44
- LOGGER.report e, :error, timestamp: true
45
- end
46
- end
47
- end
48
- end
49
-
50
- # Now we can start receiving messages from ZeroMQ.
51
- # On every received message we call a handler method, which parses it
52
- # appropriately (each ZeroMQ channel has its own handler method) and then
53
- # adds additional tasks, such as writing to the DB, in the queue.
54
- # They queue itself is handled in the thread created above.
55
- key = "#{channel}#{address}"
56
- handler_thread = Thread.new do
57
- context = ZMQ::Context.new
58
- socket = context.socket(ZMQ::SUB)
59
- socket.setsockopt(ZMQ::SUBSCRIBE, channel)
60
- socket.connect(address)
61
- @sockets[key] = { object: socket }
62
- loop do
63
- topic = []
64
- message = []
65
- socket.recv_strings(message)
66
- if message[1]
67
- message_hex = hexlify(message[1]).downcase
68
- @sockets[key][:last_message] = message_hex
69
- send("handle_#{channel}", "#{message_hex}")
70
- end
71
- end
72
- end
73
-
74
- end # listen_to_zeromq_message
75
-
76
83
  def handle_rawtx(txhex)
77
84
  LOGGER.report "received tx hex: #{txhex[0..50]}..."
78
85
  @queue["rawtx"] << ( Proc.new {
@@ -10,17 +10,20 @@ module TxCatcher
10
10
  unknown: 5
11
11
  }
12
12
 
13
- def initialize(log_file: "txcatcher.log", error_file: "error.log", error_log_delimiter: "\n\n")
13
+ attr_accessor :reporters
14
+
15
+ def initialize(log_file: "txcatcher.log", error_file: "error.log", error_log_delimiter: "\n\n", reporters: [:logfile, :stdout, :sentry])
14
16
  @log_file_name = log_file
15
17
  @error_log_file_name = error_file
16
18
  @error_log_delimiter = error_log_delimiter
19
+ @reporters = reporters
17
20
  end
18
21
 
19
22
 
20
23
  def report(message, log_level=:info, data: nil, timestamp: false)
21
- [:logfile, :stdout, :sentry].each do |out|
24
+ @reporters.each do |out|
22
25
  if LOG_LEVELS[log_level] >= LOG_LEVELS[Config["logger"]["#{out}_level"].to_sym]
23
- send("report_to_#{out}", message, log_level, data: data, timestamp: timestamp)
26
+ send("report_to_#{out}", message, log_level, data: data, timestamp: timestamp)
24
27
  end
25
28
  end
26
29
  end
@@ -54,7 +57,8 @@ module TxCatcher
54
57
 
55
58
  def prepare_message(e, timestamp: false)
56
59
  result = if e.kind_of?(Exception)
57
- result = e.to_s + "\n"
60
+ result = e.class.to_s + " - "
61
+ result += e.to_s + "\n"
58
62
  result += e.message + "\n\n" if e.message != e.to_s
59
63
  result += (e.backtrace&.join("\n") || "[no backtrace]")
60
64
  result
@@ -2,9 +2,16 @@ module TxCatcher
2
2
 
3
3
  class Transaction < Sequel::Model
4
4
 
5
- plugin :validation_helpers
5
+ plugin :validation_helpers
6
6
  one_to_many :deposits
7
7
 
8
+ # Updates only those transactions that have changed
9
+ def self.update_all(transactions)
10
+ transactions_to_update = transactions.select { |t| !t.column_changes.empty? }
11
+ transactions_to_update.each(&:save)
12
+ return transactions_to_update.map(&:id)
13
+ end
14
+
8
15
  def before_validation
9
16
  return if !self.new? || !self.deposits.empty?
10
17
  parse_transaction
@@ -44,11 +51,11 @@ module TxCatcher
44
51
 
45
52
  # Queries rpc node to check whether the transaction has been included in any of the blocks,
46
53
  # but only if current block_height is nil.
47
- def update_block_height!(dont_save: false, blocks: nil)
54
+ def check_block_height!(dont_save: false, blocks: nil)
48
55
  return self.block_height if self.block_height
49
56
  blocks = TxCatcher.rpc_node.get_blocks(blocks_to_check_for_inclusion_if_unconfirmed) unless blocks
50
57
 
51
- for block in blocks do
58
+ for block in blocks.values do
52
59
  if block["tx"] && block["tx"].include?(self.txid)
53
60
  LOGGER.report "tx #{self.txid} block height updated to #{block["height"]}"
54
61
  self.update(block_height: block["height"]) if !dont_save
@@ -65,7 +72,7 @@ module TxCatcher
65
72
  # However, to make absolute sure, we always bump up this number by 10 blocks.
66
73
  # Over larger periods of time, the avg block per minute value should even out, so
67
74
  # it's probably going to be fine either way.
68
- def blocks_to_check_for_inclusion_if_unconfirmed(limit=100)
75
+ def blocks_to_check_for_inclusion_if_unconfirmed(limit=TxCatcher::Config[:max_blocks_in_memory])
69
76
  return false if self.block_height
70
77
  created_minutes_ago = ((self.created_at - Time.now).to_i/60)
71
78
  blocks_to_check = (created_minutes_ago / 10).abs + 10
@@ -36,16 +36,20 @@ module TxCatcher
36
36
  end
37
37
 
38
38
  def address(path)
39
- path = path.split("/").delete_if { |i| i.empty? }
39
+ path = path.sub(/\?.*/, '').split("/").delete_if { |i| i.empty? }
40
40
  addr = path.last
41
41
 
42
- address = Address.where(address: addr).eager(deposits: :transactions).first
42
+ address = Address.where(address: addr).first
43
43
  if address
44
- transactions_ids = address.deposits.map { |d| d.transaction.txid }
45
- deposits = address.deposits.map do |d|
44
+ deposits = Deposit.where(address_id: address.id)
45
+ deposits_count = deposits.count
46
+ deposits = deposits.eager(:transaction).limit(params["limit"] || 100)
47
+ transactions = deposits.map { |d| d.transaction }
48
+ deposits = deposits.map do |d|
46
49
  t = d.transaction
47
50
  t.update(protected: true) unless t.protected
48
- t.update_block_height!
51
+ t.check_block_height!(dont_save: true)
52
+
49
53
  {
50
54
  txid: t.txid,
51
55
  amount: d.amount_in_btc,
@@ -54,68 +58,51 @@ module TxCatcher
54
58
  block_height: t.block_height
55
59
  }
56
60
  end
57
- [200, {}, { address: address.address, received: address.received, deposits: deposits }.to_json]
61
+ return [200, {}, { address: address.address, received: address.received, deposits_count: deposits_count, deposits_shown: deposits.size, deposits: deposits }.to_json]
58
62
  else
59
- [200, {}, { address: addr, received: 0, deposits: [] }.to_json]
63
+ return [200, {}, { address: addr, received: 0, deposits: [] }.to_json]
60
64
  end
61
65
  end
62
66
 
63
67
  def utxo(path)
64
- path = path.split("/").delete_if { |i| i.empty? }
68
+ path = path.sub(/\?.*/, '').split("/").delete_if { |i| i.empty? }
65
69
  path.pop
66
70
  addr = path.last
67
71
 
68
- model = Address.where(address: addr).eager(deposits: :transactions).first
69
- return [200, {}, "{}"] unless model
72
+ address = Address.where(address: addr).first
73
+ return [200, {}, "{}"] unless address
74
+ deposits = Deposit.where(address_id: address.id).limit(params["limit"] || 100).eager(:transaction)
75
+
76
+ transactions = deposits.map { |d| d.transaction }
77
+ transactions.sort! { |t1,t2| t2.created_at <=> t1.created_at }
78
+
79
+ transactions.each do |t|
80
+ # If we see a transaction with 0 confirmations, let's check if it got any news confirmations
81
+ # by querying Bitcoind RPC.
82
+ t.check_block_height!(dont_save: true) if t.confirmations == 0
83
+ # If still not confirmed, let's make it protected so it's not deleted during cleanup.
84
+ t.protected = true if t.confirmations == 0
85
+ end
70
86
 
71
- confirmed_txs_to_update = []
72
- unconfirmed_txs_to_update = []
73
- newly_confirmed_txs_to_update = []
87
+ Transaction.update_all(transactions) # will only update the ones that changed!
74
88
 
75
- transactions = model.deposits.map { |d| d.transaction }
76
- transactions.sort! { |t1,t2| t2.created_at <=> t1.created_at }
77
89
  utxos = transactions.map do |t|
78
90
  outs = t.tx_hash["vout"].select { |out| out["scriptPubKey"]["addresses"] == [addr] }
79
91
  outs.map! do |out|
80
- out["confirmations"] = t.confirmations || 0
81
- out["txid"] = t.txid
82
- confirmed_txs_to_update << t if out["confirmations"] > 0
83
- unconfirmed_txs_to_update << t if out["confirmations"] == 0
92
+ out["confirmations"] = t.confirmations || 0
93
+ out["txid"] = t.txid
84
94
  out
85
95
  end
86
96
  outs
87
97
  end.flatten
88
98
 
89
- unless unconfirmed_txs_to_update.empty?
90
- blocks = TxCatcher.rpc_node.get_blocks(unconfirmed_txs_to_update.first.blocks_to_check_for_inclusion_if_unconfirmed)
91
-
92
- unconfirmed_txs_to_update = unconfirmed_txs_to_update.map do |t|
93
- tx_block_height = t.update_block_height!(dont_save: true, blocks: blocks)
94
- if tx_block_height && tx_block_height > 0
95
- newly_confirmed_txs_to_update << t
96
- nil
97
- else
98
- t
99
- end
100
- end.compact
101
- end
102
-
103
- # Update all txs after we map them. We don't use regular Transaction#update
104
- # in the #map loop above, because this will quickly get out of hand and result
105
- # in a gateway timeout, if there are a lot of transactions to be updated. Instead,
106
- # we collect all txs in an array and then update them in bulk here.
107
- no_block_height_change_txs_ids = (confirmed_txs_to_update + unconfirmed_txs_to_update).map(&:id)
108
- block_height_change_txs_ids = newly_confirmed_txs_to_update.map(&:id)
109
- TxCatcher.db_connection[:transactions].where(id: no_block_height_change_txs_ids).update(protected: true)
110
- TxCatcher.db_connection[:transactions].where(id: block_height_change_txs_ids).update(protected: true, block_height: TxCatcher.current_block_height)
111
-
112
- [200, {}, utxos.to_json]
99
+ return [200, {}, utxos.to_json]
113
100
  end
114
101
 
115
102
  def broadcast_tx(txhex)
116
103
  TxCatcher.rpc_node.sendrawtransaction(txhex)
117
104
  tx = TxCatcher.rpc_node.decoderawtransaction(txhex)
118
- [200, {}, tx.to_json]
105
+ return [200, {}, tx.to_json]
119
106
  end
120
107
 
121
108
  def feerate(blocks_target)
@@ -127,7 +114,7 @@ module TxCatcher
127
114
  result = { "feerate" => result, "blocks" => blocks_target}
128
115
  end
129
116
 
130
- [200, {}, result["feerate"].to_s]
117
+ return [200, {}, result["feerate"].to_s]
131
118
  end
132
119
 
133
120
  end
data/lib/txcatcher.rb CHANGED
@@ -22,3 +22,4 @@ prepare
22
22
  require_relative 'txcatcher/models/transaction'
23
23
  require_relative 'txcatcher/models/address'
24
24
  require_relative 'txcatcher/models/deposit'
25
+ Sequel::Model.plugin :dirty
data/spec/catcher_spec.rb CHANGED
@@ -9,93 +9,77 @@ require_relative '../lib/txcatcher/catcher'
9
9
  RSpec.describe TxCatcher::Catcher do
10
10
 
11
11
  before(:all) do
12
- @tx_sock = ZMQ::Context.create(1).socket(ZMQ::PUB)
13
- @block_sock = ZMQ::Context.create(2).socket(ZMQ::PUB)
14
- @tx_sock.bind("ipc:///tmp/bitcoind.rawtx")
15
- @block_sock.bind("ipc:///tmp/bitcoind.hashblock")
12
+ @tx_sock = ZMQ::Context.create.socket(ZMQ::PUB)
13
+ @block_sock = ZMQ::Context.create.socket(ZMQ::PUB)
14
+
15
+ @tx_sock.bind("ipc:///tmp/bitcoind_test.rawtx")
16
+ @block_sock.bind("ipc:///tmp/bitcoind_test.hashblock")
17
+
16
18
  @hextx = File.read(File.dirname(__FILE__) + "/fixtures/transaction.txt").strip
17
19
  @rawtx = unhexlify(File.read(File.dirname(__FILE__) + "/fixtures/transaction.txt"))
18
- @catcher = TxCatcher::Catcher.new(name: "bitcoind")
20
+ @catcher = TxCatcher::Catcher.new(name: "bitcoind_test")
19
21
  sleep 1
20
22
  end
21
23
 
22
24
  after(:all) do
23
- @tx_sock.unbind('ipc:///tmp/bitcoind_testnet.rawtx')
24
- @block_sock.unbind('ipc:///tmp/bitcoind_testnet.hashblock')
25
+ @catcher.close_all_connections
26
+ @tx_sock.unbind("ipc:///tmp/bitcoind_test.rawtx")
27
+ @block_sock.unbind('ipc:///tmp/bitcoind_test.hashblock')
25
28
  end
26
29
 
27
- it "creates a new transaction in the DB" do
28
- @tx_sock.send_string('rawtx', ZMQ::SNDMORE)
29
- @tx_sock.send_string(@rawtx)
30
-
31
- i = 0
32
- until (@catcher.sockets.values&.first && @catcher.sockets.values.first[:last_message]) || i > 10
33
- sleep 1 and i += 1
34
- end
30
+ after(:each) do
31
+ @catcher.sockets["rawtx"][:last_message] = nil
32
+ @catcher.sockets["hashblock"][:last_message] = nil
33
+ end
35
34
 
35
+ it "creates a new transaction in the DB" do
36
+ @tx_sock.send_strings(['rawtx', @rawtx])
36
37
  i = 0
37
- until tx = TxCatcher::Transaction.last
38
- sleep 1 and i += 1
38
+ until (tx = TxCatcher::Transaction.last) || i > 3
39
+ sleep 1
40
+ i+=1
39
41
  end
40
42
  expect(tx.hex).to eq(@hextx)
41
-
42
43
  end
43
44
 
44
- describe "upon receiving a new block" do
45
+ it "updates transactions block height upon receiving a new block " do
46
+ transaction = TxCatcher::Transaction.create(hex: @hextx)
47
+ expect(TxCatcher.rpc_node).to receive(:getblock).at_least(:once).and_return({ "height" => TxCatcher.current_block_height + 1, "tx" => [transaction.txid]})
45
48
 
46
- it "updates transactions block height" do
47
- transaction = TxCatcher::Transaction.create(hex: @hextx)
48
- allow(TxCatcher.rpc_node).to receive(:getblock).and_return({ "height" => TxCatcher.current_block_height + 1, "tx" => [transaction.txid]})
49
- @block_sock.send_string('hashblock', ZMQ::SNDMORE)
50
- @block_sock.send_string("hello")
51
- #transaction.update_block_height!
49
+ @block_sock.send_strings(["hashblock", 'hello'])
52
50
 
53
- i = 0
54
- until transaction.reload.confirmations == 1
55
- sleep 1 and i += 1
56
- end
57
- expect(transaction.block_height).to eq(TxCatcher.current_block_height)
58
-
59
- end
51
+ i = 0
52
+ begin
53
+ sleep 1 and i += 1
54
+ end until @catcher.sockets["hashblock"][:last_message] || i > 3
60
55
 
56
+ expect(transaction.reload.block_height).to eq(TxCatcher.current_block_height)
61
57
  end
62
58
 
63
- describe "error logging" do
64
-
65
- before(:each) do
66
- @error_log = File.dirname(__FILE__) + "/config/error.log"
67
- TxCatcher::Config.config_dir = File.dirname(@error_log)
68
- end
59
+ it "ignores validation errors" do
60
+ tx = eval File.read(File.dirname(__FILE__) + "/fixtures/transaction_decoded_no_outputs.txt")
61
+ expect(TxCatcher.rpc_node).to receive(:decoderawtransaction).at_least(:once).and_return(tx)
62
+ @tx_sock.send_strings(["rawtx", @rawtx])
69
63
 
70
- after(:each) do
71
- File.delete(@error_log) if File.exists?(@error_log)
72
- end
73
-
74
- it "ignores validation errors" do
75
- tx = eval File.read(File.dirname(__FILE__) + "/fixtures/transaction_decoded_no_outputs.txt")
76
- allow(TxCatcher.rpc_node).to receive(:decoderawtransaction).and_return(tx)
77
- @tx_sock.send_string('rawtx', ZMQ::SNDMORE)
78
- @tx_sock.send_string(@rawtx)
79
-
80
- i = 0
81
- until (@catcher.sockets.values&.first && @catcher.sockets.values.first[:last_message]) || i > 10
82
- sleep 1 and i += 1
83
- end
84
- expect(File.exists?(@error_log)).to be_falsey
85
- end
64
+ i = 0
65
+ begin
66
+ sleep 1 and i += 1
67
+ end until @catcher.sockets["rawtx"][:last_message] || i > 3
86
68
 
87
- it "logs all other errors" do
88
- allow(TxCatcher.rpc_node).to receive(:decoderawtransaction).and_raise(StandardError)
89
- @tx_sock.send_string('rawtx', ZMQ::SNDMORE)
90
- @tx_sock.send_string(@rawtx)
69
+ expect(File.exists?(ERRFILE)).to be_falsey
70
+ end
91
71
 
92
- i = 0
93
- until File.exists?(@error_log)
94
- sleep 1 and i += 1
95
- end
96
- expect(File.read(@error_log)).not_to be_empty
97
- end
72
+ it "logs all other errors" do
73
+ sleep 1
74
+ expect(TxCatcher.rpc_node).to receive(:decoderawtransaction).at_least(:once).and_raise(StandardError)
75
+ @tx_sock.send_strings(["rawtx", @rawtx])
98
76
 
77
+ i = 0
78
+ begin
79
+ sleep 1 and i += 1
80
+ end until @catcher.sockets["rawtx"][:last_message] || i > 3
81
+ expect(File.read(ERRFILE)).not_to be_empty
99
82
  end
100
83
 
84
+
101
85
  end
@@ -26,6 +26,7 @@ logger:
26
26
 
27
27
  zeromq: bitcoind
28
28
  max_db_transactions_stored: 10
29
+ max_blocks_in_memory: 100
29
30
  db_clean_period_seconds: 300
30
31
  protected_transactions: true
31
32
  host: localhost
@@ -2,6 +2,12 @@ require_relative '../spec_helper'
2
2
 
3
3
  RSpec.describe TxCatcher::Transaction do
4
4
 
5
+ class TxCatcher::Transaction
6
+ def assign_transaction_attrs
7
+ self.txid = @tx_hash["txid"] unless self.txid
8
+ end
9
+ end
10
+
5
11
  before(:each) do
6
12
  @hextx = File.read(File.dirname(__FILE__) + "/../fixtures/transaction.txt").strip
7
13
  @transaction = TxCatcher::Transaction.create(hex: @hextx)
@@ -29,8 +35,19 @@ RSpec.describe TxCatcher::Transaction do
29
35
  it "updates block height by making manual requests to RPC and searching if tx is included in one of the previous blocks" do
30
36
  expect(TxCatcher.rpc_node).to receive(:getblockhash).exactly(10).times.and_return("blockhash123")
31
37
  expect(TxCatcher.rpc_node).to receive(:getblock).exactly(10).times.and_return({ "height" => "123", "tx" => [@transaction.txid], "hash" => "blockhash123"})
32
- @transaction.update_block_height!
38
+ @transaction.check_block_height!
33
39
  expect(@transaction.block_height).to eq(123)
34
40
  end
35
41
 
42
+ it "updates multiple records at once" do
43
+ transaction1 = TxCatcher::Transaction.create(hex: @hextx, txid: "123")
44
+ transaction2 = TxCatcher::Transaction.create(hex: @hextx, txid: "1234")
45
+ transaction3 = TxCatcher::Transaction.create(hex: @hextx, txid: "1235")
46
+ transaction2.block_height = 2
47
+ transaction3.block_height = 3
48
+ expect(
49
+ TxCatcher::Transaction.update_all([transaction1, transaction2, transaction3])
50
+ ).to eq([transaction2.id, transaction3.id])
51
+ end
52
+
36
53
  end
data/spec/spec_helper.rb CHANGED
@@ -28,6 +28,7 @@ connect_to_rpc_node
28
28
  require_relative '../lib/txcatcher/models/transaction'
29
29
  require_relative '../lib/txcatcher/models/address'
30
30
  require_relative '../lib/txcatcher/models/deposit'
31
+ Sequel::Model.plugin :dirty
31
32
 
32
33
  def unhexlify(msg)
33
34
  msg.scan(/../).collect { |c| c.to_i(16).chr }.join
@@ -40,6 +41,7 @@ RSpec.configure do |config|
40
41
 
41
42
  config.default_formatter = 'doc'
42
43
  config.before(:all) do
44
+ #TxCatcher::LOGGER.reporters = [:logfile]
43
45
  end
44
46
 
45
47
  config.after(:all) do
@@ -52,6 +54,10 @@ RSpec.configure do |config|
52
54
  end
53
55
 
54
56
  config.after(:each) do
57
+ delete_log_files
58
+ end
59
+
60
+ def delete_log_files
55
61
  [LOGFILE, ERRFILE].each do |f|
56
62
  FileUtils.rm(f) if File.exist?(f)
57
63
  end
data/txcatcher.gemspec CHANGED
@@ -1,12 +1,6 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
- # -*- encoding: utf-8 -*-
5
- # stub: txcatcher 0.2.2 ruby lib
6
-
7
1
  Gem::Specification.new do |s|
8
2
  s.name = "txcatcher".freeze
9
- s.version = "0.2.3"
3
+ s.version = "0.2.4"
10
4
 
11
5
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
6
  s.require_paths = ["lib".freeze]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: txcatcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Snitko