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 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