bitcoinrb-grpc 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.
@@ -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
+ }