glueby 0.4.0 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,50 +1,51 @@
1
- # frozen_string_literal: true
2
-
3
- module Glueby
4
- module Internal
5
- class Wallet
6
- module AR
7
- class Utxo < ::ActiveRecord::Base
8
- belongs_to :key
9
-
10
- validates :txid, uniqueness: { scope: :index, case_sensitive: false }
11
-
12
- enum status: { init: 0, broadcasted: 1, finalized: 2 }
13
-
14
- def color_id
15
- script = Tapyrus::Script.parse_from_payload(script_pubkey.htb)
16
- script.color_id&.to_hex
17
- end
18
-
19
- # Delete utxo spent by specified tx.
20
- #
21
- # @param [Tapyrus::Tx] the spending tx
22
- def self.destroy_for_inputs(tx)
23
- tx.inputs.each do |input|
24
- Utxo.destroy_by(txid: input.out_point.txid, index: input.out_point.index)
25
- end
26
- end
27
-
28
- # Create utxo or update utxo for tx outputs
29
- # if there is no key for script pubkey in an output, utxo for the output is not created.
30
- #
31
- # @param [Tapyrus::Tx] tx
32
- def self.create_or_update_for_outputs(tx, status: :finalized)
33
- tx.outputs.each.with_index do |output, index|
34
- key = Key.key_for_output(output)
35
- next unless key
36
-
37
- utxo = Utxo.find_or_initialize_by(txid: tx.txid, index: index)
38
- utxo.update!(
39
- script_pubkey: output.script_pubkey.to_hex,
40
- value: output.value,
41
- status: status,
42
- key: key
43
- )
44
- end
45
- end
46
- end
47
- end
48
- end
49
- end
50
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Glueby
4
+ module Internal
5
+ class Wallet
6
+ module AR
7
+ class Utxo < ::ActiveRecord::Base
8
+ belongs_to :key
9
+
10
+ validates :txid, uniqueness: { scope: :index, case_sensitive: false }
11
+
12
+ enum status: { init: 0, broadcasted: 1, finalized: 2 }
13
+
14
+ def color_id
15
+ script = Tapyrus::Script.parse_from_payload(script_pubkey.htb)
16
+ script.color_id&.to_hex
17
+ end
18
+
19
+ # Delete utxo spent by specified tx.
20
+ #
21
+ # @param [Tapyrus::Tx] the spending tx
22
+ def self.destroy_for_inputs(tx)
23
+ tx.inputs.each do |input|
24
+ Utxo.destroy_by(txid: input.out_point.txid, index: input.out_point.index)
25
+ end
26
+ end
27
+
28
+ # Create utxo or update utxo for tx outputs
29
+ # if there is no key for script pubkey in an output, utxo for the output is not created.
30
+ #
31
+ # @param [Tapyrus::Tx] tx
32
+ def self.create_or_update_for_outputs(tx, status: :finalized)
33
+ tx.outputs.each.with_index do |output, index|
34
+ key = Key.key_for_output(output)
35
+ next unless key
36
+
37
+ utxo = Utxo.find_or_initialize_by(txid: tx.txid, index: index)
38
+ utxo.update!(
39
+ label: key.label,
40
+ script_pubkey: output.script_pubkey.to_hex,
41
+ value: output.value,
42
+ status: status,
43
+ key: key
44
+ )
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ module Glueby
2
+ module Internal
3
+ class Wallet
4
+ class ActiveRecordWalletAdapter
5
+ class Syncer
6
+ def tx_sync(tx)
7
+ Glueby::Internal::Wallet::AR::Utxo.destroy_for_inputs(tx)
8
+ Glueby::Internal::Wallet::AR::Utxo.create_or_update_for_outputs(tx)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,143 +1,151 @@
1
- # frozen_string_literal: true
2
-
3
- require 'securerandom'
4
-
5
- module Glueby
6
- module Internal
7
- class Wallet
8
- # ActiveRecordWalletAdapter
9
- #
10
- # This class represents a wallet adapter that use Active Record to manage wallet and utxos.
11
- # To use this wallet adapter, you should do the following steps:
12
- #
13
- # (1) Generate migrations for wallets, keys, utxos tables.
14
- # The generator `glueby:contract:wallet_adapter` is available for migration.
15
- # ```
16
- # rails g glueby:contract:wallet_adapter
17
- # ```
18
- # this generator generates 3 files to create tables used by ActiveRecordWalletAdatper.
19
- # then, migrate them
20
- # ```
21
- # $ rails db:migrate
22
- # ```
23
- #
24
- # (2) Add configuration for activerecord
25
- # ```
26
- # config = {adapter: 'activerecord', schema: 'http', host: '127.0.0.1', port: 12381, user: 'user', password: 'pass'}
27
- # Glueby::Wallet.configure(config)
28
- # ```
29
- #
30
- # (3) Generate wallet and receive address
31
- # ```
32
- # alice_wallet = Glueby::Wallet.create
33
- # address = alice_wallet.internal_wallet.receive_address
34
- # ```
35
- # `Glueby::Internal::Wallet#receive_address` returns Base58 encoded Tapyrus address like '1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a'.
36
- #
37
- # (4) Send TPC to created address and import into wallet.
38
- # In general, ActiveRecordWalletAdapter handle only utxos generated by glueby.
39
- # So, to start to use wallet, some external utxo should be imported into the wallet first.
40
- # This step should be done by external transaction without Glueby::Wallet, such as 'sendtoaddress' or 'generatetoaddress' RPC command of Tapyrus Core
41
- # ```
42
- # $ tapyrus-cli sendtoaddress 1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a 1
43
- # 1740af9f65ffd8537bdd06ccfa911bf1f4d6833222807d29c99d72b47838917d
44
- # ```
45
- #
46
- # then, import into wallet by rake task `glueby:contract:wallet_adapter:import_tx` or `glueby:contract:wallet_adapter:import_block`
47
- # ```
48
- # $ rails "glueby:contract:wallet_adapter:import_tx[1740af9f65ffd8537bdd06ccfa911bf1f4d6833222807d29c99d72b47838917d]""
49
- # ```
50
- #
51
- # (5) You are ready to use ActiveRecordWalletAdatper, check `Glueby::Internal::Wallet#list_unspent` or `Glueby::Wallet#balances`
52
- # ```
53
- # alice_wallet = Glueby::Wallet.create
54
- # alice_wallet.balances
55
- # ```
56
- class ActiveRecordWalletAdapter < AbstractWalletAdapter
57
- def create_wallet(wallet_id = nil)
58
- wallet_id = SecureRandom.hex(16) unless wallet_id
59
- begin
60
- AR::Wallet.create!(wallet_id: wallet_id)
61
- rescue ActiveRecord::RecordInvalid => _
62
- raise Errors::WalletAlreadyCreated, "wallet_id '#{wallet_id}' is already exists"
63
- end
64
- wallet_id
65
- end
66
-
67
- def delete_wallet(wallet_id)
68
- AR::Wallet.destroy_by(wallet_id: wallet_id)
69
- end
70
-
71
- def load_wallet(wallet_id)
72
- raise Errors::WalletNotFound, "Wallet #{wallet_id} does not found" unless AR::Wallet.where(wallet_id: wallet_id).exists?
73
- end
74
-
75
- def unload_wallet(wallet_id)
76
- end
77
-
78
- def wallets
79
- AR::Wallet.all.map(&:wallet_id).sort
80
- end
81
-
82
- def balance(wallet_id, only_finalized = true)
83
- wallet = AR::Wallet.find_by(wallet_id: wallet_id)
84
- utxos = wallet.utxos
85
- utxos = utxos.where(status: :finalized) if only_finalized
86
- utxos.sum(&:value)
87
- end
88
-
89
- def list_unspent(wallet_id, only_finalized = true)
90
- wallet = AR::Wallet.find_by(wallet_id: wallet_id)
91
- utxos = wallet.utxos
92
- utxos = utxos.where(status: :finalized) if only_finalized
93
- utxos.map do |utxo|
94
- {
95
- txid: utxo.txid,
96
- vout: utxo.index,
97
- script_pubkey: utxo.script_pubkey,
98
- color_id: utxo.color_id,
99
- amount: utxo.value,
100
- finalized: utxo.status == 'finalized'
101
- }
102
- end
103
- end
104
-
105
- def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
106
- wallet = AR::Wallet.find_by(wallet_id: wallet_id)
107
- wallet.sign(tx, prevtxs, sighashtype: sighashtype)
108
- end
109
-
110
- def broadcast(wallet_id, tx)
111
- ::ActiveRecord::Base.transaction do
112
- AR::Utxo.destroy_for_inputs(tx)
113
- AR::Utxo.create_or_update_for_outputs(tx, status: :broadcasted)
114
- Glueby::Internal::RPC.client.sendrawtransaction(tx.to_hex)
115
- end
116
- end
117
-
118
- def receive_address(wallet_id)
119
- wallet = AR::Wallet.find_by(wallet_id: wallet_id)
120
- key = wallet.keys.create(purpose: :receive)
121
- key.address
122
- end
123
-
124
- def change_address(wallet_id)
125
- wallet = AR::Wallet.find_by(wallet_id: wallet_id)
126
- key = wallet.keys.create(purpose: :change)
127
- key.address
128
- end
129
-
130
- def create_pubkey(wallet_id)
131
- wallet = AR::Wallet.find_by(wallet_id: wallet_id)
132
- key = wallet.keys.create(purpose: :receive)
133
- Tapyrus::Key.new(pubkey: key.public_key)
134
- end
135
-
136
- def get_addresses(wallet_id)
137
- wallet = AR::Wallet.find_by(wallet_id: wallet_id)
138
- wallet.keys.map(&:address)
139
- end
140
- end
141
- end
142
- end
143
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Glueby
6
+ module Internal
7
+ class Wallet
8
+ # ActiveRecordWalletAdapter
9
+ #
10
+ # This class represents a wallet adapter that use Active Record to manage wallet and utxos.
11
+ # To use this wallet adapter, you should do the following steps:
12
+ #
13
+ # (1) Generate migrations for wallets, keys, utxos tables.
14
+ # The generator `glueby:contract:wallet_adapter` is available for migration.
15
+ # ```
16
+ # rails g glueby:contract:wallet_adapter
17
+ # ```
18
+ # this generator generates 3 files to create tables used by ActiveRecordWalletAdatper.
19
+ # then, migrate them
20
+ # ```
21
+ # $ rails db:migrate
22
+ # ```
23
+ #
24
+ # (2) Add configuration for activerecord
25
+ # ```
26
+ # config = {adapter: 'activerecord', schema: 'http', host: '127.0.0.1', port: 12381, user: 'user', password: 'pass'}
27
+ # Glueby::Wallet.configure(config)
28
+ # ```
29
+ #
30
+ # (3) Generate wallet and receive address
31
+ # ```
32
+ # alice_wallet = Glueby::Wallet.create
33
+ # address = alice_wallet.internal_wallet.receive_address
34
+ # ```
35
+ # `Glueby::Internal::Wallet#receive_address` returns Base58 encoded Tapyrus address like '1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a'.
36
+ #
37
+ # (4) Send TPC to created address and import into wallet.
38
+ # In general, ActiveRecordWalletAdapter handle only utxos generated by glueby.
39
+ # So, to start to use wallet, some external utxo should be imported into the wallet first.
40
+ # This step should be done by external transaction without Glueby::Wallet, such as 'sendtoaddress' or 'generatetoaddress' RPC command of Tapyrus Core
41
+ # ```
42
+ # $ tapyrus-cli sendtoaddress 1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a 1
43
+ # 1740af9f65ffd8537bdd06ccfa911bf1f4d6833222807d29c99d72b47838917d
44
+ # ```
45
+ #
46
+ # then, import into wallet by rake task `glueby:contract:wallet_adapter:import_tx` or `glueby:contract:wallet_adapter:import_block`
47
+ # ```
48
+ # $ rails "glueby:contract:wallet_adapter:import_tx[1740af9f65ffd8537bdd06ccfa911bf1f4d6833222807d29c99d72b47838917d]""
49
+ # ```
50
+ #
51
+ # (5) You are ready to use ActiveRecordWalletAdatper, check `Glueby::Internal::Wallet#list_unspent` or `Glueby::Wallet#balances`
52
+ # ```
53
+ # alice_wallet = Glueby::Wallet.create
54
+ # alice_wallet.balances
55
+ # ```
56
+ class ActiveRecordWalletAdapter < AbstractWalletAdapter
57
+
58
+ autoload :Syncer, 'glueby/internal/wallet/active_record_wallet_adapter/syncer'
59
+
60
+ def create_wallet(wallet_id = nil)
61
+ wallet_id = SecureRandom.hex(16) unless wallet_id
62
+ begin
63
+ AR::Wallet.create!(wallet_id: wallet_id)
64
+ rescue ActiveRecord::RecordInvalid => _
65
+ raise Errors::WalletAlreadyCreated, "wallet_id '#{wallet_id}' is already exists"
66
+ end
67
+ wallet_id
68
+ end
69
+
70
+ def delete_wallet(wallet_id)
71
+ AR::Wallet.destroy_by(wallet_id: wallet_id)
72
+ end
73
+
74
+ def load_wallet(wallet_id)
75
+ raise Errors::WalletNotFound, "Wallet #{wallet_id} does not found" unless AR::Wallet.where(wallet_id: wallet_id).exists?
76
+ end
77
+
78
+ def unload_wallet(wallet_id)
79
+ end
80
+
81
+ def wallets
82
+ AR::Wallet.all.map(&:wallet_id).sort
83
+ end
84
+
85
+ def balance(wallet_id, only_finalized = true)
86
+ wallet = AR::Wallet.find_by(wallet_id: wallet_id)
87
+ utxos = wallet.utxos
88
+ utxos = utxos.where(status: :finalized) if only_finalized
89
+ utxos.sum(&:value)
90
+ end
91
+
92
+ def list_unspent(wallet_id, only_finalized = true, label = nil)
93
+ wallet = AR::Wallet.find_by(wallet_id: wallet_id)
94
+ utxos = wallet.utxos
95
+ utxos = utxos.where(status: :finalized) if only_finalized
96
+ utxos = utxos.where(label: label) if label && (label != :unlabeled)
97
+ utxos = utxos.where(label: nil) if label == :unlabeled
98
+ utxos.map do |utxo|
99
+ {
100
+ txid: utxo.txid,
101
+ vout: utxo.index,
102
+ script_pubkey: utxo.script_pubkey,
103
+ color_id: utxo.color_id,
104
+ amount: utxo.value,
105
+ finalized: utxo.status == 'finalized'
106
+ }
107
+ end
108
+ end
109
+
110
+ def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
111
+ wallet = AR::Wallet.find_by(wallet_id: wallet_id)
112
+ wallet.sign(tx, prevtxs, sighashtype: sighashtype)
113
+ end
114
+
115
+ def broadcast(wallet_id, tx, &block)
116
+ ::ActiveRecord::Base.transaction do
117
+ AR::Utxo.destroy_for_inputs(tx)
118
+ AR::Utxo.create_or_update_for_outputs(tx, status: :broadcasted)
119
+ block.call(tx) if block
120
+ Glueby::Internal::RPC.client.sendrawtransaction(tx.to_hex)
121
+ end
122
+ end
123
+
124
+ def receive_address(wallet_id, label = nil)
125
+ wallet = AR::Wallet.find_by(wallet_id: wallet_id)
126
+ key = wallet.keys.create(purpose: :receive, label: label)
127
+ key.address
128
+ end
129
+
130
+ def change_address(wallet_id)
131
+ wallet = AR::Wallet.find_by(wallet_id: wallet_id)
132
+ key = wallet.keys.create(purpose: :change)
133
+ key.address
134
+ end
135
+
136
+ def create_pubkey(wallet_id)
137
+ wallet = AR::Wallet.find_by(wallet_id: wallet_id)
138
+ key = wallet.keys.create(purpose: :receive)
139
+ Tapyrus::Key.new(pubkey: key.public_key)
140
+ end
141
+
142
+ def get_addresses(wallet_id, label = nil)
143
+ wallet = AR::Wallet.find_by(wallet_id: wallet_id)
144
+ keys = wallet.keys
145
+ keys = keys.where(label: label) if label
146
+ keys.map(&:address)
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -1,183 +1,186 @@
1
- # frozen_string_literal: true
2
-
3
- require 'securerandom'
4
- require 'bigdecimal'
5
-
6
- module Glueby
7
- module Internal
8
- class Wallet
9
- class TapyrusCoreWalletAdapter < AbstractWalletAdapter
10
- module Util
11
- module_function
12
-
13
- # Convert TPC to tapyrus. 1 TPC is 10**8 tapyrus.
14
- # @param [String] tpc
15
- # @return [Integer] tapyrus
16
- #
17
- # Example) "0.00000010" to 10.
18
- def tpc_to_tapyrus(tpc)
19
- (BigDecimal(tpc) * 10**8).to_i
20
- end
21
- end
22
-
23
- include Glueby::Internal::Wallet::TapyrusCoreWalletAdapter::Util
24
-
25
- WALLET_PREFIX = 'wallet-'
26
- ADDRESS_TYPE = 'legacy'
27
-
28
- RPC_WALLET_ERROR_ERROR_CODE = -4 # Unspecified problem with wallet (key not found etc.)
29
- RPC_WALLET_NOT_FOUND_ERROR_CODE = -18 # Invalid wallet specified
30
-
31
- def create_wallet(wallet_id = nil)
32
- wallet_id = SecureRandom.hex(16) unless wallet_id
33
- begin
34
- RPC.client.createwallet(wallet_name(wallet_id))
35
- rescue Tapyrus::RPC::Error => ex
36
- if ex.rpc_error['code'] == RPC_WALLET_ERROR_ERROR_CODE && /Wallet wallet-wallet already exists\./ =~ ex.rpc_error['message']
37
- raise Errors::WalletAlreadyCreated, "Wallet #{wallet_id} has been already created."
38
- else
39
- raise ex
40
- end
41
- end
42
-
43
- wallet_id
44
- end
45
-
46
- # On TapyrusCoreWallet, there are no way to delete real wallet via RPC.
47
- # So, here just unload.
48
- def delete_wallet(wallet_id)
49
- unload_wallet(wallet_id)
50
- end
51
-
52
- def load_wallet(wallet_id)
53
- RPC.client.loadwallet(wallet_name(wallet_id))
54
- rescue Tapyrus::RPC::Error => ex
55
- if ex.rpc_error['code'] == RPC_WALLET_ERROR_ERROR_CODE && /Duplicate -wallet filename specified/ =~ ex.rpc_error['message']
56
- raise Errors::WalletAlreadyLoaded, "Wallet #{wallet_id} has been already loaded."
57
- elsif ex.rpc_error['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
58
- raise Errors::WalletNotFound, "Wallet #{wallet_id} does not found"
59
- else
60
- raise ex
61
- end
62
- end
63
-
64
- def unload_wallet(wallet_id)
65
- RPC.client.unloadwallet(wallet_name(wallet_id))
66
- end
67
-
68
- def wallets
69
- RPC.client.listwallets.map do |wallet_name|
70
- match = /\A#{WALLET_PREFIX}(?<wallet_id>[0-9A-Fa-f]{32})\z/.match(wallet_name)
71
- next unless match
72
-
73
- match[:wallet_id]
74
- end.compact
75
- end
76
-
77
- def balance(wallet_id, only_finalized = true)
78
- perform_as(wallet_id) do |client|
79
- confirmed = tpc_to_tapyrus(client.getbalance)
80
- return confirmed if only_finalized
81
-
82
- confirmed + tpc_to_tapyrus(client.getunconfirmedbalance)
83
- end
84
- end
85
-
86
- def list_unspent(wallet_id, only_finalized = true)
87
- perform_as(wallet_id) do |client|
88
- min_conf = only_finalized ? 1 : 0
89
- res = client.listunspent(min_conf)
90
-
91
- res.map do |i|
92
- script = Tapyrus::Script.parse_from_payload(i['scriptPubKey'].htb)
93
- color_id = if script.cp2pkh? || script.cp2sh?
94
- Tapyrus::Color::ColorIdentifier.parse_from_payload(script.chunks[0].pushed_data).to_hex
95
- end
96
- {
97
- txid: i['txid'],
98
- vout: i['vout'],
99
- script_pubkey: i['scriptPubKey'],
100
- color_id: color_id,
101
- amount: tpc_to_tapyrus(i['amount']),
102
- finalized: i['confirmations'] != 0
103
- }
104
- end
105
- end
106
- end
107
-
108
- def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
109
- perform_as(wallet_id) do |client|
110
- res = client.signrawtransactionwithwallet(tx.to_hex, prevtxs, encode_sighashtype(sighashtype))
111
- if res['complete']
112
- Tapyrus::Tx.parse_from_payload(res['hex'].htb)
113
- else
114
- raise res['errors'].to_json
115
- end
116
- end
117
- end
118
-
119
- def broadcast(wallet_id, tx)
120
- perform_as(wallet_id) do |client|
121
- client.sendrawtransaction(tx.to_hex)
122
- end
123
- end
124
-
125
- def receive_address(wallet_id)
126
- perform_as(wallet_id) do |client|
127
- client.getnewaddress('', ADDRESS_TYPE)
128
- end
129
- end
130
-
131
- def change_address(wallet_id)
132
- perform_as(wallet_id) do |client|
133
- client.getrawchangeaddress(ADDRESS_TYPE)
134
- end
135
- end
136
-
137
- def create_pubkey(wallet_id)
138
- perform_as(wallet_id) do |client|
139
- address = client.getnewaddress('', ADDRESS_TYPE)
140
- info = client.getaddressinfo(address)
141
- Tapyrus::Key.new(pubkey: info['pubkey'])
142
- end
143
- end
144
-
145
- private
146
-
147
- def perform_as(wallet_id)
148
- RPC.perform_as(wallet_name(wallet_id)) do |client|
149
- begin
150
- yield(client)
151
- rescue Tapyrus::RPC::Error => ex
152
- if ex.rpc_error['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
153
- raise Errors::WalletUnloaded, "The wallet #{wallet_id} is unloaded. You should load before use it."
154
- else
155
- raise ex
156
- end
157
- end
158
- end
159
- end
160
-
161
- def wallet_name(wallet_id)
162
- "#{WALLET_PREFIX}#{wallet_id}"
163
- end
164
-
165
- def encode_sighashtype(sighashtype)
166
- type = case sighashtype & (~(Tapyrus::SIGHASH_TYPE[:anyonecanpay]))
167
- when Tapyrus::SIGHASH_TYPE[:all] then 'ALL'
168
- when Tapyrus::SIGHASH_TYPE[:none] then 'NONE'
169
- when Tapyrus::SIGHASH_TYPE[:single] then 'SIGNLE'
170
- else
171
- raise Errors::InvalidSighashType, "Invalid sighash type '#{sighashtype}'"
172
- end
173
-
174
- if sighashtype & Tapyrus::SIGHASH_TYPE[:anyonecanpay] == 0x80
175
- type += '|ANYONECANPAY'
176
- end
177
-
178
- type
179
- end
180
- end
181
- end
182
- end
183
- end
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'bigdecimal'
5
+
6
+ module Glueby
7
+ module Internal
8
+ class Wallet
9
+ class TapyrusCoreWalletAdapter < AbstractWalletAdapter
10
+ module Util
11
+ module_function
12
+
13
+ # Convert TPC to tapyrus. 1 TPC is 10**8 tapyrus.
14
+ # @param [String] tpc
15
+ # @return [Integer] tapyrus
16
+ #
17
+ # Example) "0.00000010" to 10.
18
+ def tpc_to_tapyrus(tpc)
19
+ (BigDecimal(tpc) * 10**8).to_i
20
+ end
21
+ end
22
+
23
+ include Glueby::Internal::Wallet::TapyrusCoreWalletAdapter::Util
24
+
25
+ WALLET_PREFIX = 'wallet-'
26
+
27
+ RPC_WALLET_ERROR_ERROR_CODE = -4 # Unspecified problem with wallet (key not found etc.)
28
+ RPC_WALLET_NOT_FOUND_ERROR_CODE = -18 # Invalid wallet specified
29
+
30
+ def create_wallet(wallet_id = nil)
31
+ wallet_id = SecureRandom.hex(16) unless wallet_id
32
+ begin
33
+ RPC.client.createwallet(wallet_name(wallet_id))
34
+ rescue Tapyrus::RPC::Error => ex
35
+ if ex.rpc_error && ex.rpc_error['code'] == RPC_WALLET_ERROR_ERROR_CODE && /Wallet wallet-#{wallet_id} already exists\./ =~ ex.rpc_error['message']
36
+ raise Errors::WalletAlreadyCreated, "Wallet #{wallet_id} has been already created."
37
+ else
38
+ raise ex
39
+ end
40
+ end
41
+
42
+ wallet_id
43
+ end
44
+
45
+ # On TapyrusCoreWallet, there are no way to delete real wallet via RPC.
46
+ # So, here just unload.
47
+ def delete_wallet(wallet_id)
48
+ unload_wallet(wallet_id)
49
+ end
50
+
51
+ def load_wallet(wallet_id)
52
+ RPC.client.loadwallet(wallet_name(wallet_id))
53
+ rescue Tapyrus::RPC::Error => ex
54
+ if ex.rpc_error && ex.rpc_error['code'] == RPC_WALLET_ERROR_ERROR_CODE && /Duplicate -wallet filename specified/ =~ ex.rpc_error['message']
55
+ raise Errors::WalletAlreadyLoaded, "Wallet #{wallet_id} has been already loaded."
56
+ elsif ex.rpc_error && ex.rpc_error['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
57
+ raise Errors::WalletNotFound, "Wallet #{wallet_id} does not found"
58
+ else
59
+ raise ex
60
+ end
61
+ end
62
+
63
+ def unload_wallet(wallet_id)
64
+ RPC.client.unloadwallet(wallet_name(wallet_id))
65
+ end
66
+
67
+ def wallets
68
+ RPC.client.listwallets.map do |wallet_name|
69
+ match = /\A#{WALLET_PREFIX}(?<wallet_id>[0-9A-Fa-f]{32})\z/.match(wallet_name)
70
+ next unless match
71
+
72
+ match[:wallet_id]
73
+ end.compact
74
+ end
75
+
76
+ def balance(wallet_id, only_finalized = true)
77
+ perform_as(wallet_id) do |client|
78
+ confirmed = tpc_to_tapyrus(client.getbalance)
79
+ return confirmed if only_finalized
80
+
81
+ confirmed + tpc_to_tapyrus(client.getunconfirmedbalance)
82
+ end
83
+ end
84
+
85
+ def list_unspent(wallet_id, only_finalized = true, label = nil)
86
+ perform_as(wallet_id) do |client|
87
+ min_conf = only_finalized ? 1 : 0
88
+ res = client.listunspent(min_conf)
89
+
90
+ res = res.filter { |i| i['label'] == label } if label && (label != :unlabeled)
91
+ res = res.filter { |i| i['label'] == "" } if label == :unlabeled
92
+
93
+ res.map do |i|
94
+ script = Tapyrus::Script.parse_from_payload(i['scriptPubKey'].htb)
95
+ color_id = if script.cp2pkh? || script.cp2sh?
96
+ Tapyrus::Color::ColorIdentifier.parse_from_payload(script.chunks[0].pushed_data).to_hex
97
+ end
98
+ {
99
+ txid: i['txid'],
100
+ vout: i['vout'],
101
+ script_pubkey: i['scriptPubKey'],
102
+ color_id: color_id,
103
+ amount: tpc_to_tapyrus(i['amount']),
104
+ finalized: i['confirmations'] != 0
105
+ }
106
+ end
107
+ end
108
+ end
109
+
110
+ def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
111
+ perform_as(wallet_id) do |client|
112
+ res = client.signrawtransactionwithwallet(tx.to_hex, prevtxs, encode_sighashtype(sighashtype))
113
+ if res['complete']
114
+ Tapyrus::Tx.parse_from_payload(res['hex'].htb)
115
+ else
116
+ raise res['errors'].to_json
117
+ end
118
+ end
119
+ end
120
+
121
+ def broadcast(wallet_id, tx, &block)
122
+ perform_as(wallet_id) do |client|
123
+ block.call(tx) if block
124
+ client.sendrawtransaction(tx.to_hex)
125
+ end
126
+ end
127
+
128
+ def receive_address(wallet_id, label = nil)
129
+ perform_as(wallet_id) do |client|
130
+ client.getnewaddress(label || '')
131
+ end
132
+ end
133
+
134
+ def change_address(wallet_id)
135
+ perform_as(wallet_id) do |client|
136
+ client.getrawchangeaddress
137
+ end
138
+ end
139
+
140
+ def create_pubkey(wallet_id)
141
+ perform_as(wallet_id) do |client|
142
+ address = client.getnewaddress('')
143
+ info = client.getaddressinfo(address)
144
+ Tapyrus::Key.new(pubkey: info['pubkey'])
145
+ end
146
+ end
147
+
148
+ private
149
+
150
+ def perform_as(wallet_id)
151
+ RPC.perform_as(wallet_name(wallet_id)) do |client|
152
+ begin
153
+ yield(client)
154
+ rescue Tapyrus::RPC::Error => ex
155
+ if ex.rpc_error && ex.rpc_error['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
156
+ raise Errors::WalletUnloaded, "The wallet #{wallet_id} is unloaded. You should load before use it."
157
+ else
158
+ raise ex
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ def wallet_name(wallet_id)
165
+ "#{WALLET_PREFIX}#{wallet_id}"
166
+ end
167
+
168
+ def encode_sighashtype(sighashtype)
169
+ type = case sighashtype & (~(Tapyrus::SIGHASH_TYPE[:anyonecanpay]))
170
+ when Tapyrus::SIGHASH_TYPE[:all] then 'ALL'
171
+ when Tapyrus::SIGHASH_TYPE[:none] then 'NONE'
172
+ when Tapyrus::SIGHASH_TYPE[:single] then 'SIGNLE'
173
+ else
174
+ raise Errors::InvalidSighashType, "Invalid sighash type '#{sighashtype}'"
175
+ end
176
+
177
+ if sighashtype & Tapyrus::SIGHASH_TYPE[:anyonecanpay] == 0x80
178
+ type += '|ANYONECANPAY'
179
+ end
180
+
181
+ type
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end