sibit 0.28.0 → 0.29.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7846df06f5fbb2cae2ee00122f75f640901fc2d2d5321d810ae0848ee56c01e2
4
- data.tar.gz: 1ff75f919747d146c038c8cdfcc2cefbbd5dbf45136a5102691740ab7224dbd2
3
+ metadata.gz: ddde20b613a744a0618b9b91d4adfea42d945a13d778ecad2492b1a15afe3c0e
4
+ data.tar.gz: 7d83be599123ff738035fbf610d1ae6f763c5c36cec2a83a626658707cf37cda
5
5
  SHA512:
6
- metadata.gz: 892d1c222af88708788f791a8508c7622f3e148190c7aa2cf4a8c6fbb3a47fdd2dd78ebd0190512fb11c0a512ac4d474153c2467bf54a4cb4f7afa260d732b1f
7
- data.tar.gz: b9d78f193a4e81d5c9659528927c6526f6d2413f65303bd016b6fdd220c0afac25c0555f05ddeaf34ab5e0d9fd257ba8d0f5f70b31667eeb1ebc549cab232bc6
6
+ metadata.gz: 9811599df9ebb242927bcf50a8ed2b1e24adad74832b72748b1f1c113c9f30e89c0b9079f3a0898308963c55c59d0f899534d529d673f0120a718176402a4e10
7
+ data.tar.gz: 2f3821bb817a4fadbe8137ccce649d66ba388ae17f0a3b99f6293947e0d3ef084d2d0f1501fabcbec8d933b7127d655a1fae74bdb907019ae39e088d4bd6bf52
data/Gemfile.lock CHANGED
@@ -72,6 +72,7 @@ GEM
72
72
  logger (~> 1.0)
73
73
  memoist3 (1.0.0)
74
74
  mini_mime (1.1.5)
75
+ mini_portile2 (2.8.9)
75
76
  minitest (6.0.1)
76
77
  prism (~> 1.5)
77
78
  minitest-reporters (1.7.1)
@@ -80,9 +81,8 @@ GEM
80
81
  minitest (>= 5.0)
81
82
  ruby-progressbar
82
83
  multi_test (1.1.0)
83
- nokogiri (1.18.10-arm64-darwin)
84
- racc (~> 1.4)
85
- nokogiri (1.18.10-x86_64-linux-gnu)
84
+ nokogiri (1.18.10)
85
+ mini_portile2 (~> 2.8.2)
86
86
  racc (~> 1.4)
87
87
  openssl (4.0.0)
88
88
  parallel (1.27.0)
