tapyrus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +12 -0
  7. data/CODE_OF_CONDUCT.md +49 -0
  8. data/Gemfile +6 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +100 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/exe/tapyrusrb-cli +5 -0
  15. data/exe/tapyrusrbd +41 -0
  16. data/lib/openassets/marker_output.rb +20 -0
  17. data/lib/openassets/payload.rb +54 -0
  18. data/lib/openassets/util.rb +28 -0
  19. data/lib/openassets.rb +9 -0
  20. data/lib/tapyrus/base58.rb +38 -0
  21. data/lib/tapyrus/block.rb +77 -0
  22. data/lib/tapyrus/block_header.rb +88 -0
  23. data/lib/tapyrus/bloom_filter.rb +78 -0
  24. data/lib/tapyrus/chain_params.rb +90 -0
  25. data/lib/tapyrus/chainparams/mainnet.yml +41 -0
  26. data/lib/tapyrus/chainparams/regtest.yml +38 -0
  27. data/lib/tapyrus/chainparams/testnet.yml +41 -0
  28. data/lib/tapyrus/constants.rb +195 -0
  29. data/lib/tapyrus/descriptor.rb +147 -0
  30. data/lib/tapyrus/ext_key.rb +337 -0
  31. data/lib/tapyrus/key.rb +296 -0
  32. data/lib/tapyrus/key_path.rb +26 -0
  33. data/lib/tapyrus/logger.rb +42 -0
  34. data/lib/tapyrus/merkle_tree.rb +149 -0
  35. data/lib/tapyrus/message/addr.rb +35 -0
  36. data/lib/tapyrus/message/base.rb +28 -0
  37. data/lib/tapyrus/message/block.rb +46 -0
  38. data/lib/tapyrus/message/block_transaction_request.rb +45 -0
  39. data/lib/tapyrus/message/block_transactions.rb +31 -0
  40. data/lib/tapyrus/message/block_txn.rb +27 -0
  41. data/lib/tapyrus/message/cmpct_block.rb +42 -0
  42. data/lib/tapyrus/message/error.rb +10 -0
  43. data/lib/tapyrus/message/fee_filter.rb +27 -0
  44. data/lib/tapyrus/message/filter_add.rb +28 -0
  45. data/lib/tapyrus/message/filter_clear.rb +17 -0
  46. data/lib/tapyrus/message/filter_load.rb +39 -0
  47. data/lib/tapyrus/message/get_addr.rb +17 -0
  48. data/lib/tapyrus/message/get_block_txn.rb +27 -0
  49. data/lib/tapyrus/message/get_blocks.rb +29 -0
  50. data/lib/tapyrus/message/get_data.rb +21 -0
  51. data/lib/tapyrus/message/get_headers.rb +28 -0
  52. data/lib/tapyrus/message/header_and_short_ids.rb +57 -0
  53. data/lib/tapyrus/message/headers.rb +35 -0
  54. data/lib/tapyrus/message/headers_parser.rb +24 -0
  55. data/lib/tapyrus/message/inv.rb +21 -0
  56. data/lib/tapyrus/message/inventories_parser.rb +23 -0
  57. data/lib/tapyrus/message/inventory.rb +51 -0
  58. data/lib/tapyrus/message/mem_pool.rb +17 -0
  59. data/lib/tapyrus/message/merkle_block.rb +42 -0
  60. data/lib/tapyrus/message/network_addr.rb +63 -0
  61. data/lib/tapyrus/message/not_found.rb +21 -0
  62. data/lib/tapyrus/message/ping.rb +30 -0
  63. data/lib/tapyrus/message/pong.rb +26 -0
  64. data/lib/tapyrus/message/prefilled_tx.rb +29 -0
  65. data/lib/tapyrus/message/reject.rb +46 -0
  66. data/lib/tapyrus/message/send_cmpct.rb +43 -0
  67. data/lib/tapyrus/message/send_headers.rb +16 -0
  68. data/lib/tapyrus/message/tx.rb +30 -0
  69. data/lib/tapyrus/message/ver_ack.rb +17 -0
  70. data/lib/tapyrus/message/version.rb +69 -0
  71. data/lib/tapyrus/message.rb +70 -0
  72. data/lib/tapyrus/mnemonic/wordlist/chinese_simplified.txt +2048 -0
  73. data/lib/tapyrus/mnemonic/wordlist/chinese_traditional.txt +2048 -0
  74. data/lib/tapyrus/mnemonic/wordlist/english.txt +2048 -0
  75. data/lib/tapyrus/mnemonic/wordlist/french.txt +2048 -0
  76. data/lib/tapyrus/mnemonic/wordlist/italian.txt +2048 -0
  77. data/lib/tapyrus/mnemonic/wordlist/japanese.txt +2048 -0
  78. data/lib/tapyrus/mnemonic/wordlist/spanish.txt +2048 -0
  79. data/lib/tapyrus/mnemonic.rb +77 -0
  80. data/lib/tapyrus/network/connection.rb +73 -0
  81. data/lib/tapyrus/network/message_handler.rb +241 -0
  82. data/lib/tapyrus/network/peer.rb +223 -0
  83. data/lib/tapyrus/network/peer_discovery.rb +42 -0
  84. data/lib/tapyrus/network/pool.rb +135 -0
  85. data/lib/tapyrus/network.rb +13 -0
  86. data/lib/tapyrus/node/cli.rb +112 -0
  87. data/lib/tapyrus/node/configuration.rb +38 -0
  88. data/lib/tapyrus/node/spv.rb +79 -0
  89. data/lib/tapyrus/node.rb +7 -0
  90. data/lib/tapyrus/opcodes.rb +178 -0
  91. data/lib/tapyrus/out_point.rb +44 -0
  92. data/lib/tapyrus/rpc/http_server.rb +65 -0
  93. data/lib/tapyrus/rpc/request_handler.rb +150 -0
  94. data/lib/tapyrus/rpc/tapyrus_core_client.rb +72 -0
  95. data/lib/tapyrus/rpc.rb +7 -0
  96. data/lib/tapyrus/script/multisig.rb +92 -0
  97. data/lib/tapyrus/script/script.rb +551 -0
  98. data/lib/tapyrus/script/script_error.rb +111 -0
  99. data/lib/tapyrus/script/script_interpreter.rb +668 -0
  100. data/lib/tapyrus/script/tx_checker.rb +81 -0
  101. data/lib/tapyrus/script_witness.rb +38 -0
  102. data/lib/tapyrus/secp256k1/native.rb +174 -0
  103. data/lib/tapyrus/secp256k1/ruby.rb +123 -0
  104. data/lib/tapyrus/secp256k1.rb +12 -0
  105. data/lib/tapyrus/slip39/share.rb +122 -0
  106. data/lib/tapyrus/slip39/sss.rb +245 -0
  107. data/lib/tapyrus/slip39/wordlist/english.txt +1024 -0
  108. data/lib/tapyrus/slip39.rb +93 -0
  109. data/lib/tapyrus/store/chain_entry.rb +67 -0
  110. data/lib/tapyrus/store/db/level_db.rb +98 -0
  111. data/lib/tapyrus/store/db.rb +9 -0
  112. data/lib/tapyrus/store/spv_chain.rb +101 -0
  113. data/lib/tapyrus/store.rb +9 -0
  114. data/lib/tapyrus/tx.rb +347 -0
  115. data/lib/tapyrus/tx_in.rb +89 -0
  116. data/lib/tapyrus/tx_out.rb +74 -0
  117. data/lib/tapyrus/util.rb +133 -0
  118. data/lib/tapyrus/validation.rb +115 -0
  119. data/lib/tapyrus/version.rb +3 -0
  120. data/lib/tapyrus/wallet/account.rb +151 -0
  121. data/lib/tapyrus/wallet/base.rb +162 -0
  122. data/lib/tapyrus/wallet/db.rb +81 -0
  123. data/lib/tapyrus/wallet/master_key.rb +110 -0
  124. data/lib/tapyrus/wallet.rb +8 -0
  125. data/lib/tapyrus.rb +219 -0
  126. data/tapyrusrb.conf.sample +0 -0
  127. data/tapyrusrb.gemspec +47 -0
  128. metadata +451 -0
