sibit 0.33.0 → 0.34.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 +4 -5
- data/Gemfile.lock +78 -58
- data/README.md +21 -2
- data/Rakefile +1 -1
- data/bin/sibit +6 -4
- data/cross/alpine.df +6 -0
- data/cross/centos.df +8 -0
- data/cross/debian.df +9 -0
- data/cross/fedora.df +7 -0
- data/cross/opensuse.df +17 -0
- data/cross/rocky.df +8 -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 +23 -15
- data/lib/sibit/bestof.rb +41 -11
- data/lib/sibit/bin.rb +68 -54
- data/lib/sibit/bitcoinchain.rb +10 -10
- data/lib/sibit/blockchain.rb +10 -20
- data/lib/sibit/blockchair.rb +14 -24
- data/lib/sibit/btc.rb +51 -60
- data/lib/sibit/cex.rb +12 -11
- data/lib/sibit/coins.rb +35 -0
- data/lib/sibit/cryptoapis.rb +35 -30
- data/lib/sibit/firstof.rb +8 -5
- data/lib/sibit/httpproxy.rb +1 -1
- data/lib/sibit/json.rb +8 -9
- data/lib/sibit/key.rb +39 -41
- data/lib/sibit/script.rb +3 -6
- data/lib/sibit/sochain.rb +180 -0
- data/lib/sibit/tx.rb +75 -36
- data/lib/sibit/txbuilder.rb +15 -3
- data/lib/sibit/version.rb +1 -2
- data/lib/sibit.rb +80 -62
- data/sibit.gemspec +11 -11
- metadata +10 -1
data/lib/sibit.rb
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
require 'ellipsized'
|
|
7
7
|
require 'loog'
|
|
8
8
|
require_relative 'sibit/base58'
|
|
9
|
+
require_relative 'sibit/blockchain'
|
|
10
|
+
require_relative 'sibit/coins'
|
|
9
11
|
require_relative 'sibit/key'
|
|
10
12
|
require_relative 'sibit/script'
|
|
11
13
|
require_relative 'sibit/tx'
|
|
12
14
|
require_relative 'sibit/txbuilder'
|
|
13
|
-
require_relative 'sibit/blockchain'
|
|
14
15
|
require_relative 'sibit/version'
|
|
15
16
|
|
|
16
17
|
# Sibit main class.
|
|
@@ -19,8 +20,7 @@ require_relative 'sibit/version'
|
|
|
19
20
|
# Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
|
|
20
21
|
# License:: MIT
|
|
21
22
|
class Sibit
|
|
22
|
-
|
|
23
|
-
MIN_SATOSHI_PER_BYTE = 0.1
|
|
23
|
+
MIN_SATOSHI_PER_BYTE = 1
|
|
24
24
|
|
|
25
25
|
# Constructor.
|
|
26
26
|
#
|
|
@@ -46,7 +46,7 @@ class Sibit
|
|
|
46
46
|
|
|
47
47
|
# Current price of 1 BTC in USD (or another currency), float returned.
|
|
48
48
|
def price(currency = 'USD')
|
|
49
|
-
raise
|
|
49
|
+
raise(Error, "Invalid currency #{currency.inspect}") unless /^[A-Z]{3}$/.match?(currency)
|
|
50
50
|
@api.price(currency)
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -59,25 +59,31 @@ class Sibit
|
|
|
59
59
|
|
|
60
60
|
# Creates Bitcoin address using the private key in Hash160 format.
|
|
61
61
|
def create(pvt)
|
|
62
|
-
raise
|
|
62
|
+
raise(Error, 'Invalid private key (must be 64 hex chars)') unless /^[0-9a-f]{64}$/.match?(pvt)
|
|
63
63
|
Key.new(pvt).bech32
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
# Gets the balance of the address, in satoshi.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
# Gets the balance of the address, in satoshi. When +trust+ is greater
|
|
67
|
+
# than zero, only UTXOs with at least that many confirmations are summed,
|
|
68
|
+
# which requires the underlying API to support +utxos+.
|
|
69
|
+
def balance(address, trust: 0)
|
|
70
|
+
raise(Error, "Invalid address #{address.inspect}") unless /^[0-9a-zA-Z]+$/.match?(address)
|
|
71
|
+
unless trust.is_a?(Integer) && !trust.negative?
|
|
72
|
+
raise(Error, "Trust must be a non-negative Integer, got #{trust.inspect}")
|
|
73
|
+
end
|
|
74
|
+
return @api.balance(address) if trust.zero?
|
|
75
|
+
@api.utxos([address]).sum { |u| (u[:confirmations] || 0) >= trust ? u[:value] : 0 }
|
|
70
76
|
end
|
|
71
77
|
|
|
72
78
|
# Get the height of the block.
|
|
73
79
|
def height(hash)
|
|
74
|
-
raise
|
|
80
|
+
raise(Error, "Invalid block hash #{hash.inspect}") unless /^[0-9a-f]{64}$/.match?(hash)
|
|
75
81
|
@api.height(hash)
|
|
76
82
|
end
|
|
77
83
|
|
|
78
84
|
# Get the hash of the next block.
|
|
79
85
|
def next_of(hash)
|
|
80
|
-
raise
|
|
86
|
+
raise(Error, "Invalid block hash #{hash.inspect}") unless /^[0-9a-f]{64}$/.match?(hash)
|
|
81
87
|
@api.next_of(hash)
|
|
82
88
|
end
|
|
83
89
|
|
|
@@ -91,9 +97,13 @@ class Sibit
|
|
|
91
97
|
#
|
|
92
98
|
# If the payment can't be signed (the key is wrong, for example) or the
|
|
93
99
|
# previous transaction is not found, or there is a network error, or
|
|
94
|
-
# any other reason, you will get an exception.
|
|
95
|
-
#
|
|
96
|
-
#
|
|
100
|
+
# any other reason, you will get an exception. You may retry, but only
|
|
101
|
+
# while no earlier attempt could have reached the network: inputs are
|
|
102
|
+
# selected deterministically, so an immediate retry rebuilds a conflicting
|
|
103
|
+
# transaction that the network rejects as a double-spend. Once an earlier
|
|
104
|
+
# attempt might have confirmed, the wallet holds a fresh set of UTXOs and
|
|
105
|
+
# a blind retry may pay the target twice; check the target address (or the
|
|
106
|
+
# previous transaction hash) before re-sending.
|
|
97
107
|
#
|
|
98
108
|
# If there are more than 1000 UTXOs in the address where you are trying
|
|
99
109
|
# to send bitcoins from, this method won't be helpful.
|
|
@@ -105,39 +115,47 @@ class Sibit
|
|
|
105
115
|
# +change+: the address where the change has to be sent to
|
|
106
116
|
# +network+: optional network override (:mainnet, :testnet, :regtest)
|
|
107
117
|
# +price+: optional BTC price in USD (skips API fetch if provided)
|
|
108
|
-
def pay(
|
|
109
|
-
|
|
118
|
+
def pay(
|
|
119
|
+
amount, fee, sources, target, change, skip_utxo: [], network: nil, base58: false,
|
|
120
|
+
price: nil, trust: 0
|
|
121
|
+
)
|
|
110
122
|
unless amount.is_a?(Integer) || amount.is_a?(String)
|
|
111
|
-
raise
|
|
123
|
+
raise(Error, "The amount #{amount.inspect} must be Integer or String")
|
|
112
124
|
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)
|
|
125
|
+
raise(Error, 'The amount must be positive') if amount.is_a?(Integer) && amount.negative?
|
|
126
|
+
raise(Error, 'The sources must be an Array') unless sources.is_a?(Array)
|
|
127
|
+
raise(Error, 'The target must be a String') unless target.is_a?(String)
|
|
128
|
+
raise(Error, 'The change must be a String') unless change.is_a?(String)
|
|
129
|
+
unless trust.is_a?(Integer) && !trust.negative?
|
|
130
|
+
raise(Error, "Trust must be a non-negative Integer, got #{trust.inspect}")
|
|
124
131
|
end
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
k.
|
|
132
|
+
p = price || price('USD')
|
|
133
|
+
keys =
|
|
134
|
+
sources.map do |k|
|
|
135
|
+
raise(Error, 'Each source private key must be a String') unless k.is_a?(String)
|
|
136
|
+
wif = /^[5KLc][1-9A-HJ-NP-Za-km-z]{50,51}$/.match?(k)
|
|
137
|
+
unless /^[0-9a-f]{64}$/i.match?(k) || wif
|
|
138
|
+
raise(Error, "Invalid private key format: #{k.inspect.ellipsized(8)}")
|
|
132
139
|
end
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
Key.new(k, network: network)
|
|
141
|
+
end
|
|
142
|
+
network = keys.first&.network || :mainnet
|
|
143
|
+
sources =
|
|
144
|
+
keys.to_h do |k|
|
|
145
|
+
pub =
|
|
146
|
+
if base58
|
|
147
|
+
k.base58
|
|
148
|
+
else
|
|
149
|
+
k.bech32
|
|
150
|
+
end
|
|
151
|
+
@log.debug("Private key #{k.priv.ellipsized(8).inspect} is public as #{pub}:")
|
|
152
|
+
[pub, k.priv]
|
|
153
|
+
end
|
|
136
154
|
satoshi = satoshi(amount)
|
|
137
155
|
builder = TxBuilder.new
|
|
138
156
|
unspent = 0
|
|
139
157
|
size = 100
|
|
140
|
-
utxos = @api.utxos(sources.keys)
|
|
158
|
+
utxos = @api.utxos(sources.keys).sort_by { |u| [u[:hash], u[:index]] }
|
|
141
159
|
@log.debug("#{utxos.count} UTXOs found, these will be used (value/confirmations at tx_hash):")
|
|
142
160
|
utxos.each do |utxo|
|
|
143
161
|
if skip_utxo.include?(utxo[:hash])
|
|
@@ -148,6 +166,10 @@ class Sibit
|
|
|
148
166
|
@log.debug("UTXO with no confirmations: #{utxo[:hash]}")
|
|
149
167
|
next
|
|
150
168
|
end
|
|
169
|
+
if utxo[:confirmations] < trust
|
|
170
|
+
@log.debug("UTXO below trust=#{trust} (#{utxo[:confirmations]} conf): #{utxo[:hash]}")
|
|
171
|
+
next
|
|
172
|
+
end
|
|
151
173
|
unspent += utxo[:value]
|
|
152
174
|
builder.input do |i|
|
|
153
175
|
i.prev_out(utxo[:hash])
|
|
@@ -156,34 +178,30 @@ class Sibit
|
|
|
156
178
|
i.prev_out_value(utxo[:value])
|
|
157
179
|
address = Script.new(script_hex(utxo[:script])).address(network)
|
|
158
180
|
k = sources[address]
|
|
159
|
-
raise
|
|
181
|
+
raise(Error, "UTXO arrived to #{address} is incorrect") unless k
|
|
160
182
|
i.signature_key(key(k))
|
|
161
183
|
end
|
|
162
184
|
size += 180
|
|
163
|
-
@log.debug(
|
|
164
|
-
" #{num(utxo[:value], p)}/#{utxo[:confirmations]} at #{utxo[:hash]}"
|
|
165
|
-
)
|
|
185
|
+
@log.debug(" #{num(utxo[:value], p)}/#{utxo[:confirmations]} at #{utxo[:hash]}")
|
|
166
186
|
break if unspent > satoshi
|
|
167
187
|
end
|
|
168
188
|
if unspent < satoshi
|
|
169
|
-
raise
|
|
189
|
+
raise(Error, "Not enough funds to send #{num(satoshi, p)}, only #{num(unspent, p)} left")
|
|
170
190
|
end
|
|
171
191
|
f = mfee(fee, size)
|
|
172
192
|
if f.negative?
|
|
173
193
|
satoshi += f
|
|
174
194
|
f = -f
|
|
175
195
|
end
|
|
176
|
-
raise
|
|
177
|
-
raise
|
|
196
|
+
raise(Error, "The fee #{f.abs} covers the entire amount") if satoshi.zero?
|
|
197
|
+
raise(Error, "The fee #{f.abs} is bigger than the amount #{satoshi}") if satoshi.negative?
|
|
178
198
|
builder.output(satoshi, target)
|
|
179
199
|
tx = builder.tx(
|
|
180
200
|
input_value: unspent,
|
|
181
201
|
leave_fee: true,
|
|
182
|
-
extra_fee: [f, (size * MIN_SATOSHI_PER_BYTE)
|
|
202
|
+
extra_fee: [f, Integer(size * MIN_SATOSHI_PER_BYTE)].max,
|
|
183
203
|
change_address: change
|
|
184
204
|
)
|
|
185
|
-
left = unspent - tx.outputs.sum(&:value)
|
|
186
|
-
has_change = tx.out.count > 1
|
|
187
205
|
@log.debug(
|
|
188
206
|
[
|
|
189
207
|
"A new Bitcoin transaction #{tx.hash} prepared:",
|
|
@@ -197,12 +215,12 @@ class Sibit
|
|
|
197
215
|
end,
|
|
198
216
|
"Min fee: #{num(MIN_SATOSHI_PER_BYTE, p)} /byte",
|
|
199
217
|
"Fee requested: #{num(f, p)} as \"#{fee}\"",
|
|
200
|
-
"Fee actually paid: #{num(
|
|
218
|
+
"Fee actually paid: #{num(unspent - tx.outputs.sum(&:value), p)}",
|
|
201
219
|
"Tx size: #{size} bytes",
|
|
202
220
|
"Unspent: #{num(unspent, p)}",
|
|
203
221
|
"Amount: #{num(satoshi, p)}",
|
|
204
222
|
"Target address: #{target}",
|
|
205
|
-
("Change address: #{change}" if
|
|
223
|
+
("Change address: #{change}" if tx.out.count > 1)
|
|
206
224
|
].flatten.compact.join("\n")
|
|
207
225
|
)
|
|
208
226
|
@api.push(tx.to_payload.bth)
|
|
@@ -225,8 +243,8 @@ class Sibit
|
|
|
225
243
|
# in satoshi. The callback should return non-false if the transaction
|
|
226
244
|
# found was useful.
|
|
227
245
|
def scan(start, max: 4)
|
|
228
|
-
raise
|
|
229
|
-
raise
|
|
246
|
+
raise(Error, "Invalid block hash #{start.inspect}") unless /^[0-9a-f]{64}$/.match?(start)
|
|
247
|
+
raise(Error, "The max number must be above zero: #{max}") if max < 1
|
|
230
248
|
block = start
|
|
231
249
|
count = 0
|
|
232
250
|
json = {}
|
|
@@ -243,11 +261,11 @@ class Sibit
|
|
|
243
261
|
next
|
|
244
262
|
end
|
|
245
263
|
checked = 0
|
|
246
|
-
|
|
264
|
+
outputs = 0
|
|
247
265
|
json[:txns].each do |t|
|
|
248
266
|
t[:outputs].each_with_index do |o, i|
|
|
249
267
|
address = o[:address]
|
|
250
|
-
|
|
268
|
+
outputs += 1
|
|
251
269
|
hash = "#{t[:hash]}:#{i}"
|
|
252
270
|
satoshi = o[:value]
|
|
253
271
|
if yield(address, hash, satoshi)
|
|
@@ -258,7 +276,7 @@ class Sibit
|
|
|
258
276
|
end
|
|
259
277
|
count += 1
|
|
260
278
|
@log.debug(
|
|
261
|
-
"Checked #{checked} txns and #{
|
|
279
|
+
"Checked #{checked} txns and #{outputs} outputs " \
|
|
262
280
|
"in block #{block} (by #{json[:provider]})"
|
|
263
281
|
)
|
|
264
282
|
block = json[:next]
|
|
@@ -298,10 +316,10 @@ class Sibit
|
|
|
298
316
|
def satoshi(amount)
|
|
299
317
|
return amount if amount.is_a?(Integer)
|
|
300
318
|
unless amount.is_a?(String)
|
|
301
|
-
raise
|
|
319
|
+
raise(Error, "Amount should either be a String or Integer, #{amount.class.name} provided")
|
|
302
320
|
end
|
|
303
|
-
return (amount.gsub(/BTC$/, '')
|
|
304
|
-
raise
|
|
321
|
+
return Coins.new(amount.gsub(/BTC$/, '')).satoshi if amount.end_with?('BTC')
|
|
322
|
+
raise(Error, "Can't understand the amount #{amount.inspect}")
|
|
305
323
|
end
|
|
306
324
|
|
|
307
325
|
# Calculates a fee in satoshi for the transaction of the specified size.
|
|
@@ -310,15 +328,15 @@ class Sibit
|
|
|
310
328
|
# fee will be calculated using the +size+ argument (which is the size
|
|
311
329
|
# of the transaction in bytes).
|
|
312
330
|
def mfee(fee, size)
|
|
313
|
-
return fee
|
|
314
|
-
raise
|
|
331
|
+
return fee if fee.is_a?(Integer)
|
|
332
|
+
raise(Error, 'Fee should either be a String or Integer') unless fee.is_a?(String)
|
|
315
333
|
mul = 1
|
|
316
334
|
if fee.end_with?('+', '-')
|
|
317
335
|
mul = -1 if fee.end_with?('-')
|
|
318
336
|
fee = fee[0..-2]
|
|
319
337
|
end
|
|
320
338
|
sat = fees[fee.to_sym]
|
|
321
|
-
raise
|
|
339
|
+
raise(Error, "Can't understand the fee: #{fee.inspect}") if sat.nil?
|
|
322
340
|
f = mul * sat * size
|
|
323
341
|
@log.debug("Fee calculated as #{mul} * #{sat}s * #{size} = #{f}s")
|
|
324
342
|
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.1
|
|
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
|
|
@@ -185,6 +192,7 @@ files:
|
|
|
185
192
|
- lib/sibit/blockchair.rb
|
|
186
193
|
- lib/sibit/btc.rb
|
|
187
194
|
- lib/sibit/cex.rb
|
|
195
|
+
- lib/sibit/coins.rb
|
|
188
196
|
- lib/sibit/cryptoapis.rb
|
|
189
197
|
- lib/sibit/dry.rb
|
|
190
198
|
- lib/sibit/error.rb
|
|
@@ -195,6 +203,7 @@ files:
|
|
|
195
203
|
- lib/sibit/json.rb
|
|
196
204
|
- lib/sibit/key.rb
|
|
197
205
|
- lib/sibit/script.rb
|
|
206
|
+
- lib/sibit/sochain.rb
|
|
198
207
|
- lib/sibit/tx.rb
|
|
199
208
|
- lib/sibit/txbuilder.rb
|
|
200
209
|
- lib/sibit/version.rb
|