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,26 @@
1
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ # Source: bitcoin/grpc/grpc.proto for package 'bitcoin.grpc'
3
+
4
+ require 'grpc'
5
+ require 'bitcoin/grpc/grpc_pb'
6
+
7
+ module Bitcoin
8
+ module Grpc
9
+ module Blockchain
10
+ class Service
11
+
12
+ include GRPC::GenericService
13
+
14
+ self.marshal_class_method = :encode
15
+ self.unmarshal_class_method = :decode
16
+ self.service_name = 'bitcoin.grpc.Blockchain'
17
+
18
+ rpc :WatchTxConfirmed, WatchTxConfirmedRequest, stream(WatchTxConfirmedResponse)
19
+ rpc :WatchUtxo, WatchUtxoRequest, stream(WatchUtxoResponse)
20
+ rpc :WatchToken, WatchTokenRequest, stream(WatchTokenResponse)
21
+ end
22
+
23
+ Stub = Service.rpc_stub_class
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ require 'jsonclient'
2
+
3
+ module Bitcoin
4
+ module Grpc
5
+ module OapService
6
+ def self.outputs_with_open_asset_id(tx_hash)
7
+ client = JSONClient.new
8
+ client.debug_dev = STDOUT
9
+
10
+ response = client.get("#{oae_url}#{tx_hash.rhex}?format=json")
11
+ response.body['outputs']
12
+ rescue RuntimeError => _
13
+ nil
14
+ end
15
+
16
+ def self.oae_url
17
+ case
18
+ when Bitcoin.chain_params.mainnet?
19
+ 'https://www.oaexplorer.com/tx/'
20
+ when Bitcoin.chain_params.testnet?
21
+ 'https://testnet.oaexplorer.com/tx/'
22
+ when Bitcoin.chain_params.regtest?
23
+ 'http://localhost:9292/tx/'
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,113 @@
1
+ module Bitcoin
2
+ module Grpc
3
+ class Server < Bitcoin::Grpc::Blockchain::Service
4
+ def self.run(spv)
5
+ addr = "0.0.0.0:8080"
6
+ s = GRPC::RpcServer.new
7
+ s.add_http2_port(addr, :this_port_is_insecure)
8
+ s.handle(new(spv))
9
+ s.run_till_terminated
10
+ end
11
+
12
+ attr_reader :spv, :utxo_handler, :asset_handler, :publisher, :logger
13
+
14
+ def initialize(spv)
15
+ @spv = spv
16
+ @publisher = Bitcoin::Wallet::Publisher.spawn(:publisher)
17
+ @utxo_handler = Bitcoin::Wallet::UtxoHandler.spawn(:utxo_handler, spv, publisher)
18
+ @asset_handler = Bitcoin::Wallet::AssetHandler.spawn(:asset_handler, spv, publisher)
19
+ @logger = Bitcoin::Logger.create(:debug)
20
+ end
21
+
22
+ def watch_tx_confirmed(request, call)
23
+ logger.info("watch_tx_confirmed: #{request}")
24
+ utxo_handler << request
25
+ channel = Concurrent::Channel.new
26
+ Receiver.spawn(:receiver, channel, publisher, [Bitcoin::Grpc::EventTxConfirmed])
27
+ ResponseEnum.new(request, channel, WatchTxConfirmedResponseBuilder).each
28
+ end
29
+
30
+ def watch_utxo(request, call)
31
+ logger.info("watch_utxo: #{request}")
32
+ utxo_handler << request
33
+ channel = Concurrent::Channel.new
34
+ Receiver.spawn(:receiver, channel, publisher, [Bitcoin::Grpc::EventUtxoRegistered, Bitcoin::Grpc::EventUtxoSpent])
35
+ ResponseEnum.new(request, channel, WatchUtxoResponseBuilder).each
36
+ end
37
+
38
+ def watch_token(request, call)
39
+ logger.info("watch_token: #{request}")
40
+ utxo_handler << request
41
+ channel = Concurrent::Channel.new
42
+ Receiver.spawn(:receiver, channel, publisher, [Bitcoin::Grpc::EventTokenIssued, Bitcoin::Grpc::EventTokenTransfered])
43
+ ResponseEnum.new(request, channel, WatchTokenResponseBuilder).each
44
+ end
45
+ end
46
+
47
+ class WatchTxConfirmedResponseBuilder
48
+ def self.build(event)
49
+ case event
50
+ when Bitcoin::Grpc::EventTxConfirmed
51
+ Bitcoin::Grpc::WatchTxConfirmedResponse.new(confirmed: event)
52
+ end
53
+ end
54
+ end
55
+
56
+ class WatchUtxoResponseBuilder
57
+ def self.build(event)
58
+ case event
59
+ when Bitcoin::Grpc::EventUtxoRegistered
60
+ Bitcoin::Grpc::WatchUtxoResponse.new(registered: event)
61
+ when Bitcoin::Grpc::EventUtxoSpent
62
+ Bitcoin::Grpc::WatchUtxoResponse.new(spent: event)
63
+ end
64
+ end
65
+ end
66
+
67
+ class WatchTokenResponseBuilder
68
+ def self.build(event)
69
+ case event
70
+ when Bitcoin::Grpc::EventTokenIssued
71
+ Bitcoin::Grpc::WatchTokenResponse.new(issued: event)
72
+ when Bitcoin::Grpc::EventTokenTransfered
73
+ Bitcoin::Grpc::WatchTokenResponse.new(transfered: event)
74
+ when Bitcoin::Grpc::EventTokenBurned
75
+ Bitcoin::Grpc::WatchTokenResponse.new(burned: event)
76
+ end
77
+ end
78
+ end
79
+
80
+ class Receiver < Concurrent::Actor::Context
81
+ include Concurrent::Concern::Logging
82
+
83
+ attr_reader :channel
84
+ def initialize(channel, publisher, classes)
85
+ @channel = channel
86
+ classes.each {|c| publisher << [:subscribe, c] }
87
+ end
88
+ def on_message(message)
89
+ log(::Logger::DEBUG, "Receiver#on_message:#{message}")
90
+ channel << message
91
+ end
92
+ end
93
+
94
+ class ResponseEnum
95
+ attr_reader :req, :channel, :wrapper_classs, :logger
96
+
97
+ def initialize(req, channel, wrapper_classs)
98
+ @req = req
99
+ @channel = channel
100
+ @wrapper_classs = wrapper_classs
101
+ @logger = Bitcoin::Logger.create(:debug)
102
+ end
103
+
104
+ def each
105
+ logger.info("ResponseEnum#each")
106
+ return enum_for(:each) unless block_given?
107
+ loop do
108
+ yield wrapper_classs.build(channel.take)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,5 @@
1
+ module Bitcoin
2
+ module Grpc
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,132 @@
1
+ module Bitcoin
2
+ module Wallet
3
+ module AssetFeature
4
+ module AssetType
5
+ OPEN_ASSETS = 1
6
+ end
7
+
8
+ KEY_PREFIX = {
9
+ asset_out_point: 'ao', # key: out_point, value AssetOutput
10
+ asset_script_pubkey: 'as', # key: asset_type, script_pubkey and out_point, value AssetOutput
11
+ asset_height: 'ah', # key: asset_type, block_height and out_point, value AssetOutput
12
+ }
13
+
14
+ def save_token(asset_type, asset_id, asset_quantity, utxo)
15
+ logger.info("UtxoDB#save_token:#{[asset_type, asset_id, asset_quantity, utxo.inspect]}")
16
+ level_db.batch do
17
+ asset_output = Bitcoin::Grpc::AssetOutput.new(
18
+ asset_type: [asset_type].pack('C'),
19
+ asset_id: asset_id,
20
+ asset_quantity: asset_quantity,
21
+ tx_hash: utxo.tx_hash,
22
+ index: utxo.index,
23
+ block_height: utxo.block_height
24
+ )
25
+ out_point = Bitcoin::OutPoint.new(utxo.tx_hash, utxo.index)
26
+ payload = asset_output.to_proto.bth
27
+
28
+ # out_point
29
+ key = KEY_PREFIX[:asset_out_point] + out_point.to_payload.bth
30
+ return if level_db.contains?(key)
31
+ level_db.put(key, payload)
32
+
33
+ # script_pubkey
34
+ if utxo.script_pubkey
35
+ key = KEY_PREFIX[:asset_script_pubkey] + [asset_type].pack('C').bth + utxo.script_pubkey + out_point.to_payload.bth
36
+ level_db.put(key, payload)
37
+ end
38
+
39
+ # block_height
40
+ key = KEY_PREFIX[:asset_height] + [asset_type].pack('C').bth + [utxo.block_height].pack('N').bth + out_point.to_payload.bth
41
+ level_db.put(key, payload)
42
+ return asset_output
43
+ end
44
+ end
45
+
46
+ def delete_token(utxo)
47
+ logger.info("UtxoDB#delete_token:#{utxo.inspect}")
48
+ level_db.batch do
49
+ out_point = Bitcoin::OutPoint.new(utxo.tx_hash, utxo.index)
50
+
51
+ key = KEY_PREFIX[:asset_out_point] + out_point.to_payload.bth
52
+ return unless level_db.contains?(key)
53
+ asset_output = Bitcoin::Grpc::AssetOutput.decode(level_db.get(key).htb)
54
+ level_db.delete(key)
55
+
56
+ if utxo.script_pubkey
57
+ key = KEY_PREFIX[:asset_script_pubkey] + asset_output.asset_type.bth + utxo.script_pubkey + out_point.to_payload.bth
58
+ level_db.delete(key)
59
+ end
60
+
61
+ key = KEY_PREFIX[:asset_height] + asset_output.asset_type.bth + [utxo.block_height].pack('N').bth + out_point.to_payload.bth
62
+ level_db.delete(key)
63
+ return asset_output
64
+ end
65
+ end
66
+
67
+ def list_unspent_assets(asset_type, asset_id, current_block_height: 9999999, min: 0, max: 9999999, addresses: nil)
68
+ raise NotImplementedError.new("asset_type should not be nil.") unless asset_type
69
+ raise ArgumentError.new('asset_id should not be nil') unless asset_id
70
+
71
+ if addresses
72
+ list_unspent_assets_by_addresses(asset_type, asset_id, current_block_height, min: min, max: max, addresses: addresses)
73
+ else
74
+ list_unspent_assets_by_block_height(asset_type, asset_id, current_block_height, min: min, max: max)
75
+ end
76
+ end
77
+
78
+ def list_unspent_assets_in_account(asset_type, asset_id, account, current_block_height: 9999999, min: 0 , max: 9999999)
79
+ return [] unless account
80
+ script_pubkeys = account.watch_targets.map { |t| Bitcoin::Script.to_p2wpkh(t).to_payload.bth }
81
+ list_unspent_assets_by_script_pubkeys(asset_type, asset_id, current_block_height, min: min, max: max, script_pubkeys: script_pubkeys)
82
+ end
83
+
84
+ def get_asset_balance(asset_type, asset_id, account, current_block_height: 9999999, min: 0, max: 9999999, addresses: nil)
85
+ raise NotImplementedError.new("asset_type should not be nil.") unless asset_type
86
+ raise ArgumentError.new('asset_id should not be nil') unless asset_id
87
+
88
+ list_unspent_assets_in_account(asset_type, asset_id, account, current_block_height: current_block_height, min: min, max: max).sum { |u| u.asset_quantity }
89
+ end
90
+
91
+ def list_uncolored_unspent_in_account(account, current_block_height: 9999999, min: 0, max: 9999999)
92
+ utxos = list_unspent_in_account(account, current_block_height: current_block_height, min: min, max: max)
93
+ utxos.delete_if do |utxo|
94
+ out_point = Bitcoin::OutPoint.new(utxo.tx_hash, utxo.index)
95
+ level_db.contains?(KEY_PREFIX[:asset_out_point] + out_point.to_payload.bth)
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def assets_between(from, to, asset_id)
102
+ level_db.each(from: from, to: to)
103
+ .map { |k, v| Bitcoin::Grpc::AssetOutput.decode(v.htb) }
104
+ .select {|asset| asset.asset_id == asset_id }
105
+ end
106
+
107
+ def list_unspent_assets_by_block_height(asset_type, asset_id, current_block_height, min: 0, max: 9999999)
108
+ max_height = [current_block_height - min, 0].max
109
+ min_height = [current_block_height - max, 0].max
110
+
111
+ from = KEY_PREFIX[:asset_height] + [asset_type, min_height].pack('CN').bth + '000000000000000000000000000000000000000000000000000000000000000000000000'
112
+ to = KEY_PREFIX[:asset_height] + [asset_type, max_height].pack('CN').bth + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
113
+ assets_between(from, to, asset_id)
114
+ end
115
+
116
+ def list_unspent_assets_by_addresses(asset_type, asset_id, current_block_height, min: 0, max: 9999999, addresses: [])
117
+ script_pubkeys = addresses.map { |a| Bitcoin::Script.parse_from_addr(a).to_payload.bth }
118
+ list_unspent_assets_by_script_pubkeys(asset_type, asset_id, current_block_height, min: min, max: max, script_pubkeys: script_pubkeys)
119
+ end
120
+
121
+ def list_unspent_assets_by_script_pubkeys(asset_type, asset_id, current_block_height, min: 0, max: 9999999, script_pubkeys: [])
122
+ max_height = current_block_height - min
123
+ min_height = current_block_height - max
124
+ script_pubkeys.map do |key|
125
+ from = KEY_PREFIX[:asset_script_pubkey] + [asset_type].pack('C').bth + key + '000000000000000000000000000000000000000000000000000000000000000000000000'
126
+ to = KEY_PREFIX[:asset_script_pubkey] + [asset_type].pack('C').bth + key + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
127
+ assets_between(from, to, asset_id).with_height(min_height, max_height)
128
+ end.flatten
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitcoin
4
+ module Wallet
5
+ class AssetHandler < Concurrent::Actor::RestartingContext
6
+ attr_reader :utxo_db, :publisher
7
+
8
+ def initialize(spv, publisher)
9
+ publisher << [:subscribe, Bitcoin::Grpc::EventUtxoSpent]
10
+ publisher << [:subscribe, Bitcoin::Grpc::WatchAssetIdAssignedRequest]
11
+ @publisher = publisher
12
+ @utxo_db = spv.wallet.utxo_db
13
+ @logger = Bitcoin::Logger.create(:debug)
14
+ end
15
+
16
+ def on_message(message)
17
+ case message
18
+ when Bitcoin::Grpc::EventUtxoSpent
19
+ tx = Bitcoin::Tx.parse_from_payload(message.tx_payload.htb)
20
+ log(::Logger::DEBUG, "tx=#{tx}, open_assets?=#{tx.open_assets?}")
21
+ utxo_db.delete_token(message.utxo) if tx.open_assets?
22
+ when Bitcoin::Grpc::WatchAssetIdAssignedRequest
23
+ tx = Bitcoin::Tx.parse_from_payload(message.tx_payload.htb)
24
+ log(::Logger::DEBUG, "tx=#{tx}, open_assets?=#{tx.open_assets?}")
25
+ case
26
+ when tx.open_assets?
27
+ outputs = Bitcoin::Grpc::OapService.outputs_with_open_asset_id(message.tx_hash)
28
+ if outputs
29
+ outputs.each do |output|
30
+ asset_id = output['asset_id']
31
+ asset_quantity = output['asset_quantity']
32
+ oa_output_type = output['oa_output_type']
33
+ next unless asset_id
34
+ out_point = Bitcoin::OutPoint.new(tx.tx_hash, output['n'])
35
+ utxo = utxo_db.get_utxo(out_point)
36
+ next unless utxo
37
+ asset_output = utxo_db.save_token(AssetFeature::AssetType::OPEN_ASSETS, asset_id, asset_quantity, utxo)
38
+ next unless asset_output
39
+ if oa_output_type == 'issuance'
40
+ publisher << Bitcoin::Grpc::EventTokenIssued.new(asset: asset_output)
41
+ else
42
+ publisher << Bitcoin::Grpc::EventTokenTransfered.new(asset: asset_output)
43
+ end
44
+ end
45
+ else
46
+ task = Concurrent::TimerTask.new(execution_interval: 60) do
47
+ self << message
48
+ task.shutdown
49
+ end
50
+ task.execute
51
+ end
52
+ else
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitcoin
4
+ module Wallet
5
+ class Publisher < Concurrent::Actor::RestartingContext
6
+ attr_reader :receivers
7
+
8
+ def initialize
9
+ @receivers = {}
10
+ end
11
+
12
+ def on_message(message)
13
+ case message
14
+ when :unsubscribe
15
+ receivers.each { |receiver| receiver.delete(envelope.sender) }
16
+ when Array
17
+ if message[0] == :subscribe
18
+ if envelope.sender.is_a? Concurrent::Actor::Reference
19
+ receivers[message[1].name] ||= []
20
+ receivers[message[1].name] << envelope.sender
21
+ end
22
+ elsif message[0] == :subscribe?
23
+ receivers[message[1].name]&.include?(envelope.sender)
24
+ else
25
+ end
26
+ else
27
+ receivers[message&.class&.name]&.each { |r| r << message }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bitcoin
4
+ module Wallet
5
+ module Signer
6
+ def self.sign(node, account_name, tx)
7
+ account = find_account(node, account_name)
8
+ return unless account
9
+
10
+ tx.inputs.each.with_index do |input, index|
11
+ spec = input_spec(node, account, tx, index)
12
+ next unless spec
13
+ sign_tx_for_p2wpkh(tx, index, spec[0], spec[1])
14
+ end
15
+ tx
16
+ end
17
+
18
+ private
19
+
20
+ def self.input_spec(node, account, tx, index)
21
+ input = tx.inputs[index]
22
+ return unless input
23
+ utxo = node.wallet.utxo_db.get_utxo(input.out_point)
24
+ return unless utxo
25
+ script_pubkey = utxo.script_pubkey
26
+ keys = account.watch_targets
27
+ key = nil
28
+ (0..account.receive_depth + 1).reverse_each do |key_index|
29
+ path = [account.path, 0, key_index].join('/')
30
+ temp_key = node.wallet.master_key.derive(path).key
31
+ if to_p2wpkh(temp_key).to_payload.bth == script_pubkey
32
+ key = temp_key
33
+ break
34
+ end
35
+ end
36
+ unless key
37
+ (0..account.change_depth + 1).reverse_each do |key_index|
38
+ path = [account.path, 1, key_index].join('/')
39
+ temp_key = node.wallet.master_key.derive(path).key
40
+ if to_p2wpkh(temp_key).to_payload.bth == script_pubkey
41
+ key = temp_key
42
+ break
43
+ end
44
+ end
45
+ end
46
+ return unless key
47
+ [key, utxo.value]
48
+ end
49
+
50
+ def self.find_account(node, account_name)
51
+ node.wallet.accounts.find{|a| a.name == account_name}
52
+ end
53
+
54
+ def self.sign_tx_for_p2wpkh(tx, index, key, amount)
55
+ sig = to_sighash(tx, index, key, amount)
56
+ tx.inputs[index].script_witness = Bitcoin::ScriptWitness.new.tap do |witness|
57
+ witness.stack << sig << key.pubkey.htb
58
+ end
59
+ end
60
+
61
+ def self.to_sighash(tx, index, key, amount)
62
+ sighash = tx.sighash_for_input(index, to_p2wpkh(key), amount: amount, sig_version: :witness_v0)
63
+ key.sign(sighash) + [Bitcoin::SIGHASH_TYPE[:all]].pack('C')
64
+ end
65
+
66
+ def self.to_p2wpkh(key)
67
+ Bitcoin::Script.to_p2wpkh(Bitcoin.hash160(key.pubkey))
68
+ end
69
+ end
70
+ end
71
+ end