sibit 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,7 +27,7 @@ class Sibit::Cryptoapis
27
27
 
28
28
  # Current price of BTC in USD (float returned).
29
29
  def price(_currency = 'USD')
30
- raise Sibit::NotSupportedError, 'Cryptoapis doesn\'t provide BTC price'
30
+ raise(Sibit::NotSupportedError, 'Cryptoapis doesn\'t provide BTC price')
31
31
  end
32
32
 
33
33
  # Get hash of the block after this one.
@@ -43,29 +43,31 @@ class Sibit::Cryptoapis
43
43
 
44
44
  # The height of the block.
45
45
  def height(hash)
46
- json = Sibit::Json.new(http: @http, log: @log).get(
46
+ h = Sibit::Json.new(http: @http, log: @log).get(
47
47
  Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/blocks').append(hash),
48
48
  headers: headers
49
- )['payload']
50
- h = json['height']
49
+ )['payload']['height']
51
50
  @log.debug("The height of #{hash} is #{h}")
52
51
  h
53
52
  end
54
53
 
55
54
  # Gets the balance of the address, in satoshi.
56
55
  def balance(address)
57
- json = Sibit::Json.new(http: @http, log: @log).get(
58
- Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/address').append(address),
59
- headers: headers
60
- )['payload']
61
- b = (json['balance'].to_f * 100_000_000).to_i
56
+ b = Integer(
57
+ Float(
58
+ Sibit::Json.new(http: @http, log: @log).get(
59
+ Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/address').append(address),
60
+ headers: headers
61
+ )['payload']['balance']
62
+ ) * 100_000_000
63
+ )
62
64
  @log.debug("The balance of #{address} is #{b} satoshi")
63
65
  b
64
66
  end
65
67
 
66
68
  # Get recommended fees, in satoshi per byte.
67
69
  def fees
68
- raise Sibit::NotSupportedError, 'Cryptoapis doesn\'t provide recommended fees'
70
+ raise(Sibit::NotSupportedError, 'Cryptoapis doesn\'t provide recommended fees')
69
71
  end
70
72
 
71
73
  # Gets the hash of the latest block.
@@ -80,13 +82,13 @@ class Sibit::Cryptoapis
80
82
 
81
83
  # Fetch all unspent outputs per address.
82
84
  def utxos(_sources)
83
- raise Sibit::NotSupportedError, 'Not implemented yet'
85
+ raise(Sibit::NotSupportedError, 'Not implemented yet')
84
86
  end
85
87
 
86
88
  # Push this transaction (in hex format) to the network.
87
89
  def push(hex)
88
90
  Sibit::Json.new(http: @http, log: @log).post(
89
- Iri.new('https://api.cryptoapis.io/v1/bc/btc/testnet/txs/send'),
91
+ Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/txs/send'),
90
92
  JSON.pretty_generate(hex: hex),
91
93
  headers: headers
92
94
  )
@@ -122,21 +124,22 @@ class Sibit::Cryptoapis
122
124
  limit = 200
123
125
  all = []
124
126
  loop do
125
- txns = Sibit::Json.new(http: @http, log: @log).get(
126
- Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/txs/block/')
127
- .append(hash).add(index: index, limit: limit),
128
- headers: headers
129
- )['payload'].map do |t|
130
- {
131
- hash: t['hash'],
132
- outputs: t['txouts'].map do |o|
133
- {
134
- address: o['addresses'][0],
135
- value: o['amount'].to_f * 100_000_000
136
- }
137
- end
138
- }
139
- end
127
+ txns =
128
+ Sibit::Json.new(http: @http, log: @log).get(
129
+ Iri.new('https://api.cryptoapis.io/v1/bc/btc/mainnet/txs/block/')
130
+ .append(hash).add(index: index, limit: limit),
131
+ headers: headers
132
+ )['payload'].map do |t|
133
+ {
134
+ hash: t['hash'],
135
+ outputs: t['txouts'].map do |o|
136
+ {
137
+ address: o['addresses'][0],
138
+ value: Float(o['amount'] || 0) * 100_000_000
139
+ }
140
+ end
141
+ }
142
+ end
140
143
  all += txns
