glueby 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,18 +11,32 @@ module Glueby
11
11
  end
12
12
 
13
13
  # Create new public key, and new transaction that sends TPC to it
14
- def create_funding_tx(wallet:, script: nil, fee_estimator: FixedFeeEstimator.new)
15
- tx = Tapyrus::Tx.new
16
- fee = fee_estimator.fee(dummy_tx(tx))
17
-
18
- sum, outputs = wallet.internal_wallet.collect_uncolored_outputs(fee + FUNDING_TX_AMOUNT)
19
- fill_input(tx, outputs)
20
-
21
- receiver_script = script ? script : Tapyrus::Script.parse_from_addr(wallet.internal_wallet.receive_address)
22
- tx.outputs << Tapyrus::TxOut.new(value: FUNDING_TX_AMOUNT, script_pubkey: receiver_script)
23
-
24
- fill_change_tpc(tx, wallet, sum - fee - FUNDING_TX_AMOUNT)
25
- wallet.internal_wallet.sign_tx(tx)
14
+ def create_funding_tx(wallet:, script: nil, fee_estimator: FixedFeeEstimator.new, utxo_provider: nil)
15
+ if utxo_provider
16
+ script_pubkey = script ? script : Tapyrus::Script.parse_from_addr(wallet.internal_wallet.receive_address)
17
+ funding_tx, _index = utxo_provider.get_utxo(script_pubkey, FUNDING_TX_AMOUNT)
18
+ utxo_provider.wallet.sign_tx(funding_tx)
19
+ else
20
+ txb = Tapyrus::TxBuilder.new
21
+ fee = fee_estimator.fee(dummy_tx(txb.build))
22
+
23
+ sum, outputs = wallet.internal_wallet.collect_uncolored_outputs(fee + FUNDING_TX_AMOUNT)
24
+ outputs.each do |utxo|
25
+ txb.add_utxo({
26
+ script_pubkey: Tapyrus::Script.parse_from_payload(utxo[:script_pubkey].htb),
27
+ txid: utxo[:txid],
28
+ index: utxo[:vout],
29
+ value: utxo[:amount]
30
+ })
31
+ end
32
+
33
+ receiver_address = script ? script.addresses.first : wallet.internal_wallet.receive_address
34
+ tx = txb.pay(receiver_address, FUNDING_TX_AMOUNT)
35
+ .change_address(wallet.internal_wallet.change_address)
36
+ .fee(fee)
37
+ .build
38
+ wallet.internal_wallet.sign_tx(tx)
39
+ end
26
40
  end
27
41
 
28
42
  def create_issue_tx_for_reissuable_token(funding_tx:, issuer:, amount:, fee_estimator: FixedFeeEstimator.new)
@@ -48,21 +62,27 @@ module Glueby
48
62
  issuer.internal_wallet.sign_tx(tx, prev_txs)
49
63
  end
50
64
 
51
- def create_issue_tx_for_non_reissuable_token(issuer:, amount:, fee_estimator: FixedFeeEstimator.new)
52
- create_issue_tx_from_out_point(token_type: Tapyrus::Color::TokenTypes::NON_REISSUABLE, issuer: issuer, amount: amount, fee_estimator: fee_estimator)
65
+ def create_issue_tx_for_non_reissuable_token(funding_tx: nil, issuer:, amount:, fee_estimator: FixedFeeEstimator.new)
66
+ create_issue_tx_from_out_point(funding_tx: funding_tx, token_type: Tapyrus::Color::TokenTypes::NON_REISSUABLE, issuer: issuer, amount: amount, fee_estimator: fee_estimator)
53
67
  end
54
68
 
55
- def create_issue_tx_for_nft_token(issuer:, fee_estimator: FixedFeeEstimator.new)
56
- create_issue_tx_from_out_point(token_type: Tapyrus::Color::TokenTypes::NFT, issuer: issuer, amount: 1, fee_estimator: fee_estimator)
69
+ def create_issue_tx_for_nft_token(funding_tx: nil, issuer:, fee_estimator: FixedFeeEstimator.new)
70
+ create_issue_tx_from_out_point(funding_tx: funding_tx, token_type: Tapyrus::Color::TokenTypes::NFT, issuer: issuer, amount: 1, fee_estimator: fee_estimator)
57
71
  end
