glueby 0.4.0 → 0.4.4

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.
@@ -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