sibit 0.18.4 → 0.19.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9190dd104e8af92e3a5d8f1f6a967ef5d001fcf47e51adbaf260a281ecf043a5
4
- data.tar.gz: f6b2280a10d118ad474b02022ad428de4c5775632c8f98a7f552c100ddb25460
3
+ metadata.gz: c16c2d9c8fd8acd2635dc38d1b97599dc2138851ffc162db62aa3dc35feec993
4
+ data.tar.gz: b17f0c383453182402041ac8793eb2e308480b660e4bd79577a846a6517cf66e
5
5
  SHA512:
6
- metadata.gz: 863c1ce07d8c74d1029d29a27711244e12350d23f2cdcdc0da21a41793fdd98ce5685a8a725814c691d633e65e6d6858aa1cc716382a4465f93973df773a802c
7
- data.tar.gz: 4b7556e9a790db63e63c2cdb0b836b8e3a7f1a9153cb1eba18f722c701b460c379ccda784feb40773f627e78f8129ef78ca7d2e9648431ecaa3608ef6409c89f
6
+ metadata.gz: 3a7234573110a1fd17ed0e67257474f5c07801e0fd74a0212ca1e08453e9088aeae00b803382814119b9e16fe1b22a9f2fab1974724fdc5c1f5f0d11649e5e29
7
+ data.tar.gz: '089e4b96cf9c5fb0b3e17bd1692e9ee66f553c2752fd8669a520008a533dacc60063c473264f0469d394a5c272209419026f4920dfa3f3f97bbfc422fe359751'
@@ -4,6 +4,8 @@ AllCops:
4
4
  DisplayCopNames: true
5
5
  TargetRubyVersion: 2.3
6
6
 
7
+ Style/ClassAndModuleChildren:
8
+ Enabled: false
7
9
  Metrics/LineLength:
8
10
  Max: 100
9
11
  Layout/EndOfLine:
@@ -1,15 +1,11 @@
1
1
  assets:
2
2
  rubygems.yml: yegor256/home#assets/rubygems.yml
3
3
  install: |
4
- export GEM_HOME=~/.ruby
5
- export GEM_PATH=$GEM_HOME:$GEM_PATH
6
- sudo apt-get -y update
7
- sudo gem install pdd -v 0.20.5
8
- bundle install
4
+ pdd -f /dev/null
5
+ sudo bundle install --no-color "--gemfile=$(pwd)/Gemfile"
9
6
  release:
10
7
  script: |-
11
8
  bundle exec rake
12
- rm -rf *.gem
13
9
  sed -i "s/1\.0\.snapshot/${tag}/g" lib/sibit/version.rb
14
10
  git add lib/sibit/version.rb
15
11
  git commit -m "version set to ${tag}"
@@ -19,7 +15,6 @@ release:
19
15
  merge:
20
16
  script: |-
21
17
  bundle exec rake
22
- pdd -f /dev/null
23
18
  deploy:
24
19
  script: |-
25
20
  echo "There is nothing to deploy"
data/README.md CHANGED
@@ -107,7 +107,7 @@ pkey = sibit.generate
107
107
  address = sibit.create(pkey)
108
108
  balance = sibit.balance(address)
109
109
  target = sibit.create(pkey) # where to send coins to
110
- change = sibit.create(pkey) # where the change will sent to
110
+ change = sibit.create(pkey) # where the change will be sent to
111
111
  tx = sibit.pay(10_000_000, 'XL', { address => pkey }, target, change)
112
112
  ```
113
113
 
@@ -145,7 +145,14 @@ will be used one by one, until a successful response is obtained:
145
145
  require 'sibit'
146
146
  require 'sibit/btc'
147
147
  require 'sibit/cryptoapis'
148
- sibit = Sibit.new(api: [Sibit::Btc.new, Sibit::Cryptoapis.new('key')])
148
+ sibit = Sibit.new(
149
+ api: Sibit::FirstOf.new(
150
+ [
151
+ Sibit::Btc.new,
152
+ Sibit::Cryptoapis.new('key')
153
+ ]
154
+ )
155
+ )
149
156
  ```
150
157
 
151
158
  If you think we may need to use some other API, you can submit a ticket,
data/bin/sibit CHANGED
@@ -32,6 +32,7 @@ require_relative '../lib/sibit/blockchain'
32
32
  require_relative '../lib/sibit/btc'
33
33
  require_relative '../lib/sibit/bitcoinchain'
34
34
  require_relative '../lib/sibit/earn'
35
+ require_relative '../lib/sibit/firstof'
35
36
 
36
37
  begin
37
38
  begin
@@ -86,7 +87,7 @@ Options are:"
86
87
  api = RetriableProxy.for_object(api, on: Sibit::Error) if opts[:attempts] > 1
87
88
  api
88
89
  end
89
- sibit = Sibit.new(log: log, api: apis)
90
+ sibit = Sibit.new(log: log, api: Sibit::FirstOf.new(apis, log: log))
90
91
  case opts.arguments[0]
91
92
  when 'price'
92
93
  puts sibit.price
@@ -55,9 +55,7 @@ class Sibit
55
55
 
56
56
  # Current price of 1 BTC in USD (or another currency), float returned.
57
57
  def price(currency = 'USD')
