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