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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -0
- data/README.md +57 -3
- data/lib/binary-codec/binary_codec.rb +22 -1
- data/lib/binary-codec/enums/definitions.json +84 -288
- data/lib/binary-codec/enums/definitions.rb +38 -1
- data/lib/binary-codec/types/uint.rb +32 -39
- data/lib/wallet/wallet.rb +32 -0
- data/lib/xrpl/client.rb +30 -6
- 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 +14 -163
- data/lib/xrpl/version.rb +1 -1
- data/lib/xrpl-ruby.rb +3 -0
- metadata +5 -2
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
data/lib/xrpl/transaction.rb
CHANGED
|
@@ -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
|
|
7
|
-
#
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
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'
|