xrpl-ruby 0.6.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -0
- data/README.md +49 -0
- data/lib/binary-codec/enums/definitions.json +3716 -112
- data/lib/binary-codec/enums/definitions.rb +8 -0
- 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 +23 -5
- data/lib/binary-codec/types/xchain_bridge.rb +17 -2
- data/lib/xrpl/client.rb +17 -3
- data/lib/xrpl/transaction.rb +202 -0
- data/lib/xrpl/version.rb +1 -1
- data/lib/xrpl-ruby.rb +2 -0
- metadata +19 -3
|
@@ -47,6 +47,14 @@ module BinaryCodec
|
|
|
47
47
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# The parsed definitions.json, for the sections this class does not index
|
|
51
|
+
# itself - TRANSACTION_FORMATS and TRANSACTION_FLAGS, which the transaction
|
|
52
|
+
# models are built from.
|
|
53
|
+
#
|
|
54
|
+
# @return [Hash] the raw document
|
|
55
|
+
attr_reader :definitions
|
|
56
|
+
alias raw definitions
|
|
57
|
+
|
|
50
58
|
# Returns the singleton instance of the Definitions class.
|
|
51
59
|
# @return [Definitions] The singleton instance.
|
|
52
60
|
def self.instance
|
|
@@ -166,8 +166,11 @@ module BinaryCodec
|
|
|
166
166
|
|
|
167
167
|
# value = mantissa * 10^exponent
|
|
168
168
|
value = BigDecimal(mantissa_int) * (BigDecimal(10)**exponent)
|
|
169
|
-
|
|
170
|
-
|
|
169
|
+
|
|
170
|
+
# A zero IOU has its own encoding in which the sign bit is not set, so
|
|
171
|
+
# negating it would produce "-0" where rippled writes "0".
|
|
172
|
+
value = -value unless is_positive || mantissa_int.zero?
|
|
173
|
+
|
|
171
174
|
# Format the value string to match xrpl.js (stripping trailing .0)
|
|
172
175
|
formatted_value = value.to_s('F').sub(/\.0$/, '')
|
|
173
176
|
|
|
@@ -51,8 +51,15 @@ module BinaryCodec
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
# Returns the JSON representation of the currency.
|
|
54
|
+
#
|
|
55
|
+
# The two ignored parameters are the SerializedType contract. Every type
|
|
56
|
+
# is called as to_json(definitions, field_name) from STObject, so a
|
|
57
|
+
# zero-arity to_json raises ArgumentError for every Currency field nested
|
|
58
|
+
# in an object - which is what silently dropped BaseAsset and QuoteAsset
|
|
59
|
+
# from PriceDataSeries.
|
|
60
|
+
#
|
|
54
61
|
# @return [String] The ISO code or hex string.
|
|
55
|
-
def to_json
|
|
62
|
+
def to_json(_definitions = nil, _field_name = nil)
|
|
56
63
|
iso = self.iso
|
|
57
64
|
return iso unless iso.nil?
|
|
58
65
|
|
|
@@ -1,47 +1,92 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module BinaryCodec
|
|
4
|
+
# An asset without an amount: XRP, an issued token, or an MPT.
|
|
5
|
+
#
|
|
6
|
+
# The width is not fixed, and it cannot be supplied from outside. XRP is the
|
|
7
|
+
# 20 byte currency code on its own, an issued token adds the 20 byte issuer,
|
|
8
|
+
# and an MPT carries the issuer, a reserved placeholder and the four byte
|
|
9
|
+
# issuance sequence. Only the currency code says which of the three it is, so
|
|
10
|
+
# the parser has to read that before it knows how much to consume.
|
|
11
|
+
#
|
|
12
|
+
# This class used to take a size hint and read a flat 40 bytes, which for an
|
|
13
|
+
# XRP issue swallowed the field that follows - that is why every AMM and
|
|
14
|
+
# XChain fixture decoded with a nil Asset or the wrong door account.
|
|
4
15
|
class Issue < SerializedType
|
|
16
|
+
CURRENCY_LENGTH = 20
|
|
17
|
+
ISSUER_LENGTH = 20
|
|
18
|
+
SEQUENCE_LENGTH = 4
|
|
19
|
+
MPT_LENGTH = CURRENCY_LENGTH + ISSUER_LENGTH + SEQUENCE_LENGTH
|
|
20
|
+
|
|
21
|
+
# The reserved "no account" value. Sitting in the issuer slot, it marks the
|
|
22
|
+
# issue as an MPT rather than as a token issued by that account.
|
|
23
|
+
NO_ACCOUNT = '0000000000000000000000000000000000000001'
|
|
24
|
+
|
|
5
25
|
def initialize(bytes = nil)
|
|
6
26
|
super(bytes || [])
|
|
7
27
|
end
|
|
8
28
|
|
|
9
29
|
def self.from(value)
|
|
10
30
|
return value if value.is_a?(Issue)
|
|
31
|
+
return Issue.new(hex_to_bytes(value)) if value.is_a?(String)
|
|
11
32
|
|
|
12
|
-
|
|
13
|
-
|
|
33
|
+
unless value.is_a?(::Hash)
|
|
34
|
+
raise StandardError, "Cannot construct Issue from #{value.class}"
|
|
14
35
|
end
|
|
15
36
|
|
|
16
|
-
|
|
17
|
-
bytes = []
|
|
18
|
-
bytes.concat(Currency.from(value['currency']).to_bytes)
|
|
19
|
-
bytes.concat(AccountId.from(value['issuer']).to_bytes) if value['issuer']
|
|
20
|
-
return Issue.new(bytes)
|
|
21
|
-
end
|
|
37
|
+
return Issue.new(mpt_bytes(value['mpt_issuance_id'])) if value['mpt_issuance_id']
|
|
22
38
|
|
|
23
|
-
|
|
39
|
+
bytes = Currency.from(value['currency']).to_bytes
|
|
40
|
+
bytes += AccountId.from(value['issuer']).to_bytes if value['issuer']
|
|
41
|
+
Issue.new(bytes)
|
|
24
42
|
end
|
|
25
43
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
# An MPT issuance id is the issuance sequence followed by the issuer. The
|
|
45
|
+
# sequence is big endian there but little endian on the wire.
|
|
46
|
+
def self.mpt_bytes(issuance_id)
|
|
47
|
+
id = hex_to_bytes(issuance_id)
|
|
48
|
+
sequence = id[0, SEQUENCE_LENGTH]
|
|
49
|
+
issuer = id[SEQUENCE_LENGTH..]
|
|
50
|
+
|
|
51
|
+
issuer + hex_to_bytes(NO_ACCOUNT) + sequence.reverse
|
|
52
|
+
end
|
|
53
|
+
private_class_method :mpt_bytes
|
|
54
|
+
|
|
55
|
+
# The size hint is accepted and ignored: callers cannot know the width, and
|
|
56
|
+
# passing one in is how the fixed 40 byte read came about.
|
|
57
|
+
def self.from_parser(parser, _size_hint = nil)
|
|
58
|
+
return Issue.new([]) if parser.end?
|
|
59
|
+
|
|
60
|
+
currency = parser.read(CURRENCY_LENGTH)
|
|
61
|
+
return Issue.new(currency) if Currency.new(currency).to_json == 'XRP'
|
|
62
|
+
|
|
63
|
+
issuer = parser.read(ISSUER_LENGTH)
|
|
64
|
+
bytes = currency + issuer
|
|
65
|
+
bytes += parser.read(SEQUENCE_LENGTH) if bytes_to_hex(issuer).upcase == NO_ACCOUNT
|
|
66
|
+
|
|
33
67
|
Issue.new(bytes)
|
|
34
68
|
end
|
|
35
69
|
|
|
36
70
|
def to_json(_definitions = nil, _field_name = nil)
|
|
71
|
+
return { 'mpt_issuance_id' => mpt_issuance_id } if to_bytes.length == MPT_LENGTH
|
|
72
|
+
|
|
37
73
|
parser = BinaryParser.new(to_hex)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
{ 'currency' =>
|
|
74
|
+
currency = Currency.from_parser(parser).to_json
|
|
75
|
+
|
|
76
|
+
# An XRP issue has no issuer. Reporting one means 20 bytes of the next
|
|
77
|
+
# field were read as an account.
|
|
78
|
+
return { 'currency' => currency } if currency == 'XRP'
|
|
79
|
+
|
|
80
|
+
{ 'currency' => currency, 'issuer' => AccountId.from_parser(parser).to_json }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def mpt_issuance_id
|
|
86
|
+
issuer = to_bytes[0, ISSUER_LENGTH]
|
|
87
|
+
sequence = to_bytes[CURRENCY_LENGTH + ISSUER_LENGTH, SEQUENCE_LENGTH].reverse
|
|
88
|
+
|
|
89
|
+
bytes_to_hex(sequence + issuer).upcase
|
|
45
90
|
end
|
|
46
91
|
end
|
|
47
92
|
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BinaryCodec
|
|
4
|
+
# STNumber, the XRPL "Number" type.
|
|
5
|
+
#
|
|
6
|
+
# Always 12 bytes: a signed 64 bit mantissa followed by a signed 32 bit
|
|
7
|
+
# exponent, both big endian. Used by the Vault and Lending Protocol fields
|
|
8
|
+
# (AssetsAvailable, DebtTotal, PeriodicPayment and 14 others), which could
|
|
9
|
+
# not be serialised at all before this class existed.
|
|
10
|
+
#
|
|
11
|
+
# Reference: xrpl.js packages/ripple-binary-codec/src/types/st-number.ts
|
|
12
|
+
class Number < SerializedType
|
|
13
|
+
BYTE_LENGTH = 12
|
|
14
|
+
|
|
15
|
+
MIN_MANTISSA = 10**18
|
|
16
|
+
MAX_MANTISSA = 10**19 - 1
|
|
17
|
+
MAX_INT64 = 2**63 - 1
|
|
18
|
+
|
|
19
|
+
MIN_EXPONENT = -32_768
|
|
20
|
+
MAX_EXPONENT = 32_768
|
|
21
|
+
|
|
22
|
+
# The exponent rippled uses to encode a canonical zero.
|
|
23
|
+
DEFAULT_VALUE_EXPONENT = -2_147_483_648
|
|
24
|
+
|
|
25
|
+
# Significant decimal digits rippled renders with.
|
|
26
|
+
RANGE_LOG = 18
|
|
27
|
+
|
|
28
|
+
NUMBER_PATTERN = /\A([-+]?)([0-9]+)(?:\.([0-9]+))?(?:[eE]([+-]?[0-9]+))?\z/
|
|
29
|
+
|
|
30
|
+
def initialize(bytes = nil)
|
|
31
|
+
bytes ||= Array.new(BYTE_LENGTH, 0)
|
|
32
|
+
|
|
33
|
+
unless bytes.length == BYTE_LENGTH
|
|
34
|
+
raise StandardError, "Invalid Number length #{bytes.length}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
super(bytes)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.from(value)
|
|
41
|
+
return value if value.is_a?(Number)
|
|
42
|
+
mantissa, exponent = extract_parts(value.to_s)
|
|
43
|
+
mantissa, exponent = normalize(mantissa, exponent)
|
|
44
|
+
|
|
45
|
+
Number.new(pack_signed(mantissa, 8) + pack_signed(exponent, 4))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.from_parser(parser, _size_hint = nil)
|
|
49
|
+
Number.new(parser.read(BYTE_LENGTH))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_json(_definitions = nil, _field_name = nil)
|
|
53
|
+
mantissa = self.class.unpack_signed(to_bytes[0, 8])
|
|
54
|
+
exponent = self.class.unpack_signed(to_bytes[8, 4])
|
|
55
|
+
|
|
56
|
+
return '0' if mantissa.zero? && exponent == DEFAULT_VALUE_EXPONENT
|
|
57
|
+
|
|
58
|
+
negative = mantissa.negative?
|
|
59
|
+
mantissa = mantissa.abs
|
|
60
|
+
|
|
61
|
+
# A mantissa above 2^63-1 is shrunk by one digit before serialisation.
|
|
62
|
+
# Restore it so the rendering matches rippled's internal value.
|
|
63
|
+
if !mantissa.zero? && mantissa < MIN_MANTISSA
|
|
64
|
+
mantissa *= 10
|
|
65
|
+
exponent -= 1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
sign = negative ? '-' : ''
|
|
69
|
+
|
|
70
|
+
if exponent != 0 && (exponent < -(RANGE_LOG + 10) || exponent > -(RANGE_LOG - 10))
|
|
71
|
+
return sign + scientific(mantissa, exponent)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sign + positional(mantissa, exponent)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Scientific notation, with trailing zeros moved into the exponent.
|
|
78
|
+
def self.scientific(mantissa, exponent)
|
|
79
|
+
while !mantissa.zero? && (mantissa % 10).zero? && exponent < MAX_EXPONENT
|
|
80
|
+
mantissa /= 10
|
|
81
|
+
exponent += 1
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
"#{mantissa}e#{exponent}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def scientific(mantissa, exponent)
|
|
88
|
+
self.class.scientific(mantissa, exponent)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Plain decimal notation, built by padding the digits out far enough that
|
|
92
|
+
# the decimal point always lands inside the string.
|
|
93
|
+
def positional(mantissa, exponent)
|
|
94
|
+
pad_prefix = RANGE_LOG + 12
|
|
95
|
+
pad_suffix = RANGE_LOG + 8
|
|
96
|
+
|
|
97
|
+
raw = ('0' * pad_prefix) + mantissa.to_s + ('0' * pad_suffix)
|
|
98
|
+
offset = exponent + pad_prefix + RANGE_LOG + 1
|
|
99
|
+
|
|
100
|
+
integer = raw[0, offset].sub(/\A0+/, '')
|
|
101
|
+
integer = '0' if integer.empty?
|
|
102
|
+
fraction = raw[offset..].sub(/0+\z/, '')
|
|
103
|
+
|
|
104
|
+
fraction.empty? ? integer : "#{integer}.#{fraction}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Split a number string into an unnormalised mantissa and exponent.
|
|
108
|
+
def self.extract_parts(value)
|
|
109
|
+
match = NUMBER_PATTERN.match(value)
|
|
110
|
+
raise StandardError, "Unable to parse number from string: #{value}" unless match
|
|
111
|
+
|
|
112
|
+
sign, int_part, frac_part, exp_part = match.captures
|
|
113
|
+
|
|
114
|
+
digits = int_part.sub(/\A0+(?=.)/, '')
|
|
115
|
+
exponent = 0
|
|
116
|
+
|
|
117
|
+
unless frac_part.nil? || frac_part.empty?
|
|
118
|
+
digits += frac_part
|
|
119
|
+
exponent -= frac_part.length
|
|
120
|
+
end
|
|
121
|
+
exponent += exp_part.to_i unless exp_part.nil? || exp_part.empty?
|
|
122
|
+
|
|
123
|
+
while digits.length > 1 && digits.end_with?('0')
|
|
124
|
+
digits = digits[0..-2]
|
|
125
|
+
exponent += 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
mantissa = digits.to_i
|
|
129
|
+
mantissa = -mantissa if sign == '-'
|
|
130
|
+
|
|
131
|
+
[mantissa, exponent]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Bring mantissa and exponent into the range rippled expects.
|
|
135
|
+
def self.normalize(mantissa, exponent)
|
|
136
|
+
return [0, DEFAULT_VALUE_EXPONENT] if mantissa.zero?
|
|
137
|
+
|
|
138
|
+
negative = mantissa.negative?
|
|
139
|
+
m = mantissa.abs
|
|
140
|
+
|
|
141
|
+
while m < MIN_MANTISSA && exponent > MIN_EXPONENT
|
|
142
|
+
exponent -= 1
|
|
143
|
+
m *= 10
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
last_digit = nil
|
|
147
|
+
while m > MAX_MANTISSA
|
|
148
|
+
raise StandardError, 'Mantissa and exponent are too large' if exponent >= MAX_EXPONENT
|
|
149
|
+
|
|
150
|
+
exponent += 1
|
|
151
|
+
last_digit = m % 10
|
|
152
|
+
m /= 10
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
raise StandardError, 'Underflow: value too small to represent' if exponent < MIN_EXPONENT || m < MIN_MANTISSA
|
|
156
|
+
raise StandardError, 'Exponent overflow: value too large to represent' if exponent > MAX_EXPONENT
|
|
157
|
+
|
|
158
|
+
if m > MAX_INT64
|
|
159
|
+
raise StandardError, 'Exponent overflow: value too large to represent' if exponent >= MAX_EXPONENT
|
|
160
|
+
|
|
161
|
+
exponent += 1
|
|
162
|
+
last_digit = m % 10
|
|
163
|
+
m /= 10
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if last_digit && last_digit >= 5
|
|
167
|
+
m += 1
|
|
168
|
+
|
|
169
|
+
if m > MAX_INT64
|
|
170
|
+
raise StandardError, 'Exponent overflow: value too large to represent' if exponent >= MAX_EXPONENT
|
|
171
|
+
|
|
172
|
+
last_digit = m % 10
|
|
173
|
+
exponent += 1
|
|
174
|
+
m /= 10
|
|
175
|
+
m += 1 if last_digit >= 5
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
[negative ? -m : m, exponent]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Big endian two's complement.
|
|
183
|
+
def self.pack_signed(value, byte_length)
|
|
184
|
+
value += 2**(byte_length * 8) if value.negative?
|
|
185
|
+
|
|
186
|
+
Array.new(byte_length) { |i| (value >> (8 * (byte_length - 1 - i))) & 0xFF }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def self.unpack_signed(bytes)
|
|
190
|
+
value = bytes.reduce(0) { |acc, b| (acc << 8) + b }
|
|
191
|
+
boundary = 2**(bytes.length * 8 - 1)
|
|
192
|
+
|
|
193
|
+
value >= boundary ? value - 2**(bytes.length * 8) : value
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -99,8 +99,11 @@ module BinaryCodec
|
|
|
99
99
|
when "UInt160" then Uint160
|
|
100
100
|
when "UInt192" then Uint192
|
|
101
101
|
when "UInt256" then Uint256
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
# The reference definitions call these Hash384/Hash512. The old UInt
|
|
103
|
+
# names are kept so a caller passing them still resolves.
|
|
104
|
+
when "Hash384", "UInt384" then Uint384
|
|
105
|
+
when "Hash512", "UInt512" then Uint512
|
|
106
|
+
when "Number" then Number
|
|
104
107
|
when "Int32" then Int32
|
|
105
108
|
when "Int64" then Int64
|
|
106
109
|
when "PathSet" then PathSet
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module BinaryCodec
|
|
4
4
|
class STArray < SerializedType
|
|
5
|
+
ARRAY_END_MARKER = 0xF1
|
|
6
|
+
|
|
5
7
|
def initialize(byte_buf = nil)
|
|
6
8
|
super(byte_buf || [])
|
|
7
9
|
end
|
|
@@ -38,7 +40,7 @@ module BinaryCodec
|
|
|
38
40
|
raise StandardError, "STArray item must be a Hash, got #{item.class}"
|
|
39
41
|
end
|
|
40
42
|
end
|
|
41
|
-
bytes.concat([
|
|
43
|
+
bytes.concat([ARRAY_END_MARKER])
|
|
42
44
|
return STArray.new(bytes)
|
|
43
45
|
end
|
|
44
46
|
|
|
@@ -53,7 +55,7 @@ module BinaryCodec
|
|
|
53
55
|
bytes = []
|
|
54
56
|
until parser.end?
|
|
55
57
|
# Check if we reached the ArrayEndMarker (0xF1)
|
|
56
|
-
if parser.peek ==
|
|
58
|
+
if parser.peek == ARRAY_END_MARKER
|
|
57
59
|
parser.read(1) # Consume 0xF1
|
|
58
60
|
break
|
|
59
61
|
end
|
|
@@ -70,6 +72,11 @@ module BinaryCodec
|
|
|
70
72
|
bytes.concat(obj.to_bytes)
|
|
71
73
|
bytes.concat([0xE1]) unless bytes.last == 0xE1
|
|
72
74
|
end
|
|
75
|
+
|
|
76
|
+
# The ArrayEndMarker has to go back in. Without it the reconstructed
|
|
77
|
+
# bytes have no terminator, so re-parsing them - which is what to_json
|
|
78
|
+
# does - runs the array on into whatever field follows it.
|
|
79
|
+
bytes.concat([ARRAY_END_MARKER])
|
|
73
80
|
STArray.new(bytes)
|
|
74
81
|
end
|
|
75
82
|
|
|
@@ -82,23 +89,14 @@ module BinaryCodec
|
|
|
82
89
|
parser = BinaryParser.new(to_hex)
|
|
83
90
|
result = []
|
|
84
91
|
until parser.end?
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# Read the STObject item
|
|
94
|
-
obj = STObject.from_parser(parser)
|
|
95
|
-
|
|
96
|
-
# Array item in JSON is { "FieldName": { ... } }
|
|
97
|
-
item_json = obj.to_json(definitions)
|
|
98
|
-
result << { field_name => item_json.is_a?(String) ? JSON.parse(item_json) : item_json }
|
|
99
|
-
rescue => e
|
|
100
|
-
break
|
|
101
|
-
end
|
|
92
|
+
break if parser.peek == ARRAY_END_MARKER
|
|
93
|
+
|
|
94
|
+
# Each item is an STObject behind its own field header, e.g. "Signer".
|
|
95
|
+
field_header = parser.read_field_header
|
|
96
|
+
field_name = definitions.get_field_name_from_header(field_header)
|
|
97
|
+
obj = STObject.from_parser(parser)
|
|
98
|
+
|
|
99
|
+
result << { field_name => obj.to_json(definitions) }
|
|
102
100
|
end
|
|
103
101
|
result
|
|
104
102
|
end
|
|
@@ -119,9 +119,14 @@ module BinaryCodec
|
|
|
119
119
|
STObject.new(list.to_bytes)
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
#
|
|
122
|
+
# The JSON interpretation of self.bytes.
|
|
123
123
|
#
|
|
124
|
-
#
|
|
124
|
+
# Returns a Hash, not a JSON string. It used to return a string, which
|
|
125
|
+
# forced every nested value to be serialised and parsed again on the way
|
|
126
|
+
# out - and that round trip is what turned native XRP amounts into
|
|
127
|
+
# integers, because JSON.parse("370000000") is a number.
|
|
128
|
+
#
|
|
129
|
+
# @return [Hash] The decoded object
|
|
125
130
|
def to_json(_definitions = nil, _field_name = nil)
|
|
126
131
|
definitions = _definitions || Definitions.instance
|
|
127
132
|
parser = BinaryParser.new(to_hex)
|
|
@@ -129,7 +134,7 @@ module BinaryCodec
|
|
|
129
134
|
|
|
130
135
|
until parser.end?
|
|
131
136
|
begin
|
|
132
|
-
# Check if we are at the end marker (0xE1)
|
|
137
|
+
# Check if we are at the end marker (0xE1)
|
|
133
138
|
break if parser.peek == 0xE1
|
|
134
139
|
|
|
135
140
|
field = parser.read_field
|
|
@@ -142,21 +147,18 @@ module BinaryCodec
|
|
|
142
147
|
else
|
|
143
148
|
value_obj = parser.read_field_value(field)
|
|
144
149
|
value = value_obj.to_json(definitions, field.name)
|
|
145
|
-
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
|
|
151
|
+
# Some types still hand back a serialised structure. Parse only
|
|
152
|
+
# what actually is one: an Amount for XRP is the string
|
|
153
|
+
# "370000000", and parsing that yields a number where rippled has
|
|
154
|
+
# a string.
|
|
155
|
+
value = JSON.parse(value) if value.is_a?(String) && value.start_with?('{', '[')
|
|
150
156
|
end
|
|
151
157
|
accumulator[field.name] = value
|
|
152
|
-
rescue => e
|
|
153
|
-
break
|
|
154
158
|
end
|
|
155
159
|
end
|
|
156
160
|
|
|
157
|
-
|
|
158
|
-
# To satisfy spec/binary-codec/types/st_object_spec.rb:10
|
|
159
|
-
JSON.generate(accumulator)
|
|
161
|
+
accumulator
|
|
160
162
|
end
|
|
161
163
|
|
|
162
164
|
private
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
module BinaryCodec
|
|
4
4
|
|
|
5
5
|
class Uint < ComparableSerializedType
|
|
6
|
+
# UInt64 fields that rippled renders in base 10 rather than as hex. They
|
|
7
|
+
# hold MPToken amounts, where a hex string would be a needless surprise.
|
|
8
|
+
BASE10_UINT64_FIELDS = %w[
|
|
9
|
+
MaximumAmount
|
|
10
|
+
OutstandingAmount
|
|
11
|
+
MPTAmount
|
|
12
|
+
LockedAmount
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
6
15
|
# Returns the width of the Uint type in bytes.
|
|
7
16
|
# @return [Integer] The width.
|
|
8
17
|
def self.width
|
|
@@ -93,11 +102,20 @@ module BinaryCodec
|
|
|
93
102
|
end
|
|
94
103
|
end
|
|
95
104
|
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
# rippled renders the narrow unsigned integers as JSON numbers and UInt64
|
|
106
|
+
# as a 16 digit hex string, because a UInt64 does not survive a round trip
|
|
107
|
+
# through a JSON number. The MPToken amount fields are the exception to
|
|
108
|
+
# that exception: they are UInt64 but carry a base 10 string.
|
|
109
|
+
#
|
|
110
|
+
# Everything wider than 8 bytes (Uint96 and up) is hash-like and stays
|
|
111
|
+
# hex. Do not widen the numeric branch to cover it.
|
|
112
|
+
val = value_of
|
|
113
|
+
return val if self.class.width < 8
|
|
114
|
+
return val.to_s if self.class.width == 8 && BASE10_UINT64_FIELDS.include?(_field_name)
|
|
115
|
+
|
|
116
|
+
# Hex is unsigned, so a negative signed value has to wrap first.
|
|
117
|
+
val += (1 << (self.class.width * 8)) if val < 0
|
|
118
|
+
val.to_s(16).upcase.rjust(self.class.width * 2, '0')
|
|
101
119
|
end
|
|
102
120
|
# @param other [Uint] The other Uint to compare to.
|
|
103
121
|
# @return [Integer] Comparison result (-1, 0, or 1).
|
|
@@ -15,9 +15,9 @@ module BinaryCodec
|
|
|
15
15
|
|
|
16
16
|
if value.is_a?(::Hash)
|
|
17
17
|
bytes = []
|
|
18
|
-
bytes.concat(
|
|
18
|
+
bytes.concat(door_bytes(value['LockingChainDoor']))
|
|
19
19
|
bytes.concat(Issue.from(value['LockingChainIssue']).to_bytes)
|
|
20
|
-
bytes.concat(
|
|
20
|
+
bytes.concat(door_bytes(value['IssuingChainDoor']))
|
|
21
21
|
bytes.concat(Issue.from(value['IssuingChainIssue']).to_bytes)
|
|
22
22
|
return XChainBridge.new(bytes)
|
|
23
23
|
end
|
|
@@ -25,10 +25,23 @@ module BinaryCodec
|
|
|
25
25
|
raise StandardError, "Cannot construct XChainBridge from #{value.class}"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# An account inside a bridge carries the same length prefix as a
|
|
29
|
+
# standalone AccountID field. Leaving it off produces bytes that decode
|
|
30
|
+
# as something else entirely, which is why every XChain fixture failed
|
|
31
|
+
# to serialise.
|
|
32
|
+
DOOR_LENGTH_PREFIX = 0x14
|
|
33
|
+
|
|
34
|
+
def self.door_bytes(account)
|
|
35
|
+
[DOOR_LENGTH_PREFIX] + AccountId.from(account).to_bytes
|
|
36
|
+
end
|
|
37
|
+
private_class_method :door_bytes
|
|
38
|
+
|
|
28
39
|
def self.from_parser(parser, _hint = nil)
|
|
29
40
|
bytes = []
|
|
41
|
+
bytes.concat(parser.read(1)) # length prefix
|
|
30
42
|
bytes.concat(parser.read(20)) # LockingChainDoor
|
|
31
43
|
bytes.concat(Issue.from_parser(parser, 40).to_bytes) # LockingChainIssue
|
|
44
|
+
bytes.concat(parser.read(1)) # length prefix
|
|
32
45
|
bytes.concat(parser.read(20)) # IssuingChainDoor
|
|
33
46
|
bytes.concat(Issue.from_parser(parser, 40).to_bytes) # IssuingChainIssue
|
|
34
47
|
XChainBridge.new(bytes)
|
|
@@ -37,8 +50,10 @@ module BinaryCodec
|
|
|
37
50
|
def to_json(_definitions = nil, _field_name = nil)
|
|
38
51
|
parser = BinaryParser.new(to_hex)
|
|
39
52
|
result = {}
|
|
53
|
+
parser.read(1) # length prefix
|
|
40
54
|
result['LockingChainDoor'] = AccountId.from_parser(parser).to_json
|
|
41
55
|
result['LockingChainIssue'] = Issue.from_parser(parser, 40).to_json
|
|
56
|
+
parser.read(1) # length prefix
|
|
42
57
|
result['IssuingChainDoor'] = AccountId.from_parser(parser).to_json
|
|
43
58
|
result['IssuingChainIssue'] = Issue.from_parser(parser, 40).to_json
|
|
44
59
|
result
|
data/lib/xrpl/client.rb
CHANGED
|
@@ -35,6 +35,19 @@ module XRPL
|
|
|
35
35
|
|
|
36
36
|
attr_reader :url, :connection
|
|
37
37
|
|
|
38
|
+
# Accepts a transaction as a plain Hash or as an XRPL::Transaction and
|
|
39
|
+
# hands back the Hash the rest of the pipeline works with. Anything else
|
|
40
|
+
# is passed through untouched, so a pre-signed blob still reaches submit.
|
|
41
|
+
#
|
|
42
|
+
# @param transaction [Hash, XRPL::Transaction, Object]
|
|
43
|
+
# @return [Hash, Object]
|
|
44
|
+
def self.to_transaction_hash(transaction)
|
|
45
|
+
return transaction if transaction.is_a?(Hash)
|
|
46
|
+
return transaction.to_h if transaction.is_a?(XRPL::Transaction)
|
|
47
|
+
|
|
48
|
+
transaction
|
|
49
|
+
end
|
|
50
|
+
|
|
38
51
|
# @param url [String, Symbol] a network alias (:testnet/:mainnet/:devnet) or a WebSocket URL.
|
|
39
52
|
# @param logger [Logger, nil] optional logger for diagnostic messages. When nil
|
|
40
53
|
# (the default), the client stays silent — a library must not write to the
|
|
@@ -306,11 +319,11 @@ module XRPL
|
|
|
306
319
|
# Fills in the fields a transaction needs before signing: +Sequence+, +Fee+
|
|
307
320
|
# and +LastLedgerSequence+. Existing values are never overwritten.
|
|
308
321
|
#
|
|
309
|
-
# @param transaction [Hash] the
|
|
322
|
+
# @param transaction [Hash, XRPL::Transaction] the transaction to complete.
|
|
310
323
|
# @param signers_count [Integer] number of signatures for multisign fee scaling.
|
|
311
324
|
# @return [Hash] a copy of the transaction with the missing fields filled in.
|
|
312
325
|
def autofill(transaction, signers_count: 0)
|
|
313
|
-
tx = transaction.dup
|
|
326
|
+
tx = self.class.to_transaction_hash(transaction).dup
|
|
314
327
|
tx['Sequence'] ||= fetch_sequence(tx.fetch('Account'))
|
|
315
328
|
tx['Fee'] ||= calculate_fee(signers_count)
|
|
316
329
|
tx['LastLedgerSequence'] ||= current_ledger_index + LEDGER_OFFSET
|
|
@@ -399,7 +412,8 @@ module XRPL
|
|
|
399
412
|
def prepare_for_submit(transaction, wallet:, autofill:)
|
|
400
413
|
raise ArgumentError, 'wallet: is required to sign the transaction' if wallet.nil?
|
|
401
414
|
|
|
402
|
-
tx =
|
|
415
|
+
tx = self.class.to_transaction_hash(transaction)
|
|
416
|
+
tx = tx.dup if tx.is_a?(Hash)
|
|
403
417
|
tx = autofill(tx) if autofill && tx.is_a?(Hash)
|
|
404
418
|
|
|
405
419
|
signed = wallet.sign(tx)
|