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.
@@ -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.32.8' 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
 
@@ -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
- def pay(amount, fee, sources, target, change, skip_utxo: [], network: nil, base58: false)
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 Error, "The amount #{amount.inspect} must be Integer or String"
118
+ raise(Error, "The amount #{amount.inspect} must be Integer or String")
110
119
  end
111
- raise Error, 'The amount must be positive' if amount.is_a?(Integer) && amount.negative?
112
- raise Error, 'The sources must be an Array' unless sources.is_a?(Array)
113
- raise Error, 'The target must be a String' unless target.is_a?(String)
114
- raise Error, 'The change must be a String' unless change.is_a?(String)
115
- p = price('USD')
116
- keys = sources.map do |k|
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
- network = keys.first&.network || :mainnet
124
- sources = keys.to_h do |k|
125
- pub =
126
- if base58
127
- k.base58
128
- else
129
- 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)}")
130
134
  end
131
- @log.debug("Private key #{k.priv.ellipsized(8).inspect} is public as #{pub}:")
132
- [pub, k.priv]
133
- 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
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 Error, "UTXO arrived to #{address} is incorrect" unless k
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 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")
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 Error, "The fee #{f.abs} covers the entire amount" if satoshi.zero?
175
- 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?
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).to_i].max,
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(left, p)}",
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 has_change)
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 Error, "Invalid block hash #{start.inspect}" unless /^[0-9a-f]{64}$/.match?(start)
227
- 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
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
- checked_outputs = 0
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
- checked_outputs += 1
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 #{checked_outputs} outputs " \
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 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")
300
315
  end
301
- return (amount.gsub(/BTC$/, '').to_f * 100_000_000).to_i if amount.end_with?('BTC')
302
- 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}")
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.to_i if fee.is_a?(Integer)
312
- 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)
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 Error, "Can't understand the fee: #{fee.inspect}" if sat.nil?
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? :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.32.8
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