txcatcher 0.2.6 → 0.2.8
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 +27 -27
- data/VERSION +1 -1
- data/lib/txcatcher/bitcoin_rpc.rb +19 -0
- data/lib/txcatcher/catcher.rb +12 -16
- data/lib/txcatcher/initializer.rb +1 -0
- data/lib/txcatcher/logger.rb +19 -11
- data/lib/txcatcher/models/transaction.rb +80 -40
- data/lib/txcatcher/server.rb +6 -6
- data/spec/models/transaction_spec.rb +11 -7
- data/txcatcher.gemspec +1 -2
- metadata +1 -2
- data/db/schema.rb +0 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2e24688c0b712c8752c126180e4ccdc7508bb51d83273c0ee95975ee683c45b
|
|
4
|
+
data.tar.gz: 0fc259dc40b77ff6f3ad51d31a389b6ad6c290e50e00fdd29b45dc9c0aff8706
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa710896fdaa6ff9a504e73168eef31eec3a69ba4b583817c7dce9132f8c1924e1579437a47162a8c1a5643dc366d54ef08cc9b41e102d6e0f738d291c9c3855
|
|
7
|
+
data.tar.gz: 0cd0b9f730fc45229c7a9547d19a4299b2a103549c7687e0b144f9e042e2ccdbb9d82960de72de154eb95c55f494c2e44ae202c1939d64f3522eef7f176e41b5
|
data/Gemfile.lock
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
GEM
|
|
2
2
|
remote: https://rubygems.org/
|
|
3
3
|
specs:
|
|
4
|
-
addressable (2.
|
|
5
|
-
public_suffix (>= 2.0.2, <
|
|
4
|
+
addressable (2.7.0)
|
|
5
|
+
public_suffix (>= 2.0.2, < 5.0)
|
|
6
6
|
async-rack (0.5.1)
|
|
7
7
|
rack (~> 1.1)
|
|
8
8
|
aws-eventstream (1.0.3)
|
|
9
|
-
aws-partitions (1.
|
|
10
|
-
aws-sdk-core (3.
|
|
9
|
+
aws-partitions (1.269.0)
|
|
10
|
+
aws-sdk-core (3.89.1)
|
|
11
11
|
aws-eventstream (~> 1.0, >= 1.0.2)
|
|
12
|
-
aws-partitions (~> 1.0)
|
|
12
|
+
aws-partitions (~> 1, >= 1.239.0)
|
|
13
13
|
aws-sigv4 (~> 1.1)
|
|
14
14
|
jmespath (~> 1.0)
|
|
15
|
-
aws-sdk-ses (1.
|
|
16
|
-
aws-sdk-core (~> 3, >= 3.
|
|
15
|
+
aws-sdk-ses (1.27.0)
|
|
16
|
+
aws-sdk-core (~> 3, >= 3.71.0)
|
|
17
17
|
aws-sigv4 (~> 1.1)
|
|
18
18
|
aws-sigv4 (1.1.0)
|
|
19
19
|
aws-eventstream (~> 1.0, >= 1.0.2)
|
|
@@ -26,9 +26,9 @@ GEM
|
|
|
26
26
|
addressable (>= 2.1.1)
|
|
27
27
|
eventmachine (>= 0.12.9)
|
|
28
28
|
eventmachine (1.2.7)
|
|
29
|
-
faraday (0.
|
|
29
|
+
faraday (1.0.0)
|
|
30
30
|
multipart-post (>= 1.2, < 3)
|
|
31
|
-
ffi (1.
|
|
31
|
+
ffi (1.12.1)
|
|
32
32
|
ffi-rzmq (2.0.7)
|
|
33
33
|
ffi-rzmq-core (>= 1.0.7)
|
|
34
34
|
ffi-rzmq-core (1.0.7)
|
|
@@ -48,32 +48,32 @@ GEM
|
|
|
48
48
|
http_parser.rb (0.6.0)
|
|
49
49
|
jmespath (1.4.0)
|
|
50
50
|
log4r (1.1.10)
|
|
51
|
-
multi_json (1.
|
|
51
|
+
multi_json (1.14.1)
|
|
52
52
|
multipart-post (2.1.1)
|
|
53
|
-
public_suffix (
|
|
54
|
-
rack (1.6.
|
|
53
|
+
public_suffix (4.0.3)
|
|
54
|
+
rack (1.6.12)
|
|
55
55
|
rack-accept-media-types (0.9)
|
|
56
56
|
rack-contrib (1.8.0)
|
|
57
57
|
rack (~> 1.4)
|
|
58
58
|
rack-respond_to (0.9.8)
|
|
59
59
|
rack-accept-media-types (>= 0.6)
|
|
60
|
-
rspec (3.
|
|
61
|
-
rspec-core (~> 3.
|
|
62
|
-
rspec-expectations (~> 3.
|
|
63
|
-
rspec-mocks (~> 3.
|
|
64
|
-
rspec-core (3.
|
|
65
|
-
rspec-support (~> 3.
|
|
66
|
-
rspec-expectations (3.
|
|
60
|
+
rspec (3.9.0)
|
|
61
|
+
rspec-core (~> 3.9.0)
|
|
62
|
+
rspec-expectations (~> 3.9.0)
|
|
63
|
+
rspec-mocks (~> 3.9.0)
|
|
64
|
+
rspec-core (3.9.1)
|
|
65
|
+
rspec-support (~> 3.9.1)
|
|
66
|
+
rspec-expectations (3.9.0)
|
|
67
67
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
68
|
-
rspec-support (~> 3.
|
|
69
|
-
rspec-mocks (3.
|
|
68
|
+
rspec-support (~> 3.9.0)
|
|
69
|
+
rspec-mocks (3.9.1)
|
|
70
70
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
71
|
-
rspec-support (~> 3.
|
|
72
|
-
rspec-support (3.
|
|
73
|
-
sentry-raven (
|
|
74
|
-
faraday (>= 0.7.6
|
|
75
|
-
sequel (5.
|
|
76
|
-
sqlite3 (1.4.
|
|
71
|
+
rspec-support (~> 3.9.0)
|
|
72
|
+
rspec-support (3.9.2)
|
|
73
|
+
sentry-raven (1.1.0)
|
|
74
|
+
faraday (>= 0.7.6)
|
|
75
|
+
sequel (5.28.0)
|
|
76
|
+
sqlite3 (1.4.2)
|
|
77
77
|
|
|
78
78
|
PLATFORMS
|
|
79
79
|
ruby
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.8
|
|
@@ -7,10 +7,29 @@ require 'json'
|
|
|
7
7
|
|
|
8
8
|
class BitcoinRPC
|
|
9
9
|
|
|
10
|
+
class NoTxIndexErorr < StandardError;end
|
|
11
|
+
|
|
10
12
|
def initialize(service_url)
|
|
11
13
|
@uri = URI.parse(service_url)
|
|
12
14
|
end
|
|
13
15
|
|
|
16
|
+
def txindex_enabled?
|
|
17
|
+
return @tx_index_enabled unless @tx_index_enabled.nil?
|
|
18
|
+
begin
|
|
19
|
+
txid = self.get_block_transactions(self.getblockcount-1000)["tx"].first
|
|
20
|
+
self.getrawtransaction(txid, 1)
|
|
21
|
+
TxCatcher::LOGGER.report "Pruning is off, -txindex enabled, can perform RPC requests to check block_height for transactions, that's much more reliable!"
|
|
22
|
+
return @tx_index_enabled = true
|
|
23
|
+
rescue BitcoinRPC::JSONRPCError => e
|
|
24
|
+
if e.message.include?("pruned data") || e.message.include?("-txindex")
|
|
25
|
+
TxCatcher::LOGGER.report "WARNING: Pruning is ON, will NOT be able to use RPC requests to check block_height for transactions!", :warn
|
|
26
|
+
return @tx_index_enabled = false
|
|
27
|
+
else
|
|
28
|
+
raise e
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
14
33
|
def method_missing(name, *args)
|
|
15
34
|
post_body = { 'method' => name, 'params' => args, 'id' => 'jsonrpc' }.to_json
|
|
16
35
|
resp = JSON.parse( http_post_request(post_body) )
|
data/lib/txcatcher/catcher.rb
CHANGED
|
@@ -81,36 +81,32 @@ module TxCatcher
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def handle_rawtx(txhex)
|
|
84
|
-
LOGGER.report "received tx hex: #{txhex[0..50]}..."
|
|
85
84
|
@queue["rawtx"] << ( Proc.new {
|
|
86
85
|
tx = TxCatcher::Transaction.new(hex: txhex)
|
|
86
|
+
tx.assign_transaction_attrs
|
|
87
87
|
begin
|
|
88
|
-
LOGGER.report "tx #{tx.txid} caught (id: #{tx.id}), deposits (outputs):"
|
|
89
88
|
tx.save
|
|
90
89
|
rescue Sequel::ValidationFailed => e
|
|
91
|
-
if tx.errors[:txid].include?("is already taken")
|
|
92
|
-
LOGGER.report " it's already in DB, no need to save it!"
|
|
93
|
-
else
|
|
90
|
+
if !tx.errors[:txid] || !tx.errors[:txid].include?("is already taken")
|
|
94
91
|
raise e
|
|
95
92
|
end
|
|
96
93
|
end
|
|
97
|
-
tx.deposits.each do |d|
|
|
98
|
-
LOGGER.report " id: #{d.id}, addr: #{d.address.address}, amount: #{CryptoUnit.new(Config["currency"], d.amount, from_unit: :primary).to_standart}"
|
|
99
|
-
end
|
|
100
94
|
})
|
|
101
95
|
end
|
|
102
96
|
|
|
103
97
|
def handle_hashblock(block_hex)
|
|
104
|
-
block_hash
|
|
105
|
-
|
|
106
|
-
height
|
|
107
|
-
LOGGER.report "*** Block #{height} mined, transactions received:\n #{
|
|
98
|
+
block_hash = TxCatcher.rpc_node.getblock(block_hex)
|
|
99
|
+
block_transactions_ids = block_hash["tx"]
|
|
100
|
+
height = TxCatcher.current_block_height = block_hash["height"].to_i
|
|
101
|
+
LOGGER.report "*** Block #{height} mined, transactions received:\n #{block_transactions_ids.join("\n\s\s")}"
|
|
108
102
|
@queue["hashblock"] << ( Proc.new {
|
|
109
|
-
existing_transactions
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
existing_transactions = Transaction.where(txid: block_transactions_ids)
|
|
104
|
+
existing_transactions_ids = existing_transactions.map(&:txid)
|
|
105
|
+
undetected_transactions_ids = block_transactions_ids - existing_transactions_ids
|
|
106
|
+
undetected_transactions_ids.each { |txid| Transaction.create_from_rpc(txid) }
|
|
107
|
+
Transaction.where(txid: existing_transactions_ids).update(block_height: height)
|
|
112
108
|
})
|
|
113
|
-
# Update RBF transactions and deposits if a transaction with lower fee (no associated deposit) got
|
|
109
|
+
# Update RBF transactions and deposits if a transaction a with lower fee (no associated deposit) got
|
|
114
110
|
# accidentally confirmed.
|
|
115
111
|
TxCatcher::Transaction.where(block_height: height).exclude(rbf_next_transaction_id: nil).each do |t|
|
|
116
112
|
t.force_deposit_association_on_rbf!
|
|
@@ -123,6 +123,7 @@ module TxCatcher
|
|
|
123
123
|
n = TxCatcher::Config.rpcnode
|
|
124
124
|
print "Checking #{n["name"]} RPC connection... "
|
|
125
125
|
TxCatcher.rpc_node = BitcoinRPC.new("http://#{n["user"]}:#{n["password"]}@#{n["host"]}:#{n["port"]}")
|
|
126
|
+
TxCatcher.rpc_node.txindex_enabled?
|
|
126
127
|
|
|
127
128
|
i = 0 # try to connect to RPC 100 times before exiting with error
|
|
128
129
|
until TxCatcher.current_block_height
|
data/lib/txcatcher/logger.rb
CHANGED
|
@@ -20,34 +20,42 @@ module TxCatcher
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def report(message, log_level=:info, data: nil, timestamp: false)
|
|
23
|
+
def report(message, log_level=:info, data: nil, timestamp: false, newline: "\n")
|
|
24
24
|
@reporters.each do |out|
|
|
25
25
|
if LOG_LEVELS[log_level] >= LOG_LEVELS[Config["logger"]["#{out}_level"].to_sym]
|
|
26
|
-
send("report_to_#{out}", message, log_level, data: data, timestamp: timestamp)
|
|
26
|
+
send("report_to_#{out}", message, log_level, data: data, timestamp: timestamp, newline: newline)
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
private
|
|
32
32
|
|
|
33
|
-
def report_to_stdout(message, log_level, data: nil, timestamp: timestamp)
|
|
34
|
-
$stdout.print prepare_message(message, timestamp: timestamp)
|
|
35
|
-
$stdout.print " additional data: #{data.to_s}
|
|
36
|
-
|
|
33
|
+
def report_to_stdout(message, log_level, data: nil, timestamp: timestamp, newline: "\n")
|
|
34
|
+
$stdout.print prepare_message(message, timestamp: timestamp)
|
|
35
|
+
$stdout.print "\n additional data: #{data.to_s}" if data
|
|
36
|
+
if LOG_LEVELS[log_level] >= LOG_LEVELS[:error]
|
|
37
|
+
$stdout.print(@error_log_delimiter)
|
|
38
|
+
elsif newline
|
|
39
|
+
$stdout.print newline
|
|
40
|
+
end
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
def report_to_logfile(message, log_level, data: nil, timestamp: true) # always gonna be forcing timestamp to be true here
|
|
43
|
+
def report_to_logfile(message, log_level, data: nil, timestamp: true, newline: true) # always gonna be forcing timestamp to be true here
|
|
40
44
|
fn = LOG_LEVELS[log_level] >= LOG_LEVELS[:error] ? @error_log_file_name : @log_file_name
|
|
41
45
|
fn = TxCatcher::Config.config_dir + "/#{fn}"
|
|
42
46
|
|
|
43
47
|
File.open(fn, "a") do |f|
|
|
44
|
-
f.print "#{prepare_message(message, timestamp: true)}
|
|
45
|
-
f.print " additional data: #{data.to_s}\n" if data
|
|
46
|
-
|
|
48
|
+
f.print "#{prepare_message(message, timestamp: true)}"
|
|
49
|
+
f.print "\n additional data: #{data.to_s}\n" if data
|
|
50
|
+
if LOG_LEVELS[log_level] >= LOG_LEVELS[:error]
|
|
51
|
+
f.print(@error_log_delimiter)
|
|
52
|
+
elsif newline
|
|
53
|
+
f.print newline
|
|
54
|
+
end
|
|
47
55
|
end
|
|
48
56
|
end
|
|
49
57
|
|
|
50
|
-
def report_to_sentry(e, log_level, data: nil, timestamp: timestamp)
|
|
58
|
+
def report_to_sentry(e, log_level, data: nil, timestamp: timestamp, newline: true)
|
|
51
59
|
return unless TxCatcher::Config["logger"]["sentry_dsn"]
|
|
52
60
|
data ||= {}
|
|
53
61
|
data.merge!(environment: Config["environment"], host: Config["host"], currency: Config["currency"])
|
|
@@ -5,23 +5,36 @@ module TxCatcher
|
|
|
5
5
|
plugin :validation_helpers
|
|
6
6
|
one_to_many :deposits
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
attr_accessor :manual_rpc_request
|
|
9
|
+
|
|
10
|
+
def self.find_or_create_from_rpc(txid)
|
|
9
11
|
if tx = self.where(txid: txid).first
|
|
12
|
+
tx.update_block_height!
|
|
10
13
|
tx
|
|
11
14
|
else
|
|
12
|
-
self.
|
|
15
|
+
self.create_from_rpc(txid)
|
|
13
16
|
end
|
|
14
17
|
end
|
|
15
18
|
|
|
16
|
-
def self.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
tx = self.new(hex:
|
|
19
|
+
def self.create_from_rpc(txid)
|
|
20
|
+
raise BitcoinRPC::NoTxIndexErorr, "Cannot create transaction from RPC request, (txid: #{txid}) please use -txindex and don't use pruning" unless TxCatcher.rpc_node.txindex_enabled?
|
|
21
|
+
if tx_from_rpc = TxCatcher.rpc_node.getrawtransaction(txid, 1)
|
|
22
|
+
tx = self.new(hex: tx_from_rpc["hex"], txid: txid)
|
|
23
|
+
tx.manual_rpc_request = true
|
|
24
|
+
tx.update_block_height(confirmations: tx_from_rpc["confirmations"])
|
|
20
25
|
tx.save
|
|
21
26
|
tx
|
|
22
27
|
end
|
|
23
28
|
end
|
|
24
29
|
|
|
30
|
+
def log_the_catch!
|
|
31
|
+
manual_rpc_request_caption = (self.manual_rpc_request ? " (fetched via a manual RPC request) " : " ")
|
|
32
|
+
LOGGER.report "tx #{self.txid} caught#{manual_rpc_request_caption}and saved to DB (id: #{self.id}), deposits (outputs):"
|
|
33
|
+
self.deposits.each do |d|
|
|
34
|
+
LOGGER.report " id: #{d.id}, addr: #{d.address.address}, amount: #{CryptoUnit.new(Config["currency"], d.amount, from_unit: :primary).to_standart}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
25
38
|
# Updates only those transactions that have changed
|
|
26
39
|
def self.update_all(transactions)
|
|
27
40
|
transactions_to_update = transactions.select { |t| !t.column_changes.empty? }
|
|
@@ -57,13 +70,15 @@ module TxCatcher
|
|
|
57
70
|
d.rbf_transaction_ids = d.rbf_transaction_ids.uniq
|
|
58
71
|
end
|
|
59
72
|
d.save
|
|
60
|
-
self.rbf_previous_transaction&.update(rbf_next_transaction_id: self.id)
|
|
61
73
|
end
|
|
74
|
+
self.rbf_previous_transaction&.update(rbf_next_transaction_id: self.id)
|
|
75
|
+
self.log_the_catch!
|
|
62
76
|
end
|
|
63
77
|
|
|
64
78
|
def tx_hash
|
|
65
|
-
@tx_hash ||=
|
|
79
|
+
@tx_hash ||= TxCatcher.rpc_node.decoderawtransaction(self.hex)
|
|
66
80
|
end
|
|
81
|
+
alias :parse_transaction :tx_hash
|
|
67
82
|
|
|
68
83
|
def confirmations
|
|
69
84
|
if self.block_height
|
|
@@ -73,19 +88,48 @@ module TxCatcher
|
|
|
73
88
|
end
|
|
74
89
|
end
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
def update_block_height(confirmations: nil)
|
|
92
|
+
return false if self.block_height
|
|
93
|
+
if TxCatcher.rpc_node.txindex_enabled? || !confirmations.nil?
|
|
94
|
+
update_block_height_from_rpc(confirmations: confirmations)
|
|
95
|
+
else
|
|
96
|
+
self.update_block_height_from_latest_blocks
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
end
|
|
81
100
|
|
|
101
|
+
def update_block_height!(confirmations: nil)
|
|
102
|
+
return false if self.block_height
|
|
103
|
+
self.update_block_height(confirmations: confirmations)
|
|
104
|
+
self.save if self.column_changed?(:block_height)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Checks the last n blocks to see if current transaction has been included in any of them,
|
|
108
|
+
# This is for cases when -txindex is not enabled and you can't make an RPC query for a particular
|
|
109
|
+
# txid, which would be more reliable.
|
|
110
|
+
def update_block_height_from_latest_blocks
|
|
111
|
+
blocks = TxCatcher.rpc_node.get_blocks(blocks_to_check_for_inclusion_if_unconfirmed) unless blocks
|
|
82
112
|
for block in blocks.values do
|
|
83
113
|
if block["tx"] && block["tx"].include?(self.txid)
|
|
84
114
|
LOGGER.report "tx #{self.txid} block height updated to #{block["height"]}"
|
|
85
|
-
self.
|
|
115
|
+
self.block_height = block["height"].to_i
|
|
86
116
|
return block["height"].to_i
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Directly queries the RPC, fetches transaction confirmations number and calculates
|
|
122
|
+
# the block_height. Of confirmations number is provided, doesn't do the RPC request
|
|
123
|
+
# (used in Transaction.create_from_rpc).
|
|
124
|
+
def update_block_height_from_rpc(confirmations: nil)
|
|
125
|
+
begin
|
|
126
|
+
confirmations ||= TxCatcher.rpc_node.getrawtransaction(self.txid, 1)["confirmations"]
|
|
127
|
+
self.block_height = confirmations && confirmations > 0 ? TxCatcher.current_block_height - confirmations + 1 : nil
|
|
128
|
+
rescue BitcoinRPC::JSONRPCError => e
|
|
129
|
+
if e.message.include?("No such mempool or blockchain transaction") && self.rbf?
|
|
130
|
+
LOGGER.report "tx #{self.txid} is an RBF transcation with a lower fee, bitcoin RPC says it's not in the mempool anymore. No need to check for confirmations", :warn
|
|
87
131
|
else
|
|
88
|
-
|
|
132
|
+
raise e
|
|
89
133
|
end
|
|
90
134
|
end
|
|
91
135
|
end
|
|
@@ -128,7 +172,7 @@ module TxCatcher
|
|
|
128
172
|
end
|
|
129
173
|
|
|
130
174
|
def input_hexes
|
|
131
|
-
@input_hexes ||= self.tx_hash["vin"].map { |input| input["scriptSig"]["hex"] }.compact.sort
|
|
175
|
+
@input_hexes ||= self.tx_hash["vin"].select { |input| !input["scriptSig"].nil? }.map { |input| input["scriptSig"]["hex"] }.compact.sort
|
|
132
176
|
end
|
|
133
177
|
|
|
134
178
|
def output_addresses
|
|
@@ -150,34 +194,30 @@ module TxCatcher
|
|
|
150
194
|
end
|
|
151
195
|
|
|
152
196
|
def to_json
|
|
153
|
-
#self.tx_hash.to_json
|
|
154
197
|
self.tx_hash.merge(confirmations: self.confirmations, block_height: self.block_height).to_json
|
|
155
198
|
end
|
|
156
199
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
200
|
+
def assign_transaction_attrs
|
|
201
|
+
self.txid = self.tx_hash["txid"] unless self.txid
|
|
202
|
+
self.block_height = self.tx_hash["block_height"] unless self.block_height
|
|
203
|
+
# In order to be able to identify RBF - those are normally transactions with
|
|
204
|
+
# identical inputs and outputs - we hash inputs and outputs that hash serves
|
|
205
|
+
# as an identifier that we store in our DB and thus can search all
|
|
206
|
+
# previous transactions which the current transaction might be an RBF transaction to.
|
|
207
|
+
#
|
|
208
|
+
# A few comments:
|
|
209
|
+
#
|
|
210
|
+
# 1. Although an RBF transaction may techinically have different outputs as per
|
|
211
|
+
# protocol specification, it is true in most cases that outputs will also be
|
|
212
|
+
# the same (that's how most wallets implement RBF). Thus,
|
|
213
|
+
# we're also incorporating outputs into the hashed value.
|
|
214
|
+
#
|
|
215
|
+
# 2. For inputs, we're using input hexes, because pruned bitcoin-core
|
|
216
|
+
# doesn't provide addresses.
|
|
217
|
+
self.inputs_outputs_hash ||= Digest::SHA256.hexdigest((self.input_hexes + self.output_addresses).join(""))
|
|
218
|
+
end
|
|
162
219
|
|
|
163
|
-
|
|
164
|
-
self.txid = self.tx_hash["txid"] unless self.txid
|
|
165
|
-
# In order to be able to identify RBF - those are normally transactions with
|
|
166
|
-
# identical inputs and outputs - we hash inputs and outputs that hash serves
|
|
167
|
-
# as an identifier that we store in our DB and thus can search all
|
|
168
|
-
# previous transactions which the current transaction might be an RBF transaction to.
|
|
169
|
-
#
|
|
170
|
-
# A few comments:
|
|
171
|
-
#
|
|
172
|
-
# 1. Although an RBF transaction may techinically have different outputs as per
|
|
173
|
-
# protocol specification, it is true in most cases that outputs will also be
|
|
174
|
-
# the same (that's how most wallets implement RBF). Thus,
|
|
175
|
-
# we're also incorporating outputs into the hashed value.
|
|
176
|
-
#
|
|
177
|
-
# 2. For inputs, we're using input hexes, because pruned bitcoin-core
|
|
178
|
-
# doesn't provide addresses.
|
|
179
|
-
self.inputs_outputs_hash ||= Digest::SHA256.hexdigest((self.input_hexes + self.output_addresses).join(""))
|
|
180
|
-
end
|
|
220
|
+
private
|
|
181
221
|
|
|
182
222
|
def validate
|
|
183
223
|
super
|
data/lib/txcatcher/server.rb
CHANGED
|
@@ -50,7 +50,7 @@ module TxCatcher
|
|
|
50
50
|
deposits = deposits.map do |d|
|
|
51
51
|
t = d.transaction
|
|
52
52
|
t.update(protected: true) unless t.protected
|
|
53
|
-
t.
|
|
53
|
+
t.update_block_height
|
|
54
54
|
result = {
|
|
55
55
|
txid: t.txid,
|
|
56
56
|
amount: d.amount_in_btc,
|
|
@@ -73,8 +73,8 @@ module TxCatcher
|
|
|
73
73
|
addr = path.last
|
|
74
74
|
|
|
75
75
|
address = Address.find_or_create(address: addr)
|
|
76
|
-
return [200, {}, "{}"] unless address.deposits.empty?
|
|
77
76
|
deposits = Deposit.where(address_id: address.id).limit(params["limit"] || 100).eager(:transaction)
|
|
77
|
+
return [200, {}, "{}"] if address.deposits.empty?
|
|
78
78
|
|
|
79
79
|
transactions = deposits.map { |d| d.transaction }
|
|
80
80
|
transactions.sort! { |t1,t2| t2.created_at <=> t1.created_at }
|
|
@@ -82,7 +82,7 @@ module TxCatcher
|
|
|
82
82
|
transactions.each do |t|
|
|
83
83
|
# If we see a transaction with 0 confirmations, let's check if it got any news confirmations
|
|
84
84
|
# by querying Bitcoind RPC.
|
|
85
|
-
t.
|
|
85
|
+
t.update_block_height_from_rpc if t.confirmations == 0
|
|
86
86
|
# If still not confirmed, let's make it protected so it's not deleted during cleanup.
|
|
87
87
|
t.protected = true if t.confirmations == 0
|
|
88
88
|
end
|
|
@@ -109,10 +109,10 @@ module TxCatcher
|
|
|
109
109
|
def tx(path)
|
|
110
110
|
path = path.sub(/\?.*/, '').split("/").delete_if { |i| i.empty? }
|
|
111
111
|
txid = path.last
|
|
112
|
-
tx = Transaction.
|
|
113
|
-
tx = Transaction.where(txid: txid).first
|
|
112
|
+
tx = Transaction.find_or_create_from_rpc(txid)
|
|
114
113
|
|
|
115
|
-
if tx && !tx.deposits.empty?
|
|
114
|
+
if tx && (!tx.deposits.empty? || tx.rbf?)
|
|
115
|
+
tx.update_block_height! if tx.block_height.nil?
|
|
116
116
|
return [200, {}, tx.to_json]
|
|
117
117
|
else
|
|
118
118
|
return [404, {}, "Transaction not found"]
|
|
@@ -32,13 +32,6 @@ RSpec.describe TxCatcher::Transaction do
|
|
|
32
32
|
expect(TxCatcher::Transaction.where(txid: transaction.txid).count).to eq(1)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
it "updates block height by making manual requests to RPC and searching if tx is included in one of the previous blocks" do
|
|
36
|
-
expect(TxCatcher.rpc_node).to receive(:getblockhash).exactly(10).times.and_return("blockhash123")
|
|
37
|
-
expect(TxCatcher.rpc_node).to receive(:getblock).exactly(10).times.and_return({ "height" => "123", "tx" => [@transaction.txid], "hash" => "blockhash123"})
|
|
38
|
-
@transaction.check_block_height!
|
|
39
|
-
expect(@transaction.block_height).to eq(123)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
35
|
it "updates multiple records at once" do
|
|
43
36
|
transaction1 = TxCatcher::Transaction.create(hex: @hextx, txid: "123")
|
|
44
37
|
transaction2 = TxCatcher::Transaction.create(hex: @hextx, txid: "1234")
|
|
@@ -98,4 +91,15 @@ RSpec.describe TxCatcher::Transaction do
|
|
|
98
91
|
|
|
99
92
|
end
|
|
100
93
|
|
|
94
|
+
describe "updating block height" do
|
|
95
|
+
|
|
96
|
+
it "updates block height by searching if tx is included in one of the previous blocks" do
|
|
97
|
+
expect(TxCatcher.rpc_node).to receive(:getblockhash).exactly(10).times.and_return("blockhash123")
|
|
98
|
+
expect(TxCatcher.rpc_node).to receive(:getblock).exactly(10).times.and_return({ "height" => "123", "tx" => [@transaction.txid], "hash" => "blockhash123"})
|
|
99
|
+
@transaction.check_block_height!
|
|
100
|
+
expect(@transaction.block_height).to eq(123)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
|
|
101
105
|
end
|
data/txcatcher.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Gem::Specification.new do |s|
|
|
2
2
|
s.name = "txcatcher".freeze
|
|
3
|
-
s.version = "0.2.
|
|
3
|
+
s.version = "0.2.8"
|
|
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]
|
|
@@ -29,7 +29,6 @@ Gem::Specification.new do |s|
|
|
|
29
29
|
"db/migrations/003_create_deposits.rb",
|
|
30
30
|
"db/migrations/004_add_timestamps_to_transactions.rb",
|
|
31
31
|
"db/migrations/005_add_protected_flag_to_transactions.rb",
|
|
32
|
-
"db/schema.rb",
|
|
33
32
|
"lib/tasks/db.rake",
|
|
34
33
|
"lib/txcatcher.rb",
|
|
35
34
|
"lib/txcatcher/bitcoin_rpc.rb",
|
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.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Snitko
|
|
@@ -162,7 +162,6 @@ files:
|
|
|
162
162
|
- db/migrations/003_create_deposits.rb
|
|
163
163
|
- db/migrations/004_add_timestamps_to_transactions.rb
|
|
164
164
|
- db/migrations/005_add_protected_flag_to_transactions.rb
|
|
165
|
-
- db/schema.rb
|
|
166
165
|
- lib/tasks/db.rake
|
|
167
166
|
- lib/txcatcher.rb
|
|
168
167
|
- lib/txcatcher/bitcoin_rpc.rb
|
data/db/schema.rb
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
Sequel.migration do
|
|
2
|
-
change do
|
|
3
|
-
create_table(:addresses) do
|
|
4
|
-
String :address, :size=>255, :null=>false
|
|
5
|
-
|
|
6
|
-
primary_key [:address]
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
create_table(:deposits, :ignore_index_errors=>true) do
|
|
10
|
-
primary_key :id
|
|
11
|
-
Bignum :amount, :null=>false
|
|
12
|
-
Integer :transaction_id
|
|
13
|
-
Integer :address_id
|
|
14
|
-
|
|
15
|
-
index [:address_id]
|
|
16
|
-
index [:transaction_id]
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
create_table(:schema_info) do
|
|
20
|
-
Integer :version, :default=>0, :null=>false
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
create_table(:transactions) do
|
|
24
|
-
String :txid, :size=>255, :null=>false
|
|
25
|
-
DateTime :created_at
|
|
26
|
-
TrueClass :protected, :default=>false
|
|
27
|
-
|
|
28
|
-
primary_key [:txid]
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|