sibit 0.32.8 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +4 -5
- data/Gemfile.lock +76 -56
- data/README.md +16 -2
- data/Rakefile +1 -1
- data/bin/sibit +6 -4
- data/cross/alpine.df +6 -0
- data/cross/centos.df +13 -0
- data/cross/debian.df +9 -0
- data/cross/fedora.df +7 -0
- data/cross/opensuse.df +9 -0
- data/cross/rocky.df +13 -0
- data/cross/ubuntu.df +9 -0
- data/features/cli.feature +1 -0
- data/features/step_definitions/steps.rb +5 -5
- data/features/support/env.rb +1 -1
- data/lib/sibit/base58.rb +3 -6
- data/lib/sibit/bech32.rb +9 -14
- data/lib/sibit/bestof.rb +7 -4
- data/lib/sibit/bin.rb +65 -51
- data/lib/sibit/bitcoinchain.rb +8 -8
- data/lib/sibit/blockchain.rb +5 -19
- data/lib/sibit/blockchair.rb +14 -24
- data/lib/sibit/btc.rb +39 -45
- data/lib/sibit/cex.rb +12 -11
- data/lib/sibit/cryptoapis.rb +30 -27
- data/lib/sibit/firstof.rb +8 -5
- data/lib/sibit/httpproxy.rb +1 -1
- data/lib/sibit/json.rb +7 -7
- data/lib/sibit/key.rb +39 -41
- data/lib/sibit/script.rb +3 -6
- data/lib/sibit/sochain.rb +177 -0
- data/lib/sibit/tx.rb +48 -32
- data/lib/sibit/txbuilder.rb +3 -3
- data/lib/sibit/version.rb +1 -2
- data/lib/sibit.rb +71 -56
- data/sibit.gemspec +11 -11
- metadata +9 -1
data/lib/sibit/txbuilder.rb
CHANGED
|
@@ -24,7 +24,7 @@ class Sibit
|
|
|
24
24
|
|
|
25
25
|
def input
|
|
26
26
|
inp = Input.new
|
|
27
|
-
yield
|
|
27
|
+
yield(inp)
|
|
28
28
|
@inputs << inp
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -43,10 +43,10 @@ class Sibit
|
|
|
43
43
|
value: inp.amount
|
|
44
44
|
)
|
|
45
45
|
end
|
|
46
|
-
|
|
46
|
+
total = @outputs.sum { |o| o[:value] }
|
|
47
47
|
@outputs.each { |o| txn.add_output(o[:value], o[:address]) }
|
|
48
48
|
if leave_fee
|
|
49
|
-
change = input_value -
|
|
49
|
+
change = input_value - total - extra_fee
|
|
50
50
|
txn.add_output(change, change_address) if change.positive?
|
|
51
51
|
end
|
|
52
52
|
Built.new(txn, @inputs, @outputs)
|
data/lib/sibit/version.rb
CHANGED
data/lib/sibit.rb
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
require 'ellipsized'
|
|
7
7
|
require 'loog'
|
|
8
8
|
require_relative 'sibit/base58'
|
|
9
|
+
require_relative 'sibit/blockchain'
|
|
9
10
|
require_relative 'sibit/key'
|
|
10
11
|
require_relative 'sibit/script'
|
|
11
12
|
require_relative 'sibit/tx'
|
|
12
13
|
require_relative 'sibit/txbuilder'
|
|
13
|
-
require_relative 'sibit/blockchain'
|
|
14
14
|
require_relative 'sibit/version'
|
|
15
15
|
|
|
16
16
|
# Sibit main class.
|
|
@@ -19,7 +19,6 @@ require_relative 'sibit/version'
|
|
|
19
19
|
# Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
20
20
|
# License:: MIT
|
|
21
21
|
class Sibit
|
|
22
|
-
# Minimum fee we must pay for transaction processing:
|
|
23
22
|
MIN_SATOSHI_PER_BYTE = 0.1
|
|
24
23
|
|
|
25
24
|
# Constructor.
|
|
@@ -46,7 +45,7 @@ class Sibit
|
|
|
46
45
|
|
|
47
46
|
# Current price of 1 BTC in USD (or another currency), float returned.
|
|
48
47
|
def price(currency = 'USD')
|
|
49
|
-
raise
|
|
48
|
+
raise(Error, "Invalid currency #{currency.inspect}") unless /^[A-Z]{3}$/.match?(currency)
|
|
50
49
|
@api.price(currency)
|
|
51
50
|
end
|
|
52
51
|
|
|
@@ -59,25 +58,31 @@ class Sibit
|
|
|
59
58
|
|
|
60
59
|
# Creates Bitcoin address using the private key in Hash160 format.
|
|
61
60
|
def create(pvt)
|
|
62
|
-
raise
|
|
61
|
+
raise(Error, 'Invalid private key (must be 64 hex chars)') unless /^[0-9a-f]{64}$/.match?(pvt)
|
|
63
62
|
Key.new(pvt).bech32
|
|
64
63
|
end
|
|
65
64
|
|
|
66
|
-
# Gets the balance of the address, in satoshi.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
# Gets the balance of the address, in satoshi. When +trust+ is greater
|
|
66
|
+
# than zero, only UTXOs with at least that many confirmations are summed,
|
|
67
|
+
# which requires the underlying API to support +utxos+.
|
|
68
|
+
def balance(address, trust: 0)
|
|
69
|
+
raise(Error, "Invalid address #{address.inspect}") unless /^[0-9a-zA-Z]+$/.match?(address)
|
|
70
|
+
unless trust.is_a?(Integer) && !trust.negative?
|
|
71
|
+
raise(Error, "Trust must be a non-negative Integer, got #{trust.inspect}")
|
|
72
|
+
end
|
|
73
|
+
return @api.balance(address) if trust.zero?
|
|
74
|
+
@api.utxos([address]).sum { |u| (u[:confirmations] || 0) >= trust ? u[:value] : 0 }
|
|
70
75
|
end
|
|
71
76
|
|
|
72
77
|
# Get the height of the block.
|
|
73
78
|
def height(hash)
|
|
74
|
-
raise
|
|
79
|
+
raise(Error, "Invalid block hash #{hash.inspect}") unless /^[0-9a-f]{64}$/.match?(hash)
|
|
75
80
|
@api.height(hash)
|
|
76
81
|
end
|
|
77
82
|
|
|
78
83
|
# Get the hash of the next block.
|
|
79
84
|
def next_of(hash)
|
|
80
|
-
raise
|
|
85
|
+
raise(Error, "Invalid block hash #{hash.inspect}") unless /^[0-9a-f]{64}$/.match?(hash)
|
|
81
86
|
@api.next_of(hash)
|
|
82
87
|
end
|
|
83
88
|
|
|
@@ -104,33 +109,43 @@ class Sibit
|
|
|
104
109
|
# +target+: the target address to send to
|
|
105
110
|
# +change+: the address where the change has to be sent to
|
|
106
111
|
# +network+: optional network override (:mainnet, :testnet, :regtest)
|
|
107
|
-
|
|
112
|
+
# +price+: optional BTC price in USD (skips API fetch if provided)
|
|
113
|
+
def pay(
|
|
114
|
+
amount, fee, sources, target, change, skip_utxo: [], network: nil, base58: false,
|
|
115
|
+
price: nil, trust: 0
|
|
116
|
+
)
|
|
108
117
|
unless amount.is_a?(Integer) || amount.is_a?(String)
|
|
109
|
-
raise
|
|
118
|
+
raise(Error, "The amount #{amount.inspect} must be Integer or String")
|
|
110
119
|
end
|
|
111
|
-
raise
|
|
112
|
-
raise
|
|
113
|
-
raise
|
|
114
|
-
raise
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
raise Error, 'Each source private key must be a String' unless k.is_a?(String)
|
|
118
|
-
hex = /^[0-9a-f]{64}$/i.match?(k)
|
|
119
|
-
wif = /^[5KLc][1-9A-HJ-NP-Za-km-z]{50,51}$/.match?(k)
|
|
120
|
-
raise Error, "Invalid private key format: #{k.inspect.ellipsized(8)}" unless hex || wif
|
|
121
|
-
Key.new(k, network: network)
|
|
120
|
+
raise(Error, 'The amount must be positive') if amount.is_a?(Integer) && amount.negative?
|
|
121
|
+
raise(Error, 'The sources must be an Array') unless sources.is_a?(Array)
|
|
122
|
+
raise(Error, 'The target must be a String') unless target.is_a?(String)
|
|
123
|
+
raise(Error, 'The change must be a String') unless change.is_a?(String)
|
|
124
|
+
unless trust.is_a?(Integer) && !trust.negative?
|
|
125
|
+
raise(Error, "Trust must be a non-negative Integer, got #{trust.inspect}")
|
|
122
126
|
end
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
k.
|
|
127
|
+
p = price || price('USD')
|
|
128
|
+
keys =
|
|
129
|
+
sources.map do |k|
|
|
130
|
+
raise(Error, 'Each source private key must be a String') unless k.is_a?(String)
|
|
131
|
+
wif = /^[5KLc][1-9A-HJ-NP-Za-km-z]{50,51}$/.match?(k)
|
|
132
|
+
unless /^[0-9a-f]{64}$/i.match?(k) || wif
|
|
133
|
+
raise(Error, "Invalid private key format: #{k.inspect.ellipsized(8)}")
|
|
130
134
|
end
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
Key.new(k, network: network)
|
|
136
|
+
end
|
|
137
|
+
network = keys.first&.network || :mainnet
|
|
138
|
+
sources =
|
|
139
|
+
keys.to_h do |k|
|
|
140
|
+
pub =
|
|
141
|
+
if base58
|
|
142
|
+
k.base58
|
|
143
|
+
else
|
|
144
|
+
k.bech32
|
|
145
|
+
end
|
|
146
|
+
@log.debug("Private key #{k.priv.ellipsized(8).inspect} is public as #{pub}:")
|
|
147
|
+
[pub, k.priv]
|
|
148
|
+
end
|
|
134
149
|
satoshi = satoshi(amount)
|
|
135
150
|
builder = TxBuilder.new
|
|
136
151
|
unspent = 0
|
|
@@ -146,6 +161,10 @@ class Sibit
|
|
|
146
161
|
@log.debug("UTXO with no confirmations: #{utxo[:hash]}")
|
|
147
162
|
next
|
|
148
163
|
end
|
|
164
|
+
if utxo[:confirmations] < trust
|
|
165
|
+
@log.debug("UTXO below trust=#{trust} (#{utxo[:confirmations]} conf): #{utxo[:hash]}")
|
|
166
|
+
next
|
|
167
|
+
end
|
|
149
168
|
unspent += utxo[:value]
|
|
150
169
|
builder.input do |i|
|
|
151
170
|
i.prev_out(utxo[:hash])
|
|
@@ -154,34 +173,30 @@ class Sibit
|
|
|
154
173
|
i.prev_out_value(utxo[:value])
|
|
155
174
|
address = Script.new(script_hex(utxo[:script])).address(network)
|
|
156
175
|
k = sources[address]
|
|
157
|
-
raise
|
|
176
|
+
raise(Error, "UTXO arrived to #{address} is incorrect") unless k
|
|
158
177
|
i.signature_key(key(k))
|
|
159
178
|
end
|
|
160
179
|
size += 180
|
|
161
|
-
@log.debug(
|
|
162
|
-
" #{num(utxo[:value], p)}/#{utxo[:confirmations]} at #{utxo[:hash]}"
|
|
163
|
-
)
|
|
180
|
+
@log.debug(" #{num(utxo[:value], p)}/#{utxo[:confirmations]} at #{utxo[:hash]}")
|
|
164
181
|
break if unspent > satoshi
|
|
165
182
|
end
|
|
166
183
|
if unspent < satoshi
|
|
167
|
-
raise
|
|
184
|
+
raise(Error, "Not enough funds to send #{num(satoshi, p)}, only #{num(unspent, p)} left")
|
|
168
185
|
end
|
|
169
186
|
f = mfee(fee, size)
|
|
170
187
|
if f.negative?
|
|
171
188
|
satoshi += f
|
|
172
189
|
f = -f
|
|
173
190
|
end
|
|
174
|
-
raise
|
|
175
|
-
raise
|
|
191
|
+
raise(Error, "The fee #{f.abs} covers the entire amount") if satoshi.zero?
|
|
192
|
+
raise(Error, "The fee #{f.abs} is bigger than the amount #{satoshi}") if satoshi.negative?
|
|
176
193
|
builder.output(satoshi, target)
|
|
177
194
|
tx = builder.tx(
|
|
178
195
|
input_value: unspent,
|
|
179
196
|
leave_fee: true,
|
|
180
|
-
extra_fee: [f, (size * MIN_SATOSHI_PER_BYTE)
|
|
197
|
+
extra_fee: [f, Integer(size * MIN_SATOSHI_PER_BYTE)].max,
|
|
181
198
|
change_address: change
|
|
182
199
|
)
|
|
183
|
-
left = unspent - tx.outputs.sum(&:value)
|
|
184
|
-
has_change = tx.out.count > 1
|
|
185
200
|
@log.debug(
|
|
186
201
|
[
|
|
187
202
|
"A new Bitcoin transaction #{tx.hash} prepared:",
|
|
@@ -195,12 +210,12 @@ class Sibit
|
|
|
195
210
|
end,
|
|
196
211
|
"Min fee: #{num(MIN_SATOSHI_PER_BYTE, p)} /byte",
|
|
197
212
|
"Fee requested: #{num(f, p)} as \"#{fee}\"",
|
|
198
|
-
"Fee actually paid: #{num(
|
|
213
|
+
"Fee actually paid: #{num(unspent - tx.outputs.sum(&:value), p)}",
|
|
199
214
|
"Tx size: #{size} bytes",
|
|
200
215
|
"Unspent: #{num(unspent, p)}",
|
|
201
216
|
"Amount: #{num(satoshi, p)}",
|
|
202
217
|
"Target address: #{target}",
|
|
203
|
-
("Change address: #{change}" if
|
|
218
|
+
("Change address: #{change}" if tx.out.count > 1)
|
|
204
219
|
].flatten.compact.join("\n")
|
|
205
220
|
)
|
|
206
221
|
@api.push(tx.to_payload.bth)
|
|
@@ -223,8 +238,8 @@ class Sibit
|
|
|
223
238
|
# in satoshi. The callback should return non-false if the transaction
|
|
224
239
|
# found was useful.
|
|
225
240
|
def scan(start, max: 4)
|
|
226
|
-
raise
|
|
227
|
-
raise
|
|
241
|
+
raise(Error, "Invalid block hash #{start.inspect}") unless /^[0-9a-f]{64}$/.match?(start)
|
|
242
|
+
raise(Error, "The max number must be above zero: #{max}") if max < 1
|
|
228
243
|
block = start
|
|
229
244
|
count = 0
|
|
230
245
|
json = {}
|
|
@@ -241,11 +256,11 @@ class Sibit
|
|
|
241
256
|
next
|
|
242
257
|
end
|
|
243
258
|
checked = 0
|
|
244
|
-
|
|
259
|
+
outputs = 0
|
|
245
260
|
json[:txns].each do |t|
|
|
246
261
|
t[:outputs].each_with_index do |o, i|
|
|
247
262
|
address = o[:address]
|
|
248
|
-
|
|
263
|
+
outputs += 1
|
|
249
264
|
hash = "#{t[:hash]}:#{i}"
|
|
250
265
|
satoshi = o[:value]
|
|
251
266
|
if yield(address, hash, satoshi)
|
|
@@ -256,7 +271,7 @@ class Sibit
|
|
|
256
271
|
end
|
|
257
272
|
count += 1
|
|
258
273
|
@log.debug(
|
|
259
|
-
"Checked #{checked} txns and #{
|
|
274
|
+
"Checked #{checked} txns and #{outputs} outputs " \
|
|
260
275
|
"in block #{block} (by #{json[:provider]})"
|
|
261
276
|
)
|
|
262
277
|
block = json[:next]
|
|
@@ -296,10 +311,10 @@ class Sibit
|
|
|
296
311
|
def satoshi(amount)
|
|
297
312
|
return amount if amount.is_a?(Integer)
|
|
298
313
|
unless amount.is_a?(String)
|
|
299
|
-
raise
|
|
314
|
+
raise(Error, "Amount should either be a String or Integer, #{amount.class.name} provided")
|
|
300
315
|
end
|
|
301
|
-
return (amount.gsub(/BTC$/, '')
|
|
302
|
-
raise
|
|
316
|
+
return Integer(Float(amount.gsub(/BTC$/, '')) * 100_000_000) if amount.end_with?('BTC')
|
|
317
|
+
raise(Error, "Can't understand the amount #{amount.inspect}")
|
|
303
318
|
end
|
|
304
319
|
|
|
305
320
|
# Calculates a fee in satoshi for the transaction of the specified size.
|
|
@@ -308,15 +323,15 @@ class Sibit
|
|
|
308
323
|
# fee will be calculated using the +size+ argument (which is the size
|
|
309
324
|
# of the transaction in bytes).
|
|
310
325
|
def mfee(fee, size)
|
|
311
|
-
return fee
|
|
312
|
-
raise
|
|
326
|
+
return fee if fee.is_a?(Integer)
|
|
327
|
+
raise(Error, 'Fee should either be a String or Integer') unless fee.is_a?(String)
|
|
313
328
|
mul = 1
|
|
314
329
|
if fee.end_with?('+', '-')
|
|
315
330
|
mul = -1 if fee.end_with?('-')
|
|
316
331
|
fee = fee[0..-2]
|
|
317
332
|
end
|
|
318
333
|
sat = fees[fee.to_sym]
|
|
319
|
-
raise
|
|
334
|
+
raise(Error, "Can't understand the fee: #{fee.inspect}") if sat.nil?
|
|
320
335
|
f = mul * sat * size
|
|
321
336
|
@log.debug("Fee calculated as #{mul} * #{sat}s * #{size} = #{f}s")
|
|
322
337
|
f
|
data/sibit.gemspec
CHANGED
|
@@ -9,7 +9,7 @@ lib = File.expand_path('lib', __dir__)
|
|
|
9
9
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
10
10
|
require_relative 'lib/sibit/version'
|
|
11
11
|
Gem::Specification.new do |s|
|
|
12
|
-
if s.respond_to?
|
|
12
|
+
if s.respond_to?(:required_rubygems_version=)
|
|
13
13
|
s.required_rubygems_version = Gem::Requirement.new('>= 0')
|
|
14
14
|
end
|
|
15
15
|
s.required_ruby_version = '>= 3.0'
|
|
@@ -29,14 +29,14 @@ Gem::Specification.new do |s|
|
|
|
29
29
|
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
|
30
30
|
s.rdoc_options = ['--charset=UTF-8']
|
|
31
31
|
s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
|
|
32
|
-
s.add_dependency
|
|
33
|
-
s.add_dependency
|
|
34
|
-
s.add_dependency
|
|
35
|
-
s.add_dependency
|
|
36
|
-
s.add_dependency
|
|
37
|
-
s.add_dependency
|
|
38
|
-
s.add_dependency
|
|
39
|
-
s.add_dependency
|
|
40
|
-
s.add_dependency
|
|
41
|
-
s.add_dependency
|
|
32
|
+
s.add_dependency('backtrace', '~> 0.3')
|
|
33
|
+
s.add_dependency('decoor', '~> 0.1')
|
|
34
|
+
s.add_dependency('elapsed', '~> 0.2')
|
|
35
|
+
s.add_dependency('ellipsized', '~> 0.3')
|
|
36
|
+
s.add_dependency('iri', '~> 0.5')
|
|
37
|
+
s.add_dependency('json', '~> 2.18')
|
|
38
|
+
s.add_dependency('loog', '~> 0.6')
|
|
39
|
+
s.add_dependency('openssl', '~> 3.0')
|
|
40
|
+
s.add_dependency('retriable_proxy', '~> 1.0')
|
|
41
|
+
s.add_dependency('thor', '~> 1.3')
|
|
42
42
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sibit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.34.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yegor Bugayenko
|
|
@@ -168,6 +168,13 @@ files:
|
|
|
168
168
|
- REUSE.toml
|
|
169
169
|
- Rakefile
|
|
170
170
|
- bin/sibit
|
|
171
|
+
- cross/alpine.df
|
|
172
|
+
- cross/centos.df
|
|
173
|
+
- cross/debian.df
|
|
174
|
+
- cross/fedora.df
|
|
175
|
+
- cross/opensuse.df
|
|
176
|
+
- cross/rocky.df
|
|
177
|
+
- cross/ubuntu.df
|
|
171
178
|
- cucumber.yml
|
|
172
179
|
- features/cli.feature
|
|
173
180
|
- features/dry.feature
|
|
@@ -195,6 +202,7 @@ files:
|
|
|
195
202
|
- lib/sibit/json.rb
|
|
196
203
|
- lib/sibit/key.rb
|
|
197
204
|
- lib/sibit/script.rb
|
|
205
|
+
- lib/sibit/sochain.rb
|
|
198
206
|
- lib/sibit/tx.rb
|
|
199
207
|
- lib/sibit/txbuilder.rb
|
|
200
208
|
- lib/sibit/version.rb
|