txcatcher 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 19e98ad3469e868a632b24ac5b511e557be28c9c
4
+ data.tar.gz: 83b80df4f5cd3b2367adfbc26e9782184c8901f7
5
+ SHA512:
6
+ metadata.gz: 6e5e8b687a4feb36016a0ebe7f8e07cb0c604dc9add1d31b6ce0ca412fa871f7222f45f102cef733017276277b301a52dd9f3de56a06deaa7581ef2eb1819cb5
7
+ data.tar.gz: 8436da5751fdf20ce4ef8d45d0d58842d3f807e73ae8fe0b83412c919b41e1bccb47d2c780e07c5cc9d6ae3f083953e195ca73c8bc1dd8bbf2b370126a1f3efc
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "goliath"
4
+ gem "sequel"
5
+ gem "ffi-rzmq"
6
+ gem "satoshi-unit"
7
+
8
+ group :development do
9
+ gem "bundler"
10
+ gem "jeweler"
11
+ end
12
+
13
+ group :test do
14
+ gem "rspec"
15
+ gem "sqlite3"
16
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,116 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ addressable (2.4.0)
5
+ async-rack (0.5.1)
6
+ rack (~> 1.1)
7
+ builder (3.2.3)
8
+ descendants_tracker (0.0.4)
9
+ thread_safe (~> 0.3, >= 0.3.1)
10
+ diff-lcs (1.3)
11
+ einhorn (0.7.4)
12
+ em-synchrony (1.0.6)
13
+ eventmachine (>= 1.0.0.beta.1)
14
+ em-websocket (0.3.8)
15
+ addressable (>= 2.1.1)
16
+ eventmachine (>= 0.12.9)
17
+ eventmachine (1.2.5)
18
+ faraday (0.9.2)
19
+ multipart-post (>= 1.2, < 3)
20
+ ffi (1.9.18)
21
+ ffi-rzmq (2.0.5)
22
+ ffi-rzmq-core (>= 1.0.6)
23
+ ffi-rzmq-core (1.0.6)
24
+ ffi
25
+ git (1.3.0)
26
+ github_api (0.16.0)
27
+ addressable (~> 2.4.0)
28
+ descendants_tracker (~> 0.0.4)
29
+ faraday (~> 0.8, < 0.10)
30
+ hashie (>= 3.4)
31
+ mime-types (>= 1.16, < 3.0)
32
+ oauth2 (~> 1.0)
33
+ goliath (1.0.5)
34
+ async-rack
35
+ einhorn
36
+ em-synchrony (>= 1.0.0)
37
+ em-websocket (= 0.3.8)
38
+ eventmachine (>= 1.0.0.beta.4)
39
+ http_parser.rb (>= 0.6.0)
40
+ log4r
41
+ multi_json
42
+ rack (>= 1.2.2)
43
+ rack-contrib
44
+ rack-respond_to
45
+ hashie (3.5.6)
46
+ highline (1.7.8)
47
+ http_parser.rb (0.6.0)
48
+ jeweler (2.3.7)
49
+ builder
50
+ bundler (>= 1)
51
+ git (>= 1.2.5)
52
+ github_api (~> 0.16.0)
53
+ highline (>= 1.6.15)
54
+ nokogiri (>= 1.5.10)
55
+ psych (~> 2.2)
56
+ rake
57
+ rdoc
58
+ semver2
59
+ jwt (1.5.6)
60
+ log4r (1.1.10)
61
+ mime-types (2.99.3)
62
+ mini_portile2 (2.2.0)
63
+ multi_json (1.12.1)
64
+ multi_xml (0.6.0)
65
+ multipart-post (2.0.0)
66
+ nokogiri (1.8.0)
67
+ mini_portile2 (~> 2.2.0)
68
+ oauth2 (1.4.0)
69
+ faraday (>= 0.8, < 0.13)
70
+ jwt (~> 1.0)
71
+ multi_json (~> 1.3)
72
+ multi_xml (~> 0.5)
73
+ rack (>= 1.2, < 3)
74
+ psych (2.2.4)
75
+ rack (1.6.8)
76
+ rack-accept-media-types (0.9)
77
+ rack-contrib (1.5.0)
78
+ rack (~> 1.4)
79
+ rack-respond_to (0.9.8)
80
+ rack-accept-media-types (>= 0.6)
81
+ rake (12.0.0)
82
+ rdoc (5.1.0)
83
+ rspec (3.6.0)
84
+ rspec-core (~> 3.6.0)
85
+ rspec-expectations (~> 3.6.0)
86
+ rspec-mocks (~> 3.6.0)
87
+ rspec-core (3.6.0)
88
+ rspec-support (~> 3.6.0)
89
+ rspec-expectations (3.6.0)
90
+ diff-lcs (>= 1.2.0, < 2.0)
91
+ rspec-support (~> 3.6.0)
92
+ rspec-mocks (3.6.0)
93
+ diff-lcs (>= 1.2.0, < 2.0)
94
+ rspec-support (~> 3.6.0)
95
+ rspec-support (3.6.0)
96
+ satoshi-unit (0.2.2)
97
+ semver2 (3.4.2)
98
+ sequel (4.49.0)
99
+ sqlite3 (1.3.13)
100
+ thread_safe (0.3.6)
101
+
102
+ PLATFORMS
103
+ ruby
104
+
105
+ DEPENDENCIES
106
+ bundler
107
+ ffi-rzmq
108
+ goliath
109
+ jeweler
110
+ rspec
111
+ satoshi-unit
112
+ sequel
113
+ sqlite3
114
+
115
+ BUNDLED WITH
116
+ 1.15.4
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2017 Roman Snitko
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,11 @@
1
+ = txcatcher
2
+
3
+ Dependencies
4
+ ------------
5
+
6
+ In order for bitcoin-core and litecoin-core to stream new transactions
7
+ and blocks, we need ZeroMQ. Ubuntu/Debian installation instructions are here:
8
+
9
+ http://zeromq.org/distro:debian
10
+
11
+ you can safely use a package weirdly located at download.opensuse.org.
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
17
+ gem.name = "txcatcher"
18
+ gem.homepage = "http://github.com/snitko/txcatcher"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{An lightweight version of Bitpay's Insight, allows to check Bitcoin/Litecoin addresses}
21
+ gem.description = %Q{Ccurrently, the only job of this gem is to collect all new Bitcoin/Litecoin transactions, store them in a DB, index addresses.}
22
+ gem.email = "roman.snitko@gmail.com"
23
+ gem.authors = ["Roman Snitko"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ Dir.glob('lib/tasks/*.rake').each { |r| load r }
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/bin/txcatcher ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'goliath'
4
+ require_relative '../lib/txcatcher'
5
+ require_relative '../lib/txcatcher/server'
6
+ require 'ffi-rzmq'
7
+
8
+ TxCatcher::Catcher.new(name: TxCatcher::Config.zeromq)
9
+ TxCatcher::Cleaner.start
@@ -0,0 +1,11 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:transactions) do
4
+ primary_key :id
5
+ String :txid, index: true
6
+ String :hex, text: true
7
+ Integer :amount
8
+ Integer :block_height
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:addresses) do
4
+ primary_key :id
5
+ String :address, index: true
6
+ Integer :received, default: 0
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,10 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:deposits) do
4
+ primary_key :id
5
+ Integer :amount, null: false
6
+ foreign_key :transaction_id, index: true
7
+ foreign_key :address_id, index: true
8
+ end
9
+ end
10
+ end
data/db/schema.rb ADDED
@@ -0,0 +1,35 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:addresses, :ignore_index_errors=>true) do
4
+ primary_key :id
5
+ String :address, :size=>255
6
+ Integer :received, :default=>0
7
+
8
+ index [:address]
9
+ end
10
+
11
+ create_table(:deposits, :ignore_index_errors=>true) do
12
+ primary_key :id
13
+ Integer :amount, :null=>false
14
+ Integer :transaction_id
15
+ Integer :address_id
16
+
17
+ index [:address_id]
18
+ index [:transaction_id]
19
+ end
20
+
21
+ create_table(:schema_info) do
22
+ Integer :version, :default=>0, :null=>false
23
+ end
24
+
25
+ create_table(:transactions, :ignore_index_errors=>true) do
26
+ primary_key :id
27
+ String :txid, :size=>255
28
+ String :hex, :text=>true
29
+ Integer :amount
30
+ Integer :block_height
31
+
32
+ index [:txid]
33
+ end
34
+ end
35
+ end
data/lib/tasks/db.rake ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'yaml'
3
+ require 'json'
4
+ require 'sequel'
5
+ require_relative '../txcatcher/utils/hash_string_to_sym_keys'
6
+ require_relative '../txcatcher/config'
7
+ require_relative '../txcatcher/initializer'
8
+
9
+ Sequel.extension :migration
10
+
11
+ namespace :db do
12
+
13
+ task :environment do
14
+ include TxCatcher::Initializer
15
+ ConfigFile.set!
16
+ create_config_files
17
+ read_config_file
18
+ connect_to_db
19
+ end
20
+
21
+ desc "Migrates the database"
22
+ task :migrate, [:step] => :environment do |t, args|
23
+ target = args[:step] && (step = args[:step].to_i) > 0 ?
24
+ current_migration_version + step : nil
25
+
26
+ Sequel::Migrator.run(TxCatcher.db_connection, MIGRATIONS_ROOT, target: target)
27
+ puts "Migrated DB to version #{current_migration_version}"
28
+ dump_schema
29
+ end
30
+
31
+ desc "Rollbacks database migrations"
32
+ task :rollback, [:step] => :environment do |t, args|
33
+ target = args[:step] && (step = args[:step].to_i) > 0 ?
34
+ current_migration_version - step : current_migration_version - 1
35
+
36
+ Sequel::Migrator.run(TxCatcher.db_connection, MIGRATIONS_ROOT, target: target)
37
+ puts "Rolled back DB to version #{current_migration_version}"
38
+ dump_schema
39
+ end
40
+
41
+ def current_migration_version
42
+ db = TxCatcher.db_connection
43
+ Sequel::Migrator.migrator_class(MIGRATIONS_ROOT).new(db, MIGRATIONS_ROOT, {}).current
44
+ end
45
+
46
+ def dump_schema
47
+ TxCatcher.db_connection.extension :schema_dumper
48
+ open('db/schema.rb', 'w') do |f|
49
+ f.puts TxCatcher.db_connection.dump_schema_migration(same_db: false)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,30 @@
1
+ # Credits for this file go to
2
+ # Mikica Ivosevic https://github.com/mikicaivosevic/bitcoin-rpc-ruby/
3
+
4
+ require 'net/http'
5
+ require 'uri'
6
+ require 'json'
7
+
8
+ class BitcoinRPC
9
+ def initialize(service_url)
10
+ @uri = URI.parse(service_url)
11
+ end
12
+
13
+ def method_missing(name, *args)
14
+ post_body = { 'method' => name, 'params' => args, 'id' => 'jsonrpc' }.to_json
15
+ resp = JSON.parse( http_post_request(post_body) )
16
+ raise JSONRPCError, resp['error'].to_s + " (method: '#{name}')" if resp['error']
17
+ resp['result']
18
+ end
19
+
20
+ def http_post_request(post_body)
21
+ http = Net::HTTP.new(@uri.host, @uri.port)
22
+ request = Net::HTTP::Post.new(@uri.request_uri)
23
+ request.basic_auth @uri.user, @uri.password
24
+ request.content_type = 'application/json'
25
+ request.body = post_body
26
+ http.request(request).body
27
+ end
28
+
29
+ class JSONRPCError < RuntimeError; end
30
+ end
@@ -0,0 +1,103 @@
1
+ module TxCatcher
2
+
3
+ class Catcher
4
+
5
+ attr_accessor :name, :sockets
6
+
7
+ def initialize(name:, socket: "ipc:///tmp/")
8
+ @queue = {}
9
+ @sockets = {}
10
+
11
+ {'rawtx' => "#{socket}#{name}.rawtx", 'hashblock' => "#{socket}#{name}.hashblock"}.each do |channel, address|
12
+ puts "Start listening on #{name} #{channel}... (#{address})"
13
+ listen_to_zeromq_message(channel: channel, address: address)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def hexlify(s)
20
+ a = []
21
+ s.each_byte do |b|
22
+ a << sprintf('%02X', b)
23
+ end
24
+ a.join
25
+ end
26
+
27
+ def listen_to_zeromq_message(channel:, address:)
28
+ @queue[channel] = Queue.new
29
+
30
+ # This thread is responsible for actions after the messages 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
+ puts "in #{channel} queue: #{@queue[channel].size}"
36
+ if @queue[channel].empty?
37
+ sleep 1
38
+ else
39
+ begin
40
+ @queue[channel].pop.call
41
+ rescue Sequel::ValidationFailed => e
42
+ $stdout.puts "[WARNING #{Time.now.to_s}] #{e.class} #{e.to_s}\n"
43
+ rescue StandardError => e
44
+ File.open(TxCatcher::Config.config_dir + "/error.log", "a") do |f|
45
+ f.puts "[ERROR #{Time.now.to_s}] #{e.class} #{e.to_s}\n #{e.backtrace.join("\n ")}\n\n"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ # Now we can start receiving messages from ZeroMQ.
53
+ # On every received message we call a handler method, which parses it
54
+ # appropriately (each ZeroMQ channel has its own handler method) and then
55
+ # adds additional tasks, such as writing to the DB, in the queue.
56
+ # They queue itself is handled in the thread created above.
57
+ key = "#{channel}#{address}"
58
+ handler_thread = Thread.new do
59
+ context = ZMQ::Context.new
60
+ socket = context.socket(ZMQ::SUB)
61
+ socket.setsockopt(ZMQ::SUBSCRIBE, channel)
62
+ socket.connect(address)
63
+ @sockets[key] = { object: socket }
64
+ loop do
65
+ topic = []
66
+ message = []
67
+ socket.recv_multipart(topic, message)
68
+ message
69
+ if message[1]
70
+ message_hex = hexlify(message[1].copy_out_string).downcase
71
+ @sockets[key][:last_message] = message_hex
72
+ send("handle_#{channel}", message_hex)
73
+ end
74
+ end
75
+ end
76
+
77
+ end # listen_to_zeromq_message
78
+
79
+ def handle_rawtx(txhex)
80
+ $stdout.print "received tx hex: #{txhex[0..50]}...\n"
81
+
82
+ @queue["rawtx"] << ( Proc.new {
83
+ tx = TxCatcher::Transaction.new(hex: txhex)
84
+ tx.save
85
+ $stdout.puts "tx #{tx.txid} saved (id: #{tx.id}), deposits (outputs):"
86
+ tx.deposits.each do |d|
87
+ $stdout.puts " id: #{d.id}, addr: #{d.address.address}, amount: #{Satoshi.new(d.amount, from_unit: :satoshi).to_btc}"
88
+ end
89
+ })
90
+ end
91
+
92
+ def handle_hashblock(block_hex)
93
+ block_hash = TxCatcher.rpc_node.getblock(block_hex)
94
+ transactions = block_hash["tx"]
95
+ height = TxCatcher.current_block_height = block_hash["height"].to_i
96
+ $stdout.puts "Block #{height} mined, transactions received:\n #{transactions.join(" \n")}"
97
+ @queue["hashblock"] << ( Proc.new {
98
+ Transaction.where(txid: transactions).update(block_height: height)
99
+ })
100
+ end
101
+
102
+ end # class Catcher
103
+ end
@@ -0,0 +1,65 @@
1
+ require 'thread'
2
+
3
+ module TxCatcher
4
+
5
+ # Cleans DB so that its size doesn't go above Config.max_db_transactions_stored
6
+ class Cleaner
7
+
8
+ class << self
9
+
10
+ def stopped?
11
+ @@stopped
12
+ end
13
+
14
+ def start(run_once: false)
15
+ @@stopped = false
16
+ @@stop = false
17
+ Thread.new do
18
+ loop do
19
+ @@cleaning_counter = { transactions: 0, deposits: 0, addresses: 0 }
20
+ db_tx_count = TxCatcher::Transaction.last.id - TxCatcher::Transaction.first.id + 1
21
+ $stdout.print "Cleaning transactions in DB..."
22
+ $stdout.print "#{db_tx_count} now, needs to be below #{Config.max_db_transactions_stored}\n"
23
+ if db_tx_count > Config.max_db_transactions_stored
24
+ number_to_delete = (db_tx_count - Config.max_db_transactions_stored) + (Config.max_db_transactions_stored*0.1).round
25
+ clean_transactions(number_to_delete)
26
+ $stdout.puts "DB cleaned: #{@@cleaning_counter.to_s}"
27
+ else
28
+ $stdout.puts "Nothing to be cleaned from DB, size is below threshold."
29
+ end
30
+ if @stop || run_once
31
+ @@stopped = true
32
+ break
33
+ end
34
+ sleep Config.db_clean_period_seconds
35
+ end # loop
36
+ end # Thread
37
+ @@stopped
38
+ end
39
+
40
+ def stop
41
+ @@stop = true
42
+ end
43
+
44
+ def clean_transactions(n)
45
+ transactions = Transaction.order("created_at ASC").limit(n).each do |t|
46
+ clean_deposits(t)
47
+ t.delete
48
+ @@cleaning_counter[:transactions] += 1
49
+ end
50
+ end
51
+
52
+ def clean_deposits(transaction)
53
+ transaction.deposits.each do |d|
54
+ if d.address && d.address.deposits.count == 1
55
+ d.address.delete
56
+ @@cleaning_counter[:addresses] += 1
57
+ end
58
+ d.delete
59
+ @@cleaning_counter[:deposits] += 1
60
+ end
61
+ end
62
+
63
+ end # class << self
64
+ end # Cleaner
65
+ end # TxCatcher
@@ -0,0 +1,20 @@
1
+ require 'ostruct'
2
+
3
+ module TxCatcher
4
+
5
+ class << (Config = OpenStruct.new)
6
+ def [](key_chain)
7
+ key_chain = key_chain.to_s.split('.')
8
+ config = self.public_send(key_chain.shift)
9
+ key_chain.each do |key|
10
+ if config.kind_of?(Hash)
11
+ config = config[key] || config[key.to_sym]
12
+ else
13
+ return
14
+ end
15
+ end
16
+ config
17
+ end
18
+ end
19
+
20
+ end