money-tree-extended 0.11.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 +7 -0
- data/.gitignore +17 -0
- data/.idea/$CACHE_FILE$ +6 -0
- data/.idea/.gitignore +2 -0
- data/.idea/.rakeTasks +7 -0
- data/.idea/misc.xml +7 -0
- data/.idea/modules.xml +8 -0
- data/.idea/money-tree.iml +31 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +1 -0
- data/.simplecov +7 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +258 -0
- data/Rakefile +6 -0
- data/certs/mattatgemco.pem +24 -0
- data/checksum/money-tree-0.9.0.gem.sha512 +1 -0
- data/donation_btc_qr_code.gif +0 -0
- data/lib/money-tree/address.rb +16 -0
- data/lib/money-tree/key.rb +265 -0
- data/lib/money-tree/networks.rb +71 -0
- data/lib/money-tree/node.rb +297 -0
- data/lib/money-tree/support.rb +127 -0
- data/lib/money-tree/version.rb +3 -0
- data/lib/money-tree.rb +12 -0
- data/lib/openssl_extensions.rb +73 -0
- data/money-tree.gemspec +38 -0
- data/spec/lib/money-tree/address_spec.rb +62 -0
- data/spec/lib/money-tree/node_spec.rb +807 -0
- data/spec/lib/money-tree/openssl_extensions_spec.rb +23 -0
- data/spec/lib/money-tree/private_key_spec.rb +121 -0
- data/spec/lib/money-tree/public_key_spec.rb +187 -0
- data/spec/lib/money-tree/support_spec.rb +32 -0
- data/spec/spec_helper.rb +3 -0
- metadata +184 -0
@@ -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
|
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,73 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'ffi'
|
5
|
+
|
6
|
+
module MoneyTree
|
7
|
+
module OpenSSLExtensions
|
8
|
+
extend FFI::Library
|
9
|
+
ffi_lib ['libssl.so.1.0.0', 'libssl.so.10', 'libssl1.0.0', 'ssl']
|
10
|
+
|
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)
|
25
|
+
validate_points(point_0, point_1)
|
26
|
+
eckey = EC_KEY_new_by_curve_name(NID_secp256k1)
|
27
|
+
group = EC_KEY_get0_group(eckey)
|
28
|
+
|
29
|
+
point_0_hex = point_0.to_bn.to_s(16)
|
30
|
+
point_0_pt = EC_POINT_hex2point(group, point_0_hex, nil, nil)
|
31
|
+
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
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.validate_points(*points)
|
53
|
+
points.each do |point|
|
54
|
+
if !point.is_a?(OpenSSL::PKey::EC::Point)
|
55
|
+
raise ArgumentError, "point must be an OpenSSL::PKey::EC::Point object"
|
56
|
+
elsif point.infinity?
|
57
|
+
raise ArgumentError, "point must not be infinity"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
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
ADDED
@@ -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-extended"
|
8
|
+
spec.version = MoneyTree::VERSION
|
9
|
+
spec.authors = ["Micah Winkelspecht", "Nikhar Ramchunder"]
|
10
|
+
spec.email = ["winkelspecht@gmail.com"]
|
11
|
+
spec.description = %q{A Ruby Gem implementation of Bitcoin HD Wallets (Extended)}
|
12
|
+
spec.summary = %q{Bitcoin Hierarchical Deterministic Wallets in Ruby! (Bitcoin standard BIP0032)}
|
13
|
+
spec.homepage = "https://github.com/nikharR/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
|
+
# # 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
|
+
|
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"
|
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
|