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