58
- first_one do |api|
59
- api.price(currency)
60
- end
58
+ @api.price(currency)
61
59
  end
62
60
 
63
61
  # Generates new Bitcon private key and returns in Hash160 format.
@@ -76,29 +74,23 @@ class Sibit
76
74
 
77
75
  # Gets the balance of the address, in satoshi.
78
76
  def balance(address)
79
- first_one do |api|
80
- api.balance(address)
81
- end
77
+ @api.balance(address)
82
78
  end
83
79
 
84
80
  # Get the height of the block.
85
81
  def height(hash)
86
- first_one do |api|
87
- api.height(hash)
88
- end
82
+ @api.height(hash)
89
83
  end
90
84
 
91
85
  # Get the hash of the next block.
92
86
  def next_of(hash)
93
- first_one do |api|
94
- api.next_of(hash)
95
- end
87
+ @api.next_of(hash)
96
88
  end
97
89
 
98
90
  # Get recommended fees, in satoshi per byte. The method returns
99
91
  # a hash: { S: 12, M: 45, L: 100, XL: 200 }
100
92
  def fees
101
- first_one(&:fees)
93
+ @api.fees
102
94
  end
103
95
 
104
96
  # Sends a payment and returns the transaction hash.
@@ -124,7 +116,7 @@ class Sibit
124
116
  builder = Bitcoin::Builder::TxBuilder.new
125
117
  unspent = 0
126
118
  size = 100
