xrpl-ruby 0.7.0 → 0.8.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.
data/lib/xrpl/client.rb CHANGED
@@ -52,13 +52,18 @@ module XRPL
52
52
  # @param logger [Logger, nil] optional logger for diagnostic messages. When nil
53
53
  # (the default), the client stays silent — a library must not write to the
54
54
  # host application's stdout uninvited. Pass e.g. +Logger.new($stdout)+ to opt in.
55
- def initialize(url, logger: nil)
55
+ # @param max_fee_drops [Integer] cap for the fee autofill computes, so a
56
+ # spike in the open ledger fee cannot burn an account. Transactions that
57
+ # cost an owner reserve (AccountDelete, AMMCreate, VaultCreate) are not
58
+ # capped.
59
+ def initialize(url, logger: nil, max_fee_drops: Fee::MAX_FEE_DROPS)
56
60
  @url = resolve_url(url)
57
61
  @connection = nil
58
62
  @requests = {}
59
63
  @open = false
60
64
  @ready_queue = Queue.new
61
65
  @logger = logger
66
+ @max_fee_drops = max_fee_drops
62
67
  end
63
68
 
64
69
  # Opens the WebSocket connection.
@@ -319,13 +324,17 @@ module XRPL
319
324
  # Fills in the fields a transaction needs before signing: +Sequence+, +Fee+
320
325
  # and +LastLedgerSequence+. Existing values are never overwritten.
321
326
  #
327
+ # The fee follows the transaction type (see XRPL::Fee): an EscrowFinish
328
+ # pays for its Fulfillment, AccountDelete, AMMCreate and VaultCreate cost
329
+ # the owner reserve, a Batch pays for its inner transactions.
330
+ #
322
331
  # @param transaction [Hash, XRPL::Transaction] the transaction to complete.
323
332
  # @param signers_count [Integer] number of signatures for multisign fee scaling.
324
333
  # @return [Hash] a copy of the transaction with the missing fields filled in.
325
334
  def autofill(transaction, signers_count: 0)
326
335
  tx = self.class.to_transaction_hash(transaction).dup
327
336
  tx['Sequence'] ||= fetch_sequence(tx.fetch('Account'))
328
- tx['Fee'] ||= calculate_fee(signers_count)
337
+ tx['Fee'] ||= calculate_fee(tx, signers_count)
329
338
  tx['LastLedgerSequence'] ||= current_ledger_index + LEDGER_OFFSET
330
339
  tx
331
340
  end
@@ -440,10 +449,14 @@ module XRPL
440
449
  Integer(sequence)
441
450
  end
442
451
 
443
- def calculate_fee(signers_count)
444
- base = base_fee_drops
445
- total = signers_count.to_i.positive? ? base * (1 + signers_count.to_i) : base
446
- total.to_s
452
+ def calculate_fee(tx, signers_count)
453
+ Fee.calculate(
454
+ tx,
455
+ base_fee: base_fee_drops,
456
+ signers_count: signers_count.to_i,
457
+ max_fee: @max_fee_drops,
458
+ owner_reserve: -> { owner_reserve_drops }
459
+ ).to_s
447
460
  end
448
461
 
449
462
  def base_fee_drops
@@ -455,6 +468,17 @@ module XRPL
455
468
  DEFAULT_FEE_DROPS
456
469
  end
457
470
 
471
+ # The owner reserve (reserve_inc), which AccountDelete, AMMCreate and
472
+ # VaultCreate cost instead of a fee. There is no sensible default: a
473
+ # guess would be rejected or overpay by orders of magnitude.
474
+ def owner_reserve_drops
475
+ response = request_with_retry('server_state')
476
+ reserve = response.dig('result', 'state', 'validated_ledger', 'reserve_inc')
477
+ raise TransactionError, 'Could not determine the owner reserve from server_state' unless reserve
478
+
479
+ Integer(reserve)
480
+ end
481
+
458
482
  def current_ledger_index
459
483
  response = request_with_retry('ledger_current')
460
484
  index = response.dig('result', 'ledger_current_index')
