xrpl-ruby 0.6.1 → 0.7.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.
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XRPL
4
+ # A transaction, built from the field formats in definitions.json.
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
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.
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
+
34
+ # Fields every transaction carries, regardless of type.
35
+ COMMON_FORMAT = DEFINITIONS['TRANSACTION_FORMATS'].fetch('common').freeze
36
+
37
+ # Required by the format, but supplied by autofill and signing rather than
38
+ # by the caller. Demanding them up front would make #validate! useless at
39
+ # the point where it is actually worth running.
40
+ SUPPLIED_LATER = %w[
41
+ TransactionType Sequence Fee SigningPubKey TxnSignature LastLedgerSequence
42
+ ].freeze
43
+
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
+ class << self
67
+ # 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
82
+
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}"
110
+ end
111
+
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
195
+
196
+ const_set(type, klass)
197
+ end
198
+ end
199
+
200
+ define_types!
201
+ end
202
+ 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.7.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,6 @@ 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/transaction'
42
44
  require_relative 'xrpl/client'
43
45
  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.7.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-08-26 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,7 @@ files:
170
171
  - lib/xrpl-ruby.rb
171
172
  - lib/xrpl/client.rb
172
173
  - lib/xrpl/faucet.rb
174
+ - lib/xrpl/transaction.rb
173
175
  - lib/xrpl/version.rb
174
176
  homepage: https://github.com/AlexanderBuzz/xrpl-ruby
175
177
  licenses: