txcatcher 0.2.8 → 0.2.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/lib/txcatcher/catcher.rb +8 -5
- data/lib/txcatcher/cleaner.rb +1 -1
- data/lib/txcatcher/initializer.rb +3 -3
- data/lib/txcatcher/logger.rb +2 -2
- data/lib/txcatcher/models/transaction.rb +7 -3
- data/lib/txcatcher/server.rb +2 -2
- data/spec/catcher_spec.rb +96 -56
- data/spec/models/transaction_spec.rb +6 -2
- data/txcatcher.gemspec +2 -2
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38bad27a4120b50cb51ce9071239676b1219f1177ab977090b1e57a192b8ac66
|
4
|
+
data.tar.gz: 187e7f7905bee995a8ac341d899d7d7cf00efbd05b5f32696f82f456dc8d024d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe46bcb16a750f4a03aa77f0034fac4fc8e072ce7960b8ce3a4ad82ea361b2a5464908b7d2ca2e0a6b5a9bccf98447a018e968b90b6b6eced1c20d64ecf5ba12
|
7
|
+
data.tar.gz: 97cbcab71ce183f75675e074a474af8824a9757242d4a1e5e02e6135b54950ae9c3e1e6a5d5bc47272951faf7488d97ab3ff2d5d4bcec3e5e0d068737280e2cd
|
data/Gemfile.lock
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.14
|
data/lib/txcatcher/catcher.rb
CHANGED
@@ -106,11 +106,14 @@ module TxCatcher
|
|
106
106
|
undetected_transactions_ids.each { |txid| Transaction.create_from_rpc(txid) }
|
107
107
|
Transaction.where(txid: existing_transactions_ids).update(block_height: height)
|
108
108
|
})
|
109
|
-
# Update RBF transactions and deposits if a transaction a with lower fee
|
110
|
-
# accidentally confirmed.
|
111
|
-
|
112
|
-
t
|
113
|
-
|
109
|
+
# Update RBF transactions and deposits if a transaction a with lower fee
|
110
|
+
# (no associated deposit) got accidentally confirmed.
|
111
|
+
@queue["hashblock"] << (Proc.new {
|
112
|
+
TxCatcher::Transaction.where(block_height: height).exclude(rbf_next_transaction_id: nil).each do |t|
|
113
|
+
t.force_deposit_association_on_rbf!
|
114
|
+
end
|
115
|
+
})
|
116
|
+
|
114
117
|
end
|
115
118
|
|
116
119
|
end # class Catcher
|
data/lib/txcatcher/cleaner.rb
CHANGED
@@ -51,7 +51,7 @@ module TxCatcher
|
|
51
51
|
|
52
52
|
def clean_transactions(n)
|
53
53
|
transactions = Transaction.order(Sequel.asc(:created_at))
|
54
|
-
transactions.where(Sequel.~(protected: true)) if Config.protected_transactions
|
54
|
+
transactions = transactions.where(Sequel.~(protected: true)) if Config.protected_transactions
|
55
55
|
|
56
56
|
t = n/100
|
57
57
|
t = 1 if t == 0
|
@@ -105,15 +105,15 @@ module TxCatcher
|
|
105
105
|
"#{File.dirname(ConfigFile.path)}/#{db_config[:db_path]}"
|
106
106
|
end
|
107
107
|
else
|
108
|
-
db_config[:db_name]
|
108
|
+
db_config[:db_path] || db_config[:db_name]
|
109
109
|
end
|
110
110
|
|
111
111
|
TxCatcher.db_connection = Sequel.connect(
|
112
112
|
"#{db_config[:adapter]}://" +
|
113
113
|
"#{db_config[:user]}#{(":" if db_config[:user])}" +
|
114
114
|
"#{db_config[:password]}#{("@" if db_config[:user] || db_config[:password])}" +
|
115
|
-
"#{db_config[:host]
|
116
|
-
"
|
115
|
+
"#{db_config[:host] || "localhost"}" +
|
116
|
+
":#{db_config[:port]}#{("/" if db_config[:host] || db_config[:port])}" +
|
117
117
|
"#{db_name}"
|
118
118
|
)
|
119
119
|
end
|
data/lib/txcatcher/logger.rb
CHANGED
@@ -30,7 +30,7 @@ module TxCatcher
|
|
30
30
|
|
31
31
|
private
|
32
32
|
|
33
|
-
def report_to_stdout(message, log_level, data: nil, timestamp:
|
33
|
+
def report_to_stdout(message, log_level, data: nil, timestamp: false, newline: "\n")
|
34
34
|
$stdout.print prepare_message(message, timestamp: timestamp)
|
35
35
|
$stdout.print "\n additional data: #{data.to_s}" if data
|
36
36
|
if LOG_LEVELS[log_level] >= LOG_LEVELS[:error]
|
@@ -55,7 +55,7 @@ module TxCatcher
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
def report_to_sentry(e, log_level, data: nil, timestamp:
|
58
|
+
def report_to_sentry(e, log_level, data: nil, timestamp: false, newline: true)
|
59
59
|
return unless TxCatcher::Config["logger"]["sentry_dsn"]
|
60
60
|
data ||= {}
|
61
61
|
data.merge!(environment: Config["environment"], host: Config["host"], currency: Config["currency"])
|
@@ -180,7 +180,7 @@ module TxCatcher
|
|
180
180
|
end
|
181
181
|
|
182
182
|
# Sometimes, even though an RBF transaction with higher fee was broadcasted,
|
183
|
-
# miners accept
|
183
|
+
# miners accept the lower-fee transaction instead. However, in txcatcher database, the
|
184
184
|
# deposits are already associated with the latest transaction. In this case,
|
185
185
|
# we need to find the deposits in the DB set their transaction_id field to current transaction id.
|
186
186
|
def force_deposit_association_on_rbf!
|
@@ -189,8 +189,11 @@ module TxCatcher
|
|
189
189
|
tx = tx.rbf_next_transaction
|
190
190
|
end
|
191
191
|
tx.deposits.each do |d|
|
192
|
-
d.
|
193
|
-
|
192
|
+
d.rbf_transaction_ids.delete(self.id)
|
193
|
+
d.rbf_transaction_ids.push(d.transaction_id)
|
194
|
+
d.transaction_id = self.id
|
195
|
+
d.save
|
196
|
+
end if tx
|
194
197
|
end
|
195
198
|
|
196
199
|
def to_json
|
@@ -222,6 +225,7 @@ module TxCatcher
|
|
222
225
|
def validate
|
223
226
|
super
|
224
227
|
validates_unique :txid
|
228
|
+
#errors.add(:base, "Duplicate transaction listed as RBF") if self.rbf_previous_transaction&.txid && self&.txid && self.rbf_previous_transaction&.txid == self&.txid
|
225
229
|
errors.add(:base, "No outputs for this transaction") if !self.rbf? && self.deposits.empty?
|
226
230
|
end
|
227
231
|
|
data/lib/txcatcher/server.rb
CHANGED
@@ -23,10 +23,10 @@ module TxCatcher
|
|
23
23
|
utxo(path)
|
24
24
|
elsif path.start_with? "/addr/"
|
25
25
|
address(path)
|
26
|
-
elsif path.start_with? "/tx/"
|
27
|
-
tx(path)
|
28
26
|
elsif path.start_with? "/tx/send"
|
29
27
|
broadcast_tx(params["rawtx"])
|
28
|
+
elsif path.start_with? "/tx/"
|
29
|
+
tx(path)
|
30
30
|
elsif path.start_with? "/feerate"
|
31
31
|
feerate(params["blocks_target"] || 2)
|
32
32
|
elsif path == "/" || path.empty?
|
data/spec/catcher_spec.rb
CHANGED
@@ -8,78 +8,118 @@ require_relative '../lib/txcatcher/catcher'
|
|
8
8
|
|
9
9
|
RSpec.describe TxCatcher::Catcher do
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
@block_sock = ZMQ::Context.create.socket(ZMQ::PUB)
|
11
|
+
# None of the unit tests here should require a running bitcoind
|
12
|
+
describe "(unit tests that require bitcoind to test)" do
|
14
13
|
|
15
|
-
|
16
|
-
|
14
|
+
before(:all) do
|
15
|
+
@tx_sock = ZMQ::Context.create.socket(ZMQ::PUB)
|
16
|
+
@block_sock = ZMQ::Context.create.socket(ZMQ::PUB)
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
@catcher = TxCatcher::Catcher.new(name: "bitcoind_test")
|
21
|
-
sleep 1
|
22
|
-
end
|
18
|
+
@tx_sock.bind("ipc:///tmp/bitcoind_test.rawtx")
|
19
|
+
@block_sock.bind("ipc:///tmp/bitcoind_test.hashblock")
|
23
20
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
21
|
+
@hextx = File.read(File.dirname(__FILE__) + "/fixtures/transaction.txt").strip
|
22
|
+
@rawtx = unhexlify(File.read(File.dirname(__FILE__) + "/fixtures/transaction.txt"))
|
23
|
+
@catcher = TxCatcher::Catcher.new(name: "bitcoind_test")
|
24
|
+
sleep 1
|
25
|
+
end
|
29
26
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
27
|
+
after(:all) do
|
28
|
+
@catcher.close_all_connections
|
29
|
+
@tx_sock.unbind("ipc:///tmp/bitcoind_test.rawtx")
|
30
|
+
@block_sock.unbind('ipc:///tmp/bitcoind_test.hashblock')
|
31
|
+
end
|
34
32
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
until (tx = TxCatcher::Transaction.last) || i > 3
|
39
|
-
sleep 1
|
40
|
-
i+=1
|
33
|
+
after(:each) do
|
34
|
+
@catcher.sockets["rawtx"][:last_message] = nil
|
35
|
+
@catcher.sockets["hashblock"][:last_message] = nil
|
41
36
|
end
|
42
|
-
expect(tx.hex).to eq(@hextx)
|
43
|
-
end
|
44
37
|
|
45
|
-
|
46
|
-
|
47
|
-
|
38
|
+
it "creates a new transaction in the DB" do
|
39
|
+
@tx_sock.send_strings(['rawtx', @rawtx])
|
40
|
+
i = 0
|
41
|
+
until (tx = TxCatcher::Transaction.last) || i > 3
|
42
|
+
sleep 1
|
43
|
+
i+=1
|
44
|
+
end
|
45
|
+
expect(tx.hex).to eq(@hextx)
|
46
|
+
end
|
48
47
|
|
49
|
-
|
48
|
+
it "updates transactions block height upon receiving a new block " do
|
49
|
+
transaction = TxCatcher::Transaction.create(hex: @hextx)
|
50
|
+
expect(TxCatcher.rpc_node).to receive(:getblock).at_least(:once).and_return({ "height" => TxCatcher.current_block_height + 1, "tx" => [transaction.txid]})
|
50
51
|
|
51
|
-
|
52
|
-
begin
|
53
|
-
sleep 1 and i += 1
|
54
|
-
end until @catcher.sockets["hashblock"][:last_message] || i > 3
|
52
|
+
@block_sock.send_strings(["hashblock", 'hello'])
|
55
53
|
|
56
|
-
|
57
|
-
|
54
|
+
i = 0
|
55
|
+
begin
|
56
|
+
sleep 1 and i += 1
|
57
|
+
end until @catcher.sockets["hashblock"][:last_message] || i > 3
|
58
|
+
|
59
|
+
expect(transaction.reload.block_height).to eq(TxCatcher.current_block_height)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "ignores validation errors" do
|
63
|
+
tx = eval File.read(File.dirname(__FILE__) + "/fixtures/transaction_decoded_no_outputs.txt")
|
64
|
+
expect(TxCatcher.rpc_node).to receive(:decoderawtransaction).at_least(:once).and_return(tx)
|
65
|
+
@tx_sock.send_strings(["rawtx", @rawtx])
|
58
66
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
67
|
+
i = 0
|
68
|
+
begin
|
69
|
+
sleep 1 and i += 1
|
70
|
+
end until @catcher.sockets["rawtx"][:last_message] || i > 3
|
63
71
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
72
|
+
expect(File.exists?(ERRFILE)).to be_falsey
|
73
|
+
end
|
74
|
+
|
75
|
+
it "logs all other errors" do
|
76
|
+
sleep 1
|
77
|
+
expect(TxCatcher.rpc_node).to receive(:decoderawtransaction).at_least(:once).and_raise(StandardError)
|
78
|
+
@tx_sock.send_strings(["rawtx", @rawtx])
|
79
|
+
|
80
|
+
i = 0
|
81
|
+
begin
|
82
|
+
sleep 1 and i += 1
|
83
|
+
end until @catcher.sockets["rawtx"][:last_message] || i > 3
|
84
|
+
expect(File.read(ERRFILE)).not_to be_empty
|
85
|
+
end
|
68
86
|
|
69
|
-
expect(File.exists?(ERRFILE)).to be_falsey
|
70
87
|
end
|
71
88
|
|
72
|
-
|
89
|
+
|
90
|
+
it "updates deposits association with an RBF tx if a transaction with lower fee gets confirmed" do
|
91
|
+
|
92
|
+
class TxCatcher::Transaction
|
93
|
+
def assign_tx_hash(h)
|
94
|
+
@tx_hash = h
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class CatcherStub < TxCatcher::Catcher
|
99
|
+
def listen_to_zeromq_channels(channel);end
|
100
|
+
end
|
101
|
+
|
102
|
+
hextx = File.read(File.dirname(__FILE__) + "/fixtures/transaction.txt").strip
|
103
|
+
tx1 = TxCatcher::Transaction.create(hex: hextx)
|
104
|
+
deposits = tx1.deposits
|
105
|
+
rbf_tx_hash = tx1.tx_hash
|
106
|
+
rbf_tx_hash["txid"] = "rbftxid1"
|
107
|
+
rbf_tx_hash["locktime"] = "1"
|
108
|
+
tx2 = TxCatcher::Transaction.new(hex: hextx)
|
109
|
+
tx2.assign_tx_hash(rbf_tx_hash)
|
110
|
+
tx2.save
|
111
|
+
|
112
|
+
catcher = CatcherStub.new(name: "bitcoind_test")
|
113
|
+
allow(TxCatcher.rpc_node).to receive(:getblock).and_return({ "tx" => [tx1.txid], "height" => 123 })
|
114
|
+
catcher.send(:handle_hashblock, "new block")
|
73
115
|
sleep 1
|
74
|
-
expect(TxCatcher.rpc_node).to receive(:decoderawtransaction).at_least(:once).and_raise(StandardError)
|
75
|
-
@tx_sock.send_strings(["rawtx", @rawtx])
|
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
|
82
|
-
end
|
83
116
|
|
117
|
+
deposits.each do |d|
|
118
|
+
expect(d.reload.transaction_id).to eq(tx1.id)
|
119
|
+
end
|
120
|
+
|
121
|
+
expect(tx1.reload.deposits.map(&:id)).to eq(deposits.map(&:id))
|
122
|
+
expect(tx2.reload.deposits.map(&:id)).to eq([])
|
123
|
+
end
|
84
124
|
|
85
125
|
end
|
@@ -82,11 +82,15 @@ RSpec.describe TxCatcher::Transaction do
|
|
82
82
|
rbf_tx.assign_tx_hash(rbf_tx_hash)
|
83
83
|
rbf_tx.save
|
84
84
|
|
85
|
+
expect(rbf_tx.reload.deposits.size).to eq(2)
|
86
|
+
expect(@transaction.reload.deposits.size).to eq(0)
|
87
|
+
|
85
88
|
@transaction.reload.force_deposit_association_on_rbf!
|
86
89
|
expect(rbf_tx.reload.deposits).to be_empty
|
87
90
|
expect(@transaction.reload.deposits.map(&:id)).to eq(deposit_ids)
|
91
|
+
|
88
92
|
@transaction.deposits.each do |d|
|
89
|
-
expect(d.rbf_transaction_ids).to eq([
|
93
|
+
expect(d.rbf_transaction_ids).to eq([rbf_tx.id])
|
90
94
|
end
|
91
95
|
|
92
96
|
end
|
@@ -96,7 +100,7 @@ RSpec.describe TxCatcher::Transaction do
|
|
96
100
|
it "updates block height by searching if tx is included in one of the previous blocks" do
|
97
101
|
expect(TxCatcher.rpc_node).to receive(:getblockhash).exactly(10).times.and_return("blockhash123")
|
98
102
|
expect(TxCatcher.rpc_node).to receive(:getblock).exactly(10).times.and_return({ "height" => "123", "tx" => [@transaction.txid], "hash" => "blockhash123"})
|
99
|
-
@transaction.
|
103
|
+
@transaction.update_block_height!
|
100
104
|
expect(@transaction.block_height).to eq(123)
|
101
105
|
end
|
102
106
|
|
data/txcatcher.gemspec
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "txcatcher".freeze
|
3
|
-
s.version = "0.2.
|
3
|
+
s.version = "0.2.14"
|
4
4
|
|
5
5
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
6
6
|
s.require_paths = ["lib".freeze]
|
7
7
|
s.authors = ["Roman Snitko".freeze]
|
8
8
|
s.date = "2018-10-02"
|
9
|
-
s.description = "
|
9
|
+
s.description = "Currently, the only job of this gem is to collect all new Bitcoin/Litecoin transactions, store them in a DB, index addresses.".freeze
|
10
10
|
s.email = "roman.snitko@gmail.com".freeze
|
11
11
|
s.executables = ["txcatcher".freeze, "txcatcher-monitor".freeze]
|
12
12
|
s.extra_rdoc_files = [
|
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.
|
4
|
+
version: 0.2.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roman Snitko
|
@@ -136,7 +136,7 @@ dependencies:
|
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
|
-
description:
|
139
|
+
description: Currently, the only job of this gem is to collect all new Bitcoin/Litecoin
|
140
140
|
transactions, store them in a DB, index addresses.
|
141
141
|
email: roman.snitko@gmail.com
|
142
142
|
executables:
|
@@ -206,7 +206,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
206
206
|
- !ruby/object:Gem::Version
|
207
207
|
version: '0'
|
208
208
|
requirements: []
|
209
|
-
rubygems_version: 3.
|
209
|
+
rubygems_version: 3.1.2
|
210
210
|
signing_key:
|
211
211
|
specification_version: 4
|
212
212
|
summary: An lightweight version of Bitpay's Insight, allows to check Bitcoin/Litecoin
|