sibit 0.33.0 → 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/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 -53
- 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 +70 -57
- 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
|
|
|
@@ -105,34 +110,42 @@ class Sibit
|
|
|
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)
|
|
108
|
-
def pay(
|
|
109
|
-
|
|
113
|
+
def pay(
|
|
114
|
+
amount, fee, sources, target, change, skip_utxo: [], network: nil, base58: false,
|
|
115
|
+
price: nil, trust: 0
|
|
116
|
+
)
|
|
110
117
|
unless amount.is_a?(Integer) || amount.is_a?(String)
|
|
111
|
-
raise
|
|
118
|
+
raise(Error, "The amount #{amount.inspect} must be Integer or String")
|
|
112
119
|
end
|
|
113
|
-
raise
|
|
114
|
-
raise
|
|
115
|
-
raise
|
|
116
|
-
raise
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
raise Error, 'Each source private key must be a String' unless k.is_a?(String)
|
|
120
|
-
hex = /^[0-9a-f]{64}$/i.match?(k)
|
|
121
|
-
wif = /^[5KLc][1-9A-HJ-NP-Za-km-z]{50,51}$/.match?(k)
|
|
122
|
-
raise Error, "Invalid private key format: #{k.inspect.ellipsized(8)}" unless hex || wif
|
|
123
|
-
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}")
|
|
124
126
|
end
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
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)}")
|
|
132
134
|
end
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
136
149
|
satoshi = satoshi(amount)
|
|
137
150
|
builder = TxBuilder.new
|
|
138
151
|
unspent = 0
|
|
@@ -148,6 +161,10 @@ class Sibit
|
|
|
148
161
|
@log.debug("UTXO with no confirmations: #{utxo[:hash]}")
|
|
149
162
|
next
|
|
150
163
|
end
|
|
164
|
+
if utxo[:confirmations] < trust
|
|
165
|
+
@log.debug("UTXO below trust=#{trust} (#{utxo[:confirmations]} conf): #{utxo[:hash]}")
|
|
166
|
+
next
|
|
167
|
+
end
|
|
151
168
|
unspent += utxo[:value]
|
|
152
169
|
builder.input do |i|
|
|
153
170
|
i.prev_out(utxo[:hash])
|
|
@@ -156,34 +173,30 @@ class Sibit
|
|
|
156
173
|
i.prev_out_value(utxo[:value])
|
|
157
174
|
address = Script.new(script_hex(utxo[:script])).address(network)
|
|
158
175
|
k = sources[address]
|
|
159
|
-
raise
|
|
176
|
+
raise(Error, "UTXO arrived to #{address} is incorrect") unless k
|
|
160
177
|
i.signature_key(key(k))
|
|
161
178
|
end
|
|
162
179
|
size += 180
|
|
163
|
-
@log.debug(
|
|
164
|
-
" #{num(utxo[:value], p)}/#{utxo[:confirmations]} at #{utxo[:hash]}"
|
|
165
|
-
)
|
|
180
|
+
@log.debug(" #{num(utxo[:value], p)}/#{utxo[:confirmations]} at #{utxo[:hash]}")
|
|
166
181
|
break if unspent > satoshi
|
|
167
182
|
end
|
|
168
183
|
if unspent < satoshi
|
|
169
|
-
raise
|
|
184
|
+
raise(Error, "Not enough funds to send #{num(satoshi, p)}, only #{num(unspent, p)} left")
|
|
170
185
|
end
|
|
171
186
|
f = mfee(fee, size)
|
|
172
187
|
if f.negative?
|
|
173
188
|
satoshi += f
|
|
174
189
|
f = -f
|
|
175
190
|
end
|
|
176
|
-
raise
|
|
177
|
-
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?
|
|
178
193
|
builder.output(satoshi, target)
|
|
179
194
|
tx = builder.tx(
|
|
180
195
|
input_value: unspent,
|
|
181
196
|
leave_fee: true,
|
|
182
|
-
extra_fee: [f, (size * MIN_SATOSHI_PER_BYTE)
|
|
197
|
+
extra_fee: [f, Integer(size * MIN_SATOSHI_PER_BYTE)].max,
|
|
183
198
|
change_address: change
|
|
184
199
|
)
|
|
185
|
-
left = unspent - tx.outputs.sum(&:value)
|
|
186
|
-
has_change = tx.out.count > 1
|
|
187
200
|
@log.debug(
|
|
188
201
|
[
|
|
189
202
|
"A new Bitcoin transaction #{tx.hash} prepared:",
|
|
@@ -197,12 +210,12 @@ class Sibit
|
|
|
197
210
|
end,
|
|
198
211
|
"Min fee: #{num(MIN_SATOSHI_PER_BYTE, p)} /byte",
|
|
199
212
|
"Fee requested: #{num(f, p)} as \"#{fee}\"",
|
|
200
|
-
"Fee actually paid: #{num(
|
|
213
|
+
"Fee actually paid: #{num(unspent - tx.outputs.sum(&:value), p)}",
|
|
201
214
|
"Tx size: #{size} bytes",
|
|
202
215
|
"Unspent: #{num(unspent, p)}",
|
|
203
216
|
"Amount: #{num(satoshi, p)}",
|
|
204
217
|
"Target address: #{target}",
|
|
205
|
-
("Change address: #{change}" if
|
|
218
|
+
("Change address: #{change}" if tx.out.count > 1)
|
|
206
219
|
].flatten.compact.join("\n")
|
|
207
220
|
)
|
|
208
221
|
@api.push(tx.to_payload.bth)
|
|
@@ -225,8 +238,8 @@ class Sibit
|
|
|
225
238
|
# in satoshi. The callback should return non-false if the transaction
|
|
226
239
|
# found was useful.
|
|
227
240
|
def scan(start, max: 4)
|
|
228
|
-
raise
|
|
229
|
-
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
|
|
230
243
|
block = start
|
|
231
244
|
count = 0
|
|
232
245
|
json = {}
|
|
@@ -243,11 +256,11 @@ class Sibit
|
|
|
243
256
|
next
|
|
244
257
|
end
|
|
245
258
|
checked = 0
|
|
246
|
-
|
|
259
|
+
outputs = 0
|
|
247
260
|
json[:txns].each do |t|
|
|
248
261
|
t[:outputs].each_with_index do |o, i|
|
|
249
262
|
address = o[:address]
|
|
250
|
-
|
|
263
|
+
outputs += 1
|
|
251
264
|
hash = "#{t[:hash]}:#{i}"
|
|
252
265
|
satoshi = o[:value]
|
|
253
266
|
if yield(address, hash, satoshi)
|
|
@@ -258,7 +271,7 @@ class Sibit
|
|
|
258
271
|
end
|
|
259
272
|
count += 1
|
|
260
273
|
@log.debug(
|
|
261
|
-
"Checked #{checked} txns and #{
|
|
274
|
+
"Checked #{checked} txns and #{outputs} outputs " \
|
|
262
275
|
"in block #{block} (by #{json[:provider]})"
|
|
263
276
|
)
|
|
264
277
|
block = json[:next]
|
|
@@ -298,10 +311,10 @@ class Sibit
|
|
|
298
311
|
def satoshi(amount)
|
|
299
312
|
return amount if amount.is_a?(Integer)
|
|
300
313
|
unless amount.is_a?(String)
|
|
301
|
-
raise
|
|
314
|
+
raise(Error, "Amount should either be a String or Integer, #{amount.class.name} provided")
|
|
302
315
|
end
|
|
303
|
-
return (amount.gsub(/BTC$/, '')
|
|
304
|
-
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}")
|
|
305
318
|
end
|
|
306
319
|
|
|
307
320
|
# Calculates a fee in satoshi for the transaction of the specified size.
|
|
@@ -310,15 +323,15 @@ class Sibit
|
|
|
310
323
|
# fee will be calculated using the +size+ argument (which is the size
|
|
311
324
|
# of the transaction in bytes).
|
|
312
325
|
def mfee(fee, size)
|
|
313
|
-
return fee
|
|
314
|
-
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)
|
|
315
328
|
mul = 1
|
|
316
329
|
if fee.end_with?('+', '-')
|
|
317
330
|
mul = -1 if fee.end_with?('-')
|
|
318
331
|
fee = fee[0..-2]
|
|
319
332
|
end
|
|
320
333
|
sat = fees[fee.to_sym]
|
|
321
|
-
raise
|
|
334
|
+
raise(Error, "Can't understand the fee: #{fee.inspect}") if sat.nil?
|
|
322
335
|
f = mul * sat * size
|
|
323
336
|
@log.debug("Fee calculated as #{mul} * #{sat}s * #{size} = #{f}s")
|
|
324
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
|