sibit 0.30.0 → 0.30.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 630864e9ab6ffc8c64cca7cd4e4a413f1924b756bf126c103630ff2a05146ecb
4
- data.tar.gz: 68e15a268fc7caf94ebc9d824212daab7f4c823e282db75abfec1b1c747ab53e
3
+ metadata.gz: 7463ec26890a5bd8515ce96c92190a50edaf46f5b94aed0a1b54261f0d63ca65
4
+ data.tar.gz: 71be0a78c7bfcfffdebc6daa2c63a570d561b5e6e923f0d600e2791c2856cac5
5
5
  SHA512:
6
- metadata.gz: 87245d26c76ed83e20606a99217fdf81f7c3952cda04efe3b8578908166fff04d73beb41a8ce9a723c6f535a12a67aea561b819aeb72535a93443d52dce6225f
7
- data.tar.gz: 0d777da50586ea0edc315536dfab3333ed3babbeb815d054120609a55610dd5fe90b38e80c93387f368071031c453b32e1f26c7fb079526806d3f5882a1924de
6
+ metadata.gz: ae0606a033a0506be0d3f397a0ac81bf51db53e7c35671256642764e2dcca358d95cbb9517c67c0a49f0621110926760154ea1c1c885d4aa5b6a2a32792078ff
7
+ data.tar.gz: 21a0bf5f5e27f3c22ceb82f2432d3b3fdd8a4ae1181cbdad383d34f081024c4802b6c83c2a7f51c1205728ccede22c8e45c8498f2d74ffd04282f828b29b4c54
data/Gemfile.lock CHANGED
@@ -3,10 +3,12 @@ PATH
3
3
  specs:
4
4
  sibit (0.0.0)
5
5
  backtrace (~> 0.3)
6
+ decoor (~> 0.1)
7
+ elapsed (~> 0.2)
6
8
  iri (~> 0.5)
7
- json (~> 2)
9
+ json (~> 2.18)
8
10
  loog (~> 0.6)
9
- openssl (>= 2.0)
11
+ openssl (~> 3.0)
10
12
  retriable_proxy (~> 1.0)
11
13
  slop (~> 4.6)
12
14
 
@@ -57,6 +59,7 @@ GEM
57
59
  cucumber-messages (31.1.0)
58
60
  cucumber-tag-expressions (8.1.0)
59
61
  date (3.5.1)
62
+ decoor (0.1.0)
60
63
  diff-lcs (1.6.2)
61
64
  docile (1.4.1)
62
65
  elapsed (0.2.1)
@@ -75,7 +78,6 @@ GEM
75
78
  logger (~> 1.0)
76
79
  memoist3 (1.0.0)
77
80
  mini_mime (1.1.5)
78
- mini_portile2 (2.8.9)
79
81
  minitest (6.0.1)
80
82
  prism (~> 1.5)
81
83
  minitest-reporters (1.7.1)
@@ -84,10 +86,11 @@ GEM
84
86
  minitest (>= 5.0)
85
87
  ruby-progressbar
86
88
  multi_test (1.1.0)
87
- nokogiri (1.18.10)
88
- mini_portile2 (~> 2.8.2)
89
+ nokogiri (1.18.10-arm64-darwin)
89
90
  racc (~> 1.4)
90
- openssl (4.0.0)
91
+ nokogiri (1.18.10-x86_64-linux-gnu)
92
+ racc (~> 1.4)
93
+ openssl (3.3.2)
91
94
  os (1.1.4)
92
95
  parallel (1.27.0)
93
96
  parser (3.3.10.0)
data/README.md CHANGED
@@ -91,13 +91,13 @@ If everything looks correct, remove the `--dry` and run again,
91
91
  To use an HTTPS proxy for all requests:
92
92
 
93
93
  ```bash
94
- $ sibit --proxy=host:port balance 1PfsYNygsuVL8fvBarJNQnHytkg4rGih1U
94
+ sibit --proxy=host:port balance 1PfsYNygsuVL8fvBarJNQnHytkg4rGih1U
95
95
  ```
96
96
 
97
97
  The proxy address may include authentication credentials:
98
98
 
99
99
  ```bash
100
- $ sibit --proxy=user:password@host:port balance 1PfsYNygsuVL8fvBarJNQnHytkg4rGih1U
100
+ sibit --proxy=user:password@host:port balance 1PfsYNygsuVL8fvBarJNQnHytkg4rGih1U
101
101
  ```
102
102
 
103
103
  All operations are performed through the [Blockchain API].
data/bin/sibit CHANGED
@@ -18,10 +18,13 @@ require_relative '../lib/sibit/blockchain'
18
18
  require_relative '../lib/sibit/blockchair'
19
19
  require_relative '../lib/sibit/btc'
20
20
  require_relative '../lib/sibit/cex'
21
+ require_relative '../lib/sibit/dry'
21
22
  require_relative '../lib/sibit/fake'
22
23
  require_relative '../lib/sibit/firstof'
23
24
  require_relative '../lib/sibit/version'
24
25
 
26
+ log = Loog::REGULAR
27
+
25
28
  opts =
26
29
  begin
27
30
  Slop.parse(ARGV, strict: true, help: true) do |o|
@@ -43,7 +46,7 @@ Options are:"
43
46
  )
44
47
  o.bool '--dry', 'Don\'t send a real payment, run in a read-only mode'
45
48
  o.bool '--help', 'Read this: https://github.com/yegor256/sibit' do
46
- puts o
49
+ log.info(o)
47
50
  exit
48
51
  end
49
52
  o.bool '--verbose', 'Print all possible debug messages'
