xrpl-ruby 0.0.3 → 0.5.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/lib/address-codec/address_codec.rb +22 -4
- data/lib/address-codec/codec.rb +15 -2
- data/lib/address-codec/xrp_codec.rb +29 -2
- data/lib/binary-codec/binary_codec.rb +62 -0
- data/lib/binary-codec/enums/constants.rb +8 -0
- data/lib/binary-codec/enums/definitions.json +3774 -0
- data/lib/binary-codec/enums/definitions.rb +90 -0
- data/lib/binary-codec/enums/fields.rb +104 -0
- data/lib/binary-codec/serdes/binary_parser.rb +183 -0
- data/lib/binary-codec/serdes/binary_serializer.rb +93 -0
- data/lib/binary-codec/serdes/bytes_list.rb +47 -0
- data/lib/binary-codec/types/account_id.rb +60 -0
- data/lib/binary-codec/types/amount.rb +304 -0
- data/lib/binary-codec/types/blob.rb +41 -0
- data/lib/binary-codec/types/currency.rb +116 -0
- data/lib/binary-codec/types/hash.rb +106 -0
- data/lib/binary-codec/types/issue.rb +50 -0
- data/lib/binary-codec/types/path_set.rb +93 -0
- data/lib/binary-codec/types/serialized_type.rb +157 -0
- data/lib/binary-codec/types/st_array.rb +71 -0
- data/lib/binary-codec/types/st_object.rb +157 -0
- data/lib/binary-codec/types/uint.rb +166 -0
- data/lib/binary-codec/types/vector256.rb +53 -0
- data/lib/binary-codec/types/xchain_bridge.rb +47 -0
- data/lib/binary-codec/utilities.rb +98 -0
- data/lib/core/base_58_xrp.rb +2 -0
- data/lib/core/base_x.rb +10 -0
- data/lib/core/core.rb +79 -6
- data/lib/core/utilities.rb +38 -0
- data/lib/key-pairs/ed25519.rb +64 -0
- data/lib/key-pairs/key_pairs.rb +92 -0
- data/lib/key-pairs/secp256k1.rb +116 -0
- data/lib/wallet/wallet.rb +117 -0
- data/lib/xrpl-ruby.rb +32 -1
- metadata +44 -3
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bigdecimal'
|
|
4
|
+
require 'bigdecimal/util'
|
|
5
|
+
|
|
6
|
+
module BinaryCodec
|
|
7
|
+
class Amount < SerializedType
|
|
8
|
+
|
|
9
|
+
DEFAULT_AMOUNT_HEX = "4000000000000000".freeze
|
|
10
|
+
ZERO_CURRENCY_AMOUNT_HEX = "8000000000000000".freeze
|
|
11
|
+
NATIVE_AMOUNT_BYTE_LENGTH = 8
|
|
12
|
+
CURRENCY_AMOUNT_BYTE_LENGTH = 48
|
|
13
|
+
MAX_IOU_PRECISION = 16
|
|
14
|
+
MIN_IOU_EXPONENT = -96
|
|
15
|
+
MAX_IOU_EXPONENT = 80
|
|
16
|
+
|
|
17
|
+
MAX_DROPS = BigDecimal("1e17")
|
|
18
|
+
MIN_XRP = BigDecimal("1e-6")
|
|
19
|
+
|
|
20
|
+
def initialize(bytes = nil)
|
|
21
|
+
if bytes.nil?
|
|
22
|
+
bytes = hex_to_bytes(DEFAULT_AMOUNT_HEX)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@bytes = bytes
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Construct an amount from an IOU, MPT, or string amount
|
|
29
|
+
#
|
|
30
|
+
# @param value [Amount, Hash, String] representing the amount
|
|
31
|
+
# @return [Amount] an Amount object
|
|
32
|
+
# Creates a new Amount instance from a value.
|
|
33
|
+
# @param value [Amount, String, Hash, Integer] The value to convert.
|
|
34
|
+
# @return [Amount] The created instance.
|
|
35
|
+
def self.from(value)
|
|
36
|
+
return value if value.is_a?(Amount)
|
|
37
|
+
|
|
38
|
+
amount = Array.new(8, 0) # Equivalent to a Uint8Array of 8 zeros
|
|
39
|
+
|
|
40
|
+
if value.is_a?(String)
|
|
41
|
+
Amount.assert_xrp_is_valid(value)
|
|
42
|
+
|
|
43
|
+
number = value.to_i # Use to_i for equivalent BigInt handling
|
|
44
|
+
|
|
45
|
+
int_buf = [Array.new(4, 0), Array.new(4, 0)]
|
|
46
|
+
BinaryCodec.write_uint32be(int_buf[0], (number >> 32) & 0xFFFFFFFF, 0)
|
|
47
|
+
BinaryCodec.write_uint32be(int_buf[1], number & 0xFFFFFFFF, 0)
|
|
48
|
+
|
|
49
|
+
amount = int_buf.flatten
|
|
50
|
+
|
|
51
|
+
amount[0] |= 0x40
|
|
52
|
+
|
|
53
|
+
return Amount.new(amount)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if is_amount_object_iou?(value)
|
|
57
|
+
number = BigDecimal(value[:value])
|
|
58
|
+
self.assert_iou_is_valid(number)
|
|
59
|
+
|
|
60
|
+
if number.zero?
|
|
61
|
+
amount[0] |= 0x80
|
|
62
|
+
else
|
|
63
|
+
scale = number.frac.to_s('F').split('.').last.size
|
|
64
|
+
unscaled_value = (number * (10**scale)).to_i
|
|
65
|
+
int_string = unscaled_value.abs.to_s.ljust(16, '0')
|
|
66
|
+
num = int_string.to_i
|
|
67
|
+
|
|
68
|
+
int_buf = [Array.new(4, 0), Array.new(4, 0)]
|
|
69
|
+
BinaryCodec.write_uint32be(int_buf[0], (num >> 32) & 0xFFFFFFFF)
|
|
70
|
+
BinaryCodec.write_uint32be(int_buf[1], num & 0xFFFFFFFF)
|
|
71
|
+
|
|
72
|
+
amount = int_buf.flatten
|
|
73
|
+
|
|
74
|
+
amount[0] |= 0x80
|
|
75
|
+
|
|
76
|
+
if number > 0
|
|
77
|
+
amount[0] |= 0x40
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
exponent = number.exponent - 16
|
|
81
|
+
exponent_byte = 97 + exponent
|
|
82
|
+
amount[0] |= exponent_byte >> 2
|
|
83
|
+
amount[1] |= (exponent_byte & 0x03) << 6
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
currency = Currency.from(value[:currency]).to_bytes
|
|
87
|
+
issuer = AccountId.from(value[:issuer]).to_bytes
|
|
88
|
+
|
|
89
|
+
return Amount.new(amount + currency + issuer)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Read an amount from a BinaryParser
|
|
95
|
+
#
|
|
96
|
+
# @param parser [BinaryParser] The BinaryParser to read the Amount from
|
|
97
|
+
# @return [Amount] An Amount bundle exec rspec spec/binary-codec/types/st_object_spec.rb object
|
|
98
|
+
# Creates an Amount instance from a parser.
|
|
99
|
+
# @param parser [BinaryParser] The parser to read from.
|
|
100
|
+
# @param _size_hint [Integer, nil] Optional size hint (unused).
|
|
101
|
+
# @return [Amount] The created instance.
|
|
102
|
+
def self.from_parser(parser, _size_hint = nil)
|
|
103
|
+
is_iou = parser.peek & 0x80 != 0
|
|
104
|
+
return Amount.new(parser.read(48)) if is_iou
|
|
105
|
+
|
|
106
|
+
# The amount can be either MPT or XRP at this point
|
|
107
|
+
is_mpt = parser.peek & 0x20 != 0
|
|
108
|
+
num_bytes = is_mpt ? 33 : 8
|
|
109
|
+
Amount.new(parser.read(num_bytes))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The JSON representation of this Amount
|
|
113
|
+
#
|
|
114
|
+
# @return [Hash, String] The JSON interpretation of this.bytes
|
|
115
|
+
# Returns the JSON representation of the Amount.
|
|
116
|
+
# @param _definitions [Definitions, nil] Unused.
|
|
117
|
+
# @param _field_name [String, nil] Optional field name.
|
|
118
|
+
# @return [String, Hash] The JSON representation.
|
|
119
|
+
def to_json(_definitions = nil, _field_name = nil)
|
|
120
|
+
if is_native?
|
|
121
|
+
bytes = @bytes.dup
|
|
122
|
+
is_positive = (bytes[0] & 0x40) != 0
|
|
123
|
+
sign = is_positive ? '' : '-'
|
|
124
|
+
bytes[0] &= 0x3f
|
|
125
|
+
|
|
126
|
+
msb = BinaryCodec.read_uint32be(bytes[0, 4])
|
|
127
|
+
lsb = BinaryCodec.read_uint32be(bytes[4, 4])
|
|
128
|
+
num = (msb << 32) | lsb
|
|
129
|
+
|
|
130
|
+
return "#{sign}#{num}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
if is_iou?
|
|
134
|
+
parser = BinaryParser.new(to_hex)
|
|
135
|
+
mantissa_bytes = parser.read(8)
|
|
136
|
+
currency = Currency.from_parser(parser)
|
|
137
|
+
issuer = AccountId.from_parser(parser)
|
|
138
|
+
|
|
139
|
+
b1 = mantissa_bytes[0]
|
|
140
|
+
b2 = mantissa_bytes[1]
|
|
141
|
+
|
|
142
|
+
is_positive = (b1 & 0x40) != 0
|
|
143
|
+
sign = is_positive ? '' : '-'
|
|
144
|
+
exponent = ((b1 & 0x3f) << 2) + ((b2 & 0xff) >> 6) - 97
|
|
145
|
+
|
|
146
|
+
mantissa_bytes[0] = 0
|
|
147
|
+
mantissa_bytes[1] &= 0x3f
|
|
148
|
+
|
|
149
|
+
# Convert mantissa bytes to integer
|
|
150
|
+
mantissa_int = mantissa_bytes.reduce(0) { |acc, b| (acc << 8) + b }
|
|
151
|
+
|
|
152
|
+
value = BigDecimal(mantissa_int) * (BigDecimal(10)**exponent)
|
|
153
|
+
value = -value unless is_positive
|
|
154
|
+
self.class.assert_iou_is_valid(value)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"value" => value.to_s('F').sub(/\.0$/, ''),
|
|
158
|
+
"currency" => currency.to_json,
|
|
159
|
+
"issuer" => issuer.to_json
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if is_mpt?
|
|
164
|
+
parser = BinaryParser.new(to_hex)
|
|
165
|
+
leading_byte = parser.read(1)
|
|
166
|
+
amount_bytes = parser.read(8)
|
|
167
|
+
mpt_id = Hash192.from_parser(parser)
|
|
168
|
+
|
|
169
|
+
is_positive = (leading_byte[0] & 0x40) != 0
|
|
170
|
+
sign = is_positive ? '' : '-'
|
|
171
|
+
|
|
172
|
+
msb = BinaryCodec.read_uint32be(amount_bytes[0, 4])
|
|
173
|
+
lsb = BinaryCodec.read_uint32be(amount_bytes[4, 4])
|
|
174
|
+
num = (msb << 32) | lsb
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
"value" => "#{sign}#{num}",
|
|
178
|
+
"mpt_issuance_id" => mpt_id.to_hex
|
|
179
|
+
}
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
raise 'Invalid amount to construct JSON'
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
# Type guard for AmountObjectIOU
|
|
188
|
+
def self.is_amount_object_iou?(arg)
|
|
189
|
+
keys = arg.transform_keys(&:to_s).keys.sort
|
|
190
|
+
|
|
191
|
+
keys.length == 3 &&
|
|
192
|
+
keys[0] == 'currency' &&
|
|
193
|
+
keys[1] == 'issuer' &&
|
|
194
|
+
keys[2] == 'value'
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Type guard for AmountObjectMPT
|
|
198
|
+
def self.is_amount_object_mpt?(arg)
|
|
199
|
+
keys = arg.keys.sort
|
|
200
|
+
|
|
201
|
+
keys.length == 2 &&
|
|
202
|
+
keys[0] == 'mpt_issuance_id' &&
|
|
203
|
+
keys[1] == 'value'
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Validate XRP amount
|
|
207
|
+
#
|
|
208
|
+
# @param amount [String] representing XRP amount
|
|
209
|
+
# @return [void], but raises an exception if the amount is invalid
|
|
210
|
+
def self.assert_xrp_is_valid(amount)
|
|
211
|
+
if amount.include?('.')
|
|
212
|
+
raise "#{amount} is an illegal amount"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
decimal = BigDecimal(amount)
|
|
216
|
+
unless decimal.zero?
|
|
217
|
+
if decimal < MIN_XRP || decimal > MAX_DROPS
|
|
218
|
+
raise "#{amount} is an illegal amount"
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Validate IOU.value amount
|
|
224
|
+
#
|
|
225
|
+
# @param decimal [BigDecimal] object representing IOU.value
|
|
226
|
+
# @raise [ArgumentError] if the amount is invalid
|
|
227
|
+
def self.assert_iou_is_valid(decimal)
|
|
228
|
+
return if decimal.zero?
|
|
229
|
+
|
|
230
|
+
p = decimal.precision
|
|
231
|
+
e = (decimal.exponent || 0) - 15
|
|
232
|
+
|
|
233
|
+
if p > MAX_IOU_PRECISION || e > MAX_IOU_EXPONENT || e < MIN_IOU_EXPONENT
|
|
234
|
+
raise ArgumentError, 'Decimal precision out of range'
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
verify_no_decimal(decimal)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Validate MPT.value amount
|
|
241
|
+
#
|
|
242
|
+
# @param amount [String] representing MPT.value
|
|
243
|
+
# @return [void], but raises an exception if the amount is invalid
|
|
244
|
+
def self.assert_mpt_is_valid(amount)
|
|
245
|
+
if amount.include?('.')
|
|
246
|
+
raise "#{amount} is an illegal amount"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
decimal = BigDecimal(amount)
|
|
250
|
+
unless decimal.zero?
|
|
251
|
+
if decimal < BigDecimal("0")
|
|
252
|
+
raise "#{amount} is an illegal amount"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
if (amount.to_i & mpt_mask) != 0
|
|
256
|
+
raise "#{amount} is an illegal amount"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Ensure that the value, after being multiplied by the exponent, does not
|
|
262
|
+
# contain a decimal. This function is typically used to validate numbers
|
|
263
|
+
# that need to be represented as precise integers after scaling, such as
|
|
264
|
+
# amounts in financial transactions. Example failure:1.1234567891234567
|
|
265
|
+
#
|
|
266
|
+
# @param decimal [BigDecimal] A BigDecimal object
|
|
267
|
+
# @raise [ArgumentError] if the value contains a decimal
|
|
268
|
+
# @return [String] The decimal converted to a string without a decimal point
|
|
269
|
+
def self.verify_no_decimal(decimal)
|
|
270
|
+
exponent = -((decimal.exponent || 0) - 16)
|
|
271
|
+
scaled_decimal = decimal * 10 ** exponent
|
|
272
|
+
|
|
273
|
+
raise ArgumentError, 'Decimal place found in int_string' unless scaled_decimal.frac == 0
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Check if this amount is in units of Native Currency (XRP)
|
|
277
|
+
#
|
|
278
|
+
# @return [Boolean] true if Native (XRP)
|
|
279
|
+
# Checks if the amount is a native XRP amount.
|
|
280
|
+
# @return [Boolean] True if native, false otherwise.
|
|
281
|
+
def is_native?
|
|
282
|
+
(self.bytes[0] & 0x80).zero? && (self.bytes[0] & 0x20).zero?
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Check if this amount is in units of MPT
|
|
286
|
+
#
|
|
287
|
+
# @return [Boolean] true if MPT
|
|
288
|
+
# Checks if the amount is a multi-purpose token (MPT).
|
|
289
|
+
# @return [Boolean] True if MPT, false otherwise.
|
|
290
|
+
def is_mpt?
|
|
291
|
+
(self.bytes[0] & 0x80).zero? && (self.bytes[0] & 0x20) != 0
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Check if this amount is in units of IOU
|
|
295
|
+
#
|
|
296
|
+
# @return [Boolean] true if IOU
|
|
297
|
+
# Checks if the amount is an IOU amount.
|
|
298
|
+
# @return [Boolean] True if IOU, false otherwise.
|
|
299
|
+
def is_iou?
|
|
300
|
+
(self.bytes[0] & 0x80) != 0
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
end
|
|
304
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BinaryCodec
|
|
4
|
+
class Blob < SerializedType
|
|
5
|
+
|
|
6
|
+
def initialize(byte_buf = nil)
|
|
7
|
+
super(byte_buf || [])
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Creates a new Blob instance from a value.
|
|
11
|
+
# @param value [Blob, String, Array<Integer>] The value to convert.
|
|
12
|
+
# @return [Blob] The created instance.
|
|
13
|
+
def self.from(value)
|
|
14
|
+
return value if value.is_a?(Blob)
|
|
15
|
+
|
|
16
|
+
if value.is_a?(String)
|
|
17
|
+
unless valid_hex?(value)
|
|
18
|
+
raise StandardError, 'Cannot construct Blob from a non-hex string'
|
|
19
|
+
end
|
|
20
|
+
return Blob.new(hex_to_bytes(value))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if value.is_a?(Array)
|
|
24
|
+
return Blob.new(value)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
raise StandardError, 'Cannot construct Blob from value given'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Creates a Blob instance from a parser.
|
|
31
|
+
# @param parser [BinaryParser] The parser to read from.
|
|
32
|
+
# @param hint [Integer, nil] Optional width hint.
|
|
33
|
+
# @return [Blob] The created instance.
|
|
34
|
+
def self.from_parser(parser, hint = nil)
|
|
35
|
+
new(parser.read(hint))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../core/core'
|
|
4
|
+
|
|
5
|
+
module BinaryCodec
|
|
6
|
+
class Currency < Hash160
|
|
7
|
+
|
|
8
|
+
XRP_HEX_REGEX = /^0{40}$/
|
|
9
|
+
ISO_REGEX = /^[A-Z0-9a-z?!@#$%^&*(){}\[\]\|]{3}$/
|
|
10
|
+
HEX_REGEX = /^[A-F0-9]{40}$/
|
|
11
|
+
STANDARD_FORMAT_HEX_REGEX = /^0{24}[\x00-\x7F]{6}0{10}$/
|
|
12
|
+
|
|
13
|
+
attr_reader :iso
|
|
14
|
+
|
|
15
|
+
@width = 20
|
|
16
|
+
|
|
17
|
+
def initialize(byte_buf = nil)
|
|
18
|
+
super(byte_buf)
|
|
19
|
+
hex = to_hex
|
|
20
|
+
|
|
21
|
+
if XRP_HEX_REGEX.match?(hex)
|
|
22
|
+
@_iso = 'XRP'
|
|
23
|
+
elsif STANDARD_FORMAT_HEX_REGEX.match?(hex)
|
|
24
|
+
@_iso = iso_code_from_hex(@bytes[12..14])
|
|
25
|
+
else
|
|
26
|
+
@_iso = nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the ISO code of the currency, if applicable.
|
|
31
|
+
# @return [String, nil] The ISO code.
|
|
32
|
+
def iso
|
|
33
|
+
@_iso
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Creates a new Currency instance from a value.
|
|
37
|
+
# @param value [Currency, String, Array<Integer>] The value to convert.
|
|
38
|
+
# @return [Currency] The created instance.
|
|
39
|
+
def self.from(value)
|
|
40
|
+
return value if value.is_a?(Currency)
|
|
41
|
+
|
|
42
|
+
if value.is_a?(String)
|
|
43
|
+
return new(bytes_from_representation(value))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if value.is_a?(Array)
|
|
47
|
+
return new(value)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
raise StandardError, "Cannot construct Currency from #{value.class}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Returns the JSON representation of the currency.
|
|
54
|
+
# @return [String] The ISO code or hex string.
|
|
55
|
+
def to_json
|
|
56
|
+
iso = self.iso
|
|
57
|
+
return iso unless iso.nil?
|
|
58
|
+
|
|
59
|
+
bytes_to_hex(@bytes)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def iso_code_from_hex(code)
|
|
65
|
+
iso = hex_to_string(bytes_to_hex(code))
|
|
66
|
+
return nil if iso == 'XRP'
|
|
67
|
+
return iso if is_iso_code(iso)
|
|
68
|
+
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def is_iso_code(iso)
|
|
73
|
+
!!(ISO_REGEX.match(iso)) # Equivalent to test for regex
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.bytes_from_representation(input)
|
|
77
|
+
unless is_valid_representation(input)
|
|
78
|
+
raise StandardError, "Unsupported Currency representation: #{input}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
input.length == 3 ? self.iso_to_bytes(input) : hex_to_bytes(input)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.iso_to_bytes(iso)
|
|
85
|
+
bytes = Array.new(20, 0) # Equivalent to Uint8Array(20)
|
|
86
|
+
if iso != 'XRP'
|
|
87
|
+
iso_bytes = iso.chars.map(&:ord)
|
|
88
|
+
# Insert iso_bytes at index 12
|
|
89
|
+
bytes[12, iso_bytes.length] = iso_bytes
|
|
90
|
+
end
|
|
91
|
+
bytes
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.is_valid_representation(input)
|
|
95
|
+
if input.is_a?(Array)
|
|
96
|
+
self.is_bytes_array(input)
|
|
97
|
+
else
|
|
98
|
+
self.is_string_representation(input)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.is_string_representation(input)
|
|
103
|
+
input.length == 3 || is_hex(input)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def self.is_bytes_array(bytes)
|
|
107
|
+
bytes.length == 20
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.is_hex(hex)
|
|
111
|
+
!!(HEX_REGEX.match(hex)) # Special case for Vurrency type, do not conflate with valid_hex? function
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BinaryCodec
|
|
4
|
+
class Hash < ComparableSerializedType
|
|
5
|
+
# Returns the width of the Hash type in bytes.
|
|
6
|
+
# @return [Integer] The width.
|
|
7
|
+
def self.width
|
|
8
|
+
@width
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(bytes = nil)
|
|
12
|
+
bytes = Array.new(self.class.width, 0) if bytes.nil? || bytes.empty?
|
|
13
|
+
super(bytes)
|
|
14
|
+
if @bytes.length != self.class.width
|
|
15
|
+
raise StandardError, "Invalid Hash length #{@bytes.length}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Creates a new Hash instance from a value.
|
|
20
|
+
# @param value [Hash, String, Array<Integer>] The value to convert.
|
|
21
|
+
# @return [Hash] The created instance.
|
|
22
|
+
def self.from(value)
|
|
23
|
+
return value if value.is_a?(self)
|
|
24
|
+
|
|
25
|
+
if value.is_a?(String)
|
|
26
|
+
return new if value.empty?
|
|
27
|
+
return new(hex_to_bytes(value))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if value.is_a?(Array)
|
|
31
|
+
return new(value)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
raise StandardError, "Cannot construct #{self} from the value given"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Creates a Hash instance from a parser.
|
|
38
|
+
# @param parser [BinaryParser] The parser to read from.
|
|
39
|
+
# @param hint [Integer, nil] Optional width hint.
|
|
40
|
+
# @return [Hash] The created instance.
|
|
41
|
+
def self.from_parser(parser, hint = nil)
|
|
42
|
+
new(parser.read(hint || width))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Compares this Hash to another Hash.
|
|
46
|
+
# @param other [Hash] The other Hash to compare to.
|
|
47
|
+
# @return [Integer] Comparison result (-1, 0, or 1).
|
|
48
|
+
def compare_to(other)
|
|
49
|
+
@bytes <=> other.bytes
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns four bits at the specified depth within a hash
|
|
53
|
+
#
|
|
54
|
+
# @param depth [Integer] The depth of the four bits
|
|
55
|
+
# @return [Integer] The number represented by the four bits
|
|
56
|
+
def nibblet(depth)
|
|
57
|
+
byte_index = depth > 0 ? (depth / 2).floor : 0
|
|
58
|
+
b = bytes[byte_index]
|
|
59
|
+
if depth.even?
|
|
60
|
+
(b & 0xf0) >> 4
|
|
61
|
+
else
|
|
62
|
+
b & 0x0f
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class Hash128 < Hash
|
|
69
|
+
@width = 16
|
|
70
|
+
|
|
71
|
+
def initialize(bytes = nil)
|
|
72
|
+
super(bytes)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_hex
|
|
76
|
+
hex = bytes_to_hex(to_bytes)
|
|
77
|
+
return '' if hex.match?(/^0+$/)
|
|
78
|
+
hex
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class Hash160 < Hash
|
|
83
|
+
@width = 20
|
|
84
|
+
|
|
85
|
+
def initialize(bytes = nil)
|
|
86
|
+
super(bytes)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class Hash192 < Hash
|
|
91
|
+
@width = 24
|
|
92
|
+
|
|
93
|
+
def initialize(bytes = nil)
|
|
94
|
+
super(bytes)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
class Hash256 < Hash
|
|
99
|
+
@width = 32
|
|
100
|
+
|
|
101
|
+
def initialize(bytes = nil)
|
|
102
|
+
super(bytes)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BinaryCodec
|
|
4
|
+
class Issue < SerializedType
|
|
5
|
+
def initialize(bytes = nil)
|
|
6
|
+
super(bytes || [])
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.from(value)
|
|
10
|
+
return value if value.is_a?(Issue)
|
|
11
|
+
|
|
12
|
+
if value.is_a?(String)
|
|
13
|
+
return Issue.new(hex_to_bytes(value))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if value.is_a?(Hash) || value.is_a?(::Hash)
|
|
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
|
|
22
|
+
|
|
23
|
+
raise StandardError, "Cannot construct Issue from #{value.class}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.from_parser(parser, _hint = nil)
|
|
27
|
+
bytes = []
|
|
28
|
+
bytes.concat(parser.read(20)) # Currency
|
|
29
|
+
# If there are more bytes in this field, it might have an issuer?
|
|
30
|
+
# Actually Issue is often fixed length 20 or 40.
|
|
31
|
+
# For XChainBridge it uses Issue.
|
|
32
|
+
# Let's see how much we should read.
|
|
33
|
+
# Usually if it's an Issue in a field, we might know the size.
|
|
34
|
+
# For now, let's assume it can be 20 or 40.
|
|
35
|
+
# But wait, how does the parser know?
|
|
36
|
+
# If it's not variable length, it must have a fixed width or be the rest of the object.
|
|
37
|
+
# Definitions.json says Issue is type 24.
|
|
38
|
+
bytes.concat(parser.read(20)) unless parser.end? # Try reading issuer
|
|
39
|
+
Issue.new(bytes)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def to_json(_definitions = nil, _field_name = nil)
|
|
43
|
+
parser = BinaryParser.new(to_hex)
|
|
44
|
+
result = {}
|
|
45
|
+
result['currency'] = Currency.from_parser(parser).to_json
|
|
46
|
+
result['issuer'] = AccountId.from_parser(parser).to_json unless parser.end?
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BinaryCodec
|
|
4
|
+
class PathSet < SerializedType
|
|
5
|
+
PATH_STEP_END_MARKER = 0xFF
|
|
6
|
+
PATH_END_MARKER = 0xFE
|
|
7
|
+
|
|
8
|
+
TYPE_ACCOUNT = 0x01
|
|
9
|
+
TYPE_CURRENCY = 0x10
|
|
10
|
+
TYPE_ISSUER = 0x20
|
|
11
|
+
|
|
12
|
+
def initialize(bytes = nil)
|
|
13
|
+
super(bytes || [])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.from(value)
|
|
17
|
+
return value if value.is_a?(PathSet)
|
|
18
|
+
|
|
19
|
+
if value.is_a?(String)
|
|
20
|
+
return PathSet.new(hex_to_bytes(value))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if value.is_a?(Array)
|
|
24
|
+
bytes = []
|
|
25
|
+
value.each_with_index do |path, index|
|
|
26
|
+
path.each do |step|
|
|
27
|
+
type = 0
|
|
28
|
+
step_bytes = []
|
|
29
|
+
|
|
30
|
+
if step['account']
|
|
31
|
+
type |= TYPE_ACCOUNT
|
|
32
|
+
step_bytes.concat(AccountId.from(step['account']).to_bytes)
|
|
33
|
+
end
|
|
34
|
+
if step['currency']
|
|
35
|
+
type |= TYPE_CURRENCY
|
|
36
|
+
step_bytes.concat(Currency.from(step['currency']).to_bytes)
|
|
37
|
+
end
|
|
38
|
+
if step['issuer']
|
|
39
|
+
type |= TYPE_ISSUER
|
|
40
|
+
step_bytes.concat(AccountId.from(step['issuer']).to_bytes)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
bytes << type
|
|
44
|
+
bytes.concat(step_bytes)
|
|
45
|
+
end
|
|
46
|
+
bytes << (index == value.length - 1 ? PATH_END_MARKER : PATH_STEP_END_MARKER)
|
|
47
|
+
end
|
|
48
|
+
return PathSet.new(bytes)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
raise StandardError, "Cannot construct PathSet from #{value.class}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.from_parser(parser, _hint = nil)
|
|
55
|
+
bytes = []
|
|
56
|
+
loop do
|
|
57
|
+
type = parser.read_uint8
|
|
58
|
+
bytes << type
|
|
59
|
+
break if type == PATH_END_MARKER
|
|
60
|
+
|
|
61
|
+
if type != PATH_STEP_END_MARKER
|
|
62
|
+
bytes.concat(parser.read(20)) if (type & TYPE_ACCOUNT) != 0
|
|
63
|
+
bytes.concat(parser.read(20)) if (type & TYPE_CURRENCY) != 0
|
|
64
|
+
bytes.concat(parser.read(20)) if (type & TYPE_ISSUER) != 0
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
PathSet.new(bytes)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_json(_definitions = nil, _field_name = nil)
|
|
71
|
+
parser = BinaryParser.new(to_hex)
|
|
72
|
+
paths = []
|
|
73
|
+
current_path = []
|
|
74
|
+
|
|
75
|
+
until parser.end?
|
|
76
|
+
type = parser.read_uint8
|
|
77
|
+
if type == PATH_END_MARKER || type == PATH_STEP_END_MARKER
|
|
78
|
+
paths << current_path
|
|
79
|
+
current_path = []
|
|
80
|
+
break if type == PATH_END_MARKER
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
step = {}
|
|
85
|
+
step['account'] = AccountId.from_parser(parser).to_json if (type & TYPE_ACCOUNT) != 0
|
|
86
|
+
step['currency'] = Currency.from_parser(parser).to_json if (type & TYPE_CURRENCY) != 0
|
|
87
|
+
step['issuer'] = AccountId.from_parser(parser).to_json if (type & TYPE_ISSUER) != 0
|
|
88
|
+
current_path << step
|
|
89
|
+
end
|
|
90
|
+
paths
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|