58
72
 
59
- def create_issue_tx_from_out_point(token_type:, issuer:, amount:, fee_estimator: FixedFeeEstimator.new)
73
+ def create_issue_tx_from_out_point(funding_tx: nil, token_type:, issuer:, amount:, fee_estimator: FixedFeeEstimator.new)
60
74
  tx = Tapyrus::Tx.new
61
75
 
62
76
  fee = fee_estimator.fee(dummy_issue_tx_from_out_point)
63
- sum, outputs = issuer.internal_wallet.collect_uncolored_outputs(fee)
64
- fill_input(tx, outputs)
65
-
77
+ sum = if funding_tx
78
+ out_point = Tapyrus::OutPoint.from_txid(funding_tx.txid, 0)
79
+ tx.inputs << Tapyrus::TxIn.new(out_point: out_point)
80
+ funding_tx.outputs.first.value
81
+ else
82
+ sum, outputs = issuer.internal_wallet.collect_uncolored_outputs(fee)
83
+ fill_input(tx, outputs)
84
+ sum
85
+ end
66
86
  out_point = tx.inputs.first.out_point
67
87
  color_id = case token_type
68
88
  when Tapyrus::Color::TokenTypes::NON_REISSUABLE
@@ -78,7 +98,18 @@ module Glueby
78
98
  tx.outputs << Tapyrus::TxOut.new(value: amount, script_pubkey: receiver_colored_script)
79
99
 
80
100
  fill_change_tpc(tx, issuer, sum - fee)
81
- issuer.internal_wallet.sign_tx(tx)
101
+ prev_txs = if funding_tx
102
+ output = funding_tx.outputs.first
103
+ [{
104
+ txid: funding_tx.txid,
105
+ vout: 0,
106
+ scriptPubKey: output.script_pubkey.to_hex,
107
+ amount: output.value
108
+ }]
109
+ else
110
+ []
111
+ end
112
+ issuer.internal_wallet.sign_tx(tx, prev_txs)
82
113
  end
83
114
 
84
115
  def create_reissue_tx(funding_tx:, issuer:, amount:, color_id:, fee_estimator: FixedFeeEstimator.new)
@@ -103,7 +134,7 @@ module Glueby
103
134
  issuer.internal_wallet.sign_tx(tx, prev_txs)
104
135
  end
105
136
 
106
- def create_transfer_tx(color_id:, sender:, receiver_address:, amount:, fee_estimator: FixedFeeEstimator.new)
137
+ def create_transfer_tx(funding_tx:nil, color_id:, sender:, receiver_address:, amount:, fee_estimator: FixedFeeEstimator.new)
107
138
  tx = Tapyrus::Tx.new
108
139
 
109
140
  utxos = sender.internal_wallet.list_unspent
@@ -117,14 +148,32 @@ module Glueby
117
148
  fill_change_token(tx, sender, sum_token - amount, color_id)
118
149
 
119
150
  fee = fee_estimator.fee(dummy_tx(tx))
120
- sum_tpc, outputs = sender.internal_wallet.collect_uncolored_outputs(fee)
121
- fill_input(tx, outputs)
151
+ sum_tpc = if funding_tx
152
+ out_point = Tapyrus::OutPoint.from_txid(funding_tx.txid, 0)
153
+ tx.inputs << Tapyrus::TxIn.new(out_point: out_point)
154
+ funding_tx.outputs.first.value
155
+ else
156
+ sum_tpc, outputs = sender.internal_wallet.collect_uncolored_outputs(fee)
157
+ fill_input(tx, outputs)
158
+ sum_tpc
159
+ end
122
160
 
123
161
  fill_change_tpc(tx, sender, sum_tpc - fee)
124
- sender.internal_wallet.sign_tx(tx)
162
+ prev_txs = if funding_tx
163
+ output = funding_tx.outputs.first
164
+ [{
165
+ txid: funding_tx.txid,
166
+ vout: 0,
167
+ scriptPubKey: output.script_pubkey.to_hex,
168
+ amount: output.value
169
+ }]
170
+ else
171
+ []
172
+ end
173
+ sender.internal_wallet.sign_tx(tx, prev_txs)
125
174
  end
