glueby 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +35 -0
  3. data/Gemfile +1 -0
  4. data/README.md +111 -6
  5. data/glueby.gemspec +1 -1
  6. data/lib/generators/glueby/contract/reissuable_token_generator.rb +26 -0
  7. data/lib/generators/glueby/contract/templates/key_table.rb.erb +3 -3
  8. data/lib/generators/glueby/contract/templates/reissuable_token_table.rb.erb +10 -0
  9. data/lib/generators/glueby/contract/templates/system_information_table.rb.erb +2 -2
  10. data/lib/generators/glueby/contract/templates/timestamp_table.rb.erb +1 -1
  11. data/lib/generators/glueby/contract/templates/utxo_table.rb.erb +2 -2
  12. data/lib/generators/glueby/contract/templates/wallet_table.rb.erb +2 -2
  13. data/lib/glueby.rb +25 -0
  14. data/lib/glueby/configuration.rb +62 -0
  15. data/lib/glueby/contract.rb +2 -2
  16. data/lib/glueby/contract/active_record.rb +1 -0
  17. data/lib/glueby/contract/active_record/reissuable_token.rb +26 -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.rb +6 -6
  21. data/lib/glueby/contract/token.rb +69 -22
  22. data/lib/glueby/contract/tx_builder.rb +22 -19
  23. data/lib/glueby/fee_provider.rb +73 -0
  24. data/lib/glueby/fee_provider/tasks.rb +136 -0
  25. data/lib/glueby/generator/migrate_generator.rb +1 -1
  26. data/lib/glueby/internal/wallet.rb +28 -4
  27. data/lib/glueby/internal/wallet/abstract_wallet_adapter.rb +18 -3
  28. data/lib/glueby/internal/wallet/active_record/wallet.rb +15 -5
  29. data/lib/glueby/internal/wallet/active_record_wallet_adapter.rb +15 -5
  30. data/lib/glueby/internal/wallet/errors.rb +3 -0
  31. data/lib/glueby/internal/wallet/tapyrus_core_wallet_adapter.rb +36 -11
  32. data/lib/glueby/version.rb +1 -1
  33. data/lib/glueby/wallet.rb +3 -2
  34. data/lib/tasks/glueby/contract/timestamp.rake +1 -1
  35. data/lib/tasks/glueby/fee_provider.rake +13 -0
  36. metadata +16 -9
  37. data/.travis.yml +0 -7
  38. data/lib/glueby/contract/fee_provider.rb +0 -21