@@ -62,35 +65,36 @@ Options are:"
62
65
  raise e.message
63
66
  end
64
67
 
68
+ raise 'Try --help' if opts.arguments.empty?
69
+
65
70
  begin
66
- raise 'Try --help' if opts.arguments.empty?
67
- log = opts[:verbose] ? Loog::VERBOSE : Loog::NULL
71
+ log = Loog::VERBOSE if opts[:verbose]
68
72
  http = opts[:proxy] ? Sibit::HttpProxy.new(opts[:proxy]) : Sibit::Http.new
69
73
  apis = opts[:api].map(&:downcase).map do |a|
70
- api = nil
71
74
  case a
72
75
  when 'blockchain'
73
- api = Sibit::Blockchain.new(http: http, log: log, dry: opts[:dry])
76
+ Sibit::Blockchain.new(http: http, log: log)
74
77
  when 'btc'
75
- api = Sibit::Btc.new(http: http, log: log, dry: opts[:dry])
78
+ Sibit::Btc.new(http: http, log: log)
76
79
  when 'bitcoinchain'
77
- api = Sibit::Bitcoinchain.new(http: http, log: log, dry: opts[:dry])
80
+ Sibit::Bitcoinchain.new(http: http, log: log)
78
81
  when 'blockchair'
79
- api = Sibit::Blockchair.new(http: http, log: log, dry: opts[:dry])
82
+ Sibit::Blockchair.new(http: http, log: log)
80
83
  when 'cex'
81
- api = Sibit::Cex.new(http: http, log: log, dry: opts[:dry])
84
+ Sibit::Cex.new(http: http, log: log)
82
85
  when 'fake'
83
- api = Sibit::Fake.new
86
+ Sibit::Fake.new
84
87
  else
85
88
  raise Sibit::Error, "Unknown API \"#{a}\""
86
89
  end
87
- api = RetriableProxy.for_object(api, on: Sibit::Error) if opts[:attempts] > 1
88
- api
89
90
  end
90
- sibit = Sibit.new(log: log, api: Sibit::FirstOf.new(apis, log: log, verbose: true))
91
+ api = Sibit::FirstOf.new(apis, log: log, verbose: true)
92
+ api = Sibit::Dry.new(api, log: log) if opts[:dry]
93
+ api = RetriableProxy.for_object(api, on: Sibit::Error) if opts[:attempts] > 1
94
+ sibit = Sibit.new(log: log, api: api)
91
95
  case opts.arguments[0]
92
96
  when 'price'
93
- puts sibit.price
97
+ log.info(sibit.price)
94
98
  when 'fees'
95
99
  fees = sibit.fees
96
100
  text = %i[S M L XL].map do |m|
@@ -98,19 +102,19 @@ begin
98
102
  usd = sat * sibit.price / 100_000_000
99
103
  "#{m}: #{sat}sat / $#{format('%<usd>.02f', usd: usd)}"
100
104
  end.join("\n")
101
- puts text
105
+ log.info(text)
102
106
  when 'latest'
103
- puts sibit.latest
107
+ log.info(sibit.latest)
104
108
  when 'generate'
105
- puts sibit.generate
109
+ log.info(sibit.generate)
106
110
  when 'create'
107
111
  pvt = opts.arguments[1]
108
112
  raise 'Private key argument is required' if pvt.nil?
109
- puts sibit.create(pvt)
113
+ log.info(sibit.create(pvt))
110
114
  when 'balance'
111
115
  address = opts.arguments[1]
112
116
  raise 'Address argument is required' if address.nil?
113
- puts sibit.balance(address)
117
+ log.info(sibit.balance(address))
114
118
  when 'pay'
115
119
  amount = opts.arguments[1]
116
120
  raise 'Amount argument is required' if amount.nil?
@@ -124,20 +128,22 @@ begin
124
128
  raise 'Target argument is required' if target.nil?
125
129
  change = opts.arguments[5]
126
130
  raise 'Change argument is required' if change.nil?
127
- puts sibit.pay(
128
- amount, fee,
129
- sources.split(','),
130
- target, change,
131
- skip_utxo: opts['skip-utxo']
131
+ log.info(
132
+ sibit.pay(
133
+ amount, fee,
134
+ sources.split(','),
135
+ target, change,
136
+ skip_utxo: opts['skip-utxo']
137
+ )
132
138
  )
133
139
  else
134
140
  raise "Command #{opts.arguments[0]} is not supported"
135
141
  end
136
142
  rescue StandardError => e
137
143
  if opts[:verbose]
138
- puts Backtrace.new(e)
144
+ log.error(Backtrace.new(e))
139
145
  else
140
- puts "ERROR: #{e.message}"
146
+ log.error(e.message)
141
147
  end
142
148
  exit(255)
143
149
  end
data/lib/sibit/bestof.rb CHANGED
@@ -92,10 +92,10 @@ class Sibit::BestOf
92
92
  # Just ignore it
93
93
  rescue Sibit::Error => e
94
94
  errors << e
95
- @log.info("The API #{api.class.name} failed at #{method}(): #{e.message}") if @verbose
95
+ @log.debug("The API #{api.class.name} failed at #{method}(): #{e.message}") if @verbose
96
96
  end
97
97
  if results.empty?
98
- errors.each { |e| @log.info(Backtrace.new(e).to_s) }
98
+ errors.each { |e| @log.debug(Backtrace.new(e).to_s) }
99
99
  raise Sibit::Error, "No APIs out of #{@list.length} managed to succeed at #{method}(): \
100
100
  #{@list.map { |a| a.class.name }.join(', ')}"
101
101
  end
@@ -19,10 +19,9 @@ require_relative 'version'
19
19
  # License:: MIT
20
20
  class Sibit::Bitcoinchain
21
21
  # Constructor.
22
- def initialize(log: Loog::NULL, http: Sibit::Http.new, dry: false)
22
+ def initialize(log: Loog::NULL, http: Sibit::Http.new)
23
23
  @http = http
24
24
  @log = log
25
- @dry = dry
26
25
  end
27
26
 
28
27
  # Current price of BTC in USD (float returned).
@@ -43,8 +42,8 @@ class Sibit::Bitcoinchain
43
42
  raise Sibit::Error, "Block #{hash} not found" if block.nil?
44
43
  nxt = block['next_block']
45
44
  nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
46
- @log.info("The block #{hash} is the latest, there is no next block") if nxt.nil?
47
- @log.info("The next block of #{hash} is #{nxt}") unless nxt.nil?
45
+ @log.debug("The block #{hash} is the latest, there is no next block") if nxt.nil?
46
+ @log.debug("The next block of #{hash} is #{nxt}") unless nxt.nil?
48
47
  nxt
49
48
  end
50
49
 
@@ -56,12 +55,12 @@ class Sibit::Bitcoinchain
56
55
  )[0]
