glueby 0.3.0 → 0.4.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.
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