data/lib/xrpl/fee.rb ADDED
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XRPL
4
+ # The fee a transaction needs, in drops, by transaction type.
5
+ #
6
+ # rippled charges most transactions the network's base fee, but not all of
7
+ # them: an EscrowFinish pays for the size of its Fulfillment, AccountDelete,
8
+ # AMMCreate and VaultCreate cost an owner reserve rather than a fee, a Batch
9
+ # pays for its inner transactions, and the confidential MPT transactions
10
+ # cost ten base fees. Multisigning adds one base fee per signature. The
11
+ # rules are the ones xrpl.js applies in its autofill.
12
+ #
13
+ # This module is pure: it takes the base fee and, when a type needs it, a
14
+ # way to get the owner reserve. XRPL::Client wires it to the ledger.
15
+ module Fee
16
+ # Where an ordinary fee is capped, so that a spike in the open ledger fee
17
+ # cannot burn an account. The reserve-priced types are not capped: their
18
+ # cost is what it is.
19
+ MAX_FEE_DROPS = 2_000_000
20
+
21
+ # Types whose cost is the owner reserve (reserve_inc), not a fee.
22
+ RESERVE_PRICED = %w[AccountDelete AMMCreate VaultCreate].freeze
23
+
24
+ # rippled's kCONFIDENTIAL_FEE_MULTIPLIER: extra base fees on top of the
25
+ # standard one, so ten in total.
26
+ CONFIDENTIAL_MPT_MULTIPLIER = 9
27
+
28
+ CONFIDENTIAL_MPT_TYPES = %w[
29
+ ConfidentialMPTConvert ConfidentialMPTConvertBack ConfidentialMPTSend
30
+ ConfidentialMPTClawback ConfidentialMPTMergeInbox
31
+ ].freeze
32
+
33
+ module_function
34
+
35
+ # @param transaction [Hash, XRPL::Transaction] the transaction, with its
36
+ # TransactionType
37
+ # @param base_fee [Integer] the network's base fee in drops
38
+ # @param signers_count [Integer] number of signatures for multisign scaling
39
+ # @param max_fee [Integer] cap for ordinary fees
40
+ # @param owner_reserve [Integer, #call, nil] the owner reserve in drops,
41
+ # or something that fetches it; only consulted for the reserve-priced
42
+ # types
43
+ # @return [Integer] the fee in drops, rounded up
44
+ def calculate(transaction, base_fee:, signers_count: 0, max_fee: MAX_FEE_DROPS, owner_reserve: nil)
45
+ tx = transaction.respond_to?(:to_h) ? transaction.to_h : transaction
46
+ type = tx['TransactionType'] || tx[:TransactionType]
47
+ base_fee = Integer(base_fee)
48
+
49
+ fee = Rational(base_fee)
50
+ reserve_priced = RESERVE_PRICED.include?(type)
51
+
52
+ if type == 'EscrowFinish' && tx['Fulfillment']
53
+ # Base fee × (33 + Fulfillment size in bytes / 16)
54
+ bytes = (tx['Fulfillment'].to_s.length + 1) / 2
55
+ fee = base_fee * (33 + Rational(bytes, 16))
56
+ elsif reserve_priced
57
+ fee = Rational(resolve_owner_reserve(owner_reserve, type))
58
+ elsif type == 'Batch'
59
+ inner = Array(tx['RawTransactions']).sum(0r) do |raw|
60
+ calculate(raw['RawTransaction'] || raw, base_fee: base_fee, max_fee: max_fee,
61
+ owner_reserve: owner_reserve)
62
+ end
63
+ fee = base_fee * 2 + inner
64
+ elsif CONFIDENTIAL_MPT_TYPES.include?(type)
65
+ fee += base_fee * CONFIDENTIAL_MPT_MULTIPLIER
66
+ end
67
+
68
+ # Multisigned: base fee × (1 + number of signatures)
69
+ fee += base_fee * signers_count if signers_count.to_i.positive?
70
+
71
+ fee = [fee, Rational(max_fee)].min unless reserve_priced
72
+ fee.ceil
73
+ end
74
+
75
+ def resolve_owner_reserve(owner_reserve, type)
76
+ reserve = owner_reserve.respond_to?(:call) ? owner_reserve.call : owner_reserve
77
+ raise ArgumentError, "#{type} costs the owner reserve, which was not given" if reserve.nil?
78
+
79
+ Integer(reserve)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XRPL
4
+ # A ledger entry - AccountRoot, RippleState, Offer, Escrow and the other
5
+ # objects that make up the ledger's state - built from LEDGER_ENTRY_FORMATS
6
+ # in definitions.json, the same way XRPL::Transaction is built from
7
+ # TRANSACTION_FORMATS.
8
+ #
9
+ # entry = XRPL::LedgerEntry.from(response.dig('result', 'node'))
10
+ # entry.class # => XRPL::LedgerEntry::AccountRoot
11
+ # entry.balance # => "370000000"
12
+ # entry.flag?(:lsf_default_ripple)
13
+ # entry.flag_names # => ["lsfDefaultRipple"]
14
+ #
15
+ # Where rippled hands out the entry's own hash under "index" (ledger_entry,
16
+ # account_objects, ledger_data), that value is kept as #index; it is not a
17
+ # field of the object and is left out of #to_h.
18
+ class LedgerEntry < Model
19
+ # Fields every ledger entry carries, regardless of type.
20
+ COMMON_FORMAT = DEFINITIONS['LEDGER_ENTRY_FORMATS'].fetch('common').freeze
21
+
22
+ # LEDGER_ENTRY_FLAGS keys DirectoryNode's flags under rippled's short
23
+ # name for the type.
24
+ FLAG_ALIASES = { 'DirectoryNode' => 'DirNode' }.freeze
25
+
26
+ # The entry's hash, as rippled reports it under "index".
27
+ attr_accessor :index
28
+
29
+ class << self
30
+ # The ledger's name for this entry type, e.g. "AccountRoot".
31
+ alias ledger_entry_type type_name
32
+
33
+ def type_field
34
+ 'LedgerEntryType'
35
+ end
36
+
37
+ def label
38
+ 'ledger entry'
39
+ end
40
+ end
41
+
42
+ def []=(name, value)
43
+ if name.to_s == 'index'
44
+ @index = value
45
+ return
46
+ end
47
+
48
+ super
49
+ end
50
+
51
+ define_types!(DEFINITIONS['LEDGER_ENTRY_FORMATS'], DEFINITIONS['LEDGER_ENTRY_FLAGS'], FLAG_ALIASES)
52
+ end
53
+ end
data/lib/xrpl/model.rb ADDED
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XRPL
4
+ # Common ground for the objects definitions.json describes field by field:
5
+ # transactions (TRANSACTION_FORMATS) and ledger entries (LEDGER_ENTRY_FORMATS).
6
+ #
7
+ # A family - XRPL::Transaction, XRPL::LedgerEntry - is a subclass that names
8
+ # the type field ("TransactionType", "LedgerEntryType") and calls
9
+ # .define_types! with the right tables. That creates one class per entry in
10
+ # the table, with an accessor for every field the type accepts and a
11
+ # constant for every flag it defines. None of them is written by hand, so
12
+ # syncing definitions.json updates them all and they cannot drift.
13
+ #
14
+ # Fields are written in snake_case and stored under the ledger's own
15
+ # PascalCase names, so #to_h hands the binary codec exactly what it expects.
16
+ class Model
17
+ # Raised when required fields are missing or a field is not part of the
18
+ # type.
19
+ class ValidationError < StandardError; end
20
+
21
+ # rippled's SOEStyle: what the format says about a field.
22
+ REQUIRED = 0
23
+ OPTIONAL = 1
24
+ DEFAULT = 2
25
+
26
+ DEFINITIONS = BinaryCodec::Definitions.instance.raw
27
+
28
+ # Ledger field name -> snake_case accessor, and back.
29
+ #
30
+ # XChain, NFToken and MPToken are brand names rather than acronyms, so they
31
+ # are folded to a single word the way the reference SDKs write them.
32
+ def self.underscore(name)
33
+ name
34
+ .sub(/\AXChain/, 'Xchain')
35
+ .sub(/\ANFToken/, 'Nftoken')
36
+ .sub(/\AMPToken/, 'Mptoken')
37
+ .gsub(/([A-Z]{2,})s(?=[A-Z]|\z)/) { "#{Regexp.last_match(1).capitalize}s" }
38
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
39
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
40
+ .downcase
41
+ end
42
+
43
+ FIELD_TO_ACCESSOR = DEFINITIONS['FIELDS'].to_h { |name, _| [name, underscore(name)] }.freeze
44
+ ACCESSOR_TO_FIELD = FIELD_TO_ACCESSOR.invert.freeze
45
+
46
+ if ACCESSOR_TO_FIELD.size != FIELD_TO_ACCESSOR.size
47
+ raise "Ambiguous accessor names in definitions.json"
48
+ end
49
+
50
+ class << self
51
+ # The ledger's name for this type, e.g. "Payment" or "AccountRoot".
52
+ attr_reader :type_name
53
+
54
+ # Ledger field name -> optionality, including the common fields.
55
+ attr_reader :format
56
+
57
+ # Flag name -> bit, e.g. "tfPartialPayment" => 131072.
58
+ attr_reader :flags
59
+
60
+ # The field that names the type: "TransactionType" or "LedgerEntryType".
61
+ # A family defines it.
62
+ def type_field
63
+ raise NotImplementedError, "#{name}.type_field"
64
+ end
65
+
66
+ # How the family reads in an error message, e.g. "transaction".
67
+ def label
68
+ raise NotImplementedError, "#{name}.label"
69
+ end
70
+
71
+ # Required by the format, but supplied by a later step (autofill,
72
+ # signing) rather than by the caller. Not demanded by #validate!.
73
+ def supplied_later
74
+ []
75
+ end
76
+
77
+ # The class for a type name, or nil if the ledger has no such type.
78
+ def for(type)
79
+ const_get(type) if const_defined?(type, false)
80
+ end
81
+
82
+ # Build the right subclass from a hash, PascalCase or snake.
83
+ def from(hash)
84
+ type = hash[type_field] || hash[type_field.to_sym] || hash[underscore(type_field).to_sym]
85
+ raise ValidationError, "#{label.capitalize} hash has no #{type_field}" unless type
86
+
87
+ klass = self.for(type.to_s)
88
+ raise ValidationError, "Unknown #{label} type #{type}" unless klass
89
+
90
+ klass.new(hash)
91
+ end
92
+
93
+ # Translates an accessor name to the ledger's field name.
94
+ def resolve(name)
95
+ key = name.to_s
96
+ return key if FIELD_TO_ACCESSOR.key?(key)
97
+
98
+ ACCESSOR_TO_FIELD[key] || key
99
+ end
100
+
101
+ # Builds one subclass per type in +formats+, with an accessor for every
102
+ # field the type accepts and a constant for every flag it defines.
103
+ #
104
+ # @param formats [Hash] a *_FORMATS table, with its "common" entry
105
+ # @param flags [Hash] the matching *_FLAGS table
106
+ # @param flag_aliases [Hash] type name -> key in +flags+, where they differ
107
+ def define_types!(formats, flags, flag_aliases = {})
108
+ common = formats.fetch('common')
109
+
110
+ formats.each do |type, own_format|
111
+ next if type == 'common'
112
+
113
+ format = (common + own_format)
114
+ .to_h { |field| [field['name'], field['optionality']] }
115
+ .freeze
116
+
117
+ klass = Class.new(self)
118
+ klass.instance_variable_set(:@type_name, type)
119
+ klass.instance_variable_set(:@format, format)
120
+ klass.instance_variable_set(:@flags, (flags[flag_aliases.fetch(type, type)] || {}).freeze)
121
+
122
+ format.each_key do |field|
123
+ next if field == type_field
124
+
125
+ accessor = FIELD_TO_ACCESSOR[field] or next
126
+
127
+ klass.define_method(accessor) { self[field] }
128
+ klass.define_method("#{accessor}=") { |value| self[field] = value }
129
+ end
130
+
131
+ klass.flags.each do |flag, bit|
132
+ klass.const_set(underscore(flag).upcase, bit)
133
+ end
134
+
135
+ const_set(type, klass)
136
+ end
137
+ end
138
+ end
139
+
140
+ def initialize(fields = {})
141
+ @fields = {}
142
+ fields.each { |name, value| self[name] = value }
143
+ end
144
+
145
+ # Reads a field by accessor name, symbol or ledger name.
146
+ def [](name)
147
+ @fields[self.class.resolve(name)]
148
+ end
149
+
150
+ # Writes a field, rejecting anything the type does not define. The type
151
+ # field itself is accepted, and checked, so a hash read off the ledger can
152
+ # be passed in whole.
153
+ def []=(name, value)
154
+ field = self.class.resolve(name)
155
+
156
+ if field == self.class.type_field
157
+ return if value.to_s == self.class.type_name
158
+
159
+ raise ValidationError, "#{self.class.type_name} cannot carry #{field} #{value}"
160
+ end
161
+
162
+ unless self.class.format.key?(field)
163
+ raise ValidationError, "#{self.class.type_name} has no field #{field}"
164
+ end
165
+
166
+ value.nil? ? @fields.delete(field) : @fields[field] = value
167
+ end
168
+
169
+ # The object as the binary codec wants it: ledger field names, with the
170
+ # type field filled in.
171
+ def to_h
172
+ { self.class.type_field => self.class.type_name }.merge(@fields)
173
+ end
174
+ alias to_hash to_h
175
+
176
+ # Fields the format requires that have not been set, ignoring the ones a
177
+ # later step provides.
178
+ def missing_fields
179
+ self.class.format
180
+ .select { |_, optionality| optionality == REQUIRED }
181
+ .keys
182
+ .reject { |field| field == self.class.type_field }
183
+ .reject { |field| self.class.supplied_later.include?(field) || @fields.key?(field) }
184
+ end
185
+
186
+ def valid?
187
+ missing_fields.empty?
188
+ end
189
+
190
+ # Raises unless every required field is present.
191
+ def validate!
192
+ missing = missing_fields
193
+ return self if missing.empty?
194
+
195
+ raise ValidationError,
196
+ "#{self.class.type_name} is missing #{missing.join(', ')}"
197
+ end
198
+
199
+ # Whether a flag is set in Flags. Takes the ledger's name
200
+ # ("lsfDefaultRipple"), the constant's name (:lsf_default_ripple) or the
201
+ # bit itself.
202
+ def flag?(flag)
203
+ (self['Flags'].to_i & self.class.flag_bit(flag)) != 0
204
+ end
205
+
206
+ # The names of the flags set in Flags, in the ledger's spelling.
207
+ def flag_names
208
+ value = self['Flags'].to_i
209
+ self.class.flags.select { |_, bit| (value & bit) != 0 }.keys
210
+ end
211
+
212
+ # The bit for a flag given by ledger name, constant name or bit.
213
+ def self.flag_bit(flag)
214
+ return flag if flag.is_a?(Integer)
215
+
216
+ key = flag.to_s
217
+ _, bit = flags.find { |name, _| name == key || underscore(name) == key.downcase }
218
+ bit or raise ArgumentError, "#{type_name} has no flag #{flag}"
219
+ end
220
+
221
+ # The serialised object, as hex.
222
+ def to_blob
223
+ BinaryCodec.json_to_binary(to_h)
224
+ end
225
+
226
+ def ==(other)
227
+ other.is_a?(Model) && other.to_h == to_h
228
+ end
229
+ alias eql? ==
230
+
231
+ def hash
232
+ to_h.hash
233
+ end
234
+
235
+ def inspect
236
+ "#<#{self.class.name} #{to_h.inspect}>"
237
+ end
238
+ end
239
+ end
@@ -3,8 +3,8 @@
3
3
  module XRPL