@@ -0,0 +1,136 @@
1
+ module Glueby
2
+ class FeeProvider
3
+ class Tasks
4
+ attr_reader :fee_provider
5
+
6
+ STATUS = {
7
+ # FeeProvider is ready to pay fees.
8
+ ready: 'Ready',
9
+ # FeeProvider is ready to pay fees, but it doesn't have enough amount to fill the UTXO pool by UTXOs which is for paying fees.
10
+ insufficient_amount: 'Insufficient Amount',
11
+ # FeeProvider is not ready to pay fees. It has no UTXOs for paying fee and amounts.
12
+ not_ready: 'Not Ready'
13
+ }
14
+
15
+ def initialize
16
+ @fee_provider = Glueby::FeeProvider.new
17
+ end
18
+
19
+ # Create UTXOs for paying fee from TPC amount of the wallet FeeProvider has. Then show the status.
20
+ #
21
+ # About the UTXO Pool
22
+ # FeeProvider have the UTXO pool. the pool is manged to keep some number of UTXOs that have fixed fee value. The
23
+ # value is configurable by :fixed_fee. This method do the management to the pool.
24
+ def manage_utxo_pool
25
+ txb = Tapyrus::TxBuilder.new
26
+
27
+ sum, utxos = collect_outputs
28
+ return if utxos.empty?
29
+
30
+ utxos.each { |utxo| txb.add_utxo(utxo) }
31
+ address = wallet.receive_address
32
+
33
+ shortage = [fee_provider.utxo_pool_size - current_utxo_pool_size, 0].max
34
+ can_create = (sum - fee_provider.fixed_fee) / fee_provider.fixed_fee
35
+ fee_outputs_count_to_be_created = [shortage, can_create].min
36
+
37
+ return if fee_outputs_count_to_be_created == 0
38
+
39
+ fee_outputs_count_to_be_created.times do
40
+ txb.pay(address, fee_provider.fixed_fee)
41
+ end
42
+
43
+ tx = txb.change_address(address)
44
+ .fee(fee_provider.fixed_fee)
45
+ .build
46
+ tx = wallet.sign_tx(tx)
47
+ wallet.broadcast(tx)
48
+ ensure
49
+ status
50
+ end
51
+
52
+ # Show the status of the UTXO pool
53
+ def status
54
+ status = :ready
55
+
56
+ if current_utxo_pool_size < fee_provider.utxo_pool_size
57
+ if tpc_amount < value_to_fill_utxo_pool
58
+ status = :insufficient_amount
59
+ message = <<~MESSAGE
60
+ 1. Please replenishment TPC which is for paying fee to FeeProvider.
61
+ FeeProvider needs #{value_to_fill_utxo_pool} tapyrus at least for paying 20 transaction fees.
62
+ FeeProvider wallet's address is '#{wallet.receive_address}'
63
+ 2. Then create UTXOs for paying in UTXO pool with 'rake glueby:fee_provider:manage_utxo_pool'
64
+ MESSAGE
65
+ else
66
+ message = "Please create UTXOs for paying in UTXO pool with 'rake glueby:fee_provider:manage_utxo_pool'\n"
67
+ end
68
+ end
69
+
70
+ status = :not_ready if current_utxo_pool_size == 0
71
+
72
+ puts <<~EOS
73
+ Status: #{STATUS[status]}
74
+ TPC amount: #{delimit(tpc_amount)}
75
+ UTXO pool size: #{delimit(current_utxo_pool_size)}
76
+ #{"\n" if message}#{message}
77
+ Configuration:
78
+ fixed_fee = #{delimit(fee_provider.fixed_fee)}
79
+ utxo_pool_size = #{delimit(fee_provider.utxo_pool_size)}
80
+ EOS
81
+ end
82
+
83
+ private
84
+
85
+ def check_wallet_amount!
86
+ if tpc_amount < fee_provider.fixed_fee
87
+ raise InsufficientTPC, <<~MESSAGE
88
+ FeeProvider has insufficient TPC to create fee outputs to fill the UTXO pool.
89
+ 1. Please replenishment TPC which is for paying fee to FeeProvider. FeeProvider needs #{fee_provider.utxo_pool_size * fee_provider.fixed_fee} tapyrus at least. FeeProvider wallet's address is '#{wallet.receive_address}'
90
+ 2. Then create UTXOs for paying in UTXO pool with 'rake glueby:fee_provider:manage_utxo_pool'
91
+ MESSAGE
92
+ end
93
+ end
94
+
95
+ def tpc_amount
96
+ wallet.balance(false)
97
+ end
98
+
99
+ def collect_outputs
100
+ wallet.list_unspent.inject([0, []]) do |sum, output|
101
+ next sum if output[:color_id] || output[:amount] == fee_provider.fixed_fee
102
+
103
+ new_sum = sum[0] + output[:amount]
104
+ new_outputs = sum[1] << {
105
+ txid: output[:txid],
106
+ script_pubkey: output[:script_pubkey],
107
+ value: output[:amount],
108
+ index: output[:vout] ,
109
+ finalized: output[:finalized]
110
+ }
111
+ return [new_sum, new_outputs] if new_sum >= value_to_fill_utxo_pool
112
+
113
+ [new_sum, new_outputs]
114
+ end
115
+ end
116
+
117
+ def current_utxo_pool_size
118
+ wallet
119
+ .list_unspent(false)
120
+ .count { |o| !o[:color_id] && o[:amount] == fee_provider.fixed_fee }
121
+ end
122
+
123
+ def value_to_fill_utxo_pool
124
+ fee_provider.fixed_fee * (fee_provider.utxo_pool_size + 1) # +1 is for paying fee
125
+ end
126
+
127
+ def wallet
128
+ fee_provider.wallet
129
+ end
130
+
131
+ def delimit(num)
132
+ num.to_s.reverse.scan(/.{1,3}/).join('_').reverse
133
+ end
134
+ end
135
+ end
136
+ end
@@ -28,7 +28,7 @@ module Glueby
28
28
 
