glueby 0.4.1 → 0.5.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.
@@ -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