sibit 0.12.5 → 0.12.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a60738f08eff40722e87f46d4d8e16c7f59f2d2488bdb95bfee6625057bf38f3
4
- data.tar.gz: a62597ee21efd55bb466316f21960df713d7ab7fc5063eb4ad721524f8a87279
3
+ metadata.gz: 84dbc9a25777fde7ba775b8adc200bc19719c2acf153e0513795e8a8f0ffc161
4
+ data.tar.gz: 6520e17706434cd239f23e38d764ea4c0a82a8d319970ac5a7d46b5d6acaa55a
5
5
  SHA512:
6
- metadata.gz: 2e9a89b805345c0a8c07ccbb8325340e73d15cf518d11d2fb5bcc11796e8ba47652d05ebcb31210b497f830fcbcc7212c2ce301cbdcca2d78acc4de260fc292f
7
- data.tar.gz: 174ea54310174a0f8732510e1acc9e1aae389f87fa32e31e28b2df9ca687753641ceeb91d469156913dfafeb2e49c6be87cf30705e7bb5669934f6e46f99c0f5
6
+ metadata.gz: 7dd7c3cbef60a7ce2242a932d39d6812ddb704928d50f9a7343e1a349cc319d8913cbb7f221257ee2dc7d06a07053d6d7a19247ea35f538fe3b70c29b0a707ed
7
+ data.tar.gz: f927d8c28599512da7bc0d2c44af18bfbb79c4683aa916324e368b321a306f648bb17a43add9aeedd71af14052a45bbfa1cbdfc6cfa7b753d5feb92e333b4efc
data/.gitignore CHANGED
@@ -5,3 +5,4 @@ Gemfile.lock
5
5
  .bundle/
6
6
  .DS_Store
7
7
  rdoc/
8
+ tmp/
data/.travis.yml CHANGED
@@ -6,8 +6,10 @@ branches:
6
6
  only:
7
7
  - master
8
8
  install:
9
+ - gem install pdd -v 0.20.5
9
10
  - travis_retry bundle update
10
11
  script:
11
- - rake
12
+ - pdd -f /dev/null
13
+ - bundle exec rake
12
14
  after_success:
13
15
  - "bash <(curl -s https://codecov.io/bash)"
data/bin/sibit CHANGED
@@ -25,8 +25,11 @@ STDOUT.sync = true
25
25
 
26
26
  require 'slop'
27
27
  require 'backtrace'
28
+ require 'retriable_proxy'
28
29
  require_relative '../lib/sibit'
29
30
  require_relative '../lib/sibit/version'
31
+ require_relative '../lib/sibit/blockchain'
32
+ require_relative '../lib/sibit/btc'
30
33
 
31
34
  begin
32
35
  begin
@@ -53,17 +56,27 @@ Options are:"
53
56
  exit
54
57
  end
55
58
  o.bool '--verbose', 'Print all possible debug messages'
59
+ o.array '--api', 'Ordered List of APIs to use, e.g. "blockchain,btc"', default: ['blockchain']
56
60
  end
57
61
  rescue Slop::Error => ex
58
62
  raise ex.message
59
63
  end
60
64
  raise 'Try --help' if opts.arguments.empty?
61
- sibit = Sibit.new(
62
- log: opts[:verbose] ? STDOUT : nil,
63
- http: opts[:proxy] ? Sibit.proxy_http(opts[:proxy]) : Sibit.default_http,
64
- dry: opts[:dry],
65
- attempts: opts[:attempts]
66
- )
65
+ log = Sibit::Log.new(opts[:verbose] ? STDOUT : nil)
66
+ http = opts[:proxy] ? Sibit::HttpProxy.new(opts[:proxy]) : Sibit::Http.new
67
+ apis = opts[:api].map(&:downcase).map do |a|
68
+ api = nil
69
+ if a == 'blockchain'
70
+ api = Sibit::Blockchain.new(http: http, log: log, dry: opts[:dry])
71
+ elsif a == 'btc'
72
+ api = Sibit::Btc.new(http: http, log: log, dry: opts[:dry])
73
+ else
74
+ raise Sibit::Error, "Unknown API \"#{a}\""
75
+ end
76
+ api = RetriableProxy.for_object(api, on: Sibit::Error) if opts[:attempts] > 1
77
+ api
78
+ end
79
+ sibit = Sibit.new(log: log, api: apis)
67
80
  case opts.arguments[0]
68
81
  when 'price'
69
82
  puts sibit.price
