sibit 0.14.2 → 0.14.3
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/bin/sibit +8 -1
- data/lib/sibit/bitcoinchain.rb +10 -5
- data/lib/sibit/blockchain.rb +22 -141
- data/lib/sibit/btc.rb +10 -5
- data/lib/sibit/earn.rb +91 -0
- data/lib/sibit/fake.rb +6 -2
- data/lib/sibit/version.rb +1 -1
- data/lib/sibit.rb +98 -2
- data/test/test_fake.rb +1 -4
- data/test/test_sibit.rb +5 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3746b345c4e7b36083c7a2377e4a5449532116978e38f964ac558046670fb5d
|
|
4
|
+
data.tar.gz: 480071555f2cc5e4b15a81274b526fb74a75c223d7b98a98226a134dd87e0e03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '097f3d44a66b8ef449e26429ec068bc8395258d80a2e0d5f520f9af24f5e0323d1f83f2f41e691d9daee570fb9527288c4b721ffdfa43762eafd1ef8c62d8095'
|
|
7
|
+
data.tar.gz: 8e9d7c89c01e6d537d159e16af0facdffbee6ea1ed4c16104a1c1fd33e4342b0a0a44bc7b3b5b2b5b45e11fa0fd222b4ac8e54aa4de6a75e416e4837540bc796
|
data/bin/sibit
CHANGED
|
@@ -31,6 +31,7 @@ require_relative '../lib/sibit/version'
|
|
|
31
31
|
require_relative '../lib/sibit/blockchain'
|
|
32
32
|
require_relative '../lib/sibit/btc'
|
|
33
33
|
require_relative '../lib/sibit/bitcoinchain'
|
|
34
|
+
require_relative '../lib/sibit/earn'
|
|
34
35
|
|
|
35
36
|
begin
|
|
36
37
|
begin
|
|
@@ -57,7 +58,11 @@ Options are:"
|
|
|
57
58
|
exit
|
|
58
59
|
end
|
|
59
60
|
o.bool '--verbose', 'Print all possible debug messages'
|
|
60
|
-
o.array
|
|
61
|
+
o.array(
|
|
62
|
+
'--api',
|
|
63
|
+
'Ordered List of APIs to use, e.g. "earn,blockchain,btc,bitcoinchain"',
|
|
64
|
+
default: %w[earn blockchain btc bitcoinchain]
|
|
65
|
+
)
|
|
61
66
|
end
|
|
62
67
|
rescue Slop::Error => ex
|
|
63
68
|
raise ex.message
|
|
@@ -73,6 +78,8 @@ Options are:"
|
|
|
73
78
|
api = Sibit::Btc.new(http: http, log: log, dry: opts[:dry])
|
|
74
79
|
elsif a == 'bitcoinchain'
|
|
75
80
|
api = Sibit::Bitcoinchain.new(http: http, log: log, dry: opts[:dry])
|
|
81
|
+
elsif a == 'earn'
|
|
82
|
+
api = Sibit::Earn.new(http: http, log: log, dry: opts[:dry])
|
|
76
83
|
else
|
|
77
84
|
raise Sibit::Error, "Unknown API \"#{a}\""
|
|
78
85
|
end
|
data/lib/sibit/bitcoinchain.rb
CHANGED
|
@@ -60,11 +60,6 @@ class Sibit
|
|
|
60
60
|
raise Sibit::Error, 'Not implemented yet'
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
# Sends a payment and returns the transaction hash.
|
|
64
|
-
def pay(_amount, _fee, _sources, _target, _change)
|
|
65
|
-
raise Sibit::Error, 'Not implemented yet'
|
|
66
|
-
end
|
|
67
|
-
|
|
68
63
|
# Gets the hash of the latest block.
|
|
69
64
|
def latest
|
|
70
65
|
Sibit::Json.new(http: @http, log: @log).get(
|
|
@@ -72,6 +67,16 @@ class Sibit
|
|
|
72
67
|
)['hash']
|
|
73
68
|
end
|
|
74
69
|
|
|
70
|
+
# Fetch all unspent outputs per address.
|
|
71
|
+
def utxos(_sources)
|
|
72
|
+
raise Sibit::Error, 'Not implemented yet'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Push this transaction (in hex format) to the network.
|
|
76
|
+
def push(_hex)
|
|
77
|
+
raise Sibit::Error, 'Not implemented yet'
|
|
78
|
+
end
|
|
79
|
+
|
|
75
80
|
# This method should fetch a Blockchain block and return as a hash. Raises
|
|
76
81
|
# an exception if the block is not found.
|
|
77
82
|
def block(hash)
|
data/lib/sibit/blockchain.rb
CHANGED
|
@@ -67,85 +67,33 @@ class Sibit
|
|
|
67
67
|
json['final_balance']
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
-
# Get recommended fees
|
|
71
|
-
# a hash: { S: 12, M: 45, L: 100, XL: 200 }
|
|
70
|
+
# Get recommended fees.
|
|
72
71
|
def fees
|
|
73
|
-
|
|
74
|
-
URI('https://bitcoinfees.earn.com/api/v1/fees/recommended')
|
|
75
|
-
)
|
|
76
|
-
@log.info("Current recommended Bitcoin fees: \
|
|
77
|
-
#{json['hourFee']}/#{json['halfHourFee']}/#{json['fastestFee']} sat/byte")
|
|
78
|
-
{
|
|
79
|
-
S: json['hourFee'] / 3,
|
|
80
|
-
M: json['hourFee'],
|
|
81
|
-
L: json['halfHourFee'],
|
|
82
|
-
XL: json['fastestFee']
|
|
83
|
-
}
|
|
72
|
+
raise Sibit::Error, 'fees() not implemented yet'
|
|
84
73
|
end
|
|
85
74
|
|
|
86
|
-
#
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
satoshi = satoshi(amount)
|
|
90
|
-
f = mfee(fee, size_of(amount, sources))
|
|
91
|
-
satoshi += f if f.negative?
|
|
92
|
-
raise Error, "The fee #{f.abs} covers the entire amount" if satoshi.zero?
|
|
93
|
-
raise Error, "The fee #{f.abs} is bigger than the amount #{satoshi}" if satoshi.negative?
|
|
94
|
-
builder = Bitcoin::Builder::TxBuilder.new
|
|
95
|
-
unspent = 0
|
|
96
|
-
size = 100
|
|
97
|
-
utxos = Sibit::Json.new(http: @http, log: @log).get(
|
|
75
|
+
# Fetch all unspent outputs per address.
|
|
76
|
+
def utxos(sources)
|
|
77
|
+
Sibit::Json.new(http: @http, log: @log).get(
|
|
98
78
|
URI("https://blockchain.info/unspent?active=#{sources.keys.join('|')}&limit=1000")
|
|
99
|
-
)['unspent_outputs']
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
i.prev_out_script = [utxo['script']].pack('H*')
|
|
108
|
-
address = Bitcoin::Script.new([utxo['script']].pack('H*')).get_address
|
|
109
|
-
i.signature_key(key(sources[address]))
|
|
110
|
-
end
|
|
111
|
-
size += 180
|
|
112
|
-
@log.info(
|
|
113
|
-
" #{num(utxo['value'], p)}/#{utxo['confirmations']} at #{utxo['tx_hash_big_endian']}"
|
|
114
|
-
)
|
|
115
|
-
break if unspent > satoshi
|
|
79
|
+
)['unspent_outputs'].map do |u|
|
|
80
|
+
{
|
|
81
|
+
value: u['value'],
|
|
82
|
+
hash: u['tx_hash_big_endian'],
|
|
83
|
+
index: u['tx_output_n'],
|
|
84
|
+
confirmations: u['confirmations'],
|
|
85
|
+
script: [u['script']].pack('H*')
|
|
86
|
+
}
|
|
116
87
|
end
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
extra_fee: [f, Bitcoin.network[:min_tx_fee]].max,
|
|
126
|
-
change_address: change
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Push this transaction (in hex format) to the network.
|
|
91
|
+
def push(hex)
|
|
92
|
+
return if @dry
|
|
93
|
+
Sibit::Json.new(http: @http, log: @log).post(
|
|
94
|
+
URI('https://blockchain.info/pushtx'),
|
|
95
|
+
hex
|
|
127
96
|
)
|
|
128
|
-
left = unspent - tx.outputs.map(&:value).inject(&:+)
|
|
129
|
-
@log.info("A new Bitcoin transaction #{tx.hash} prepared:
|
|
130
|
-
#{tx.in.count} input#{tx.in.count > 1 ? 's' : ''}:
|
|
131
|
-
#{tx.inputs.map { |i| " in: #{i.prev_out.bth}:#{i.prev_out_index}" }.join("\n ")}
|
|
132
|
-
#{tx.out.count} output#{tx.out.count > 1 ? 's' : ''}:
|
|
133
|
-
#{tx.outputs.map { |o| "out: #{o.script.bth} / #{num(o.value, p)}" }.join("\n ")}
|
|
134
|
-
Min tx fee: #{num(Bitcoin.network[:min_tx_fee], p)}
|
|
135
|
-
Fee requested: #{num(f, p)} as \"#{fee}\"
|
|
136
|
-
Fee actually paid: #{num(left, p)}
|
|
137
|
-
Tx size: #{size} bytes
|
|
138
|
-
Unspent: #{num(unspent, p)}
|
|
139
|
-
Amount: #{num(satoshi, p)}
|
|
140
|
-
Target address: #{target}
|
|
141
|
-
Change address is #{change}")
|
|
142
|
-
unless @dry
|
|
143
|
-
Sibit::Json.new(http: @http, log: @log).post(
|
|
144
|
-
URI('https://blockchain.info/pushtx'),
|
|
145
|
-
tx.to_payload.bth
|
|
146
|
-
)
|
|
147
|
-
end
|
|
148
|
-
tx.hash
|
|
149
97
|
end
|
|
150
98
|
|
|
151
99
|
# Gets the hash of the latest block.
|
|
@@ -157,74 +105,7 @@ class Sibit
|
|
|
157
105
|
|
|
158
106
|
# This method should fetch a Blockchain block and return as a hash.
|
|
159
107
|
def block(_hash)
|
|
160
|
-
raise Sibit::Error, '
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
private
|
|
164
|
-
|
|
165
|
-
def num(satoshi, usd)
|
|
166
|
-
format(
|
|
167
|
-
'%<satoshi>ss/$%<dollars>0.2f',
|
|
168
|
-
satoshi: satoshi.to_s.gsub(/\d(?=(...)+$)/, '\0,'),
|
|
169
|
-
dollars: satoshi * usd / 100_000_000
|
|
170
|
-
)
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Convert text to amount.
|
|
174
|
-
def satoshi(amount)
|
|
175
|
-
return amount if amount.is_a?(Integer)
|
|
176
|
-
raise Error, 'Amount should either be a String or Integer' unless amount.is_a?(String)
|
|
177
|
-
return (amount.gsub(/BTC$/, '').to_f * 100_000_000).to_i if amount.end_with?('BTC')
|
|
178
|
-
raise Error, "Can't understand the amount #{amount.inspect}"
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# Calculates a fee in satoshi for the transaction of the specified size.
|
|
182
|
-
# The +fee+ argument could be a number in satoshi, in which case it will
|
|
183
|
-
# be returned as is, or a string like "XL" or "S", in which case the
|
|
184
|
-
# fee will be calculated using the +size+ argument (which is the size
|
|
185
|
-
# of the transaction in bytes).
|
|
186
|
-
def mfee(fee, size)
|
|
187
|
-
return fee.to_i if fee.is_a?(Integer)
|
|
188
|
-
raise Error, 'Fee should either be a String or Integer' unless fee.is_a?(String)
|
|
189
|
-
mul = 1
|
|
190
|
-
if fee.start_with?('+', '-')
|
|
191
|
-
mul = -1 if fee.start_with?('-')
|
|
192
|
-
fee = fee[1..-1]
|
|
193
|
-
end
|
|
194
|
-
sat = fees[fee.to_sym]
|
|
195
|
-
raise Error, "Can't understand the fee: #{fee.inspect}" if sat.nil?
|
|
196
|
-
mul * sat * size
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
# Make key from private key string in Hash160.
|
|
200
|
-
def key(hash160)
|
|
201
|
-
key = Bitcoin::Key.new
|
|
202
|
-
key.priv = hash160
|
|
203
|
-
key
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Calculate an approximate size of the transaction.
|
|
207
|
-
def size_of(amount, sources)
|
|
208
|
-
satoshi = satoshi(amount)
|
|
209
|
-
builder = Bitcoin::Builder::TxBuilder.new
|
|
210
|
-
unspent = 0
|
|
211
|
-
size = 100
|
|
212
|
-
utxos = Sibit::Json.new(http: @http, log: @log).get(
|
|
213
|
-
URI("https://blockchain.info/unspent?active=#{sources.keys.join('|')}&limit=1000")
|
|
214
|
-
)['unspent_outputs']
|
|
215
|
-
utxos.each do |utxo|
|
|
216
|
-
unspent += utxo['value']
|
|
217
|
-
builder.input do |i|
|
|
218
|
-
i.prev_out(utxo['tx_hash_big_endian'])
|
|
219
|
-
i.prev_out_index(utxo['tx_output_n'])
|
|
220
|
-
i.prev_out_script = [utxo['script']].pack('H*')
|
|
221
|
-
address = Bitcoin::Script.new([utxo['script']].pack('H*')).get_address
|
|
222
|
-
i.signature_key(key(sources[address]))
|
|
223
|
-
end
|
|
224
|
-
size += 180
|
|
225
|
-
break if unspent > satoshi
|
|
226
|
-
end
|
|
227
|
-
size
|
|
108
|
+
raise Sibit::Error, 'block() not implemented yet'
|
|
228
109
|
end
|
|
229
110
|
end
|
|
230
111
|
end
|
data/lib/sibit/btc.rb
CHANGED
|
@@ -68,11 +68,6 @@ class Sibit
|
|
|
68
68
|
raise Sibit::Error, 'Not implemented yet'
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
# Sends a payment and returns the transaction hash.
|
|
72
|
-
def pay(_amount, _fee, _sources, _target, _change)
|
|
73
|
-
raise Sibit::Error, 'Not implemented yet'
|
|
74
|
-
end
|
|
75
|
-
|
|
76
71
|
# Gets the hash of the latest block.
|
|
77
72
|
def latest
|
|
78
73
|
uri = URI('https://chain.api.btc.com/v3/block/latest')
|
|
@@ -82,6 +77,16 @@ class Sibit
|
|
|
82
77
|
hash
|
|
83
78
|
end
|
|
84
79
|
|
|
80
|
+
# Fetch all unspent outputs per address.
|
|
81
|
+
def utxos(_sources)
|
|
82
|
+
raise Sibit::Error, 'Not implemented yet'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Push this transaction (in hex format) to the network.
|
|
86
|
+
def push(_hex)
|
|
87
|
+
raise Sibit::Error, 'Not implemented yet'
|
|
88
|
+
end
|
|
89
|
+
|
|
85
90
|
# This method should fetch a Blockchain block and return as a hash.
|
|
86
91
|
def block(hash)
|
|
87
92
|
head = Sibit::Json.new(http: @http, log: @log).get(
|
data/lib/sibit/earn.rb
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2019-2020 Yegor Bugayenko
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the 'Software'), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
# copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
# SOFTWARE.
|
|
22
|
+
|
|
23
|
+
require 'json'
|
|
24
|
+
require 'uri'
|
|
25
|
+
require_relative 'version'
|
|
26
|
+
require_relative 'error'
|
|
27
|
+
require_relative 'http'
|
|
28
|
+
require_relative 'json'
|
|
29
|
+
|
|
30
|
+
# Earn.com API.
|
|
31
|
+
#
|
|
32
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
33
|
+
# Copyright:: Copyright (c) 2019-2020 Yegor Bugayenko
|
|
34
|
+
# License:: MIT
|
|
35
|
+
class Sibit
|
|
36
|
+
# Blockchain.info API.
|
|
37
|
+
class Earn
|
|
38
|
+
# Constructor.
|
|
39
|
+
def initialize(log: Sibit::Log.new, http: Sibit::Http.new, dry: false)
|
|
40
|
+
@http = http
|
|
41
|
+
@log = log
|
|
42
|
+
@dry = dry
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Current price of BTC in USD (float returned).
|
|
46
|
+
def price(_currency)
|
|
47
|
+
raise Sibit::Error, 'price() doesn\'t work here'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Gets the balance of the address, in satoshi.
|
|
51
|
+
def balance(_address)
|
|
52
|
+
raise Sibit::Error, 'balance() doesn\'t work here'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get recommended fees, in satoshi per byte. The method returns
|
|
56
|
+
# a hash: { S: 12, M: 45, L: 100, XL: 200 }
|
|
57
|
+
def fees
|
|
58
|
+
json = Sibit::Json.new(http: @http, log: @log).get(
|
|
59
|
+
URI('https://bitcoinfees.earn.com/api/v1/fees/recommended')
|
|
60
|
+
)
|
|
61
|
+
@log.info("Current recommended Bitcoin fees: \
|
|
62
|
+
#{json['hourFee']}/#{json['halfHourFee']}/#{json['fastestFee']} sat/byte")
|
|
63
|
+
{
|
|
64
|
+
S: json['hourFee'] / 3,
|
|
65
|
+
M: json['hourFee'],
|
|
66
|
+
L: json['halfHourFee'],
|
|
67
|
+
XL: json['fastestFee']
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Fetch all unspent outputs per address.
|
|
72
|
+
def utxos(_sources)
|
|
73
|
+
raise Sibit::Error, 'Not implemented yet'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Push this transaction (in hex format) to the network.
|
|
77
|
+
def push(_hex)
|
|
78
|
+
raise Sibit::Error, 'Not implemented yet'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Gets the hash of the latest block.
|
|
82
|
+
def latest
|
|
83
|
+
raise Sibit::Error, 'latest() doesn\'t work here'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# This method should fetch a Blockchain block and return as a hash.
|
|
87
|
+
def block(_hash)
|
|
88
|
+
raise Sibit::Error, 'block() doesn\'t work here'
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
data/lib/sibit/fake.rb
CHANGED
|
@@ -42,8 +42,12 @@ class Sibit
|
|
|
42
42
|
100_000_000
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
def
|
|
46
|
-
|
|
45
|
+
def utxos(_sources)
|
|
46
|
+
[]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def push(hex)
|
|
50
|
+
# Nothing to do here
|
|
47
51
|
end
|
|
48
52
|
|
|
49
53
|
def latest
|
data/lib/sibit/version.rb
CHANGED
data/lib/sibit.rb
CHANGED
|
@@ -105,9 +105,61 @@ class Sibit
|
|
|
105
105
|
# +target+: the target address to send to
|
|
106
106
|
# +change+: the address where the change has to be sent to
|
|
107
107
|
def pay(amount, fee, sources, target, change)
|
|
108
|
+
p = price('USD')
|
|
109
|
+
satoshi = satoshi(amount)
|
|
110
|
+
builder = Bitcoin::Builder::TxBuilder.new
|
|
111
|
+
unspent = 0
|
|
112
|
+
size = 100
|
|
113
|
+
utxos = first_one { |api| api.utxos(sources) }
|
|
114
|
+
@log.info("#{utxos.count} UTXOs found, these will be used \
|
|
115
|
+
(value/confirmations at tx_hash):")
|
|
116
|
+
utxos.each do |utxo|
|
|
117
|
+
unspent += utxo[:value]
|
|
118
|
+
builder.input do |i|
|
|
119
|
+
i.prev_out(utxo[:hash])
|
|
120
|
+
i.prev_out_index(utxo[:index])
|
|
121
|
+
i.prev_out_script = utxo[:script]
|
|
122
|
+
address = Bitcoin::Script.new(utxo[:script]).get_address
|
|
123
|
+
i.signature_key(key(sources[address]))
|
|
124
|
+
end
|
|
125
|
+
size += 180
|
|
126
|
+
@log.info(
|
|
127
|
+
" #{num(utxo[:value], p)}/#{utxo[:confirmations]} at #{utxo[:hash]}"
|
|
128
|
+
)
|
|
129
|
+
break if unspent > satoshi
|
|
130
|
+
end
|
|
131
|
+
if unspent < satoshi
|
|
132
|
+
raise Error, "Not enough funds to send #{num(satoshi, p)}, only #{num(unspent, p)} left"
|
|
133
|
+
end
|
|
134
|
+
builder.output(satoshi, target)
|
|
135
|
+
f = mfee(fee, size)
|
|
136
|
+
satoshi += f if f.negative?
|
|
137
|
+
raise Error, "The fee #{f.abs} covers the entire amount" if satoshi.zero?
|
|
138
|
+
raise Error, "The fee #{f.abs} is bigger than the amount #{satoshi}" if satoshi.negative?
|
|
139
|
+
tx = builder.tx(
|
|
140
|
+
input_value: unspent,
|
|
141
|
+
leave_fee: true,
|
|
142
|
+
extra_fee: [f, Bitcoin.network[:min_tx_fee]].max,
|
|
143
|
+
change_address: change
|
|
144
|
+
)
|
|
145
|
+
left = unspent - tx.outputs.map(&:value).inject(&:+)
|
|
146
|
+
@log.info("A new Bitcoin transaction #{tx.hash} prepared:
|
|
147
|
+
#{tx.in.count} input#{tx.in.count > 1 ? 's' : ''}:
|
|
148
|
+
#{tx.inputs.map { |i| " in: #{i.prev_out.bth}:#{i.prev_out_index}" }.join("\n ")}
|
|
149
|
+
#{tx.out.count} output#{tx.out.count > 1 ? 's' : ''}:
|
|
150
|
+
#{tx.outputs.map { |o| "out: #{o.script.bth} / #{num(o.value, p)}" }.join("\n ")}
|
|
151
|
+
Min tx fee: #{num(Bitcoin.network[:min_tx_fee], p)}
|
|
152
|
+
Fee requested: #{num(f, p)} as \"#{fee}\"
|
|
153
|
+
Fee actually paid: #{num(left, p)}
|
|
154
|
+
Tx size: #{size} bytes
|
|
155
|
+
Unspent: #{num(unspent, p)}
|
|
156
|
+
Amount: #{num(satoshi, p)}
|
|
157
|
+
Target address: #{target}
|
|
158
|
+
Change address is #{change}")
|
|
108
159
|
first_one do |api|
|
|
109
|
-
api.
|
|
160
|
+
api.push(tx.to_payload.bth)
|
|
110
161
|
end
|
|
162
|
+
tx.hash
|
|
111
163
|
end
|
|
112
164
|
|
|
113
165
|
# Gets the hash of the latest block.
|
|
@@ -186,7 +238,51 @@ class Sibit
|
|
|
186
238
|
@log.info("The API #{api.class.name} failed: #{e.message}")
|
|
187
239
|
end
|
|
188
240
|
end
|
|
189
|
-
|
|
241
|
+
unless done
|
|
242
|
+
raise Sibit::Error, "No APIs out of #{@api.length} managed to succeed: \
|
|
243
|
+
#{@api.map { |a| a.class.name }.join(', ')}"
|
|
244
|
+
end
|
|
190
245
|
result
|
|
191
246
|
end
|
|
247
|
+
|
|
248
|
+
def num(satoshi, usd)
|
|
249
|
+
format(
|
|
250
|
+
'%<satoshi>ss/$%<dollars>0.2f',
|
|
251
|
+
satoshi: satoshi.to_s.gsub(/\d(?=(...)+$)/, '\0,'),
|
|
252
|
+
dollars: satoshi * usd / 100_000_000
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Convert text to amount.
|
|
257
|
+
def satoshi(amount)
|
|
258
|
+
return amount if amount.is_a?(Integer)
|
|
259
|
+
raise Error, 'Amount should either be a String or Integer' unless amount.is_a?(String)
|
|
260
|
+
return (amount.gsub(/BTC$/, '').to_f * 100_000_000).to_i if amount.end_with?('BTC')
|
|
261
|
+
raise Error, "Can't understand the amount #{amount.inspect}"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Calculates a fee in satoshi for the transaction of the specified size.
|
|
265
|
+
# The +fee+ argument could be a number in satoshi, in which case it will
|
|
266
|
+
# be returned as is, or a string like "XL" or "S", in which case the
|
|
267
|
+
# fee will be calculated using the +size+ argument (which is the size
|
|
268
|
+
# of the transaction in bytes).
|
|
269
|
+
def mfee(fee, size)
|
|
270
|
+
return fee.to_i if fee.is_a?(Integer)
|
|
271
|
+
raise Error, 'Fee should either be a String or Integer' unless fee.is_a?(String)
|
|
272
|
+
mul = 1
|
|
273
|
+
if fee.start_with?('+', '-')
|
|
274
|
+
mul = -1 if fee.start_with?('-')
|
|
275
|
+
fee = fee[1..-1]
|
|
276
|
+
end
|
|
277
|
+
sat = fees[fee.to_sym]
|
|
278
|
+
raise Error, "Can't understand the fee: #{fee.inspect}" if sat.nil?
|
|
279
|
+
mul * sat * size
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Make key from private key string in Hash160.
|
|
283
|
+
def key(hash160)
|
|
284
|
+
key = Bitcoin::Key.new
|
|
285
|
+
key.priv = hash160
|
|
286
|
+
key
|
|
287
|
+
end
|
|
192
288
|
end
|
data/test/test_fake.rb
CHANGED
|
@@ -35,10 +35,7 @@ class TestFake < Minitest::Test
|
|
|
35
35
|
assert_equal(4_000, sibit.price)
|
|
36
36
|
assert_equal(12, sibit.fees[:S])
|
|
37
37
|
assert_equal(100_000_000, sibit.balance(''))
|
|
38
|
-
assert_equal(
|
|
39
|
-
'9dfe55a30b5ee732005158c589179a398117117a68d21531fb6c78b85b544c54',
|
|
40
|
-
sibit.pay(0, 'M', {}, '', '')
|
|
41
|
-
)
|
|
38
|
+
assert_equal([], sibit.utxos(nil))
|
|
42
39
|
assert_equal('00000000000000000008df8a6e1b61d1136803ac9791b8725235c9f780b4ed71', sibit.latest)
|
|
43
40
|
end
|
|
44
41
|
|
data/test/test_sibit.rb
CHANGED
|
@@ -24,6 +24,9 @@ require 'minitest/autorun'
|
|
|
24
24
|
require 'webmock/minitest'
|
|
25
25
|
require 'json'
|
|
26
26
|
require_relative '../lib/sibit'
|
|
27
|
+
require_relative '../lib/sibit/earn'
|
|
28
|
+
require_relative '../lib/sibit/fake'
|
|
29
|
+
require_relative '../lib/sibit/blockchain'
|
|
27
30
|
|
|
28
31
|
# Sibit.
|
|
29
32
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
@@ -34,7 +37,7 @@ class TestSibit < Minitest::Test
|
|
|
34
37
|
stub_request(
|
|
35
38
|
:get, 'https://bitcoinfees.earn.com/api/v1/fees/recommended'
|
|
36
39
|
).to_return(body: '{"fastestFee":300,"halfHourFee":200,"hourFee":180}')
|
|
37
|
-
sibit = Sibit.new
|
|
40
|
+
sibit = Sibit.new(api: Sibit::Earn.new)
|
|
38
41
|
fees = sibit.fees
|
|
39
42
|
assert_equal(60, fees[:S])
|
|
40
43
|
assert_equal(180, fees[:M])
|
|
@@ -124,7 +127,7 @@ class TestSibit < Minitest::Test
|
|
|
124
127
|
'https://blockchain.info/unspent?active=1JvCsJtLmCxEk7ddZFnVkGXpr9uhxZPmJi&limit=1000'
|
|
125
128
|
).to_return(body: JSON.pretty_generate(json))
|
|
126
129
|
stub_request(:post, 'https://blockchain.info/pushtx').to_return(status: 200)
|
|
127
|
-
sibit = Sibit.new
|
|
130
|
+
sibit = Sibit.new(api: [Sibit::Earn.new, Sibit::Blockchain.new])
|
|
128
131
|
target = sibit.create(sibit.generate)
|
|
129
132
|
change = sibit.create(sibit.generate)
|
|
130
133
|
tx = sibit.pay(
|
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.14.
|
|
4
|
+
version: 0.14.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yegor Bugayenko
|
|
@@ -284,6 +284,7 @@ files:
|
|
|
284
284
|
- lib/sibit/bitcoinchain.rb
|
|
285
285
|
- lib/sibit/blockchain.rb
|
|
286
286
|
- lib/sibit/btc.rb
|
|
287
|
+
- lib/sibit/earn.rb
|
|
287
288
|
- lib/sibit/error.rb
|
|
288
289
|
- lib/sibit/fake.rb
|
|
289
290
|
- lib/sibit/http.rb
|