money-tree-extended 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|