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