tapyrus 0.1.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.
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