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.
@@ -24,7 +24,7 @@ class Sibit
24
24
 
25
25
  def input
26
26
  inp = Input.new
27
- yield inp
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
- total_out = @outputs.sum { |o| o[:value] }
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 - total_out - extra_fee
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
@@ -8,6 +8,5 @@
8
8
  # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
9
9
  # License:: MIT
10
10
  class Sibit
11
- # Current version of the library.
12
- VERSION = '0.33.0' unless defined?(VERSION)
11
+ VERSION = '0.34.0' unless defined?(VERSION)
13
12
  end
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 Error, "Invalid currency #{currency.inspect}" unless /^[A-Z]{3}$/.match?(currency)
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 Error, 'Invalid private key (must be 64 hex chars)' unless /^[0-9a-f]{64}$/.match?(pvt)
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
- def balance(address)
68
- raise Error, "Invalid address #{address.inspect}" unless /^[0-9a-zA-Z]+$/.match?(address)
69
- @api.balance(address)
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 Error, "Invalid block hash #{hash.inspect}" unless /^[0-9a-f]{64}$/.match?(hash)
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 Error, "Invalid block hash #{hash.inspect}" unless /^[0-9a-f]{64}$/.match?(hash)
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(amount, fee, sources, target, change, skip_utxo: [], network: nil, base58: false,
109
- price: nil)
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 Error, "The amount #{amount.inspect} must be Integer or String"
118
+ raise(Error, "The amount #{amount.inspect} must be Integer or String")
112
119
  end
113
- raise Error, 'The amount must be positive' if amount.is_a?(Integer) && amount.negative?
114
- raise Error, 'The sources must be an Array' unless sources.is_a?(Array)
115
- raise Error, 'The target must be a String' unless target.is_a?(String)
116
- raise Error, 'The change must be a String' unless change.is_a?(String)
117
- p = price || price('USD')
118
- keys = sources.map do |k|
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
- network = keys.first&.network || :mainnet
126
- sources = keys.to_h do |k|
127
- pub =
128
- if base58
129
- k.base58
130
- else
131
- k.bech32
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
- @log.debug("Private key #{k.priv.ellipsized(8).inspect} is public as #{pub}:")
134
- [pub, k.priv]
135
- end
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 Error, "UTXO arrived to #{address} is incorrect" unless k
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 Error, "Not enough funds to send #{num(satoshi, p)}, only #{num(unspent, p)} left"
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 Error, "The fee #{f.abs} covers the entire amount" if satoshi.zero?
177
- raise Error, "The fee #{f.abs} is bigger than the amount #{satoshi}" if satoshi.negative?
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).to_i].max,
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(left, p)}",
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 has_change)
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 Error, "Invalid block hash #{start.inspect}" unless /^[0-9a-f]{64}$/.match?(start)
229
- raise Error, "The max number must be above zero: #{max}" if max < 1
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
- checked_outputs = 0
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
- checked_outputs += 1
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 #{checked_outputs} outputs " \
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 Error, "Amount should either be a String or Integer, #{amount.class.name} provided"
314
+ raise(Error, "Amount should either be a String or Integer, #{amount.class.name} provided")
302
315
  end
303
- return (amount.gsub(/BTC$/, '').to_f * 100_000_000).to_i if amount.end_with?('BTC')
304
- raise Error, "Can't understand the amount #{amount.inspect}"
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.to_i if fee.is_a?(Integer)
314
- raise Error, 'Fee should either be a String or Integer' unless fee.is_a?(String)
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 Error, "Can't understand the fee: #{fee.inspect}" if sat.nil?
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? :required_rubygems_version=
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 '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'
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.33.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