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