4
4
  # A transaction, built from the field formats in definitions.json.
5
5
  #
6
- # The subclasses below are not written by hand: one is created for each entry
7
- # in TRANSACTION_FORMATS when this file loads, so syncing definitions.json
6
+ # The subclasses are not written by hand: one is created for each entry in
7
+ # TRANSACTION_FORMATS when this file loads, so syncing definitions.json
8
8
  # updates the models with it and they cannot drift from the ledger.
9
9
  #
10
10
  # tx = XRPL::Transaction::Payment.new(
@@ -18,19 +18,9 @@ module XRPL
18
18
  # Fields are given in snake_case and stored under the ledger's own PascalCase
19
19
  # names, so #to_h hands the binary codec exactly what it expects. A plain
20
20
  # Hash still works everywhere a transaction is accepted; these classes are an
21
- # addition, not a replacement.
22
- class Transaction
23
- # Raised when required fields are missing or a field is not part of the
24
- # transaction type.
25
- class ValidationError < StandardError; end
26
-
27
- # rippled's SOEStyle: what the format says about a field.
28
- REQUIRED = 0
29
- OPTIONAL = 1
30
- DEFAULT = 2
31
-
32
- DEFINITIONS = BinaryCodec::Definitions.instance.raw
33
-
21
+ # addition, not a replacement. The machinery lives in XRPL::Model and is
22
+ # shared with XRPL::LedgerEntry.
23
+ class Transaction < Model
34
24
  # Fields every transaction carries, regardless of type.