57
56
  b = json['balance']
58
57
  if b.nil?
59
- @log.info("The balance of #{address} is not visible")
58
+ @log.debug("The balance of #{address} is not visible")
60
59
  return 0
61
60
  end
62
61
  b *= 100_000_000
63
62
  b = b.to_i
64
- @log.info("The balance of #{address} is #{b} satoshi (#{json['transactions']} txns)")
63
+ @log.debug("The balance of #{address} is #{b} satoshi (#{json['transactions']} txns)")
65
64
  b
66
65
  end
67
66
 
@@ -75,7 +74,7 @@ class Sibit::Bitcoinchain
75
74
  hash = Sibit::Json.new(http: @http, log: @log).get(
76
75
  Iri.new('https://api-r.bitcoinchain.com/v1/status')
77
76
  )['hash']
78
- @log.info("The latest block hash is #{hash}")
77
+ @log.debug("The latest block hash is #{hash}")
79
78
  hash
80
79
  end
81
80
 
@@ -22,10 +22,9 @@ require_relative 'version'
22
22
  # License:: MIT
23
23
  class Sibit::Blockchain
24
24
  # Constructor.
25
- def initialize(log: Loog::NULL, http: Sibit::Http.new, dry: false)
25
+ def initialize(log: Loog::NULL, http: Sibit::Http.new)
26
26
  @http = http
27
27
  @log = log
28
- @dry = dry
29
28
  end
30
29
 
31
30
  # Current price of BTC in USD (float returned).
@@ -35,7 +34,7 @@ class Sibit::Blockchain
35
34
  )[currency]
36
35
  raise Error, "Unrecognized currency #{currency}" if h.nil?
37
36
  price = h['15m']
38
- @log.info("The price of BTC is #{price} USD")
37
+ @log.debug("The price of BTC is #{price} USD")
39
38
  price
40
39
  end
41
40
 
@@ -47,9 +46,9 @@ class Sibit::Blockchain
47
46
  # )
48
47
  # nxt = json['next_block'][0]
49
48
  # if nxt.nil?
50
- # @log.info("There is no block after #{hash}")
49
+ # @log.debug("There is no block after #{hash}")
51
50
  # else
52
- # @log.info("The next block of #{hash} is #{nxt}")
51
+ # @log.debug("The next block of #{hash} is #{nxt}")
53
52
  # end
54
53
  # nxt
55
54
  end
@@ -60,7 +59,7 @@ class Sibit::Blockchain
60
59
  Iri.new('https://blockchain.info/rawblock').append(hash)
61
60
  )
62
61
  h = json['height']
63
- @log.info("The height of #{hash} is #{h}")
62
+ @log.debug("The height of #{hash} is #{h}")
64
63
  h
65
64
  end
66
65
 
@@ -71,7 +70,7 @@ class Sibit::Blockchain
71
70
  accept: [200, 500]
72
71
  )
73
72
  b = json['final_balance']
74
- @log.info("The balance of #{address} is #{b} satoshi (#{json['n_tx']} txns)")
73
+ @log.debug("The balance of #{address} is #{b} satoshi (#{json['n_tx']} txns)")
75
74
  b
76
75
  end
77
76
 
@@ -80,8 +79,12 @@ class Sibit::Blockchain
80
79
  json = Sibit::Json.new(http: @http, log: @log).get(
81
80
  Iri.new('https://api.blockchain.info/mempool/fees')
82
81
  )