126
175
 
127
- def create_burn_tx(color_id:, sender:, amount: 0, fee_estimator: FixedFeeEstimator.new)
176
+ def create_burn_tx(funding_tx:nil, color_id:, sender:, amount: 0, fee_estimator: FixedFeeEstimator.new)
128
177
  tx = Tapyrus::Tx.new
129
178
 
130
179
  utxos = sender.internal_wallet.list_unspent
@@ -135,12 +184,30 @@ module Glueby
135
184
 
136
185
  fee = fee_estimator.fee(dummy_tx(tx))
137
186
 
138
- dust = 600 # in case that the wallet has output which has just fee amount.
139
- sum_tpc, outputs = sender.internal_wallet.collect_uncolored_outputs(fee + dust)
140
- fill_input(tx, outputs)
187
+ sum_tpc = if funding_tx
188
+ out_point = Tapyrus::OutPoint.from_txid(funding_tx.txid, 0)
189
+ tx.inputs << Tapyrus::TxIn.new(out_point: out_point)
190
+ funding_tx.outputs.first.value
191
+ else
192
+ dust = 600 # in case that the wallet has output which has just fee amount.
193
+ sum_tpc, outputs = sender.internal_wallet.collect_uncolored_outputs(fee + dust)
194
+ fill_input(tx, outputs)
195
+ sum_tpc
196
+ end
141
197
 
142
198
  fill_change_tpc(tx, sender, sum_tpc - fee)
143
- sender.internal_wallet.sign_tx(tx)
199
+ prev_txs = if funding_tx
200
+ output = funding_tx.outputs.first
201
+ [{
202
+ txid: funding_tx.txid,
203
+ vout: 0,
204
+ scriptPubKey: output.script_pubkey.to_hex,
205
+ amount: output.value
206
+ }]
207
+ else
208
+ []
209
+ end
210
+ sender.internal_wallet.sign_tx(tx, prev_txs)
144
211
  end
145
212
 
146
213
  def fill_input(tx, outputs)
@@ -44,7 +44,7 @@ module Glueby
44
44
  .fee(fee_provider.fixed_fee)
45
45
  .build
46
46
  tx = wallet.sign_tx(tx)
47
- wallet.broadcast(tx)
47
+ wallet.broadcast(tx, without_fee_provider: true)
48
48
  ensure
49
49
  status
50
50
  end
@@ -80,6 +80,11 @@ module Glueby
80
80
  EOS
81
81
  end
82
82
 
83
+ # Show the address of Fee Provider
84
+ def print_address
85
+ puts wallet.receive_address
86
+ end
87
+
83
88
  private
84
89
 
85
90
  def check_wallet_amount!
@@ -8,6 +8,7 @@ module Glueby
8
8
  WALLET_ID = 'FEE_PROVIDER_WALLET'
9
9
  DEFAULT_FIXED_FEE = 1000
10
10
  DEFAULT_UTXO_POOL_SIZE = 20
11
+ MAX_UTXO_POOL_SIZE = 2_000
11
12
 
12
13
  attr_reader :fixed_fee, :utxo_pool_size, :wallet,
13
14
 
@@ -33,6 +34,7 @@ module Glueby
33
34
  Internal::Wallet.create(WALLET_ID)
34
35
  end
35
36
 
37
+ validate_config!
36
38
  @fixed_fee = (FeeProvider.config && FeeProvider.config[:fixed_fee]) || DEFAULT_FIXED_FEE
37
39
  @utxo_pool_size = (FeeProvider.config && FeeProvider.config[:utxo_pool_size]) || DEFAULT_UTXO_POOL_SIZE
38
40
  end
@@ -69,5 +71,14 @@ module Glueby
69
71
  def get_signature(script_sig)
70
72
  script_sig.chunks.first.pushed_data
71
73
  end