35
25
  COMMON_FORMAT = DEFINITIONS['TRANSACTION_FORMATS'].fetch('common').freeze
36
26
 
@@ -41,162 +31,23 @@ module XRPL
41
31
  TransactionType Sequence Fee SigningPubKey TxnSignature LastLedgerSequence
42
32
  ].freeze
43
33
 
44
- # Ledger field name -> snake_case accessor, and back.
45
- #
46
- # XChain, NFToken and MPToken are brand names rather than acronyms, so they
47
- # are folded to a single word the way the reference SDKs write them.
48
- def self.underscore(name)
49
- name
50
- .sub(/\AXChain/, 'Xchain')
51
- .sub(/\ANFToken/, 'Nftoken')
52
- .sub(/\AMPToken/, 'Mptoken')
53
- .gsub(/([A-Z]{2,})s(?=[A-Z]|\z)/) { "#{Regexp.last_match(1).capitalize}s" }
54
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
55
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
56
- .downcase
57
- end
58
-
59
- FIELD_TO_ACCESSOR = DEFINITIONS['FIELDS'].to_h { |name, _| [name, underscore(name)] }.freeze
60
- ACCESSOR_TO_FIELD = FIELD_TO_ACCESSOR.invert.freeze
61
-
62
- if ACCESSOR_TO_FIELD.size != FIELD_TO_ACCESSOR.size
63
- raise "Ambiguous accessor names in definitions.json"
64
- end
65
-
66
34
  class << self