127
- utxos = first_one { |api| api.utxos(sources.keys) }
119
+ utxos = @api.utxos(sources.keys)
128
120
  @log.info("#{utxos.count} UTXOs found, these will be used \
129
121
  (value/confirmations at tx_hash):")
130
122
  utxos.each do |utxo|
@@ -170,15 +162,13 @@ class Sibit
170
162
  Amount: #{num(satoshi, p)}
171
163
  Target address: #{target}
172
164
  Change address is #{change}")
173
- first_one do |api|
174
- api.push(tx.to_payload.bth)
175
- end
165
+ @api.push(tx.to_payload.bth)
176
166
  tx.hash
177
167
  end
178
168
 
179
169
  # Gets the hash of the latest block.
180
170
  def latest
181
- first_one(&:latest)
171
+ @api.latest
182
172
  end
183
173
 
184
174
  # You call this method and provide a callback. You provide the hash
@@ -197,7 +187,7 @@ class Sibit
197
187
  wrong = []
198
188
  json = {}
199
189
  loop do
200
- json = first_one { |api| api.block(block) }
190
+ json = @api.block(block)
201
191
  if json[:orphan]
202
192
  steps = 4
203
193
  @log.info("Orphan block found at #{block}, moving #{steps} steps back...")
@@ -206,7 +196,7 @@ class Sibit
206
196
  block = json[:previous]
207
197
  wrong << block
208
198
  @log.info("Moved back to #{block}")
209
- json = first_one { |api| api.block(block) }
199
+ json = @api.block(block)
210
200
  end
211
201
  next
212
202
  end
@@ -236,31 +226,12 @@ class Sibit
236
226
  break
237
227
  end
238
228
  end
229
+ @log.info("Scanned from #{start} to #{json[:hash]} (#{count} blocks)")
239
230
  json[:hash]
240
231
  end
241
232
 
242
233
  private
243
234
 
244
- def first_one
245
- return yield @api unless @api.is_a?(Array)
246
- done = false
247
- result = nil
248
- @api.each do |api|
249
- begin
250
- result = yield api
251
- done = true
252
- break
253
- rescue Sibit::Error => e
254
- @log.info("The API #{api.class.name} failed: #{e.message}")
255
- end
256
- end
257
- unless done
258
- raise Sibit::Error, "No APIs out of #{@api.length} managed to succeed: \
259
- #{@api.map { |a| a.class.name }.join(', ')}"
260
- end
261
- result
262
- end
263
-
264
235
  def num(satoshi, usd)
265
236
  format(
266
237
  '%<satoshi>ss/$%<dollars>0.2f',
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019-2020 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 'error'
24
+ require_relative 'log'
25
+
26
+ # API best of.
27
+ #
28
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
29
+ # Copyright:: Copyright (c) 2019-2020 Yegor Bugayenko
30
+ # License:: MIT
31
+ class Sibit
32
+ # Best of API.
33
+ class BestOf
34
+ # Constructor.
35
+ def initialize(list, log: Sibit::Log.new)
36
+ @list = list
37
+ @log = log
38
+ end
39
+
40
+ # Current price of BTC in USD (float returned).
41
+ def price(currency = 'USD')
42
+ best_of('price') do |api|
43
+ api.price(currency)
44
+ end
45
+ end
46
+
47
+ # Gets the balance of the address, in satoshi.
48
+ def balance(address)
49
+ best_of('balance') do |api|
50
+ api.balance(address)
51
+ end
52
+ end
53
+
54
+ # Get the height of the block.
55
+ def height(hash)
56
+ best_of('height') do |api|
57
+ api.height(hash)
58
+ end
59
+ end
60
+
61
+ # Get the hash of the next block.
62
+ def next_of(hash)
63
+ best_of('next_of') do |api|
64
+ api.next_of(hash)
65
+ end
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
+ best_of('fees', &:fees)
72
+ end
73
+
74
+ # Fetch all unspent outputs per address.
75
+ def utxos(keys)
76
+ best_of('utxos') do |api|
77
+ api.utxos(keys)
78
+ end
79
+ end
80
+
81
+ # Latest block hash.
82
+ def latest
83
+ best_of('latest', &:latest)
84
+ end
85
+
86
+ # Push this transaction (in hex format) to the network.
87
+ def push(hex)
88
+ best_of('push') do |api|
89
+ api.push(hex)
90
+ end
91
+ end
92
+
93
+ # This method should fetch a block and return as a hash.
94
+ def block(hash)
95
+ best_of('block') do |api|
96
+ api.block(hash)
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def best_of(method)
103
+ return yield @list unless @list.is_a?(Array)
104
+ results = []
105
+ @list.each do |api|
106
+ begin
107
+ results << yield(api)
108
+ rescue Sibit::Error => e
109
+ @log.info("The API #{api.class.name} failed at #{method}(): #{e.message}")
110
+ end
111
+ end
112
+ if results.empty?
113
+ raise Sibit::Error, "No APIs out of #{@api.length} managed to succeed at #{method}(): \
114
+ #{@api.map { |a| a.class.name }.join(', ')}"
115
+ end
116
+ results.group_by(&:to_s).values.max_by(&:size)[0]
117
+ end
118
+ end
119
+ end
@@ -45,21 +45,24 @@ class Sibit
45
45
 
46
46
  # Current price of BTC in USD (float returned).
47
47
  def price(_currency = 'USD')
48
- raise Sibit::Error, 'Bitcoinchain API doesn\'t provide BTC price'
48
+ raise Sibit::NotSupportedError, 'Bitcoinchain API doesn\'t provide BTC price'
49
49
  end
50
50
 
51
51
  # The height of the block.
52
52
  def height(_hash)
53
- raise Sibit::Error, 'Bitcoinchain API doesn\'t provide height()'
53
+ raise Sibit::NotSupportedError, 'Bitcoinchain API doesn\'t provide height()'
54
54
  end
55
55
 
56
56
  # Get hash of the block after this one.
57
57
  def next_of(hash)
58
- nxt = Sibit::Json.new(http: @http, log: @log).get(
58
+ block = Sibit::Json.new(http: @http, log: @log).get(
59
59
  URI("https://api-r.bitcoinchain.com/v1/block/#{hash}")
60
- )[0]['next_block']
60
+ )[0]
61
+ raise Sibit::Error, "Block #{hash} not found" if block.nil?
62
+ nxt = block['next_block']
61
63
  nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
62
- @log.info("The next block of #{hash} is #{nxt}")
64
+ @log.info("The block #{hash} is the latest, there is no next block") if nxt.nil?
65
+ @log.info("The next block of #{hash} is #{nxt}") unless nxt.nil?
63
66
  nxt
64
67
  end
65
68
 
@@ -76,13 +79,13 @@ class Sibit
76
79
  end
77
80
  b *= 100_000_000
78
81
  b = b.to_i
79
- @log.info("The balance of #{address} is #{b} satoshi")
82
+ @log.info("The balance of #{address} is #{b} satoshi (#{json['transactions']} txns)")
80
83
  b
81
84
  end
82
85
 
83
86
  # Get recommended fees, in satoshi per byte.
84
87
  def fees
85
- raise Sibit::Error, 'Not implemented yet'
88
+ raise Sibit::NotSupportedError, 'Not implemented yet'
86
89
  end
87
90
 
88
91
  # Gets the hash of the latest block.
@@ -96,12 +99,12 @@ class Sibit
96
99
 
97
100
  # Fetch all unspent outputs per address.
98
101
  def utxos(_sources)
99
- raise Sibit::Error, 'Not implemented yet'
102
+ raise Sibit::NotSupportedError, 'Not implemented yet'
100
103
  end
101
104
 
102
105
  # Push this transaction (in hex format) to the network.
103
106
  def push(_hex)
104
- raise Sibit::Error, 'Not implemented yet'
107
+ raise Sibit::NotSupportedError, 'Not implemented yet'
105
108
  end
106
109
 
107
110
  # This method should fetch a Blockchain block and return as a hash. Raises
@@ -59,7 +59,7 @@ class Sibit
59
59
 
60
60
  # Get hash of the block after this one.
61
61
  def next_of(_hash)
62
- raise Sibit::Error, 'Blockchain API doesn\'t provide next_of()'
62
+ raise Sibit::NotSupportedError, 'Blockchain API doesn\'t provide next_of()'
63
63
  end
64
64
 
65
65
  # The height of the block.
@@ -78,13 +78,14 @@ class Sibit
78
78
  URI("https://blockchain.info/rawaddr/#{address}?limit=0"),
79
79
  accept: [200, 500]
80
80
  )
81
- @log.info("Received/sent: #{json['total_received']}/#{json['total_sent']}")
82
- json['final_balance']
81
+ b = json['final_balance']
82
+ @log.info("The balance of #{address} is #{b} satoshi (#{json['n_tx']} txns)")
83
+ b
83
84
  end
84
85
 
85
86
  # Get recommended fees.
86
87
  def fees
87
- raise Sibit::Error, 'fees() is not provided by Blockchain API'
88
+ raise Sibit::NotSupportedError, 'fees() is not provided by Blockchain API'
88
89
  end
89
90
 
90
91
  # Fetch all unspent outputs per address. The argument is an array
@@ -47,18 +47,18 @@ class Sibit
47
47
 
48
48
  # Current price of BTC in USD (float returned).
49
49
  def price(_currency = 'USD')
50
- raise Sibit::Error, 'Blockchair doesn\'t provide BTC price'
50
+ raise Sibit::NotSupportedError, 'Blockchair doesn\'t provide BTC price'
51
51
  end
52
52
 
53
53
  # The height of the block.
54
54
  def height(_hash)
55
- raise Sibit::Error, 'Blockchair API doesn\'t provide height()'
55
+ raise Sibit::NotSupportedError, 'Blockchair API doesn\'t provide height()'
56
56
  end
57
57
 
58
58
  # Get hash of the block after this one.
59
59
  def next_of(_hash)
60
60
  # They don't provide next block hash
61
- raise Sibit::Error, 'Blockchair API doesn\'t provide next_of()'
61
+ raise Sibit::NotSupportedError, 'Blockchair API doesn\'t provide next_of()'
62
62
  end
63
63
 
64
64
  # Gets the balance of the address, in satoshi.
@@ -70,24 +70,25 @@ class Sibit
70
70
  @log.info("Address #{address} not found")
71
71
  return 0
72
72
  end
73
- b = json['address']['balance']
73
+ a = json['address']
74
+ b = a['balance']
74
75
  @log.info("The balance of #{address} is #{b} satoshi")
75
76
  b
76
77
  end
77
78
 
78
79
  # Get recommended fees, in satoshi per byte.
79
80
  def fees
80
- raise Sibit::Error, 'Blockchair doesn\'t implement fees()'
81
+ raise Sibit::NotSupportedError, 'Blockchair doesn\'t implement fees()'
81
82
  end
82
83
 
83
84
  # Gets the hash of the latest block.
84
85
  def latest
85
- raise Sibit::Error, 'Blockchair doesn\'t implement latest()'
86
+ raise Sibit::NotSupportedError, 'Blockchair doesn\'t implement latest()'
86
87
  end
87
88
 
88
89
  # Fetch all unspent outputs per address.
89
90
  def utxos(_sources)
90
- raise Sibit::Error, 'Blockchair doesn\'t implement utxos()'
91
+ raise Sibit::NotSupportedError, 'Blockchair doesn\'t implement utxos()'
91
92
  end
92
93
 
93
94
  # Push this transaction (in hex format) to the network.
@@ -101,7 +102,7 @@ class Sibit
101
102
 
102
103
  # This method should fetch a Blockchain block and return as a hash.
103
104
  def block(_hash)
104
- raise Sibit::Error, 'Blockchair doesn\'t implement block()'
105
+ raise Sibit::NotSupportedError, 'Blockchair doesn\'t implement block()'
105
106
  end
106
107
 
107
108
  private
@@ -47,7 +47,7 @@ class Sibit
47
47
 
48
48
  # Current price of BTC in USD (float returned).
49
49
  def price(_currency = 'USD')
50
- raise Sibit::Error, 'Btc.com API doesn\'t provide prices'
50
+ raise Sibit::NotSupportedError, 'Btc.com API doesn\'t provide prices'
51
51
  end
52
52
 
53
53
  # Gets the balance of the address, in satoshi.
@@ -69,9 +69,12 @@ class Sibit
69
69
  head = Sibit::Json.new(http: @http, log: @log).get(
70
70
  URI("https://chain.api.btc.com/v3/block/#{hash}")
71
71
  )
72
- nxt = head['data']['next_block_hash']
72
+ data = head['data']
73
+ raise Sibit::Error, "The block #{hash} not found" if data.nil?
74
+ nxt = data['next_block_hash']
73
75
  nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
74
- @log.info("The next block of #{hash} is #{nxt}")
76
+ @log.info("The block #{hash} is the latest, there is no next block") if nxt.nil?
77
+ @log.info("The next block of #{hash} is #{nxt}") unless nxt.nil?
75
78
  nxt
76
79
  end
77
80
 
@@ -80,21 +83,25 @@ class Sibit
80
83
  json = Sibit::Json.new(http: @http, log: @log).get(
81
84
  URI("https://chain.api.btc.com/v3/block/#{hash}")
82
85
  )
83
- h = json['data']['height']
86
+ data = json['data']
87
+ raise Sibit::Error, "The block #{hash} not found" if data.nil?
88
+ h = data['height']
84
89
  @log.info("The height of #{hash} is #{h}")
85
90
  h
86
91
  end
87
92
 
88
93
  # Get recommended fees, in satoshi per byte.
89
94
  def fees
90
- raise Sibit::Error, 'Btc.com doesn\'t provide recommended fees'
95
+ raise Sibit::NotSupportedError, 'Btc.com doesn\'t provide recommended fees'
91
96
  end
92
97
 
93
98
  # Gets the hash of the latest block.
94
99
  def latest
95
100
  uri = URI('https://chain.api.btc.com/v3/block/latest')
96
101
  json = Sibit::Json.new(http: @http, log: @log).get(uri)
97
- hash = json['data']['hash']
102
+ data = json['data']
103
+ raise Sibit::Error, 'The latest block not found' if data.nil?
104
+ hash = data['hash']
98
105
  @log.info("The hash of the latest block is #{hash}")
99
106
  hash
100
107
  end
@@ -106,7 +113,9 @@ class Sibit
106
113
  json = Sibit::Json.new(http: @http, log: @log).get(
107
114
  URI("https://chain.api.btc.com/v3/address/#{hash}/unspent")
108
115
  )
109
- json['data']['list'].each do |u|
116
+ data = json['data']
117
+ raise Sibit::Error, "The address #{hash} not found" if data.nil?
118
+ data['list'].each do |u|
110
119
  outs = Sibit::Json.new(http: @http, log: @log).get(
111
120
  URI("https://chain.api.btc.com/v3/tx/#{u['tx_hash']}?verbose=3")
112
121
  )['data']['outputs']
@@ -127,7 +136,7 @@ class Sibit
127
136
 
128
137
  # Push this transaction (in hex format) to the network.
129
138
  def push(_hex)
130
- raise Sibit::Error, 'Btc.com doesn\'t provide payment gateway'
139
+ raise Sibit::NotSupportedError, 'Btc.com doesn\'t provide payment gateway'
131
140
  end
132
141
 
133
142
  # This method should fetch a Blockchain block and return as a hash.
@@ -135,13 +144,15 @@ class Sibit
135
144
  head = Sibit::Json.new(http: @http, log: @log).get(
136
145
  URI("https://chain.api.btc.com/v3/block/#{hash}")
137
146
  )
138
- nxt = head['data']['next_block_hash']
147
+ data = head['data']
148
+ raise Sibit::Error, "The block #{hash} not found" if data.nil?
149
+ nxt = data['next_block_hash']
139
150
  nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
140
151
  {
141
- hash: head['data']['hash'],
142
- orphan: head['data']['is_orphan'],
152
+ hash: data['hash'],
153
+ orphan: data['is_orphan'],
143
154
  next: nxt,
144
- previous: head['data']['prev_block_hash'],
155
+ previous: data['prev_block_hash'],
145
156
  txns: txns(hash)
146
157
  }
147
158
  end
@@ -54,41 +54,41 @@ class Sibit
54
54
 
55
55
  # Get hash of the block after this one.
56
56
  def next_of(_hash)
57
- raise Sibit::Error, 'Cex.io API doesn\'t provide next_of()'
57
+ raise Sibit::NotSupportedError, 'Cex.io API doesn\'t provide next_of()'
58
58
  end
59
59
 
60
60
  # Gets the balance of the address, in satoshi.
61
61
  def balance(_address)
62
- raise Sibit::Error, 'Cex.io doesn\'t implement balance()'
62
+ raise Sibit::NotSupportedError, 'Cex.io doesn\'t implement balance()'
63
63
  end
64
64
 
65
65
  # The height of the block.
66
66
  def height(_hash)
67
- raise Sibit::Error, 'Cex.io doesn\'t implement height()'
67
+ raise Sibit::NotSupportedError, 'Cex.io doesn\'t implement height()'
68
68
  end
69
69
 
70
70
  # Get recommended fees, in satoshi per byte.
71
71
  def fees
72
- raise Sibit::Error, 'Cex.io doesn\'t implement fees()'
72
+ raise Sibit::NotSupportedError, 'Cex.io doesn\'t implement fees()'
73
73
  end
74
74
 
75
75
  # Gets the hash of the latest block.
76
76
  def latest
77
- raise Sibit::Error, 'Cex.io doesn\'t implement latest()'
77
+ raise Sibit::NotSupportedError, 'Cex.io doesn\'t implement latest()'
78
78
  end
79
79
 
80
80
  # Fetch all unspent outputs per address.
81
81
  def utxos(_sources)
82
- raise Sibit::Error, 'Cex.io doesn\'t implement utxos()'
82
+ raise Sibit::NotSupportedError, 'Cex.io doesn\'t implement utxos()'
83
83
  end
84
84
 
85
85
  # Push this transaction (in hex format) to the network.
86
86
  def push(_hex)
87
- raise Sibit::Error, 'Cex.io doesn\'t implement push()'
87
+ raise Sibit::NotSupportedError, 'Cex.io doesn\'t implement push()'
88
88
  end
89
89
 
90
90
  def block(_hash)
91
- raise Sibit::Error, 'Cex.io doesn\'t implement block()'
91
+ raise Sibit::NotSupportedError, 'Cex.io doesn\'t implement block()'
92
92
  end
93
93
  end
94
94
  end
@@ -46,15 +46,18 @@ class Sibit
46
46
 
47
47
  # Current price of BTC in USD (float returned).
48
48
  def price(_currency = 'USD')
49
- raise Sibit::Error, 'Cryptoapis doesn\'t provide BTC price'
49
+ raise Sibit::NotSupportedError, 'Cryptoapis doesn\'t provide BTC price'
50
50
  end
51
51
 
52
52
  # Get hash of the block after this one.
53
53
  def next_of(hash)
54
- Sibit::Json.new(http: @http, log: @log).get(
54
+ nxt = Sibit::Json.new(http: @http, log: @log).get(
55
55
  URI("https://api.cryptoapis.io/v1/bc/btc/mainnet/blocks/#{hash}"),
56
56
  headers: headers
57
57
  )['payload']['hash']
58
+ @log.info("The block #{hash} is the latest, there is no next block") if nxt.nil?
59
+ @log.info("The next block of #{hash} is #{nxt}") unless nxt.nil?
60
+ nxt
58
61
  end
59
62
 
60
63
  # The height of the block.
@@ -81,7 +84,7 @@ class Sibit
81
84
 
82
85
  # Get recommended fees, in satoshi per byte.
83
86
  def fees
84
- raise Sibit::Error, 'Cryptoapis doesn\'t provide recommended fees'
87
+ raise Sibit::NotSupportedError, 'Cryptoapis doesn\'t provide recommended fees'
85
88
  end
86
89
 
87
90
  # Gets the hash of the latest block.
@@ -96,7 +99,7 @@ class Sibit
96
99
 
97
100
  # Fetch all unspent outputs per address.
98
101
  def utxos(_sources)
99
- raise Sibit::Error, 'Not implemented yet'
102
+ raise Sibit::NotSupportedError, 'Not implemented yet'
100
103
  end
101
104
 
102
105
  # Push this transaction (in hex format) to the network.
@@ -44,22 +44,22 @@ class Sibit
44
44
 
45
45
  # Current price of BTC in USD (float returned).
46
46
  def price(_currency)
47
- raise Sibit::Error, 'price() doesn\'t work here'
47
+ raise Sibit::NotSupportedError, 'price() doesn\'t work here'
48
48
  end
49
49
 
50
50
  # Gets the balance of the address, in satoshi.
51
51
  def balance(_address)
52
- raise Sibit::Error, 'balance() doesn\'t work here'
52
+ raise Sibit::NotSupportedError, 'balance() doesn\'t work here'
53
53
  end
54
54
 
55
55
  # Get hash of the block after this one.
56
56
  def next_of(_hash)
57
- raise Sibit::Error, 'Earn.com API doesn\'t provide next_of()'
57
+ raise Sibit::NotSupportedError, 'Earn.com API doesn\'t provide next_of()'
58
58
  end
59
59
 
60
60
  # The height of the block.
61
61
  def height(_hash)
62
- raise Sibit::Error, 'Earn API doesn\'t provide height()'
62
+ raise Sibit::NotSupportedError, 'Earn API doesn\'t provide height()'
63
63
  end
64
64
 
65
65
  # Get recommended fees, in satoshi per byte. The method returns
@@ -80,22 +80,22 @@ class Sibit
80
80
 
81
81
  # Fetch all unspent outputs per address.
82
82
  def utxos(_sources)
83
- raise Sibit::Error, 'Not implemented yet'
83
+ raise Sibit::NotSupportedError, 'Not implemented yet'
84
84
  end
85
85
 
86
86
  # Push this transaction (in hex format) to the network.
87
87
  def push(_hex)
88
- raise Sibit::Error, 'Not implemented yet'
88
+ raise Sibit::NotSupportedError, 'Not implemented yet'
89
89
  end
90
90
 
91
91
  # Gets the hash of the latest block.
92
92
  def latest
93
- raise Sibit::Error, 'latest() doesn\'t work here'
93
+ raise Sibit::NotSupportedError, 'latest() doesn\'t work here'
94
94
  end
95
95
 
96
96
  # This method should fetch a Blockchain block and return as a hash.
97
97
  def block(_hash)
98
- raise Sibit::Error, 'block() doesn\'t work here'
98
+ raise Sibit::NotSupportedError, 'block() doesn\'t work here'
99
99
  end
100
100
  end
101
101
  end
@@ -28,4 +28,7 @@
28
28
  class Sibit
29
29
  # The error.
30
30
  class Error < StandardError; end
31
+
32
+ # The operation is not supported.
33
+ class NotSupportedError < Error; end
31
34
  end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019-2020 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 'error'
24
+ require_relative 'log'
25
+
26
+ # API first of.
27
+ #
28
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
29
+ # Copyright:: Copyright (c) 2019-2020 Yegor Bugayenko
30
+ # License:: MIT
31
+ class Sibit
32
+ # First of API.
33
+ class FirstOf
34
+ # Constructor.
35
+ def initialize(list, log: Sibit::Log.new)
36
+ @list = list
37
+ @log = log
38
+ end
39
+
40
+ # Current price of BTC in USD (float returned).
41
+ def price(currency = 'USD')
42
+ first_of('price') do |api|
43
+ api.price(currency)
44
+ end
45
+ end
46
+
47
+ # Gets the balance of the address, in satoshi.
48
+ def balance(address)
49
+ first_of('balance') do |api|
50
+ api.balance(address)
51
+ end
52
+ end
53
+
54
+ # Get the height of the block.
55
+ def height(hash)
56
+ first_of('height') do |api|
57
+ api.height(hash)
58
+ end
59
+ end
60
+
61
+ # Get the hash of the next block.
62
+ def next_of(hash)
63
+ first_of('next_of') do |api|
64
+ api.next_of(hash)
65
+ end
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
+ first_of('fees', &:fees)
72
+ end
73
+
74
+ # Fetch all unspent outputs per address.
75
+ def utxos(keys)
76
+ first_of('utxos') do |api|
77
+ api.utxos(keys)
78
+ end
79
+ end
80
+
81
+ # Latest block hash.
82
+ def latest
83
+ first_of('latest', &:latest)
84
+ end
85
+
86
+ # Push this transaction (in hex format) to the network.
87
+ def push(hex)
88
+ first_of('push') do |api|
89
+ api.push(hex)
90
+ end
91
+ end
92
+
93
+ # This method should fetch a block and return as a hash.
94
+ def block(hash)
95
+ first_of('block') do |api|
96
+ api.block(hash)
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def first_of(method)
103
+ return yield @list unless @list.is_a?(Array)
104
+ done = false
105
+ result = nil
106
+ @list.each do |api|
107
+ begin
108
+ result = yield api
109
+ done = true
110
+ break
111
+ rescue Sibit::Error => e
112
+ @log.info("The API #{api.class.name} failed at #{method}(): #{e.message}")
113
+ end
114
+ end
115
+ unless done
116
+ raise Sibit::Error, "No APIs out of #{@api.length} managed to succeed at #{method}(): \
117
+ #{@api.map { |a| a.class.name }.join(', ')}"
118
+ end
119
+ result
120
+ end
121
+ end
122
+ end
@@ -26,5 +26,5 @@
26
26
  # License:: MIT
27
27
  class Sibit
28
28
  # Current version of the library.
29
- VERSION = '0.18.4'
29
+ VERSION = '0.19.0'
30
30
  end
@@ -36,9 +36,9 @@ Gem::Specification.new do |s|
36
36
  s.version = Sibit::VERSION
37
37
  s.license = 'MIT'
38
38
  s.summary = 'Simple Bitcoin Client'
39
- s.description = 'This is a simple Bitcoin client, to use from command line \
40
- or from your Ruby app. You don\'t need to run any Bitcoin software, \
41
- no need to install anything, etc. All you need is just a command line \
39
+ s.description = 'This is a simple Bitcoin client, to use from command line
40
+ or from your Ruby app. You don\'t need to run any Bitcoin software,
41
+ no need to install anything, etc. All you need is just a command line
42
42
  and Ruby 2.3+.'
43
43
  s.authors = ['Yegor Bugayenko']
44
44
  s.email = 'yegor256@gmail.com'
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019-2020 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 'minitest/autorun'
24
+ require_relative '../lib/sibit'
25
+ require_relative '../lib/sibit/fake'
26
+ require_relative '../lib/sibit/bestof'
27
+
28
+ # Sibit::BestOf test.
29
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
30
+ # Copyright:: Copyright (c) 2019-2020 Yegor Bugayenko
31
+ # License:: MIT
32
+ class TestBestOf < Minitest::Test
33
+ def test_not_array
34
+ sibit = Sibit::BestOf.new(Sibit::Fake.new)
35
+ assert_equal(64, sibit.latest.length)
36
+ assert_equal(12, sibit.fees[:S])
37
+ end
38
+
39
+ def test_one_apis
40
+ sibit = Sibit::BestOf.new([Sibit::Fake.new])
41
+ assert_equal(64, sibit.latest.length)
42
+ assert_equal(12, sibit.fees[:S])
43
+ end
44
+
45
+ def test_two_apis
46
+ sibit = Sibit::BestOf.new([Sibit::Fake.new, Sibit::Fake.new])
47
+ assert_equal(64, sibit.latest.length)
48
+ assert_equal(12, sibit.fees[:S])
49
+ end
50
+ end
@@ -34,7 +34,8 @@ class TestBlockchair < Minitest::Test
34
34
  def test_fetch_balance
35
35
  hash = '1GkQmKAmHtNfnD3LHhTkewJxKHVSta4m2a'
36
36
  stub_request(:get, "https://api.blockchair.com/bitcoin/dashboards/address/#{hash}")
37
- .to_return(body: "{\"data\": {\"#{hash}\": {\"address\": {\"balance\": 1}}}}")
37
+ .to_return(body: "{\"data\": {\"#{hash}\": {\"address\":
38
+ {\"balance\": 1, \"transactions\": []}}}}")
38
39
  sibit = Sibit::Blockchair.new
39
40
  satoshi = sibit.balance(hash)
40
41
  assert_equal(1, satoshi)
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright (c) 2019-2020 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 'minitest/autorun'
24
+ require_relative '../lib/sibit'
25
+ require_relative '../lib/sibit/fake'
26
+ require_relative '../lib/sibit/firstof'
27
+
28
+ # Sibit::FirstOf test.
29
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
30
+ # Copyright:: Copyright (c) 2019-2020 Yegor Bugayenko
31
+ # License:: MIT
32
+ class TestFirstOf < Minitest::Test
33
+ def test_not_array
34
+ sibit = Sibit::FirstOf.new(Sibit::Fake.new)
35
+ assert_equal(64, sibit.latest.length)
36
+ assert_equal(12, sibit.fees[:S])
37
+ end
38
+
39
+ def test_one_apis
40
+ sibit = Sibit::FirstOf.new([Sibit::Fake.new])
41
+ assert_equal(64, sibit.latest.length)
42
+ assert_equal(12, sibit.fees[:S])
43
+ end
44
+
45
+ def test_two_apis
46
+ sibit = Sibit::FirstOf.new([Sibit::Fake.new, Sibit::Fake.new])
47
+ assert_equal(64, sibit.latest.length)
48
+ assert_equal(12, sibit.fees[:S])
49
+ end
50
+ end
@@ -27,6 +27,8 @@ require_relative '../lib/sibit'
27
27
  require_relative '../lib/sibit/earn'
28
28
  require_relative '../lib/sibit/fake'
29
29
  require_relative '../lib/sibit/blockchain'
30
+ require_relative '../lib/sibit/firstof'
31
+ require_relative '../lib/sibit/bestof'
30
32
 
31
33
  # Sibit.
32
34
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
@@ -127,7 +129,7 @@ class TestSibit < Minitest::Test
127
129
  'https://blockchain.info/unspent?active=1JvCsJtLmCxEk7ddZFnVkGXpr9uhxZPmJi&limit=1000'
128
130
  ).to_return(body: JSON.pretty_generate(json))
129
131
  stub_request(:post, 'https://blockchain.info/pushtx').to_return(status: 200)
130
- sibit = Sibit.new(api: [Sibit::Earn.new, Sibit::Blockchain.new])
132
+ sibit = Sibit.new(api: Sibit::FirstOf.new([Sibit::Earn.new, Sibit::Blockchain.new]))
131
133
  target = sibit.create(sibit.generate)
132
134
  change = sibit.create(sibit.generate)
133
135
  tx = sibit.pay(
@@ -156,7 +158,7 @@ class TestSibit < Minitest::Test
156
158
  :get,
157
159
  'https://blockchain.info/unspent?active=1JvCsJtLmCxEk7ddZFnVkGXpr9uhxZPmJi&limit=1000'
158
160
  ).to_return(body: JSON.pretty_generate(json))
159
- sibit = Sibit.new
161
+ sibit = Sibit.new(api: Sibit::BestOf.new([Sibit::Fake.new, Sibit::Fake.new]))
160
162
  target = sibit.create(sibit.generate)
161
163
  change = sibit.create(sibit.generate)
162
164
  assert_raises Sibit::Error do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sibit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.4
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-27 00:00:00.000000000 Z
11
+ date: 2020-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: backtrace
@@ -249,9 +249,9 @@ dependencies:
249
249
  - !ruby/object:Gem::Version
250
250
  version: 3.7.6
251
251
  description: |-
252
- This is a simple Bitcoin client, to use from command line \
253
- or from your Ruby app. You don't need to run any Bitcoin software, \
254
- no need to install anything, etc. All you need is just a command line \
252
+ This is a simple Bitcoin client, to use from command line
253
+ or from your Ruby app. You don't need to run any Bitcoin software,
254
+ no need to install anything, etc. All you need is just a command line
255
255
  and Ruby 2.3+.
256
256
  email: yegor256@gmail.com
257
257
  executables:
@@ -281,6 +281,7 @@ files:
281
281
  - features/step_definitions/steps.rb
282
282
  - features/support/env.rb
283
283
  - lib/sibit.rb
284
+ - lib/sibit/bestof.rb
284
285
  - lib/sibit/bitcoinchain.rb
285
286
  - lib/sibit/blockchain.rb
286
287
  - lib/sibit/blockchair.rb
@@ -290,6 +291,7 @@ files:
290
291
  - lib/sibit/earn.rb
291
292
  - lib/sibit/error.rb
292
293
  - lib/sibit/fake.rb
294
+ - lib/sibit/firstof.rb
293
295
  - lib/sibit/http.rb
294
296
  - lib/sibit/json.rb
295
297
  - lib/sibit/log.rb
@@ -297,6 +299,7 @@ files:
297
299
  - logo.svg
298
300
  - sibit.gemspec
299
301
  - test/test__helper.rb
302
+ - test/test_bestof.rb
300
303
  - test/test_bitcoinchain.rb
301
304
  - test/test_blockchain.rb
302
305
  - test/test_blockchair.rb
@@ -304,6 +307,7 @@ files:
304
307
  - test/test_cex.rb
305
308
  - test/test_cryptoapis.rb
306
309
  - test/test_fake.rb
310
+ - test/test_firstof.rb
307
311
  - test/test_json.rb
308
312
  - test/test_live.rb
309
313
  - test/test_sibit.rb
@@ -337,6 +341,7 @@ test_files:
337
341
  - features/step_definitions/steps.rb
338
342
  - features/support/env.rb
339
343
  - test/test__helper.rb
344
+ - test/test_bestof.rb
340
345
  - test/test_bitcoinchain.rb
341
346
  - test/test_blockchain.rb
342
347
  - test/test_blockchair.rb
@@ -344,6 +349,7 @@ test_files:
344
349
  - test/test_cex.rb
345
350
  - test/test_cryptoapis.rb
346
351
  - test/test_fake.rb
352
+ - test/test_firstof.rb
347
353
  - test/test_json.rb
348
354
  - test/test_live.rb
349
355
  - test/test_sibit.rb