glueby 0.8.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2f53ca4e5784495dc9ddc7669bc021985614b3719205247c42005ec5d12a5c4
4
- data.tar.gz: d5bb0540f3db7cf5ccdb96d4487ff627c4090f69e8719f746badae31689f864d
3
+ metadata.gz: 53718ece4b629ff6474c139b047e08c4803739cb992b61aa4f49c10ca96bc90f
4
+ data.tar.gz: 774199828793ab8c00c3b5d3a501ffa5e265395e7e036407d7ef0edd7f798998
5
5
  SHA512:
6
- metadata.gz: 037ac4f566439facb0971a34c086bb128afd4b00566b7ab0f9013d4b4c357082c88a620eb8262221a245586429387228811e940077868b254b1ec82d155de8ab
7
- data.tar.gz: 9a031d6505750f550abe9948ff977cd01f4fbcad8ab9b733f0b322101cc77d9856c6d2e3f633e61c9e49b4657dc7f286db1abdd4df0511cedf96dba9c27a2d3d
6
+ metadata.gz: 89cb7a6e1a9573bd840811bf75da2d83f9a77eaf1d265e0ba617093e788eeb785f23d80ea67e934714f2773565d26065a4a3daa56bc8d544cc13d2bbd66f3049
7
+ data.tar.gz: d1616df8862ede1f1046119f321b22faf8dd7f6193d4a14ca7a38388d60c6f9e5ac97408480b4c38c2d817036424aeba9f2c50574c655b7ac7f9f3e0522a0cac
data/README.md CHANGED
@@ -457,6 +457,11 @@ Glueby.configure do |config|
457
457
  end