29
29
  def table_options
30
30
  if mysql?
31
- ', { options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci" }'
31
+ ', :options => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci"'
32
32
  else
33
33
  ""
34
34
  end
@@ -36,8 +36,13 @@ module Glueby
36
36
  class << self
37
37
  attr_writer :wallet_adapter
38
38
 
39
- def create
40
- new(wallet_adapter.create_wallet)
39
+ def create(wallet_id = nil)
40
+ begin
41
+ wallet_id = wallet_adapter.create_wallet(wallet_id)
42
+ rescue Errors::WalletAlreadyCreated => _
43
+ # Ignore when wallet is already created.
44
+ end
45
+ new(wallet_id)
41
46
  end
42
47
 
43
48
  def load(wallet_id)
@@ -77,12 +82,27 @@ module Glueby
77
82
  wallet_adapter.delete_wallet(id)
78
83
  end
79
84
 
80
- def sign_tx(tx, prev_txs = [])
81
- wallet_adapter.sign_tx(id, tx, prev_txs)
85
+ # @param [Tapyrus::Tx] tx The tx that is signed
86
+ # @param [Array<Hash>] prev_txs An array of hash that represents unbroadcasted transaction outputs used by signing tx
87
+ # @option prev_txs [String] :txid
88
+ # @option prev_txs [Integer] :vout
89
+ # @option prev_txs [String] :scriptPubkey
90
+ # @option prev_txs [Integer] :amount
91
+ # @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.
92
+ def sign_tx(tx, prev_txs = [], for_fee_provider_input: false)
93
+ sighashtype = Tapyrus::SIGHASH_TYPE[:all]
94
+
95
+ if !for_fee_provider_input && Glueby.configuration.fee_provider_bears?
96
+ sighashtype |= Tapyrus::SIGHASH_TYPE[:anyonecanpay]
97
+ end
98
+
99
+ wallet_adapter.sign_tx(id, tx, prev_txs, sighashtype: sighashtype)
82
100
  end
83
101
 
84
102
  def broadcast(tx)
103
+ tx = FeeProvider.provide(tx) if Glueby.configuration.fee_provider_bears?
85
104
  wallet_adapter.broadcast(id, tx)
105
+ tx
86
106
  end
87
107
 
88
108
  def receive_address
@@ -112,6 +132,10 @@ module Glueby
112
132
  raise Glueby::Contract::Errors::InsufficientFunds
113
133
  end
114
134
 
135
+ def get_addresses
136
+ wallet_adapter.get_addresses(id)
137
+ end
138
+
115
139
  private
116
140
 
117
141
  def wallet_adapter
@@ -7,8 +7,10 @@ module Glueby
7
7
  class AbstractWalletAdapter
8
8
  # Creates a new wallet inside the wallet component and returns `wallet_id`. The created
9
9
  # wallet is loaded from at first.
10
+ # @params [String] wallet_id - Option. The wallet id that if for the wallet to be created. If this is nil, wallet adapter generates it.
10
11
  # @return [String] wallet_id
11
- def create_wallet
12
+ # @raise [Glueby::Internal::Wallet::Errors::WalletAlreadyCreated] when the specified wallet has been already created.
13
+ def create_wallet(wallet_id = nil)
12
14
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
13
15
  end
14
16
 
@@ -32,6 +34,7 @@ module Glueby
32
34
  #
33
35
  # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
34
36
  # @raise [Glueby::Internal::Wallet::Errors::WalletAlreadyLoaded] when the specified wallet has been already loaded.