74
+
75
+ def validate_config!
76
+ if FeeProvider.config
77
+ utxo_pool_size = FeeProvider.config[:utxo_pool_size]
78
+ if utxo_pool_size && (!utxo_pool_size.is_a?(Integer) || utxo_pool_size > MAX_UTXO_POOL_SIZE)
79
+ raise Glueby::Configuration::Errors::InvalidConfiguration, "utxo_pool_size(#{utxo_pool_size}) should not be greater than #{MAX_UTXO_POOL_SIZE}"
80
+ end
81
+ end
82
+ end
72
83
  end
73
84
  end
@@ -101,8 +101,10 @@ module Glueby
101
101
  #
102
102
  # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
103
103
  # @param [Tapyrus::Tx] tx - The transaction to be broadcasterd.
104
+ # @yield Option. If a block given, the block is called before actual broadcasting.
105
+ # @yieldparam [Tapyrus::Tx] tx - The tx that is going to be broadcasted as is.
104
106
  # @return [String] txid
105
- def broadcast(wallet_id, tx)
107
+ def broadcast(wallet_id, tx, &block)
106
108
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
107
109
  end
108
110
 
@@ -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
@@ -54,6 +54,9 @@ module Glueby
54
54
  # alice_wallet.balances
55
55
  # ```
56
56
  class ActiveRecordWalletAdapter < AbstractWalletAdapter
57
+
58
+ autoload :Syncer, 'glueby/internal/wallet/active_record_wallet_adapter/syncer'
59
+
57
60
  def create_wallet(wallet_id = nil)
58
61
  wallet_id = SecureRandom.hex(16) unless wallet_id
59
62
  begin
@@ -90,7 +93,8 @@ module Glueby
90
93
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
91
94
  utxos = wallet.utxos
92
95
  utxos = utxos.where(status: :finalized) if only_finalized
93
- utxos = utxos.where(label: label) if label
96
+ utxos = utxos.where(label: label) if label && (label != :unlabeled)
97
+ utxos = utxos.where(label: nil) if label == :unlabeled
94
98
  utxos.map do |utxo|
