sibit 0.34.3 → 0.35.1
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 +4 -4
- data/Gemfile +2 -2
- data/Gemfile.lock +49 -54
- data/README.md +10 -11
- data/bin/sibit +1 -2
- data/lib/sibit/bestof.rb +23 -6
- data/lib/sibit/bin.rb +6 -7
- data/lib/sibit/cryptoapis.rb +46 -52
- data/lib/sibit/mempool.rb +147 -0
- data/lib/sibit/sochain.rb +58 -108
- data/lib/sibit/version.rb +1 -1
- metadata +2 -3
- data/lib/sibit/bitcoinchain.rb +0 -122
- data/lib/sibit/btc.rb +0 -176
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d545c42be046368aee30cdc1baab63944b45c97b8b68b4f9fabe6b44ca036d15
|
|
4
|
+
data.tar.gz: b0dcc6193397326031efdf8089f15682946083f6dac8e221ed8a3f3f18f37abf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 78728c5d74334bd3c02f07bf6c33139d13f98d01f48ecb81790f5901d27106d10e6e069c5e7a779f3b01df73e22b9c2b7e435f0d5039734ec811b435c1a111c0
|
|
7
|
+
data.tar.gz: 32c68a1e19d52ec99aae38c165d8b501d210315fcd843a789c351c2efbba2516ff69c80efcba022a5ab59edad4ef23a69f668351ba640f32a568b505b9facab2
|
data/Gemfile
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
source 'https://rubygems.org'
|
|
7
7
|
gemspec
|
|
8
8
|
|
|
9
|
-
gem 'aruba', '~>
|
|
10
|
-
gem 'cucumber', '~>
|
|
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 (
|
|
23
|
-
bundler (>=
|
|
22
|
+
aruba (3.0.1)
|
|
23
|
+
bundler (>= 2.4)
|
|
24
24
|
contracts (>= 0.16.0, < 0.18.0)
|
|
25
|
-
cucumber (>=
|
|
26
|
-
|
|
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.
|
|
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 (
|
|
39
|
+
cucumber (12.0.1)
|
|
39
40
|
base64 (~> 0.2)
|
|
40
41
|
builder (~> 3.2)
|
|
41
|
-
cucumber-ci-environment (> 9, <
|
|
42
|
-
cucumber-core (>=
|
|
43
|
-
cucumber-cucumber-expressions (> 17, <
|
|
44
|
-
cucumber-html-formatter (> 21, <
|
|
45
|
-
|
|
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 (
|
|
50
|
+
multi_test (> 1, < 4)
|
|
49
51
|
sys-uname (~> 1.5)
|
|
50
|
-
cucumber-ci-environment (
|
|
51
|
-
cucumber-core (
|
|
52
|
-
cucumber-gherkin (> 36, <
|
|
53
|
-
cucumber-messages (> 31, <
|
|
54
|
-
cucumber-tag-expressions (> 6, <
|
|
55
|
-
cucumber-cucumber-expressions (
|
|
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 (
|
|
58
|
-
cucumber-messages (>=
|
|
59
|
-
cucumber-html-formatter (
|
|
60
|
-
cucumber-messages (> 23, <
|
|
61
|
-
cucumber-messages (
|
|
62
|
-
cucumber-
|
|
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,16 +76,10 @@ GEM
|
|
|
72
76
|
loog (~> 0.6)
|
|
73
77
|
tago (~> 0.1)
|
|
74
78
|
ellipsized (0.3.0)
|
|
75
|
-
erb (6.0.
|
|
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
84
|
json (2.21.2)
|
|
87
85
|
language_server-protocol (3.17.0.6)
|
|
@@ -90,6 +88,7 @@ GEM
|
|
|
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 (
|
|
104
|
-
nokogiri (1.19.
|
|
102
|
+
multi_test (2.0.0)
|
|
103
|
+
nokogiri (1.19.7-arm64-darwin)
|
|
105
104
|
racc (~> 1.4)
|
|
106
|
-
nokogiri (1.19.
|
|
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.
|
|
111
|
-
parser (3.3.
|
|
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
|
|
123
|
+
rbs (4.2.0)
|
|
128
124
|
logger
|
|
129
125
|
prism (>= 1.6.0)
|
|
130
126
|
tsort
|
|
131
|
-
rdoc (8.
|
|
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.
|
|
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,14 +138,14 @@ 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.
|
|
141
|
+
rubocop (1.92.2)
|
|
148
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.
|
|
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)
|
|
@@ -161,9 +155,9 @@ GEM
|
|
|
161
155
|
rubocop-elegant (0.8.0)
|
|
162
156
|
lint_roller (~> 1.1)
|
|
163
157
|
rubocop (~> 1.75)
|
|
164
|
-
rubocop-minitest (0.
|
|
158
|
+
rubocop-minitest (0.41.0)
|
|
165
159
|
lint_roller (~> 1.1)
|
|
166
|
-
rubocop (>= 1.
|
|
160
|
+
rubocop (>= 1.89.0, < 2.0)
|
|
167
161
|
rubocop-ast (>= 1.38.0, < 2.0)
|
|
168
162
|
rubocop-performance (1.27.0)
|
|
169
163
|
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.
|
|
192
|
-
unicode-emoji (~> 4.
|
|
193
|
-
unicode-emoji (4.
|
|
194
|
-
webmock (3.
|
|
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 (~>
|
|
208
|
-
cucumber (~>
|
|
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]
|
|
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/
|
|
164
|
-
sibit = Sibit.new(api: Sibit::
|
|
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::
|
|
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://
|
|
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/bestof.rb
CHANGED
|
@@ -20,11 +20,12 @@ class Sibit::BestOf
|
|
|
20
20
|
@verbose = verbose
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
# Current price of BTC in USD (float returned).
|
|
23
|
+
# Current price of BTC in USD (float returned). Exchanges never quote
|
|
24
|
+
# the exact same number, so this one does not vote: it averages all
|
|
25
|
+
# answers and fails when they are more than five percent apart.
|
|
24
26
|
def price(currency = 'USD')
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
end
|
|
27
|
+
return @list.price(currency) unless @list.is_a?(Array)
|
|
28
|
+
average('price', answers('price') { |api| api.price(currency) })
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
# Gets the balance of the address, in satoshi.
|
|
@@ -97,8 +98,12 @@ class Sibit::BestOf
|
|
|
97
98
|
|
|
98
99
|
private
|
|
99
100
|
|
|
100
|
-
def best_of(method)
|
|
101
|
+
def best_of(method, &blk)
|
|
101
102
|
return yield(@list) unless @list.is_a?(Array)
|
|
103
|
+
majority(method, answers(method, &blk))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def answers(method)
|
|
102
107
|
votes = []
|
|
103
108
|
errors = []
|
|
104
109
|
@list.each do |api|
|
|
@@ -117,7 +122,19 @@ class Sibit::BestOf
|
|
|
117
122
|
"#{@list.map { |a| a.class.name }.join(', ')}"
|
|
118
123
|
)
|
|
119
124
|
end
|
|
120
|
-
|
|
125
|
+
votes
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def average(method, votes)
|
|
129
|
+
prices = votes.map { |v| v[1] }
|
|
130
|
+
if (prices.max - prices.min) / prices.min > 0.05
|
|
131
|
+
raise(
|
|
132
|
+
Sibit::Error,
|
|
133
|
+
"The answers at #{method}() are more than 5% apart: " \
|
|
134
|
+
"#{votes.map { |name, answer| "#{name}=#{answer}" }.join(', ')}"
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
prices.sum / prices.size
|
|
121
138
|
end
|
|
122
139
|
|
|
123
140
|
def majority(method, votes)
|
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,
|
|
33
|
-
|
|
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
|
|
|
@@ -165,18 +166,16 @@ class Sibit
|
|
|
165
166
|
case a
|
|
166
167
|
when 'blockchain'
|
|
167
168
|
Sibit::Blockchain.new(http: http, log: log)
|
|
168
|
-
when 'btc'
|
|
169
|
-
Sibit::Btc.new(http: http, log: log)
|
|
170
|
-
when 'bitcoinchain'
|
|
171
|
-
Sibit::Bitcoinchain.new(http: http, log: log)
|
|
172
169
|
when 'blockchair'
|
|
173
170
|
Sibit::Blockchair.new(key: ENV.fetch('SIBIT_BLOCKCHAIR_KEY', nil), http: http, log: log)
|
|
174
171
|
when 'cex'
|
|
175
172
|
Sibit::Cex.new(http: http, log: log)
|
|
176
173
|
when 'cryptoapis'
|
|
177
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)
|
|
178
177
|
when 'sochain'
|
|
179
|
-
Sibit::Sochain.new(http: http, log: log)
|
|
178
|
+
Sibit::Sochain.new(key: ENV.fetch('SIBIT_SOCHAIN_KEY', nil), http: http, log: log)
|
|
180
179
|
when 'fake'
|
|
181
180
|
Sibit::Fake.new
|
|
182
181
|
else
|
data/lib/sibit/cryptoapis.rb
CHANGED
|
@@ -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
|
-
|
|
37
|
-
|
|
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 =
|
|
51
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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 =
|
|
78
|
-
|
|
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://
|
|
94
|
-
JSON.
|
|
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 =
|
|
102
|
-
|
|
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:
|
|
110
|
-
previous: head['
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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['
|
|
137
|
-
outputs: t['
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
36
|
+
# Current price of BTC in the currency (float returned).
|
|
31
37
|
def price(currency = 'USD')
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
110
|
-
sources.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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.
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
152
|
-
|
|
153
|
-
|
|
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, "
|
|
156
|
-
|
|
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
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.
|
|
4
|
+
version: 0.35.1
|
|
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
|
data/lib/sibit/bitcoinchain.rb
DELETED
|
@@ -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
|