67
35
  # The ledger's name for this transaction type, e.g. "Payment".
68
- attr_reader :transaction_type
69
-
70
- # Ledger field name -> optionality, including the common fields.
71
- attr_reader :format
72
-
73
- # Flag name -> bit, e.g. "tfPartialPayment" => 131072.
74
- attr_reader :flags
75
- end
76
-
77
- # The class for a transaction type name, or nil if the ledger has no such
78
- # type.
79
- def self.for(type)
80
- const_get(type) if const_defined?(type, false)
81
- end
36
+ alias transaction_type type_name
82
37
 
83
- # Build the right subclass from a transaction hash, PascalCase or snake.
84
- def self.from(hash)
85
- type = hash['TransactionType'] || hash[:TransactionType] || hash[:transaction_type]
86
- raise ValidationError, 'Transaction hash has no TransactionType' unless type
87
-
88
- klass = self.for(type.to_s)
89
- raise ValidationError, "Unknown transaction type #{type}" unless klass
90
-
91
- klass.new(hash)
92
- end
93
-
94
- def initialize(fields = {})
95
- @fields = {}
96
- fields.each { |name, value| self[name] = value }
97
- end
98
-
99
- # Reads a field by accessor name, symbol or ledger name.
100
- def [](name)
101
- @fields[self.class.resolve(name)]
102
- end
103
-
104
- # Writes a field, rejecting anything the type does not define.
105
- def []=(name, value)
106
- field = self.class.resolve(name)
107
-
108
- unless self.class.format.key?(field)
109
- raise ValidationError, "#{self.class.transaction_type} has no field #{field}"
38
+ def type_field
39
+ 'TransactionType'
110
40
  end