37
+ # @raise [Glueby::Internal::Wallet::Errors::WalletNotFound] when the specified wallet is not found.
35
38
  def load_wallet(wallet_id)
36
39
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
37
40
  end
@@ -83,10 +86,12 @@ module Glueby
83
86
  #
84
87
  # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
85
88
  # @param [Tapyrus::Tx] tx - The transaction will be signed.
86
- # @param [Array] prevtxs array of hash that represents unbroadcasted transaction outputs used by signing tx.
89
+ # @param [Array] prevtxs - array of hash that represents unbroadcasted transaction outputs used by signing tx.
87
90
  # Each hash has `txid`, `vout`, `scriptPubKey`, `amount` fields.
91
+ # @param [Integer] sighashtype - The sighash flag for each signature that would be produced here.
88
92
  # @return [Tapyrus::Tx]
89
- def sign_tx(wallet_id, tx, prevtxs = [])
93
+ # @raise [Glueby::Internal::Wallet::Errors::InvalidSighashType] when the specified sighashtype is invalid
94
+ def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
90
95
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
91
96
  end
92
97
 
@@ -125,6 +130,16 @@ module Glueby
125
130
  def create_pubkey(wallet_id)
126
131
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
127
132
  end
133
+
134
+ # Returns an array of addresses
135
+ #
136
+ # This method is expected to return the list of addresses that wallet has.
137
+ #
138
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
139
+ # @return [Array<String>] array of P2PKH address
140
+ def get_addresses(wallet_id)
141
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
142
+ end
128
143
  end
129
144
  end
130
145
  end
@@ -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
@@ -54,9 +54,13 @@ 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
+ 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
60
64
  wallet_id
61
65
  end
62
66
 
@@ -65,6 +69,7 @@ module Glueby
65
69
  end
66
70
 
67
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?
68
73
  end
69
74
 
70
75
  def unload_wallet(wallet_id)
@@ -97,9 +102,9 @@ module Glueby
97
102
  end
98
103
  end
99
104
 
100
- def sign_tx(wallet_id, tx, prevtxs = [])
105
+ def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
101
106
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
102
- wallet.sign(tx, prevtxs)
107
+ wallet.sign(tx, prevtxs, sighashtype: sighashtype)
103
108
  end
104
109
 
105
110
  def broadcast(wallet_id, tx)
@@ -127,6 +132,11 @@ module Glueby
127
132
  key = wallet.keys.create(purpose: :receive)
128
133
  Tapyrus::Key.new(pubkey: key.public_key)
129
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
130
140
  end
131
141
  end
132
142
  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['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
+
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['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['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
@@ -95,9 +105,9 @@ module Glueby
95
105
  end
96
106
  end
97
107
 
98
- def sign_tx(wallet_id, tx, prevtxs = [])
108
+ def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
99
109
  perform_as(wallet_id) do |client|
100
- res = client.signrawtransactionwithwallet(tx.to_hex, prevtxs)
110
+ res = client.signrawtransactionwithwallet(tx.to_hex, prevtxs, encode_sighashtype(sighashtype))
101
111
  if res['complete']
102
112
  Tapyrus::Tx.parse_from_payload(res['hex'].htb)
103
113
  else
@@ -138,9 +148,8 @@ module Glueby
138
148
  RPC.perform_as(wallet_name(wallet_id)) do |client|
139
149
  begin
140
150
  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
151
+ rescue Tapyrus::RPC::Error => ex
152
+ if ex.rpc_error['code'] == RPC_WALLET_NOT_FOUND_ERROR_CODE
144
153
  raise Errors::WalletUnloaded, "The wallet #{wallet_id} is unloaded. You should load before use it."
145
154
  else
146
155
  raise ex
@@ -152,6 +161,22 @@ module Glueby
152
161
  def wallet_name(wallet_id)
153
162
  "#{WALLET_PREFIX}#{wallet_id}"
154
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
155
180
  end
156
181
  end
157
182
  end
@@ -1,3 +1,3 @@
1
1
  module Glueby
2
- VERSION = "0.3.0"
2
+ VERSION = "0.4.0"
3
3
  end