money-tree 0.10.0 → 0.11.1

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.
@@ -1,16 +1,18 @@
1
- require 'openssl'
2
- require 'base64'
1
+ require "base64"
2
+ require "bech32"
3
+ require "openssl"
3
4
 
4
5
  module MoneyTree
5
6
  module Support
6
7
  include OpenSSL
7
-
8
+ extend self
9
+
8
10
  INT32_MAX = 256 ** [1].pack("L*").size
9
11
  INT64_MAX = 256 ** [1].pack("Q*").size
10
12
  BASE58_CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
11
-
12
- def int_to_base58(int_val, leading_zero_bytes=0)
13
- base58_val, base = '', BASE58_CHARS.size
13
+
14
+ def int_to_base58(int_val, leading_zero_bytes = 0)
15
+ base58_val, base = "", BASE58_CHARS.size
14
16
  while int_val > 0
15
17
  int_val, remainder = int_val.divmod(base)
16
18
  base58_val = BASE58_CHARS[remainder] + base58_val
@@ -20,27 +22,28 @@ module MoneyTree
20
22
 
21
23
  def base58_to_int(base58_val)
22
24
  int_val, base = 0, BASE58_CHARS.size
23
- base58_val.reverse.each_char.with_index do |char,index|
24
- raise ArgumentError, 'Value not a valid Base58 String.' unless char_index = BASE58_CHARS.index(char)
25
- int_val += char_index*(base**index)
25
+ base58_val.reverse.each_char.with_index do |char, index|
26
+ raise ArgumentError, "Value not a valid Base58 String." unless char_index = BASE58_CHARS.index(char)
27
+ int_val += char_index * (base ** index)
26
28
  end
27
29
  int_val
28
30
  end
29
31
 
30
32
  def encode_base58(hex)
31
- leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : '').size / 2
32
- ("1"*leading_zero_bytes) + int_to_base58( hex.to_i(16) )
33
+ leading_zero_bytes = (hex.match(/^([0]+)/) ? $1 : "").size / 2
34
+ ("1" * leading_zero_bytes) + int_to_base58(hex.to_i(16))
33
35
  end
34
36
 
35
37
  def decode_base58(base58_val)
36
- s = base58_to_int(base58_val).to_s(16); s = (s.bytesize.odd? ? '0'+s : s)
37
- s = '' if s == '00'
38
- leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : '').size
39
- s = ("00"*leading_zero_bytes) + s if leading_zero_bytes > 0
38
+ s = base58_to_int(base58_val).to_s(16); s = (s.bytesize.odd? ? "0" + s : s)
39
+ s = "" if s == "00"
40
+ leading_zero_bytes = (base58_val.match(/^([1]+)/) ? $1 : "").size
41
+ s = ("00" * leading_zero_bytes) + s if leading_zero_bytes > 0
40
42
  s
41
43
  end
44
+
42
45
  alias_method :base58_to_hex, :decode_base58
43
-
46
+
44
47
  def to_serialized_base58(hex)
45
48
  hash = sha256 hex
46
49
  hash = sha256 hash
@@ -48,7 +51,14 @@ module MoneyTree
48
51
  address = hex + checksum
49
52
  encode_base58 address
50
53
  end
51
-
54
+
55
+ def to_serialized_bech32(human_readable_part, hex)
56
+ segwit_addr = Bech32::SegwitAddr.new
57
+ segwit_addr.hrp = human_readable_part
58
+ segwit_addr.script_pubkey = "0000" + hex
59
+ segwit_addr.addr
60
+ end
61
+
52
62
  def from_serialized_base58(base58)
53
63
  hex = decode_base58 base58
54
64
  checksum = hex.slice!(-8..-1)
@@ -56,46 +66,46 @@ module MoneyTree
56
66
  raise EncodingError unless checksum == compare_checksum
57
67
  hex
58
68
  end
59
-
69
+
60
70
  def digestify(digest_type, source, opts = {})
61
71
  source = [source].pack("H*") unless opts[:ascii]
62
72
  bytes_to_hex Digest.digest(digest_type, source)
