txcatcher 0.2.3 → 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
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