458
458
  ```
459
459
 
460
+ ## Error handling
461
+
462
+ Glueby has base error classes like `Glueby::Error` and `Glueby::ArgumentError`.
463
+ `Glueby::Error` is the base class for the all errors that are raises in the glueby.
464
+ `Glueby::ArgumentError` is the error class for argument errors in public contracts. This notifies the arguments is something wrong to glueby library user-side.
460
465
 
461
466
  ## Development
462
467
 
data/glueby.gemspec CHANGED
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
27
  spec.require_paths = ["lib"]
28
28
 
29
- spec.add_runtime_dependency 'tapyrus', '>= 0.2.9'
29
+ spec.add_runtime_dependency 'tapyrus', '>= 0.2.13'
30
30
  spec.add_runtime_dependency 'activerecord', '~> 6.1.3'
31
31
  spec.add_development_dependency 'sqlite3'
32
32
  spec.add_development_dependency 'rails', '~> 6.1.3'
@@ -9,6 +9,10 @@ class CreateTimestamp < ActiveRecord::Migration<%= migration_version %>
9
9
  t.integer :timestamp_type, null: false, default: 0
10
10
  t.string :p2c_address
11
11
  t.string :payment_base
12
+ t.bigint :prev_id
13
+ t.boolean :latest, null: false, default: true
12
14
  end
15
+
16
+ add_index :glueby_timestamps, [:prev_id], unique: true
13
17
  end
14
18
  end
@@ -12,22 +12,91 @@ module Glueby
12
12
  find_by(info_key: "use_only_finalized_utxo")&.int_value != 0
13
13
  end
14
14
 
15
+
16
+ # Set use_only_finalized_utxo
17
+ # @param [Boolean] status status of whether to use only finalized utxo
18
+ def self.set_use_only_finalized_utxo(status)
19
+ current = find_by(info_key: "use_only_finalized_utxo")
20
+ if current
21
+ current.update!(info_value: boolean_to_string(status))
22
+ else
23
+ create!(
24
+ info_key: "use_only_finalized_utxo",
25
+ info_value: boolean_to_string(status)
26
+ )
27
+ end
28
+ end
29
+
15
30
  # Return default value of the utxo provider
16
31
  # @return [Integer] default value of utxo provider
17
32
  def self.utxo_provider_default_value
18
33
  find_by(info_key: "utxo_provider_default_value")&.int_value
19
34
  end
20
35
 
36
+ # Set utxo_provider_default_value
37
+ # @param [Integer] value default value for utxo provider
38
+ def self.set_utxo_provider_default_value(value)
39
+ current = find_by(info_key: "utxo_provider_default_value")
40
+ if current
41
+ current.update!(info_value: value)
42
+ else
43
+ create!(
44
+ info_key: "utxo_provider_default_value",
45
+ info_value: value
46
+ )
47
+ end
48
+ end
49
+
21
50
  # Return pool size of the utxo provider
22
51
  # @return [Integer] pool size of utxo provider
23
52
  def self.utxo_provider_pool_size
24
53
  find_by(info_key: "utxo_provider_pool_size")&.int_value
25
54
  end
26
55
 
56
+ # Set utxo_provider_pool_size
57
+ # @param [Integer] size pool size of the utxo provider
58
+ def self.set_utxo_provider_pool_size(size)
59
+ current = find_by(info_key: "utxo_provider_pool_size")
60
+ if current
61
+ current.update!(info_value: size)
62
+ else
63
+ create!(
64
+ info_key: "utxo_provider_pool_size",
65
+ info_value: size
66
+ )
67
+ end
68
+ end
69
+
70
+ # If return timestamp is to be executed immediately
71
+ # @return [Boolean] true status of broadcast_on_background
72
+ def self.broadcast_on_background?
73
+ find_by(info_key: "broadcast_on_background")&.int_value != 0
74
+ end
75
+
76
+ # Set the status of broadcast_on_background
77
+ # @param [Boolean] status status of broadcast_on_background
78
+ def self.set_broadcast_on_background(status)
79
+ current = find_by(info_key: "broadcast_on_background")
80
+ if current
81
+ current.update!(info_value: boolean_to_string(status))
82
+ else
83
+ create!(
84
+ info_key: "broadcast_on_background",
85
+ info_value: boolean_to_string(status)
86
+ )
87
+ end
88
+ end
89
+
27
90
  def int_value
28
91
  info_value.to_i
29
92
  end
30
93
 
94
+ private
95
+
96
+ def self.boolean_to_string(status)
97
+ status ? "1" : "0"
98
+ end
99
+
31
100
  end
32
101
  end
33
102
  end
@@ -15,7 +15,7 @@ module Glueby
15
15
  alias_method :use_utxo_provider?, :use_utxo_provider
16
16
 
17
17
  module Errors
18
- class InvalidConfiguration < StandardError; end
18
+ class InvalidConfiguration < Error; end
19
19
  end
20
20
 
21
21
  def initialize
@@ -3,53 +3,179 @@ module Glueby
3
3
  module AR
4
4
  class Timestamp < ::ActiveRecord::Base
5
5
  include Glueby::GluebyLogger
6
- include Glueby::Contract::Timestamp::Util
7
- include Glueby::Contract::TxBuilder
8
6
 
9
7
  enum status: { init: 0, unconfirmed: 1, confirmed: 2 }
10
8
  enum timestamp_type: { simple: 0, trackable: 1 }
11
9
 
10
+ attr_reader :tx
11
+
12
+ belongs_to :prev, class_name: 'Glueby::Contract::AR::Timestamp'
13
+
14
+ class << self
15
+ def digest_content(content, digest)
16
+ case digest&.downcase
17
+ when :sha256
18
+ Tapyrus.sha256(content).bth
19
+ when :double_sha256
20
+ Tapyrus.double_sha256(content).bth
21
+ when :none
22
+ content
23
+ else
24
+ raise Glueby::Contract::Errors::UnsupportedDigestType
25
+ end
26
+ end
27
+ end
28
+
12
29
  # @param [Hash] attributes attributes which consist of:
13
30
  # - wallet_id
14
31
  # - content
15
32
  # - prefix(optional)
16
33
  # - timestamp_type(optional)
34
+ # @raise [Glueby::ArgumentError] If the timestamp_type is not in :simple or :trackable
17
35
  def initialize(attributes = nil)
18
- @content_hash = Tapyrus.sha256(attributes[:content]).bth
19
- super(wallet_id: attributes[:wallet_id], content_hash: @content_hash,
20
- prefix: attributes[:prefix] ? attributes[:prefix] : '', status: :init, timestamp_type: attributes[:timestamp_type] || :simple)
36
+ # Set content_hash from :content attribute
37
+ content_hash = Timestamp.digest_content(attributes[:content], attributes[:digest] || :sha256)
38
+ super(
39
+ wallet_id: attributes[:wallet_id],
40
+ content_hash: content_hash,
41
+ prefix: attributes[:prefix] ? attributes[:prefix] : '',
42
+ status: :init,
43
+ timestamp_type: attributes[:timestamp_type] || :simple,
44
+ prev_id: attributes[:prev_id]
45
+ )
46
+ rescue ::ArgumentError => e
47
+ raise Glueby::ArgumentError, e.message
21
48
  end
22
49
 
23
50
  # Return true if timestamp type is 'trackable' and output in timestamp transaction has not been spent yet, otherwise return false.
24
- def latest
25
- trackable?
51
+ def latest?
52
+ trackable? && attributes['latest']
53
+ end
54
+ alias_method :latest, :latest?
55
+
56
+ # Returns a UTXO that corresponds to the timestamp
57
+ # @return [Hash] UTXO
58
+ # - [String] script_pubkey A script pubkey hex string
59
+ # - [String] txid A txid
60
+ # - [Integer] vout An index of the tx
61
+ # - [Integer] amount A value of
62
+ def utxo
63
+ {
64
+ script_pubkey: Tapyrus::Script.parse_from_addr(p2c_address).to_hex,
65
+ txid: txid,
66
+ vout: Contract::Timestamp::TxBuilder::PAY_TO_CONTRACT_INPUT_INDEX,
67
+ amount: Glueby::Contract::Timestamp::P2C_DEFAULT_VALUE
68
+ }
26
69
  end
27
70
 
28
71
  # Broadcast and save timestamp
29
72
  # @param [Glueby::Contract::FixedFeeEstimator] fee_estimator
73
+ # @param [Glueby::UtxoProvider] utxo_provider
30
74
  # @return true if tapyrus transactions were broadcasted and the timestamp was updated successfully, otherwise false.
31
- def save_with_broadcast(fee_estimator: Glueby::Contract::FixedFeeEstimator.new)
32
- utxo_provider = Glueby::UtxoProvider.new if Glueby.configuration.use_utxo_provider?
33
- wallet = Glueby::Wallet.load(wallet_id)
34
- funding_tx, tx, p2c_address, payment_base = create_txs(wallet, prefix, content_hash, fee_estimator, utxo_provider, type: timestamp_type.to_sym)
75
+ def save_with_broadcast(fee_estimator: Glueby::Contract::FixedFeeEstimator.new, utxo_provider: nil)
76
+ save_with_broadcast!(fee_estimator: fee_estimator, utxo_provider: utxo_provider)
77
+ rescue Errors::FailedToBroadcast, Errors::PrevTimestampNotFound, Errors::PrevTimestampIsNotTrackable => e
78
+ logger.error("failed to broadcast (id=#{id}, reason=#{e.message})")
79
+ false
80
+ end
81
+
82
+ # Broadcast and save timestamp, and it raises errors
83
+ # @param [Glueby::Contract::FixedFeeEstimator] fee_estimator
84
+ # @param [Glueby::UtxoProvider] utxo_provider
85
+ # @return true if tapyrus transactions were broadcasted and the timestamp was updated successfully
86
+ # @raise [Glueby::Contract::Errors::FailedToBroadcast] If the broadcasting is failure
87
+ # @raise [Glueby::Contract::Errors::PrevTimestampNotFound] If it is not available that the timestamp record which correspond with the prev_id attribute
88
+ # @raise [Glueby::Contract::Errors::PrevTimestampIsNotTrackable] If the timestamp record by prev_id is not trackable
89
+ def save_with_broadcast!(fee_estimator: Glueby::Contract::FixedFeeEstimator.new, utxo_provider: nil)
90
+ utxo_provider = Glueby::UtxoProvider.new if !utxo_provider && Glueby.configuration.use_utxo_provider?
91
+
92
+ funding_tx, tx, p2c_address, payment_base = create_txs(fee_estimator, utxo_provider)
93
+
35
94
  if funding_tx
36
95
  ::ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
37
96
  wallet.internal_wallet.broadcast(funding_tx)
38
- logger.info("funding tx was broadcasted(id=#{id}, funding_tx.txid=#{funding_tx.txid})")
39
97
  end
98
+ logger.info("funding tx was broadcasted(id=#{id}, funding_tx.txid=#{funding_tx.txid})")
40
99
  end
41
100
  ::ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
42
101
  wallet.internal_wallet.broadcast(tx) do |tx|
43
102
  assign_attributes(txid: tx.txid, status: :unconfirmed, p2c_address: p2c_address, payment_base: payment_base)
103
+ @tx = tx
44
104
  save!
105
+
106
+ if update_trackable?
107
+ prev.latest = false
108
+ prev.save!
109
+ end
45
110
  end
46
- logger.info("timestamp tx was broadcasted (id=#{id}, txid=#{tx.txid})")
47
111
  end
112
+ logger.info("timestamp tx was broadcasted (id=#{id}, txid=#{tx.txid})")
48
113
  true
49
- rescue => e
50
- logger.error("failed to broadcast (id=#{id}, reason=#{e.message})")
114
+ rescue ActiveRecord::RecordInvalid,
115
+ Tapyrus::RPC::Error,
116
+ Internal::Wallet::Errors::WalletAlreadyLoaded,
117
+ Internal::Wallet::Errors::WalletNotFound,
118
+ Errors::InsufficientFunds => e
51
119
  errors.add(:base, "failed to broadcast (id=#{id}, reason=#{e.message})")
52
- false
120
+ raise Errors::FailedToBroadcast, "failed to broadcast (id=#{id}, reason=#{e.message})"
121
+ end
122
+
123
+ private
124
+
125
+ def wallet
126
+ @wallet ||= Glueby::Wallet.load(wallet_id)
127
+ end
128
+
129
+ def create_txs(fee_estimator, utxo_provider)
130
+ builder = builder_class.new(wallet, fee_estimator)
131
+
132
+ if builder.instance_of?(Contract::Timestamp::TxBuilder::UpdatingTrackable)
133
+ unless prev
134
+ message = "The previous timestamp(id: #{prev_id}) not found."
135
+ errors.add(:prev_id, message)
136
+ raise Errors::PrevTimestampNotFound, message
137
+ end
138
+
139
+ unless prev.trackable?
140
+ message = "The previous timestamp(id: #{prev_id}) type must be trackable"
141
+ errors.add(:prev_id, message)
142
+ raise Errors::PrevTimestampIsNotTrackable, message
143
+ end
144
+
145
+ builder.set_prev_timestamp_info(
146
+ timestamp_utxo: prev.utxo,
147
+ payment_base: prev.payment_base,
148
+ prefix: prev.prefix,
149
+ data: prev.content_hash
150
+ )
151
+ end
152
+
153
+ tx = builder.set_data(prefix, content_hash)
154
+ .set_inputs(utxo_provider)
155
+ .build
156
+
157
+ if builder.instance_of?(Contract::Timestamp::TxBuilder::Simple)
158
+ [builder.funding_tx, tx, nil, nil]
159
+ else
160
+ [builder.funding_tx, tx, builder.p2c_address, builder.payment_base]
161
+ end
162
+ end
163
+
164
+ def builder_class
165
+ case timestamp_type.to_sym
166
+ when :simple
167
+ Contract::Timestamp::TxBuilder::Simple
168
+ when :trackable
169
+ if prev_id
170
+ Contract::Timestamp::TxBuilder::UpdatingTrackable
171
+ else
172
+ Contract::Timestamp::TxBuilder::Trackable
173
+ end
174
+ end
175
+ end
176
+
177
+ def update_trackable?
178
+ trackable? && prev_id
53
179
  end
54
180
  end
55
181
  end
@@ -1,16 +1,22 @@
1
1
  module Glueby
2
2
  module Contract
3
3
  module Errors
4
- class InsufficientFunds < StandardError; end
5
- class InsufficientTokens < StandardError; end
6
- class InvalidAmount < StandardError; end
7
- class InvalidSplit < StandardError; end
8
- class InvalidTokenType < StandardError; end
9
- class InvalidTimestampType < StandardError; end
10
- class TxAlreadyBroadcasted < StandardError; end
11
- class UnsupportedTokenType < StandardError; end
12
- class UnknownScriptPubkey < StandardError; end
13
- class UnsupportedDigestType < StandardError; end
4
+ class InsufficientFunds < Error; end
5
+ class InsufficientTokens < Error; end
6
+ class TxAlreadyBroadcasted < Error; end
7
+ class FailedToBroadcast < Error; end
8
+
9
+ # Argument Errors
10
+ class ArgumentError < ArgumentError; end
11
+ class InvalidAmount < ArgumentError; end
12
+ class InvalidSplit < ArgumentError; end
13
+ class InvalidTokenType < ArgumentError; end
14
+ class InvalidTimestampType < ArgumentError; end
15
+ class UnsupportedTokenType < ArgumentError; end
16
+ class UnknownScriptPubkey < ArgumentError; end
17
+ class UnsupportedDigestType < ArgumentError; end
18
+ class PrevTimestampNotFound < ArgumentError; end
19
+ class PrevTimestampIsNotTrackable < ArgumentError; end
14
20
  end
15
21
  end
16
22
  end
@@ -0,0 +1,97 @@
1
+ module Glueby
2
+ module Contract
3
+ class Timestamp
4
+ module TxBuilder
5
+ # The simple Timestamp method
6
+ class Simple
7
+ include Glueby::Contract::TxBuilder
8
+
9
+ attr_reader :funding_tx
10
+
11
+ def initialize(wallet, fee_estimator)
12
+ @wallet = wallet
13
+ @fee_estimator = fee_estimator
14
+
15
+ @txb = Tapyrus::TxBuilder.new
16
+ @prev_txs = []
17
+ end
18
+
19
+ def build
20
+ @txb.fee(fee).change_address(@wallet.internal_wallet.change_address)
21
+ sign_tx
22
+ end
23
+
24
+ def set_data(prefix, data)
25
+ @prefix = prefix
26
+ @data = data
27
+
28
+ @txb.data(prefix + data)
29
+ self
30
+ end
31
+
32
+ def set_inputs(utxo_provider)
33
+ if utxo_provider
34
+ script_pubkey = Tapyrus::Script.parse_from_addr(@wallet.internal_wallet.receive_address)
35
+ @funding_tx, index = utxo_provider.get_utxo(script_pubkey, fee)
36
+
37
+ utxo = {
38
+ script_pubkey: @funding_tx.outputs[index].script_pubkey.to_hex,
39
+ txid: @funding_tx.txid,
40
+ vout: index,
41
+ amount: funding_tx.outputs[index].value
42
+ }
43
+
44
+ @txb.add_utxo(to_tapyrusrb_utxo_hash(utxo))
45
+ @prev_txs << to_sign_tx_utxo_hash(utxo)
46
+ else
47
+ _, outputs = @wallet.internal_wallet.collect_uncolored_outputs(fee)
48
+ outputs.each do |utxo|
49
+ @txb.add_utxo(to_tapyrusrb_utxo_hash(utxo))
50
+ end
51
+ end
52
+ self
53
+ end
54
+
55
+ private
56
+
57
+ def fee
58
+ @fee ||= @fee_estimator.fee(dummy_tx(@txb.build))
59
+ end
60
+
61
+ def sign_tx
62
+ # General signing process skips signing to p2c inputs because no key of the p2c address in the wallet.
63
+ @wallet.internal_wallet.sign_tx(@txb.build, @prev_txs)
64
+ end
65
+
66
+ # @param utxo
67
+ # @option utxo [String] :txid The txid
68
+ # @option utxo [Integer] :vout The index of the output in the tx
69
+ # @option utxo [Integer] :amount The value of the output
70
+ # @option utxo [String] :script_pubkey The hex string of the script pubkey
71
+ def to_tapyrusrb_utxo_hash(utxo)
72
+ {
73
+ script_pubkey: Tapyrus::Script.parse_from_payload(utxo[:script_pubkey].htb),
74
+ txid: utxo[:txid],
75
+ index: utxo[:vout],
76
+ value: utxo[:amount]
77
+ }
78
+ end
79
+
80
+ # @param utxo
81
+ # @option utxo [String] :txid The txid
82
+ # @option utxo [Integer] :vout The index of the output in the tx
83
+ # @option utxo [Integer] :amount The value of the output
84
+ # @option utxo [String] :script_pubkey The hex string of the script pubkey
85
+ def to_sign_tx_utxo_hash(utxo)
86
+ {
87
+ scriptPubKey: utxo[:script_pubkey],
88
+ txid: utxo[:txid],
89
+ vout: utxo[:vout],
90
+ amount: utxo[:amount]
91
+ }
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,23 @@
1
+ module Glueby
2
+ module Contract
3
+ class Timestamp
4
+ module TxBuilder
5
+ class Trackable < Simple
6
+ attr_reader :p2c_address, :payment_base
7
+
8
+ # @override
9
+ def set_data(prefix, data)
10
+ @prefix = prefix
11
+ @data = data
12
+
13
+ # Create a new trackable timestamp
14
+ @p2c_address, @payment_base = @wallet.internal_wallet
15
+ .create_pay_to_contract_address([prefix, data].join)
16
+ @txb.pay(p2c_address, P2C_DEFAULT_VALUE)
17
+ self
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ module Glueby
2
+ module Contract
3
+ class Timestamp
4
+ module TxBuilder
5
+ class UpdatingTrackable < Trackable
6
+ def set_prev_timestamp_info(timestamp_utxo:, payment_base:, prefix:, data:)
7
+ @prev_timestamp_utxo = timestamp_utxo
8
+ @prev_payment_base = payment_base
9
+ @prev_prefix = prefix
10
+ @prev_data = data
11
+ @txb.add_utxo(to_tapyrusrb_utxo_hash(@prev_timestamp_utxo))
12
+ end
13
+
14
+ def sign_tx
15
+ tx = super
16
+ # Generates signature for the remain p2c input.
17
+ @wallet.internal_wallet.sign_to_pay_to_contract_address(
18
+ tx,
19
+ @prev_timestamp_utxo,
20
+ @prev_payment_base,
21
+ [@prev_prefix, @prev_data].join
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ module Glueby
2
+ module Contract
3
+ class Timestamp
4
+ module TxBuilder
5
+ autoload :Simple, 'glueby/contract/timestamp/tx_builder/simple'
6
+ autoload :Trackable, 'glueby/contract/timestamp/tx_builder/trackable'
7
+ autoload :UpdatingTrackable, 'glueby/contract/timestamp/tx_builder/updating_trackable'
8
+
9
+ # A transaction input to update trackable timestamp must placed at this index in the tx inputs.
10
+ PAY_TO_CONTRACT_INPUT_INDEX = 0
11
+ end
12
+ end
13
+ end
14
+ end
@@ -8,87 +8,16 @@ module Glueby
8
8
  #
9
9
  # Storing timestamp transaction to the blockchain enables everyone to verify that the data existed at that time and a user signed it.
10
10
  class Timestamp
11
- include Glueby::Contract::TxBuilder
12
-
13
11
  P2C_DEFAULT_VALUE = 1_000
14
12
 
15
13
  autoload :Syncer, 'glueby/contract/timestamp/syncer'
16
-
17
- module Util
18
- include Glueby::Internal::Wallet::TapyrusCoreWalletAdapter::Util
19
- module_function
20
-
21
- # @param [Glueby::Wallet] wallet
22
- # @param [Array] contents The data to be used for generating pay-to-contract address
23
- # @return [pay-to-contract address, public key used for generating address]
24
- def create_pay_to_contract_address(wallet, contents: nil)
25
- pubkey = wallet.internal_wallet.create_pubkey.pubkey
26
- # Calculate P + H(P || contents)G
27
- group = ECDSA::Group::Secp256k1
28
- p = Tapyrus::Key.new(pubkey: pubkey).to_point # P
29
- commitment = Tapyrus.sha256(p.to_hex(true).htb + contents.join).bth.to_i(16) % group.order # H(P || contents)
30
- point = p + group.generator.multiply_by_scalar(commitment) # P + H(P || contents)G
31
- [Tapyrus::Key.new(pubkey: point.to_hex(true)).to_p2pkh, pubkey] # [p2c address, P]
32
- end
33
-
34
- def create_txs(wallet, prefix, data, fee_estimator, utxo_provider, type: :simple)
35
- txb = Tapyrus::TxBuilder.new
36
- if type == :simple
37
- txb.data(prefix + data)
38
- elsif type == :trackable
39
- p2c_address, payment_base = create_pay_to_contract_address(wallet, contents: [prefix, data])
40
- txb.pay(p2c_address, P2C_DEFAULT_VALUE)
41
- end
42
-
43
- fee = fee_estimator.fee(dummy_tx(txb.build))
44
- if utxo_provider
45
- script_pubkey = Tapyrus::Script.parse_from_addr(wallet.internal_wallet.receive_address)
46
- funding_tx, index = utxo_provider.get_utxo(script_pubkey, fee)
47
- txb.add_utxo({
48
- script_pubkey: funding_tx.outputs[index].script_pubkey,
49
- txid: funding_tx.txid,
50
- index: index,
51
- value: funding_tx.outputs[index].value
52
- })
53
- else
54
- sum, outputs = wallet.internal_wallet.collect_uncolored_outputs(fee)
55
- outputs.each do |utxo|
56
- txb.add_utxo({
57
- script_pubkey: Tapyrus::Script.parse_from_payload(utxo[:script_pubkey].htb),
58
- txid: utxo[:txid],
59
- index: utxo[:vout],
60
- value: utxo[:amount]
61
- })
62
- end
63
- end
64
-
65
- prev_txs = if funding_tx
66
- output = funding_tx.outputs.first
67
- [{
68
- txid: funding_tx.txid,
69
- vout: 0,
70
- scriptPubKey: output.script_pubkey.to_hex,
71
- amount: output.value
72
- }]
73
- else
74
- []
75
- end
76
-
77
- txb.fee(fee).change_address(wallet.internal_wallet.change_address)
78
- [funding_tx, wallet.internal_wallet.sign_tx(txb.build, prev_txs), p2c_address, payment_base]
79
- end
80
-
81
- def get_transaction(tx)
82
- Glueby::Internal::RPC.client.getrawtransaction(tx.txid, 1)
83
- end
84
- end
85
- include Glueby::Contract::Timestamp::Util
14
+ autoload :TxBuilder, 'glueby/contract/timestamp/tx_builder'
86
15
 
87
16
  attr_reader :tx, :txid
88
-
89
17
  # p2c_address and payment_base is used in `trackable` type
90
18
  attr_reader :p2c_address, :payment_base
91
19
 
20
+ # @param [Gleuby::Wallet] wallet The wallet that is sender of the timestamp transaction.
92
21
  # @param [String] content Data to be hashed and stored in blockchain.
93
22
  # @param [String] prefix prefix of op_return data
94
23
  # @param [Glueby::Contract::FeeEstimator] fee_estimator
@@ -99,6 +28,7 @@ module Glueby
99
28
  # @param [Symbol] timestamp_type
100
29
  # - :simple
101
30
  # - :trackable
31
+ # @param [Integer] prev_timestamp_id The id column of glueby_timestamps that will be updated by the timestamp that will be created
102
32
  # @raise [Glueby::Contract::Errors::UnsupportedDigestType] if digest is unsupported
103
33
  # @raise [Glueby::Contract::Errors::InvalidTimestampType] if timestamp_type is unsupported
104
34
  def initialize(
@@ -108,7 +38,8 @@ module Glueby
108
38
  fee_estimator: Glueby::Contract::FixedFeeEstimator.new,
109
39
  digest: :sha256,
110
40
  utxo_provider: nil,
111
- timestamp_type: :simple
41
+ timestamp_type: :simple,
42
+ prev_timestamp_id: nil
112
43
  )
113
44
  @wallet = wallet
114
45
  @content = content
@@ -119,33 +50,46 @@ module Glueby
119
50
  @utxo_provider = utxo_provider
120
51
  raise Glueby::Contract::Errors::InvalidTimestampType, "#{timestamp_type} is invalid type, supported types are :simple, and :trackable." unless [:simple, :trackable].include?(timestamp_type)
121
52
  @timestamp_type = timestamp_type
53
+ @prev_timestamp_id = prev_timestamp_id
122
54
  end
123
55
 
124
56
  # broadcast to Tapyrus Core
125
57
  # @return [String] txid
126
- # @raise [TxAlreadyBroadcasted] if tx has been broadcasted.
127
- # @raise [InsufficientFunds] if result of listunspent is not enough to pay the specified amount
58
+ # @raise [Glueby::Contract::Errors::TxAlreadyBroadcasted] if tx has been broadcasted.
59
+ # @raise [Glueby::Contract::Errors::InsufficientFunds] if result of listunspent is not enough to pay the specified amount
128
60
  def save!
129
- raise Glueby::Contract::Errors::TxAlreadyBroadcasted if @txid
61
+ raise Glueby::Contract::Errors::TxAlreadyBroadcasted if @ar
62
+
63
+ @ar = Glueby::Contract::AR::Timestamp.new(
64
+ wallet_id: @wallet.id,
65
+ prefix: @prefix,
66
+ content: @content,
67
+ timestamp_type: @timestamp_type,
68
+ digest: @digest,
69
+ prev_id: @prev_timestamp_id
70
+ )
71
+ @ar.save_with_broadcast!(fee_estimator: @fee_estimator, utxo_provider: @utxo_provider)
72
+ @ar.txid
73
+ end
130
74
 
131
- funding_tx, @tx, @p2c_address, @payment_base = create_txs(@wallet, @prefix, digest_content, @fee_estimator, @utxo_provider, type: @timestamp_type)
132
- @wallet.internal_wallet.broadcast(funding_tx) if funding_tx
133
- @txid = @wallet.internal_wallet.broadcast(@tx)
75
+ def txid
76
+ raise Glueby::Error, 'The timestamp tx is not broadcasted yet.' unless @ar
77
+ @ar.txid
134
78
  end
135
79
 
136
- private
80
+ def tx
81
+ raise Glueby::Error, 'The timestamp tx is not broadcasted yet.' unless @ar
82
+ @ar.tx
83
+ end
84
+
85
+ def p2c_address
86
+ raise Glueby::Error, 'The timestamp tx is not broadcasted yet.' unless @ar
87
+ @ar.p2c_address
88
+ end
137
89
 
138
- def digest_content
139
- case @digest&.downcase
140
- when :sha256
141
- Tapyrus.sha256(@content)
142
- when :double_sha256
143
- Tapyrus.double_sha256(@content)
144
- when :none
145
- @content
146
- else
147
- raise Glueby::Contract::Errors::UnsupportedDigestType
148
- end
90
+ def payment_base
91
+ raise Glueby::Error, 'The timestamp tx is not broadcasted yet.' unless @ar
92
+ @ar.payment_base
149
93
  end
150
94
  end
151
95
  end
@@ -91,11 +91,14 @@ module Glueby
91
91
  script_pubkey = funding_tx.outputs.first.script_pubkey
92
92
  color_id = Tapyrus::Color::ColorIdentifier.reissuable(script_pubkey)
93
93
 
94
+ ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
95
+ funding_tx = issuer.internal_wallet.broadcast(funding_tx)
96
+ end
97
+
94
98
  ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
95
99
  # Store the script_pubkey for reissue the token.
96
100
  Glueby::Contract::AR::ReissuableToken.create!(color_id: color_id.to_hex, script_pubkey: script_pubkey.to_hex)
97
101
 
98
- funding_tx = issuer.internal_wallet.broadcast(funding_tx)
99
102
  tx = create_issue_tx_for_reissuable_token(funding_tx: funding_tx, issuer: issuer, amount: amount, split: split)
100
103
  tx = issuer.internal_wallet.broadcast(tx)
101
104
  [[funding_tx, tx], color_id]
@@ -8,6 +8,5 @@ module Glueby
8
8
  autoload :Token, 'glueby/contract/token'
9
9
  autoload :TxBuilder, 'glueby/contract/tx_builder'
10
10
  autoload :AR, 'glueby/contract/active_record'
11
- autoload :Wallet, 'glueby/contract/wallet'
12
11
  end
13
12
  end
@@ -3,7 +3,7 @@ module Glueby
3
3
 
4
4
  autoload :Tasks, 'glueby/fee_provider/tasks'
5
5
 
6
- class NoUtxosInUtxoPool < StandardError; end
6
+ class NoUtxosInUtxoPool < Error; end
7
7
 
8
8
  WALLET_ID = 'FEE_PROVIDER_WALLET'
9
9
  DEFAULT_FIXED_FEE = 1000
@@ -42,7 +42,7 @@ module Glueby
42
42
  # Provide an input for fee to the tx.
43
43
  # @param [Tapyrus::Tx] tx - The tx that is provided fee as a input. It should be signed with ANYONECANPAY flag.
44
44
  # @return [Tapyrus::Tx]
45
- # @raise [ArgumentError] If the signatures that the tx inputs has don't have ANYONECANPAY flag.
45
+ # @raise [Glueby::ArgumentError] If the signatures that the tx inputs has don't have ANYONECANPAY flag.
46
46
  # @raise [Glueby::FeeProvider::NoUtxosInUtxoPool] If there are no UTXOs for paying fee in FeeProvider's UTXO pool
47
47
  def provide(tx)
48
48
  tx.inputs.each do |txin|
@@ -148,6 +148,31 @@ module Glueby
148
148
  def get_addresses(wallet_id, label = nil)
149
149
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
150
150
  end
151
+
152
+ # Create and returns pay to contract address
153
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
154
+ # @param [String] contents - The data to be used for generating pay-to-contract address
155
+ # @return [String] pay to contract P2PKH address
156
+ def create_pay_to_contract_address(wallet_id, contents)
157
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
158
+ end
159
+
160
+ # Sign to the pay to contract input
161
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
162
+ # @param [Tapyrus::Tx] tx - The tx that has pay to contract input in the inputs list
163
+ # @param [Hash] utxo - The utxo that indicates pay to contract output to be signed
164
+ # @option utxo [String] txid - Transaction id
165
+ # @option utxo [Integer] vout - Output index
166
+ # @option utxo [Integer] amount - The amount the output has
167
+ # @option utxo [String] script_pubkey - The script_pubkey hex string
168
+ # @param [String] payment_base - The public key that is used to generate pay to contract public key
169
+ # @param [String] contents - The data to be used for generating pay-to-contract address
170
+ # @param [Integer] sighashtype - The sighash flag for each signature that would be produced here.
171
+ # @return [Tapyrus::Tx]
172
+ # @raise [Glueby::Internal::Wallet::Errors::InvalidSighashType] when the specified sighashtype is invalid
173
+ def sign_to_pay_to_contract_address(wallet_id, tx, utxo, payment_base, contents, sighashtype: Tapyrus::SIGHASH_TYPE[:all])
174
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
175
+ end
151
176
  end
152
177
  end
153
178
  end
@@ -153,6 +153,51 @@ module Glueby
153
153
  keys = keys.where(label: label) if label
154
154
  keys.map(&:address)
155
155
  end
156
+
157
+ def create_pay_to_contract_address(wallet_id, contents)
158
+ # Calculate P + H(P || contents)G
159
+ group = ECDSA::Group::Secp256k1
160
+ pubkey = create_pubkey(wallet_id)
161
+ p = pubkey.to_point # P
162
+ commitment = create_pay_to_contract_commitment(pubkey, contents)
163
+ point = p + group.generator.multiply_by_scalar(commitment) # P + H(P || contents)G
164
+ [Tapyrus::Key.new(pubkey: point.to_hex(true)).to_p2pkh, pubkey.pubkey] # [p2c address, P]
165
+ end
166
+
167
+ def sign_to_pay_to_contract_address(wallet_id, tx, utxo, payment_base, contents)
168
+ key = create_pay_to_contract_private_key(wallet_id, payment_base, contents)
169
+ sighash = tx.sighash_for_input(utxo[:vout], Tapyrus::Script.parse_from_payload(utxo[:script_pubkey].htb))
170
+
171
+ sig = key.sign(sighash, algo: :schnorr) + [Tapyrus::SIGHASH_TYPE[:all]].pack('C')
172
+ script_sig = Tapyrus::Script.parse_from_payload(Tapyrus::Script.pack_pushdata(sig) + Tapyrus::Script.pack_pushdata(key.pubkey.htb))
173
+ tx.inputs[utxo[:vout]].script_sig = script_sig
174
+ tx
175
+ end
176
+
177
+ private
178
+
179
+ # Calculate commitment = H(P || contents)
180
+ # @param [Tapyrus::Key] pubkey The public key
181
+ # @param [String] contents
182
+ # @return Integer
183
+ def create_pay_to_contract_commitment(pubkey, contents)
184
+ group = ECDSA::Group::Secp256k1
185
+ p = pubkey.to_point # P
186
+ Tapyrus.sha256(p.to_hex(true).htb + contents).bth.to_i(16) % group.order # H(P || contents)
187
+ end
188
+
189
+ # @param [String] wallet_id
190
+ # @param [String] payment_base The public key hex string
191
+ # @param [String] contents
192
+ # @return [Tapyrus::Key] pay to contract private key
193
+ def create_pay_to_contract_private_key(wallet_id, payment_base, contents)
194
+ group = ECDSA::Group::Secp256k1
195
+ wallet = AR::Wallet.find_by(wallet_id: wallet_id)
196
+ ar_key = wallet.keys.where(public_key: payment_base).first
197
+ key = Tapyrus::Key.new(pubkey: payment_base)
198
+ commitment = create_pay_to_contract_commitment(key, contents)
199
+ Tapyrus::Key.new(priv_key: ((ar_key.private_key.to_i(16) + commitment) % group.order).to_even_length_hex) # K + commitment
200
+ end
156
201
  end
157
202
  end
158
203
  end
@@ -2,12 +2,12 @@ module Glueby
2
2
  module Internal
3
3
  class Wallet
4
4
  module Errors
5
- class ShouldInitializeWalletAdapter < StandardError; end
6
- class WalletUnloaded < StandardError; end
7
- class WalletAlreadyLoaded < StandardError; end
8
- class WalletAlreadyCreated < StandardError; end
9
- class WalletNotFound < StandardError; end
10
- class InvalidSighashType < StandardError; end
5
+ class ShouldInitializeWalletAdapter < Error; end
6
+ class WalletUnloaded < Error; end
7
+ class WalletAlreadyLoaded < Error; end
8
+ class WalletAlreadyCreated < Error; end
9
+ class WalletNotFound < Error; end
10
+ class InvalidSighashType < Error; end
11
11
  end
12
12
  end
13
13
  end
@@ -154,6 +154,14 @@ module Glueby
154
154
  wallet_adapter.get_addresses(id, label)
155
155
  end
156
156
 
157
+ def create_pay_to_contract_address(contents)
158
+ wallet_adapter.create_pay_to_contract_address(id, contents)
159
+ end
160
+
161
+ def sign_to_pay_to_contract_address(tx, utxo, payment_base, contents)
162
+ wallet_adapter.sign_to_pay_to_contract_address(id, tx, utxo, payment_base, contents)
163
+ end
164
+
157
165
  private
158
166
 
159
167
  def wallet_adapter
@@ -30,14 +30,14 @@ module Glueby
30
30
 
31
31
  utxos.each { |utxo| txb.add_utxo(utxo) }
32
32
 
33
- shortage = [utxo_provider.utxo_pool_size - current_utxo_pool_size, 0].max
33
+ shortage = [utxo_provider.utxo_pool_size - utxo_provider.current_utxo_pool_size, 0].max
34
34
  return if shortage == 0
35
35
 
36
36
  added_outputs = 0
37
37
  shortage.times do
38
38
  fee = utxo_provider.fee_estimator.fee(dummy_tx(txb.build))
39
39
  break if (sum - fee) < utxo_provider.default_value
40
- txb.pay(address, utxo_provider.default_value)
40
+ txb.pay(utxo_provider.address, utxo_provider.default_value)
41
41
  sum -= utxo_provider.default_value
42
42
  added_outputs += 1
43
43
  end
@@ -45,11 +45,11 @@ module Glueby
45
45
  return if added_outputs == 0
46
46
 
47
47
  fee = utxo_provider.fee_estimator.fee(dummy_tx(txb.build))
48
- tx = txb.change_address(address)
48
+ tx = txb.change_address(utxo_provider.address)
49
49
  .fee(fee)
50
50
  .build
51
- tx = wallet.sign_tx(tx)
52
- wallet.broadcast(tx)
51
+ tx = utxo_provider.wallet.sign_tx(tx)
52
+ utxo_provider.wallet.broadcast(tx)
53
53
  ensure
54
54
  status
55
55
  end
@@ -58,13 +58,13 @@ module Glueby
58
58
  def status
59
59
  status = :ready
60
60
 
61
- if current_utxo_pool_size < utxo_provider.utxo_pool_size
62
- if tpc_amount < value_to_fill_utxo_pool
61
+ if utxo_provider.current_utxo_pool_size < utxo_provider.utxo_pool_size
62
+ if utxo_provider.tpc_amount < utxo_provider.value_to_fill_utxo_pool
63
63
  status = :insufficient_amount
64
64
  message = <<~MESSAGE
65
65
  1. Please replenishment TPC which is for paying tpc to UtxoProvider.
66
- UtxoProvider needs #{value_to_fill_utxo_pool} tapyrus in UTXO pool.
67
- UtxoProvider wallet's address is '#{address}'
66
+ UtxoProvider needs #{utxo_provider.value_to_fill_utxo_pool} tapyrus in UTXO pool.
67
+ UtxoProvider wallet's address is '#{utxo_provider.address}'
68
68
  2. Then create UTXOs for paying in UTXO pool with 'rake glueby:utxo_provider:manage_utxo_pool'
69
69
  MESSAGE
70
70
  else
@@ -72,12 +72,12 @@ module Glueby
72
72
  end
73
73
  end
74
74
 
75
- status = :not_ready if current_utxo_pool_size == 0
75
+ status = :not_ready if utxo_provider.current_utxo_pool_size == 0
76
76
 
77
77
  puts <<~EOS
78
78
  Status: #{STATUS[status]}
79
- TPC amount: #{delimit(tpc_amount)}
80
- UTXO pool size: #{delimit(current_utxo_pool_size)}
79
+ TPC amount: #{delimit(utxo_provider.tpc_amount)}
80
+ UTXO pool size: #{delimit(utxo_provider.current_utxo_pool_size)}
81
81
  #{"\n" if message}#{message}
82
82
  Configuration:
83
83
  default_value = #{delimit(utxo_provider.default_value)}
@@ -87,17 +87,13 @@ module Glueby
87
87
 
88
88
  # Show the address of Utxo Provider
89
89
  def print_address
90
- puts address
90
+ puts utxo_provider.address
91
91
  end
92
92
 
93
93
  private
94
94
 
95
- def tpc_amount
96
- wallet.balance(false)
97
- end
98
-
99
95
  def collect_outputs
100
- wallet.list_unspent.inject([0, []]) do |sum, output|
96
+ utxo_provider.wallet.list_unspent.inject([0, []]) do |sum, output|
101
97
  next sum if output[:color_id] || output[:amount] == utxo_provider.default_value
102
98
 
103
99
  new_sum = sum[0] + output[:amount]
@@ -105,34 +101,16 @@ module Glueby
105
101
  txid: output[:txid],
106
102
  script_pubkey: output[:script_pubkey],
107
103
  value: output[:amount],
108
- index: output[:vout] ,
104
+ index: output[:vout],
109
105
  finalized: output[:finalized]
110
106
  }
111
107
  [new_sum, new_outputs]
112
108
  end
113
109
  end
114
110
 
115
- def current_utxo_pool_size
116
- wallet
117
- .list_unspent(false)
118
- .count { |o| !o[:color_id] && o[:amount] == utxo_provider.default_value }
119
- end
120
-
121
- def value_to_fill_utxo_pool
122
- utxo_provider.default_value * utxo_provider.utxo_pool_size
123
- end
124
-
125
- def wallet
126
- utxo_provider.wallet
127
- end
128
-
129
111
  def delimit(num)
130
112
  num.to_s.reverse.scan(/.{1,3}/).join('_').reverse
131
113
  end
132
-
133
- def address
134
- @address ||= wallet.get_addresses.first || wallet.receive_address
135
- end
136
114
  end
137
115
  end
138
116
  end
@@ -27,7 +27,7 @@ module Glueby
27
27
  @fee_estimator = (UtxoProvider.config && UtxoProvider.config[:fee_estimator]) || Glueby::Contract::FixedFeeEstimator.new
28
28
  end
29
29
 
30
- attr_reader :wallet, :fee_estimator
30
+ attr_reader :wallet, :fee_estimator, :address
31
31
 
32
32
  # Provide a UTXO
33
33
  # @param [Tapyrus::Script] script_pubkey The script to be provided
@@ -78,6 +78,24 @@ module Glueby
78
78
  )
79
79
  end
80
80
 
81
+ def tpc_amount
82
+ wallet.balance(false)
83
+ end
84
+
85
+ def current_utxo_pool_size
86
+ wallet
87
+ .list_unspent(false)
88
+ .count { |o| !o[:color_id] && o[:amount] == default_value }
89
+ end
90
+
91
+ def address
92
+ @address ||= wallet.get_addresses.first || wallet.receive_address
93
+ end
94
+
95
+ def value_to_fill_utxo_pool
96
+ default_value * utxo_pool_size
97
+ end
98
+
81
99
  private
82
100
 
83
101
  # Create wallet for provider
@@ -1,3 +1,3 @@
1
1
  module Glueby
2
- VERSION = "0.8.0"
2
+ VERSION = "0.10.0"
3
3
  end
data/lib/glueby.rb CHANGED
@@ -48,4 +48,9 @@ module Glueby
48
48
  def self.configure
49
49
  yield configuration if block_given?
50
50
  end
51
+
52
+ # Base error classes. These error classes must be used as a super class in all error classes that is defined and
53
+ # raised in glueby library.
54
+ class Error < StandardError; end
55
+ class ArgumentError < Error; end
51
56
  end
@@ -4,7 +4,6 @@ module Glueby
4
4
  module Timestamp
5
5
  module_function
6
6
  extend Glueby::Contract::TxBuilder
7
- extend Glueby::Contract::Timestamp::Util
8
7
 
9
8
  def create
10
9
  timestamps = Glueby::Contract::AR::Timestamp.where(status: :init)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glueby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-28 00:00:00.000000000 Z
11
+ date: 2022-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tapyrus
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.2.9
19
+ version: 0.2.13
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.2.9
26
+ version: 0.2.13
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -113,6 +113,10 @@ files:
113
113
  - lib/glueby/contract/payment.rb
114
114
  - lib/glueby/contract/timestamp.rb
115
115
  - lib/glueby/contract/timestamp/syncer.rb
116
+ - lib/glueby/contract/timestamp/tx_builder.rb
117
+ - lib/glueby/contract/timestamp/tx_builder/simple.rb
118
+ - lib/glueby/contract/timestamp/tx_builder/trackable.rb
119
+ - lib/glueby/contract/timestamp/tx_builder/updating_trackable.rb
116
120
  - lib/glueby/contract/token.rb
117
121
  - lib/glueby/contract/tx_builder.rb
118
122
  - lib/glueby/fee_provider.rb