63
73
  end
64
-
74
+
65
75
  def sha256(source, opts = {})
66
- digestify('SHA256', source, opts)
76
+ digestify("SHA256", source, opts)
67
77
  end
68
-
78
+
69
79
  def ripemd160(source, opts = {})
70
- digestify('RIPEMD160', source, opts)
80
+ digestify("RIPEMD160", source, opts)
71
81
  end
72
-
82
+
73
83
  def encode_base64(hex)
74
84
  Base64.encode64([hex].pack("H*")).chomp
75
85
  end
76
-
86
+
77
87
  def decode_base64(base64)
78
88
  Base64.decode64(base64).unpack("H*")[0]
79
89
  end
80
-
90
+
81
91
  def hmac_sha512(key, message)
82
92
  digest = Digest::SHA512.new
83
93
  HMAC.digest digest, key, message
84
94
  end
85
-
95
+
86
96
  def hmac_sha512_hex(key, message)
87
97
  md = hmac_sha512(key, message)
88
- md.unpack("H*").first.rjust(64, '0')
98
+ md.unpack("H*").first.rjust(64, "0")
89
99
  end
90
-
100
+
91
101
  def bytes_to_int(bytes, base = 16)
92
102
  if bytes.is_a?(Array)
93
103
  bytes = bytes.pack("C*")
94
104
  end
95
105
  bytes.unpack("H*")[0].to_i(16)
96
106
  end
97
-
98
- def int_to_hex(i, size=nil)
107
+
108
+ def int_to_hex(i, size = nil)
99
109
  hex = i.to_s(16).downcase
100
110
  if (hex.size % 2) != 0
101
111
  hex = "#{0}#{hex}"
@@ -107,21 +117,36 @@ module MoneyTree
107
117
  hex
108
118
  end
109
119
  end
110
-
120
+
111
121
  def int_to_bytes(i)
112
122
  [int_to_hex(i)].pack("H*")
113
123
  end
114
-
124
+
115
125
  def bytes_to_hex(bytes)
116
126
  bytes.unpack("H*")[0].downcase
117
127
  end
118
-
128
+
119
129
  def hex_to_bytes(hex)
120
130
  [hex].pack("H*")
121
131
  end
122
-
132
+
123
133
  def hex_to_int(hex)
124
134
  hex.to_i(16)
125
135
  end
136
+
137
+ def encode_p2wpkh_p2sh(value)
138
+ chk = [Digest::SHA256.hexdigest(Digest::SHA256.digest(value))].pack("H*")[0...4]
139
+ encode_base58 (value + chk).unpack("H*")[0]
140
+ end
141
+
142
+ def custom_hash_160(value)
143
+ [OpenSSL::Digest::RIPEMD160.hexdigest(Digest::SHA256.digest(value))].pack("H*")
144
+ end
145
+
146
+ def convert_p2wpkh_p2sh(key_hex, prefix)
147
+ push_20 = ["0014"].pack("H*")
148
+ script_sig = push_20 + custom_hash_160([key_hex].pack("H*"))
149
+ encode_p2wpkh_p2sh(prefix + custom_hash_160(script_sig))
150
+ end
126
151
  end
127
152
  end
@@ -1,3 +1,3 @@
1
1
  module MoneyTree
2
- VERSION = "0.10.0"
2
+ VERSION = "0.11.1"
3
3
  end
data/lib/money-tree.rb CHANGED
@@ -1,12 +1,11 @@
1
- require "openssl_extensions"
2
- require "money-tree/version"
3
1
  require "money-tree/support"
4
- require "money-tree/networks"
5
- require "money-tree/key"
2
+
6
3
  require "money-tree/address"
4
+ require "money-tree/key"
7
5
  require "money-tree/networks"
8
6
  require "money-tree/node"
7
+ require "money-tree/version"
8
+
9
+ require "openssl_extensions"
9
10
 
10
- module MoneyTree
11
-
12
- end
11
+ module MoneyTree; end
@@ -1,73 +1,30 @@
1
1
  # encoding: ascii-8bit
