tapyrus 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +100 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/tapyrusrb-cli +5 -0
- data/exe/tapyrusrbd +41 -0
- data/lib/openassets/marker_output.rb +20 -0
- data/lib/openassets/payload.rb +54 -0
- data/lib/openassets/util.rb +28 -0
- data/lib/openassets.rb +9 -0
- data/lib/tapyrus/base58.rb +38 -0
- data/lib/tapyrus/block.rb +77 -0
- data/lib/tapyrus/block_header.rb +88 -0
- data/lib/tapyrus/bloom_filter.rb +78 -0
- data/lib/tapyrus/chain_params.rb +90 -0
- data/lib/tapyrus/chainparams/mainnet.yml +41 -0
- data/lib/tapyrus/chainparams/regtest.yml +38 -0
- data/lib/tapyrus/chainparams/testnet.yml +41 -0
- data/lib/tapyrus/constants.rb +195 -0
- data/lib/tapyrus/descriptor.rb +147 -0
- data/lib/tapyrus/ext_key.rb +337 -0
- data/lib/tapyrus/key.rb +296 -0
- data/lib/tapyrus/key_path.rb +26 -0
- data/lib/tapyrus/logger.rb +42 -0
- data/lib/tapyrus/merkle_tree.rb +149 -0
- data/lib/tapyrus/message/addr.rb +35 -0
- data/lib/tapyrus/message/base.rb +28 -0
- data/lib/tapyrus/message/block.rb +46 -0
- data/lib/tapyrus/message/block_transaction_request.rb +45 -0
- data/lib/tapyrus/message/block_transactions.rb +31 -0
- data/lib/tapyrus/message/block_txn.rb +27 -0
- data/lib/tapyrus/message/cmpct_block.rb +42 -0
- data/lib/tapyrus/message/error.rb +10 -0
- data/lib/tapyrus/message/fee_filter.rb +27 -0
- data/lib/tapyrus/message/filter_add.rb +28 -0
- data/lib/tapyrus/message/filter_clear.rb +17 -0
- data/lib/tapyrus/message/filter_load.rb +39 -0
- data/lib/tapyrus/message/get_addr.rb +17 -0
- data/lib/tapyrus/message/get_block_txn.rb +27 -0
- data/lib/tapyrus/message/get_blocks.rb +29 -0
- data/lib/tapyrus/message/get_data.rb +21 -0
- data/lib/tapyrus/message/get_headers.rb +28 -0
- data/lib/tapyrus/message/header_and_short_ids.rb +57 -0
- data/lib/tapyrus/message/headers.rb +35 -0
- data/lib/tapyrus/message/headers_parser.rb +24 -0
- data/lib/tapyrus/message/inv.rb +21 -0
- data/lib/tapyrus/message/inventories_parser.rb +23 -0
- data/lib/tapyrus/message/inventory.rb +51 -0
- data/lib/tapyrus/message/mem_pool.rb +17 -0
- data/lib/tapyrus/message/merkle_block.rb +42 -0
- data/lib/tapyrus/message/network_addr.rb +63 -0
- data/lib/tapyrus/message/not_found.rb +21 -0
- data/lib/tapyrus/message/ping.rb +30 -0
- data/lib/tapyrus/message/pong.rb +26 -0
- data/lib/tapyrus/message/prefilled_tx.rb +29 -0
- data/lib/tapyrus/message/reject.rb +46 -0
- data/lib/tapyrus/message/send_cmpct.rb +43 -0
- data/lib/tapyrus/message/send_headers.rb +16 -0
- data/lib/tapyrus/message/tx.rb +30 -0
- data/lib/tapyrus/message/ver_ack.rb +17 -0
- data/lib/tapyrus/message/version.rb +69 -0
- data/lib/tapyrus/message.rb +70 -0
- data/lib/tapyrus/mnemonic/wordlist/chinese_simplified.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/chinese_traditional.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/english.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/french.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/italian.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/japanese.txt +2048 -0
- data/lib/tapyrus/mnemonic/wordlist/spanish.txt +2048 -0
- data/lib/tapyrus/mnemonic.rb +77 -0
- data/lib/tapyrus/network/connection.rb +73 -0
- data/lib/tapyrus/network/message_handler.rb +241 -0
- data/lib/tapyrus/network/peer.rb +223 -0
- data/lib/tapyrus/network/peer_discovery.rb +42 -0
- data/lib/tapyrus/network/pool.rb +135 -0
- data/lib/tapyrus/network.rb +13 -0
- data/lib/tapyrus/node/cli.rb +112 -0
- data/lib/tapyrus/node/configuration.rb +38 -0
- data/lib/tapyrus/node/spv.rb +79 -0
- data/lib/tapyrus/node.rb +7 -0
- data/lib/tapyrus/opcodes.rb +178 -0
- data/lib/tapyrus/out_point.rb +44 -0
- data/lib/tapyrus/rpc/http_server.rb +65 -0
- data/lib/tapyrus/rpc/request_handler.rb +150 -0
- data/lib/tapyrus/rpc/tapyrus_core_client.rb +72 -0
- data/lib/tapyrus/rpc.rb +7 -0
- data/lib/tapyrus/script/multisig.rb +92 -0
- data/lib/tapyrus/script/script.rb +551 -0
- data/lib/tapyrus/script/script_error.rb +111 -0
- data/lib/tapyrus/script/script_interpreter.rb +668 -0
- data/lib/tapyrus/script/tx_checker.rb +81 -0
- data/lib/tapyrus/script_witness.rb +38 -0
- data/lib/tapyrus/secp256k1/native.rb +174 -0
- data/lib/tapyrus/secp256k1/ruby.rb +123 -0
- data/lib/tapyrus/secp256k1.rb +12 -0
- data/lib/tapyrus/slip39/share.rb +122 -0
- data/lib/tapyrus/slip39/sss.rb +245 -0
- data/lib/tapyrus/slip39/wordlist/english.txt +1024 -0
- data/lib/tapyrus/slip39.rb +93 -0
- data/lib/tapyrus/store/chain_entry.rb +67 -0
- data/lib/tapyrus/store/db/level_db.rb +98 -0
- data/lib/tapyrus/store/db.rb +9 -0
- data/lib/tapyrus/store/spv_chain.rb +101 -0
- data/lib/tapyrus/store.rb +9 -0
- data/lib/tapyrus/tx.rb +347 -0
- data/lib/tapyrus/tx_in.rb +89 -0
- data/lib/tapyrus/tx_out.rb +74 -0
- data/lib/tapyrus/util.rb +133 -0
- data/lib/tapyrus/validation.rb +115 -0
- data/lib/tapyrus/version.rb +3 -0
- data/lib/tapyrus/wallet/account.rb +151 -0
- data/lib/tapyrus/wallet/base.rb +162 -0
- data/lib/tapyrus/wallet/db.rb +81 -0
- data/lib/tapyrus/wallet/master_key.rb +110 -0
- data/lib/tapyrus/wallet.rb +8 -0
- data/lib/tapyrus.rb +219 -0
- data/tapyrusrb.conf.sample +0 -0
- data/tapyrusrb.gemspec +47 -0
- 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,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
|