pochette_toshi 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: 5f50e277f409de70f366b93f9677902028df8b17
4
+ data.tar.gz: 1f536b620dc7930970051e56f14b5339b22a9b64
5
+ SHA512:
6
+ metadata.gz: b1b9ddb4759fa69eb26a61bde1d1c549f0b302aae9e230204bf7c99384538b3983cb417d62dd1541b8c15bea1a123629ae25d6b89a4e6cb0d83a773044c027f0
7
+ data.tar.gz: 92dc48c252c261316593f2536627b5434c5a2b5a809c8f0556a1fecf994d1b51637a9442506962c4a4f79582ac371989b97dcb2cb6abbe2ab52955191375e988
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pochette_toshi.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # PochetteToshi
2
+
3
+ A [Pochette](https://github.com/bitex-la/pochette) backend using Toshi.
4
+
5
+ It will connect to your Toshi postgres database directly (not using Toshi's JSON RPC)
6
+
7
+ For better performance it is recommended you create the following indexes in your
8
+ database:
9
+
10
+ CREATE INDEX inputs_hsh_index on inputs (hsh);
11
+ CREATE INDEX inputs_is_coinbase_index on inputs (prev_out, hsh) WHERE prev_out = '0000000000000000000000000000000000000000000000000000000000000000';
12
+ CREATE INDEX unspent_outputs_usable_index on unspent_outputs (amount) WHERE amount > 5000;
13
+ CREATE INDEX transactions_hsh_height ON transactions (hsh, height);
14
+
15
+ You can instatiate a Toshi backend passing your postgres connection options, they will be passed
16
+ to the pg gem [as seen in their docs](http://deveiate.org/code/pg/PG/Connection.html#method-c-new)
17
+
18
+ >>> Pochette::Backends::Toshi.new(host: 'your-db-host', dbname: 'toshi')
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'pochette_toshi'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ $ bundle
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install pochette_toshi
35
+
36
+ ## Development
37
+
38
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
39
+
40
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
41
+
42
+ ## Contributing
43
+
44
+ 1. Fork it ( https://github.com/[my-github-username]/pochette_toshi/fork )
45
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
46
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
47
+ 4. Push to the branch (`git push origin my-new-feature`)
48
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "pochette_toshi"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,226 @@
1
+ require "pochette_toshi/version"
2
+ require "active_support"
3
+ require "active_support/core_ext"
4
+ require "pg"
5
+
6
+ module Pochette
7
+ module Backends
8
+ end
9
+ end
10
+
11
+ # This class is not properly tested.
12
+ # Be careful when changing anything, and/or make sure
13
+ # you can run and assert your changes with a local copy of
14
+ # a toshi testnet database.
15
+ class Pochette::Backends::Toshi
16
+ attr_accessor :connection
17
+
18
+ def initialize(options)
19
+ self.connection = PG.connect(options)
20
+ end
21
+
22
+ def incoming_for(addresses, min_date)
23
+ addresses.in_groups_of(500, false).collect do |group|
24
+ incoming_for_helper(group, min_date)
25
+ end.flatten(1)
26
+ end
27
+
28
+ def incoming_for_helper(addresses, min_date)
29
+ addresses_sql = sanitize_list(addresses)
30
+ current_height = block_height
31
+ from_block = current_height - ((Time.now - min_date) / 60 / 60 * 6).ceil
32
+
33
+ query(%{
34
+ SELECT ale.amount, a.address, t.hsh,
35
+ (#{current_height + 1} - t.height) as confirmations, o.position,
36
+ (SELECT string_agg(a2.address,',') as sender
37
+ FROM address_ledger_entries ale2
38
+ INNER JOIN addresses a2 ON a2.id = ale2.address_id
39
+ WHERE ale2.transaction_id = t.id AND ale2.input_id IS NOT NULL
40
+ ) as senders
41
+ FROM address_ledger_entries ale
42
+ INNER JOIN addresses a ON a.id = ale.address_id AND a.address in (#{addresses_sql})
43
+ INNER JOIN transactions t ON t.id = ale.transaction_id AND t.pool = 1 AND t.height > #{from_block}
44
+ INNER JOIN outputs o ON o.id = ale.output_id AND o.branch = 0
45
+ UNION
46
+ SELECT ale.amount, a.address, t.hsh, 0 as confirmations, o.position,
47
+ (
48
+ SELECT string_agg(a2.address,',') as sender
49
+ FROM unconfirmed_ledger_entries ale2
50
+ INNER JOIN unconfirmed_addresses a2 ON a2.id = ale2.address_id
51
+ WHERE ale2.transaction_id = t.id AND ale2.input_id IS NOT NULL
52
+ ) as senders
53
+ FROM unconfirmed_ledger_entries ale
54
+ INNER JOIN unconfirmed_addresses a ON a.id = ale.address_id AND a.address in (#{addresses_sql})
55
+ INNER JOIN unconfirmed_transactions t ON t.id = ale.transaction_id AND t.pool = 1
56
+ INNER JOIN unconfirmed_outputs o ON o.id = ale.output_id
57
+ }).collect{|a,addr,hsh,confs,pos,sender| [a.to_i, addr, hsh, confs.to_i, pos.to_i, sender] }
58
+ end
59
+
60
+ def balances_for(addresses, confirmations)
61
+ addresses.in_groups_of(500, false).reduce({}) do |accum, group|
62
+ accum.merge!(balances_for_helper(group, confirmations))
63
+ end
64
+ end
65
+
66
+ def balances_for_helper(addresses, confirmations)
67
+ # Addresses have denormalized sent and received columns for all
68
+ # transactions. We must then calculate which portion
69
+ # of that is unconfirmed in order to get the 'confirmed' balances.
70
+ # And to get the 'unconfirmed' balances we also need to fetch
71
+ # ledger entries for transactions that were not added to a block yet.
72
+ addresses_sql = sanitize_list(addresses)
73
+
74
+ confirmed_id_to_address = {}
75
+ unconfirmed_id_to_address = {}
76
+ result = addresses.reduce({}) do |accum, address|
77
+ accum[address] = [0,0,0,0,0,0]
78
+ accum
79
+ end
80
+
81
+ query(%{
82
+ SELECT id, address, total_received, total_sent
83
+ FROM addresses WHERE address in (#{addresses_sql})
84
+ }).each do |id, address, received, sent|
85
+ confirmed_id_to_address[id] = address
86
+ received = received.to_d / 1_0000_0000
87
+ sent = sent.to_d / 1_0000_0000
88
+ balance = received - sent
89
+ result[address] = [received, sent, balance, received, sent, balance]
90
+ end
91
+
92
+ # Now we take the previous 1 confirmation balances and substract
93
+ # what was below threshold to get the 'confirmed' balances.
94
+ query(%{
95
+ SELECT ale.address_id,
96
+ sum(CASE WHEN ale.amount > 0 THEN ale.amount ELSE 0 END) as received,
97
+ sum(CASE WHEN ale.amount < 0 THEN ale.amount ELSE 0 END) as sent
98
+ FROM address_ledger_entries ale
99
+ INNER JOIN addresses a ON a.id = ale.address_id AND a.address in (#{addresses_sql})
100
+ INNER JOIN transactions t ON t.id = ale.transaction_id AND t.pool = 1
101
+ AND t.height > #{block_height - confirmations + 1}
102
+ GROUP BY ale.address_id
103
+ }).each do |id, received, sent|
104
+ row = result[confirmed_id_to_address[id]]
105
+ row[0] = row[0] - (received.to_d / 1_0000_0000)
106
+ row[1] = row[1] - (sent.to_d.abs / 1_0000_0000)
107
+ row[2] = row[0] - row[1]
108
+ end
109
+
110
+ query(%{SELECT id, address
111
+ FROM unconfirmed_addresses WHERE address in (#{addresses_sql})
112
+ }).each do |id, address|
113
+ unconfirmed_id_to_address[id] = address
114
+ end
115
+
116
+ # And then we also add the unconfirmed stuff to the already
117
+ # cached 1 confirmation balances
118
+ query(%{
119
+ SELECT ale.address_id,
120
+ sum(CASE WHEN ale.amount > 0 THEN ale.amount ELSE 0 END) as received,
121
+ sum(CASE WHEN ale.amount < 0 THEN ale.amount ELSE 0 END) as sent
122
+ FROM unconfirmed_ledger_entries ale
123
+ INNER JOIN unconfirmed_addresses a ON a.id = ale.address_id
124
+ AND a.address in (#{addresses_sql})
125
+ INNER JOIN unconfirmed_transactions t ON t.id = ale.transaction_id AND t.pool = 1
126
+ GROUP BY ale.address_id
127
+ }).each do |id, received, sent|
128
+ row = result[unconfirmed_id_to_address[id]]
129
+ row[3] += (received.to_d / 1_0000_0000)
130
+ row[4] += (sent.to_d.abs / 1_0000_0000)
131
+ row[5] = row[3] - row[4]
132
+ end
133
+
134
+ return result
135
+ end
136
+
137
+ # Performs the low level queries to the toshi database
138
+ # and returns a JSON structure for the unspents and the transactions.
139
+ # THIS METHOD IS NOT TESTED IN CI!!!! DO NOT JUST REFACTOR THIS WITHOUT
140
+ # TESTING IT LOCALLY AGAINST THE ~20 GB TOSHI TESTNET DATABASE.
141
+ def list_unspent(addresses)
142
+ unspents = []
143
+ addresses.in_groups_of(500, false).collect do |group|
144
+ unspents += list_unspent_helper(group)
145
+ end
146
+ unspents
147
+ end
148
+
149
+ def list_unspent_helper(addresses)
150
+ addresses_sql = sanitize_list(addresses)
151
+ query(%{
152
+ SELECT a.address, o.hsh, o.position, uo.amount
153
+ FROM addresses a
154
+ INNER JOIN unspent_outputs uo ON uo.address_id = a.id AND uo.amount > 5000
155
+ LEFT JOIN outputs o ON o.id = uo.output_id
156
+ WHERE a.address in (#{addresses_sql})
157
+ AND NOT EXISTS (
158
+ SELECT i.id FROM inputs i
159
+ LEFT JOIN transactions t ON t.hsh = i.hsh
160
+ WHERE i.hsh = o.hsh AND
161
+ i.prev_out = '0000000000000000000000000000000000000000000000000000000000000000'
162
+ AND t.height > #{block_height - 100}
163
+ )
164
+ }).collect{|a,b,c,d| [a,b,c.to_i,d.to_i]}
165
+ end
166
+
167
+ def list_transactions(txids)
168
+ transactions = []
169
+ addresses.in_groups_of(500, false).collect do |group|
170
+ transactions += list_transactions_helper(group)
171
+ end
172
+ transactions
173
+ end
174
+
175
+ def list_transactions_helper(hashes)
176
+ transactions_sql = sanitize_list(hashes)
177
+ transactions = query(%{SELECT * FROM transactions WHERE hsh IN (#{transactions_sql})})
178
+ inputs = query(%{SELECT * FROM inputs WHERE hsh IN (#{transactions_sql})
179
+ ORDER BY position})
180
+ inputs_by_hsh = inputs.reduce({}) do |d, i|
181
+ d[i[1]] ||= []
182
+ d[i[1]] << i
183
+ d
184
+ end
185
+ outputs = query(%{SELECT * FROM outputs WHERE hsh IN (#{transactions_sql})
186
+ ORDER BY position})
187
+ outputs_by_hsh = outputs.reduce({}) do |d, o|
188
+ d[o[1]] ||= []
189
+ d[o[1]] << o
190
+ d
191
+ end
192
+
193
+ transactions_json = transactions.collect do |_, hsh, ver, lock_time|
194
+ inputs_json = inputs_by_hsh[hsh].collect do |_, _, prev, index, script, seq, pos|
195
+ { prev_hash: prev,
196
+ prev_index: index.to_i,
197
+ sequence: [seq[2..-1]].pack('H*').unpack('V')[0],
198
+ script_sig: script[2..-1]
199
+ }
200
+ end
201
+ outputs_json = outputs_by_hsh[hsh].collect do |_, _, amount, script|
202
+ { amount: amount.to_i, script_pubkey: script[2..-1] }
203
+ end
204
+ { hash: hsh,
205
+ version: ver.to_i,
206
+ lock_time: lock_time.to_i,
207
+ inputs: inputs_json,
208
+ bin_outputs: outputs_json
209
+ }
210
+ end
211
+ transactions_json
212
+ end
213
+
214
+ def block_height
215
+ connection.exec('select max(height) from blocks where branch = 0').values[0][0].to_i
216
+ end
217
+
218
+ def query(sql)
219
+ connection.exec(sql).values
220
+ end
221
+
222
+ def sanitize_list(list)
223
+ list.collect{|a| "'#{a}'"}.join(',')
224
+ end
225
+ end
226
+
@@ -0,0 +1,3 @@
1
+ module PochetteToshi
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pochette_toshi/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pochette_toshi"
8
+ spec.version = PochetteToshi::VERSION
9
+ spec.authors = ["Nubis"]
10
+ spec.email = ["yo@nubis.im"]
11
+ spec.license = "MIT"
12
+
13
+ spec.summary = "Toshi backend for pochette"
14
+ spec.description = "Toshi backend for the pochette wallet library"
15
+ spec.homepage = "https://github.com/bitex-la/pochette-toshi"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.require_paths = ["lib"]
19
+ spec.add_dependency "pochette", "~> 0.1"
20
+ spec.add_dependency "pg", "~> 0.17"
21
+ spec.add_dependency "activesupport", "~> 4.2"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.9"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "rspec", "~> 2"
26
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pochette_toshi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nubis
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pochette
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.17'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.17'
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.9'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.9'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2'
97
+ description: Toshi backend for the pochette wallet library
98
+ email:
99
+ - yo@nubis.im
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".travis.yml"
107
+ - Gemfile
108
+ - README.md
109
+ - Rakefile
110
+ - bin/console
111
+ - bin/setup
112
+ - lib/pochette_toshi.rb
113
+ - lib/pochette_toshi/version.rb
114
+ - pochette_toshi.gemspec
115
+ homepage: https://github.com/bitex-la/pochette-toshi
116
+ licenses:
117
+ - MIT
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubyforge_project:
135
+ rubygems_version: 2.4.5
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: Toshi backend for pochette
139
+ test_files: []