83
- @log.info("Current recommended Bitcoin fees: \
84
- #{json['regular']}/#{json['priority']}/#{json['limits']['max']} sat/byte")
82
+ @log.debug(
83
+ [
84
+ 'Currently recommended Bitcoin fees: ',
85
+ "#{json['regular']}/#{json['priority']}/#{json['limits']['max']} sat/byte"
86
+ ].join
87
+ )
85
88
  {
86
89
  S: json['regular'] / 3,
87
90
  M: json['regular'],
@@ -108,7 +111,6 @@ class Sibit::Blockchain
108
111
 
109
112
  # Push this transaction (in hex format) to the network.
110
113
  def push(hex)
111
- return if @dry
112
114
  Sibit::Json.new(http: @http, log: @log).post(
113
115
  Iri.new('https://blockchain.info/pushtx'),
114
116
  hex
@@ -120,7 +122,7 @@ class Sibit::Blockchain
120
122
  hash = Sibit::Json.new(http: @http, log: @log).get(
121
123
  Iri.new('https://blockchain.info/latestblock')
122
124
  )['hash']
123
- @log.info("The latest block hash is #{hash}")
125
+ @log.debug("The latest block hash is #{hash}")
124
126
  hash
125
127
  end
126
128
 
@@ -20,11 +20,10 @@ require_relative 'version'
20
20
  # License:: MIT
21
21
  class Sibit::Blockchair
22
22
  # Constructor.
23
- def initialize(key: nil, log: Loog::NULL, http: Sibit::Http.new, dry: false)
23
+ def initialize(key: nil, log: Loog::NULL, http: Sibit::Http.new)
24
24
  @key = key
25
25
  @http = http
26
26
  @log = log
27
- @dry = dry
28
27
  end
29
28
 
30
29
  # Current price of BTC in USD (float returned).
@@ -49,12 +48,12 @@ class Sibit::Blockchair
49
48
  Iri.new('https://api.blockchair.com/bitcoin/dashboards/address').append(address).fragment(the_key)
50
49
  )['data'][address]
51
50
  if json.nil?
52
- @log.info("Address #{address} not found")
51
+ @log.debug("Address #{address} not found")
53
52
  return 0
54
53
  end
55
54
  a = json['address']
56
55
  b = a['balance']
57
- @log.info("The balance of #{address} is #{b} satoshi")
56
+ @log.debug("The balance of #{address} is #{b} satoshi")
58
57
  b
59
58
  end
60
59
 
@@ -79,7 +78,7 @@ class Sibit::Blockchair
79
78
  Iri.new('https://api.blockchair.com/bitcoin/push/transaction').fragment(the_key),
80
79
  "data=#{hex}"
81
80
  )
82
- @log.info("Transaction (#{hex.length} in hex) has been pushed to Blockchair")
81
+ @log.debug("Transaction (#{hex.length} in hex) has been pushed to Blockchair")
83
82
  end
84
83
 
85
84
  # This method should fetch a Blockchain block and return as a hash.
data/lib/sibit/btc.rb CHANGED
@@ -21,10 +21,9 @@ require_relative 'version'
21
21
  # License:: MIT
22
22
  class Sibit::Btc
23
23
  # Constructor.
24
- def initialize(log: Loog::NULL, http: Sibit::Http.new, dry: false)
24
+ def initialize(log: Loog::NULL, http: Sibit::Http.new)
25
25
  @http = http
26
26
  @log = log
27
- @dry = dry
28
27
  end
29
28
 
30
29
  # Current price of BTC in USD (float returned).
@@ -37,21 +36,21 @@ class Sibit::Btc
37
36
  uri = Iri.new('https://chain.api.btc.com/v3/address').append(address).append('unspent')
38
37
  json = Sibit::Json.new(http: @http, log: @log).get(uri)
39
38
  if json['err_no'] == 1
40
- @log.info("The balance of #{address} is zero (not found)")
39
+ @log.debug("The balance of #{address} is zero (not found)")
41
40
  return 0
42
41
  end
43
42
  data = json['data']
44
43
  if data.nil?
45
- @log.info("The balance of #{address} is probably zero (not found)")
44
+ @log.debug("The balance of #{address} is probably zero (not found)")
46
45
  return 0
47
46
  end
48
47
  txns = data['list']
49
48
  if txns.nil?
50
- @log.info("The balance of #{address} is probably zero (not found)")
49
+ @log.debug("The balance of #{address} is probably zero (not found)")
51
50
  return 0
52
51
  end
53
52
  balance = txns.sum { |tx| tx['value'] } || 0
54
- @log.info("The balance of #{address} is #{balance}, total txns: #{txns.count}")
53
+ @log.debug("The balance of #{address} is #{balance}, total txns: #{txns.count}")
55
54
  balance
56
55
  end
57
56
 
@@ -64,8 +63,8 @@ class Sibit::Btc
64
63
  raise Sibit::Error, "The block #{hash} not found" if data.nil?
65
64
  nxt = data['next_block_hash']
66
65
  nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
67
- @log.info("In BTC.com the block #{hash} is the latest, there is no next block") if nxt.nil?
68
- @log.info("The next block of #{hash} is #{nxt}") unless nxt.nil?
66
+ @log.debug("In BTC.com the block #{hash} is the latest, there is no next block") if nxt.nil?
67
+ @log.debug("The next block of #{hash} is #{nxt}") unless nxt.nil?
69
68
  nxt
70
69
  end
71
70
 
@@ -78,7 +77,7 @@ class Sibit::Btc
78
77
  raise Sibit::Error, "The block #{hash} not found" if data.nil?
79
78
  h = data['height']
80
79
  raise Sibit::Error, "The block #{hash} found but the height is absent" if h.nil?
81
- @log.info("The height of #{hash} is #{h}")
80
+ @log.debug("The height of #{hash} is #{h}")
82
81
  h
83
82
  end
84
83
 
@@ -94,7 +93,7 @@ class Sibit::Btc
94
93
  data = json['data']
95
94
  raise Sibit::Error, 'The latest block not found' if data.nil?
96
95
  hash = data['hash']
97
- @log.info("The hash of the latest block is #{hash}")
96
+ @log.debug("The hash of the latest block is #{hash}")
98
97
  hash
99
98
  end
100
99
 
data/lib/sibit/cex.rb CHANGED
@@ -18,10 +18,9 @@ require_relative 'json'
18
18
  # License:: MIT
19
19
  class Sibit::Cex
20
20
  # Constructor.
21
- def initialize(log: Loog::NULL, http: Sibit::Http.new, dry: false)
21
+ def initialize(log: Loog::NULL, http: Sibit::Http.new)
22
22
  @http = http
23
23
  @log = log
24
- @dry = dry
25
24
  end
26
25
 
27
26
  # Current price of BTC in USD (float returned).
@@ -30,7 +29,7 @@ class Sibit::Cex
30
29
  Iri.new('https://cex.io/api/last_price/BTC').append(currency)
31
30
  )
