sibit 0.25.1 → 0.26.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +18 -28
  3. data/Gemfile.lock +187 -0
  4. data/LICENSE.txt +1 -1
  5. data/LICENSES/MIT.txt +21 -0
  6. data/README.md +82 -60
  7. data/REUSE.toml +35 -0
  8. data/Rakefile +3 -29
  9. data/bin/sibit +7 -23
  10. data/cucumber.yml +3 -0
  11. data/features/cli.feature +2 -0
  12. data/features/gem_package.feature +4 -1
  13. data/features/step_definitions/steps.rb +2 -19
  14. data/features/support/env.rb +2 -19
  15. data/lib/sibit/bestof.rb +5 -22
  16. data/lib/sibit/bitcoin/base58.rb +50 -0
  17. data/lib/sibit/bitcoin/key.rb +87 -0
  18. data/lib/sibit/bitcoin/script.rb +58 -0
  19. data/lib/sibit/bitcoin/tx.rb +212 -0
  20. data/lib/sibit/bitcoin/txbuilder.rb +120 -0
  21. data/lib/sibit/bitcoinchain.rb +5 -22
  22. data/lib/sibit/blockchain.rb +5 -22
  23. data/lib/sibit/blockchair.rb +5 -22
  24. data/lib/sibit/btc.rb +6 -23
  25. data/lib/sibit/cex.rb +5 -22
  26. data/lib/sibit/cryptoapis.rb +5 -22
  27. data/lib/sibit/earn.rb +5 -21
  28. data/lib/sibit/error.rb +3 -20
  29. data/lib/sibit/fake.rb +3 -20
  30. data/lib/sibit/firstof.rb +5 -22
  31. data/lib/sibit/http.rb +3 -20
  32. data/lib/sibit/json.rb +6 -23
  33. data/lib/sibit/version.rb +4 -21
  34. data/lib/sibit.rb +31 -40
  35. data/logo.svg +1 -1
  36. data/sibit.gemspec +16 -32
  37. metadata +26 -47
  38. data/.0pdd.yml +0 -9
  39. data/.gitattributes +0 -7
  40. data/.github/workflows/codecov.yml +0 -21
  41. data/.github/workflows/pdd.yml +0 -15
  42. data/.github/workflows/rake.yml +0 -24
  43. data/.github/workflows/xcop.yml +0 -17
  44. data/.gitignore +0 -8
  45. data/.pdd +0 -7
  46. data/.rubocop.yml +0 -38
  47. data/.rultor.yml +0 -21
  48. data/.simplecov +0 -40
  49. data/lib/sibit/log.rb +0 -49
  50. data/renovate.json +0 -6
  51. data/test/test__helper.rb +0 -29
  52. data/test/test_bestof.rb +0 -62
  53. data/test/test_bitcoinchain.rb +0 -73
  54. data/test/test_blockchain.rb +0 -58
  55. data/test/test_blockchair.rb +0 -43
  56. data/test/test_btc.rb +0 -117
  57. data/test/test_cex.rb +0 -43
  58. data/test/test_cryptoapis.rb +0 -51
  59. data/test/test_fake.rb +0 -55
  60. data/test/test_firstof.rb +0 -62
  61. data/test/test_json.rb +0 -40
  62. data/test/test_live.rb +0 -138
  63. data/test/test_sibit.rb +0 -209
data/features/cli.feature CHANGED
@@ -1,3 +1,5 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
2
+ # SPDX-License-Identifier: MIT
1
3
  Feature: Command Line Processing
2
4
  As a newsletter author I want to be able to send a newsletter
3
5
 
@@ -1,3 +1,5 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
2
+ # SPDX-License-Identifier: MIT
1
3
  Feature: Gem Package
2
4
  As a source code writer I want to be able to
3
5
  package the Gem into .gem file
@@ -17,7 +19,8 @@ Feature: Gem Package
17
19
  """
18
20
  cd sibit
19
21
  gem build sibit.gemspec
20
- gem specification --ruby sibit-*.gem > ../spec.rb
22
+ gemfile=$(ls -t sibit-*.gem | head -1)
23
+ gem specification --ruby "$gemfile" > ../spec.rb
21
24
  cd ..
22
25
  ruby execs.rb
23
26
  """
@@ -1,24 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2019-2023 Yegor Bugayenko
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the 'Software'), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in all
13
- # copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- # SOFTWARE.
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
22
5
 
23
6
  require 'nokogiri'
24
7
  require 'tmpdir'
