xrpl-ruby 0.6.1 → 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/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
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XRPL
4
+ # A transaction, built from the field formats in definitions.json.
5
+ #
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
+ # updates the models with it and they cannot drift from the ledger.
9
+ #
10
+ # tx = XRPL::Transaction::Payment.new(
11
+ # account: wallet.classic_address,
12
+ # destination: receiver.classic_address,
13
+ # amount: '1000000'
14
+ # )
15
+ # tx.validate!
16
+ # client.submit_and_wait(tx, wallet: wallet)
17
+ #
18
+ # Fields are given in snake_case and stored under the ledger's own PascalCase
19
+ # names, so #to_h hands the binary codec exactly what it expects. A plain
20
+ # Hash still works everywhere a transaction is accepted; these classes are an
21
+ # addition, not a replacement. The machinery lives in XRPL::Model and is
22
+ # shared with XRPL::LedgerEntry.
23
+ class Transaction < Model
24
+ # Fields every transaction carries, regardless of type.
25
+ COMMON_FORMAT = DEFINITIONS['TRANSACTION_FORMATS'].fetch('common').freeze
26
+
27
+ # Required by the format, but supplied by autofill and signing rather than
28
+ # by the caller. Demanding them up front would make #validate! useless at
29
+ # the point where it is actually worth running.
30
+ SUPPLIED_LATER = %w[
31
+ TransactionType Sequence Fee SigningPubKey TxnSignature LastLedgerSequence
32
+ ].freeze
33
+
34
+ class << self
35
+ # The ledger's name for this transaction type, e.g. "Payment".
36
+ alias transaction_type type_name
37
+
38
+ def type_field
39
+ 'TransactionType'
40
+ end
41
+
42
+ def label
43
+ 'transaction'
44
+ end
45
+
46
+ def supplied_later
47
+ SUPPLIED_LATER
48
+ end
49
+ end
50
+
51
+ define_types!(DEFINITIONS['TRANSACTION_FORMATS'], DEFINITIONS['TRANSACTION_FLAGS'])
52
+ end
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.6.1'
4
+ VERSION = '0.8.0'
5
5
  end
data/lib/xrpl-ruby.rb CHANGED
@@ -21,6 +21,7 @@ require_relative 'binary-codec/serdes/binary_serializer'
21
21
  require_relative 'binary-codec/types/serialized_type'
22
22
  require_relative 'binary-codec/types/hash'
23
23
  require_relative 'binary-codec/types/uint'
24
+ require_relative 'binary-codec/types/number'
24
25
  require_relative 'binary-codec/types/account_id'
25
26
  require_relative 'binary-codec/types/amount'
26
27
  require_relative 'binary-codec/types/blob'
@@ -39,5 +40,9 @@ require_relative 'key-pairs/ed25519'
39
40
  require_relative 'key-pairs/secp256k1'
40
41
  require_relative 'key-pairs/key_pairs'
41
42
 
43
+ require_relative 'xrpl/model'
44
+ require_relative 'xrpl/transaction'
45
+ require_relative 'xrpl/ledger_entry'
46
+ require_relative 'xrpl/fee'
42
47
  require_relative 'xrpl/client'
43
48
  require_relative 'xrpl/faucet'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xrpl-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Busse
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-12 00:00:00.000000000 Z
11
+ date: 2026-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -151,6 +151,7 @@ files:
151
151
  - lib/binary-codec/types/currency.rb
152
152
  - lib/binary-codec/types/hash.rb
153
153
  - lib/binary-codec/types/issue.rb
154
+ - lib/binary-codec/types/number.rb
154
155
  - lib/binary-codec/types/path_set.rb
155
156
  - lib/binary-codec/types/serialized_type.rb
156
157
  - lib/binary-codec/types/st_array.rb
@@ -170,6 +171,10 @@ files:
170
171
  - lib/xrpl-ruby.rb
171
172
  - lib/xrpl/client.rb
172
173
  - lib/xrpl/faucet.rb
174
+ - lib/xrpl/fee.rb
175
+ - lib/xrpl/ledger_entry.rb
176
+ - lib/xrpl/model.rb
177
+ - lib/xrpl/transaction.rb
173
178
  - lib/xrpl/version.rb
174
179
  homepage: https://github.com/AlexanderBuzz/xrpl-ruby
175
180
  licenses: