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 +4 -4
- data/Gemfile.lock +0 -40
- data/VERSION +1 -1
- data/lib/txcatcher/bitcoin_rpc.rb +27 -5
- data/lib/txcatcher/catcher.rb +63 -56
- data/lib/txcatcher/logger.rb +8 -4
- data/lib/txcatcher/models/transaction.rb +11 -4
- data/lib/txcatcher/server.rb +32 -45
- data/lib/txcatcher.rb +1 -0
- data/spec/catcher_spec.rb +48 -64
- data/spec/config/config.yml.sample +1 -0
- data/spec/models/transaction_spec.rb +18 -1
- data/spec/spec_helper.rb +6 -0
- data/txcatcher.gemspec +1 -7
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b03f8177e761f95c4e004bdee74aa320bb19495799a2752a0fc0cf476840025a
|
|
4
|
+
data.tar.gz: 2604d5b41d3d7f8c750c91d06abfaa453bc7d15326ad96484cada3f3bd543d4c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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=
|
|
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
|
|
55
|
+
unless @blocks[height]
|
|
56
|
+
blocks_cached << height
|
|
57
|
+
@blocks[height] = get_block_transactions(height)
|
|
58
|
+
end
|
|
41
59
|
end
|
|
42
|
-
|
|
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
|
data/lib/txcatcher/catcher.rb
CHANGED
|
@@ -2,15 +2,71 @@ module TxCatcher
|
|
|
2
2
|
|
|
3
3
|
class Catcher
|
|
4
4
|
|
|
5
|
-
attr_accessor :
|
|
5
|
+
attr_accessor :break_all_loops
|
|
6
|
+
attr_reader :name, :queue, :zeromq_threads, :queue_threads, :sockets
|
|
6
7
|
|
|
7
|
-
def initialize(name:,
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 {
|
data/lib/txcatcher/logger.rb
CHANGED
|
@@ -10,17 +10,20 @@ module TxCatcher
|
|
|
10
10
|
unknown: 5
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
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
|
-
|
|
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 + "
|
|
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
|
|
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
|
|
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=
|
|
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
|
data/lib/txcatcher/server.rb
CHANGED
|
@@ -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
|
|
42
|
+
address = Address.where(address: addr).first
|
|
43
43
|
if address
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
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
|
-
|
|
69
|
-
return [200, {}, "{}"] unless
|
|
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
|
-
|
|
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"]
|
|
81
|
-
out["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
|
-
|
|
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
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
|
|
13
|
-
@block_sock = ZMQ::Context.create
|
|
14
|
-
|
|
15
|
-
@
|
|
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: "
|
|
20
|
+
@catcher = TxCatcher::Catcher.new(name: "bitcoind_test")
|
|
19
21
|
sleep 1
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
after(:all) do
|
|
23
|
-
@
|
|
24
|
-
@
|
|
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
|
-
|
|
28
|
-
@
|
|
29
|
-
@
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
@@ -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.
|
|
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
|
+
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]
|