sibit 0.34.2 → 0.35.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: 7ea3404e8f546263164e5b87826a723238cb6bd21589149429f9e3d5b32f0e5a
4
- data.tar.gz: 81c897434a338c46c517c8de6d4fc69693f190b10beffe8ef43f909daa92e47d
3
+ metadata.gz: 513237b6306d27583cb85cb5e95665d650557e1913446c6ac1bfea6a3b2ba2ef
4
+ data.tar.gz: 36206100588673eb924460d7e5b7774ab2a4f5004eb98effd71eaca77e0a08ff
5
5
  SHA512:
6
- metadata.gz: 0f481815314355cdf7cad857fb35d9b08599e8b2a6e8aa175c33ec5e68962957b390033d8aafb01d580c1e33035376a52b6dee983d3fe31469b15f1be6868ae0
7
- data.tar.gz: 1652d8c4bee002326b3f9e6b885e85cf750742b10b7de6fd71379f64331e07d94d6aefbc49cab2a71f8a0ab8a6b7606a7458eb395fec7e50a715a309db1ffa2d
6
+ metadata.gz: 125db74f9d3c3cbbe34dd8bf0456a27717e164141e5e6aa811d11b5ceeaf9cf2744c7f117ab03f0163979eefb3c1d17bc11e0182f90bdafe814b93d8c5b707d4
7
+ data.tar.gz: 2f8337f9f35a9afb34ae65d90b9aef46ae631bfaa0c3f9e1622e638b08fc1289018048c93f2037663733d9e73fdd3914ace5384241cbe894db3e894a435976c4
data/Gemfile CHANGED
@@ -6,8 +6,8 @@
6
6
  source 'https://rubygems.org'
7
7
  gemspec
8
8
 
9
- gem 'aruba', '~>2.2', require: false
10
- gem 'cucumber', '~>11.0', require: false
9
+ gem 'aruba', '~>3.0', require: false
10
+ gem 'cucumber', '~>12.0', require: false
11
11
  gem 'donce', '~>0.2', require: false
12
12
  gem 'logger', '~>1.7', require: false
13
13
  gem 'minitest', '~>6.0', require: false
data/Gemfile.lock CHANGED
@@ -19,47 +19,51 @@ GEM
19
19
  addressable (2.9.0)
20
20
  public_suffix (>= 2.0.2, < 8.0)
21
21
  ansi (1.6.0)
22
- aruba (2.4.1)
23
- bundler (>= 1.17)
22
+ aruba (3.0.1)
23
+ bundler (>= 2.4)
24
24
  contracts (>= 0.16.0, < 0.18.0)
25
- cucumber (>= 8.0, < 12.0)
26
- irb (~> 1.16)
25
+ cucumber-core (>= 16.0, < 21.0)
26
+ marcel (>= 1.2, < 3.0)
27
27
  rspec-expectations (>= 3.4, < 5.0)
28
+ tempfile (~> 0.3.1)
28
29
  thor (~> 1.0)
29
30
  ast (2.4.3)
30
31
  backtrace (0.4.1)
31
32
  base64 (0.3.0)
32
- bigdecimal (4.1.2)
33
+ bigdecimal (4.1.3)
33
34
  builder (3.3.0)
34
35
  contracts (0.17.3)
35
36
  crack (1.0.1)
36
37
  bigdecimal
37
38
  rexml
38
- cucumber (11.1.1)
39
+ cucumber (12.0.1)
39
40
  base64 (~> 0.2)
40
41
  builder (~> 3.2)
41
- cucumber-ci-environment (> 9, < 12)
42
- cucumber-core (>= 16.2.0, < 17)
43
- cucumber-cucumber-expressions (> 17, < 20)
44
- cucumber-html-formatter (> 21, < 24)
45
- diff-lcs (~> 1.5)
42
+ cucumber-ci-environment (> 9, < 14)
43
+ cucumber-core (>= 20, < 21)
44
+ cucumber-cucumber-expressions (> 17, < 21)
45
+ cucumber-html-formatter (> 21, < 25)
46
+ cucumber-query (~> 16.1)
47
+ diff-lcs (>= 1.5, < 3.0)
46
48
  logger (~> 1.6)
47
49
  mini_mime (~> 1.1)
48
- multi_test (~> 1.1)
50
+ multi_test (> 1, < 4)
49
51
  sys-uname (~> 1.5)
50
- cucumber-ci-environment (11.0.0)
51
- cucumber-core (16.2.0)
52
- cucumber-gherkin (> 36, < 40)
53
- cucumber-messages (> 31, < 33)
54
- cucumber-tag-expressions (> 6, < 9)
55
- cucumber-cucumber-expressions (19.0.1)
52
+ cucumber-ci-environment (13.0.0)
53
+ cucumber-core (20.0.0)
54
+ cucumber-gherkin (> 36, < 43)
55
+ cucumber-messages (> 31, < 35)
56
+ cucumber-tag-expressions (> 6, < 12)
57
+ cucumber-cucumber-expressions (20.1.0)
56
58
  bigdecimal
57
- cucumber-gherkin (39.1.0)
58
- cucumber-messages (>= 31, < 33)
59
- cucumber-html-formatter (23.1.0)
60
- cucumber-messages (> 23, < 33)
61
- cucumber-messages (32.3.1)
62
- cucumber-tag-expressions (8.1.0)
59
+ cucumber-gherkin (42.0.1)
60
+ cucumber-messages (>= 34, < 35)
61
+ cucumber-html-formatter (24.2.0)
62
+ cucumber-messages (> 23, < 35)
63
+ cucumber-messages (34.2.1)
64
+ cucumber-query (16.1.1)
65
+ cucumber-messages (>= 34.1, < 36)
66
+ cucumber-tag-expressions (11.0.1)
63
67
  decoor (0.1.0)
64
68
  diff-lcs (1.6.2)
65
69
  docile (1.4.1)
@@ -72,24 +76,19 @@ GEM
72
76
  loog (~> 0.6)
73
77
  tago (~> 0.1)
74
78
  ellipsized (0.3.0)
75
- erb (6.0.6)
79
+ erb (6.0.7)
76
80
  ffi (1.17.4-arm64-darwin)
77
81
  ffi (1.17.4-x86_64-linux-gnu)
78
82
  hashdiff (1.2.1)
79
- io-console (0.8.2)
80
- irb (1.18.0)
81
- pp (>= 0.6.0)
82
- prism (>= 1.3.0)
83
- rdoc (>= 4.0.0)
84
- reline (>= 0.4.2)
85
83
  iri (0.11.8)
86
- json (2.21.1)
84
+ json (2.21.2)
87
85
  language_server-protocol (3.17.0.6)
88
86
  lint_roller (1.1.0)
89
87
  logger (1.7.0)
90
88
  loog (0.8.0)
91
89
  ellipsized
92
90
  logger (~> 1.0)
91
+ marcel (2.1.0)
93
92
  memoist3 (1.0.0)
94
93
  mini_mime (1.1.5)
95
94
  minitest (6.0.6)
@@ -100,20 +99,17 @@ GEM
100
99
  builder
101
100
  minitest (>= 5.0, < 7)
102
101
  ruby-progressbar
