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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/address-codec/address_codec.rb +22 -4
  3. data/lib/address-codec/codec.rb +15 -2
  4. data/lib/address-codec/xrp_codec.rb +29 -2
  5. data/lib/binary-codec/binary_codec.rb +62 -0
  6. data/lib/binary-codec/enums/constants.rb +8 -0
  7. data/lib/binary-codec/enums/definitions.json +3774 -0
  8. data/lib/binary-codec/enums/definitions.rb +90 -0
  9. data/lib/binary-codec/enums/fields.rb +104 -0
  10. data/lib/binary-codec/serdes/binary_parser.rb +183 -0
  11. data/lib/binary-codec/serdes/binary_serializer.rb +93 -0
  12. data/lib/binary-codec/serdes/bytes_list.rb +47 -0
  13. data/lib/binary-codec/types/account_id.rb +60 -0
  14. data/lib/binary-codec/types/amount.rb +304 -0
  15. data/lib/binary-codec/types/blob.rb +41 -0
  16. data/lib/binary-codec/types/currency.rb +116 -0
  17. data/lib/binary-codec/types/hash.rb +106 -0
  18. data/lib/binary-codec/types/issue.rb +50 -0
  19. data/lib/binary-codec/types/path_set.rb +93 -0
  20. data/lib/binary-codec/types/serialized_type.rb +157 -0
  21. data/lib/binary-codec/types/st_array.rb +71 -0
  22. data/lib/binary-codec/types/st_object.rb +157 -0
  23. data/lib/binary-codec/types/uint.rb +166 -0
  24. data/lib/binary-codec/types/vector256.rb +53 -0
  25. data/lib/binary-codec/types/xchain_bridge.rb +47 -0
  26. data/lib/binary-codec/utilities.rb +98 -0
  27. data/lib/core/base_58_xrp.rb +2 -0
  28. data/lib/core/base_x.rb +10 -0
  29. data/lib/core/core.rb +79 -6
  30. data/lib/core/utilities.rb +38 -0
  31. data/lib/key-pairs/ed25519.rb +64 -0
  32. data/lib/key-pairs/key_pairs.rb +92 -0
  33. data/lib/key-pairs/secp256k1.rb +116 -0
  34. data/lib/wallet/wallet.rb +117 -0
  35. data/lib/xrpl-ruby.rb +32 -1
  36. 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