bitcoinrb-grpc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,163 @@
1
+ module Bitcoin
2
+ module Wallet
3
+ class UtxoDB
4
+ include Bitcoin::Wallet::AssetFeature
5
+
6
+ KEY_PREFIX = {
7
+ out_point: 'o', # key: out_point(tx_hash and index), value: Utxo
8
+ script: 's', # key: script_pubkey and out_point(tx_hash and index), value: Utxo
9
+ height: 'h', # key: block_height and out_point, value: Utxo
10
+ tx_hash: 't', # key: tx_hash of transaction, value: [block_height, tx_index]
11
+ block: 'b', # key: block_height and tx_index, value: tx_hash
12
+ tx_payload: 'p', # key: tx_hash, value: Tx
13
+ }
14
+
15
+ attr_reader :level_db, :logger
16
+
17
+ def initialize(path = "#{Bitcoin.base_dir}/db/utxo")
18
+ FileUtils.mkdir_p(path)
19
+ @level_db = ::LevelDB::DB.new(path)
20
+ @logger = Bitcoin::Logger.create(:debug)
21
+ end
22
+
23
+ def close
24
+ level_db.close
25
+ end
26
+
27
+ def save_tx(tx_hash, tx_payload)
28
+ level_db.batch do
29
+ # tx_hash -> [block_height, tx_index]
30
+ key = KEY_PREFIX[:tx_payload] + tx_hash
31
+ level_db.put(key, tx_payload)
32
+ end
33
+ end
34
+
35
+ def save_tx_position(tx_hash, block_height, tx_index)
36
+ logger.info("UtxoDB#save_tx:#{[tx_hash, block_height, tx_index]}")
37
+ level_db.batch do
38
+ # tx_hash -> [block_height, tx_index]
39
+ key = KEY_PREFIX[:tx_hash] + tx_hash
40
+ level_db.put(key, [block_height, tx_index].pack('N2').bth)
41
+
42
+ # block_hash and tx_index -> tx_hash
43
+ key = KEY_PREFIX[:block] + [block_height, tx_index].pack('N2').bth
44
+ level_db.put(key, tx_hash)
45
+ end
46
+ end
47
+
48
+ # @return [block_height, tx_index, tx_payload]
49
+ def get_tx(tx_hash)
50
+ key = KEY_PREFIX[:tx_hash] + tx_hash
51
+ return [] unless level_db.contains?(key)
52
+ block_height, tx_index = level_db.get(key).htb.unpack('N2')
53
+ key = KEY_PREFIX[:tx_payload] + tx_hash
54
+ tx_payload = level_db.get(key)
55
+ [block_height, tx_index, tx_payload]
56
+ end
57
+
58
+ def save_utxo(out_point, value, script_pubkey, block_height)
59
+ logger.info("UtxoDB#save_utxo:#{[out_point, value, script_pubkey, block_height]}")
60
+ level_db.batch do
61
+ utxo = Bitcoin::Grpc::Utxo.new(tx_hash: out_point.txid.rhex, index: out_point.index, block_height: block_height, value: value, script_pubkey: script_pubkey)
62
+ payload = utxo.to_proto.bth
63
+
64
+ # out_point
65
+ key = KEY_PREFIX[:out_point] + out_point.to_payload.bth
66
+ return if level_db.contains?(key)
67
+ level_db.put(key, payload)
68
+
69
+ # script_pubkey
70
+ if script_pubkey
71
+ key = KEY_PREFIX[:script] + script_pubkey + out_point.to_payload.bth
72
+ level_db.put(key, payload)
73
+ end
74
+
75
+ # block_height
76
+ key = KEY_PREFIX[:height] + [block_height].pack('N').bth + out_point.to_payload.bth
77
+ level_db.put(key, payload)
78
+ return utxo
79
+ end
80
+ end
81
+
82
+ def delete_utxo(out_point)
83
+ level_db.batch do
84
+ key = KEY_PREFIX[:out_point] + out_point.to_payload.bth
85
+ return unless level_db.contains?(key)
86
+ utxo = Bitcoin::Grpc::Utxo.decode(level_db.get(key).htb)
87
+ level_db.delete(key)
88
+
89
+ if utxo.script_pubkey
90
+ key = KEY_PREFIX[:script] + utxo.script_pubkey + out_point.to_payload.bth
91
+ level_db.delete(key)
92
+ end
93
+
94
+ key = KEY_PREFIX[:height] + [utxo.block_height].pack('N').bth + out_point.to_payload.bth
95
+ level_db.delete(key)
96
+ return utxo
97
+ end
98
+ end
99
+
100
+ def get_utxo(out_point)
101
+ level_db.batch do
102
+ key = KEY_PREFIX[:out_point] + out_point.to_payload.bth
103
+ return unless level_db.contains?(key)
104
+ return Bitcoin::Grpc::Utxo.decode(level_db.get(key).htb)
105
+ end
106
+ end
107
+
108
+ def list_unspent(current_block_height: 9999999, min: 0, max: 9999999, addresses: nil)
109
+ if addresses
110
+ list_unspent_by_addresses(current_block_height, min: min, max: max, addresses: addresses)
111
+ else
112
+ list_unspent_by_block_height(current_block_height, min: min, max: max)
113
+ end
114
+ end
115
+
116
+ def list_unspent_in_account(account, current_block_height: 9999999, min: 0, max: 9999999)
117
+ return [] unless account
118
+ script_pubkeys = account.watch_targets.map { |t| Bitcoin::Script.to_p2wpkh(t).to_payload.bth }
119
+ list_unspent_by_script_pubkeys(current_block_height, min: min, max: max, script_pubkeys: script_pubkeys)
120
+ end
121
+
122
+ def get_balance(account, current_block_height: 9999999, min: 0, max: 9999999)
123
+ list_unspent_in_account(account, current_block_height: current_block_height, min: min, max: max).sum { |u| u.value }
124
+ end
125
+
126
+ private
127
+
128
+ def utxos_between(from, to)
129
+ level_db.each(from: from, to: to).map { |k, v| Bitcoin::Grpc::Utxo.decode(v.htb) }
130
+ end
131
+
132
+ class ::Array
133
+ def with_height(min, max)
134
+ select { |u| u.block_height >= min && u.block_height <= max }
135
+ end
136
+ end
137
+
138
+ def list_unspent_by_block_height(current_block_height, min: 0, max: 9999999)
139
+ max_height = [current_block_height - min, 0].max
140
+ min_height = [current_block_height - max, 0].max
141
+
142
+ from = KEY_PREFIX[:height] + [min_height].pack('N').bth + '000000000000000000000000000000000000000000000000000000000000000000000000'
143
+ to = KEY_PREFIX[:height] + [max_height].pack('N').bth + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
144
+ utxos_between(from, to)
145
+ end
146
+
147
+ def list_unspent_by_addresses(current_block_height, min: 0, max: 9999999, addresses: [])
148
+ script_pubkeys = addresses.map { |a| Bitcoin::Script.parse_from_addr(a).to_payload.bth }
149
+ list_unspent_by_script_pubkeys(current_block_height, min: min, max: max, script_pubkeys: script_pubkeys)
150
+ end
151
+
152
+ def list_unspent_by_script_pubkeys(current_block_height, min: 0, max: 9999999, script_pubkeys: [])
153
+ max_height = current_block_height - min
154
+ min_height = current_block_height - max
155
+ script_pubkeys.map do |key|
156
+ from = KEY_PREFIX[:script] + key + '000000000000000000000000000000000000000000000000000000000000000000000000'
157
+ to = KEY_PREFIX[:script] + key + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
158
+ utxos_between(from, to).with_height(min_height, max_height)
159
+ end.flatten
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,90 @@
1
+ module Bitcoin
2
+ module Wallet
3
+ class UtxoHandler < Concurrent::Actor::Context
4
+ attr_reader :watchings, :spv, :utxo_db, :publisher
5
+
6
+ def initialize(spv, publisher)
7
+ @watchings = []
8
+ @spv = spv
9
+ @spv.add_observer(self)
10
+
11
+ @utxo_db = spv.wallet.utxo_db
12
+ @publisher = publisher
13
+ end
14
+
15
+ def update(event, data)
16
+ send(event, data)
17
+ end
18
+
19
+ def on_message(message)
20
+ case message
21
+ when Bitcoin::Grpc::WatchTxConfirmedRequest
22
+ spv.filter_add(message.tx_hash)
23
+ watchings << message
24
+ when :watchings
25
+ watchings
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def tx(data)
32
+ tx = data.tx
33
+ block_height = spv.chain.latest_block.height
34
+ watch_targets = spv.wallet.watch_targets
35
+
36
+ tx.outputs.each_with_index do |output, index|
37
+ next unless watch_targets.find { |target| output.script_pubkey == Bitcoin::Script.to_p2wpkh(target) }
38
+ out_point = Bitcoin::OutPoint.new(tx.tx_hash, index)
39
+ utxo = utxo_db.save_utxo(out_point, output.value, output.script_pubkey.to_payload.bth, block_height)
40
+ publisher << Bitcoin::Grpc::EventUtxoRegistered.new(tx_hash: tx.tx_hash, tx_payload: tx.to_payload.bth, utxo: utxo) if utxo
41
+ end
42
+
43
+ tx.inputs.each do |input|
44
+ utxo = utxo_db.delete_utxo(input.out_point)
45
+ publisher << Bitcoin::Grpc::EventUtxoSpent.new(tx_hash: tx.tx_hash, tx_payload: tx.to_payload.bth, utxo: utxo) if utxo
46
+ end
47
+
48
+ utxo_db.save_tx(tx.tx_hash, tx.to_payload.bth)
49
+
50
+ publisher << Bitcoin::Grpc::WatchAssetIdAssignedRequest.new(tx_hash: tx.tx_hash, tx_payload: tx.to_payload.bth) if tx.colored?
51
+ end
52
+
53
+ def merkleblock(data)
54
+ block_height = spv.chain.latest_block.height
55
+ tree = Bitcoin::MerkleTree.build_partial(data.tx_count, data.hashes, Bitcoin.byte_to_bit(data.flags.htb))
56
+ tx_blockhash = data.header.block_hash
57
+
58
+ log(::Logger::DEBUG, "UtxoHandler#merkleblock:#{data.hashes}")
59
+
60
+ watchings
61
+ .select { |item| item.is_a? Bitcoin::Grpc::WatchTxConfirmedRequest }
62
+ .select { |item| data.hashes.include?(item.tx_hash) }
63
+ .each do |item|
64
+ tx_index = tree.find_node(item.tx_hash).index
65
+ log(::Logger::DEBUG, "UtxoHandler#merkleblock:#{[tx_index]}")
66
+ next unless tx_index
67
+ utxo_db.save_tx_position(item.tx_hash, block_height, tx_index)
68
+ end
69
+ end
70
+
71
+ def header(data)
72
+ log(::Logger::DEBUG, "UtxoHandler#header:#{[data, watchings]}")
73
+ block_height = data[:height]
74
+ watchings.select do |item|
75
+ case item
76
+ when Bitcoin::Grpc::WatchTxConfirmedRequest
77
+ height, tx_index, tx_payload = utxo_db.get_tx(item.tx_hash)
78
+ log(::Logger::DEBUG, "UtxoHandler#header:#{[height, tx_index]}")
79
+ next unless (height || tx_index)
80
+ if block_height >= height + item.confirmations
81
+ publisher << Bitcoin::Grpc::EventTxConfirmed.new(tx_hash: item.tx_hash, tx_payload: tx_payload, block_height: height, tx_index: tx_index, confirmations: item.confirmations)
82
+ watchings.delete(item)
83
+ end
84
+ else
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,99 @@
1
+ module Bitcoin
2
+ module RPC
3
+ module RequestHandler
4
+ def listunspent(min = 0, max = 999999, addresses: nil)
5
+ height = node.chain.latest_block.height
6
+ utxos = node.wallet.list_unspent(current_block_height: height, min: min, max: max, addresses: addresses)
7
+ utxos.map do |u|
8
+ {
9
+ tx_hash: u.tx_hash,
10
+ index: u.index,
11
+ value: u.value,
12
+ script_pubkey: u.script_pubkey,
13
+ confirmations: height - u.block_height
14
+ }
15
+ end
16
+ end
17
+
18
+ def listunspentinaccount(account_name, min = 0, max = 999999)
19
+ height = node.chain.latest_block.height
20
+ utxos = node.wallet.list_unspent(account_name: account_name, current_block_height: height, min: min, max: max)
21
+ utxos.map do |u|
22
+ {
23
+ tx_hash: u.tx_hash,
24
+ index: u.index,
25
+ value: u.value,
26
+ script_pubkey: u.script_pubkey,
27
+ confirmations: height - u.block_height
28
+ }
29
+ end
30
+ end
31
+
32
+ def listuncoloredunspentinaccount(account_name, min = 0, max = 999999)
33
+ height = node.chain.latest_block.height
34
+ utxos = node.wallet.list_uncolored_unspent(account_name: account_name, current_block_height: height, min: min, max: max)
35
+ utxos.map do |u|
36
+ {
37
+ tx_hash: u.tx_hash,
38
+ index: u.index,
39
+ value: u.value,
40
+ script_pubkey: u.script_pubkey,
41
+ confirmations: height - u.block_height
42
+ }
43
+ end
44
+ end
45
+
46
+ def listcoloredunspentinaccount(account_name, asset_type = Bitcoin::Wallet::AssetFeature::AssetType::OPEN_ASSETS , asset_id = nil, min = 0, max = 999999)
47
+ height = node.chain.latest_block.height
48
+ assets = node.wallet.list_unspent_assets_in_account(asset_type, asset_id, account_name: account_name, current_block_height: height, min: min, max: max).map do |asset|
49
+ out_point = Bitcoin::OutPoint.new(asset.tx_hash, asset.index)
50
+ utxo = node.wallet.utxo_db.get_utxo(out_point)
51
+ next unless utxo
52
+ [asset, utxo]
53
+ end.compact
54
+ assets = assets.map do |(asset, utxo)|
55
+ {
56
+ tx_hash: utxo.tx_hash,
57
+ index: utxo.index,
58
+ value: utxo.value,
59
+ asset_type: asset.asset_type,
60
+ asset_id: asset.asset_id,
61
+ asset_quantity: asset.asset_quantity,
62
+ script_pubkey: utxo.script_pubkey,
63
+ confirmations: height - utxo.block_height
64
+ }
65
+ end
66
+ end
67
+
68
+ def getbalance(account_name)
69
+ node.wallet.get_balance(account_name)
70
+ end
71
+
72
+ def getassetbalance(account_name, asset_type = Bitcoin::Wallet::AssetFeature::AssetType::OPEN_ASSETS , asset_id = nil)
73
+ node.wallet.get_asset_balance(asset_type, asset_id, account_name: account_name)
74
+ end
75
+
76
+ # create new bitcoin address for receiving payments.
77
+ def getnewaddress(account_name)
78
+ address = node.wallet.generate_new_address(account_name)
79
+ script = Bitcoin::Script.parse_from_addr(address)
80
+ pubkey_hash = script.witness_data[1].bth
81
+ node.filter_add(pubkey_hash)
82
+ address
83
+ end
84
+
85
+ def createaccount(account_name)
86
+ account = node.wallet.create_account(Bitcoin::Wallet::Account::PURPOSE_TYPE[:native_segwit], account_name)
87
+ account.to_h
88
+ rescue
89
+ {}
90
+ end
91
+
92
+ def signrawtransaction(account_name, payload)
93
+ tx = Bitcoin::Tx.parse_from_payload(payload.htb)
94
+ signed_tx = Bitcoin::Wallet::Signer.sign(node, account_name, tx)
95
+ { hex: signed_tx.to_payload.bth }
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,11 @@
1
+ module Bitcoin
2
+ class Tx
3
+ def colored?
4
+ open_assets?
5
+ end
6
+
7
+ def open_assets?
8
+ !outputs.find(&:open_assets_marker?).nil?
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,50 @@
1
+ module Bitcoin
2
+ module Wallet
3
+ class Base
4
+ attr_reader :utxo_db
5
+
6
+ def get_balance(account_name)
7
+ account = find_account(account_name)
8
+ return 0 unless account
9
+ utxo_db.get_balance(account)
10
+ end
11
+
12
+ def list_unspent(account_name: nil, current_block_height: 9999999, min: 0, max: 9999999, addresses: nil)
13
+ if account_name
14
+ account = find_account(account_name)
15
+ return [] unless account
16
+ utxo_db.list_unspent_in_account(account, current_block_height: current_block_height, min: min, max: max)
17
+ else
18
+ utxo_db.list_unspent(current_block_height: current_block_height, min: min, max: max, addresses: addresses)
19
+ end
20
+ end
21
+
22
+ def list_uncolored_unspent(account_name: nil, current_block_height: 9999999, min: 0, max: 9999999)
23
+ account = find_account(account_name)
24
+ return [] unless account
25
+ utxo_db.list_uncolored_unspent_in_account(account, current_block_height: current_block_height, min: min, max: max)
26
+ end
27
+
28
+ def get_asset_balance(asset_type, asset_id, account_name: nil, current_block_height: 9999999, min: 0, max: 9999999)
29
+ account = find_account(account_name)
30
+ return 0 unless account
31
+ utxo_db.get_asset_balance(asset_type, asset_id, account, current_block_height: current_block_height, min: min, max: max)
32
+ end
33
+
34
+ def list_unspent_assets_in_account(asset_type, asset_id, account_name: nil, current_block_height: 9999999, min: 0, max: 9999999)
35
+ account = find_account(account_name)
36
+ return [] unless account
37
+ utxo_db.list_unspent_assets_in_account(asset_type, asset_id, account, current_block_height: current_block_height, min: min, max: max)
38
+ end
39
+
40
+ private
41
+
42
+ def initialize(wallet_id, path_prefix)
43
+ @path = "#{path_prefix}wallet#{wallet_id}/"
44
+ @db = Bitcoin::Wallet::DB.new(@path)
45
+ @wallet_id = wallet_id
46
+ @utxo_db = Bitcoin::Wallet::UtxoDB.new("#{path_prefix}utxo#{wallet_id}/")
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,97 @@
1
+ syntax = "proto3";
2
+
3
+ package bitcoin.grpc;
4
+
5
+
6
+ service Blockchain {
7
+ rpc WatchTxConfirmed(WatchTxConfirmedRequest) returns (stream WatchTxConfirmedResponse);
8
+ rpc WatchUtxo(WatchUtxoRequest) returns (stream WatchUtxoResponse);
9
+ rpc WatchToken(WatchTokenRequest) returns (stream WatchTokenResponse);
10
+ }
11
+
12
+ message WatchTxConfirmedRequest {
13
+ string tx_hash = 1;
14
+ uint32 confirmations = 2;
15
+ }
16
+
17
+ message WatchTxConfirmedResponse {
18
+ oneof event {
19
+ EventTxConfirmed confirmed = 1;
20
+ }
21
+ }
22
+
23
+ message WatchUtxoRequest {
24
+ }
25
+
26
+ message WatchUtxoResponse {
27
+ oneof event {
28
+ EventUtxoRegistered registered = 1;
29
+ EventUtxoSpent spent = 2;
30
+ }
31
+ }
32
+
33
+ message WatchTokenRequest {
34
+ bytes asset_type = 1;
35
+ }
36
+
37
+ message WatchTokenResponse {
38
+ oneof event {
39
+ EventTokenIssued issued = 1;
40
+ EventTokenTransfered transfered = 2;
41
+ EventTokenBurned burned = 3;
42
+ }
43
+ }
44
+
45
+ message WatchAssetIdAssignedRequest {
46
+ string tx_hash = 1;
47
+ string tx_payload = 2;
48
+ }
49
+
50
+ message EventTxConfirmed {
51
+ string tx_hash = 1;
52
+ string tx_payload = 2;
53
+ uint32 block_height = 3;
54
+ uint32 tx_index = 4;
55
+ uint32 confirmations = 5;
56
+ }
57
+
58
+ message EventUtxoRegistered {
59
+ string tx_hash = 1;
60
+ string tx_payload = 2;
61
+ Utxo utxo = 3;
62
+ }
63
+
64
+ message EventUtxoSpent {
65
+ string tx_hash = 1;
66
+ string tx_payload = 2;
67
+ Utxo utxo = 3;
68
+ }
69
+
70
+ message EventTokenIssued {
71
+ AssetOutput asset = 1;
72
+ }
73
+
74
+ message EventTokenTransfered {
75
+ AssetOutput asset = 2;
76
+ }
77
+
78
+ message EventTokenBurned {
79
+ AssetOutput asset = 3;
80
+ }
81
+
82
+ message Utxo {
83
+ string tx_hash = 1;
84
+ uint32 index = 2;
85
+ uint32 block_height = 3;
86
+ uint64 value = 4;
87
+ string script_pubkey = 5;
88
+ }
89
+
90
+ message AssetOutput {
91
+ bytes asset_type = 1;
92
+ string asset_id = 2;
93
+ uint64 asset_quantity = 3;
94
+ string tx_hash = 4;
95
+ uint32 index = 5;
96
+ uint32 block_height = 6;
97
+ }