32
31
  p = json['lprice'].to_f
33
- @log.info("The price of BTC is #{p} #{currency}")
32
+ @log.debug("The price of BTC is #{p} #{currency}")
34
33
  p
35
34
  end
36
35
 
@@ -19,11 +19,10 @@ require_relative 'version'
19
19
  # License:: MIT
20
20
  class Sibit::Cryptoapis
21
21
  # Constructor.
22
- def initialize(key, log: Loog::NULL, http: Sibit::Http.new, dry: false)
22
+ def initialize(key, log: Loog::NULL, http: Sibit::Http.new)
23
23
  @key = key
24
24
  @http = http
25
25
  @log = log
26
- @dry = dry
27
26
  end
28
27
 
29
28
  # Current price of BTC in USD (float returned).
@@ -37,8 +36,8 @@ class Sibit::Cryptoapis
37
36
  Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/blocks').append(hash),
38
37
  headers: headers
39
38
  )['payload']['hash']
40
- @log.info("The block #{hash} is the latest, there is no next block") if nxt.nil?
41
- @log.info("The next block of #{hash} is #{nxt}") unless nxt.nil?
39
+ @log.debug("The block #{hash} is the latest, there is no next block") if nxt.nil?
40
+ @log.debug("The next block of #{hash} is #{nxt}") unless nxt.nil?
42
41
  nxt
43
42
  end
44
43
 
@@ -49,7 +48,7 @@ class Sibit::Cryptoapis
49
48
  headers: headers
50
49
  )['payload']
51
50
  h = json['height']
52
- @log.info("The height of #{hash} is #{h}")
51
+ @log.debug("The height of #{hash} is #{h}")
53
52
  h
54
53
  end
55
54
 
@@ -60,7 +59,7 @@ class Sibit::Cryptoapis
60
59
  headers: headers
61
60
  )['payload']
62
61
  b = (json['balance'].to_f * 100_000_000).to_i
63
- @log.info("The balance of #{address} is #{b} satoshi")
62
+ @log.debug("The balance of #{address} is #{b} satoshi")
64
63
  b
65
64
  end
66
65
 
@@ -75,7 +74,7 @@ class Sibit::Cryptoapis
75
74
  Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/blocks/latest'),
76
75
  headers: headers
77
76
  )['payload']['hash']
78
- @log.info("The latest block hash is #{hash}")
77
+ @log.debug("The latest block hash is #{hash}")
79
78
  hash
80
79
  end
81
80
 
data/lib/sibit/dry.rb ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'decoor'
7
+ require 'loog'
8
+
9
+ # Dry mode decorator for API classes.
10
+ #
11
+ # Wraps any API object and prevents push() from sending transactions.
12
+ # All other methods are delegated to the wrapped API unchanged.
13
+ #
14
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
15
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
16
+ # License:: MIT
17
+ class Sibit::Dry
18
+ def initialize(api, log: Loog::NULL)
19
+ @api = api
20
+ @log = log
21
+ end
22
+
23
+ decoor(:api)
24
+
25
+ def push(_hex)
26
+ @log.debug('Transaction not pushed, dry mode is ON')
27
+ nil
28
+ end
29
+ end
data/lib/sibit/firstof.rb CHANGED
@@ -88,7 +88,7 @@ class Sibit::FirstOf
88
88
  done = false
89
89
  result = nil
90
90
  @list.each do |api|
91
- @log.info("Calling #{api.class.name}##{method}()...")
91
+ @log.debug("Calling #{api.class.name}##{method}()...")
92
92
  begin
93
93
  result = yield api
94
94
  done = true
@@ -97,11 +97,11 @@ class Sibit::FirstOf
97
97
  # Just ignore it
98
98
  rescue Sibit::Error => e
99
99
  errors << e
100
- @log.info("The API #{api.class.name} failed at #{method}(): #{e.message}") if @verbose
100
+ @log.debug("The API #{api.class.name} failed at #{method}(): #{e.message}") if @verbose
101
101
  end
102
102
  end
103
103
  unless done
104
- errors.each { |e| @log.info(Backtrace.new(e).to_s) }
104
+ errors.each { |e| @log.debug(Backtrace.new(e).to_s) }
105
105
  raise Sibit::Error, "No APIs out of #{@list.length} managed to succeed at #{method}(): \
106
106
  #{@list.map { |a| a.class.name }.join(', ')}"
107
107
  end
data/lib/sibit/json.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require 'cgi'
7
+ require 'elapsed'
7
8
  require 'json'
8
9
  require 'loog'
9
10
  require 'uri'
@@ -30,52 +31,55 @@ class Sibit::Json
30
31
  # This method will also log the process and will validate the
31
32
  # response for correctness.
32
33
  def get(address, headers: {}, accept: [200])