141
144
  index += txns.length
142
145
  break if txns.length < limit
data/lib/sibit/firstof.rb CHANGED
@@ -83,18 +83,18 @@ class Sibit::FirstOf
83
83
  private
84
84
 
85
85
  def first_of(method)
86
- return yield @list unless @list.is_a?(Array)
86
+ return yield(@list) unless @list.is_a?(Array)
87
87
  errors = []
88
88
  done = false
89
89
  result = nil
90
90
  @list.each do |api|
91
91
  @log.debug("Calling #{api.class.name}##{method}()...")
92
92
  begin
93
- result = yield api
93
+ result = yield(api)
94
94
  done = true
95
95
  break
96
96
  rescue Sibit::NotSupportedError
97
- # Just ignore it
97
+ nil
98
98
  rescue Sibit::Error => e
99
99
  errors << e
100
100
  @log.debug("The API #{api.class.name} failed at #{method}(): #{e.message}") if @verbose
@@ -102,8 +102,11 @@ class Sibit::FirstOf
102
102
  end
103
103
  unless done
104
104
  errors.each { |e| @log.debug(Backtrace.new(e).to_s) }
105
- raise Sibit::Error, "No APIs out of #{@list.length} managed to succeed at #{method}(): \
106
- #{@list.map { |a| a.class.name }.join(', ')}"
105
+ raise(
106
+ Sibit::Error,
107
+ "No APIs out of #{@list.length} managed to succeed at #{method}(): " \
108
+ "#{@list.map { |a| a.class.name }.join(', ')}"
109
+ )
107
110
  end
108
111
  result
109
112
  end
@@ -22,7 +22,7 @@ class Sibit
22
22
  attr_reader :host
23
23
 
24
24
  def client(uri)
25
- http = Net::HTTP.new(uri.host, uri.port, @host, @port.to_i, @user, @password)
25
+ http = Net::HTTP.new(uri.host, uri.port, @host, Integer(@port, 10), @user, @password)
26
26
  http.use_ssl = true
27
27
  http.read_timeout = 240
28
28
  http
data/lib/sibit/json.rb CHANGED
@@ -43,16 +43,16 @@ class Sibit::Json
43
43
  'Accept-Encoding' => ''
44
44
  }.merge(headers)
45
45
  )
46
- unless accept.include?(res.code.to_i)
47
- raise Sibit::Error, "Failed to retrieve #{uri} (#{res.code}): #{res.body}"
46
+ unless accept.include?(Integer(res.code, 10))
47
+ raise(Sibit::Error, "Failed to retrieve #{uri} (#{res.code}): #{res.body}")
48
48
  end
49
49
  ret =
50
50
  begin
51
51
  JSON.parse(res.body)
52
52
  rescue JSON::ParserError => e
53
- raise Sibit::Error, "Can't parse JSON: #{e.message}"
53
+ raise(Sibit::Error, "Can't parse JSON: #{e.message}")
54
54
  end
55
- throw :"GET #{uri}: #{res.code}/#{length(res.body.length)}"
55
+ throw(:"GET #{uri}: #{res.code}/#{length(res.body.length)}")
56
56
  end
57
57
  ret
58
58
  end
@@ -61,7 +61,7 @@ class Sibit::Json
61
61
  uri = URI(address.to_s)
62
62
  elapsed(@log) do
63
63
  res = @http.client(uri).post(
64
- "#{uri.path}?#{uri.query}",
64
+ "#{uri.path.empty? ? '/' : uri.path}#{"?#{uri.query}" if uri.query}",
65
65
  "tx=#{CGI.escape(body)}",
66
66
  {
67
67
  'Accept' => 'text/plain',
@@ -72,9 +72,9 @@ class Sibit::Json
72
72
  }.merge(headers)
73
73
  )
