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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +155 -0
- data/README.md +99 -3
- data/lib/binary-codec/binary_codec.rb +22 -1
- data/lib/binary-codec/enums/definitions.json +3856 -456
- data/lib/binary-codec/enums/definitions.rb +46 -1
- data/lib/binary-codec/types/amount.rb +5 -2
- data/lib/binary-codec/types/currency.rb +8 -1
- data/lib/binary-codec/types/issue.rb +68 -23
- data/lib/binary-codec/types/number.rb +196 -0
- data/lib/binary-codec/types/serialized_type.rb +5 -2
- data/lib/binary-codec/types/st_array.rb +17 -19
- data/lib/binary-codec/types/st_object.rb +15 -13
- data/lib/binary-codec/types/uint.rb +55 -44
- data/lib/binary-codec/types/xchain_bridge.rb +17 -2
- data/lib/wallet/wallet.rb +32 -0
- data/lib/xrpl/client.rb +47 -9
- data/lib/xrpl/fee.rb +82 -0
- data/lib/xrpl/ledger_entry.rb +53 -0
- data/lib/xrpl/model.rb +239 -0
- data/lib/xrpl/transaction.rb +53 -0
- data/lib/xrpl/version.rb +1 -1
- data/lib/xrpl-ruby.rb +5 -0
- metadata +7 -2
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
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.
|
|
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-
|
|
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:
|