103
- multi_test (1.1.0)
104
- nokogiri (1.19.4-arm64-darwin)
102
+ multi_test (2.0.0)
103
+ nokogiri (1.19.7-arm64-darwin)
105
104
  racc (~> 1.4)
106
- nokogiri (1.19.4-x86_64-linux-gnu)
105
+ nokogiri (1.19.7-x86_64-linux-gnu)
107
106
  racc (~> 1.4)
108
107
  openssl (3.3.3)
109
108
  os (1.1.4)
110
- parallel (2.1.0)
111
- parser (3.3.12.0)
109
+ parallel (2.3.0)
110
+ parser (3.3.13.0)
112
111
  ast (~> 2.4.1)
113
112
  racc
114
- pp (0.6.4)
115
- prettyprint
116
- prettyprint (0.2.0)
117
113
  prism (1.9.0)
118
114
  public_suffix (7.0.5)
119
115
  qbash (0.8.4)
@@ -124,18 +120,16 @@ GEM
124
120
  racc (1.8.1)
125
121
  rainbow (3.1.1)
126
122
  rake (13.4.2)
127
- rbs (4.0.3)
123
+ rbs (4.2.0)
128
124
  logger
129
125
  prism (>= 1.6.0)
130
126
  tsort
131
- rdoc (8.0.0)
127
+ rdoc (8.1.0)
132
128
  erb
133
129
  prism (>= 1.6.0)
134
130
  rbs (>= 4.0.0)
135
131
  tsort
136
- regexp_parser (2.12.0)
137
- reline (0.6.3)
138
- io-console (~> 0.5)
132
+ regexp_parser (2.13.1)
139
133
  retriable (3.8.0)
140
134
  retriable_proxy (1.0.2)
141
135
  retriable (>= 2.0, < 4.0)
@@ -144,30 +138,30 @@ GEM
144
138
  diff-lcs (>= 1.2.0, < 2.0)
145
139
  rspec-support (~> 3.13.0)
146
140
  rspec-support (3.13.7)
147
- rubocop (1.88.2)
148
- json (~> 2.3)
141
+ rubocop (1.92.2)
142
+ json (>= 2.3)
149
143
  language_server-protocol (~> 3.17.0.2)
150
144
  lint_roller (~> 1.1.0)
151
145
  parallel (>= 1.10)
152
146
  parser (>= 3.3.0.2)
153
147
  rainbow (>= 2.2.2, < 4.0)
154
- regexp_parser (>= 2.9.3, < 3.0)
148
+ regexp_parser (>= 2.11.3, < 3.0)
155
149
  rubocop-ast (>= 1.49.0, < 2.0)
156
150
  ruby-progressbar (~> 1.7)
157
151
  unicode-display_width (>= 2.4.0, < 4.0)
158
152
  rubocop-ast (1.50.0)
159
153
  parser (>= 3.3.7.2)
160
154
  prism (~> 1.7)
161
- rubocop-elegant (0.7.1)
155
+ rubocop-elegant (0.8.0)
162
156
  lint_roller (~> 1.1)
163
157
  rubocop (~> 1.75)
164
- rubocop-minitest (0.40.0)
158
+ rubocop-minitest (0.41.0)
165
159
  lint_roller (~> 1.1)
166
- rubocop (>= 1.75.0, < 2.0)
160
+ rubocop (>= 1.89.0, < 2.0)
167
161
  rubocop-ast (>= 1.38.0, < 2.0)
168
- rubocop-performance (1.26.1)
162
+ rubocop-performance (1.27.0)
169
163
  lint_roller (~> 1.1)
170
- rubocop (>= 1.75.0, < 2.0)
164
+ rubocop (>= 1.89.0, < 2.0)
171
165
  rubocop-ast (>= 1.47.1, < 2.0)
172
166
  rubocop-rake (0.7.1)
173
167
  lint_roller (~> 1.1)
@@ -186,12 +180,13 @@ GEM
186
180
  ffi (~> 1.1)
187
181
  memoist3 (~> 1.0.0)
188
182
  tago (0.7.0)
183
+ tempfile (0.3.2)
189
184
  thor (1.5.0)
190
185
  tsort (0.2.0)
191
- unicode-display_width (3.2.0)
192
- unicode-emoji (~> 4.1)
193
- unicode-emoji (4.2.0)
194
- webmock (3.26.2)
186
+ unicode-display_width (3.3.0)
187
+ unicode-emoji (~> 4.3)
188
+ unicode-emoji (4.3.0)
189
+ webmock (3.34.0)
195
190
  addressable (>= 2.8.0)
196
191
  crack (>= 0.3.2)
197
192
  hashdiff (>= 0.4.0, < 2.0.0)
@@ -204,8 +199,8 @@ PLATFORMS
204
199
  x86_64-linux
205
200
 
206
201
  DEPENDENCIES
207
- aruba (~> 2.2)
208
- cucumber (~> 11.0)
202
+ aruba (~> 3.0)
203
+ cucumber (~> 12.0)
209
204
  donce (~> 0.2)
210
205
  logger (~> 1.7)
211
206
  minitest (~> 6.0)
data/README.md CHANGED
@@ -116,8 +116,9 @@ sibit --proxy=user:password@host:port balance 1PfsYNygsuVL8fvBarJNQnHytkg4rGih1U
116
116
  ```
117
117
 
118
118
  Some providers accept an API key, read from an environment variable:
119
- `SIBIT_BLOCKCHAIR_KEY` for [Blockchair.com] and
120
- `SIBIT_CRYPTOAPIS_KEY` for [Cryptoapis.io].
119
+ `SIBIT_BLOCKCHAIR_KEY` for [Blockchair.com],
120
+ `SIBIT_CRYPTOAPIS_KEY` for [Cryptoapis.io], and
121
+ `SIBIT_SOCHAIN_KEY` for [SoChain].
121
122
  When the variable is set, requests to that provider use the authenticated tier.
122
123
 
123
124
  All operations are performed through the [Blockchain API].
@@ -147,11 +148,10 @@ The library works through one (or a few) public APIs for fetching
147
148
  At the moment we work with the following APIs:
148
149
 
149
150
  * [Blockchain.com] - `Sibit::Blockchain`
150
- * [BTC.com] - `Sibit::Btc`
151
151
  * [Cryptoapis.io] - `Sibit::Cryptoapis`
152
- * [Bitcoinchain.com] - `Sibit::Bitcoinchain`
153
152
  * [Blockchair.com] - `Sibit::Blockchair`
154
153
  * [Cex.io] - `Sibit::Cex`
154
+ * [Mempool.space] - `Sibit::Mempool`
155
155
  * [SoChain] - `Sibit::Sochain`
156
156
 
157
157
  The first one in this list is used by default.
@@ -160,8 +160,8 @@ If you want to use a different one,
160
160
 
161
161
  ```ruby
162
162
  require 'sibit'
163
- require 'sibit/btc'
164
- sibit = Sibit.new(api: Sibit::Btc.new)
163
+ require 'sibit/mempool'
164
+ sibit = Sibit.new(api: Sibit::Mempool.new)
165
165
  ```
166
166
 
167
167
  You may also use a combination of APIs.
@@ -172,12 +172,12 @@ You can provide an array of objects and they are used one by one,
172
172
 
173
173
  ```ruby
174
174
  require 'sibit'
175
- require 'sibit/btc'
176
175
  require 'sibit/cryptoapis'
176
+ require 'sibit/mempool'
177
177
  sibit = Sibit.new(
178
178
  api: Sibit::FirstOf.new(
179
179
  [
180
- Sibit::Btc.new,
180
+ Sibit::Mempool.new,
181
181
  Sibit::Cryptoapis.new('key')
182
182
  ]
183
183
  )
@@ -218,17 +218,16 @@ If it's clean and you don't see any error messages, submit your pull request.
218
218
 
219
219
  [address]: https://en.bitcoin.it/wiki/Address
220
220
  [bitcoin-ruby]: https://github.com/lian/bitcoin-ruby
221
- [Bitcoinchain.com]: https://bitcoinchain.com/api
222
221
  [Bitcointalk]: https://bitcointalk.org/index.php?topic=5130324
223
222
  [Blockchain API]: https://www.blockchain.com/api/blockchain_api
224
223
  [Blockchain.com]: https://www.blockchain.com/explorer
225
224
  [Blockchair.com]: https://blockchair.com/api/docs
226
225
  [blog]: https://www.yegor256.com/2019/05/07/sibit-bitcoin-command-line-client.html
227
- [BTC.com]: https://btc.com/api-doc
228
226
  [Bundler]: https://bundler.io/
229
227
  [Cex.io]: https://cex.io/rest-api
230
- [Cryptoapis.io]: https://docs.cryptoapis.io/rest-apis/blockchain-as-a-service-apis/btc/index
228
+ [Cryptoapis.io]: https://developers.cryptoapis.io/
231
229
  [Electrum]: https://electrum.org/
230
+ [Mempool.space]: https://mempool.space/docs/api/rest
232
231
  [SoChain]: https://sochain.com/api
233
232
  [miner fee]: https://en.bitcoin.it/wiki/Miner_fees
234
233
  [private key]: https://en.bitcoin.it/wiki/Private_key
data/bin/sibit CHANGED
@@ -10,10 +10,8 @@ require 'backtrace'
10
10
  require 'loog'
11
11
  require_relative '../lib/sibit'
12
12
  require_relative '../lib/sibit/bin'
13
- require_relative '../lib/sibit/bitcoinchain'
14
13
  require_relative '../lib/sibit/blockchain'
15
14
  require_relative '../lib/sibit/blockchair'
16
- require_relative '../lib/sibit/btc'
17
15
  require_relative '../lib/sibit/cex'
18
16
  require_relative '../lib/sibit/cryptoapis'
19
17
  require_relative '../lib/sibit/dry'
@@ -21,6 +19,7 @@ require_relative '../lib/sibit/fake'
21
19
  require_relative '../lib/sibit/firstof'
22
20
  require_relative '../lib/sibit/http'
23
21
  require_relative '../lib/sibit/httpproxy'
22
+ require_relative '../lib/sibit/mempool'
24
23
  require_relative '../lib/sibit/sochain'
25
24
  require_relative '../lib/sibit/version'
26
25
 
data/lib/sibit/bin.rb CHANGED
@@ -29,8 +29,9 @@ class Sibit
29
29
  desc: "Don't send a real payment, run in a read-only mode"
30
30
  class_option :verbose, type: :boolean, default: false, desc: 'Print all possible debug messages'
31
31
  class_option :quiet, type: :boolean, default: false, desc: 'Print only informative messages'
32
- class_option :api, type: :array, default: %w[blockchain btc bitcoinchain blockchair cex],
33
- desc: 'Ordered List of APIs to use, e.g. "blockchain,btc,bitcoinchain"'
32
+ class_option :api, type: :array,
33
+ default: %w[blockchain blockchair cex mempool],
34
+ desc: 'Ordered List of APIs to use, e.g. "blockchain,mempool"'
34
35
  class_option :base58, type: :boolean, default: false,
35
36
  desc: 'Use base58 address format instead of bech32'
36
37
 
@@ -145,27 +146,36 @@ class Sibit
145
146
  end
146
147
  end
147
148
 
149
+ def proxy
150
+ addr = options[:proxy] || ENV.fetch('SIBIT_PROXY', nil)
151
+ return if addr.nil?
152
+ if options[:proxy]
153
+ log.debug("Proxy found in the --proxy command line option (#{addr.length} symbols)")
154
+ else
155
+ log.debug("Proxy found in the SIBIT_PROXY env variable (#{addr.length} symbols)")
156
+ end
157
+ addr
158
+ end
159
+
148
160
  def client(dry: false)
149
- proxy = options[:proxy] || ENV.fetch('SIBIT_PROXY', nil)
150
- http = proxy ? Sibit::HttpProxy.new(proxy) : Sibit::Http.new
151
- log.debug("Using proxy at #{http.host}") if proxy
161
+ addr = proxy
162
+ http = addr ? Sibit::HttpProxy.new(addr) : Sibit::Http.new
163
+ log.debug("Using proxy at #{http}") if addr
152
164
  api = Sibit::FirstOf.new(
153
165
  options[:api].flat_map { |a| a.split(',') }.map(&:downcase).map do |a|
154
166
  case a
155
167
  when 'blockchain'
156
168
  Sibit::Blockchain.new(http: http, log: log)
157
- when 'btc'
158
- Sibit::Btc.new(http: http, log: log)
159
- when 'bitcoinchain'
160
- Sibit::Bitcoinchain.new(http: http, log: log)
161
169
  when 'blockchair'
162
170
  Sibit::Blockchair.new(key: ENV.fetch('SIBIT_BLOCKCHAIR_KEY', nil), http: http, log: log)
163
171
  when 'cex'
164
172
  Sibit::Cex.new(http: http, log: log)
165
173
  when 'cryptoapis'
166
174
  Sibit::Cryptoapis.new(ENV.fetch('SIBIT_CRYPTOAPIS_KEY', nil), http: http, log: log)
175
+ when 'mempool'
176
+ Sibit::Mempool.new(http: http, log: log)
167
177
  when 'sochain'
168
- Sibit::Sochain.new(http: http, log: log)
178
+ Sibit::Sochain.new(key: ENV.fetch('SIBIT_SOCHAIN_KEY', nil), http: http, log: log)
169
179
  when 'fake'
170
180
  Sibit::Fake.new
171
181
  else
@@ -13,7 +13,11 @@ require_relative 'http'
13
13
  require_relative 'json'
14
14
  require_relative 'version'
15
15
 
16
- # Cryptoapis.io API.
16
+ # Cryptoapis.io API, version 2.
17
+ #
18
+ # Every request needs a key, which goes into the X-API-Key header.
19
+ #
20
+ # Documentation: https://developers.cryptoapis.io/
17
21
  #
18
22
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
19
23
  # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
@@ -31,15 +35,10 @@ class Sibit::Cryptoapis
31
35
  raise(Sibit::NotSupportedError, 'Cryptoapis doesn\'t provide BTC price')
32
36
  end
33
37
 
34
- # Get hash of the block after this one.
38
+ # Get hash of the block after this one, or NIL if it's the last one in Blockchain.
35
39
  def next_of(hash)
36
- head = Sibit::Json.new(http: @http, log: @log).get(
37
- Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/blocks').append(hash),
38
- headers: headers
39
- )['payload']
40
- raise(Sibit::Error, "The block #{hash} not found") if head.nil?
41
- nxt = head['nextblockhash']
42
- nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
40
+ nxt = item('blocks', 'hash', hash)['nextBlockHash']
41
+ nxt = nil if nxt.nil? || nxt.empty?
43
42
  @log.debug("The block #{hash} is the latest, there is no next block") if nxt.nil?
44
43
  @log.debug("The next block of #{hash} is #{nxt}") unless nxt.nil?
45
44
  nxt
@@ -47,22 +46,17 @@ class Sibit::Cryptoapis
47
46
 
48
47
  # The height of the block.
49
48
  def height(hash)
50
- h = Sibit::Json.new(http: @http, log: @log).get(
51
- Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/blocks').append(hash),
52
- headers: headers
53
- )['payload']['height']
49
+ h = item('blocks', 'hash', hash)['height']
50
+ raise(Sibit::Error, "The block #{hash} found but the height is absent") if h.nil?
54
51
  @log.debug("The height of #{hash} is #{h}")
55
52
  h
56
53
  end
57
54
 
58
- # Gets the balance of the address, in satoshi.
55
+ # Gets the confirmed balance of the address, in satoshi.
59
56
  def balance(address)
60
- b = Sibit::Coins.new(
61
- Sibit::Json.new(http: @http, log: @log).get(
62
- Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/address').append(address),
63
- headers: headers
64
- )['payload']['balance']
65
- ).satoshi
57
+ amount = item('addresses', address, 'balance').dig('confirmedBalance', 'amount')
58
+ raise(Sibit::Error, "The balance of #{address} is absent at Cryptoapis") if amount.nil?
59
+ b = Sibit::Coins.new(amount).satoshi
66
60
  @log.debug("The balance of #{address} is #{b} satoshi")
67
61
  b
68
62
  end
@@ -74,10 +68,8 @@ class Sibit::Cryptoapis
74
68
 
75
69
  # Gets the hash of the latest block.
76
70
  def latest
77
- hash = Sibit::Json.new(http: @http, log: @log).get(
78
- Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/blocks/latest'),
79
- headers: headers
80
- )['payload']['hash']
71
+ hash = item('blocks', 'last')['hash']
72
+ raise(Sibit::Error, 'The latest block hash is absent at Cryptoapis') if hash.nil?
81
73
  @log.debug("The latest block hash is #{hash}")
82
74
  hash
83
75
  end
@@ -90,24 +82,23 @@ class Sibit::Cryptoapis
90
82
  # Push this transaction (in hex format) to the network.
91
83
  def push(hex)
92
84
  Sibit::Json.new(http: @http, log: @log).post(
93
- Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/txs/send'),
94
- JSON.pretty_generate(hex: hex),
85
+ Iri.new('https://rest.cryptoapis.io/blockchain-tools/bitcoin/mainnet/transactions/broadcast'),
86
+ JSON.generate(data: { item: { signedTransactionHex: hex } }),
95
87
  headers: headers.merge('Content-Type' => 'application/json')
96
88
  )
89
+ @log.debug("Transaction (#{hex.length} in hex) has been pushed to Cryptoapis")
97
90
  end
98
91
 
99
92
  # This method should fetch a Blockchain block and return as a hash.
100
93
  def block(hash)
101
- head = Sibit::Json.new(http: @http, log: @log).get(
102
- Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/blocks').append(hash),
103
- headers: headers
104
- )['payload']
94
+ head = item('blocks', 'hash', hash)
95
+ nxt = head['nextBlockHash']
105
96
  {
106
97
  provider: self.class.name,
107
98
  hash: head['hash'],
108
99
  orphan: false,
109
- next: head['nextblockhash'],
110
- previous: head['previousblockhash'],
100
+ next: nxt.nil? || nxt.empty? ? nil : nxt,
101
+ previous: head['previousBlockHash'],
111
102
  txns: txns(hash)
112
103
  }
113
104
  end
@@ -116,35 +107,38 @@ class Sibit::Cryptoapis
116
107
 
117
108
  def headers
118
109
  return {} if @key.nil? || @key.empty?
119
- {
120
- 'X-API-Key': @key
121
- }
110
+ { 'X-API-Key' => @key }
111
+ end
112
+
113
+ def uri(*path)
114
+ path.reduce(Iri.new('https://rest.cryptoapis.io/blockchain-data/bitcoin/mainnet')) { |u, p| u.append(p) }
115
+ end
116
+
117
+ def item(*path)
118
+ i = Sibit::Json.new(http: @http, log: @log)
119
+ .get(uri(*path), headers: headers).dig('data', 'item')
120
+ raise(Sibit::Error, "Cryptoapis returned no item at #{uri(*path)}") if i.nil?
121
+ i
122
122
  end
123
123
 
124
124
  def txns(hash)
125
- index = 0
126
- limit = 200
127
125
  all = []
128
126
  loop do
129
- txns =
130
- Sibit::Json.new(http: @http, log: @log).get(
131
- Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/txs/block/')
132
- .append(hash).add(index: index, limit: limit),
133
- headers: headers
134
- )['payload'].map do |t|
127
+ items = Sibit::Json.new(http: @http, log: @log).get(
128
+ uri('blocks', 'hash', hash, 'transactions').add(limit: 50, offset: all.length),
129
+ headers: headers
130
+ ).dig('data', 'items')
131
+ raise(Sibit::Error, "No transactions of #{hash} at offset #{all.length}") if items.nil?
132
+ all +=
133
+ items.map do |t|
135
134
  {
136
- hash: t['hash'],
137
- outputs: t['txouts'].map do |o|
138
- {
139
- address: o['addresses'][0],
140
- value: Sibit::Coins.new(o['amount'] || 0).satoshi
141
- }
135
+ hash: t['transactionId'],
136
+ outputs: t['blockchainSpecific']['vout'].map do |o|
137
+ { address: o['scriptPubKey']['addresses'][0], value: Sibit::Coins.new(o['value']).satoshi }
142
138
  end
143
139
  }
144
140
  end
145
- all += txns
146
- index += txns.length
147
- break if txns.length < limit
141
+ break if items.length < 50
148
142
  end
149
143
  all
150
144
  end
@@ -19,7 +19,13 @@ class Sibit
19
19
  @user, @password, @host, @port = parse(addr)
20
20
  end
21
21
 
22
- attr_reader :host
22
+ def to_s
23
+ [
24
+ "#{@host}:#{@port}",
25
+ @user ? " as #{@user}" : '',
26
+ @password ? ", password #{'*' * @password.length}" : ''
27
+ ].join
28
+ end
23
29
 
24
30
  def client(uri)
25
31
  http = Net::HTTP.new(uri.host, uri.port, @host, Integer(@port, 10), @user, @password)
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'iri'
7
+ require 'json'
8
+ require 'loog'
9
+ require 'uri'
10
+ require_relative 'error'
11
+ require_relative 'http'
12
+ require_relative 'json'
13
+ require_relative 'version'
14
+
15
+ # Mempool.space API.
16
+ #
17
+ # It works through the open-source Esplora API, which needs no key:
18
+ # https://mempool.space/docs/api/rest
19
+ #
20
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
21
+ # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
22
+ # License:: MIT
23
+ class Sibit::Mempool
24
+ # Constructor.
25
+ def initialize(log: Loog::NULL, http: Sibit::Http.new)
26
+ @http = http
27
+ @log = log
28
+ end
29
+
30
+ # Current price of BTC in the currency (float returned).
31
+ def price(currency = 'USD')
32
+ p = Sibit::Json.new(http: @http, log: @log).get(Iri.new('https://mempool.space/api/v1/prices'))[currency]
33
+ raise(Sibit::Error, "The price of BTC in #{currency} is absent at Mempool") if p.nil?
34
+ @log.debug("The price of BTC is #{p} #{currency}")
35
+ Float(p)
36
+ end
37
+
38
+ # Gets the balance of the address, in satoshi.
39
+ def balance(address)
40
+ stats = Sibit::Json.new(http: @http, log: @log).get(
41
+ Iri.new('https://mempool.space/api/address').append(address)
42
+ )['chain_stats']
43
+ raise(Sibit::Error, "The balance of #{address} is absent at Mempool") if stats.nil?
44
+ b = stats['funded_txo_sum'] - stats['spent_txo_sum']
45
+ @log.debug("The balance of #{address} is #{b} satoshi")
46
+ b
47
+ end
48
+
49
+ # Get hash of the block after this one, or NIL if it's the last one in Blockchain.
50
+ def next_of(hash)
51
+ nxt = status(hash)['next_best']
52
+ @log.debug("At Mempool the block #{hash} is the latest, there is no next block") if nxt.nil?
53
+ @log.debug("The next block of #{hash} is #{nxt}") unless nxt.nil?
54
+ nxt
55
+ end
56
+
57
+ # The height of the block.
58
+ def height(hash)
59
+ h = head(hash)['height']
60
+ raise(Sibit::Error, "The block #{hash} found but the height is absent") if h.nil?
61
+ @log.debug("The height of #{hash} is #{h}")
62
+ h
63
+ end
64
+
65
+ # Get recommended fees, in satoshi per byte.
66
+ def fees
67
+ json = Sibit::Json.new(http: @http, log: @log).get(Iri.new('https://mempool.space/api/v1/fees/recommended'))
68
+ @log.debug(
69
+ "Currently recommended Bitcoin fees at Mempool: #{json['economyFee']}/#{json['hourFee']}/" \
70
+ "#{json['halfHourFee']}/#{json['fastestFee']} sat/vbyte"
71
+ )
72
+ { S: json['economyFee'], M: json['hourFee'], L: json['halfHourFee'], XL: json['fastestFee'] }
73
+ end
74
+
75
+ # Gets the hash of the latest block.
76
+ def latest
77
+ hash = Sibit::Json.new(http: @http, log: @log).get(Iri.new('https://mempool.space/api/blocks'))[0]&.dig('id')
78
+ raise(Sibit::Error, 'The latest block not found at Mempool') if hash.nil?
79
+ @log.debug("The hash of the latest block is #{hash}")
80
+ hash
81
+ end
82
+
83
+ # Fetch all unspent outputs per address.
84
+ def utxos(sources)
85
+ tip = Sibit::Json.new(http: @http, log: @log).get(Iri.new('https://mempool.space/api/blocks/tip/height'))
86
+ sources.flat_map do |address|
87
+ Sibit::Json.new(http: @http, log: @log).get(
88
+ Iri.new('https://mempool.space/api/address').append(address).append('utxo')
89
+ ).map do |u|
90
+ out = Sibit::Json.new(http: @http, log: @log).get(
91
+ Iri.new('https://mempool.space/api/tx').append(u['txid'])
92
+ )['vout'][u['vout']]
93
+ raise(Sibit::Error, "The output #{u['vout']} of #{u['txid']} is absent") if out.nil?
94
+ {
95
+ value: u['value'],
96
+ hash: u['txid'],
97
+ index: u['vout'],
98
+ confirmations: u['status']['confirmed'] ? tip - u['status']['block_height'] + 1 : 0,
99
+ script: [out['scriptpubkey']].pack('H*')
100
+ }
101
+ end
102
+ end
103
+ end
104
+
105
+ # Push this transaction (in hex format) to the network.
106
+ def push(hex)
107
+ Sibit::Json.new(http: @http, log: @log).post(
108
+ Iri.new('https://mempool.space/api/tx'), hex, headers: { 'Content-Type' => 'text/plain' }
109
+ )
110
+ @log.debug("Transaction (#{hex.length} in hex) has been pushed to Mempool")
111
+ end
112
+
113
+ # This method should fetch a Blockchain block and return as a hash.
114
+ def block(hash)
115
+ json = head(hash)
116
+ s = status(hash)
117
+ {
118
+ provider: self.class.name,
119
+ hash: json['id'],
120
+ orphan: !s['in_best_chain'],
121
+ next: s['next_best'],
122
+ previous: json['previousblockhash'],
123
+ txns: (0...json['tx_count']).step(25).flat_map do |start|
124
+ Sibit::Json.new(http: @http, log: @log).get(
125
+ Iri.new('https://mempool.space/api/block').append(hash).append('txs').append(start.to_s)
126
+ ).map do |t|
127
+ {
128
+ hash: t['txid'],
129
+ outputs: t['vout'].map { |o| { address: o['scriptpubkey_address'], value: o['value'] } }
130
+ }
131
+ end
132
+ end
133
+ }
134
+ end
135
+
136
+ private
137
+
138
+ def head(hash)
139
+ Sibit::Json.new(http: @http, log: @log).get(Iri.new('https://mempool.space/api/block').append(hash))
140
+ end
141
+
142
+ def status(hash)
143
+ Sibit::Json.new(http: @http, log: @log).get(
144
+ Iri.new('https://mempool.space/api/block').append(hash).append('status')
145
+ )
146
+ end
147
+ end
data/lib/sibit/sochain.rb CHANGED
@@ -7,12 +7,17 @@ require 'iri'
7
7
  require 'json'
8
8
  require 'loog'
9
9
  require 'uri'
10
+ require_relative 'coins'
10
11
  require_relative 'error'
11
12
  require_relative 'http'
12
13
  require_relative 'json'
13
14
  require_relative 'version'
14
15
 
15
- # SoChain API (formerly known as Block.io).
16
+ # SoChain API v3 (formerly known as Block.io).
17
+ #
18
+ # It works without a key, within a daily limit per IP address,
19
+ # while a key lifts that limit. API v3 has no way to broadcast a
20
+ # transaction and lists only bare transaction hashes in a block.
16
21
  #
17
22
  # Documentation: https://sochain.com/api
18
23
  #
@@ -21,67 +26,42 @@ require_relative 'version'
21
26
  # License:: MIT
22
27
  class Sibit::Sochain
23
28
  # Constructor.
24
- def initialize(log: Loog::NULL, http: Sibit::Http.new, network: 'BTC')
29
+ def initialize(key: nil, log: Loog::NULL, http: Sibit::Http.new, network: 'BTC')
30
+ @key = key
25
31
  @http = http
26
32
  @log = log
27
33
  @network = network
28
34
  end
29
35
 
30
- # Current price of BTC in USD (float returned).
36
+ # Current price of BTC in the currency (float returned).
31
37
  def price(currency = 'USD')
32
- data = Sibit::Json.new(http: @http, log: @log).get(
33
- Iri.new('https://sochain.com/api/v2/get_price').append(@network).append(currency)
34
- )['data']
35
- raise(Sibit::Error, "No price data returned for #{@network}/#{currency}") if data.nil?
36
- prices = data['prices']
37
- if prices.nil? || prices.empty?
38
- raise(Sibit::Error, "No price quotes for #{@network}/#{currency}")
39
- end
40
- price = Float(prices[0]['price'])
41
- @log.debug("The price of #{@network} is #{price} #{currency}")
42
- price
38
+ p = data('price', currency: currency)['value']
39
+ raise(Sibit::Error, "No price of #{@network} in #{currency} at SoChain") if p.nil?
40
+ @log.debug("The price of #{@network} is #{p} #{currency}")
41
+ Float(p)
43
42
  end
44
43
 
45
44
  # The height of the block, identified by hash.
46
45
  def height(hash)
47
- data = Sibit::Json.new(http: @http, log: @log).get(
48
- Iri.new('https://sochain.com/api/v2/block').append(@network).append(hash)
49
- )['data']
50
- raise(Sibit::Error, "The block #{hash} not found") if data.nil?
51
- h = data['block_no']
46
+ h = data('block', hash)['height']
52
47
  raise(Sibit::Error, "The block #{hash} found but the height is absent") if h.nil?
53
48
  @log.debug("The height of #{hash} is #{h}")
54
49
  Integer(h)
55
50
  end
56
51
 
57
- # Get hash of the block after this one.
52
+ # Get hash of the block after this one, or NIL if it's the last one in Blockchain.
58
53
  def next_of(hash)
59
- data = Sibit::Json.new(http: @http, log: @log).get(
60
- Iri.new('https://sochain.com/api/v2/block').append(@network).append(hash)
61
- )['data']
62
- raise(Sibit::Error, "The block #{hash} not found") if data.nil?
63
- nxt = data['next_blockhash']
64
- nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
54
+ nxt = data('block', hash)['next_block_hash']
65
55
  @log.debug("In SoChain the block #{hash} is the latest, there is no next block") if nxt.nil?
66
56
  @log.debug("The next block of #{hash} is #{nxt}") unless nxt.nil?
67
57
  nxt
68
58
  end
69
59
 
70
- # Gets the balance of the address, in satoshi.
60
+ # Gets the confirmed balance of the address, in satoshi.
71
61
  def balance(address)
72
- data = Sibit::Json.new(http: @http, log: @log).get(
73
- Iri.new('https://sochain.com/api/v2/get_address_balance').append(@network).append(address)
74
- )['data']
75
- if data.nil?
76
- @log.debug("The balance of #{address} is probably zero (not found)")
77
- return 0
78
- end
79
- confirmed = data['confirmed_balance']
80
- if confirmed.nil?
81
- @log.debug("The balance of #{address} is probably zero (no confirmed_balance)")
82
- return 0
83
- end
84
- b = Integer((Float(confirmed) * 100_000_000).round)
62
+ confirmed = data('balance', address)['confirmed']
63
+ raise(Sibit::Error, "The balance of #{address} is absent at SoChain") if confirmed.nil?
64
+ b = Sibit::Coins.new(confirmed).satoshi
85
65
  @log.debug("The balance of #{address} is #{b} satoshi")
86
66
  b
87
67
  end
@@ -93,12 +73,8 @@ class Sibit::Sochain
93
73
 
94
74
  # Gets the hash of the latest block.
95
75
  def latest
96
- data = Sibit::Json.new(http: @http, log: @log).get(
97
- Iri.new('https://sochain.com/api/v2/get_info').append(@network)
98
- )['data']
99
- raise(Sibit::Error, 'The latest block info not found') if data.nil?
100
- hash = data['blockhash']
101
- raise(Sibit::Error, 'The latest block hash is absent') if hash.nil?
76
+ hash = data('best_block_hash')['hash']
77
+ raise(Sibit::Error, 'The latest block hash is absent at SoChain') if hash.nil?
102
78
  @log.debug("The latest block hash is #{hash}")
103
79
  hash
104
80
  end
@@ -106,75 +82,49 @@ class Sibit::Sochain
106
82
  # Fetch all unspent outputs per address. The argument is an array
107
83
  # of Bitcoin addresses.
108
84
  def utxos(sources)
109
- out = []
110
- sources.each do |address|
111
- data = Sibit::Json.new(http: @http, log: @log).get(
112
- Iri.new('https://sochain.com/api/v2/get_tx_unspent').append(@network).append(address)
113
- )['data']
114
- raise(Sibit::Error, "The address #{address} unspent lookup returned no data") if data.nil?
115
- txs = data['txs']
116
- raise(Sibit::Error, "The address #{address} unspent lookup returned no txs") if txs.nil?
117
- txs.each do |u|
118
- out << {
119
- value: Integer((Float(u['value']) * 100_000_000).round),
120
- hash: u['txid'],
121
- index: u['output_no'],
122
- confirmations: u['confirmations'],
123
- script: [u['script_hex']].pack('H*')
124
- }
85
+ tip = Integer(data('network_info')['block_count'])
86
+ sources.flat_map do |address|
87
+ all = []
88
+ page = 1
89
+ loop do
90
+ outputs = data('unspent_outputs', address, page.to_s)['outputs']
91
+ raise(Sibit::Error, "The unspent outputs of #{address} are absent") if outputs.nil?
92
+ all +=
93
+ outputs.map do |u|
94
+ {
95
+ value: Sibit::Coins.new(u['value']).satoshi,
96
+ hash: u['hash'],
97
+ index: u['index'],
98
+ confirmations: u['block'].nil? ? 0 : tip - u['block'] + 1,
99
+ script: [u['script']].pack('H*')
100
+ }
101
+ end
102
+ break if outputs.length < 10
103
+ page += 1
125
104
  end
105
+ all
126
106
  end
127
- out
128
107
  end
129
108
 
130
- # Push this transaction (in hex format) to the network. SoChain expects a
131
- # JSON payload with the +tx_hex+ field on POST /send_tx/{network}.
132
- def push(hex)
133
- uri = URI(Iri.new('https://sochain.com/api/v2/send_tx').append(@network).to_s)
134
- res = @http.client(uri).post(
135
- uri.path,
136
- JSON.generate(tx_hex: hex),
137
- {
138
- 'Accept' => 'application/json',
139
- 'Content-Type' => 'application/json',
140
- 'Accept-Charset' => 'UTF-8',
141
- 'Accept-Encoding' => ''
142
- }
143
- )
144
- unless res.code == '200'
145
- raise(Sibit::Error, "Failed to push transaction to #{uri}: #{res.code}\n#{res.body}")
146
- end
147
- @log.debug("Transaction (#{hex.length} chars in hex) has been pushed to SoChain")
109
+ # Push this transaction (in hex format) to the network.
110
+ def push(_hex)
111
+ raise(Sibit::NotSupportedError, 'SoChain API v3 doesn\'t broadcast transactions')
148
112
  end
149
113
 
150
114
  # This method should fetch a block and return it as a hash.
151
- def block(hash)
152
- data = Sibit::Json.new(http: @http, log: @log).get(
153
- Iri.new('https://sochain.com/api/v2/block').append(@network).append(hash)
115
+ def block(_hash)
116
+ raise(Sibit::NotSupportedError, 'SoChain API v3 doesn\'t list outputs of block transactions')
117
+ end
118
+
119
+ private
120
+
121
+ def data(method, *path, **query)
122
+ uri = path.reduce(Iri.new('https://sochain.com/api/v3').append(method).append(@network)) { |u, p| u.append(p) }
123
+ uri = uri.add(**query) unless query.empty?
124
+ d = Sibit::Json.new(http: @http, log: @log).get(
125
+ uri, headers: @key.nil? ? {} : { 'API-KEY' => @key }
154
126
  )['data']
155
- raise(Sibit::Error, "The block #{hash} not found") if data.nil?
156
- nxt = data['next_blockhash']
157
- nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
158
- {
159
- provider: self.class.name,
160
- hash: data['blockhash'],
161
- orphan: data['is_orphan'] == true,
162
- next: nxt,
163
- previous: data['previous_blockhash'],
164
- txns: (data['txs'] || []).map do |t|
165
- unless t.is_a?(Hash)
166
- raise(Sibit::NotSupportedError, "SoChain returned bare txid #{t} without outputs")
167
- end
168
- {
169
- hash: t['txid'],
170
- outputs: (t['outputs'] || []).map do |o|
171
- {
172
- address: o['address'],
173
- value: Integer((Float(o['value']) * 100_000_000).round)
174
- }
175
- end
176
- }
177
- end
178
- }
127
+ raise(Sibit::Error, "SoChain returned no data at #{uri}") if d.nil?
128
+ d
179
129
  end
180
130
  end
data/lib/sibit/version.rb CHANGED
@@ -8,5 +8,5 @@
8
8
  # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
9
9
  # License:: MIT
10
10
  class Sibit
11
- VERSION = '0.34.2' unless defined?(VERSION)
11
+ VERSION = '0.35.0' unless defined?(VERSION)
12
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sibit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.34.2
4
+ version: 0.35.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -187,10 +187,8 @@ files:
187
187
  - lib/sibit/bech32.rb
188
188
  - lib/sibit/bestof.rb
189
189
  - lib/sibit/bin.rb
190
- - lib/sibit/bitcoinchain.rb
191
190
  - lib/sibit/blockchain.rb
192
191
  - lib/sibit/blockchair.rb
193
- - lib/sibit/btc.rb
194
192
  - lib/sibit/cex.rb
195
193
  - lib/sibit/coins.rb
196
194
  - lib/sibit/cryptoapis.rb
@@ -202,6 +200,7 @@ files:
202
200
  - lib/sibit/httpproxy.rb
203
201
  - lib/sibit/json.rb
204
202
  - lib/sibit/key.rb
203
+ - lib/sibit/mempool.rb
205
204
  - lib/sibit/script.rb
206
205
  - lib/sibit/sochain.rb
207
206
  - lib/sibit/tx.rb
@@ -1,122 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
- # SPDX-License-Identifier: MIT
5
-
6
- require 'iri'
7
- require 'json'
8
- require 'loog'
9
- require 'uri'
10
- require_relative 'coins'
11
- require_relative 'error'
12
- require_relative 'http'
13
- require_relative 'json'
14
- require_relative 'version'
15
-
16
- # Bitcoinchain.com API.
17
- #
18
- # Author:: Yegor Bugayenko (yegor256@gmail.com)
19
- # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
20
- # License:: MIT
21
- class Sibit::Bitcoinchain
22
- # Constructor.
23
- def initialize(log: Loog::NULL, http: Sibit::Http.new)
24
- @http = http
25
- @log = log
26
- end
27
-
28
- # Current price of BTC in USD (float returned).
29
- def price(_currency = 'USD')
30
- raise(Sibit::NotSupportedError, 'Bitcoinchain API doesn\'t provide BTC price')
31
- end
32
-
33
- # The height of the block.
34
- def height(_hash)
35
- raise(Sibit::NotSupportedError, 'Bitcoinchain API doesn\'t provide height()')
36
- end
37
-
38
- # Get hash of the block after this one.
39
- def next_of(hash)
40
- block = Sibit::Json.new(http: @http, log: @log).get(
41
- Iri.new('https://api-r.bitcoinchain.com/v1/block').append(hash)
42
- )[0]
43
- raise(Sibit::Error, "Block #{hash} not found") if block.nil?
44
- nxt = block['next_block']
45
- nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
46
- @log.debug("The block #{hash} is the latest, there is no next block") if nxt.nil?
47
- @log.debug("The next block of #{hash} is #{nxt}") unless nxt.nil?
48
- nxt
49
- end
50
-
51
- # Gets the balance of the address, in satoshi.
52
- def balance(address)
53
- json = Sibit::Json.new(http: @http, log: @log).get(
54
- Iri.new('https://api-r.bitcoinchain.com/v1/address').append(address),
55
- accept: [200, 409]
56
- )[0]
57
- b = json['balance']
58
- if b.nil?
59
- @log.debug("The balance of #{address} is not visible")
60
- return 0
61
- end
62
- b = Sibit::Coins.new(b).satoshi
63
- @log.debug("The balance of #{address} is #{b} satoshi (#{json['transactions']} txns)")
64
- b
65
- end
66
-
67
- # Get recommended fees, in satoshi per byte.
68
- def fees
69
- raise(Sibit::NotSupportedError, 'Not implemented yet')
70
- end
71
-
72
- # Gets the hash of the latest block.
73
- def latest
74
- hash = Sibit::Json.new(http: @http, log: @log).get(
75
- Iri.new('https://api-r.bitcoinchain.com/v1/status')
76
- )['hash']
77
- @log.debug("The latest block hash is #{hash}")
78
- hash
79
- end
80
-
81
- # Fetch all unspent outputs per address.
82
- def utxos(_sources)
83
- raise(Sibit::NotSupportedError, 'Not implemented yet')
84
- end
85
-
86
- # Push this transaction (in hex format) to the network.
87
- def push(_hex)
88
- raise(Sibit::NotSupportedError, 'Not implemented yet')
89
- end
90
-
91
- # This method should fetch a Blockchain block and return as a hash. Raises
92
- # an exception if the block is not found.
93
- def block(hash)
94
- head = Sibit::Json.new(http: @http, log: @log).get(
95
- Iri.new('https://api-r.bitcoinchain.com/v1/block').append(hash)
96
- )[0]
97
- raise(Sibit::Error, "The block #{hash} is not found") if head.nil?
98
- txs = Sibit::Json.new(http: @http, log: @log).get(
99
- Iri.new('https://api-r.bitcoinchain.com/v1/block/txs').append(hash)
100
- )
101
- nxt = head['next_block']
102
- nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
103
- {
104
- provider: self.class.name,
105
- hash: head['hash'],
106
- orphan: !head['is_main'],
107
- next: nxt,
108
- previous: head['prev_block'],
109
- txns: txs[0]['txs'].map do |t|
110
- {
111
- hash: t['self_hash'],
112
- outputs: t['outputs'].map do |o|
113
- {
114
- address: o['receiver'],
115
- value: Sibit::Coins.new(o['value']).satoshi
116
- }
117
- end
118
- }
119
- end
120
- }
121
- end
122
- end
data/lib/sibit/btc.rb DELETED
@@ -1,176 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
- # SPDX-License-Identifier: MIT
5
-
6
- require 'iri'
7
- require 'json'
8
- require 'loog'
9
- require 'uri'
10
- require_relative 'error'
11
- require_relative 'http'
12
- require_relative 'json'
13
- require_relative 'version'
14
-
15
- # Btc.com API.
16
- #
17
- # Here: https://btc.com/api-doc
18
- #
19
- # Author:: Yegor Bugayenko (yegor256@gmail.com)
20
- # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
21
- # License:: MIT
22
- class Sibit::Btc
23
- # Constructor.
24
- def initialize(log: Loog::NULL, http: Sibit::Http.new)
25
- @http = http
26
- @log = log
27
- end
28
-
29
- # Current price of BTC in USD (float returned).
30
- def price(_currency = 'USD')
31
- raise(Sibit::NotSupportedError, 'Btc.com API doesn\'t provide prices')
32
- end
33
-
34
- # Gets the balance of the address, in satoshi.
35
- def balance(address)
36
- json = Sibit::Json.new(http: @http, log: @log).get(Iri.new('https://chain.api.btc.com/v3/address').append(address).append('unspent'))
37
- if json['err_no'] == 1
38
- @log.debug("The balance of #{address} is zero (not found)")
39
- return 0
40
- end
41
- data = json['data']
42
- raise(Sibit::Error, "The address #{address} not found") if data.nil?
43
- txns = data['list']
44
- if txns.nil?
45
- @log.debug("The balance of #{address} is probably zero (not found)")
46
- return 0
47
- end
48
- balance = txns.sum { |tx| tx['value'] } || 0
49
- @log.debug("The balance of #{address} is #{balance}, total txns: #{txns.count}")
50
- balance
51
- end
52
-
53
- # Get hash of the block after this one, or NIL if it's the last one in Blockchain.
54
- def next_of(hash)
55
- data = Sibit::Json.new(http: @http, log: @log).get(
56
- Iri.new('https://chain.api.btc.com/v3/block').append(hash)
57
- )['data']
58
- raise(Sibit::Error, "The block #{hash} not found") if data.nil?
59
- nxt = data['next_block_hash']
60
- nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
61
- @log.debug("In BTC.com the block #{hash} is the latest, there is no next block") if nxt.nil?
62
- @log.debug("The next block of #{hash} is #{nxt}") unless nxt.nil?
63
- nxt
64
- end
65
-
66
- # The height of the block.
67
- def height(hash)
68
- data = Sibit::Json.new(http: @http, log: @log).get(
69
- Iri.new('https://chain.api.btc.com/v3/block').append(hash)
70
- )['data']
71
- raise(Sibit::Error, "The block #{hash} not found") if data.nil?
72
- h = data['height']
73
- raise(Sibit::Error, "The block #{hash} found but the height is absent") if h.nil?
74
- @log.debug("The height of #{hash} is #{h}")
75
- h
76
- end
77
-
78
- # Get recommended fees, in satoshi per byte.
79
- def fees
80
- raise(Sibit::NotSupportedError, 'Btc.com doesn\'t provide recommended fees')
81
- end
82
-
83
- # Gets the hash of the latest block.
84
- def latest
85
- data = Sibit::Json.new(http: @http, log: @log).get(Iri.new('https://chain.api.btc.com/v3/block/latest'))['data']
86
- raise(Sibit::Error, 'The latest block not found') if data.nil?
87
- hash = data['hash']
88
- @log.debug("The hash of the latest block is #{hash}")
89
- hash
90
- end
91
-
92
- # Fetch all unspent outputs per address.
93
- def utxos(sources)
94
- results = []
95
- sources.each do |hash|
96
- data = Sibit::Json.new(http: @http, log: @log).get(
97
- Iri.new('https://chain.api.btc.com/v3/address').append(hash).append('unspent')
98
- )['data']
99
- raise(Sibit::Error, "The address #{hash} not found") if data.nil?
100
- list = data['list']
101
- next if list.nil?
102
- list.each do |u|
103
- index = u['tx_output_n']
104
- raise(Sibit::Error, "The unspent output of #{hash} has no tx_output_n") if index.nil?
105
- out = Sibit::Json.new(http: @http, log: @log).get(
106
- Iri.new('https://chain.api.btc.com/v3/tx').append(u['tx_hash']).add(verbose: 3)
107
- )['data']['outputs'][index]
108
- raise(Sibit::Error, "The output #{index} of #{u['tx_hash']} is absent") if out.nil?
109
- results << {
110
- value: u['value'],
111
- hash: u['tx_hash'],
112
- index: index,
113
- confirmations: u['confirmations'],
114
- script: [out['script_hex']].pack('H*')
115
- }
116
- end
117
- end
118
- results
119
- end
120
-
121
- # Push this transaction (in hex format) to the network.
122
- def push(_hex)
123
- raise(Sibit::NotSupportedError, 'Btc.com doesn\'t provide payment gateway')
124
- end
125
-
126
- # This method should fetch a Blockchain block and return as a hash.
127
- def block(hash)
128
- data = Sibit::Json.new(http: @http, log: @log).get(
129
- Iri.new('https://chain.api.btc.com/v3/block').append(hash)
130
- )['data']
131
- raise(Sibit::Error, "The block #{hash} not found") if data.nil?
132
- nxt = data['next_block_hash']
133
- nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
134
- {
135
- provider: self.class.name,
136
- hash: data['hash'],
137
- orphan: data['is_orphan'],
138
- next: nxt,
139
- previous: data['prev_block_hash'],
140
- txns: txns(hash)
141
- }
142
- end
143
-
144
- private
145
-
146
- def txns(hash)
147
- page = 1
148
- psize = 50
149
- all = []
150
- loop do
151
- data = Sibit::Json.new(http: @http, log: @log).get(
152
- Iri.new('https://chain.api.btc.com/v3/block')
153
- .append(hash).append('tx').add(page: page, pagesize: psize)
154
- )['data']
155
- raise(Sibit::Error, "The block #{hash} has no data at page #{page}") if data.nil?
156
- list = data['list']
157
- raise(Sibit::Error, "The list is empty for block #{hash} at page #{page}") if list.nil?
158
- txns =
159
- list.map do |t|
160
- {
161
- hash: t['hash'],
162
- outputs: t['outputs'].reject { |o| o['spent_by_tx'] }.map do |o|
163
- {
164
- address: o['addresses'][0],
165
- value: o['value']
166
- }
167
- end
168
- }
169
- end
170
- all += txns
171
- page += 1
172
- break if txns.length < psize
173
- end
174
- all
175
- end
176
- end