95
99
  {
96
100
  txid: utxo.txid,
@@ -108,17 +112,18 @@ module Glueby
108
112
  wallet.sign(tx, prevtxs, sighashtype: sighashtype)
109
113
  end
110
114
 
111
- def broadcast(wallet_id, tx)
115
+ def broadcast(wallet_id, tx, &block)
112
116
  ::ActiveRecord::Base.transaction do
113
117
  AR::Utxo.destroy_for_inputs(tx)
114
118
  AR::Utxo.create_or_update_for_outputs(tx, status: :broadcasted)
119
+ block.call(tx) if block
115
120
  Glueby::Internal::RPC.client.sendrawtransaction(tx.to_hex)
116
121
  end
117
122
  end
118
123
 
119
124
  def receive_address(wallet_id, label = nil)
120
125
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
121
- key = wallet.keys.create(purpose: :receive, label: label || '')
126
+ key = wallet.keys.create(purpose: :receive, label: label)
122
127
  key.address
123
128
  end
124
129
 
@@ -23,7 +23,6 @@ module Glueby
23
23
  include Glueby::Internal::Wallet::TapyrusCoreWalletAdapter::Util
24
24
 
25
25
  WALLET_PREFIX = 'wallet-'
26
- ADDRESS_TYPE = 'legacy'
27
26
 
28
27
  RPC_WALLET_ERROR_ERROR_CODE = -4 # Unspecified problem with wallet (key not found etc.)
29
28
  RPC_WALLET_NOT_FOUND_ERROR_CODE = -18 # Invalid wallet specified
@@ -33,7 +32,7 @@ module Glueby
33
32
  begin
34
33
  RPC.client.createwallet(wallet_name(wallet_id))
35
34
  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']
35
+ if ex.rpc_error && ex.rpc_error['code'] == RPC_WALLET_ERROR_ERROR_CODE && /Wallet wallet-#{wallet_id} already exists\./ =~ ex.rpc_error['message']
37
36
  raise Errors::WalletAlreadyCreated, "Wallet #{wallet_id} has been already created."
38
37
  else
39
38
  raise ex
@@ -52,9 +51,9 @@ module Glueby
52
51
  def load_wallet(wallet_id)
53
52
  RPC.client.loadwallet(wallet_name(wallet_id))
54
53
  rescue Tapyrus::RPC::Error => ex
55
- if ex.rpc_error['code'] == RPC_WALLET_ERROR_ERROR_CODE && /Duplicate -wallet filename specified/ =~ ex.rpc_error['message']
54
+ if ex.rpc_error && ex.rpc_error['code'] == RPC_WALLET_ERROR_ERROR_CODE && /Duplicate -wallet filename specified/ =~ ex.rpc_error['message']
56
55
  raise Errors::WalletAlreadyLoaded, "Wallet #{wallet_id} has been already loaded."
57
- elsif ex.rpc_error['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
56
+ elsif ex.rpc_error && ex.rpc_error['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
58
57
  raise Errors::WalletNotFound, "Wallet #{wallet_id} does not found"
59
58
  else
60
59
  raise ex
@@ -88,7 +87,8 @@ module Glueby
88
87
  min_conf = only_finalized ? 1 : 0
89
88
  res = client.listunspent(min_conf)
90
89
 
91
- res = res.filter { |i| i['label'] == label } if label
90
+ res = res.filter { |i| i['label'] == label } if label && (label != :unlabeled)
91
+ res = res.filter { |i| i['label'] == "" } if label == :unlabeled
92
92
 
93
93
  res.map do |i|
94
94
  script = Tapyrus::Script.parse_from_payload(i['scriptPubKey'].htb)
@@ -118,27 +118,28 @@ module Glueby
118
118
  end
119
119
  end
120
120
 
121
- def broadcast(wallet_id, tx)
121
+ def broadcast(wallet_id, tx, &block)
122
122
  perform_as(wallet_id) do |client|
123
+ block.call(tx) if block
123
124
  client.sendrawtransaction(tx.to_hex)
124
125
  end
125
126
  end
126
127
 
127
128
  def receive_address(wallet_id, label = nil)
128
129
  perform_as(wallet_id) do |client|
129
- client.getnewaddress(label || '', ADDRESS_TYPE)
130
+ client.getnewaddress(label || '')
130
131
  end
131
132
  end
132
133
 
133
134
  def change_address(wallet_id)
134
135
  perform_as(wallet_id) do |client|
135
- client.getrawchangeaddress(ADDRESS_TYPE)
136
+ client.getrawchangeaddress
136
137
  end
137
138
  end
138
139
 
139
140
  def create_pubkey(wallet_id)
140
141
  perform_as(wallet_id) do |client|
141
- address = client.getnewaddress('', ADDRESS_TYPE)
142
+ address = client.getnewaddress('')
142
143
  info = client.getaddressinfo(address)
143
144
  Tapyrus::Key.new(pubkey: info['pubkey'])
144
145
  end
@@ -151,7 +152,7 @@ module Glueby
151
152
  begin
152
153
  yield(client)
153
154
  rescue Tapyrus::RPC::Error => ex
154
- if ex.rpc_error['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
155
+ if ex.rpc_error && ex.rpc_error['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
155
156
  raise Errors::WalletUnloaded, "The wallet #{wallet_id} is unloaded. You should load before use it."
156
157
  else
157
158
  raise ex
@@ -34,8 +34,6 @@ module Glueby
34
34
  autoload :Errors, 'glueby/internal/wallet/errors'
35
35
 
36
36
  class << self
37
- attr_writer :wallet_adapter
38
-
39
37
  def create(wallet_id = nil)
40
38
  begin
41
39
  wallet_id = wallet_adapter.create_wallet(wallet_id)
@@ -58,6 +56,16 @@ module Glueby
58
56
  wallet_adapter.wallets.map { |id| new(id) }
59
57
  end
60
58
 
59
+ def wallet_adapter=(adapter)
60
+ if adapter.is_a?(ActiveRecordWalletAdapter)
61
+ BlockSyncer.register_syncer(ActiveRecordWalletAdapter::Syncer)
62
+ else
63
+ BlockSyncer.unregister_syncer(ActiveRecordWalletAdapter::Syncer)
64
+ end
65
+
66
+ @wallet_adapter = adapter
67
+ end
68
+
61
69
  def wallet_adapter
62
70
  @wallet_adapter or
63
71
  raise Errors::ShouldInitializeWalletAdapter, 'You should initialize wallet adapter using `Glueby::Internal::Wallet.wallet_adapter = some wallet adapter instance`.'
@@ -74,6 +82,10 @@ module Glueby
74
82
  wallet_adapter.balance(id, only_finalized)
75
83
  end
76
84
 
85
+ # @param only_finalized [Boolean] The flag to get a UTXO with status only finalized
86
+ # @param label [String] This label is used to filtered the UTXOs with labeled if a key or Utxo is labeled.
87
+ # - If label is not specified (label=nil), all UTXOs will be returned.
88
+ # - If label=:unlabeled, only unlabeled UTXOs will be returned.
77
89
  def list_unspent(only_finalized = true, label = nil)
78
90
  wallet_adapter.list_unspent(id, only_finalized, label)
79
91
  end
@@ -99,9 +111,13 @@ module Glueby
99
111
  wallet_adapter.sign_tx(id, tx, prev_txs, sighashtype: sighashtype)
100
112
  end
101
113
 
102
- def broadcast(tx)
103
- tx = FeeProvider.provide(tx) if Glueby.configuration.fee_provider_bears?
104
- wallet_adapter.broadcast(id, tx)
114
+ # Broadcast a transaction via Tapyrus Core RPC
115
+ # @param [Tapyrus::Tx] tx The tx that would be broadcasted
116
+ # @option [Boolean] without_fee_provider The flag to avoid to use FeeProvider temporary.
117
+ # @param [Proc] block The block that is called before broadcasting. It can be used to handle tx that is modified by FeeProvider.
118
+ def broadcast(tx, without_fee_provider: false, &block)
119
+ tx = FeeProvider.provide(tx) if !without_fee_provider && Glueby.configuration.fee_provider_bears?
120
+ wallet_adapter.broadcast(id, tx, &block)
105
121
  tx
106
122
  end
107
123
 
@@ -117,8 +133,9 @@ module Glueby
117
133
  wallet_adapter.create_pubkey(id)
118
134
  end
119
135
 
120
- def collect_uncolored_outputs(amount, label = nil)
121
- utxos = list_unspent(true, label)
136
+ def collect_uncolored_outputs(amount, label = nil, only_finalized = true, shuffle = false)
137
+ utxos = list_unspent(only_finalized, label)
138
+ utxos.shuffle! if shuffle
122
139
 
123
140
  utxos.inject([0, []]) do |sum, output|
124
141
  next sum if output[:color_id]
@@ -0,0 +1,11 @@
1
+ module Glueby
2
+ class Railtie < ::Rails::Railtie
3
+ rake_tasks do
4
+ load "tasks/glueby/contract.rake"
5
+ load "tasks/glueby/contract/timestamp.rake"
6
+ load "tasks/glueby/block_syncer.rake"
7
+ load "tasks/glueby/fee_provider.rake"
8
+ load "tasks/glueby/utxo_provider.rake"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,135 @@
1
+ module Glueby
2
+ class UtxoProvider
3
+ class Tasks
4
+ include Glueby::Contract::TxBuilder
5
+
6
+ attr_reader :utxo_provider
7
+
8
+ STATUS = {
9
+ # UtxoProvider is ready to pay tpcs.
10
+ ready: 'Ready',
11
+ # UtxoProvider is ready to pay tpcs, but it doesn't have enough amount to fill the UTXO pool by UTXOs which is for paying tpcs.
12
+ insufficient_amount: 'Insufficient Amount',
13
+ # UtxoProvider is not ready to pay tpcs. It has no UTXOs for paying amounts.
14
+ not_ready: 'Not Ready'
15
+ }
16
+
17
+ def initialize
18
+ @utxo_provider = Glueby::UtxoProvider.new
19
+ end
20
+
21
+ # Create UTXOs for paying tpc
22
+ #
23
+ # UtxoProvider have the UTXO pool. the pool is manged to keep some number of UTXOs that have fixed value. The
24
+ # value is configurable by :default_value. This method do the management to the pool.
25
+ def manage_utxo_pool
26
+ txb = Tapyrus::TxBuilder.new
27
+
28
+ sum, utxos = collect_outputs
29
+ return if utxos.empty?
30
+
31
+ utxos.each { |utxo| txb.add_utxo(utxo) }
32
+ address = wallet.receive_address
33
+
34
+ shortage = [utxo_provider.utxo_pool_size - current_utxo_pool_size, 0].max
35
+ return if shortage == 0
36
+
37
+ added_outputs = 0
38
+ shortage.times do
39
+ fee = utxo_provider.fee_estimator.fee(dummy_tx(txb.build))
40
+ break if (sum - fee) < utxo_provider.default_value
41
+ txb.pay(address, utxo_provider.default_value)
42
+ sum -= utxo_provider.default_value
43
+ added_outputs += 1
44
+ end
45
+
46
+ return if added_outputs == 0
47
+
48
+ fee = utxo_provider.fee_estimator.fee(dummy_tx(txb.build))
49
+ tx = txb.change_address(address)
50
+ .fee(fee)
51
+ .build
52
+ tx = wallet.sign_tx(tx)
53
+ wallet.broadcast(tx)
54
+ ensure
55
+ status
56
+ end
57
+
58
+ # Show the status of the UTXO pool
59
+ def status
60
+ status = :ready
61
+
62
+ if current_utxo_pool_size < utxo_provider.utxo_pool_size
63
+ if tpc_amount < value_to_fill_utxo_pool
64
+ status = :insufficient_amount
65
+ message = <<~MESSAGE
66
+ 1. Please replenishment TPC which is for paying tpc to UtxoProvider.
67
+ UtxoProvider needs #{value_to_fill_utxo_pool} tapyrus in UTXO pool.
68
+ UtxoProvider wallet's address is '#{wallet.receive_address}'
69
+ 2. Then create UTXOs for paying in UTXO pool with 'rake glueby:utxo_provider:manage_utxo_pool'
70
+ MESSAGE
71
+ else
72
+ message = "Please create UTXOs for paying in UTXO pool with 'rake glueby:utxo_provider:manage_utxo_pool'\n"
73
+ end
74
+ end
75
+
76
+ status = :not_ready if current_utxo_pool_size == 0
77
+
78
+ puts <<~EOS
79
+ Status: #{STATUS[status]}
80
+ TPC amount: #{delimit(tpc_amount)}
81
+ UTXO pool size: #{delimit(current_utxo_pool_size)}
82
+ #{"\n" if message}#{message}
83
+ Configuration:
84
+ default_value = #{delimit(utxo_provider.default_value)}
85
+ utxo_pool_size = #{delimit(utxo_provider.utxo_pool_size)}
86
+ EOS
87
+ end
88
+
89
+ # Show the address of Utxo Provider
90
+ def print_address
91
+ puts wallet.receive_address
92
+ end
93
+
94
+ private
95
+
96
+ def tpc_amount
97
+ wallet.balance(false)
98
+ end
99
+
100
+ def collect_outputs
101
+ wallet.list_unspent.inject([0, []]) do |sum, output|
102
+ next sum if output[:color_id] || output[:amount] == utxo_provider.default_value
103
+
104
+ new_sum = sum[0] + output[:amount]
105
+ new_outputs = sum[1] << {
106
+ txid: output[:txid],
107
+ script_pubkey: output[:script_pubkey],
108
+ value: output[:amount],
109
+ index: output[:vout] ,
110
+ finalized: output[:finalized]
111
+ }
112
+ [new_sum, new_outputs]
113
+ end
114
+ end
115
+
116
+ def current_utxo_pool_size
117
+ wallet
118
+ .list_unspent(false)
119
+ .count { |o| !o[:color_id] && o[:amount] == utxo_provider.default_value }
120
+ end
121
+
122
+ def value_to_fill_utxo_pool
123
+ utxo_provider.default_value * utxo_provider.utxo_pool_size
124
+ end
125
+
126
+ def wallet
127
+ utxo_provider.wallet
128
+ end
129
+
130
+ def delimit(num)
131
+ num.to_s.reverse.scan(/.{1,3}/).join('_').reverse
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,85 @@
1
+ module Glueby
2
+ class UtxoProvider
3
+ include Glueby::Contract::TxBuilder
4
+
5
+ autoload :Tasks, 'glueby/utxo_provider/tasks'
6
+
7
+ WALLET_ID = 'UTXO_PROVIDER_WALLET'
8
+ DEFAULT_VALUE = 1_000
9
+ DEFAULT_UTXO_POOL_SIZE = 20
10
+ MAX_UTXO_POOL_SIZE = 2_000
11
+
12
+ class << self
13
+ attr_reader :config
14
+
15
+ # @param [Hash] config
16
+ # @option config [Integer] :default_value
17
+ # @option opts [Integer] :utxo_pool_size
18
+ # @option opts [Glueby::Contract::FeeEstimator] :fee_estimator
19
+ def configure(config)
20
+ @config = config
21
+ end
22
+ end
23
+
24
+ def initialize
25
+ @wallet = load_wallet
26
+ validate_config!
27
+ @fee_estimator = (UtxoProvider.config && UtxoProvider.config[:fee_estimator]) || Glueby::Contract::FixedFeeEstimator.new
28
+ @default_value = (UtxoProvider.config && UtxoProvider.config[:default_value]) || DEFAULT_VALUE
29
+ @utxo_pool_size = (UtxoProvider.config && UtxoProvider.config[:utxo_pool_size]) || DEFAULT_UTXO_POOL_SIZE
30
+ end
31
+
32
+ attr_reader :wallet, :fee_estimator, :default_value, :utxo_pool_size
33
+
34
+ # Provide a UTXO
35
+ # @param [Tapyrus::Script] script_pubkey The script to be provided
36
+ # @param [Integer] value The tpc amount to be provided
37
+ # @return [Array<(Tapyrus::Tx, Integer)>]
38
+ # The tx that has a UTXO to be provided in its outputs.
39
+ # The output index in the tx to indicate the place of a provided UTXO.
40
+ # @raise [Glueby::Contract::Errors::InsufficientFunds] if provider does not have any utxo which has specified value.
41
+ def get_utxo(script_pubkey, value = DEFAULT_VALUE)
42
+ txb = Tapyrus::TxBuilder.new
43
+ txb.pay(script_pubkey.addresses.first, value)
44
+
45
+ fee = fee_estimator.fee(dummy_tx(txb.build))
46
+ # The outputs need to be shuffled so that no utxos are spent twice as possible.
47
+ sum, outputs = wallet.collect_uncolored_outputs(fee + value, nil, true, true)
48
+
49
+ outputs.each do |utxo|
50
+ txb.add_utxo({
51
+ script_pubkey: Tapyrus::Script.parse_from_payload(utxo[:script_pubkey].htb),
52
+ txid: utxo[:txid],
53
+ index: utxo[:vout],
54
+ value: utxo[:amount]
55
+ })
56
+ end
57
+
58
+ txb.fee(fee).change_address(wallet.change_address)
59
+
60
+ tx = txb.build
61
+ signed_tx = wallet.sign_tx(tx)
62
+ [signed_tx, 0]
63
+ end
64
+
65
+ private
66
+
67
+ # Create wallet for provider
68
+ def load_wallet
69
+ begin
70
+ Glueby::Internal::Wallet.load(WALLET_ID)
71
+ rescue Glueby::Internal::Wallet::Errors::WalletNotFound => _
72
+ Glueby::Internal::Wallet.create(WALLET_ID)
73
+ end
74
+ end
75
+
76
+ def validate_config!
77
+ if UtxoProvider.config
78
+ utxo_pool_size = UtxoProvider.config[:utxo_pool_size]
79
+ if utxo_pool_size && (!utxo_pool_size.is_a?(Integer) || utxo_pool_size > MAX_UTXO_POOL_SIZE)
80
+ raise Glueby::Configuration::Errors::InvalidConfiguration, "utxo_pool_size(#{utxo_pool_size}) should not be greater than #{MAX_UTXO_POOL_SIZE}"
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end