2
2
 
3
- require 'openssl'
4
- require 'ffi'
3
+ require "openssl"
5
4
 
6
5
  module MoneyTree
7
6
  module OpenSSLExtensions
8
- extend FFI::Library
9
- ffi_lib ['libssl.so.1.0.0', 'libssl.so.10', 'libssl1.0.0', 'ssl']
7
+ extend self
10
8
 
11
- NID_secp256k1 = 714
12
- POINT_CONVERSION_COMPRESSED = 2
13
- POINT_CONVERSION_UNCOMPRESSED = 4
14
-
15
- attach_function :EC_KEY_free, [:pointer], :int
16
- attach_function :EC_KEY_get0_group, [:pointer], :pointer
17
- attach_function :EC_KEY_new_by_curve_name, [:int], :pointer
18
- attach_function :EC_POINT_clear_free, [:pointer], :int
19
- attach_function :EC_POINT_add, [:pointer, :pointer, :pointer, :pointer, :pointer], :int
20
- attach_function :EC_POINT_point2hex, [:pointer, :pointer, :int, :pointer], :string
21
- attach_function :EC_POINT_hex2point, [:pointer, :string, :pointer, :pointer], :pointer
22
- attach_function :EC_POINT_new, [:pointer], :pointer
23
-
24
- def self.add(point_0, point_1)
9
+ def add(point_0, point_1)
25
10
  validate_points(point_0, point_1)
26
- eckey = EC_KEY_new_by_curve_name(NID_secp256k1)
27
- group = EC_KEY_get0_group(eckey)
28
-
11
+ group = OpenSSL::PKey::EC::Group.new("secp256k1")
29
12
  point_0_hex = point_0.to_bn.to_s(16)
30
- point_0_pt = EC_POINT_hex2point(group, point_0_hex, nil, nil)
13
+ point_0_pt = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(point_0_hex, 16))
31
14
  point_1_hex = point_1.to_bn.to_s(16)
32
- point_1_pt = EC_POINT_hex2point(group, point_1_hex, nil, nil)
33
-
34
- sum_point = EC_POINT_new(group)
35
- success = EC_POINT_add(group, sum_point, point_0_pt, point_1_pt, nil)
36
- hex = EC_POINT_point2hex(group, sum_point, POINT_CONVERSION_UNCOMPRESSED, nil)
37
-
38
- EC_KEY_free(eckey)
39
- EC_POINT_clear_free(sum_point)
40
- EC_POINT_clear_free(point_0_pt)
41
- EC_POINT_clear_free(point_1_pt)
42
-
43
- eckey = nil
44
- group = nil
45
- sum_point = nil
46
- point_0_pt = nil
47
- point_1_pt = nil
48
-
49
- hex
15
+ point_1_pt = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(point_1_hex, 16))
16
+ sum_point = point_0_pt.add(point_1_pt)
17
+ sum_point.to_bn.to_s(16)
50
18
  end
51
19
 
52
- def self.validate_points(*points)
20
+ def validate_points(*points)
53
21
  points.each do |point|
54
22
  if !point.is_a?(OpenSSL::PKey::EC::Point)
55
- raise ArgumentError, "point must be an OpenSSL::PKey::EC::Point object"
23
+ raise ArgumentError, "point must be an OpenSSL::PKey::EC::Point object"
56
24
  elsif point.infinity?
57
- raise ArgumentError, "point must not be infinity"
25
+ raise ArgumentError, "point must not be infinity"
58
26
  end
59
27
  end
60
28
  end
61
29
  end
62
30
  end
63
-
64
-
65
- class OpenSSL::PKey::EC::Point
66
- include MoneyTree::OpenSSLExtensions
67
-
68
- def add(point)
69
- sum_point_hex = MoneyTree::OpenSSLExtensions.add(self, point)
70
- self.class.new group, OpenSSL::BN.new(sum_point_hex, 16)
71
- end
72
-
73
- end
data/money-tree.gemspec CHANGED
@@ -1,38 +1,30 @@
1
1
  # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