74
74
  unless res.code == '200'
75
- raise Sibit::Error, "Failed to post tx to #{uri}: #{res.code}\n#{res.body}"
75
+ raise(Sibit::Error, "Failed to post tx to #{uri}: #{res.code}\n#{res.body}")
76
76
  end
77
- throw :"POST #{uri}: #{res.code}"
77
+ throw(:"POST #{uri}: #{res.code}")
78
78
  end
79
79
  end
80
80
 
data/lib/sibit/key.rb CHANGED
@@ -32,13 +32,14 @@ class Sibit
32
32
  def self.generate(network: :mainnet)
33
33
  key = OpenSSL::PKey::EC.generate('secp256k1')
34
34
  pvt = key.private_key
35
- raise 'Invalid private key: zero' if pvt.zero?
36
- raise 'Invalid private key: out of range' if pvt >= SECP256K1_N
37
- raise 'Invalid public key: not on curve' unless key.public_key.on_curve?
35
+ raise(Sibit::Error, 'Invalid private key: zero') if pvt.zero?
36
+ raise(Sibit::Error, 'Invalid private key: out of range') if pvt >= SECP256K1_N
37
+ raise(Sibit::Error, 'Invalid public key: not on curve') unless key.public_key.on_curve?
38
38
  hex = key.private_key.to_s(16).rjust(64, '0').downcase
39
- raise 'Invalid private key encoding' unless hex.match?(/\A[0-9a-f]{64}\z/)
40
- trip = OpenSSL::BN.new(hex, 16)
41
- raise 'Private key serialization is lossy' unless trip == pvt
39
+ raise(Sibit::Error, 'Invalid private key encoding') unless hex.match?(/\A[0-9a-f]{64}\z/)
40
+ unless OpenSSL::BN.new(hex, 16) == pvt
41
+ raise(Sibit::Error, 'Private key serialization is lossy')
42
+ end
42
43
  new(hex, network: network)
43
44
  end
44
45
 
@@ -55,42 +56,38 @@ class Sibit
55
56
  end
56
57
 
57
58
  def pub
58
- point = @key.public_key
59
- point.to_octet_string(@compressed ? :compressed : :uncompressed).unpack1('H*')
59
+ @key.public_key.to_octet_string(@compressed ? :compressed : :uncompressed).unpack1('H*')
60
60
  end
61
61
 
62
62
  def bech32
63
63
  hrp = { mainnet: 'bc', testnet: 'tb', regtest: 'bcrt' }[@network]
64
64
  hex = pub
65
- raise Error, 'Invalid public key: not on curve' unless @key.public_key.on_curve?
66
- raise Error, 'Invalid public key format' unless hex.match?(/\A0[23][0-9a-f]{64}\z/)
65
+ raise(Error, 'Invalid public key: not on curve') unless @key.public_key.on_curve?
66
+ raise(Error, 'Invalid public key format') unless hex.match?(/\A0[23][0-9a-f]{64}\z/)
67
67
  addr = Bech32.encode(hrp, 0, hash160(hex))
