sibit 0.25.1 → 0.27.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 +80 -60
  7. data/REUSE.toml +35 -0
  8. data/Rakefile +3 -29
  9. data/bin/sibit +9 -28
  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/error.rb +3 -20
  28. data/lib/sibit/fake.rb +3 -20
  29. data/lib/sibit/firstof.rb +5 -22
  30. data/lib/sibit/http.rb +3 -20
  31. data/lib/sibit/json.rb +6 -23
  32. data/lib/sibit/version.rb +4 -21
  33. data/lib/sibit.rb +31 -40
  34. data/logo.svg +1 -1
  35. data/sibit.gemspec +16 -32
  36. metadata +26 -48
  37. data/.0pdd.yml +0 -9
  38. data/.gitattributes +0 -7
  39. data/.github/workflows/codecov.yml +0 -21
  40. data/.github/workflows/pdd.yml +0 -15
  41. data/.github/workflows/rake.yml +0 -24
  42. data/.github/workflows/xcop.yml +0 -17
  43. data/.gitignore +0 -8
  44. data/.pdd +0 -7
  45. data/.rubocop.yml +0 -38
  46. data/.rultor.yml +0 -21
  47. data/.simplecov +0 -40
  48. data/lib/sibit/earn.rb +0 -102
  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/bin/sibit CHANGED
@@ -1,25 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Copyright (c) 2019-2023 Yegor Bugayenko
5
- #
6
- # Permission is hereby granted, free of charge, to any person obtaining a copy
7
- # of this software and associated documentation files (the 'Software'), to deal
8
- # in the Software without restriction, including without limitation the rights
9
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- # copies of the Software, and to permit persons to whom the Software is
11
- # furnished to do so, subject to the following conditions:
12
- #
13
- # The above copyright notice and this permission notice shall be included in all
14
- # copies or substantial portions of the Software.
15
- #
16
- # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
19
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- # SOFTWARE.
4
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
5
+ # SPDX-License-Identifier: MIT
23
6
 
24
7
  $stdout.sync = true
25
8
 
@@ -28,19 +11,19 @@ require 'openssl'
28
11
  OpenSSL::SSL::VERIFY_PEER ||= OpenSSL::SSL::VERIFY_NONE
29
12
  puts OpenSSL::X509::DEFAULT_CERT_FILE
30
13
 
31
- require 'slop'
32
14
  require 'backtrace'
15
+ require 'loog'
33
16
  require 'retriable_proxy'
17
+ require 'slop'
34
18
  require_relative '../lib/sibit'
35
- require_relative '../lib/sibit/version'
19
+ require_relative '../lib/sibit/bitcoinchain'
36
20
  require_relative '../lib/sibit/blockchain'
37
21
  require_relative '../lib/sibit/blockchair'
38
22
  require_relative '../lib/sibit/btc'
39
- require_relative '../lib/sibit/bitcoinchain'
40
23
  require_relative '../lib/sibit/cex'
41
- require_relative '../lib/sibit/earn'
42
24
  require_relative '../lib/sibit/fake'
43
25
  require_relative '../lib/sibit/firstof'
26
+ require_relative '../lib/sibit/version'
44
27
 
45
28
  begin
46
29
  begin
@@ -69,8 +52,8 @@ Options are:"
69
52
  o.bool '--verbose', 'Print all possible debug messages'
70
53
  o.array(
71
54
  '--api',
72
- 'Ordered List of APIs to use, e.g. "earn,blockchain,btc,bitcoinchain"',
73
- default: %w[earn blockchain btc bitcoinchain blockchair cex]
55
+ 'Ordered List of APIs to use, e.g. "eblockchain,btc,bitcoinchain"',
56
+ default: %w[blockchain btc bitcoinchain blockchair cex]
74
57
  )
75
58
  o.array(
76
59
  '--skip-utxo',
@@ -82,7 +65,7 @@ Options are:"
82
65
  raise e.message
83
66
  end
84
67
  raise 'Try --help' if opts.arguments.empty?
85
- log = Sibit::Log.new(opts[:verbose] ? $stdout : nil)
68
+ log = opts[:verbose] ? Loog::VERBOSE : Loog::NULL
86
69
  http = opts[:proxy] ? Sibit::HttpProxy.new(opts[:proxy]) : Sibit::Http.new
87
70
  apis = opts[:api].map(&:downcase).map do |a|
88
71
  api = nil
@@ -99,8 +82,6 @@ Options are:"
99
82
  api = Sibit::Cex.new(http: http, log: log, dry: opts[:dry])
100
83
  when 'fake'
101
84
  api = Sibit::Fake.new
102
- when 'earn'
103
- api = Sibit::Earn.new(http: http, log: log, dry: opts[:dry])
104
85
  else
105
86
  raise Sibit::Error, "Unknown API \"#{a}\""
106
87
  end
data/cucumber.yml CHANGED
@@ -1,3 +1,6 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
2
+ # SPDX-License-Identifier: MIT
3
+ ---
1
4
  default: --format pretty
2
5
  travis: --format progress
3
6
  html_report: --format progress --format html --out=features_report.html
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