data/README.md CHANGED
@@ -7,15 +7,13 @@
7
7
  [![rake](https://github.com/yegor256/sibit/actions/workflows/rake.yml/badge.svg)](https://github.com/yegor256/sibit/actions/workflows/rake.yml)
8
8
  [![PDD status](https://www.0pdd.com/svg?name=yegor256/sibit)](https://www.0pdd.com/p?name=yegor256/sibit)
9
9
  [![Gem Version](https://badge.fury.io/rb/sibit.svg)](https://badge.fury.io/rb/sibit)
10
- [![Maintainability](https://api.codeclimate.com/v1/badges/74c909f06d4afa0d8001/maintainability)](https://codeclimate.com/github/yegor256/sibit/maintainability)
11
10
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/yegor256/takes/sibit/master/LICENSE.txt)
12
11
  [![Test Coverage](https://img.shields.io/codecov/c/github/yegor256/sibit.svg)](https://codecov.io/github/yegor256/sibit?branch=master)
13
12
  [![Hits-of-Code](https://hitsofcode.com/github/yegor256/sibit)](https://hitsofcode.com/view/github/yegor256/sibit)
14
13
 
15
- To understand how the Bitcoin protocol works,
16
- I recommend you watching this [short video] and
17
- then reading this blog post of mine:
18
- [_Sibit Demonstrates How Bitcoin Works_][blog].
14
+ To understand how the Bitcoin protocol works, I recommend you watching
15
+ this [short video] and then reading this blog post of mine:
16
+ [_Sibit Demonstrates How Bitcoin Works_][blog].
19
17
 
20
18
  This is a simple Bitcoin client for use from the command line
21
19
  or from your Ruby app.
@@ -26,10 +24,10 @@ If you need something more complex, I would recommend using
26
24
  [bitcoin-ruby] for Ruby and [Electrum] as a GUI client.
27
25
 
28
26
  You may want to discuss this tool at [Bitcointalk]
29
- and give the thread a few merits.
27
+ and give the thread a few merits.
30
28
 
31
29
  This is a Ruby gem, install it first (if it doesn't work, there are
32
- some hints at the bottom of this page):
30
+ some hints at the bottom of this page):
33
31
 
34
32
  ```bash
35
33
  gem install sibit
@@ -82,18 +80,16 @@ Say, you are sending 0.5 BTC and the fee is 0.0001 BTC.
82
80
  Totally, you spend 0.5001.
83
81
  However, you can make Sibit deduct the fee from the payment amount.
84
82
  In this case you should provide a negative amount
85
- of the fee or one of `-S`, `-M`, `-L`, `-XL`.
86
- You can also say `+S`, if you want the opposite, which is the default.
83
+ of the fee or one of `S-`, `M-`, `L-`, `XL-`.
84
+ You can also say `S+`, if you want the opposite, which is the default.
87
85
 
88
86
  It is recommended to run it with `--dry --verbose` options first,
89
87
  to see what's going to be sent to the network.
90
88
  If everything looks correct, remove the `--dry` and run again,
91
89
  the transaction is pushed to the network.
92
90
 
93
- All operations are performed through the
94
- [Blockchain API].
95
- Transactions are pushed to the Bitcoin network via
96
- [this relay].
91
+ All operations are performed through the [Blockchain API].
92
+ Transactions are pushed to the Bitcoin network via [this relay].
97
93
 
98
94
  ## Ruby SDK
99
95
 
@@ -115,8 +111,8 @@ It should work.
115
111
  ## APIs
116
112
 
117
113
  The library works through one (or a few) public APIs for fetching
118
- Bitcoin data and pushing transactions to the network. At the moment we
119
- work with the following APIs:
114
+ Bitcoin data and pushing transactions to the network.
115
+ At the moment we work with the following APIs:
120
116
 
121
117
  * [Blockchain.com] - `Sibit::Blockchain`
122
118
  * [BTC.com] - `Sibit::Btc`
@@ -125,8 +121,9 @@ work with the following APIs:
125
121
  * [Blockchair.com] - `Sibit::Blockchair`
126
122
  * [Cex.io] - `Sibit::Cex`
127
123
 
128
- The first one in this list is used by default. If you want to use a different
129
- one, you just specify it in the constructor of `Sibit` object:
124
+ The first one in this list is used by default.
125
+ If you want to use a different one,
126
+ you just specify it in the constructor of `Sibit` object:
130
127
 
131
128
  ```ruby
132
129
  require 'sibit'
data/bin/sibit CHANGED
@@ -6,10 +6,6 @@
6
6
 
7
7
  $stdout.sync = true
8
8
 
9
- # see https://stackoverflow.com/a/6048451/187141
10
- require 'openssl'
11
- OpenSSL::SSL::VERIFY_PEER ||= OpenSSL::SSL::VERIFY_NONE
12
-
13
9
  require 'backtrace'
14
10
  require 'loog'
15
11
  require 'retriable_proxy'
data/lib/sibit/bestof.rb CHANGED
@@ -12,96 +12,93 @@ require_relative 'error'
12
12
  # Author:: Yegor Bugayenko (yegor256@gmail.com)
13
13
  # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
14
14
  # License:: MIT
15
- class Sibit
16
- # Best of API.
17
- class BestOf
18
- # Constructor.
19
- def initialize(list, log: Loog::NULL, verbose: false)
20
- @list = list
21
- @log = log
22
- @verbose = verbose
23
- end
15
+ class Sibit::BestOf
16
+ # Constructor.
17
+ def initialize(list, log: Loog::NULL, verbose: false)
18
+ @list = list
19
+ @log = log
20
+ @verbose = verbose
21
+ end
24
22
 
25
- # Current price of BTC in USD (float returned).
26
- def price(currency = 'USD')
27
- best_of('price') do |api|
28
- api.price(currency)
29
- end
23
+ # Current price of BTC in USD (float returned).
24
+ def price(currency = 'USD')
25
+ best_of('price') do |api|
26
+ api.price(currency)
30
27
  end
28
+ end
31
29
 
32
- # Gets the balance of the address, in satoshi.
33
- def balance(address)
34
- best_of('balance') do |api|
35
- api.balance(address)
36
- end
30
+ # Gets the balance of the address, in satoshi.
31
+ def balance(address)
32
+ best_of('balance') do |api|
33
+ api.balance(address)
37
34
  end
35
+ end
38
36
 
39
- # Get the height of the block.
40
- def height(hash)
41
- best_of('height') do |api|
42
- api.height(hash)
43
- end
37
+ # Get the height of the block.
38
+ def height(hash)
39
+ best_of('height') do |api|
40
+ api.height(hash)
44
41
  end
42
+ end
45
43
 
46
- # Get the hash of the next block.
47
- def next_of(hash)
48
- best_of('next_of') do |api|
49
- api.next_of(hash)
50
- end
44
+ # Get the hash of the next block.
45
+ def next_of(hash)
46
+ best_of('next_of') do |api|
47
+ api.next_of(hash)
51
48
  end
49
+ end
52
50
 
53
- # Get recommended fees, in satoshi per byte. The method returns
54
- # a hash: { S: 12, M: 45, L: 100, XL: 200 }
55
- def fees
56
- best_of('fees', &:fees)
57
- end
51
+ # Get recommended fees, in satoshi per byte. The method returns
52
+ # a hash: { S: 12, M: 45, L: 100, XL: 200 }
53
+ def fees
54
+ best_of('fees', &:fees)
55
+ end
58
56
 
59
- # Fetch all unspent outputs per address.
60
- def utxos(keys)
61
- best_of('utxos') do |api|
62
- api.utxos(keys)
63
- end
57
+ # Fetch all unspent outputs per address.
58
+ def utxos(keys)
59
+ best_of('utxos') do |api|
60
+ api.utxos(keys)
64
61
  end
62
+ end
65
63
 
66
- # Latest block hash.
67
- def latest
68
- best_of('latest', &:latest)
69
- end
64
+ # Latest block hash.
65
+ def latest
66
+ best_of('latest', &:latest)
67
+ end
70
68
 
71
- # Push this transaction (in hex format) to the network.
72
- def push(hex)
73
- best_of('push') do |api|
74
- api.push(hex)
75
- end
69
+ # Push this transaction (in hex format) to the network.
70
+ def push(hex)
71
+ best_of('push') do |api|
72
+ api.push(hex)
76
73
  end
74
+ end
77
75
 
78
- # This method should fetch a block and return as a hash.
79
- def block(hash)
80
- best_of('block') do |api|
81
- api.block(hash)
82
- end
76
+ # This method should fetch a block and return as a hash.
77
+ def block(hash)
78
+ best_of('block') do |api|
79
+ api.block(hash)
83
80
  end
81
+ end
84
82
 
85
- private
83
+ private
86
84
 
87
- def best_of(method)
88
- return yield @list unless @list.is_a?(Array)
89
- results = []
90
- errors = []
91
- @list.each do |api|
92
- results << yield(api)
93
- rescue Sibit::NotSupportedError
94
- # Just ignore it
95
- rescue Sibit::Error => e
96
- errors << e
97
- @log.info("The API #{api.class.name} failed at #{method}(): #{e.message}") if @verbose
98
- end
99
- if results.empty?
100
- errors.each { |e| @log.info(Backtrace.new(e).to_s) }
101
- raise Sibit::Error, "No APIs out of #{@list.length} managed to succeed at #{method}(): \
85
+ def best_of(method)
86
+ return yield @list unless @list.is_a?(Array)
87
+ results = []
88
+ errors = []
89
+ @list.each do |api|
90
+ results << yield(api)
91
+ rescue Sibit::NotSupportedError
92
+ # Just ignore it
93
+ rescue Sibit::Error => e
94
+ errors << e
95
+ @log.info("The API #{api.class.name} failed at #{method}(): #{e.message}") if @verbose
96
+ end
97
+ if results.empty?
98
+ errors.each { |e| @log.info(Backtrace.new(e).to_s) }
99
+ raise Sibit::Error, "No APIs out of #{@list.length} managed to succeed at #{method}(): \
102
100
  #{@list.map { |a| a.class.name }.join(', ')}"
103
- end
104
- results.group_by(&:to_s).values.max_by(&:size)[0]
105
101
  end
102
+ results.group_by(&:to_s).values.max_by(&:size)[0]
106
103
  end
107
104
  end
@@ -5,46 +5,44 @@
5
5
 
6
6
  require 'digest'
7
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
8
+ # Bitcoin primitives module.
9
+ #
10
+ # Pure Ruby implementation of Bitcoin functionality using OpenSSL 3.0+.
11
+ # Replaces the bitcoin-ruby dependency which is incompatible with OpenSSL 3.0.
12
+ module Sibit::Bitcoin
13
+ MIN_TX_FEE = 10_000
15
14
 
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'
15
+ # Base58 encoding for Bitcoin addresses.
16
+ #
17
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
18
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
19
+ # License:: MIT
20
+ module Base58
21
+ ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
23
22
 
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
23
+ def self.encode(hex)
24
+ bytes = [hex].pack('H*')
25
+ leading = bytes.match(/^\x00*/)[0].length
26
+ num = hex.to_i(16)
27
+ result = ''
28
+ while num.positive?
29
+ num, remainder = num.divmod(58)
30
+ result = ALPHABET[remainder] + result
34
31
  end
32
+ ('1' * leading) + result
33
+ end
35
34
 
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
35
+ def self.decode(str)
36
+ leading = str.match(/^1*/)[0].length
37
+ num = 0
38
+ str.each_char { |c| num = (num * 58) + ALPHABET.index(c) }
39
+ hex = num.zero? ? '' : num.to_s(16)
40
+ hex = "0#{hex}" if hex.length.odd?
41
+ ('00' * leading) + hex
42
+ end
44
43
 
45
- def self.check(hex)
46
- Digest::SHA256.hexdigest(Digest::SHA256.digest([hex].pack('H*')))[0...8]
47
- end
44
+ def self.check(hex)
45
+ Digest::SHA256.hexdigest(Digest::SHA256.digest([hex].pack('H*')))[0...8]
48
46
  end
49
47
  end
50
48
  end
@@ -3,85 +3,83 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2019-2025 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require 'openssl'
7
6
  require 'digest'
7
+ require 'openssl'
8
8
  require_relative 'base58'
9
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
10
+ module Sibit::Bitcoin
11
+ # Bitcoin ECDSA key using secp256k1 curve.
12
+ #
13
+ # Supports OpenSSL 3.0+ by constructing keys via DER encoding instead
14
+ # of using deprecated mutable key APIs.
15
+ #
16
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
17
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
18
+ # License:: MIT
19
+ class Key
20
+ MIN_PRIV = 0x01
21
+ MAX_PRIV = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364140
23
22
 
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
23
+ def self.generate
24
+ key = OpenSSL::PKey::EC.generate('secp256k1')
25
+ pvt = key.private_key.to_s(16).rjust(64, '0').downcase
26
+ new(pvt)
27
+ end
29
28
 
30
- def initialize(privkey)
31
- @privkey = privkey
32
- @compressed = true
33
- @key = build(privkey)
34
- end
29
+ def initialize(privkey)
30
+ @privkey = privkey
31
+ @compressed = true
32
+ @key = build(privkey)
33
+ end
35
34
 
36
- def priv
37
- @privkey
38
- end
35
+ def priv
36
+ @privkey
37
+ end
39
38
 
40
- def pub
41
- point = @key.public_key
42
- point.to_octet_string(@compressed ? :compressed : :uncompressed).unpack1('H*')
43
- end
39
+ def pub
40
+ point = @key.public_key
41
+ point.to_octet_string(@compressed ? :compressed : :uncompressed).unpack1('H*')
42
+ end
44
43
 
45
- def addr
46
- hash = hash160(pub)
47
- versioned = "00#{hash}"
48
- checksum = Base58.check(versioned)
49
- Base58.encode(versioned + checksum)
50
- end
44
+ def addr
45
+ hash = hash160(pub)
46
+ versioned = "00#{hash}"
47
+ checksum = Base58.check(versioned)
48
+ Base58.encode(versioned + checksum)
49
+ end
51
50
 
52
- def sign(data)
53
- @key.sign('SHA256', data)
54
- end
51
+ def sign(data)
52
+ @key.sign('SHA256', data)
53
+ end
55
54
 
56
- def verify(data, sig)
57
- @key.verify('SHA256', sig, data)
58
- rescue OpenSSL::PKey::PKeyError
59
- false
60
- end
55
+ def verify(data, sig)
56
+ @key.verify('SHA256', sig, data)
57
+ rescue OpenSSL::PKey::PKeyError
58
+ false
59
+ end
61
60
 
62
- private
61
+ private
63
62
 
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
63
+ def build(privkey)
64
+ value = privkey.to_i(16)
65
+ raise 'private key is not on curve' unless value.between?(MIN_PRIV, MAX_PRIV)
66
+ group = OpenSSL::PKey::EC::Group.new('secp256k1')
67
+ bn = OpenSSL::BN.new(privkey, 16)
68
+ pubkey = group.generator.mul(bn)
69
+ asn1 = OpenSSL::ASN1::Sequence(
70
+ [
71
+ OpenSSL::ASN1::Integer.new(1),
72
+ OpenSSL::ASN1::OctetString(bn.to_s(2)),
73
+ OpenSSL::ASN1::ObjectId('secp256k1', 0, :EXPLICIT),
74
+ OpenSSL::ASN1::BitString(pubkey.to_octet_string(:uncompressed), 1, :EXPLICIT)
75
+ ]
76
+ )
77
+ OpenSSL::PKey::EC.new(asn1.to_der)
78
+ end
80
79
 
81
- def hash160(hex)
82
- bytes = [hex].pack('H*')
83
- Digest::RMD160.hexdigest(Digest::SHA256.digest(bytes))
84
- end
80
+ def hash160(hex)
81
+ bytes = [hex].pack('H*')
82
+ Digest::RMD160.hexdigest(Digest::SHA256.digest(bytes))
85
83
  end
86
84
  end
87
85
  end
@@ -6,53 +6,51 @@
6
6
  require 'digest'
7
7
  require_relative 'base58'
8
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
9
+ module Sibit::Bitcoin
10
+ # Bitcoin Script parser.
11
+ #
12
+ # Parses standard P2PKH scripts to extract addresses.
13
+ #
14
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
15
+ # Copyright:: Copyright (c) 2019-2025 Yegor Bugayenko
16
+ # License:: MIT
17
+ class Script
18
+ OP_DUP = 0x76
19
+ OP_HASH160 = 0xa9
20
+ OP_EQUALVERIFY = 0x88
21
+ OP_CHECKSIG = 0xac
22
+
23
+ def initialize(hex)
24
+ @bytes = [hex].pack('H*').bytes
25
+ end
26
+
27
+ def address
28
+ return p2pkh_address if p2pkh?
29
+ nil
30
+ end
31
+
32
+ def p2pkh?
33
+ @bytes.length == 25 &&
34
+ @bytes[0] == OP_DUP &&
35
+ @bytes[1] == OP_HASH160 &&
36
+ @bytes[2] == 20 &&
37
+ @bytes[23] == OP_EQUALVERIFY &&
38
+ @bytes[24] == OP_CHECKSIG
39
+ end
40
+
41
+ def hash160
42
+ return nil unless p2pkh?
43
+ @bytes[3, 20].pack('C*').unpack1('H*')
44
+ end
45
+
46
+ private
47
+
48
+ def p2pkh_address
49
+ h = hash160
50
+ return nil unless h
51
+ versioned = "00#{h}"
52
+ checksum = Base58.check(versioned)
53
+ Base58.encode(versioned + checksum)
56
54
  end
57
55
  end
58
56
  end