money-tree-openssl 1.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.
@@ -0,0 +1,35 @@
1
+ module MoneyTree
2
+ NETWORKS =
3
+ begin
4
+ hsh = Hash.new do |_, key|
5
+ raise "#{key} is not a valid network!"
6
+ end.merge(
7
+ bitcoin: {
8
+ address_version: '00',
9
+ p2sh_version: '05',
10
+ p2sh_char: '3',
11
+ privkey_version: '80',
12
+ privkey_compression_flag: '01',
13
+ extended_privkey_version: "0488ade4",
14
+ extended_pubkey_version: "0488b21e",
15
+ compressed_wif_chars: %w(K L),
16
+ uncompressed_wif_chars: %w(5),
17
+ protocol_version: 70001
18
+ },
19
+ bitcoin_testnet: {
20
+ address_version: '6f',
21
+ p2sh_version: 'c4',
22
+ p2sh_char: '2',
23
+ privkey_version: 'ef',
24
+ privkey_compression_flag: '01',
25
+ extended_privkey_version: "04358394",
26
+ extended_pubkey_version: "043587cf",
27
+ compressed_wif_chars: %w(c),
28
+ uncompressed_wif_chars: %w(9),
29
+ protocol_version: 70001
30
+ }
31
+ )
32
+ hsh[:testnet3] = hsh[:bitcoin_testnet]
33
+ hsh
34
+ end
35
+ end
@@ -0,0 +1,302 @@
1
+ module MoneyTree
2
+ class Node
3
+ include Support
4
+ extend Support
5
+ attr_reader :private_key, :public_key, :chain_code,
6
+ :is_private, :depth, :index, :parent
7
+
8
+ class PublicDerivationFailure < StandardError; end
9
+ class InvalidKeyForIndex < StandardError; end
10
+ class ImportError < StandardError; end
11
+ class PrivatePublicMismatch < StandardError; end
12
+
13
+ def initialize(opts = {})
14
+ opts.each { |k, v| instance_variable_set "@#{k}", v }
15
+ end
16
+
17
+ def self.from_bip32(address, has_version: true)
18
+ hex = from_serialized_base58 address
19
+ hex.slice!(0..7) if has_version
20
+ self.new({
21
+ depth: hex.slice!(0..1).to_i(16),
22
+ parent_fingerprint: hex.slice!(0..7),
23
+ index: hex.slice!(0..7).to_i(16),
24
+ chain_code: hex.slice!(0..63).to_i(16)
25
+ }.merge(parse_out_key(hex)))
26
+ end
27
+
28
+ def self.from_serialized_address(address)
29
+ puts 'Node.from_serialized_address is DEPRECATED. Please use .from_bip32 instead.'
30
+ from_bip32(address)
31
+ end
32
+
33
+ def self.parse_out_key(hex)
34
+ if hex.slice(0..1) == '00'
35
+ private_key = MoneyTree::PrivateKey.new(key: hex.slice(2..-1))
36
+ {
37
+ private_key: private_key,
38
+ public_key: MoneyTree::PublicKey.new(private_key)
39
+ }
40
+ elsif %w(02 03).include? hex.slice(0..1)
41
+ { public_key: MoneyTree::PublicKey.new(hex) }
42
+ else
43
+ raise ImportError, 'Public or private key data does not match version type'
44
+ end
45
+ end
46
+
47
+ def is_private?
48
+ index >= 0x80000000 || index < 0
49
+ end
50
+
51
+ def index_hex(i = index)
52
+ if i < 0
53
+ [i].pack('l>').unpack('H*').first
54
+ else
55
+ i.to_s(16).rjust(8, "0")
56
+ end
57
+ end
58
+
59
+ def depth_hex(depth)
60
+ depth.to_s(16).rjust(2, "0")
61
+ end
62
+
63
+ def private_derivation_message(i)
64
+ "\x00" + private_key.to_bytes + i_as_bytes(i)
65
+ end
66
+
67
+ def public_derivation_message(i)
68
+ public_key.to_bytes << i_as_bytes(i)
69
+ end
70
+
71
+ def i_as_bytes(i)
72
+ [i].pack('N')
73
+ end
74
+
75
+ def derive_private_key(i = 0)
76
+ message = i >= 0x80000000 || i < 0 ? private_derivation_message(i) : public_derivation_message(i)
77
+ hash = hmac_sha512 hex_to_bytes(chain_code_hex), message
78
+ left_int = left_from_hash(hash)
79
+ raise InvalidKeyForIndex, 'greater than or equal to order' if left_int >= MoneyTree::Key::ORDER # very low probability
80
+ child_private_key = (left_int + private_key.to_i) % MoneyTree::Key::ORDER
81
+ raise InvalidKeyForIndex, 'equal to zero' if child_private_key == 0 # very low probability
82
+ child_chain_code = right_from_hash(hash)
83
+ return child_private_key, child_chain_code
84
+ end
85
+
86
+ def derive_public_key(i = 0)
87
+ raise PrivatePublicMismatch if i >= 0x80000000
88
+ message = public_derivation_message(i)
89
+ hash = hmac_sha512 hex_to_bytes(chain_code_hex), message
90
+ left_int = left_from_hash(hash)
91
+ raise InvalidKeyForIndex, 'greater than or equal to order' if left_int >= MoneyTree::Key::ORDER # very low probability
92
+ factor = BN.new left_int.to_s
93
+
94
+ gen_point = public_key.uncompressed.group.generator.mul(factor)
95
+
96
+ sum_point_hex = MoneyTree::OpenSSLExtensions.add(gen_point, public_key.uncompressed.point)
97
+ child_public_key = OpenSSL::PKey::EC::Point.new(public_key.group, OpenSSL::BN.new(sum_point_hex, 16)).to_bn.to_i
98
+
99
+ raise InvalidKeyForIndex, 'at infinity' if child_public_key == 1/0.0 # very low probability
100
+ child_chain_code = right_from_hash(hash)
101
+ return child_public_key, child_chain_code
102
+ end
103
+
104
+ def left_from_hash(hash)
105
+ bytes_to_int hash.bytes.to_a[0..31]
106
+ end
107
+
108
+ def right_from_hash(hash)
109
+ bytes_to_int hash.bytes.to_a[32..-1]
110
+ end
111
+
112
+ def to_serialized_hex(type = :public, network: :bitcoin)
113
+ raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil?
114
+ version_key = type.to_sym == :private ? :extended_privkey_version : :extended_pubkey_version
115
+ hex = NETWORKS[network][version_key] # version (4 bytes)
116
+ hex += depth_hex(depth) # depth (1 byte)
117
+ hex += parent_fingerprint # fingerprint of key (4 bytes)
118
+ hex += index_hex(index) # child number i (4 bytes)
119
+ hex += chain_code_hex
120
+ hex += type.to_sym == :private ? "00#{private_key.to_hex}" : public_key.compressed.to_hex
121
+ end
122
+
123
+ def to_bip32(type = :public, network: :bitcoin)
124
+ raise PrivatePublicMismatch if type.to_sym == :private && private_key.nil?
125
+ to_serialized_base58 to_serialized_hex(type, network: network)
126
+ end
127
+
128
+ def to_serialized_address(type = :public, network: :bitcoin)
129
+ puts 'Node.to_serialized_address is DEPRECATED. Please use .to_bip32.'
130
+ to_bip32(type, network: network)
131
+ end
132
+
133
+ def to_identifier(compressed=true)
134
+ key = compressed ? public_key.compressed : public_key.uncompressed
135
+ key.to_ripemd160
136
+ end
137
+
138
+ def to_fingerprint
139
+ public_key.compressed.to_fingerprint
140
+ end
141
+
142
+ def parent_fingerprint
143
+ if @parent_fingerprint
144
+ @parent_fingerprint
145
+ else
146
+ depth.zero? ? '00000000' : parent.to_fingerprint
147
+ end
148
+ end
149
+
150
+ def to_address(compressed=true, network: :bitcoin)
151
+ address = NETWORKS[network][:address_version] + to_identifier(compressed)
152
+ to_serialized_base58 address
153
+ end
154
+
155
+ def subnode(i = 0, opts = {})
156
+ if private_key.nil?
157
+ child_public_key, child_chain_code = derive_public_key(i)
158
+ child_public_key = MoneyTree::PublicKey.new child_public_key
159
+ else
160
+ child_private_key, child_chain_code = derive_private_key(i)
161
+ child_private_key = MoneyTree::PrivateKey.new key: child_private_key
162
+ child_public_key = MoneyTree::PublicKey.new child_private_key
163
+ end
164
+
165
+ MoneyTree::Node.new( depth: depth+1,
166
+ index: i,
167
+ private_key: private_key.nil? ? nil : child_private_key,
168
+ public_key: child_public_key,
169
+ chain_code: child_chain_code,
170
+ parent: self)
171
+ end
172
+
173
+ # path: a path of subkeys denoted by numbers and slashes. Use
174
+ # p or i<0 for private key derivation. End with .pub to force
175
+ # the key public.
176
+ #
177
+ # Examples:
178
+ # 1p/-5/2/1 would call subkey(i=1, is_prime=True).subkey(i=-5).
179
+ # subkey(i=2).subkey(i=1) and then yield the private key
180
+ # 0/0/458.pub would call subkey(i=0).subkey(i=0).subkey(i=458) and
181
+ # then yield the public key
182
+ #
183
+ # You should choose either the p or the negative number convention for private key derivation.
184
+ def node_for_path(path)
185
+ force_public = path[-4..-1] == '.pub'
186
+ path = path[0..-5] if force_public
187
+ parts = path.split('/')
188
+ nodes = []
189
+ parts.each_with_index do |part, depth|
190
+ if part =~ /m/i
191
+ nodes << self
192
+ else
193
+ i = parse_index(part)
194
+ node = nodes.last || self
195
+ nodes << node.subnode(i)
196
+ end
197
+ end
198
+ if force_public or parts.first == 'M'
199
+ node = nodes.last
200
+ node.strip_private_info!
201
+ node
202
+ else
203
+ nodes.last
204
+ end
205
+ end
206
+
207
+ def parse_index(path_part)
208
+ is_prime = %w(p ').include? path_part[-1]
209
+ i = path_part.to_i
210
+
211
+ i = if i < 0
212
+ i
213
+ elsif is_prime
214
+ i | 0x80000000
215
+ else
216
+ i & 0x7fffffff
217
+ end
218
+ end
219
+
220
+ def strip_private_info!
221
+ @private_key = nil
222
+ end
223
+
224
+ def chain_code_hex
225
+ int_to_hex chain_code, 64
226
+ end
227
+ end
228
+
229
+ class Master < Node
230
+ module SeedGeneration
231
+ class Failure < Exception; end
232
+ class RNGFailure < Failure; end
233
+ class LengthFailure < Failure; end
234
+ class ValidityError < Failure; end
235
+ class ImportError < Failure; end
236
+ class TooManyAttempts < Failure; end
237
+ end
238
+
239
+ HD_WALLET_BASE_KEY = "Bitcoin seed"
240
+ RANDOM_SEED_SIZE = 32
241
+
242
+ attr_reader :seed, :seed_hash
243
+
244
+ def initialize(opts = {})
245
+ @depth = 0
246
+ @index = 0
247
+ opts[:seed] = [opts[:seed_hex]].pack("H*") if opts[:seed_hex]
248
+ if opts[:seed]
249
+ @seed = opts[:seed]
250
+ @seed_hash = generate_seed_hash(@seed)
251
+ raise SeedGeneration::ImportError unless seed_valid?(@seed_hash)
252
+ set_seeded_keys
253
+ elsif opts[:private_key] || opts[:public_key]
254
+ raise ImportError, 'chain code required' unless opts[:chain_code]
255
+ @chain_code = opts[:chain_code]
256
+ if opts[:private_key]
257
+ @private_key = opts[:private_key]
258
+ @public_key = MoneyTree::PublicKey.new @private_key
259
+ else opts[:public_key]
260
+ @public_key = if opts[:public_key].is_a?(MoneyTree::PublicKey)
261
+ opts[:public_key]
262
+ else
263
+ MoneyTree::PublicKey.new(opts[:public_key])
264
+ end
265
+ end
266
+ else
267
+ generate_seed
268
+ set_seeded_keys
269
+ end
270
+ end
271
+
272
+ def is_private?
273
+ true
274
+ end
275
+
276
+ def generate_seed
277
+ @seed = OpenSSL::Random.random_bytes(32)
278
+ @seed_hash = generate_seed_hash(@seed)
279
+ raise SeedGeneration::ValidityError unless seed_valid?(@seed_hash)
280
+ end
281
+
282
+ def generate_seed_hash(seed)
283
+ hmac_sha512 HD_WALLET_BASE_KEY, seed
284
+ end
285
+
286
+ def seed_valid?(seed_hash)
287
+ return false unless seed_hash.bytesize == 64
288
+ master_key = left_from_hash(seed_hash)
289
+ !master_key.zero? && master_key < MoneyTree::Key::ORDER
290
+ end
291
+
292
+ def set_seeded_keys
293
+ @private_key = MoneyTree::PrivateKey.new key: left_from_hash(seed_hash)
294
+ @chain_code = right_from_hash(seed_hash)
295
+ @public_key = MoneyTree::PublicKey.new @private_key
296
+ end
297
+
298
+ def seed_hex
299
+ bytes_to_hex(seed)
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,127 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module MoneyTree
5
+ module Support
6
+ include OpenSSL
7
+
8
+ INT32_MAX = 256 ** [1].pack("L*").size
9
+ INT64_MAX = 256 ** [1].pack("Q*").size
10
+ BASE58_CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
11
+
12
+ def int_to_base58(int_val, leading_zero_bytes=0)
13
+ base58_val, base = '', BASE58_CHARS.size
14
+ while int_val > 0
15
+ int_val, remainder = int_val.divmod(base)
16
+ base58_val = BASE58_CHARS[remainder] + base58_val
17
+ end
18
+ base58_val
19
+ end
20
+
21
+ def base58_to_int(base58_val)
22
+ 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)
26
+ end
27
+ int_val
28
+ end
29
+
30
+ 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
+ end
34
+
35
+ 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
40
+ s
41
+ end
42
+ alias_method :base58_to_hex, :decode_base58
43
+
44
+ def to_serialized_base58(hex)
45
+ hash = sha256 hex
46
+ hash = sha256 hash
47
+ checksum = hash.slice(0..7)
48
+ address = hex + checksum
49
+ encode_base58 address
50
+ end
51
+
52
+ def from_serialized_base58(base58)
53
+ hex = decode_base58 base58
54
+ checksum = hex.slice!(-8..-1)
55
+ compare_checksum = sha256(sha256(hex)).slice(0..7)
56
+ raise EncodingError unless checksum == compare_checksum
57
+ hex
58
+ end
59
+
60
+ def digestify(digest_type, source, opts = {})
61
+ source = [source].pack("H*") unless opts[:ascii]
62
+ bytes_to_hex Digest.digest(digest_type, source)
63
+ end
64
+
65
+ def sha256(source, opts = {})
66
+ digestify('SHA256', source, opts)
67
+ end
68
+
69
+ def ripemd160(source, opts = {})
70
+ digestify('RIPEMD160', source, opts)
71
+ end
72
+
73
+ def encode_base64(hex)
74
+ Base64.encode64([hex].pack("H*")).chomp
75
+ end
76
+
77
+ def decode_base64(base64)
78
+ Base64.decode64(base64).unpack("H*")[0]
79
+ end
80
+
81
+ def hmac_sha512(key, message)
82
+ digest = Digest::SHA512.new
83
+ HMAC.digest digest, key, message
84
+ end
85
+
86
+ def hmac_sha512_hex(key, message)
87
+ md = hmac_sha512(key, message)
88
+ md.unpack("H*").first.rjust(64, '0')
89
+ end
90
+
91
+ def bytes_to_int(bytes, base = 16)
92
+ if bytes.is_a?(Array)
93
+ bytes = bytes.pack("C*")
94
+ end
95
+ bytes.unpack("H*")[0].to_i(16)
96
+ end
97
+
98
+ def int_to_hex(i, size=nil)
99
+ hex = i.to_s(16).downcase
100
+ if (hex.size % 2) != 0
101
+ hex = "#{0}#{hex}"
102
+ end
103
+
104
+ if size
105
+ hex.rjust(size, "0")
106
+ else
107
+ hex
108
+ end
109
+ end
110
+
111
+ def int_to_bytes(i)
112
+ [int_to_hex(i)].pack("H*")
113
+ end
114
+
115
+ def bytes_to_hex(bytes)
116
+ bytes.unpack("H*")[0].downcase
117
+ end
118
+
119
+ def hex_to_bytes(hex)
120
+ [hex].pack("H*")
121
+ end
122
+
123
+ def hex_to_int(hex)
124
+ hex.to_i(16)
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,3 @@
1
+ module MoneyTree
2
+ VERSION = "1.1"
3
+ end
data/lib/money-tree.rb ADDED
@@ -0,0 +1,12 @@
1
+ require "openssl_extensions"
2
+ require "money-tree/version"
3
+ require "money-tree/support"
4
+ require "money-tree/networks"
5
+ require "money-tree/key"
6
+ require "money-tree/address"
7
+ require "money-tree/networks"
8
+ require "money-tree/node"
9
+
10
+ module MoneyTree
11
+
12
+ end
@@ -0,0 +1,33 @@
1
+ # encoding: ascii-8bit
2
+
3
+ require 'openssl'
4
+
5
+ module MoneyTree
6
+ module OpenSSLExtensions
7
+ def self.add(point_0, point_1)
8
+ validate_points(point_0, point_1)
9
+
10
+ group = OpenSSL::PKey::EC::Group.new('secp256k1')
11
+
12
+ point_0_hex = point_0.to_bn.to_s(16)
13
+ point_0_pt = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(point_0_hex, 16))
14
+ point_1_hex = point_1.to_bn.to_s(16)
15
+ point_1_pt = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(point_1_hex, 16))
16
+
17
+ sum_point = point_0_pt.add(point_1_pt)
18
+
19
+ sum_point.to_bn.to_s(16)
20
+ end
21
+
22
+ def self.validate_points(*points)
23
+ points.each do |point|
24
+ if !point.is_a?(OpenSSL::PKey::EC::Point)
25
+ raise ArgumentError, "point must be an OpenSSL::PKey::EC::Point object"
26
+ elsif point.infinity?
27
+ raise ArgumentError, "point must not be infinity"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
@@ -0,0 +1,38 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'money-tree/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "money-tree-openssl"
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 (forked to remove ffi)}
12
+ spec.summary = %q{Bitcoin Hierarchical Deterministic Wallets in Ruby! (Bitcoin standard BIP0032)}
13
+ spec.homepage = "https://github.com/cte/money-tree"
14
+ spec.license = "MIT"
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)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ # used with gem install ... -P HighSecurity
22
+ # spec.cert_chain = ["certs/mattatgemco.pem"]
23
+
24
+ # # Sign gem when evaluating spec with `gem` command
25
+ # # unless ENV has set a SKIP_GEM_SIGNING
26
+ # if ($0 =~ /gem\z/) and not ENV.include?("SKIP_GEM_SIGNING")
27
+ # spec.signing_key = File.join(Gem.user_home, ".ssh", "gem-private_key.pem")
28
+ # end
29
+
30
+ spec.add_dependency 'openssl', '>= 2.2'
31
+
32
+ spec.add_development_dependency "bundler"
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"
38
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ describe MoneyTree::Address do
4
+ describe "initialize" do
5
+ it "generates a private key by default" do
6
+ address = MoneyTree::Address.new
7
+ expect(address.private_key.key.length).to eql(64)
8
+ end
9
+
10
+ it "generates a public key by default" do
11
+ address = MoneyTree::Address.new
12
+ expect(address.public_key.key.length).to eql(66)
13
+ end
14
+
15
+ it "imports a private key in hex form" do
16
+ address = MoneyTree::Address.new private_key: "5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b"
17
+ expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
18
+ expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
19
+ expect(address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
20
+ expect(address.private_key.to_s).to eql("KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK")
21
+ expect(address.public_key.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
22
+ end
23
+
24
+ it "imports a private key in compressed wif format" do
25
+ address = MoneyTree::Address.new private_key: "KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK"
26
+ expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
27
+ expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
28
+ expect(address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
29
+ end
30
+
31
+ it "imports a private key in uncompressed wif format" do
32
+ address = MoneyTree::Address.new private_key: "5JXz5ZyFk31oHVTQxqce7yitCmTAPxBqeGQ4b7H3Aj3L45wUhoa"
33
+ expect(address.private_key.key).to eql("5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b")
34
+ expect(address.public_key.key).to eql("022dfc2557a007c93092c2915f11e8aa70c4f399a6753e2e908330014091580e4b")
35
+ end
36
+ end
37
+
38
+ describe "to_s" do
39
+ before do
40
+ @address = MoneyTree::Address.new private_key: "5eae5375fb5f7a0ea650566363befa2830ef441bdcb19198adf318faee86d64b"
41
+ end
42
+
43
+ it "returns compressed base58 public key" do
44
+ expect(@address.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
45
+ expect(@address.public_key.to_s).to eql("13uVqa35BMo4mYq9LiZrXVzoz9EFZ6aoXe")
46
+ end
47
+
48
+ it "returns compressed WIF private key" do
49
+ expect(@address.private_key.to_s).to eql("KzPkwAXJ4wtXHnbamTaJqoMrzwCUUJaqhUxnqYhnZvZH6KhgmDPK")
50
+ end
51
+ end
52
+
53
+ context "testnet3" do
54
+ before do
55
+ @address = MoneyTree::Address.new network: :bitcoin_testnet
56
+ end
57
+
58
+ it "returns a testnet address" do
59
+ expect(%w(m n)).to include(@address.to_s(network: :bitcoin_testnet)[0])
60
+ end
61
+ end
62
+ end