@@ -0,0 +1,115 @@
1
+ module Tapyrus
2
+
3
+ class Validation
4
+
5
+ # check transaction validation
6
+ def check_tx(tx, state)
7
+ # Basic checks that don't depend on any context
8
+ if tx.inputs.empty?
9
+ return state.DoS(10, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-txns-vin-empty')
10
+ end
11
+
12
+ if tx.outputs.empty?
13
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-txns-vout-empty')
14
+ end
15
+
16
+ # Size limits (this doesn't take the witness into account, as that hasn't been checked for malleability)
17
+ if tx.serialize_old_format.bytesize * Tapyrus::WITNESS_SCALE_FACTOR > Tapyrus::MAX_BLOCK_WEIGHT
18
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-txns-oversize')
19
+ end
20
+
21
+ # Check for negative or overflow output values
22
+ amount = 0
23
+ tx.outputs.each do |o|
24
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-txns-vout-negative') if o.value < 0
25
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-txns-vout-toolarge') if MAX_MONEY < o.value
26
+ amount += o.value
27
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-txns-vout-toolarge') if MAX_MONEY < amount
28
+ end
29
+
30
+ # Check for duplicate inputs - note that this check is slow so we skip it in CheckBlock
31
+ out_points = tx.inputs.map{|i|i.out_point.to_payload}
32
+ unless out_points.size == out_points.uniq.size
33
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-txns-inputs-duplicate')
34
+ end
35
+
36
+ if tx.coinbase_tx?
37
+ if tx.inputs[0].script_sig.size < 2 || tx.inputs[0].script_sig.size > 100
38
+ return state.DoS(100, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-cb-length')
39
+ end
40
+ else
41
+ tx.inputs.each do |i|
42
+ if i.out_point.nil? || !i.out_point.valid?
43
+ return state.DoS(10, reject_code: Message::Reject::CODE_INVALID, reject_reason: 'bad-txns-prevout-null')
44
+ end
45
+ end
46
+ end
47
+ true
48
+ end
49
+
50
+ # check proof of work
51
+ def check_block_header(header, state)
52
+ header.block_hash
53
+ header.bits
54
+
55
+ end
56
+
57
+ def check_block(block, state)
58
+ # check block header
59
+ return false unless check_block_header(block.header, state)
60
+
61
+ # check merkle root
62
+
63
+ # size limits
64
+
65
+ # first tx is coinbase?
66
+
67
+ # check tx count
68
+
69
+ # check sigop count
70
+ end
71
+
72
+ end
73
+
74
+ class ValidationState
75
+
76
+ MODE = {valid: 0, invlid: 1, error: 2}
77
+
78
+ attr_accessor :mode
79
+ attr_accessor :n_dos
80
+ attr_accessor :reject_reason
81
+ attr_accessor :reject_code
82
+ attr_accessor :corruption_possible
83
+ attr_accessor :debug_message
84
+
85
+ def initialize
86
+ @mode = MODE[:valid]
87
+ @n_dos = 0
88
+ @reject_code = 0
89
+ @corruption_possible = false
90
+ end
91
+
92
+ def DoS(level, ret: false, reject_code: 0, reject_reason: '', corruption_in: false, debug_message: '')
93
+ @reject_code = reject_code
94
+ @reject_reason = reject_reason
95
+ @corruption_possible = corruption_in
96
+ @debug_message = debug_message
97
+ return ret if mode == MODE[:error]
98
+ @n_dos += level
99
+ @mode = MODE[:invalid]
100
+ ret
101
+ end
102
+
103
+ def valid?
104
+ mode == MODE[:valid]
105
+ end
106
+
107
+ def invalid?
108
+ mode == MODE[:invalid]
109
+ end
110
+
111
+ def error?
112
+ mode == MODE[:error]
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,3 @@
1
+ module Tapyrus
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,151 @@
1
+ module Tapyrus
2
+ module Wallet
3
+
4
+ # the account in BIP-44
5
+ class Account
6
+
7
+ PURPOSE_TYPE = {legacy: 44, nested_witness: 49, native_segwit: 84}
8
+
9
+ attr_reader :purpose # either 44 or 49 or 84
10
+ attr_reader :index # BIP-44 index
11
+ attr_reader :name # account name
12
+ attr_reader :account_key # account xpub key Tapyrus::ExtPubkey
13
+ attr_accessor :receive_depth # receive address depth(address index)
14
+ attr_accessor :change_depth # change address depth(address index)
15
+ attr_accessor :lookahead
16
+ attr_accessor :wallet
17
+
18
+ def initialize(account_key, purpose = PURPOSE_TYPE[:native_segwit], index = 0, name = '')
19
+ validate_params!(account_key, purpose, index)
20
+ @purpose = purpose
21
+ @index = index
22
+ @name = name
23
+ @receive_depth = 0
24
+ @change_depth = 0
25
+ @lookahead = 10
26
+ @account_key = account_key
27
+ end
28
+
29
+ def self.parse_from_payload(payload)
30
+ buf = StringIO.new(payload)
31
+ account_key = Tapyrus::ExtPubkey.parse_from_payload(buf.read(78))
32
+ payload = buf.read
33
+ name, payload = Tapyrus.unpack_var_string(payload)
34
+ name = name.force_encoding('utf-8')
35
+ purpose, index, receive_depth, change_depth, lookahead = payload.unpack('I*')
36
+ a = Account.new(account_key, purpose, index, name)
37
+ a.receive_depth = receive_depth
38
+ a.change_depth = change_depth
39
+ a.lookahead = lookahead
40
+ a
41
+ end
42
+
43
+ def to_payload
44
+ payload = account_key.to_payload
45
+ payload << Tapyrus.pack_var_string(name.unpack('H*').first.htb)
46
+ payload << [purpose, index, receive_depth, change_depth, lookahead].pack('I*')
47
+ payload
48
+ end
49
+
50
+ # whether support witness
51
+ def witness?
52
+ [PURPOSE_TYPE[:nested_witness], PURPOSE_TYPE[:native_segwit]].include?(purpose)
53
+ end
54
+
55
+ # create new receive key
56
+ # @return [Tapyrus::ExtPubkey]
57
+ def create_receive
58
+ @receive_depth += 1
59
+ save
60
+ save_key(0, @receive_depth, derive_key(0, @receive_depth))
61
+ end
62
+
63
+ # create new change key
64
+ # @return [Tapyrus::ExtPubkey]
65
+ def create_change
66
+ @change_depth += 1
67
+ save
68
+ save_key(1, @change_depth, derive_key(1, @change_depth))
69
+ end
70
+
71
+ # save this account payload to database.
72
+ def save
73
+ wallet.db.save_account(self)
74
+ save_key(0, receive_depth, derive_key(0, receive_depth)) if receive_depth.zero?
75
+ save_key(1, change_depth, derive_key(1, change_depth)) if change_depth.zero?
76
+ end
77
+
78
+ # @param purpose 0:recieve, 1:change
79
+ # @param index receive_depth or change_depth
80
+ # @param key the key to be saved
81
+ def save_key(purpose, index, key)
82
+ wallet.db.save_key(self, purpose, index, key)
83
+ end
84
+
85
+ # get the list of derived keys for receive key.
86
+ # @return [Array[Tapyrus::ExtPubkey]]
87
+ def derived_receive_keys
88
+ (receive_depth + 1).times.map{|i|derive_key(0,i)}
89
+ end
90
+
91
+ # get the list of derived keys for change key.
92
+ # @return [Array[Tapyrus::ExtPubkey]]
93
+ def derived_change_keys
94
+ (change_depth + 1).times.map{|i|derive_key(1,i)}
95
+ end
96
+
97
+ # get account type label.
98
+ def type
99
+ case purpose
100
+ when PURPOSE_TYPE[:legacy]
101
+ 'pubkeyhash'
102
+ when PURPOSE_TYPE[:nested_witness]
103
+ 'p2wpkh-p2sh'
104
+ when PURPOSE_TYPE[:native_segwit]
105
+ 'p2wpkh'
106
+ else
107
+ 'unknown'
108
+ end
109
+ end
110
+
111
+ # account derivation path
112
+ def path
113
+ "m/#{purpose}'/#{Tapyrus.chain_params.bip44_coin_type}'/#{index}'"
114
+ end
115
+
116
+ def watch_only
117
+ false # TODO implements import watch only address.
118
+ end
119
+
120
+ # get data elements tobe monitored with Bloom Filter.
121
+ # @return [Array[String]]
122
+ def watch_targets
123
+ wallet.db.get_keys(self).map { |key| Tapyrus.hash160(key) }
124
+ end
125
+
126
+ def to_h
127
+ {
128
+ name: name, type: type, index: index, receive_depth: receive_depth, change_depth: change_depth,
129
+ look_ahead: lookahead, receive_address: derive_key(0, receive_depth).addr,
130
+ change_address: derive_key(1, change_depth).addr,
131
+ account_key: account_key.to_base58, path: path, watch_only: watch_only
132
+ }
133
+ end
134
+
135
+ private
136
+
137
+ def derive_key(branch, address_index)
138
+ account_key.derive(branch).derive(address_index)
139
+ end
140
+
141
+ def validate_params!(account_key, purpose, index)
142
+ raise 'account_key must be an instance of Tapyrus::ExtPubkey.' unless account_key.is_a?(Tapyrus::ExtPubkey)
143
+ raise 'Account key and index does not match.' unless account_key.number == (index + Tapyrus::HARDENED_THRESHOLD)
144
+ version_bytes = Tapyrus::ExtPubkey.version_from_purpose(purpose + Tapyrus::HARDENED_THRESHOLD)
145
+ raise 'The purpose and the account key do not match.' unless account_key.version == version_bytes
146
+ end
147
+
148
+ end
149
+
150
+ end
151
+ end
@@ -0,0 +1,162 @@
1
+ require 'leveldb-native'
2
+
3
+ module Tapyrus
4
+ module Wallet
5
+
6
+ class Base
7
+
8
+ attr_accessor :wallet_id
9
+ attr_reader :db
10
+ attr_reader :path
11
+
12
+ VERSION = 1
13
+
14
+ # get wallet dir path
15
+ def self.default_path_prefix
16
+ "#{Tapyrus.base_dir}/db/wallet/"
17
+ end
18
+
19
+ # Create new wallet. If wallet already exist, throw error.
20
+ # The wallet generates a seed using SecureRandom and store to db at initialization.
21
+ # @param [String] wallet_id new wallet id.
22
+ # @param [String] path_prefix wallet file path prefix.
23
+ # @return [Tapyrus::Wallet::Base] the wallet
24
+ def self.create(wallet_id = 1, path_prefix = default_path_prefix)
25
+ raise ArgumentError, "wallet_id : #{wallet_id} already exist." if self.exist?(wallet_id, path_prefix)
26
+ w = self.new(wallet_id, path_prefix)
27
+ # generate seed
28
+ raise RuntimeError, 'the seed already exist.' if w.db.registered_master?
29
+ master = Tapyrus::Wallet::MasterKey.generate
30
+ w.db.register_master_key(master)
31
+ w.create_account('Default')
32
+ w
33
+ end
34
+
35
+ # load wallet with specified +wallet_id+
36
+ # @return [Tapyrus::Wallet::Base] the wallet
37
+ def self.load(wallet_id, path_prefix = default_path_prefix)
38
+ raise ArgumentError, "wallet_id : #{wallet_id} dose not exist." unless self.exist?(wallet_id, path_prefix)
39
+ self.new(wallet_id, path_prefix)
40
+ end
41
+
42
+ # get wallets path
43
+ # @return [Array] Array of paths for each wallet dir.
44
+ def self.wallet_paths(path_prefix = default_path_prefix)
45
+ Dir.glob("#{path_prefix}wallet*/").sort
46
+ end
47
+
48
+ # get current wallet
49
+ def self.current_wallet(path_prefix = default_path_prefix)
50
+ path = wallet_paths(path_prefix).first # TODO default wallet selection
51
+ return nil unless path
52
+ path.slice!(path_prefix + 'wallet')
53
+ path.slice!('/')
54
+ self.load(path.to_i, path_prefix)
55
+ end
56
+
57
+ # get account list based on BIP-44
58
+ def accounts(purpose = nil)
59
+ list = []
60
+ db.accounts.each do |raw|
61
+ a = Account.parse_from_payload(raw)
62
+ next if purpose && purpose != a.purpose
63
+ a.wallet = self
64
+ list << a
65
+ end
66
+ list
67
+ end
68
+
69
+ # create new account
70
+ # @param [Integer] purpose BIP44's purpose.
71
+ # @param [String] name a account name.
72
+ # @return [Tapyrus::Wallet::Account]
73
+ def create_account(purpose = Account::PURPOSE_TYPE[:native_segwit], name)
74
+ raise ArgumentError.new('Account already exists.') if find_account(name, purpose)
75
+ index = accounts.size
76
+ path = "m/#{purpose}'/#{Tapyrus.chain_params.bip44_coin_type}'/#{index}'"
77
+ account_key = master_key.derive(path).ext_pubkey
78
+ account = Account.new(account_key, purpose, index, name)
79
+ account.wallet = self
80
+ account.save
81
+ account
82
+ end
83
+
84
+ # get wallet balance.
85
+ # @param [Tapyrus::Wallet::Account] account a account in the wallet.
86
+ def get_balance(account)
87
+ # TODO get from utxo db.
88
+ 0.00000000
89
+ end
90
+
91
+ # create new tapyrus address for receiving payments.
92
+ # @param [String] account_name an account name.
93
+ # @return [String] generated address.
94
+ def generate_new_address(account_name)
95
+ account = find_account(account_name)
96
+ raise ArgumentError.new('Account does not exist.') unless account
97
+ account.create_receive.addr
98
+ end
99
+
100
+ # get wallet version.
101
+ def version
102
+ db.version
103
+ end
104
+
105
+ # close database wallet
106
+ def close
107
+ db.close
108
+ end
109
+
110
+ # get master key
111
+ # @return [Tapyrus::Wallet::MasterKey]
112
+ def master_key
113
+ db.master_key
114
+ end
115
+
116
+ # encrypt wallet
117
+ # @param [String] passphrase the wallet passphrase
118
+ def encrypt(passphrase)
119
+ master_key.encrypt(passphrase)
120
+ db.register_master_key(master_key)
121
+ end
122
+
123
+ # decrypt wallet
124
+ # @param [String] passphrase the wallet passphrase
125
+ def decrypt(passphrase)
126
+
127
+ end
128
+
129
+ # wallet information
130
+ def to_h
131
+ a = accounts.map(&:to_h)
132
+ { wallet_id: wallet_id, version: version, account_depth: a.size, accounts: a, master: {encrypted: master_key.encrypted} }
133
+ end
134
+
135
+ # get data elements tobe monitored with Bloom Filter.
136
+ # @return [Array[String]]
137
+ def watch_targets
138
+ accounts.map(&:watch_targets).flatten
139
+ end
140
+
141
+ private
142
+
143
+ def initialize(wallet_id, path_prefix)
144
+ @path = "#{path_prefix}wallet#{wallet_id}/"
145
+ @db = Tapyrus::Wallet::DB.new(@path)
146
+ @wallet_id = wallet_id
147
+ end
148
+
149
+ def self.exist?(wallet_id, path_prefix)
150
+ path = "#{path_prefix}wallet#{wallet_id}"
151
+ Dir.exist?(path)
152
+ end
153
+
154
+ # find account using +account_name+
155
+ def find_account(account_name, purpose = nil)
156
+ accounts(purpose).find{|a| a.name == account_name}
157
+ end
158
+
159
+ end
160
+
161
+ end
162
+ end
@@ -0,0 +1,81 @@
1
+ module Tapyrus
2
+ module Wallet
3
+
4
+ class DB
5
+
6
+ KEY_PREFIX = {
7
+ account: 'a', # key: account index, value: Account raw data.
8
+ master: 'm', # value: wallet seed.
9
+ version: 'v', # value: wallet version
10
+ key: 'k', # key: path to the key, value: public key
11
+ }
12
+
13
+ attr_reader :level_db
14
+ attr_accessor :master_key
15
+
16
+ def initialize(path = "#{Tapyrus.base_dir}/db/wallet")
17
+ FileUtils.mkdir_p(path)
18
+ @level_db = ::LevelDBNative::DB.new(path)
19
+ end
20
+
21
+ # close database
22
+ def close
23
+ level_db.close
24
+ end
25
+
26
+ # get accounts raw data.
27
+ def accounts
28
+ from = KEY_PREFIX[:account] + '00000000'
29
+ to = KEY_PREFIX[:account] + 'ffffffff'
30
+ level_db.each(from: from, to: to).map { |k, v| v}
31
+ end
32
+
33
+ def save_account(account)
34
+ level_db.batch do
35
+ id = [account.purpose, account.index].pack('I*').bth
36
+ key = KEY_PREFIX[:account] + id
37
+ level_db.put(key, account.to_payload)
38
+ end
39
+ end
40
+
41
+ def save_key(account, purpose, index, key)
42
+ pubkey = key.pub
43
+ id = [account.purpose, account.index, purpose, index].pack('I*').bth
44
+ k = KEY_PREFIX[:key] + id
45
+ level_db.put(k, pubkey)
46
+ key
47
+ end
48
+
49
+ def get_keys(account)
50
+ id = [account.purpose, account.index].pack('I*').bth
51
+ from = KEY_PREFIX[:key] + id + '00000000'
52
+ to = KEY_PREFIX[:key] + id + 'ffffffff'
53
+ level_db.each(from: from, to: to).map { |k, v| v}
54
+ end
55
+
56
+ # get master_key
57
+ def master_key
58
+ @master_key ||= Tapyrus::Wallet::MasterKey.parse_from_payload(level_db.get(KEY_PREFIX[:master]))
59
+ end
60
+
61
+ # save seed
62
+ # @param [Tapyrus::Wallet::MasterKey] master a master key.
63
+ def register_master_key(master)
64
+ level_db.put(KEY_PREFIX[:master], master.to_payload)
65
+ level_db.put(KEY_PREFIX[:version], Tapyrus::Wallet::Base::VERSION.to_s)
66
+ @master_key = master
67
+ end
68
+
69
+ # whether master key registered.
70
+ def registered_master?
71
+ !level_db.get(KEY_PREFIX[:master]).nil?
72
+ end
73
+
74
+ # wallet version
75
+ def version
76
+ level_db.get(KEY_PREFIX[:version]).to_i
77
+ end
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,110 @@
1
+ module Tapyrus
2
+ module Wallet
3
+
4
+ # HD Wallet master seed
5
+ class MasterKey
6
+ extend Tapyrus::Util
7
+ include Tapyrus::Util
8
+ include Tapyrus::KeyPath
9
+
10
+ attr_reader :seed
11
+ attr_accessor :salt
12
+ attr_accessor :encrypted
13
+ attr_accessor :mnemonic # ephemeral data existing only at initialization
14
+
15
+ def initialize(seed, salt: '', encrypted: false, mnemonic: nil)
16
+ @mnemonic = mnemonic
17
+ @seed = seed
18
+ @encrypted = encrypted
19
+ @salt = salt
20
+ end
21
+
22
+ # generate new master key.
23
+ # @return Tapyrus::Wallet::MasterKey
24
+ def self.generate
25
+ entropy = SecureRandom.hex(32)
26
+ mnemonic = Tapyrus::Mnemonic.new('english')
27
+ self.recover_from_words(mnemonic.to_mnemonic(entropy))
28
+ end
29
+
30
+ # recover master key from mnemonic word list.
31
+ # @param [Array] words the mnemonic word list.
32
+ # @return Tapyrus::Wallet::MasterKey
33
+ def self.recover_from_words(words)
34
+ mnemonic = Tapyrus::Mnemonic.new('english')
35
+ seed = mnemonic.to_seed(words)
36
+ self.new(seed, mnemonic: words)
37
+ end
38
+
39
+ # parse master key raw data
40
+ # @param [String] payload raw data
41
+ # @return [Tapyrus::Wallet::MasterKey]
42
+ def self.parse_from_payload(payload)
43
+ flag, payload = unpack_var_int(payload)
44
+ raise 'encrypted flag is invalid.' unless [0, 1].include?(flag)
45
+ salt, payload = unpack_var_string(payload)
46
+ salt = '' unless salt
47
+ seed, payload = unpack_var_string(payload)
48
+ self.new(seed.bth, salt: salt.bth, encrypted: flag == 1)
49
+ end
50
+
51
+ # generate payload with following format
52
+ # [encrypted(false:0, true:1)][salt(var str)][seed(var str)]
53
+ def to_payload
54
+ flg = encrypted ? 1 : 0
55
+ pack_var_int(flg) << [salt, seed].map{|v|pack_var_string(v.htb)}.join
56
+ end
57
+
58
+ # get master key
59
+ # @return [Tapyrus::ExtKey] the master key
60
+ def key
61
+ raise 'seed is encrypted. please decrypt the seed.' if encrypted
62
+ Tapyrus::ExtKey.generate_master(seed)
63
+ end
64
+
65
+ # derive child key using derivation path.
66
+ # @return [Tapyrus::ExtKey]
67
+ def derive(path)
68
+ derived_key = key
69
+ parse_key_path(path).each{|num| derived_key = derived_key.derive(num)}
70
+ derived_key
71
+ end
72
+
73
+ # encrypt seed
74
+ def encrypt(passphrase)
75
+ raise 'The wallet is already encrypted.' if encrypted
76
+ @salt = SecureRandom.hex(16)
77
+ enc = OpenSSL::Cipher.new('AES-256-CBC')
78
+ enc.encrypt
79
+ enc.key, enc.iv = key_iv(enc, passphrase)
80
+ encrypted_data = ''
81
+ encrypted_data << enc.update(seed)
82
+ encrypted_data << enc.final
83
+ @seed = encrypted_data
84
+ @encrypted = true
85
+ end
86
+
87
+ # decrypt seed
88
+ def decrypt(passphrase)
89
+ raise 'The wallet is not encrypted.' unless encrypted
90
+ dec = OpenSSL::Cipher.new('AES-256-CBC')
91
+ dec.decrypt
92
+ dec.key, dec.iv = key_iv(dec, passphrase)
93
+ decrypted_data = ''
94
+ decrypted_data << dec.update(seed)
95
+ decrypted_data << dec.final
96
+ @seed = decrypted_data
97
+ @encrypted = false
98
+ @salt = ''
99
+ end
100
+
101
+ private
102
+
103
+ def key_iv(enc, passphrase)
104
+ key_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(passphrase, salt, 2000, enc.key_len + enc.iv_len)
105
+ [key_iv[0, enc.key_len], key_iv[enc.key_len, enc.iv_len]]
106
+ end
107
+
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,8 @@
1
+ module Tapyrus
2
+ module Wallet
3
+ autoload :Base, 'tapyrus/wallet/base'
4
+ autoload :Account, 'tapyrus/wallet/account'
5
+ autoload :DB, 'tapyrus/wallet/db'
6
+ autoload :MasterKey, 'tapyrus/wallet/master_key'
7
+ end
8
+ end