2
+ lib = File.expand_path("../lib", __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'money-tree/version'
4
+ require "money-tree/version"
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "money-tree"
8
- spec.version = MoneyTree::VERSION
9
- spec.authors = ["Micah Winkelspecht"]
10
- spec.email = ["winkelspecht@gmail.com"]
11
- spec.description = %q{A Ruby Gem implementation of Bitcoin HD Wallets}
12
- spec.summary = %q{Bitcoin Hierarchical Deterministic Wallets in Ruby! (Bitcoin standard BIP0032)}
13
- spec.homepage = "https://github.com/gemhq/money-tree"
14
- spec.license = "MIT"
7
+ spec.name = "money-tree"
8
+ spec.version = MoneyTree::VERSION
9
+ spec.authors = ["Micah Winkelspecht", "Afri Schoedon"]
10
+ spec.email = ["winkelspecht@gmail.com", "gems@q9f.cc"]
11
+ spec.description = %q{A Ruby Gem implementation of Bitcoin HD Wallets}
12
+ spec.summary = %q{Bitcoin Hierarchical Deterministic Wallets in Ruby! (Bitcoin standard BIP0032)}
13
+ spec.homepage = "https://github.com/GemHQ/money-tree"
14
+ spec.license = "MIT"
15
15
 
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
-
21
- # used with gem install ... -P HighSecurity
22
- spec.cert_chain = ["certs/mattatgemco.pem"]
23
- # Sign gem when evaluating spec with `gem` command
24
- # unless ENV has set a SKIP_GEM_SIGNING
25
- if ($0 =~ /gem\z/) and not ENV.include?("SKIP_GEM_SIGNING")
26
- spec.signing_key = File.join(Gem.user_home, ".ssh", "gem-private_key.pem")
27
- end
28
20
 
29
-
30
- spec.add_dependency "ffi"
31
-
32
- spec.add_development_dependency "bundler", "~> 1.3"
33
- spec.add_development_dependency "rake"
34
- spec.add_development_dependency "rspec"
35
- spec.add_development_dependency "simplecov"
36
- spec.add_development_dependency "coveralls"
37
- spec.add_development_dependency "pry"
21
+ spec.metadata = {
22
+ "homepage_uri" => "https://github.com/GemHQ/money-tree",
23
+ "source_code_uri" => "https://github.com/GemHQ/money-tree",
24
+ "github_repo" => "https://github.com/GemHQ/money-tree",
25
+ "bug_tracker_uri" => "https://github.com/GemHQ/money-tree/issues",
26
+ }.freeze
27
+
28
+ spec.platform = Gem::Platform::RUBY
29
+ spec.required_ruby_version = ">= 2.6", "< 4.0"
38
30
  end
@@ -1,17 +1,23 @@
1
- require 'spec_helper'
1
+ require "spec_helper"
2
2
 
3
3
  describe MoneyTree::Address do
4
4
  describe "initialize" do
5
5
  it "generates a private key by default" do
6
6
  address = MoneyTree::Address.new
7
+ expect(address).to be
8
+ expect(address).to be_instance_of MoneyTree::Address
9
+ expect(address.private_key).to be_instance_of MoneyTree::PrivateKey
7
10
  expect(address.private_key.key.length).to eql(64)
8
11
  end
9
-
12
+
10
13
  it "generates a public key by default" do
11
14
  address = MoneyTree::Address.new
15
+ expect(address).to be
16
+ expect(address).to be_instance_of MoneyTree::Address
17
+ expect(address.public_key).to be_instance_of MoneyTree::PublicKey
12
18
  expect(address.public_key.key.length).to eql(66)
13
19
  end
14
-
20
+
15
21
  it "imports a private key in hex form" do
16
22
  address = MoneyTree::Address.new private_key: "5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b"
17
23
  expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
@@ -19,37 +25,65 @@ describe MoneyTree::Address do
19
25
  expect(address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
20
26
  expect(address.private_key.to_s).to eql("KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK")
21
27
  expect(address.public_key.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
28
+ expect(address.to_p2wpkh_p2sh).to eql("31vNN7WVDxjvc5XZVKW3qV4B3nFLxsRPnE")
29
+ expect(address.to_bech32).to eql("bc1qrlwlgt5d0sdtq882qvk3jc0sywucn76fwcmqma")
22
30
  end
23
-
31
+
24
32
  it "imports a private key in compressed wif format" do
25
33
  address = MoneyTree::Address.new private_key: "KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK"
26
34
  expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
27
35
  expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
28
36
  expect(address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
37
+ expect(address.to_p2wpkh_p2sh).to eql("31vNN7WVDxjvc5XZVKW3qV4B3nFLxsRPnE")
38
+ expect(address.to_bech32).to eql("bc1qrlwlgt5d0sdtq882qvk3jc0sywucn76fwcmqma")
29
39
  end
30
-
40
+
31
41
  it "imports a private key in uncompressed wif format" do
32
42
  address = MoneyTree::Address.new private_key: "5JXz5ZyFk31oHVTQxqce7yitCmTAPxBqeGQ4b7H3Aj3L45wUhoa"
33
43
  expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
34
44
  expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
35
45
  end
36
46
  end
37
-
47
+
38
48
  describe "to_s" do
39
49
  before do
40
50
  @address = MoneyTree::Address.new private_key: "5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b"
41
51
  end
42
-
52
+
43
53
  it "returns compressed base58 public key" do
44
54
  expect(@address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
45
55
  expect(@address.public_key.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
56
+ expect(@address.to_p2wpkh_p2sh).to eql("31vNN7WVDxjvc5XZVKW3qV4B3nFLxsRPnE")
57
+ expect(@address.to_bech32).to eql("bc1qrlwlgt5d0sdtq882qvk3jc0sywucn76fwcmqma")
46
58
  end
47
-
59
+
48
60
  it "returns compressed WIF private key" do
49
61
  expect(@address.private_key.to_s).to eql("KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK")
50
62
  end
51
63
  end
52
64
 
65
+ context "bitcoin wiki" do
66
+ # ref https://en.bitcoin.it/wiki/Technical_background_of_version_1_Bitcoin_addresses
67
+ subject(:wiki_v1) { MoneyTree::Address.new private_key: "18e14a7b6a307f426a94f8114701e7c8e774e7f9a47e2c2035db29a206321725" }
68
+
69
+ it "always regenerates the bitcoin wiki v1 example" do
70
+ expect(wiki_v1.public_key.key).to eq "0250863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352"
71
+ expect(wiki_v1.to_s).to eq "1PMycacnJaSqwwJqjawXBErnLsZ7RkXUAs"
72
+ expect(wiki_v1.to_p2wpkh_p2sh).to eql("3BxwGNjvG4CP14tAZodgYyZ7UTjruYDyAM")
73
+ expect(wiki_v1.to_bech32).to eql("bc1q7499s50fxu4c0qg23esvm5h8elvqkm33r2tdza")
74
+ end
75
+
76
+ # ref https://en.bitcoin.it/wiki/Bech32
77
+ subject(:wiki_bech32) { MoneyTree::Address.new private_key: "0000000000000000000000000000000000000000000000000000000000000001" }
78
+
79
+ it "always regenerates the bitcoin wiki v1 example" do
80
+ expect(wiki_bech32.public_key.key).to eq "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
81
+ expect(wiki_bech32.to_s).to eq "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH"
82
+ expect(wiki_bech32.to_p2wpkh_p2sh).to eql("3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN")
83
+ expect(wiki_bech32.to_bech32).to eql("bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4")
84
+ end
85
+ end
86
+
53
87
  context "testnet3" do
54
88
  before do
55
89
  @address = MoneyTree::Address.new network: :bitcoin_testnet
@@ -0,0 +1,9 @@
1
+ require "spec_helper"
2
+
3
+ describe MoneyTree do
4
+ it "0.11.1 works" do
5
+
6
+ # placeholder to set up spec in future
7
+ expect(MoneyTree::VERSION).to eq "0.11.1"
8
+ end
9
+ end