glueby 0.3.0 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +35 -0
  3. data/Gemfile +1 -0
  4. data/README.md +227 -16
  5. data/glueby.gemspec +3 -2
  6. data/lib/generators/glueby/contract/reissuable_token_generator.rb +26 -0
  7. data/lib/generators/glueby/contract/templates/initializer.rb.erb +7 -2
  8. data/lib/generators/glueby/contract/templates/key_table.rb.erb +4 -3
  9. data/lib/generators/glueby/contract/templates/reissuable_token_table.rb.erb +10 -0
  10. data/lib/generators/glueby/contract/templates/system_information_table.rb.erb +2 -2
  11. data/lib/generators/glueby/contract/templates/timestamp_table.rb.erb +1 -1
  12. data/lib/generators/glueby/contract/templates/utxo_table.rb.erb +3 -2
  13. data/lib/generators/glueby/contract/templates/wallet_table.rb.erb +2 -2
  14. data/lib/glueby/block_syncer.rb +98 -0
  15. data/lib/glueby/configuration.rb +62 -0
  16. data/lib/glueby/contract/active_record/reissuable_token.rb +26 -0
  17. data/lib/glueby/contract/active_record.rb +1 -0
  18. data/lib/glueby/contract/fee_estimator.rb +38 -0
  19. data/lib/glueby/contract/payment.rb +4 -4
  20. data/lib/glueby/contract/timestamp/syncer.rb +13 -0
  21. data/lib/glueby/contract/timestamp.rb +8 -6
  22. data/lib/glueby/contract/token.rb +70 -22
  23. data/lib/glueby/contract/tx_builder.rb +22 -19
  24. data/lib/glueby/contract.rb +2 -2
  25. data/lib/glueby/fee_provider/tasks.rb +141 -0
  26. data/lib/glueby/fee_provider.rb +73 -0
  27. data/lib/glueby/generator/migrate_generator.rb +1 -1
  28. data/lib/glueby/internal/wallet/abstract_wallet_adapter.rb +25 -6
  29. data/lib/glueby/internal/wallet/active_record/utxo.rb +1 -0
  30. data/lib/glueby/internal/wallet/active_record/wallet.rb +15 -5
  31. data/lib/glueby/internal/wallet/active_record_wallet_adapter/syncer.rb +14 -0
  32. data/lib/glueby/internal/wallet/active_record_wallet_adapter.rb +25 -8
  33. data/lib/glueby/internal/wallet/errors.rb +3 -0
  34. data/lib/glueby/internal/wallet/tapyrus_core_wallet_adapter.rb +42 -14
  35. data/lib/glueby/internal/wallet.rb +56 -13
  36. data/lib/glueby/railtie.rb +10 -0
  37. data/lib/glueby/version.rb +1 -1
  38. data/lib/glueby/wallet.rb +3 -2
  39. data/lib/glueby.rb +27 -12
  40. data/lib/tasks/glueby/block_syncer.rake +29 -0
  41. data/lib/tasks/glueby/contract/timestamp.rake +4 -26
  42. data/lib/tasks/glueby/fee_provider.rake +18 -0
  43. metadata +40 -16
  44. data/.travis.yml +0 -7
  45. data/lib/glueby/contract/fee_provider.rb +0 -21
  46. data/lib/tasks/glueby/contract/block_syncer.rake +0 -36
  47. data/lib/tasks/glueby/contract/wallet_adapter.rake +0 -42
@@ -13,13 +13,16 @@ module Glueby
13
13
 
14
14
  # @param [Tapyrus::Tx] tx
15
15
  # @param [Array] prevtxs array of outputs