68
- expected = /\A#{hrp}1q[a-z0-9]{38,58}\z/
69
- raise Error, "Invalid bech32 address: #{addr}" unless addr.match?(expected)
68
+ unless addr.match?(/\A#{hrp}1q[a-z0-9]{38,58}\z/)
69
+ raise(Error, "Invalid bech32 address: #{addr}")
70
+ end
70
71
  addr
71
72
  end
72
73
 
73
74
  def base58
74
75
  hex = pub
75
- raise Error, 'Invalid public key: not on curve' unless @key.public_key.on_curve?
76
- raise Error, 'Invalid public key format' unless hex.match?(/\A0[23][0-9a-f]{64}\z/)
77
- hash = hash160(hex)
78
- prefix = @network == :mainnet ? '00' : '6f'
79
- versioned = "#{prefix}#{hash}"
80
- checksum = Base58.new(versioned).check
81
- addr = Base58.new(versioned + checksum).encode
76
+ raise(Error, 'Invalid public key: not on curve') unless @key.public_key.on_curve?
77
+ raise(Error, 'Invalid public key format') unless hex.match?(/\A0[23][0-9a-f]{64}\z/)
78
+ versioned = "#{@network == :mainnet ? '00' : '6f'}#{hash160(hex)}"
79
+ addr = Base58.new(versioned + Base58.new(versioned).check).encode
82
80
  mainnet = /\A1[1-9A-HJ-NP-Za-km-z]{25,34}\z/
83
81
  testnet = /\A[mn][1-9A-HJ-NP-Za-km-z]{25,34}\z/
84
82
  unless addr.match?(@network == :mainnet ? mainnet : testnet)
85
- raise Error,
86
- "Invalid base58 address: #{addr}"
83
+ raise(Error, "Invalid base58 address: #{addr}")
87
84
  end
88
85
  addr
89
86
  end
90
87
 
91
88
  def sign(data)
92
89
  sig = @key.dsa_sign_asn1(data)
93
- raise Error, 'Signature verification failed' unless verify(data, sig)
90
+ raise(Error, 'Signature verification failed') unless verify(data, sig)
94
91
  sig
95
92
  end
96
93
 
@@ -103,25 +100,29 @@ class Sibit
103
100
  private
104
101
 
105
102
  def build(privkey)
106
- value = privkey.to_i(16)
107
- raise Error, 'Private key is not on curve' unless value.between?(MIN_PRIV, MAX_PRIV)
108
- group = OpenSSL::PKey::EC::Group.new('secp256k1')
103
+ raise(Error, 'Private key is not on curve') unless privkey.to_i(16).between?(
104
+ MIN_PRIV,
105
+ MAX_PRIV
106
+ )
109
107
  bn = OpenSSL::BN.new(privkey, 16)
110
- pubkey = group.generator.mul(bn)
111
- asn1 = OpenSSL::ASN1::Sequence(
112
- [
113
- OpenSSL::ASN1::Integer.new(1),
114
- OpenSSL::ASN1::OctetString(bn.to_s(2)),
115
- OpenSSL::ASN1::ObjectId('secp256k1', 0, :EXPLICIT),
116
- OpenSSL::ASN1::BitString(pubkey.to_octet_string(:uncompressed), 1, :EXPLICIT)
117
- ]
108
+ OpenSSL::PKey::EC.new(
109
+ OpenSSL::ASN1::Sequence(
110
+ [
111
+ OpenSSL::ASN1::Integer.new(1),
112
+ OpenSSL::ASN1::OctetString(bn.to_s(2)),
113
+ OpenSSL::ASN1::ObjectId('secp256k1', 0, :EXPLICIT),
114
+ OpenSSL::ASN1::BitString(
115
+ OpenSSL::PKey::EC::Group.new('secp256k1').generator.mul(bn)
116
+ .to_octet_string(:uncompressed),
117
+ 1, :EXPLICIT
118
+ )
119
+ ]
120
+ ).to_der
118
121
  )
119
- OpenSSL::PKey::EC.new(asn1.to_der)
120
122
  end
121
123
 
122
124
  def hash160(hex)
123
- bytes = [hex].pack('H*')
124
- Digest::RMD160.hexdigest(Digest::SHA256.digest(bytes))
125
+ Digest::RMD160.hexdigest(Digest::SHA256.digest([hex].pack('H*')))
125
126
  end
126
127
 
127
128
  def decode(key)
@@ -130,12 +131,9 @@ class Sibit
130
131
  return key.downcase
131
132
  end
132
133
  raw = Base58.new(key).decode
133
- payload = raw[0..-9]
134
- checksum = raw[-8..]
135
- expected = Base58.new(payload).check
136
- raise Error, 'Invalid WIF checksum' unless checksum == expected
134
+ raise(Error, 'Invalid WIF checksum') unless raw[-8..] == Base58.new(raw[0..-9]).check
137
135
  version = raw[0, 2]
138
- raise Error, "Invalid WIF version: #{version}" unless %w[80 ef].include?(version)
136
+ raise(Error, "Invalid WIF version: #{version}") unless %w[80 ef].include?(version)
139
137
  detected = version == '80' ? :mainnet : :testnet
140
138
  @network = @override || detected
141
139
  body = raw[2..-9]
data/lib/sibit/script.rb CHANGED
@@ -59,17 +59,14 @@ class Sibit
59
59
  def p2pkh_address(network)
60
60
  h = hash160
61
61
  return nil unless h
62
- prefix = network == :mainnet ? '00' : '6f'
63
- versioned = "#{prefix}#{h}"
64
- checksum = Base58.new(versioned).check
65
- Base58.new(versioned + checksum).encode
62
+ versioned = "#{network == :mainnet ? '00' : '6f'}#{h}"
63
+ Base58.new(versioned + Base58.new(versioned).check).encode
66
64
  end
67
65
 
68
66
  def p2wpkh_address(network)
69
67
  h = hash160
70
68
  return nil unless h
71
- hrp = { mainnet: 'bc', testnet: 'tb', regtest: 'bcrt' }[network]
72
- Bech32.encode(hrp, 0, h)
69
+ Bech32.encode({ mainnet: 'bc', testnet: 'tb', regtest: 'bcrt' }[network], 0, h)
73
70
  end
74
71
  end
75
72
  end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'iri'
7
+ require 'json'
8
+ require 'loog'
9
+ require 'uri'
10
+ require_relative 'error'
11
+ require_relative 'http'
12
+ require_relative 'json'
13
+ require_relative 'version'
14
+
15
+ # SoChain API (formerly known as Block.io).
16
+ #
17
+ # Documentation: https://sochain.com/api
18
+ #
19
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
20
+ # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
21
+ # License:: MIT
22
+ class Sibit::Sochain
23
+ # Constructor.
24
+ def initialize(log: Loog::NULL, http: Sibit::Http.new, network: 'BTC')
25
+ @http = http
26
+ @log = log
27
+ @network = network
28
+ end
29
+
30
+ # Current price of BTC in USD (float returned).
31
+ def price(currency = 'USD')
32
+ data = Sibit::Json.new(http: @http, log: @log).get(
33
+ Iri.new('https://sochain.com/api/v2/get_price').append(@network).append(currency)
34
+ )['data']
35
+ raise(Sibit::Error, "No price data returned for #{@network}/#{currency}") if data.nil?
36
+ prices = data['prices']
37
+ if prices.nil? || prices.empty?
38
+ raise(Sibit::Error, "No price quotes for #{@network}/#{currency}")
39
+ end
40
+ price = Float(prices[0]['price'])
41
+ @log.debug("The price of #{@network} is #{price} #{currency}")
42
+ price
43
+ end
44
+
45
+ # The height of the block, identified by hash.
46
+ def height(hash)
47
+ data = Sibit::Json.new(http: @http, log: @log).get(
48
+ Iri.new('https://sochain.com/api/v2/block').append(@network).append(hash)
49
+ )['data']
50
+ raise(Sibit::Error, "The block #{hash} not found") if data.nil?
51
+ h = data['block_no']
52
+ raise(Sibit::Error, "The block #{hash} found but the height is absent") if h.nil?
53
+ @log.debug("The height of #{hash} is #{h}")
54
+ Integer(h)
55
+ end
56
+
57
+ # Get hash of the block after this one.
58
+ def next_of(hash)
59
+ data = Sibit::Json.new(http: @http, log: @log).get(
60
+ Iri.new('https://sochain.com/api/v2/block').append(@network).append(hash)
61
+ )['data']
62
+ raise(Sibit::Error, "The block #{hash} not found") if data.nil?
63
+ nxt = data['next_blockhash']
64
+ nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
65
+ @log.debug("In SoChain the block #{hash} is the latest, there is no next block") if nxt.nil?
66
+ @log.debug("The next block of #{hash} is #{nxt}") unless nxt.nil?
67
+ nxt
68
+ end
69
+
70
+ # Gets the balance of the address, in satoshi.
71
+ def balance(address)
72
+ data = Sibit::Json.new(http: @http, log: @log).get(
73
+ Iri.new('https://sochain.com/api/v2/get_address_balance').append(@network).append(address)
74
+ )['data']
75
+ if data.nil?
76
+ @log.debug("The balance of #{address} is probably zero (not found)")
77
+ return 0
78
+ end
79
+ confirmed = data['confirmed_balance']
80
+ if confirmed.nil?
81
+ @log.debug("The balance of #{address} is probably zero (no confirmed_balance)")
82
+ return 0
83
+ end
84
+ b = Integer((Float(confirmed) * 100_000_000).round)
85
+ @log.debug("The balance of #{address} is #{b} satoshi")
86
+ b
87
+ end
88
+
89
+ # Get recommended fees, in satoshi per byte.
90
+ def fees
91
+ raise(Sibit::NotSupportedError, 'SoChain doesn\'t provide recommended fees')
92
+ end
93
+
94
+ # Gets the hash of the latest block.
95
+ def latest
96
+ data = Sibit::Json.new(http: @http, log: @log).get(
97
+ Iri.new('https://sochain.com/api/v2/get_info').append(@network)
98
+ )['data']
99
+ raise(Sibit::Error, 'The latest block info not found') if data.nil?
100
+ hash = data['blockhash']
101
+ raise(Sibit::Error, 'The latest block hash is absent') if hash.nil?
102
+ @log.debug("The latest block hash is #{hash}")
103
+ hash
104
+ end
105
+
106
+ # Fetch all unspent outputs per address. The argument is an array
107
+ # of Bitcoin addresses.
108
+ def utxos(sources)
109
+ out = []
110
+ sources.each do |address|
111
+ data = Sibit::Json.new(http: @http, log: @log).get(
112
+ Iri.new('https://sochain.com/api/v2/get_tx_unspent').append(@network).append(address)
113
+ )['data']
114
+ next if data.nil?
115
+ txs = data['txs']
116
+ next if txs.nil?
117
+ txs.each do |u|
118
+ out << {
119
+ value: Integer((Float(u['value']) * 100_000_000).round),
120
+ hash: u['txid'],
121
+ index: u['output_no'],
122
+ confirmations: u['confirmations'],
123
+ script: [u['script_hex']].pack('H*')
124
+ }
125
+ end
126
+ end
127
+ out
128
+ end
129
+
130
+ # Push this transaction (in hex format) to the network. SoChain expects a
131
+ # JSON payload with the +tx_hex+ field on POST /send_tx/{network}.
132
+ def push(hex)
133
+ uri = URI(Iri.new('https://sochain.com/api/v2/send_tx').append(@network).to_s)
134
+ res = @http.client(uri).post(
135
+ uri.path,
136
+ JSON.generate(tx_hex: hex),
137
+ {
138
+ 'Accept' => 'application/json',
139
+ 'Content-Type' => 'application/json',
140
+ 'Accept-Charset' => 'UTF-8',
141
+ 'Accept-Encoding' => ''
142
+ }
143
+ )
144
+ unless res.code == '200'
145
+ raise(Sibit::Error, "Failed to push transaction to #{uri}: #{res.code}\n#{res.body}")
146
+ end
147
+ @log.debug("Transaction (#{hex.length} chars in hex) has been pushed to SoChain")
148
+ end
149
+
150
+ # This method should fetch a block and return it as a hash.
151
+ def block(hash)
152
+ data = Sibit::Json.new(http: @http, log: @log).get(
153
+ Iri.new('https://sochain.com/api/v2/block').append(@network).append(hash)
154
+ )['data']
155
+ raise(Sibit::Error, "The block #{hash} not found") if data.nil?
156
+ nxt = data['next_blockhash']
157
+ nxt = nil if nxt == '0000000000000000000000000000000000000000000000000000000000000000'
158
+ {
159
+ provider: self.class.name,
160
+ hash: data['blockhash'],
161
+ orphan: data['is_orphan'] == true,
162
+ next: nxt,
163
+ previous: data['previous_blockhash'],
164
+ txns: (data['txs'] || []).map do |t|
165
+ {
166
+ hash: t['txid'] || t,
167
+ outputs: ((t.is_a?(Hash) ? t['outputs'] : nil) || []).map do |o|
168
+ {
169
+ address: o['address'],
170
+ value: Integer((Float(o['value']) * 100_000_000).round)
171
+ }
172
+ end
173
+ }
174
+ end
175
+ }
176
+ end
177
+ end
data/lib/sibit/tx.rb CHANGED
@@ -6,6 +6,7 @@
6
6
  require 'digest'
7
7
  require_relative 'base58'
8
8
  require_relative 'bech32'
9
+ require_relative 'error'
9
10
  require_relative 'key'
10
11
  require_relative 'script'
11
12
 
@@ -105,7 +106,9 @@ class Sibit
105
106
 
106
107
  def script
107
108
  return segwit_script if segwit?
108
- p2pkh_script
109
+ return p2pkh_script if %w[00 6f].include?(version)
110
+ return p2sh_script if %w[05 c4].include?(version)
111
+ raise(Sibit::Error, "Address '#{@address}' has an unsupported version byte 0x#{version}")
109
112
  end
110
113
 
111
114
  def segwit?
@@ -118,17 +121,25 @@ class Sibit
118
121
 
119
122
  private
120
123
 
124
+ def version
125
+ Base58.new(@address).decode[0, 2]
126
+ end
127
+
128
+ def hash160
129
+ [Base58.new(@address).decode[2, 40]].pack('H*')
130
+ end
131
+
121
132
  def p2pkh_script
122
- decoded = Base58.new(@address).decode
123
- hash = decoded[2..41]
124
- [0x76, 0xa9, 0x14].pack('C*') + [hash].pack('H*') + [0x88, 0xac].pack('C*')
133
+ [0x76, 0xa9, 0x14].pack('C*') + hash160 + [0x88, 0xac].pack('C*')
134
+ end
135
+
136
+ def p2sh_script
137
+ [0xa9, 0x14].pack('C*') + hash160 + [0x87].pack('C*')
125
138
  end
126
139
 
127
140
  def segwit_script
128
- bech = Bech32.new(@address)
129
- witness = bech.witness
130
- len = witness.length / 2
131
- [0x00, len].pack('C*') + [witness].pack('H*')
141
+ witness = Bech32.new(@address).witness
142
+ [0x00, (witness.length / 2)].pack('C*') + [witness].pack('H*')
132
143
  end
133
144
  end
134
145
 
@@ -140,12 +151,10 @@ class Sibit
140
151
 
141
152
  def sign_inputs
142
153
  @inputs.each_with_index do |input, idx|
143
- sighash = input.segwit? ? segwit_sighash(idx) : legacy_sighash(idx)
144
- sig = sign(input.key, sighash)
154
+ sig = sign(input.key, (input.segwit? ? segwit_sighash(idx) : legacy_sighash(idx)))
145
155
  pubkey = [input.key.pub].pack('H*')
146
156
  if input.segwit?
147
- witness_sig = sig + [SIGHASH_ALL].pack('C')
148
- input.witness = [witness_sig.bytes, pubkey.bytes]
157
+ input.witness = [(sig + [SIGHASH_ALL].pack('C')).bytes, pubkey.bytes]
149
158
  else
150
159
  input.script_sig = der_sig(sig) + pubkey_script(pubkey)
151
160
  end
@@ -153,9 +162,9 @@ class Sibit
153
162
  end
154
163
 
155
164
  def legacy_sighash(idx)
156
- tx_copy = serialize_for_signing(idx)
157
- hash_type = [SIGHASH_ALL].pack('V')
158
- Digest::SHA256.digest(Digest::SHA256.digest(tx_copy + hash_type))
165
+ Digest::SHA256.digest(
166
+ Digest::SHA256.digest(serialize_for_signing(idx) + [SIGHASH_ALL].pack('V'))
167
+ )
159
168
  end
160
169
 
161
170
  def segwit_sighash(idx)
@@ -175,48 +184,55 @@ class Sibit
175
184
  end
176
185
 
177
186
  def hash_prevouts
178
- data = @inputs.map { |i| [i.hash].pack('H*').reverse + [i.index].pack('V') }.join
179
- Digest::SHA256.digest(Digest::SHA256.digest(data))
187
+ Digest::SHA256.digest(
188
+ Digest::SHA256.digest(
189
+ @inputs.map do |i|
190
+ [i.hash].pack('H*').reverse + [i.index].pack('V')
191
+ end.join
192
+ )
193
+ )
180
194
  end
181
195
 
182
196
  def hash_sequence
183
- data = @inputs.map { [SEQUENCE].pack('V') }.join
184
- Digest::SHA256.digest(Digest::SHA256.digest(data))
197
+ Digest::SHA256.digest(Digest::SHA256.digest(@inputs.map { [SEQUENCE].pack('V') }.join))
185
198
  end
186
199
 
187
200
  def hash_outputs
188
- data = @outputs.map { |o| [o.value].pack('Q<') + varint(o.script.length) + o.script }.join
189
- Digest::SHA256.digest(Digest::SHA256.digest(data))
201
+ Digest::SHA256.digest(
202
+ Digest::SHA256.digest(
203
+ @outputs.map do |o|
204
+ [o.value].pack('Q<') + varint(o.script.length) + o.script
205
+ end.join
206
+ )
207
+ )
190
208
  end
191
209
 
192
210
  def script_code(input)
193
- hash160 = input.prev_script[4..]
194
- code = [0x76, 0xa9, 0x14].pack('C*') + [hash160].pack('H*') + [0x88, 0xac].pack('C*')
211
+ code = [
212
+ 0x76, 0xa9,
213
+ 0x14
214
+ ].pack('C*') + [input.prev_script[4..]].pack('H*') + [0x88, 0xac].pack('C*')
195
215
  varint(code.length) + code
196
216
  end
197
217
 
198
218
  def sign(key, hash)
199
- der = key.sign(hash)
200
- repack(der)
219
+ repack(key.sign(hash))
201
220
  end
202
221
 
203
222
  def repack(der)
204
223
  return der if low_s?(der)
205
224
  seq = OpenSSL::ASN1.decode(der)
206
- r = seq.value[0].value.to_i
207
- s = seq.value[1].value.to_i
225
+ s = Integer(seq.value[1].value)
208
226
  order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
209
227
  s = order - s if s > order / 2
210
228
  OpenSSL::ASN1::Sequence.new(
211
- [OpenSSL::ASN1::Integer.new(r), OpenSSL::ASN1::Integer.new(s)]
229
+ [OpenSSL::ASN1::Integer.new(Integer(seq.value[0].value)), OpenSSL::ASN1::Integer.new(s)]
212
230
  ).to_der
213
231
  end
214
232
 
215
233
  def low_s?(der)
216
- seq = OpenSSL::ASN1.decode(der)
217
- s = seq.value[1].value.to_i
218
- order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
219
- s <= order / 2
234
+ Integer(OpenSSL::ASN1.decode(der).value[1].value) <=
235
+ 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 / 2
220
236
  end
221
237
 
222
238
  def der_sig(sig)