@@ -1,24 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2019-2023 Yegor Bugayenko
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the 'Software'), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in all
13
- # copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- # SOFTWARE.
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
22
5
 
23
6
  require 'simplecov'
24
7
  require 'aruba/cucumber'
data/lib/sibit/bestof.rb CHANGED
@@ -1,39 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2019-2023 Yegor Bugayenko
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the 'Software'), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in all
13
- # copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- # SOFTWARE.
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
22
5
 
23
6
  require 'backtrace'
7
+ require 'loog'
24
8
  require_relative 'error'
25
- require_relative 'log'
26
9
 
27
10
  # API best of.
28
11
  #
29
12
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
30
- # Copyright:: Copyright (c) 2019-2023 Yegor Bugayenko
13
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
31
14
  # License:: MIT
32
15
  class Sibit
33
16
  # Best of API.
34
17
  class BestOf
35
18
  # Constructor.
36
- def initialize(list, log: Sibit::Log.new, verbose: false)
19
+ def initialize(list, log: Loog::NULL, verbose: false)
37
20
  @list = list
38
21
  @log = log
39
22
  @verbose = verbose
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'digest'
7
+
8
+ class Sibit
9
+ # Bitcoin primitives module.
10
+ #
11
+ # Pure Ruby implementation of Bitcoin functionality using OpenSSL 3.0+.
12
+ # Replaces the bitcoin-ruby dependency which is incompatible with OpenSSL 3.0.
13
+ module Bitcoin
14
+ MIN_TX_FEE = 10_000
15
+
16
+ # Base58 encoding for Bitcoin addresses.
17
+ #
18
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
19
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
20
+ # License:: MIT
21
+ module Base58
22
+ ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
23
+
24
+ def self.encode(hex)
25
+ bytes = [hex].pack('H*')
26
+ leading = bytes.match(/^\x00*/)[0].length
27
+ num = hex.to_i(16)
28
+ result = ''
29
+ while num.positive?
30
+ num, remainder = num.divmod(58)
31
+ result = ALPHABET[remainder] + result
32
+ end
33
+ ('1' * leading) + result
34
+ end
35
+
36
+ def self.decode(str)
37
+ leading = str.match(/^1*/)[0].length
38
+ num = 0
39
+ str.each_char { |c| num = (num * 58) + ALPHABET.index(c) }
40
+ hex = num.zero? ? '' : num.to_s(16)
41
+ hex = "0#{hex}" if hex.length.odd?
42
+ ('00' * leading) + hex
43
+ end
44
+
45
+ def self.check(hex)
46
+ Digest::SHA256.hexdigest(Digest::SHA256.digest([hex].pack('H*')))[0...8]
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'openssl'
7
+ require 'digest'
8
+ require_relative 'base58'
9
+
10
+ class Sibit
11
+ module Bitcoin
12
+ # Bitcoin ECDSA key using secp256k1 curve.
13
+ #
14
+ # Supports OpenSSL 3.0+ by constructing keys via DER encoding instead
15
+ # of using deprecated mutable key APIs.
16
+ #
17
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
18
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
19
+ # License:: MIT
20
+ class Key
21
+ MIN_PRIV = 0x01
22
+ MAX_PRIV = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140
23
+
24
+ def self.generate
25
+ key = OpenSSL::PKey::EC.generate('secp256k1')
26
+ pvt = key.private_key.to_s(16).rjust(64, '0').downcase
27
+ new(pvt)
28
+ end
29
+
30
+ def initialize(privkey)
31
+ @privkey = privkey
32
+ @compressed = true
33
+ @key = build(privkey)
34
+ end
35
+
36
+ def priv
37
+ @privkey
38
+ end
39
+
40
+ def pub
41
+ point = @key.public_key
42
+ point.to_octet_string(@compressed ? :compressed : :uncompressed).unpack1('H*')
43
+ end
44
+
45
+ def addr
46
+ hash = hash160(pub)
47
+ versioned = "00#{hash}"
48
+ checksum = Base58.check(versioned)
49
+ Base58.encode(versioned + checksum)
50
+ end
51
+
52
+ def sign(data)
53
+ @key.sign('SHA256', data)
54
+ end
55
+
56
+ def verify(data, sig)
57
+ @key.verify('SHA256', sig, data)
58
+ rescue OpenSSL::PKey::PKeyError
59
+ false
60
+ end
61
+
62
+ private
63
+
64
+ def build(privkey)
65
+ value = privkey.to_i(16)
66
+ raise 'private key is not on curve' unless value.between?(MIN_PRIV, MAX_PRIV)
67
+ group = OpenSSL::PKey::EC::Group.new('secp256k1')
68
+ bn = OpenSSL::BN.new(privkey, 16)
69
+ pubkey = group.generator.mul(bn)
70
+ asn1 = OpenSSL::ASN1::Sequence(
71
+ [
72
+ OpenSSL::ASN1::Integer.new(1),
73
+ OpenSSL::ASN1::OctetString(bn.to_s(2)),
74
+ OpenSSL::ASN1::ObjectId('secp256k1', 0, :EXPLICIT),
75
+ OpenSSL::ASN1::BitString(pubkey.to_octet_string(:uncompressed), 1, :EXPLICIT)
76
+ ]
77
+ )
78
+ OpenSSL::PKey::EC.new(asn1.to_der)
79
+ end
80
+
81
+ def hash160(hex)
82
+ bytes = [hex].pack('H*')
83
+ Digest::RMD160.hexdigest(Digest::SHA256.digest(bytes))
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'digest'
7
+ require_relative 'base58'
8
+
9
+ class Sibit
10
+ module Bitcoin
11
+ # Bitcoin Script parser.
12
+ #
13
+ # Parses standard P2PKH scripts to extract addresses.
14
+ #
15
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
16
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
17
+ # License:: MIT
18
+ class Script
19
+ OP_DUP = 0x76
20
+ OP_HASH160 = 0xa9
21
+ OP_EQUALVERIFY = 0x88
22
+ OP_CHECKSIG = 0xac
23
+
24
+ def initialize(hex)
25
+ @bytes = [hex].pack('H*').bytes
26
+ end
27
+
28
+ def address
29
+ return p2pkh_address if p2pkh?
30
+ nil
31
+ end
32
+
33
+ def p2pkh?
34
+ @bytes.length == 25 &&
35
+ @bytes[0] == OP_DUP &&
36
+ @bytes[1] == OP_HASH160 &&
37
+ @bytes[2] == 20 &&
38
+ @bytes[23] == OP_EQUALVERIFY &&
39
+ @bytes[24] == OP_CHECKSIG
40
+ end
41
+
42
+ def hash160
43
+ return nil unless p2pkh?
44
+ @bytes[3, 20].pack('C*').unpack1('H*')
45
+ end
46
+
47
+ private
48
+
49
+ def p2pkh_address
50
+ h = hash160
51
+ return nil unless h
52
+ versioned = "00#{h}"
53
+ checksum = Base58.check(versioned)
54
+ Base58.encode(versioned + checksum)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'digest'
7
+ require_relative 'base58'
8
+ require_relative 'key'
9
+ require_relative 'script'
10
+
11
+ class Sibit
12
+ module Bitcoin
13
+ # Bitcoin Transaction structure.
14
+ #
15
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
16
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
17
+ # License:: MIT
18
+ class Tx
19
+ SIGHASH_ALL = 0x01
20
+ VERSION = 1
21
+ SEQUENCE = 0xffffffff
22
+
23
+ attr_reader :inputs, :outputs
24
+
25
+ def initialize
26
+ @inputs = []
27
+ @outputs = []
28
+ end
29
+
30
+ def add_input(hash:, index:, script:, key:)
31
+ @inputs << Input.new(hash, index, script, key)
32
+ end
33
+
34
+ def add_output(value, address)
35
+ @outputs << Output.new(value, address)
36
+ end
37
+
38
+ def hash
39
+ Digest::SHA256.hexdigest(Digest::SHA256.digest(payload)).reverse.scan(/../).join
40
+ end
41
+
42
+ def payload
43
+ sign_inputs
44
+ serialize
45
+ end
46
+
47
+ def hex
48
+ payload.unpack1('H*')
49
+ end
50
+
51
+ def in
52
+ @inputs
53
+ end
54
+
55
+ def out
56
+ @outputs
57
+ end
58
+
59
+ private
60
+
61
+ def sign_inputs
62
+ @inputs.each_with_index do |input, idx|
63
+ sighash = signature_hash(idx)
64
+ sig = sign(input.key, sighash)
65
+ pubkey = [input.key.pub].pack('H*')
66
+ input.script_sig = der_sig(sig) + pubkey_script(pubkey)
67
+ end
68
+ end
69
+
70
+ def signature_hash(idx)
71
+ tx_copy = serialize_for_signing(idx)
72
+ hash_type = [SIGHASH_ALL].pack('V')
73
+ Digest::SHA256.digest(Digest::SHA256.digest(tx_copy + hash_type))
74
+ end
75
+
76
+ def sign(key, hash)
77
+ der = key.sign(hash)
78
+ repack(der)
79
+ end
80
+
81
+ def repack(der)
82
+ return der if low_s?(der)
83
+ seq = OpenSSL::ASN1.decode(der)
84
+ r = seq.value[0].value.to_i
85
+ s = seq.value[1].value.to_i
86
+ order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
87
+ s = order - s if s > order / 2
88
+ OpenSSL::ASN1::Sequence.new(
89
+ [OpenSSL::ASN1::Integer.new(r), OpenSSL::ASN1::Integer.new(s)]
90
+ ).to_der
91
+ end
92
+
93
+ def low_s?(der)
94
+ seq = OpenSSL::ASN1.decode(der)
95
+ s = seq.value[1].value.to_i
96
+ order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
97
+ s <= order / 2
98
+ end
99
+
100
+ def der_sig(sig)
101
+ data = sig + [SIGHASH_ALL].pack('C')
102
+ [data.length].pack('C') + data
103
+ end
104
+
105
+ def pubkey_script(pubkey)
106
+ [pubkey.length].pack('C') + pubkey
107
+ end
108
+
109
+ def serialize
110
+ result = [VERSION].pack('V')
111
+ result += varint(@inputs.length)
112
+ @inputs.each do |input|
113
+ result += [input.hash].pack('H*').reverse
114
+ result += [input.index].pack('V')
115
+ result += varint(input.script_sig.length)
116
+ result += input.script_sig
117
+ result += [SEQUENCE].pack('V')
118
+ end
119
+ result += varint(@outputs.length)
120
+ @outputs.each do |output|
121
+ result += [output.value].pack('Q<')
122
+ script = output.script
123
+ result += varint(script.length)
124
+ result += script
125
+ end
126
+ result += [0].pack('V')
127
+ result
128
+ end
129
+
130
+ def serialize_for_signing(idx)
131
+ result = [VERSION].pack('V')
132
+ result += varint(@inputs.length)
133
+ @inputs.each_with_index do |input, i|
134
+ result += [input.hash].pack('H*').reverse
135
+ result += [input.index].pack('V')
136
+ if i == idx
137
+ script = [input.prev_script].pack('H*')
138
+ result += varint(script.length)
139
+ result += script
140
+ else
141
+ result += varint(0)
142
+ end
143
+ result += [SEQUENCE].pack('V')
144
+ end
145
+ result += varint(@outputs.length)
146
+ @outputs.each do |output|
147
+ result += [output.value].pack('Q<')
148
+ script = output.script
149
+ result += varint(script.length)
150
+ result += script
151
+ end
152
+ result += [0].pack('V')
153
+ result
154
+ end
155
+
156
+ def varint(num)
157
+ return [num].pack('C') if num < 0xfd
158
+ return [0xfd, num].pack('Cv') if num <= 0xffff
159
+ return [0xfe, num].pack('CV') if num <= 0xffffffff
160
+ [0xff, num].pack('CQ<')
161
+ end
162
+ end
163
+
164
+ # Transaction input.
165
+ class Input
166
+ attr_reader :hash, :index, :prev_script, :key
167
+ attr_accessor :script_sig
168
+
169
+ def initialize(hash, index, script, key)
170
+ @hash = hash
171
+ @index = index
172
+ @prev_script = script
173
+ @key = key
174
+ @script_sig = ''
175
+ end
176
+
177
+ def prev_out
178
+ [@hash].pack('H*')
179
+ end
180
+
181
+ def prev_out_index
182
+ @index
183
+ end
184
+ end
185
+
186
+ # Transaction output.
187
+ class Output
188
+ attr_reader :value
189
+
190
+ def initialize(value, address)
191
+ @value = value
192
+ @address = address
193
+ end
194
+
195
+ def script
196
+ hash160 = address_to_hash160(@address)
197
+ [0x76, 0xa9, 0x14].pack('C*') + [hash160].pack('H*') + [0x88, 0xac].pack('C*')
198
+ end
199
+
200
+ def script_hex
201
+ script.unpack1('H*')
202
+ end
203
+
204
+ private
205
+
206
+ def address_to_hash160(addr)
207
+ decoded = Base58.decode(addr)
208
+ decoded[2..41]
209
+ end
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative 'tx'
7
+ require_relative 'key'
8
+
9
+ class Sibit
10
+ module Bitcoin
11
+ # Bitcoin Transaction Builder.
12
+ #
13
+ # Provides a similar interface to Bitcoin::Builder::TxBuilder for
14
+ # building and signing Bitcoin transactions.
15
+ #
16
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
17
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
18
+ # License:: MIT
19
+ class TxBuilder
20
+ def initialize
21
+ @inputs = []
22
+ @outputs = []
23
+ end
24
+
25
+ def input
26
+ inp = InputBuilder.new
27
+ yield inp
28
+ @inputs << inp
29
+ end
30
+
31
+ def output(value, address)
32
+ @outputs << { value: value, address: address }
33
+ end
34
+
35
+ def tx(input_value:, leave_fee:, extra_fee:, change_address:)
36
+ txn = Tx.new
37
+ @inputs.each do |inp|
38
+ txn.add_input(
39
+ hash: inp.prev_out_hash,
40
+ index: inp.prev_out_idx,
41
+ script: inp.script,
42
+ key: inp.key
43
+ )
44
+ end
45
+ total_out = @outputs.sum { |o| o[:value] }
46
+ @outputs.each { |o| txn.add_output(o[:value], o[:address]) }
47
+ if leave_fee
48
+ change = input_value - total_out - extra_fee
49
+ txn.add_output(change, change_address) if change.positive?
50
+ end
51
+ BuiltTx.new(txn, @inputs, @outputs)
52
+ end
53
+ end
54
+
55
+ # Input builder for collecting input parameters.
56
+ class InputBuilder
57
+ attr_reader :prev_out_hash, :prev_out_idx, :script, :key
58
+
59
+ def prev_out(hash)
60
+ @prev_out_hash = hash
61
+ end
62
+
63
+ def prev_out_index(idx)
64
+ @prev_out_idx = idx
65
+ end
66
+
67
+ def prev_out_script=(scr)
68
+ @script = scr
69
+ end
70
+
71
+ def signature_key(key)
72
+ @key = key
73
+ end
74
+ end
75
+
76
+ # Wrapper for built transaction with convenience methods.
77
+ class BuiltTx
78
+ def initialize(txn, inputs, outputs)
79
+ @tx = txn
80
+ @inputs_data = inputs
81
+ @outputs_data = outputs
82
+ end
83
+
84
+ def hash
85
+ @tx.hash
86
+ end
87
+
88
+ def in
89
+ @tx.in
90
+ end
91
+
92
+ def out
93
+ @tx.out
94
+ end
95
+
96
+ def inputs
97
+ @tx.inputs
98
+ end
99
+
100
+ def outputs
101
+ @tx.outputs
102
+ end
103
+
104
+ def to_payload
105
+ PayloadWrapper.new(@tx.payload)
106
+ end
107
+ end
108
+
109
+ # Wrapper for payload with hex conversion.
110
+ class PayloadWrapper
111
+ def initialize(bytes)
112
+ @bytes = bytes
113
+ end
114
+
115
+ def bth
116
+ @bytes.unpack1('H*')
117
+ end
118
+ end
119
+ end
120
+ end
@@ -1,44 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Copyright (c) 2019-2023 Yegor Bugayenko
4
- #
5
- # Permission is hereby granted, free of charge, to any person obtaining a copy
6
- # of this software and associated documentation files (the 'Software'), to deal
7
- # in the Software without restriction, including without limitation the rights
8
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- # copies of the Software, and to permit persons to whom the Software is
10
- # furnished to do so, subject to the following conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be included in all
13
- # copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
18
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- # SOFTWARE.
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
22
5
 
23
6
  require 'iri'
24
7
  require 'json'
25
8
  require 'uri'
26
9
  require_relative 'error'
27
10
  require_relative 'http'
11
+ require 'loog'
28
12
  require_relative 'json'
29
- require_relative 'log'
30
13
  require_relative 'version'
31
14
 
32
15
  # Bitcoinchain.com API.
33
16
  #
34
17
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
35
- # Copyright:: Copyright (c) 2019-2023 Yegor Bugayenko
18
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
36
19
  # License:: MIT
37
20
  class Sibit
38
21
  # Btc.com API.
39
22
  class Bitcoinchain
40
23
  # Constructor.
41
- def initialize(log: Sibit::Log.new, http: Sibit::Http.new, dry: false)
24
+ def initialize(log: Loog::NULL, http: Sibit::Http.new, dry: false)
42
25
  @http = http
43
26
  @log = log
44
27
  @dry = dry