33
- start = Time.now
34
- uri = URI(address.to_s)
35
- res = @http.client(uri).get(
36
- "#{uri.path.empty? ? '/' : uri.path}#{"?#{uri.query}" if uri.query}",
37
- {
38
- 'Accept' => 'application/json',
39
- 'User-Agent' => user_agent,
40
- 'Accept-Charset' => 'UTF-8',
41
- 'Accept-Encoding' => ''
42
- }.merge(headers)
43
- )
44
- unless accept.include?(res.code.to_i)
45
- raise Sibit::Error, "Failed to retrieve #{uri} (#{res.code}): #{res.body}"
34
+ ret = nil
35
+ elapsed(@log) do
36
+ uri = URI(address.to_s)
37
+ res = @http.client(uri).get(
38
+ "#{uri.path.empty? ? '/' : uri.path}#{"?#{uri.query}" if uri.query}",
39
+ {
40
+ 'Accept' => 'application/json',
41
+ 'User-Agent' => user_agent,
42
+ 'Accept-Charset' => 'UTF-8',
43
+ 'Accept-Encoding' => ''
44
+ }.merge(headers)
45
+ )
46
+ unless accept.include?(res.code.to_i)
47
+ raise Sibit::Error, "Failed to retrieve #{uri} (#{res.code}): #{res.body}"
48
+ end
49
+ ret =
50
+ begin
51
+ JSON.parse(res.body)
52
+ rescue JSON::ParserError => e
53
+ raise Sibit::Error, "Can't parse JSON: #{e.message}"
54
+ end
55
+ throw :"GET #{uri}: #{res.code}/#{length(res.body.length)}"
46
56
  end
47
- @log.info("GET #{uri}: #{res.code}/#{length(res.body.length)} in #{age(start)}")
48
- JSON.parse(res.body)
49
- rescue JSON::ParserError => e
50
- raise Sibit::Error, "Can't parse JSON: #{e.message}"
57
+ ret
51
58
  end
52
59
 
53
60
  def post(address, body, headers: {})
54
- start = Time.now
55
61
  uri = URI(address.to_s)
56
- res = @http.client(uri).post(
57
- "#{uri.path}?#{uri.query}",
58
- "tx=#{CGI.escape(body)}",
59
- {
60
- 'Accept' => 'text/plain',
61
- 'User-Agent' => user_agent,
62
- 'Accept-Charset' => 'UTF-8',
63
- 'Accept-Encoding' => '',
64
- 'Content-Type' => 'application/x-www-form-urlencoded'
65
- }.merge(headers)
66
- )
67
- unless res.code == '200'
68
- raise Sibit::Error, "Failed to post tx to #{uri}: #{res.code}\n#{res.body}"
62
+ elapsed(@log) do
63
+ res = @http.client(uri).post(
64
+ "#{uri.path}?#{uri.query}",
65
+ "tx=#{CGI.escape(body)}",
66
+ {
67
+ 'Accept' => 'text/plain',
68
+ 'User-Agent' => user_agent,
69
+ 'Accept-Charset' => 'UTF-8',
70
+ 'Accept-Encoding' => '',
71
+ 'Content-Type' => 'application/x-www-form-urlencoded'
72
+ }.merge(headers)
73
+ )
74
+ unless res.code == '200'
75
+ raise Sibit::Error, "Failed to post tx to #{uri}: #{res.code}\n#{res.body}"
76
+ end
77
+ throw :"POST #{uri}: #{res.code}"
69
78
  end
70
- @log.info("POST #{uri}: #{res.code} in #{age(start)}")
71
79
  end
72
80
 
73
81
  private
74
82
 
75
- def age(start)
76
- "#{((Time.now - start) * 1000).round}ms"
77
- end
78
-
79
83
  def length(bytes)
80
84
  if bytes > 1024 * 1024
81
85
  "#{bytes / (1024 * 1024)}mb"
data/lib/sibit/version.rb CHANGED
@@ -9,5 +9,5 @@
9
9
  # License:: MIT
10
10
  class Sibit
11
11
  # Current version of the library.
12
- VERSION = '0.30.0'
12
+ VERSION = '0.30.2'
13
13
  end
data/lib/sibit.rb CHANGED
@@ -4,11 +4,11 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require 'loog'
7
- require_relative 'sibit/bitcoin/base58'
8
- require_relative 'sibit/bitcoin/key'
9
- require_relative 'sibit/bitcoin/script'
10
- require_relative 'sibit/bitcoin/tx'
11
- require_relative 'sibit/bitcoin/txbuilder'
7
+ require_relative 'sibit/base58'
8
+ require_relative 'sibit/key'
9
+ require_relative 'sibit/script'
10
+ require_relative 'sibit/tx'
11
+ require_relative 'sibit/txbuilder'
12
12
  require_relative 'sibit/blockchain'
13
13
  require_relative 'sibit/version'
14
14
 
@@ -49,7 +49,7 @@ class Sibit
49
49
  # Generates new Bitcoin private key and returns in Hash160 format.
50
50
  def generate
51
51
  key = Key.generate.priv
52
- @log.info("Bitcoin private key generated: #{key[0..8]}...")
52
+ @log.debug("Bitcoin private key generated: #{key[0..8]}...")
53
53
  key
54
54
  end
55
55
 
@@ -100,17 +100,17 @@ class Sibit
100
100
  # +change+: the address where the change has to be sent to
101
101
  def pay(amount, fee, sources, target, change, skip_utxo: [])
102
102
  p = price('USD')
103
- sources = sources.map { |k| [Key.new(k).addr, k] }.to_h
103
+ sources = sources.to_h { |k| [Key.new(k).addr, k] }
104
104
  satoshi = satoshi(amount)
105
105
  builder = TxBuilder.new
106
106
  unspent = 0
107
107
  size = 100
108
108
  utxos = @api.utxos(sources.keys)