111
41
 
112
- value.nil? ? @fields.delete(field) : @fields[field] = value
113
- end
114
-
115
- # Translates an accessor name to the ledger's field name.
116
- def self.resolve(name)
117
- key = name.to_s
118
- return key if FIELD_TO_ACCESSOR.key?(key)
119
-
120
- ACCESSOR_TO_FIELD[key] || key
121
- end
122
-
123
- # The transaction as the binary codec wants it: ledger field names, with
124
- # TransactionType filled in.
125
- def to_h
126
- { 'TransactionType' => self.class.transaction_type }.merge(@fields)
127
- end
128
- alias to_hash to_h
129
-
130
- # Fields the format requires that have not been set, ignoring the ones
131
- # autofill and signing provide.
132
- def missing_fields
133
- self.class.format
134
- .select { |field, optionality| optionality == REQUIRED }
135
- .keys
136
- .reject { |field| SUPPLIED_LATER.include?(field) || @fields.key?(field) }
137
- end
138
-
139
- def valid?
140
- missing_fields.empty?
141
- end
142
-
143
- # Raises unless every required field is present.
144
- def validate!
145
- missing = missing_fields
146
- return self if missing.empty?
147
-
148
- raise ValidationError,
149
- "#{self.class.transaction_type} is missing #{missing.join(', ')}"
150
- end
151
-
152
- # The serialised transaction, as hex.
153
- def to_blob
154
- BinaryCodec.json_to_binary(to_h)
155
- end
156
-
157
- def ==(other)
158
- other.is_a?(Transaction) && other.to_h == to_h
159
- end
160
- alias eql? ==
161
-
162
- def hash
163
- to_h.hash
164
- end
165
-
166
- def inspect
167
- "#<#{self.class.name} #{to_h.inspect}>"
168
- end
169
-
170
- # Builds one subclass per transaction type, with an accessor for every
171
- # field the type accepts and a constant for every flag it defines.
172
- def self.define_types!
173
- DEFINITIONS['TRANSACTION_FORMATS'].each do |type, own_format|
174
- next if type == 'common'
175
-
176
- format = (COMMON_FORMAT + own_format)
177
- .to_h { |field| [field['name'], field['optionality']] }
178
- .freeze
179
-
180
- klass = Class.new(self)
181
- klass.instance_variable_set(:@transaction_type, type)
182
- klass.instance_variable_set(:@format, format)
183
- klass.instance_variable_set(:@flags, (DEFINITIONS['TRANSACTION_FLAGS'][type] || {}).freeze)
184
-
185
- format.each_key do |field|
186
- accessor = FIELD_TO_ACCESSOR[field] or next
187
-
188
- klass.define_method(accessor) { self[field] }
189
- klass.define_method("#{accessor}=") { |value| self[field] = value }
190
- end
191
-
192
- klass.flags.each do |name, bit|
193
- klass.const_set(underscore(name).upcase, bit)
194
- end
42
+ def label
43
+ 'transaction'
44
+ end
195
45
 
196
- const_set(type, klass)
46
+ def supplied_later
47
+ SUPPLIED_LATER
197
48
  end
198
49
  end
199
50
 
200
- define_types!
51
+ define_types!(DEFINITIONS['TRANSACTION_FORMATS'], DEFINITIONS['TRANSACTION_FLAGS'])
201
52
  end
202
53
  end
data/lib/xrpl/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module XRPL
4
- VERSION = '0.7.0'
4
+ VERSION = '0.8.0'
5
5
  end
data/lib/xrpl-ruby.rb CHANGED
@@ -40,6 +40,9 @@ require_relative 'key-pairs/ed25519'
40
40
  require_relative 'key-pairs/secp256k1'
41
41
  require_relative 'key-pairs/key_pairs'
42
42
 
43
+ require_relative 'xrpl/model'
43
44
  require_relative 'xrpl/transaction'
45
+ require_relative 'xrpl/ledger_entry'
46
+ require_relative 'xrpl/fee'
44
47
  require_relative 'xrpl/client'
45
48
  require_relative 'xrpl/faucet'