txcatcher 0.1.0
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 +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +116 -0
- data/LICENSE.txt +20 -0
- data/README.md +11 -0
- data/Rakefile +28 -0
- data/VERSION +1 -0
- data/bin/txcatcher +9 -0
- data/db/migrations/001_create_transactions.rb +11 -0
- data/db/migrations/002_create_addresses.rb +9 -0
- data/db/migrations/003_create_deposits.rb +10 -0
- data/db/schema.rb +35 -0
- data/lib/tasks/db.rake +52 -0
- data/lib/txcatcher/bitcoin_rpc.rb +30 -0
- data/lib/txcatcher/catcher.rb +103 -0
- data/lib/txcatcher/cleaner.rb +65 -0
- data/lib/txcatcher/config.rb +20 -0
- data/lib/txcatcher/initializer.rb +148 -0
- data/lib/txcatcher/models/address.rb +13 -0
- data/lib/txcatcher/models/deposit.rb +23 -0
- data/lib/txcatcher/models/transaction.rb +55 -0
- data/lib/txcatcher/server.rb +74 -0
- data/lib/txcatcher/utils/hash_string_to_sym_keys.rb +24 -0
- data/lib/txcatcher.rb +21 -0
- data/spec/catcher_spec.rb +96 -0
- data/spec/cleaner_spec.rb +60 -0
- data/spec/config/config.yml +22 -0
- data/spec/config/txcatcher_test.db +0 -0
- data/spec/fixtures/transaction.txt +1 -0
- data/spec/fixtures/transaction_decoded_no_outputs.txt +1 -0
- data/spec/models/address_spec.rb +5 -0
- data/spec/models/transaction_spec.rb +26 -0
- data/spec/spec_helper.rb +46 -0
- data/templates/config.yml +23 -0
- metadata +167 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
module TxCatcher
|
|
2
|
+
|
|
3
|
+
VERSION = File.read(File.expand_path('../../', File.dirname(__FILE__)) + '/VERSION')
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
attr_accessor :db_connection, :rpc_node, :current_block_height, :config_dir
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module Initializer
|
|
10
|
+
|
|
11
|
+
GEM_ROOT = File.expand_path('../..', File.dirname(__FILE__))
|
|
12
|
+
MIGRATIONS_ROOT = GEM_ROOT + '/db/migrations/'
|
|
13
|
+
|
|
14
|
+
module ConfigFile
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
|
|
18
|
+
# Determine config dir or set default. Useful when we want to
|
|
19
|
+
# have different settings for production or staging or development environments.
|
|
20
|
+
def set!(path=nil)
|
|
21
|
+
@@config_file = path and return if path
|
|
22
|
+
@@config_file = ENV['HOME'] + '/.txcatcher/config.yml'
|
|
23
|
+
ARGV.each_with_index do |a,i|
|
|
24
|
+
if a =~ /\A--config-file=.+/
|
|
25
|
+
@@config_file = File.expand_path(a.sub('--config-file=', ''))
|
|
26
|
+
ARGV.delete_at(i)
|
|
27
|
+
break
|
|
28
|
+
elsif a =~ /\A-c\Z/
|
|
29
|
+
@@config_file = File.expand_path(ARGV[1])
|
|
30
|
+
ARGV.delete_at(i) and ARGV.delete_at(i+1)
|
|
31
|
+
break
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
TxCatcher::Config.config_dir = File.dirname(@@config_file)
|
|
35
|
+
puts "Using config file: #{@@config_file}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def path
|
|
39
|
+
@@config_file
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def prepare
|
|
47
|
+
ConfigFile.set!
|
|
48
|
+
create_config_files
|
|
49
|
+
read_config_file
|
|
50
|
+
connect_to_db
|
|
51
|
+
connect_to_rpc_node
|
|
52
|
+
run_migrations if migrations_pending?
|
|
53
|
+
set_server_port
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def add_route(path, &block)
|
|
57
|
+
@routes[path] = block
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def create_config_files
|
|
61
|
+
FileUtils.mkdir_p(ConfigFile.path) unless File.exist?(ConfigFile.path)
|
|
62
|
+
|
|
63
|
+
unless File.exist?(ConfigFile.path)
|
|
64
|
+
puts "\e[1;33mWARNING!\e[0m \e[33mNo file #{ConfigFile.path} was found. Created a sample one for you.\e[0m"
|
|
65
|
+
puts "You should edit it and try starting the server again.\n"
|
|
66
|
+
|
|
67
|
+
FileUtils.cp(GEM_ROOT + '/templates/config.yml', ConfigFile.path)
|
|
68
|
+
config_contents = File.read(ConfigFile.path)
|
|
69
|
+
config_contents.sub!("$home", File.expand_path('~'))
|
|
70
|
+
File.open(ConfigFile.path, "w") { |f| f.write(config_contents) }
|
|
71
|
+
puts "Shutting down now.\n\n"
|
|
72
|
+
exit
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def read_config_file
|
|
78
|
+
YAML.load_file(ConfigFile.path).each do |k,v|
|
|
79
|
+
TxCatcher::Config.send(k + '=', v)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def set_server_port
|
|
84
|
+
# Setting default port to 9498
|
|
85
|
+
unless ARGV.include?("-p")
|
|
86
|
+
ARGV.push "-p"
|
|
87
|
+
if Config.server_port
|
|
88
|
+
ARGV.push Config.server_port.to_s
|
|
89
|
+
else
|
|
90
|
+
ARGV.push "9498"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def connect_to_db
|
|
96
|
+
|
|
97
|
+
# symbolize keys for convenience
|
|
98
|
+
db_config = TxCatcher::Config.db.keys_to_sym
|
|
99
|
+
|
|
100
|
+
db_name = if db_config[:adapter] == 'sqlite'
|
|
101
|
+
if db_config[:db_path].start_with?("/")
|
|
102
|
+
db_config[:db_path]
|
|
103
|
+
else
|
|
104
|
+
"#{File.dirname(ConfigFile.path)}/#{db_config[:db_path]}"
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
db_config[:db_name]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
TxCatcher.db_connection = Sequel.connect(
|
|
111
|
+
"#{db_config[:adapter]}://" +
|
|
112
|
+
"#{db_config[:user]}#{(":" if db_config[:user])}" +
|
|
113
|
+
"#{db_config[:password]}#{("@" if db_config[:user] || db_config[:password])}" +
|
|
114
|
+
"#{db_config[:host]}#{(":" if db_config[:port])}" +
|
|
115
|
+
"#{db_config[:port]}#{("/" if db_config[:host] || db_config[:port])}" +
|
|
116
|
+
"#{db_name}"
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Connects to all bitcoin daemons listed in config
|
|
121
|
+
def connect_to_rpc_node
|
|
122
|
+
n = TxCatcher::Config.rpcnode
|
|
123
|
+
print "Checking #{n["name"]} RPC connection... "
|
|
124
|
+
begin
|
|
125
|
+
TxCatcher.rpc_node = BitcoinRPC.new("http://#{n["user"]}:#{n["password"]}@#{n["host"]}:#{n["port"]}")
|
|
126
|
+
TxCatcher.current_block_height = TxCatcher.rpc_node.getblockcount
|
|
127
|
+
print "balance: #{TxCatcher.rpc_node.getbalance}\n"
|
|
128
|
+
print "current block height: #{TxCatcher.current_block_height}\n"
|
|
129
|
+
rescue Errno::ECONNREFUSED
|
|
130
|
+
print "ERROR, cannot connect using connection data #{n["name"]}\n"
|
|
131
|
+
exit
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def run_migrations
|
|
136
|
+
Sequel.extension :migration
|
|
137
|
+
print "\nPending migrations for the database detected. Migrating..."
|
|
138
|
+
Sequel::Migrator.run(TxCatcher.db_connection, MIGRATIONS_ROOT)
|
|
139
|
+
print "done\n\n"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def migrations_pending?
|
|
143
|
+
!Sequel::Migrator.is_current?(TxCatcher.db_connection, MIGRATIONS_ROOT)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module TxCatcher
|
|
2
|
+
|
|
3
|
+
class Deposit < Sequel::Model
|
|
4
|
+
many_to_one :transaction
|
|
5
|
+
many_to_one :address
|
|
6
|
+
|
|
7
|
+
attr_accessor :address_string
|
|
8
|
+
|
|
9
|
+
def before_save
|
|
10
|
+
if @address_string
|
|
11
|
+
self.address = Address.find_or_create(address: @address_string)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
self.address.update(received: self.address.received + self.amount)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def amount_in_btc
|
|
18
|
+
Satoshi.new(self.amount, from_unit: :satoshi).to_btc
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module TxCatcher
|
|
2
|
+
|
|
3
|
+
class Transaction < Sequel::Model
|
|
4
|
+
one_to_many :deposits
|
|
5
|
+
|
|
6
|
+
def before_validation
|
|
7
|
+
return if !self.new? || !self.deposits.empty?
|
|
8
|
+
parse_transaction
|
|
9
|
+
assign_transaction_attrs
|
|
10
|
+
@tx_hash["vout"].uniq { |out| out["n"] }.each do |out|
|
|
11
|
+
amount = Satoshi.new(out["value"], from_unit: :btc).to_i if out["value"]
|
|
12
|
+
address = out["scriptPubKey"]["addresses"]&.first
|
|
13
|
+
# Do not create a new deposit unless it actually makes sense to create one
|
|
14
|
+
if address && amount && amount > 0
|
|
15
|
+
self.deposits << Deposit.new(amount: amount, address_string: address)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def after_save
|
|
21
|
+
self.deposits.each do |d|
|
|
22
|
+
d.transaction = self
|
|
23
|
+
d.save
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def tx_hash
|
|
28
|
+
@tx_hash || parse_transaction
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def confirmations
|
|
32
|
+
if self.block_height
|
|
33
|
+
TxCatcher.current_block_height - self.block_height + 1
|
|
34
|
+
else
|
|
35
|
+
0
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def parse_transaction
|
|
42
|
+
@tx_hash = TxCatcher.rpc_node.decoderawtransaction(self.hex)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def assign_transaction_attrs
|
|
46
|
+
self.txid = @tx_hash["txid"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate
|
|
50
|
+
errors.add(:base, "No outputs for this transactions") if self.deposits.empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module TxCatcher
|
|
2
|
+
|
|
3
|
+
class Server < Goliath::API
|
|
4
|
+
|
|
5
|
+
use Goliath::Rack::Params
|
|
6
|
+
|
|
7
|
+
def response(env)
|
|
8
|
+
uri = env["REQUEST_URI"]
|
|
9
|
+
return route_for(uri)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def route_for(path)
|
|
13
|
+
puts "[REQUEST: #{path}]"
|
|
14
|
+
if path =~ /\A\/addr\/[0-9a-zA-Z]+\/utxo/
|
|
15
|
+
utxo(path)
|
|
16
|
+
elsif path.start_with? "/addr/"
|
|
17
|
+
address(path)
|
|
18
|
+
elsif path.start_with? "/tx/send"
|
|
19
|
+
broadcast_tx(params["rawtx"])
|
|
20
|
+
else
|
|
21
|
+
[404, {}, { error: "404, not found" }.to_json]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def address(path)
|
|
26
|
+
path = path.split("/").delete_if { |i| i.empty? }
|
|
27
|
+
addr = path.last
|
|
28
|
+
|
|
29
|
+
model = Address.where(address: addr).eager(deposits: :transactions).first
|
|
30
|
+
if model
|
|
31
|
+
transactions_ids = model.deposits.map { |d| d.transaction.txid }
|
|
32
|
+
deposits = model.deposits.map do |d|
|
|
33
|
+
t = d.transaction
|
|
34
|
+
{
|
|
35
|
+
txid: t.txid,
|
|
36
|
+
amount: d.amount_in_btc,
|
|
37
|
+
satoshis: d.amount,
|
|
38
|
+
confirmations: 0
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
[200, {}, { address: model.address, received: model.received, deposits: deposits }.to_json]
|
|
42
|
+
else
|
|
43
|
+
[200, {}, { address: addr, received: 0, deposits: [] }.to_json]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def utxo(path)
|
|
48
|
+
path = path.split("/").delete_if { |i| i.empty? }
|
|
49
|
+
path.pop
|
|
50
|
+
addr = path.last
|
|
51
|
+
|
|
52
|
+
model = Address.where(address: addr).eager(deposits: :transactions).first
|
|
53
|
+
transactions = model.deposits.map { |d| d.transaction }
|
|
54
|
+
utxos = transactions.map do |t|
|
|
55
|
+
outs = t.tx_hash["vout"].select { |out| out["scriptPubKey"]["addresses"] == [addr] }
|
|
56
|
+
outs.map! do |out|
|
|
57
|
+
out["confirmations"] = t.confirmations || 0
|
|
58
|
+
out["txid"] = t.txid
|
|
59
|
+
out
|
|
60
|
+
end
|
|
61
|
+
outs
|
|
62
|
+
end.flatten
|
|
63
|
+
[200, {}, utxos.to_json]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def broadcast_tx(txhex)
|
|
67
|
+
TxCatcher.rpc_node.sendrawtransaction(txhex)
|
|
68
|
+
tx = TxCatcher.rpc_node.decoderawtransaction(txhex)
|
|
69
|
+
[200, {}, tx.to_json]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class Hash
|
|
2
|
+
|
|
3
|
+
# Replace String keys in the current hash with symbol keys
|
|
4
|
+
def keys_to_sym!
|
|
5
|
+
new_hash = keys_to_sym
|
|
6
|
+
self.clear
|
|
7
|
+
new_hash.each do |k,v|
|
|
8
|
+
self[k] = v
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def keys_to_sym
|
|
13
|
+
symbolized_hash = {}
|
|
14
|
+
self.each do |k,v|
|
|
15
|
+
if k =~ /\A[a-zA-Z0-9!?_]+\Z/
|
|
16
|
+
symbolized_hash[k.to_sym] = v
|
|
17
|
+
else
|
|
18
|
+
symbolized_hash[k] = v
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
symbolized_hash
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
end
|
data/lib/txcatcher.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'yaml'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'thread'
|
|
5
|
+
require 'satoshi-unit'
|
|
6
|
+
require 'sequel'
|
|
7
|
+
Sequel.extension :migration
|
|
8
|
+
|
|
9
|
+
require_relative 'txcatcher/utils/hash_string_to_sym_keys'
|
|
10
|
+
require_relative 'txcatcher/bitcoin_rpc'
|
|
11
|
+
require_relative 'txcatcher/config'
|
|
12
|
+
require_relative 'txcatcher/initializer'
|
|
13
|
+
require_relative 'txcatcher/catcher'
|
|
14
|
+
require_relative 'txcatcher/cleaner'
|
|
15
|
+
|
|
16
|
+
include TxCatcher::Initializer
|
|
17
|
+
prepare
|
|
18
|
+
|
|
19
|
+
require_relative 'txcatcher/models/transaction'
|
|
20
|
+
require_relative 'txcatcher/models/address'
|
|
21
|
+
require_relative 'txcatcher/models/deposit'
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'ffi-rzmq'
|
|
3
|
+
|
|
4
|
+
require_relative 'spec_helper'
|
|
5
|
+
require_relative '../lib/txcatcher/models/address'
|
|
6
|
+
require_relative '../lib/txcatcher/models/transaction'
|
|
7
|
+
require_relative '../lib/txcatcher/catcher'
|
|
8
|
+
|
|
9
|
+
RSpec.describe TxCatcher::Catcher do
|
|
10
|
+
|
|
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_test.rawtx")
|
|
15
|
+
@block_sock.bind("ipc:///tmp/bitcoind_test.hashblock")
|
|
16
|
+
@hextx = File.read(File.dirname(__FILE__) + "/fixtures/transaction.txt").strip
|
|
17
|
+
@rawtx = unhexlify(File.read(File.dirname(__FILE__) + "/fixtures/transaction.txt"))
|
|
18
|
+
@catcher = TxCatcher::Catcher.new(name: "bitcoind_test")
|
|
19
|
+
sleep 1
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
after(:all) do
|
|
23
|
+
@tx_sock.unbind('ipc:///tmp/bitcoind_test.rawtx')
|
|
24
|
+
@block_sock.unbind('ipc:///tmp/bitcoind_test.hashblock')
|
|
25
|
+
end
|
|
26
|
+
|
|
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
|
|
35
|
+
|
|
36
|
+
i = 0
|
|
37
|
+
until tx = TxCatcher::Transaction.last
|
|
38
|
+
sleep 1 and i += 1
|
|
39
|
+
end
|
|
40
|
+
expect(tx.hex).to eq(@hextx)
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "upon receiving a new block updates transactions block height" do
|
|
45
|
+
transaction = TxCatcher::Transaction.create(hex: @hextx)
|
|
46
|
+
expect(TxCatcher.rpc_node).to receive(:getblock).and_return({ "height" => TxCatcher.current_block_height + 1, "tx" => [transaction.txid]})
|
|
47
|
+
@block_sock.send_string('hashblock', ZMQ::SNDMORE)
|
|
48
|
+
@block_sock.send_string("hello")
|
|
49
|
+
|
|
50
|
+
i = 0
|
|
51
|
+
until transaction.reload.confirmations == 1
|
|
52
|
+
sleep 1 and i += 1
|
|
53
|
+
end
|
|
54
|
+
expect(transaction.block_height).to eq(TxCatcher.current_block_height)
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe "error logging" do
|
|
59
|
+
|
|
60
|
+
before(:each) do
|
|
61
|
+
@error_log = File.dirname(__FILE__) + "/config/error.log"
|
|
62
|
+
TxCatcher::Config.config_dir = File.dirname(@error_log)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
after(:each) do
|
|
66
|
+
File.delete(@error_log) if File.exists?(@error_log)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "ignores validation errors" do
|
|
70
|
+
tx = eval File.read(File.dirname(__FILE__) + "/fixtures/transaction_decoded_no_outputs.txt")
|
|
71
|
+
allow(TxCatcher.rpc_node).to receive(:decoderawtransaction).and_return(tx)
|
|
72
|
+
@tx_sock.send_string('rawtx', ZMQ::SNDMORE)
|
|
73
|
+
@tx_sock.send_string(@rawtx)
|
|
74
|
+
|
|
75
|
+
i = 0
|
|
76
|
+
until (@catcher.sockets.values&.first && @catcher.sockets.values.first[:last_message]) || i > 10
|
|
77
|
+
sleep 1 and i += 1
|
|
78
|
+
end
|
|
79
|
+
expect(File.exists?(@error_log)).to be_falsey
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "logs all other errors" do
|
|
83
|
+
allow(TxCatcher.rpc_node).to receive(:decoderawtransaction).and_raise(StandardError)
|
|
84
|
+
@tx_sock.send_string('rawtx', ZMQ::SNDMORE)
|
|
85
|
+
@tx_sock.send_string(@rawtx)
|
|
86
|
+
|
|
87
|
+
i = 0
|
|
88
|
+
until File.exists?(@error_log)
|
|
89
|
+
sleep 1 and i += 1
|
|
90
|
+
end
|
|
91
|
+
expect(File.read(@error_log)).not_to be_empty
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'ffi-rzmq'
|
|
3
|
+
|
|
4
|
+
require_relative 'spec_helper'
|
|
5
|
+
require_relative '../lib/txcatcher/models/address'
|
|
6
|
+
require_relative '../lib/txcatcher/models/transaction'
|
|
7
|
+
require_relative '../lib/txcatcher/cleaner'
|
|
8
|
+
|
|
9
|
+
RSpec.describe TxCatcher::Cleaner do
|
|
10
|
+
|
|
11
|
+
before(:each) do
|
|
12
|
+
allow(TxCatcher.rpc_node).to receive(:decoderawtransaction).and_return({ "vout" => []})
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "doesn't clean anything if transaction count is below threshold" do
|
|
16
|
+
create_transactions(9)
|
|
17
|
+
clean_transactions
|
|
18
|
+
expect(TxCatcher::Transaction.count).to eq(9)
|
|
19
|
+
expect(TxCatcher::Deposit.count).to eq(9)
|
|
20
|
+
expect(TxCatcher::Address.count).to eq(9)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "cleans excessive transactions from the DB" do
|
|
24
|
+
create_transactions(15)
|
|
25
|
+
clean_transactions
|
|
26
|
+
expect(TxCatcher::Transaction.count).to eq(9)
|
|
27
|
+
expect(TxCatcher::Deposit.count).to eq(9)
|
|
28
|
+
expect(TxCatcher::Address.count).to eq(9)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "doesn't delete the address upon cleanup if it has another deposit associated with it" do
|
|
32
|
+
create_transactions(15)
|
|
33
|
+
|
|
34
|
+
d = TxCatcher::Deposit.create(address_string: "addr1", amount: 0)
|
|
35
|
+
tx = TxCatcher::Transaction.last
|
|
36
|
+
tx.deposits << d
|
|
37
|
+
tx.save
|
|
38
|
+
|
|
39
|
+
clean_transactions
|
|
40
|
+
expect(TxCatcher::Transaction.count).to eq(9)
|
|
41
|
+
expect(TxCatcher::Deposit.count).to eq(10)
|
|
42
|
+
expect(TxCatcher::Address.count).to eq(10)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_transactions(n)
|
|
47
|
+
(1..n).to_a.each do |i|
|
|
48
|
+
d = TxCatcher::Deposit.new(address_string: "addr#{i}", amount: 0)
|
|
49
|
+
tx = TxCatcher::Transaction.new
|
|
50
|
+
tx.deposits << d
|
|
51
|
+
tx.save
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def clean_transactions
|
|
56
|
+
TxCatcher::Cleaner.start(run_once: true)
|
|
57
|
+
sleep 1 until TxCatcher::Cleaner.stopped?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
db:
|
|
2
|
+
adapter: sqlite
|
|
3
|
+
db_path: txcatcher_test.db
|
|
4
|
+
|
|
5
|
+
# No need to set these options for sqlite,
|
|
6
|
+
# but other DBs may require them.
|
|
7
|
+
#
|
|
8
|
+
#user: username
|
|
9
|
+
#password: password
|
|
10
|
+
#host: hostname
|
|
11
|
+
#port: 1234
|
|
12
|
+
|
|
13
|
+
rpcnode:
|
|
14
|
+
name: bitcoind
|
|
15
|
+
user: roman
|
|
16
|
+
password: 12990duxcibciwebf
|
|
17
|
+
host: 127.0.0.1
|
|
18
|
+
port: 8332
|
|
19
|
+
|
|
20
|
+
# zeromq: bitcoind
|
|
21
|
+
max_db_transactions_stored: 10
|
|
22
|
+
db_clean_period_seconds: 300
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0100000001b0130468da00458400a40db0509a2d41fb6b3cf8cc54bd494a5ae4439979e9af01000000fdfe000048304502210085ba337244b670a3241af0a47cc05cdbf633dfda94c26fe8c9cfa77ab4e14bb002203d046ccc53d715d0632420c964eca9d8d6bca922fc95dd09344016e53793e59e01483045022100b111fe6e28d07aff12494976e4fd168f0265f21cc3c5abf5e83f4b872124e1930220669cd375691ef92f905efaaece66f672902294d283529c2b9ffcd488c861e766014c6952210204568228ddc9bd466bd346a9789eeec116ccdd1fda7823c264740b1261b5c4c221025df4d301519e9fb509361c025c5286d23ecf89be4060418ce2cec1e4a58c48d421031dc9a14a2daf2f20581e885c466fa6f6ace0e00a155fce9df476aadc390b2d3553aeffffffff02e046d11b0000000017a91403d7a39c85363805fb19e972c1e5c5956a75b016871e8e704f0000000017a9143921d410a0360f72d07b607173923b1e8ebab7578700000000
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"txid"=>"faf8368fec418a971e8fa94943e951490759498e0b2d3c4d5b9b12212c4f2e5a", "hash"=>"faf8368fec418a971e8fa94943e951490759498e0b2d3c4d5b9b12212c4f2e5a", "size"=>371, "vsize"=>371, "version"=>1, "locktime"=>0, "vin"=>[{"txid"=>"afe9799943e45a4a49bd54ccf83c6bfb412d9a50b00da400844500da680413b0", "vout"=>1, "scriptSig"=>{"asm"=>"0 304502210085ba337244b670a3241af0a47cc05cdbf633dfda94c26fe8c9cfa77ab4e14bb002203d046ccc53d715d0632420c964eca9d8d6bca922fc95dd09344016e53793e59e[ALL] 3045022100b111fe6e28d07aff12494976e4fd168f0265f21cc3c5abf5e83f4b872124e1930220669cd375691ef92f905efaaece66f672902294d283529c2b9ffcd488c861e766[ALL] 52210204568228ddc9bd466bd346a9789eeec116ccdd1fda7823c264740b1261b5c4c221025df4d301519e9fb509361c025c5286d23ecf89be4060418ce2cec1e4a58c48d421031dc9a14a2daf2f20581e885c466fa6f6ace0e00a155fce9df476aadc390b2d3553ae", "hex"=>"0048304502210085ba337244b670a3241af0a47cc05cdbf633dfda94c26fe8c9cfa77ab4e14bb002203d046ccc53d715d0632420c964eca9d8d6bca922fc95dd09344016e53793e59e01483045022100b111fe6e28d07aff12494976e4fd168f0265f21cc3c5abf5e83f4b872124e1930220669cd375691ef92f905efaaece66f672902294d283529c2b9ffcd488c861e766014c6952210204568228ddc9bd466bd346a9789eeec116ccdd1fda7823c264740b1261b5c4c221025df4d301519e9fb509361c025c5286d23ecf89be4060418ce2cec1e4a58c48d421031dc9a14a2daf2f20581e885c466fa6f6ace0e00a155fce9df476aadc390b2d3553ae"}, "sequence"=>4294967295}], "vout"=>[]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require_relative '../spec_helper'
|
|
2
|
+
|
|
3
|
+
RSpec.describe TxCatcher::Transaction do
|
|
4
|
+
|
|
5
|
+
before(:each) do
|
|
6
|
+
@hextx = File.read(File.dirname(__FILE__) + "/../fixtures/transaction.txt").strip
|
|
7
|
+
@transaction = TxCatcher::Transaction.create(hex: @hextx)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "parses rawtx using bitcoind" do
|
|
11
|
+
expect(@transaction.txid).to eq("faf8368fec418a971e8fa94943e951490759498e0b2d3c4d5b9b12212c4f2e5a")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "creates addresses and deposits" do
|
|
15
|
+
expect(TxCatcher::Address.where(address: "323LGzCm43NgbtoYJhT6oKSwmKFTQ7AHzH").first.received).to eq(466700000)
|
|
16
|
+
expect(TxCatcher::Address.where(address: "36u6xv2TvZqPPYdogzfLZpMAXrYdW4Vwjp").first.received).to eq(1332776478)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "doesn't create duplicate deposits if valid? called manually before save" do
|
|
20
|
+
transaction = TxCatcher::Transaction.new(hex: @hextx)
|
|
21
|
+
transaction.valid?
|
|
22
|
+
transaction.save
|
|
23
|
+
expect(transaction.deposits.size).to eq(2)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'yaml'
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'satoshi-unit'
|
|
5
|
+
require 'sequel'
|
|
6
|
+
Sequel.extension :migration
|
|
7
|
+
|
|
8
|
+
db_file = File.dirname(__FILE__) + "/config/txcatcher_test.db"
|
|
9
|
+
File.delete(db_file) if File.exists?(db_file)
|
|
10
|
+
|
|
11
|
+
require_relative '../lib/txcatcher/utils/hash_string_to_sym_keys'
|
|
12
|
+
require_relative '../lib/txcatcher/config'
|
|
13
|
+
require_relative '../lib/txcatcher/initializer'
|
|
14
|
+
require_relative '../lib/txcatcher/bitcoin_rpc'
|
|
15
|
+
|
|
16
|
+
include TxCatcher::Initializer
|
|
17
|
+
ConfigFile.set!(File.dirname(__FILE__) + "/config/config.yml")
|
|
18
|
+
|
|
19
|
+
read_config_file
|
|
20
|
+
connect_to_db
|
|
21
|
+
run_migrations if migrations_pending?
|
|
22
|
+
connect_to_rpc_node
|
|
23
|
+
|
|
24
|
+
require_relative '../lib/txcatcher/models/transaction'
|
|
25
|
+
require_relative '../lib/txcatcher/models/address'
|
|
26
|
+
require_relative '../lib/txcatcher/models/deposit'
|
|
27
|
+
|
|
28
|
+
def unhexlify(msg)
|
|
29
|
+
msg.scan(/../).collect { |c| c.to_i(16).chr }.join
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
RSpec.configure do |config|
|
|
33
|
+
|
|
34
|
+
config.before(:all) do
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
config.after(:all) do
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
config.before(:each) do
|
|
41
|
+
TxCatcher::Transaction.select_all.delete
|
|
42
|
+
TxCatcher::Deposit.select_all.delete
|
|
43
|
+
TxCatcher::Address.select_all.delete
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
db:
|
|
2
|
+
adapter: sqlite
|
|
3
|
+
db_path: txcatcher_test.db
|
|
4
|
+
|
|
5
|
+
# No need to set these options for sqlite,
|
|
6
|
+
# but other DBs may require them.
|
|
7
|
+
#
|
|
8
|
+
#user: username
|
|
9
|
+
#password: password
|
|
10
|
+
#host: hostname
|
|
11
|
+
#port: 1234
|
|
12
|
+
|
|
13
|
+
rpcnode:
|
|
14
|
+
name: bitcoind
|
|
15
|
+
user: roman
|
|
16
|
+
password: 12990duxcibciwebf
|
|
17
|
+
host: 127.0.0.1
|
|
18
|
+
port: 8332
|
|
19
|
+
|
|
20
|
+
zeromq: bitcoind
|
|
21
|
+
max_db_transactions_stored: 100000
|
|
22
|
+
db_clean_period_seconds: 300
|
|
23
|
+
server_port: 9498
|