109
- @log.info("#{utxos.count} UTXOs found, these will be used \
109
+ @log.debug("#{utxos.count} UTXOs found, these will be used \
110
110
  (value/confirmations at tx_hash):")
111
111
  utxos.each do |utxo|
112
112
  if skip_utxo.include?(utxo[:hash])
113
- @log.info("UTXO skipped: #{utxo[:hash]}")
113
+ @log.debug("UTXO skipped: #{utxo[:hash]}")
114
114
  next
115
115
  end
116
116
  unspent += utxo[:value]
@@ -124,7 +124,7 @@ class Sibit
124
124
  i.signature_key(key(k))
125
125
  end
126
126
  size += 180
127
- @log.info(
127
+ @log.debug(
128
128
  " #{num(utxo[:value], p)}/#{utxo[:confirmations]} at #{utxo[:hash]}"
129
129
  )
130
130
  break if unspent > satoshi
@@ -132,11 +132,14 @@ class Sibit
132
132
  if unspent < satoshi
133
133
  raise Error, "Not enough funds to send #{num(satoshi, p)}, only #{num(unspent, p)} left"
134
134
  end
135
- builder.output(satoshi, target)
136
135
  f = mfee(fee, size)
137
- satoshi += f if f.negative?
136
+ if f.negative?
137
+ satoshi += f
138
+ f = -f
139
+ end
138
140
  raise Error, "The fee #{f.abs} covers the entire amount" if satoshi.zero?
139
141
  raise Error, "The fee #{f.abs} is bigger than the amount #{satoshi}" if satoshi.negative?
142
+ builder.output(satoshi, target)
140
143
  tx = builder.tx(
141
144
  input_value: unspent,
142
145
  leave_fee: true,
@@ -144,7 +147,7 @@ class Sibit
144
147
  change_address: change
145
148
  )
146
149
  left = unspent - tx.outputs.sum(&:value)
147
- @log.info("A new Bitcoin transaction #{tx.hash} prepared:
150
+ @log.debug("A new Bitcoin transaction #{tx.hash} prepared:
148
151
  #{tx.in.count} input#{'s' if tx.in.count > 1}:
149
152
  #{tx.inputs.map { |i| " in: #{i.prev_out.unpack1('H*')}:#{i.prev_out_index}" }.join("\n ")}
150
153
  #{tx.out.count} output#{'s' if tx.out.count > 1}:
@@ -186,10 +189,10 @@ class Sibit
186
189
  json = @api.block(block)
187
190
  if json[:orphan]
188
191
  steps = 4
189
- @log.info("Orphan block found at #{block}, moving #{steps} steps back...")
192
+ @log.debug("Orphan block found at #{block}, moving #{steps} steps back...")
190
193
  steps.times do
191
194
  block = json[:previous]
192
- @log.info("Moved back to #{block}")
195
+ @log.debug("Moved back to #{block}")
193
196
  json = @api.block(block)
194
197
  end
195
198
  next
@@ -203,34 +206,34 @@ class Sibit
203
206
  hash = "#{t[:hash]}:#{i}"
204
207
  satoshi = o[:value]
205
208
  if yield(address, hash, satoshi)
206
- @log.info("Bitcoin tx found at #{hash} for #{satoshi} sent to #{address}")
209
+ @log.debug("Bitcoin tx found at #{hash} for #{satoshi} sent to #{address}")
207
210
  end
208
211
  end
209
212
  checked += 1
210
213
  end
211
214
  count += 1
212
- @log.info("We checked #{checked} txns and #{checked_outputs} outputs \
215
+ @log.debug("We checked #{checked} txns and #{checked_outputs} outputs \
213
216
  in block #{block} (by #{json[:provider]})")
214
217
  block = json[:next]
215
218
  begin
216
219
  if block.nil?
217
- @log.info("The next_block is empty in #{json[:hash]}, this may be the end...")
220
+ @log.debug("The next_block is empty in #{json[:hash]}, this may be the end...")
218
221
  block = @api.next_of(json[:hash])
219
222
  end
220
223
  rescue Sibit::Error => e
221
- @log.info("Failed to get the next_of(#{json[:hash]}), quitting: #{e.message}")
224
+ @log.debug("Failed to get the next_of(#{json[:hash]}), quitting: #{e.message}")
222
225
  break
223
226
  end
224
227
  if block.nil?
225
- @log.info("The block #{json[:hash]} is definitely the end of Blockchain, we stop.")
228
+ @log.debug("The block #{json[:hash]} is definitely the end of Blockchain, we stop.")
226
229
  break
227
230
  end
228
231
  if count >= max
229
- @log.info("Too many blocks (#{count}) in one go, let's get back to it next time")
232
+ @log.debug("Too many blocks (#{count}) in one go, let's get back to it next time")
230
233
  break
231
234
  end
232
235
  end
233
- @log.info("Scanned from #{start} to #{json[:hash]} (#{count} blocks)")
236
+ @log.debug("Scanned from #{start} to #{json[:hash]} (#{count} blocks)")
234
237
  json[:hash]
235
238
  end
236
239
 
@@ -256,7 +259,7 @@ in block #{block} (by #{json[:provider]})")
256
259
 
257
260
  # Calculates a fee in satoshi for the transaction of the specified size.
258
261
  # The +fee+ argument could be a number in satoshi, in which case it will
259
- # be returned as is, or a string like "XL" or "S", in which case the
262
+ # be returned as is, or a string like "XL" or "S-", in which case the
260
263
  # fee will be calculated using the +size+ argument (which is the size
261
264
  # of the transaction in bytes).
262
265
  def mfee(fee, size)
@@ -269,7 +272,9 @@ in block #{block} (by #{json[:provider]})")
269
272
  end
270
273
  sat = fees[fee.to_sym]
271
274
  raise Error, "Can't understand the fee: #{fee.inspect}" if sat.nil?
272
- mul * sat * size
275
+ f = mul * sat * size
276
+ @log.debug("Fee calculated as #{mul} * #{sat} * #{size} = #{f}")
277
+ f
273
278
  end
274
279
 
275
280
  # Make key from private key string in Hash160.
data/sibit.gemspec CHANGED
@@ -12,7 +12,7 @@ Gem::Specification.new do |s|
12
12
  if s.respond_to? :required_rubygems_version=
13
13
  s.required_rubygems_version = Gem::Requirement.new('>= 0')
14
14
  end
15
- s.required_ruby_version = '>= 2.5'
15
+ s.required_ruby_version = '>= 3.0'
16
16
  s.name = 'sibit'
17
17
  s.version = Sibit::VERSION
18
18
  s.license = 'MIT'
@@ -30,10 +30,12 @@ Gem::Specification.new do |s|
30
30
  s.rdoc_options = ['--charset=UTF-8']
31
31
  s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
32
32
  s.add_dependency 'backtrace', '~> 0.3'
33
+ s.add_dependency 'decoor', '~> 0.1'
34
+ s.add_dependency 'elapsed', '~> 0.2'
33
35
  s.add_dependency 'iri', '~> 0.5'
34
- s.add_dependency 'json', '~> 2'
36
+ s.add_dependency 'json', '~> 2.18'
35
37
  s.add_dependency 'loog', '~> 0.6'
36
- s.add_dependency 'openssl', '>= 2.0'
38
+ s.add_dependency 'openssl', '~> 3.0'
37
39
  s.add_dependency 'retriable_proxy', '~> 1.0'
38
40
  s.add_dependency 'slop', '~> 4.6'
39
41
  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.30.0
4
+ version: 0.30.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -23,6 +23,34 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: decoor
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: elapsed
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.2'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.2'
26
54
  - !ruby/object:Gem::Dependency
27
55
  name: iri
28
56
  requirement: !ruby/object:Gem::Requirement
@@ -43,14 +71,14 @@ dependencies:
43
71
  requirements:
44
72
  - - "~>"
45
73
  - !ruby/object:Gem::Version
46
- version: '2'
74
+ version: '2.18'
47
75
  type: :runtime
48
76
  prerelease: false
49
77
  version_requirements: !ruby/object:Gem::Requirement
50
78
  requirements:
51
79
  - - "~>"
52
80
  - !ruby/object:Gem::Version
53
- version: '2'
81
+ version: '2.18'
54
82
  - !ruby/object:Gem::Dependency
55
83
  name: loog
56
84
  requirement: !ruby/object:Gem::Requirement
@@ -69,16 +97,16 @@ dependencies:
69
97
  name: openssl
70
98
  requirement: !ruby/object:Gem::Requirement
71
99
  requirements:
72
- - - ">="
100
+ - - "~>"
73
101
  - !ruby/object:Gem::Version
74
- version: '2.0'
102
+ version: '3.0'
75
103
  type: :runtime
76
104
  prerelease: false
77
105
  version_requirements: !ruby/object:Gem::Requirement
78
106
  requirements:
79
- - - ">="
107
+ - - "~>"
80
108
  - !ruby/object:Gem::Version
81
- version: '2.0'
109
+ version: '3.0'
82
110
  - !ruby/object:Gem::Dependency
83
111
  name: retriable_proxy
84
112
  requirement: !ruby/object:Gem::Requirement
@@ -133,24 +161,25 @@ files:
133
161
  - features/step_definitions/steps.rb
134
162
  - features/support/env.rb
135
163
  - lib/sibit.rb
164
+ - lib/sibit/base58.rb
136
165
  - lib/sibit/bestof.rb
137
- - lib/sibit/bitcoin/base58.rb
138
- - lib/sibit/bitcoin/key.rb
139
- - lib/sibit/bitcoin/script.rb
140
- - lib/sibit/bitcoin/tx.rb
141
- - lib/sibit/bitcoin/txbuilder.rb
142
166
  - lib/sibit/bitcoinchain.rb
143
167
  - lib/sibit/blockchain.rb
144
168
  - lib/sibit/blockchair.rb
145
169
  - lib/sibit/btc.rb
146
170
  - lib/sibit/cex.rb
147
171
  - lib/sibit/cryptoapis.rb
172
+ - lib/sibit/dry.rb
148
173
  - lib/sibit/error.rb
149
174
  - lib/sibit/fake.rb
150
175
  - lib/sibit/firstof.rb
151
176
  - lib/sibit/http.rb
152
177
  - lib/sibit/httpproxy.rb
153
178
  - lib/sibit/json.rb
179
+ - lib/sibit/key.rb
180
+ - lib/sibit/script.rb
181
+ - lib/sibit/tx.rb
182
+ - lib/sibit/txbuilder.rb
154
183
  - lib/sibit/version.rb
155
184
  - logo.svg
156
185
  - sibit.gemspec
@@ -166,7 +195,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
166
195
  requirements:
167
196
  - - ">="
168
197
  - !ruby/object:Gem::Version
169
- version: '2.5'
198
+ version: '3.0'
170
199
  required_rubygems_version: !ruby/object:Gem::Requirement
171
200
  requirements:
172
201
  - - ">="
File without changes
File without changes
File without changes
File without changes
File without changes