data/features/cli.feature CHANGED
@@ -7,7 +7,7 @@ Feature: Command Line Processing
7
7
  And Stdout contains "--help"
8
8
 
9
9
  Scenario: Bitcoin price can be retrieved
10
- When I run bin/sibit with "price"
10
+ When I run bin/sibit with "price --attempts=4"
11
11
  Then Exit code is zero
12
12
 
13
13
  Scenario: Bitcoin latest block hash can be retrieved
@@ -23,7 +23,7 @@ Feature: Command Line Processing
23
23
  Then Exit code is zero
24
24
 
25
25
  Scenario: Bitcoin balance can be checked
26
- When I run bin/sibit with "balance 1MZT1fa6y8H9UmbZV6HqKF4UY41o9MGT5f --verbose"
26
+ When I run bin/sibit with "balance 1MZT1fa6y8H9UmbZV6HqKF4UY41o9MGT5f --verbose --api=blockchain,btc"
27
27
  Then Exit code is zero
28
28
 
29
29
  Scenario: Bitcoin fees can be printed
@@ -21,4 +21,5 @@
21
21
  # SOFTWARE.
22
22
 
23
23
  require 'simplecov'
24
+ require 'aruba/cucumber'
24
25
  require_relative '../../lib/sibit'
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'bitcoin'
24
+ require 'json'
25
+ require 'uri'
26
+ require_relative 'version'
27
+ require_relative 'error'
28
+ require_relative 'http'
29
+ require_relative 'json'
30
+
31
+ # Blockchain.info API.
32
+ #
33
+ # It works through the Blockchain API:
34
+ # https://www.blockchain.com/api/blockchain_api
35
+ #
36
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
37
+ # Copyright:: Copyright (c) 2019 Yegor Bugayenko
38
+ # License:: MIT
39
+ class Sibit
40
+ # Blockchain.info API.
41
+ class Blockchain
42
+ # Constructor.
43
+ def initialize(log: Sibit::Log.new, http: Sibit::Http.new, dry: false)
44
+ @http = http
45
+ @log = log
46
+ @dry = dry
47
+ end
48
+
49
+ # Current price of BTC in USD (float returned).
50
+ def price(currency)
51
+ h = Sibit::Json.new(http: @http, log: @log).get(
52
+ URI('https://blockchain.info/ticker')
53
+ )[currency]
54
+ raise Error, "Unrecognized currency #{currency}" if h.nil?
55
+ h['15m']
56
+ end
57
+
58
+ # Gets the balance of the address, in satoshi.
59
+ def balance(address)
60
+ json = Sibit::Json.new(http: @http, log: @log).get(
61
+ URI("https://blockchain.info/rawaddr/#{address}")
62
+ )
63
+ @log.info("Total transactions: #{json['n_tx']}")
64
+ @log.info("Received/sent: #{json['total_received']}/#{json['total_sent']}")
65
+ json['final_balance']
66
+ end
67
+
68
+ # Get recommended fees, in satoshi per byte. The method returns
69
+ # a hash: { S: 12, M: 45, L: 100, XL: 200 }
70
+ def fees
71
+ json = Sibit::Json.new(http: @http, log: @log).get(
72
+ URI('https://bitcoinfees.earn.com/api/v1/fees/recommended')
73
+ )
74
+ @log.info("Current recommended Bitcoin fees: \
75
+ #{json['hourFee']}/#{json['halfHourFee']}/#{json['fastestFee']} sat/byte")
76
+ {
77
+ S: json['hourFee'] / 3,
78
+ M: json['hourFee'],
79
+ L: json['halfHourFee'],
80
+ XL: json['fastestFee']
81
+ }
82
+ end
83
+
84
+ # Sends a payment and returns the transaction hash.
85
+ def pay(amount, fee, sources, target, change)
86
+ p = price('USD')
87
+ satoshi = satoshi(amount)
88
+ f = mfee(fee, size_of(amount, sources))
89
+ satoshi += f if f.negative?
90
+ raise Error, "The fee #{f.abs} covers the entire amount" if satoshi.zero?
91
+ raise Error, "The fee #{f.abs} is bigger than the amount #{satoshi}" if satoshi.negative?
92
+ builder = Bitcoin::Builder::TxBuilder.new
93
+ unspent = 0
94
+ size = 100
95
+ utxos = Sibit::Json.new(http: @http, log: @log).get(
96
+ URI("https://blockchain.info/unspent?active=#{sources.keys.join('|')}&limit=1000")
97
+ )['unspent_outputs']
98
+ @log.info("#{utxos.count} UTXOs found, these will be used \
99
+ (value/confirmations at tx_hash):")
100
+ utxos.each do |utxo|
101
+ unspent += utxo['value']
102
+ builder.input do |i|
103
+ i.prev_out(utxo['tx_hash_big_endian'])
104
+ i.prev_out_index(utxo['tx_output_n'])
105
+ i.prev_out_script = [utxo['script']].pack('H*')
106
+ address = Bitcoin::Script.new([utxo['script']].pack('H*')).get_address
107
+ i.signature_key(key(sources[address]))
108
+ end
109
+ size += 180
110
+ @log.info(
111
+ " #{num(utxo['value'], p)}/#{utxo['confirmations']} at #{utxo['tx_hash_big_endian']}"
112
+ )
113
+ break if unspent > satoshi
114
+ end
115
+ if unspent < satoshi
116
+ raise Error, "Not enough funds to send #{num(satoshi, p)}, only #{num(unspent, p)} left"
117
+ end
118
+ builder.output(satoshi, target)
119
+ f = mfee(fee, size)
120
+ tx = builder.tx(
121
+ input_value: unspent,
122
+ leave_fee: true,
123
+ extra_fee: [f, Bitcoin.network[:min_tx_fee]].max,
124
+ change_address: change
125
+ )
126
+ left = unspent - tx.outputs.map(&:value).inject(&:+)
127
+ @log.info("A new Bitcoin transaction #{tx.hash} prepared:
128
+ #{tx.in.count} input#{tx.in.count > 1 ? 's' : ''}:
129
+ #{tx.inputs.map { |i| " in: #{i.prev_out.bth}:#{i.prev_out_index}" }.join("\n ")}
130
+ #{tx.out.count} output#{tx.out.count > 1 ? 's' : ''}:
131
+ #{tx.outputs.map { |o| "out: #{o.script.bth} / #{num(o.value, p)}" }.join("\n ")}
132
+ Min tx fee: #{num(Bitcoin.network[:min_tx_fee], p)}
133
+ Fee requested: #{num(f, p)} as \"#{fee}\"
134
+ Fee left: #{num(left, p)}
135
+ Tx size: #{size} bytes
136
+ Unspent: #{num(unspent, p)}
137
+ Amount: #{num(satoshi, p)}
138
+ Target address: #{target}
139
+ Change address is #{change}")
140
+ unless @dry
141
+ Sibit::Json.new(http: @http, log: @log).post(
142
+ URI('https://blockchain.info/pushtx'),
143
+ tx.to_payload.bth
144
+ )
145
+ end
146
+ tx.hash
147
+ end
148
+
149
+ # Gets the hash of the latest block.
150
+ def latest
151
+ Sibit::Json.new(http: @http, log: @log).get(
152
+ URI('https://blockchain.info/latestblock')
153
+ )['hash']
154
+ end
155
+
156
+ private
157
+
158
+ def num(satoshi, usd)
159
+ format(
160
+ '%<satoshi>ss/$%<dollars>0.2f',
161
+ satoshi: satoshi.to_s.gsub(/\d(?=(...)+$)/, '\0,'),
162
+ dollars: satoshi * usd / 100_000_000
163
+ )
164
+ end
165
+
166
+ # Convert text to amount.
167
+ def satoshi(amount)
168
+ return amount if amount.is_a?(Integer)
169
+ raise Error, 'Amount should either be a String or Integer' unless amount.is_a?(String)
170
+ return (amount.gsub(/BTC$/, '').to_f * 100_000_000).to_i if amount.end_with?('BTC')
171
+ raise Error, "Can't understand the amount #{amount.inspect}"
172
+ end
173
+
174
+ # Calculates a fee in satoshi for the transaction of the specified size.
175
+ # The +fee+ argument could be a number in satoshi, in which case it will
176
+ # be returned as is, or a string like "XL" or "S", in which case the
177
+ # fee will be calculated using the +size+ argument (which is the size
178
+ # of the transaction in bytes).
179
+ def mfee(fee, size)
180
+ return fee.to_i if fee.is_a?(Integer)
181
+ raise Error, 'Fee should either be a String or Integer' unless fee.is_a?(String)
182
+ mul = 1
183
+ if fee.start_with?('+', '-')
184
+ mul = -1 if fee.start_with?('-')
185
+ fee = fee[1..-1]
186
+ end
187
+ sat = fees[fee.to_sym]
188
+ raise Error, "Can't understand the fee: #{fee.inspect}" if sat.nil?
189
+ mul * sat * size
190
+ end
191
+
192
+ # Make key from private key string in Hash160.
193
+ def key(hash160)
194
+ key = Bitcoin::Key.new
195
+ key.priv = hash160
196
+ key
197
+ end
198
+
199
+ # Calculate an approximate size of the transaction.
200
+ def size_of(amount, sources)
201
+ satoshi = satoshi(amount)
202
+ builder = Bitcoin::Builder::TxBuilder.new
203
+ unspent = 0
204
+ size = 100
205
+ utxos = Sibit::Json.new(http: @http, log: @log).get(
206
+ URI("https://blockchain.info/unspent?active=#{sources.keys.join('|')}&limit=1000")
207
+ )['unspent_outputs']
208
+ utxos.each do |utxo|
209
+ unspent += utxo['value']
210
+ builder.input do |i|
211
+ i.prev_out(utxo['tx_hash_big_endian'])
212
+ i.prev_out_index(utxo['tx_output_n'])
213
+ i.prev_out_script = [utxo['script']].pack('H*')
214
+ address = Bitcoin::Script.new([utxo['script']].pack('H*')).get_address
215
+ i.signature_key(key(sources[address]))
216
+ end
217
+ size += 180
218
+ break if unspent > satoshi
219
+ end
220
+ size
221
+ end
222
+ end
223
+ end
data/lib/sibit/btc.rb ADDED
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'uri'
24
+ require 'json'
25
+ require_relative 'version'
26
+ require_relative 'error'
27
+ require_relative 'log'
28
+ require_relative 'http'
29
+ require_relative 'json'
30
+
31
+ # Btc.com API.
32
+ #
33
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
34
+ # Copyright:: Copyright (c) 2019 Yegor Bugayenko
35
+ # License:: MIT
36
+ class Sibit
37
+ # Btc.com API.
38
+ class Btc
39
+ # Constructor.
40
+ def initialize(log: Sibit::Log.new, http: Sibit::Http.new, dry: false)
41
+ @http = http
42
+ @log = log
43
+ @dry = dry
44
+ end
45
+
46
+ # Current price of BTC in USD (float returned).
47
+ def price(_currency)
48
+ raise Sibit::Error, 'Not implemented yet'
49
+ end
50
+
51
+ # Gets the balance of the address, in satoshi.
52
+ def balance(address)
53
+ uri = URI("https://chain.api.btc.com/v3/address/#{address}/unspent")
54
+ json = Sibit::Json.new(http: @http, log: @log).get(uri)
55
+ txns = json['data']['list']
56
+ balance = txns.map { |tx| tx['value'] }.inject(&:+) || 0
57
+ @log.info("The balance of #{address} is #{balance}, total txns: #{txns.count}")
58
+ balance
59
+ end
60
+
61
+ # Get recommended fees, in satoshi per byte.
62
+ def fees
63
+ raise Sibit::Error, 'Not implemented yet'
64
+ end
65
+
66
+ # Sends a payment and returns the transaction hash.
67
+ def pay(_amount, _fee, _sources, _target, _change)
68
+ raise Sibit::Error, 'Not implemented yet'
69
+ end
70
+
71
+ # Gets the hash of the latest block.
72
+ def latest
73
+ raise Sibit::Error, 'Not implemented yet'
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ # The error.
24
+ #
25
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
26
+ # Copyright:: Copyright (c) 2019 Yegor Bugayenko
27
+ # License:: MIT
28
+ class Sibit
29
+ # The error.
30
+ class Error < StandardError; end
31
+ end
data/lib/sibit/fake.rb ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require_relative 'version'
24
+
25
+ # Fake API.
26
+ #
27
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
28
+ # Copyright:: Copyright (c) 2019 Yegor Bugayenko
29
+ # License:: MIT
30
+ class Sibit
31
+ # Fake API
32
+ class Fake
33
+ def price(_cur = 'USD')
34
+ 4_000
35
+ end
36
+
37
+ def fees
38
+ { S: 12, M: 45, L: 100, XL: 200 }
39
+ end
40
+
41
+ def balance(_address)
42
+ 100_000_000
43
+ end
44
+
45
+ def pay(_amount, _fee, _sources, _target, _change)
46
+ '9dfe55a30b5ee732005158c589179a398117117a68d21531fb6c78b85b544c54'
47
+ end
48
+
49
+ def latest
50
+ '00000000000000000008df8a6e1b61d1136803ac9791b8725235c9f780b4ed71'
51
+ end
52
+
53
+ def get_json(_uri)
54
+ {}
55
+ end
56
+ end
57
+ end
data/lib/sibit/http.rb ADDED
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'net/http'
24
+
25
+ # HTTP interface.
26
+ #
27
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
28
+ # Copyright:: Copyright (c) 2019 Yegor Bugayenko
29
+ # License:: MIT
30
+ class Sibit
31
+ # This HTTP client will be used by default.
32
+ class Http
33
+ def client(uri)
34
+ http = Net::HTTP.new(uri.host, uri.port)
35
+ http.use_ssl = true
36
+ http
37
+ end
38
+ end
39
+
40
+ # This HTTP client with proxy.
41
+ class HttpProxy
42
+ def initialize(addr)
43
+ @host, @port = addr.split(':')
44
+ end
45
+
46
+ def client(uri)
47
+ http = Net::HTTP.new(uri.host, uri.port, @host, @port.to_i)
48
+ http.use_ssl = true
49
+ http
50
+ end
51
+ end
52
+ end
data/lib/sibit/json.rb ADDED
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ require 'json'
24
+ require 'uri'
25
+ require_relative 'version'
26
+ require_relative 'error'
27
+ require_relative 'http'
28
+
29
+ # Json SDK.
30
+ #
31
+ # It works through the Blockchain API:
32
+ # https://www.blockchain.com/api/blockchain_api
33
+ #
34
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
35
+ # Copyright:: Copyright (c) 2019 Yegor Bugayenko
36
+ # License:: MIT
37
+ class Sibit
38
+ # JSON processing.
39
+ class Json
40
+ # Constructor.
41
+ def initialize(log: Sibit::Log.new, http: Sibit::Http.new)
42
+ @http = http
43
+ @log = log
44
+ end
45
+
46
+ # Send GET request to the HTTP and return JSON response.
47
+ # This method will also log the process and will validate the
48
+ # response for correctness.
49
+ def get(uri)
50
+ start = Time.now
51
+ res = @http.client(uri).get(
52
+ "#{uri.path}?#{uri.query}",
53
+ 'Accept' => 'text/plain',
54
+ 'User-Agent' => user_agent,
55
+ 'Accept-Encoding' => ''
56
+ )
57
+ unless res.code == '200'
58
+ raise Sibit::Error, "Failed to retrieve #{uri} (#{res.code}): #{res.body}"
59
+ end
60
+ @log.info("GET #{uri}: #{res.code}/#{res.body.length}b in #{age(start)}")
61
+ JSON.parse(res.body)
62
+ end
63
+
64
+ def post(uri, body)
65
+ start = Time.now
66
+ res = @http.client(uri).post(
67
+ "#{uri.path}?#{uri.query}",
68
+ "tx=#{CGI.escape(body)}",
69
+ 'Accept' => 'text/plain',
70
+ 'User-Agent' => user_agent,
71
+ 'Accept-Encoding' => '',
72
+ 'Content-Type' => 'application/x-www-form-urlencoded'
73
+ )
74
+ unless res.code == '200'
75
+ raise Sibit::Error, "Failed to post tx to #{uri}: #{res.code}\n#{res.body}"
76
+ end
77
+ @log.info("POST #{uri}: #{res.code} in #{age(start)}")
78
+ end
79
+
80
+ private
81
+
82
+ def age(start)
83
+ "#{((Time.now - start) * 1000).round}ms"
84
+ end
85
+
86
+ def user_agent
87
+ "Anonymous/#{Sibit::VERSION}"
88
+ end
89
+ end
90
+ end
data/lib/sibit/log.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019 Yegor Bugayenko
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the 'Software'), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ # The log.
24
+ #
25
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
26
+ # Copyright:: Copyright (c) 2019 Yegor Bugayenko
27
+ # License:: MIT
28
+ class Sibit
29
+ # Log.
30
+ class Log
31
+ # Constructor.
32
+ #
33
+ # You may provide the log you want to see the messages in. If you don't
34
+ # provide anything, the console will be used. The object you provide
35
+ # has to respond to the method +info+ or +puts+ in order to receive logging
36
+ # messages.
37
+ def initialize(log = STDOUT)
38
+ @log = log
39
+ end
40
+
41
+ def info(msg)
42
+ if @log.respond_to?(:info)
43
+ @log.info(msg)
44
+ elsif @log.respond_to?(:puts)
45
+ @log.puts(msg)
46
+ end
47
+ end
48
+ end
49
+ end