16
- def sign(tx, prevtxs = [])
16
+ # @param [Integer] sighashtype
17
+ def sign(tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
18
+ validate_sighashtype!(sighashtype)
19
+
17
20
  tx.inputs.each.with_index do |input, index|
18
21
  script_pubkey = script_for_input(input, prevtxs)
19
22
  next unless script_pubkey
20
23
  key = Key.key_for_script(script_pubkey)
21
24
  next unless key
22
- sign_tx_for_p2pkh(tx, index, key, script_pubkey)
25
+ sign_tx_for_p2pkh(tx, index, key, script_pubkey, sighashtype)
23
26
  end
24
27
  tx
25
28
  end
@@ -30,9 +33,9 @@ module Glueby
30
33
 
31
34
  private
32
35
 
33
- def sign_tx_for_p2pkh(tx, index, key, script_pubkey)
34
- sighash = tx.sighash_for_input(index, script_pubkey)
35
- sig = key.sign(sighash) + [Tapyrus::SIGHASH_TYPE[:all]].pack('C')
36
+ def sign_tx_for_p2pkh(tx, index, key, script_pubkey, sighashtype)
37
+ sighash = tx.sighash_for_input(index, script_pubkey, hash_type: sighashtype)
38
+ sig = key.sign(sighash) + [sighashtype].pack('C')
36
39
  script_sig = Tapyrus::Script.parse_from_payload(Tapyrus::Script.pack_pushdata(sig) + Tapyrus::Script.pack_pushdata(key.public_key.htb))
37
40
  tx.inputs[index].script_sig = script_sig
38
41
  end
@@ -47,6 +50,13 @@ module Glueby
47
50
  Tapyrus::Script.parse_from_payload(output[:scriptPubKey].htb) if output
48
51
  end
49
52
  end
53
+
54
+ def validate_sighashtype!(sighashtype)
55
+ hash_type = sighashtype & (~(Tapyrus::SIGHASH_TYPE[:anyonecanpay]))
56
+ if hash_type < Tapyrus::SIGHASH_TYPE[:all] || hash_type > Tapyrus::SIGHASH_TYPE[:single]
57
+ raise Errors::InvalidSighashType, "Invalid sighash type '#{sighashtype}'"
58
+ end
59
+ end
50
60
  end
51
61
  end
52
62
  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
@@ -54,9 +54,16 @@ module Glueby
54
54
  # alice_wallet.balances
55
55
  # ```
56
56
  class ActiveRecordWalletAdapter < AbstractWalletAdapter
57
- def create_wallet
58
- wallet_id = SecureRandom.hex(16)
59
- wallet = AR::Wallet.create(wallet_id: wallet_id)
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
60
67
  wallet_id
61
68
  end
62
69
 
@@ -65,6 +72,7 @@ module Glueby
65
72
  end
66
73
 
67
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?
68
76
  end
69
77
 
70
78
  def unload_wallet(wallet_id)
@@ -81,10 +89,12 @@ module Glueby
81
89
  utxos.sum(&:value)
82
90
  end
83
91
 
84
- def list_unspent(wallet_id, only_finalized = true)
92
+ def list_unspent(wallet_id, only_finalized = true, label = nil)
85
93
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
86
94
  utxos = wallet.utxos
87
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
88
98
  utxos.map do |utxo|
89
99
  {
90
100
  txid: utxo.txid,
@@ -97,9 +107,9 @@ module Glueby
97
107
  end
98
108
  end
99
109
 
100
- def sign_tx(wallet_id, tx, prevtxs = [])
110
+ def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
101
111
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
102
- wallet.sign(tx, prevtxs)
112
+ wallet.sign(tx, prevtxs, sighashtype: sighashtype)
103
113
  end
104
114
 
105
115
  def broadcast(wallet_id, tx)
@@ -110,9 +120,9 @@ module Glueby
110
120
  end
111
121
  end
112
122
 
113
- def receive_address(wallet_id)
123
+ def receive_address(wallet_id, label = nil)
114
124
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
115
- key = wallet.keys.create(purpose: :receive)
125
+ key = wallet.keys.create(purpose: :receive, label: label)
116
126
  key.address
117
127
  end
118
128
 
@@ -127,6 +137,13 @@ module Glueby
127
137
  key = wallet.keys.create(purpose: :receive)
128
138
  Tapyrus::Key.new(pubkey: key.public_key)
129
139
  end
140
+
141
+ def get_addresses(wallet_id, label = nil)
142
+ wallet = AR::Wallet.find_by(wallet_id: wallet_id)
143
+ keys = wallet.keys
144
+ keys = keys.where(label: label) if label
145
+ keys.map(&:address)
146
+ end
130
147
  end
131
148
  end
132
149
  end
@@ -5,6 +5,9 @@ module Glueby
5
5
  class ShouldInitializeWalletAdapter < StandardError; end
6
6
  class WalletUnloaded < StandardError; end
7
7
  class WalletAlreadyLoaded < StandardError; end
8
+ class WalletAlreadyCreated < StandardError; end
9
+ class WalletNotFound < StandardError; end
10
+ class InvalidSighashType < StandardError; end
8
11
  end
9
12
  end
10
13
  end
@@ -28,9 +28,18 @@ module Glueby
28
28
  RPC_WALLET_ERROR_ERROR_CODE = -4 # Unspecified problem with wallet (key not found etc.)
29
29
  RPC_WALLET_NOT_FOUND_ERROR_CODE = -18 # Invalid wallet specified
30
30
 
31
- def create_wallet
32
- wallet_id = SecureRandom.hex(16)
33
- RPC.client.createwallet(wallet_name(wallet_id))
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 && ex.rpc_error['code'] == RPC_WALLET_ERROR_ERROR_CODE && /Wallet wallet-#{wallet_id} 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
+
34
43
  wallet_id
35
44
  end
36
45
 
@@ -42,10 +51,11 @@ module Glueby
42
51
 
43
52
  def load_wallet(wallet_id)
44
53
  RPC.client.loadwallet(wallet_name(wallet_id))
45
- rescue RuntimeError => ex
46
- json = JSON.parse(ex.message)
47
- if json.is_a?(Hash) && json['code'] == RPC_WALLET_ERROR_ERROR_CODE && /Duplicate -wallet filename specified/ =~ ex.message
54
+ rescue Tapyrus::RPC::Error => ex
55
+ if ex.rpc_error && ex.rpc_error['code'] == RPC_WALLET_ERROR_ERROR_CODE && /Duplicate -wallet filename specified/ =~ ex.rpc_error['message']
48
56
  raise Errors::WalletAlreadyLoaded, "Wallet #{wallet_id} has been already loaded."
57
+ elsif ex.rpc_error && ex.rpc_error['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
58
+ raise Errors::WalletNotFound, "Wallet #{wallet_id} does not found"
49
59
  else
50
60
  raise ex
51
61
  end
@@ -73,11 +83,14 @@ module Glueby
73
83
  end
74
84
  end
75
85
 
76
- def list_unspent(wallet_id, only_finalized = true)
86
+ def list_unspent(wallet_id, only_finalized = true, label = nil)
77
87
  perform_as(wallet_id) do |client|
78
88
  min_conf = only_finalized ? 1 : 0
79
89
  res = client.listunspent(min_conf)
80
90
 
91
+ res = res.filter { |i| i['label'] == label } if label && (label != :unlabeled)
92
+ res = res.filter { |i| i['label'] == "" } if label == :unlabeled
93
+
81
94
  res.map do |i|
82
95
  script = Tapyrus::Script.parse_from_payload(i['scriptPubKey'].htb)
83
96
  color_id = if script.cp2pkh? || script.cp2sh?
@@ -95,9 +108,9 @@ module Glueby
95
108
  end
96
109
  end
97
110
 
98
- def sign_tx(wallet_id, tx, prevtxs = [])
111
+ def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
99
112
  perform_as(wallet_id) do |client|
100
- res = client.signrawtransactionwithwallet(tx.to_hex, prevtxs)
113
+ res = client.signrawtransactionwithwallet(tx.to_hex, prevtxs, encode_sighashtype(sighashtype))
101
114
  if res['complete']
102
115
  Tapyrus::Tx.parse_from_payload(res['hex'].htb)
103
116
  else
@@ -112,9 +125,9 @@ module Glueby
112
125
  end
113
126
  end
114
127
 
115
- def receive_address(wallet_id)
128
+ def receive_address(wallet_id, label = nil)
116
129
  perform_as(wallet_id) do |client|
117
- client.getnewaddress('', ADDRESS_TYPE)
130
+ client.getnewaddress(label || '', ADDRESS_TYPE)
118
131
  end
119
132
  end
120
133
 
@@ -138,9 +151,8 @@ module Glueby
138
151
  RPC.perform_as(wallet_name(wallet_id)) do |client|
139
152
  begin
140
153
  yield(client)
141
- rescue RuntimeError => ex
142
- json = JSON.parse(ex.message)
143
- if json.is_a?(Hash) && json['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
154
+ rescue Tapyrus::RPC::Error => ex
155
+ if ex.rpc_error && ex.rpc_error['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
144
156
  raise Errors::WalletUnloaded, "The wallet #{wallet_id} is unloaded. You should load before use it."
145
157
  else
146
158
  raise ex
@@ -152,6 +164,22 @@ module Glueby
152
164
  def wallet_name(wallet_id)
153
165
  "#{WALLET_PREFIX}#{wallet_id}"
154
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
155
183
  end
156
184
  end
157
185
  end
@@ -34,10 +34,13 @@ module Glueby
34
34
  autoload :Errors, 'glueby/internal/wallet/errors'
35
35
 
36
36
  class << self
37
- attr_writer :wallet_adapter
38
-
39
- def create
40
- new(wallet_adapter.create_wallet)
37
+ def create(wallet_id = nil)
38
+ begin
39
+ wallet_id = wallet_adapter.create_wallet(wallet_id)
40
+ rescue Errors::WalletAlreadyCreated => _
41
+ # Ignore when wallet is already created.
42
+ end
43
+ new(wallet_id)
41
44
  end
42
45
 
43
46
  def load(wallet_id)
@@ -53,6 +56,16 @@ module Glueby
53
56
  wallet_adapter.wallets.map { |id| new(id) }
54
57
  end
55
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
+
56
69
  def wallet_adapter
57
70
  @wallet_adapter or
58
71
  raise Errors::ShouldInitializeWalletAdapter, 'You should initialize wallet adapter using `Glueby::Internal::Wallet.wallet_adapter = some wallet adapter instance`.'
@@ -69,24 +82,50 @@ module Glueby
69
82
  wallet_adapter.balance(id, only_finalized)
70
83
  end
71
84
 
72
- def list_unspent(only_finalized = true)
73
- wallet_adapter.list_unspent(id, only_finalized)
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.
89
+ def list_unspent(only_finalized = true, label = nil)
90
+ wallet_adapter.list_unspent(id, only_finalized, label)
74
91
  end
75
92
 
76
93
  def delete
77
94
  wallet_adapter.delete_wallet(id)
78
95
  end
79
96
 
80
- def sign_tx(tx, prev_txs = [])
81
- wallet_adapter.sign_tx(id, tx, prev_txs)
97
+ # @param [Tapyrus::Tx] tx The tx that is signed
98
+ # @param [Array<Hash>] prev_txs An array of hash that represents unbroadcasted transaction outputs used by signing tx
99
+ # @option prev_txs [String] :txid
100
+ # @option prev_txs [Integer] :vout
101
+ # @option prev_txs [String] :scriptPubkey
102
+ # @option prev_txs [Integer] :amount
103
+ # @param [Boolean] for_fee_provider_input The flag to notify whether the caller is FeeProvider and called for signing a input that is by FeeProvider.
104
+ def sign_tx(tx, prev_txs = [], for_fee_provider_input: false)
105
+ sighashtype = Tapyrus::SIGHASH_TYPE[:all]
106
+
107
+ if !for_fee_provider_input && Glueby.configuration.fee_provider_bears?
108
+ sighashtype |= Tapyrus::SIGHASH_TYPE[:anyonecanpay]
109
+ end
110
+
111
+ wallet_adapter.sign_tx(id, tx, prev_txs, sighashtype: sighashtype)
82
112
  end
83
113
 
84
- def broadcast(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
+
121
+ block.call(tx) if block
122
+
85
123
  wallet_adapter.broadcast(id, tx)
124
+ tx
86
125
  end
87
126
 
88
- def receive_address
89
- wallet_adapter.receive_address(id)
127
+ def receive_address(label = nil)
128
+ wallet_adapter.receive_address(id, label)
90
129
  end
91
130
 
92
131
  def change_address
@@ -97,8 +136,8 @@ module Glueby
97
136
  wallet_adapter.create_pubkey(id)
98
137
  end
99
138
 
100
- def collect_uncolored_outputs(amount)
101
- utxos = list_unspent
139
+ def collect_uncolored_outputs(amount, label = nil, only_finalized = true)
140
+ utxos = list_unspent(only_finalized, label)
102
141
 
103
142
  utxos.inject([0, []]) do |sum, output|
104
143
  next sum if output[:color_id]
@@ -112,6 +151,10 @@ module Glueby
112
151
  raise Glueby::Contract::Errors::InsufficientFunds
113
152
  end
114
153
 
154
+ def get_addresses(label = nil)
155
+ wallet_adapter.get_addresses(id, label)
156
+ end
157
+
115
158
  private
116
159
 
117
160
  def wallet_adapter
@@ -0,0 +1,10 @@
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
+ end
9
+ end
10
+ end
@@ -1,3 +1,3 @@
1
1
  module Glueby
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.3"
3
3
  end
data/lib/glueby/wallet.rb CHANGED
@@ -13,6 +13,7 @@ module Glueby
13
13
  new(Glueby::Internal::Wallet.load(wallet_id))
14
14
  end
15
15
 
16
+ # @deprecated - Use Glueby.configure instead
16
17
  def configure(config)
17
18
  case config[:adapter]
18
19
  when 'core'
@@ -32,8 +33,8 @@ module Glueby
32
33
  end
33
34
 
34
35
  # @return [HashMap] hash of balances which key is color_id or empty string, and value is amount
35
- def balances
36
- utxos = @internal_wallet.list_unspent
36
+ def balances(only_finalized = true)
37
+ utxos = @internal_wallet.list_unspent(only_finalized)
37
38
  utxos.inject({}) do |balances, output|
38
39
  key = output[:color_id] || ''
39
40
  balances[key] ||= 0
data/lib/glueby.rb CHANGED
@@ -7,18 +7,33 @@ module Glueby
7
7
  autoload :Wallet, 'glueby/wallet'
8
8
  autoload :Internal, 'glueby/internal'
9
9
  autoload :AR, 'glueby/active_record'
10
+ autoload :FeeProvider, 'glueby/fee_provider'
11
+ autoload :Configuration, 'glueby/configuration'
12
+ autoload :BlockSyncer, 'glueby/block_syncer'
10
13
 
11
- begin
12
- class Railtie < ::Rails::Railtie
13
- rake_tasks do
14
- load "tasks/glueby/contract.rake"
15
- load "tasks/glueby/contract/timestamp.rake"
16
- load "tasks/glueby/contract/wallet_adapter.rake"
17
- load "tasks/glueby/contract/block_syncer.rake"
18
- end
19
- end
20
- rescue
21
- # Rake task is unavailable
22
- puts "Rake task is unavailable"
14
+ if defined? ::Rails::Railtie
15
+ require 'glueby/railtie'
16
+ end
17
+
18
+ # Add prefix to activerecord table names
19
+ def self.table_name_prefix
20
+ 'glueby_'
21
+ end
22
+
23
+ # Returns the global [Configuration](RSpec/Core/Configuration) object.
24
+ def self.configuration
25
+ @configuration ||= Glueby::Configuration.new
26
+ end
27
+
28
+ # Yields the global configuration to a block.
29
+ # @yield [Configuration] global configuration
30
+ #
31
+ # @example
32
+ # Glueby.configure do |config|
33
+ # config.wallet_adapter = :activerecord
34
+ # config.rpc_config = { schema: 'http', host: '127.0.0.1', port: 12381, user: 'user', password: 'pass' }
35
+ # end
36
+ def self.configure
37
+ yield configuration if block_given?
23
38
  end
24
39
  end