xrpl-ruby 0.0.1 → 0.2.4
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 +118 -0
- data/lib/address-codec/codec.rb +98 -0
- data/lib/address-codec/xrp_codec.rb +79 -0
- data/lib/binary-codec/binary_codec.rb +38 -0
- data/lib/binary-codec/enums/constants.rb +8 -0
- data/lib/binary-codec/enums/definitions.json +3183 -0
- data/lib/binary-codec/enums/definitions.rb +78 -0
- data/lib/binary-codec/enums/fields.rb +102 -0
- data/lib/binary-codec/serdes/binary_parser.rb +145 -0
- data/lib/binary-codec/serdes/binary_serializer.rb +82 -0
- data/lib/binary-codec/serdes/bytes_list.rb +36 -0
- data/lib/binary-codec/types/account_id.rb +79 -0
- data/lib/binary-codec/types/amount.rb +284 -0
- data/lib/binary-codec/types/blob.rb +32 -0
- data/lib/binary-codec/types/currency.rb +105 -0
- data/lib/binary-codec/types/hash.rb +105 -0
- data/lib/binary-codec/types/serialized_type.rb +133 -0
- data/lib/binary-codec/types/st_object.rb +60 -0
- data/lib/binary-codec/types/uint.rb +53 -0
- data/lib/binary-codec/utilities.rb +80 -0
- data/lib/core/base_58_xrp.rb +12 -0
- data/lib/core/base_x.rb +1 -0
- data/lib/core/core.rb +71 -1
- data/lib/xrpl-ruby.rb +12 -1
- metadata +24 -3
@@ -0,0 +1,284 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'bigdecimal/util'
|
5
|
+
|
6
|
+
require_relative '../utilities'
|
7
|
+
|
8
|
+
module BinaryCodec
|
9
|
+
class Amount < SerializedType
|
10
|
+
|
11
|
+
DEFAULT_AMOUNT_HEX = "4000000000000000".freeze
|
12
|
+
ZERO_CURRENCY_AMOUNT_HEX = "8000000000000000".freeze
|
13
|
+
NATIVE_AMOUNT_BYTE_LENGTH = 8
|
14
|
+
CURRENCY_AMOUNT_BYTE_LENGTH = 48
|
15
|
+
MAX_IOU_PRECISION = 16
|
16
|
+
MIN_IOU_EXPONENT = -96
|
17
|
+
MAX_IOU_EXPONENT = 80
|
18
|
+
|
19
|
+
MAX_DROPS = BigDecimal("1e17")
|
20
|
+
MIN_XRP = BigDecimal("1e-6")
|
21
|
+
|
22
|
+
def initialize(bytes = nil)
|
23
|
+
if bytes.nil?
|
24
|
+
bytes = hex_to_bytes(DEFAULT_AMOUNT_HEX)
|
25
|
+
end
|
26
|
+
|
27
|
+
@bytes = bytes
|
28
|
+
end
|
29
|
+
|
30
|
+
# Construct an amount from an IOU, MPT, or string amount
|
31
|
+
#
|
32
|
+
# @param value [Amount, Hash, String] representing the amount
|
33
|
+
# @return [Amount] an Amount object
|
34
|
+
def self.from(value)
|
35
|
+
return value if value.is_a?(Amount)
|
36
|
+
|
37
|
+
amount = Array.new(8, 0) # Equivalent to a Uint8Array of 8 zeros
|
38
|
+
|
39
|
+
if value.is_a?(String)
|
40
|
+
Amount.assert_xrp_is_valid(value)
|
41
|
+
|
42
|
+
number = value.to_i # Use to_i for equivalent BigInt handling
|
43
|
+
|
44
|
+
int_buf = [Array.new(4, 0), Array.new(4, 0)]
|
45
|
+
BinaryCodec.write_uint32be(int_buf[0], (number >> 32) & 0xFFFFFFFF, 0)
|
46
|
+
BinaryCodec.write_uint32be(int_buf[1], number & 0xFFFFFFFF, 0)
|
47
|
+
|
48
|
+
amount = int_buf.flatten
|
49
|
+
|
50
|
+
amount[0] |= 0x40
|
51
|
+
|
52
|
+
return Amount.new(amount)
|
53
|
+
end
|
54
|
+
|
55
|
+
if is_amount_object_iou?(value)
|
56
|
+
number = BigDecimal(value[:value])
|
57
|
+
self.assert_iou_is_valid(number)
|
58
|
+
|
59
|
+
if number.zero?
|
60
|
+
amount[0] |= 0x80
|
61
|
+
else
|
62
|
+
scale = number.frac.to_s('F').split('.').last.size
|
63
|
+
unscaled_value = (number * (10**scale)).to_i
|
64
|
+
int_string = unscaled_value.abs.to_s.ljust(16, '0')
|
65
|
+
num = int_string.to_i
|
66
|
+
|
67
|
+
int_buf = [Array.new(4, 0), Array.new(4, 0)]
|
68
|
+
BinaryCodec.write_uint32be(int_buf[0], (num >> 32) & 0xFFFFFFFF)
|
69
|
+
BinaryCodec.write_uint32be(int_buf[1], num & 0xFFFFFFFF)
|
70
|
+
|
71
|
+
amount = int_buf.flatten
|
72
|
+
|
73
|
+
amount[0] |= 0x80
|
74
|
+
|
75
|
+
if number > 0
|
76
|
+
amount[0] |= 0x40
|
77
|
+
end
|
78
|
+
|
79
|
+
exponent = number.exponent - 16
|
80
|
+
exponent_byte = 97 + exponent
|
81
|
+
amount[0] |= exponent_byte >> 2
|
82
|
+
amount[1] |= (exponent_byte & 0x03) << 6
|
83
|
+
end
|
84
|
+
|
85
|
+
currency = Currency.from(value[:currency]).to_bytes
|
86
|
+
issuer = AccountId.from(value[:issuer]).to_bytes
|
87
|
+
|
88
|
+
return Amount.new(amount + currency + issuer)
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
# Read an amount from a BinaryParser
|
94
|
+
#
|
95
|
+
# @param parser [BinaryParser] The BinaryParser to read the Amount from
|
96
|
+
# @return [Amount] An Amount object
|
97
|
+
def self.from_parser(parser)
|
98
|
+
is_iou = parser.peek & 0x80 != 0
|
99
|
+
return Amount.new(parser.read(48)) if is_iou
|
100
|
+
|
101
|
+
# The amount can be either MPT or XRP at this point
|
102
|
+
is_mpt = parser.peek & 0x20 != 0
|
103
|
+
num_bytes = is_mpt ? 33 : 8
|
104
|
+
Amount.new(parser.read(num_bytes))
|
105
|
+
end
|
106
|
+
|
107
|
+
# The JSON representation of this Amount
|
108
|
+
#
|
109
|
+
# @return [Hash, String] The JSON interpretation of this.bytes
|
110
|
+
def to_json
|
111
|
+
if is_native?
|
112
|
+
bytes = @bytes.dup # Duplicate the bytes to avoid mutation
|
113
|
+
is_positive = (bytes[0] & 0x40) != 0
|
114
|
+
sign = is_positive ? '' : '-'
|
115
|
+
bytes[0] &= 0x3f
|
116
|
+
|
117
|
+
msb = BinaryCodec.read_uint32be(bytes[0, 4])
|
118
|
+
lsb = BinaryCodec.read_uint32be(bytes[4, 4])
|
119
|
+
num = (msb << 32) | lsb
|
120
|
+
|
121
|
+
return "#{sign}#{num}"
|
122
|
+
end
|
123
|
+
|
124
|
+
if is_iou?
|
125
|
+
parser = BinaryParser.new(to_s)
|
126
|
+
mantissa = parser.read(8)
|
127
|
+
currency = Currency.from_parser(parser)
|
128
|
+
issuer = AccountId.from_parser(parser)
|
129
|
+
|
130
|
+
b1 = mantissa[0]
|
131
|
+
b2 = mantissa[1]
|
132
|
+
|
133
|
+
is_positive = (b1 & 0x40) != 0
|
134
|
+
sign = is_positive ? '' : '-'
|
135
|
+
exponent = ((b1 & 0x3f) << 2) + ((b2 & 0xff) >> 6) - 97
|
136
|
+
|
137
|
+
mantissa[0] = 0
|
138
|
+
mantissa[1] &= 0x3f
|
139
|
+
value = BigDecimal("#{sign}#{bytes_to_hex(mantissa).to_i(16)}") * BigDecimal("1e#{exponent}")
|
140
|
+
self.class.assert_iou_is_valid(value)
|
141
|
+
|
142
|
+
return {
|
143
|
+
"value" => value.to_s('F'),
|
144
|
+
"currency" => currency.to_json,
|
145
|
+
"issuer" => issuer.to_json
|
146
|
+
}.to_s
|
147
|
+
end
|
148
|
+
|
149
|
+
if is_mpt?
|
150
|
+
parser = BinaryParser.new(to_s)
|
151
|
+
leading_byte = parser.read(1)
|
152
|
+
amount = parser.read(8)
|
153
|
+
mpt_id = Hash192.from_parser(parser)
|
154
|
+
|
155
|
+
is_positive = (leading_byte[0] & 0x40) != 0
|
156
|
+
sign = is_positive ? '' : '-'
|
157
|
+
|
158
|
+
msb = read_uint32be(amount[0, 4])
|
159
|
+
lsb = read_uint32be(amount[4, 4])
|
160
|
+
num = (msb << 32) | lsb
|
161
|
+
|
162
|
+
return {
|
163
|
+
value: "#{sign}#{num}",
|
164
|
+
mpt_issuance_id: mpt_id.to_s
|
165
|
+
}.to_s
|
166
|
+
end
|
167
|
+
|
168
|
+
raise 'Invalid amount to construct JSON'
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
# Type guard for AmountObjectIOU
|
174
|
+
def self.is_amount_object_iou?(arg)
|
175
|
+
keys = arg.transform_keys(&:to_s).keys.sort
|
176
|
+
|
177
|
+
keys.length == 3 &&
|
178
|
+
keys[0] == 'currency' &&
|
179
|
+
keys[1] == 'issuer' &&
|
180
|
+
keys[2] == 'value'
|
181
|
+
end
|
182
|
+
|
183
|
+
# Type guard for AmountObjectMPT
|
184
|
+
def self.is_amount_object_mpt?(arg)
|
185
|
+
keys = arg.keys.sort
|
186
|
+
|
187
|
+
keys.length == 2 &&
|
188
|
+
keys[0] == 'mpt_issuance_id' &&
|
189
|
+
keys[1] == 'value'
|
190
|
+
end
|
191
|
+
|
192
|
+
# Validate XRP amount
|
193
|
+
#
|
194
|
+
# @param amount [String] representing XRP amount
|
195
|
+
# @return [void], but raises an exception if the amount is invalid
|
196
|
+
def self.assert_xrp_is_valid(amount)
|
197
|
+
if amount.include?('.')
|
198
|
+
raise "#{amount} is an illegal amount"
|
199
|
+
end
|
200
|
+
|
201
|
+
decimal = BigDecimal(amount)
|
202
|
+
unless decimal.zero?
|
203
|
+
if decimal < MIN_XRP || decimal > MAX_DROPS
|
204
|
+
raise "#{amount} is an illegal amount"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Validate IOU.value amount
|
210
|
+
#
|
211
|
+
# @param decimal [BigDecimal] object representing IOU.value
|
212
|
+
# @raise [ArgumentError] if the amount is invalid
|
213
|
+
def self.assert_iou_is_valid(decimal)
|
214
|
+
return if decimal.zero?
|
215
|
+
|
216
|
+
p = decimal.precision
|
217
|
+
e = (decimal.exponent || 0) - 15
|
218
|
+
|
219
|
+
if p > MAX_IOU_PRECISION || e > MAX_IOU_EXPONENT || e < MIN_IOU_EXPONENT
|
220
|
+
raise ArgumentError, 'Decimal precision out of range'
|
221
|
+
end
|
222
|
+
|
223
|
+
verify_no_decimal(decimal)
|
224
|
+
end
|
225
|
+
|
226
|
+
# Validate MPT.value amount
|
227
|
+
#
|
228
|
+
# @param amount [String] representing MPT.value
|
229
|
+
# @return [void], but raises an exception if the amount is invalid
|
230
|
+
def self.assert_mpt_is_valid(amount)
|
231
|
+
if amount.include?('.')
|
232
|
+
raise "#{amount} is an illegal amount"
|
233
|
+
end
|
234
|
+
|
235
|
+
decimal = BigDecimal(amount)
|
236
|
+
unless decimal.zero?
|
237
|
+
if decimal < BigDecimal("0")
|
238
|
+
raise "#{amount} is an illegal amount"
|
239
|
+
end
|
240
|
+
|
241
|
+
if (amount.to_i & mpt_mask) != 0
|
242
|
+
raise "#{amount} is an illegal amount"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
# Ensure that the value, after being multiplied by the exponent, does not
|
248
|
+
# contain a decimal. This function is typically used to validate numbers
|
249
|
+
# that need to be represented as precise integers after scaling, such as
|
250
|
+
# amounts in financial transactions.
|
251
|
+
#
|
252
|
+
# @param decimal [BigDecimal] A BigDecimal object
|
253
|
+
# @raise [ArgumentError] if the value contains a decimal
|
254
|
+
# @return [String] The decimal converted to a string without a decimal point
|
255
|
+
def self.verify_no_decimal(decimal)
|
256
|
+
exponent = -((decimal.exponent || 0) - 16)
|
257
|
+
scaled_decimal = decimal * 10 ** exponent
|
258
|
+
|
259
|
+
raise ArgumentError, 'Decimal place found in int_string' unless scaled_decimal.frac == 0
|
260
|
+
end
|
261
|
+
|
262
|
+
# Check if this amount is in units of Native Currency (XRP)
|
263
|
+
#
|
264
|
+
# @return [Boolean] true if Native (XRP)
|
265
|
+
def is_native?
|
266
|
+
(self.bytes[0] & 0x80).zero? && (self.bytes[0] & 0x20).zero?
|
267
|
+
end
|
268
|
+
|
269
|
+
# Check if this amount is in units of MPT
|
270
|
+
#
|
271
|
+
# @return [Boolean] true if MPT
|
272
|
+
def is_mpt?
|
273
|
+
(self.bytes[0] & 0x80).zero? && (self.bytes[0] & 0x20) != 0
|
274
|
+
end
|
275
|
+
|
276
|
+
# Check if this amount is in units of IOU
|
277
|
+
#
|
278
|
+
# @return [Boolean] true if IOU
|
279
|
+
def is_iou?
|
280
|
+
(self.bytes[0] & 0x80) != 0
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../core/core'
|
4
|
+
|
5
|
+
module BinaryCodec
|
6
|
+
class Blob < SerializedType
|
7
|
+
|
8
|
+
def initialize(byte_buf = nil)
|
9
|
+
@bytes = byte_buf || Array.new(0)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.from(value)
|
13
|
+
return value if value.is_a?(Blob)
|
14
|
+
|
15
|
+
if value.is_a?(String)
|
16
|
+
if value !~ /^[A-F0-9]*$/i
|
17
|
+
raise StandardError, 'Cannot construct Blob from a non-hex string'
|
18
|
+
end
|
19
|
+
return Blob.new(hex_to_bytes(value))
|
20
|
+
end
|
21
|
+
|
22
|
+
raise StandardError, 'Cannot construct Blob from value given'
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.from_parser(parser, hint = nil)
|
26
|
+
Blob.new(parser.read(hint))
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,105 @@
|
|
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 || Array.new(20, 0)) # Defaults to XRP bytes if no buffer is given
|
19
|
+
hex = bytes_to_hex(@bytes)
|
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
|
+
def iso
|
31
|
+
@_iso
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from(value)
|
35
|
+
return value if value.is_a?(Currency)
|
36
|
+
|
37
|
+
if value.is_a?(String)
|
38
|
+
return Currency.new(bytes_from_representation(value))
|
39
|
+
end
|
40
|
+
|
41
|
+
raise StandardError, 'Cannot construct Currency from value given'
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_json
|
45
|
+
iso = self.iso
|
46
|
+
return iso unless iso.nil?
|
47
|
+
|
48
|
+
bytes_to_hex(@bytes)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def iso_code_from_hex(code)
|
54
|
+
iso = hex_to_string(bytes_to_hex(code))
|
55
|
+
return nil if iso == 'XRP'
|
56
|
+
return iso if is_iso_code(iso)
|
57
|
+
|
58
|
+
nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def is_iso_code(iso)
|
62
|
+
!!(ISO_REGEX.match(iso)) # Equivalent to test for regex
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.bytes_from_representation(input)
|
66
|
+
unless is_valid_representation(input)
|
67
|
+
raise StandardError, "Unsupported Currency representation: #{input}"
|
68
|
+
end
|
69
|
+
|
70
|
+
input.length == 3 ? self.iso_to_bytes(input) : hex_to_bytes(input)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.iso_to_bytes(iso)
|
74
|
+
bytes = Array.new(20, 0) # Equivalent to Uint8Array(20)
|
75
|
+
if iso != 'XRP'
|
76
|
+
iso_bytes = iso.chars.map(&:ord)
|
77
|
+
# Insert iso_bytes at index 12
|
78
|
+
bytes[12, iso_bytes.length] = iso_bytes
|
79
|
+
end
|
80
|
+
bytes
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.is_valid_representation(input)
|
84
|
+
if input.is_a?(Array)
|
85
|
+
self.is_bytes_array(input)
|
86
|
+
else
|
87
|
+
self.is_string_representation(input)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.is_string_representation(input)
|
92
|
+
input.length == 3 || is_hex(input)
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.is_bytes_array(bytes)
|
96
|
+
bytes.length == 20
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.is_hex(hex)
|
100
|
+
!!(HEX_REGEX.match(hex)) # Special case for Vurrency type, do not conflate with valid_hex? function
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BinaryCodec
|
4
|
+
class Hash < ComparableSerializedType
|
5
|
+
|
6
|
+
def initialize(bytes, width)
|
7
|
+
@bytes = bytes
|
8
|
+
@width = width
|
9
|
+
if bytes.length != @width
|
10
|
+
# raise StandardError, "Invalid Hash length #{bytes.length} for width #{@width}"
|
11
|
+
raise StandardError, "Invalid Hash length #{bytes.length}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.from(hex_string)
|
16
|
+
new(hex_to_bytes(hex_string))
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.from_parser(parser, hint = nil)
|
20
|
+
new(parser.read(hint || width))
|
21
|
+
end
|
22
|
+
|
23
|
+
def compare_to(other)
|
24
|
+
@bytes <=> other.bytes
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns four bits at the specified depth within a hash
|
28
|
+
#
|
29
|
+
# @param depth [Integer] The depth of the four bits
|
30
|
+
# @return [Integer] The number represented by the four bits
|
31
|
+
def nibblet(depth)
|
32
|
+
byte_index = depth > 0 ? (depth / 2).floor : 0
|
33
|
+
b = bytes[byte_index]
|
34
|
+
if depth.even?
|
35
|
+
(b & 0xf0) >> 4
|
36
|
+
else
|
37
|
+
b & 0x0f
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
class Hash128 < Hash
|
44
|
+
@width = 16
|
45
|
+
@zero_128 = [0] * @width # Array.new(@width, 0)
|
46
|
+
|
47
|
+
class << self
|
48
|
+
attr_reader :width, :zero_128
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(bytes = nil)
|
52
|
+
bytes = self.class.zero_128 if bytes&.empty?
|
53
|
+
super(bytes, self.class.width)
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_hex
|
57
|
+
hex = bytes_to_hex(to_bytes)
|
58
|
+
return '' if hex.match?(/^0+$/)
|
59
|
+
hex
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class Hash160 < Hash
|
64
|
+
@width = 20
|
65
|
+
@zero_160 = [0] * @width # Array.new(@width, 0)
|
66
|
+
|
67
|
+
class << self
|
68
|
+
attr_reader :width, :zero_160
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize(bytes = nil)
|
72
|
+
bytes = self.class.zero_160 if bytes&.empty?
|
73
|
+
super(bytes, self.class.width)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class Hash192 < Hash
|
78
|
+
@width = 24
|
79
|
+
@zero_192 = [0] * @width # Array.new(@width, 0)
|
80
|
+
|
81
|
+
class << self
|
82
|
+
attr_reader :width, :zero_192
|
83
|
+
end
|
84
|
+
|
85
|
+
def initialize(bytes = nil)
|
86
|
+
bytes = self.class.zero_192 if bytes&.empty?
|
87
|
+
super(bytes, self.class.width)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class Hash256 < Hash
|
92
|
+
@width = 32
|
93
|
+
@zero_256 = [0] * @width # Array.new(@width, 0)
|
94
|
+
|
95
|
+
class << self
|
96
|
+
attr_reader :width, :zero_256
|
97
|
+
end
|
98
|
+
|
99
|
+
def initialize(bytes = nil)
|
100
|
+
bytes = self.class.zero_256 if bytes&.empty?
|
101
|
+
super(bytes, self.class.width)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../core/core'
|
4
|
+
|
5
|
+
module BinaryCodec
|
6
|
+
class SerializedType
|
7
|
+
|
8
|
+
attr_reader :bytes
|
9
|
+
|
10
|
+
def initialize(bytes = nil)
|
11
|
+
raise NotImplementedError, "#{self.class} is an abstract class and cannot be instantiated"
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.from(value)
|
15
|
+
raise NotImplementedError, 'from not implemented'
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.from_parser(parser, size_hint = nil)
|
19
|
+
raise NotImplementedError, 'from_parser not implemented'
|
20
|
+
end
|
21
|
+
|
22
|
+
# Check if this is needed
|
23
|
+
def self.from_json(json)
|
24
|
+
raise NotImplementedError, 'from_parser not implemented'
|
25
|
+
end
|
26
|
+
|
27
|
+
# Check if this is needed
|
28
|
+
def self.from_hex(hex)
|
29
|
+
self.from_bytes(hex_to_bytes(hex))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Check if this is needed
|
33
|
+
def self.from_bytes(bytes)
|
34
|
+
new(bytes)
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_byte_sink(sink)
|
38
|
+
sink.put(to_bytes)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Serialize the given value into bytes
|
42
|
+
# This method must be implemented in the subclasses
|
43
|
+
# @return [Array<Integer>] - Byte array representing the serialized data
|
44
|
+
def to_bytes
|
45
|
+
@bytes
|
46
|
+
end
|
47
|
+
|
48
|
+
# Convert to a hex string
|
49
|
+
# @return [String] - Hexadecimal representation of the serialized data
|
50
|
+
def to_hex
|
51
|
+
bytes_to_hex(to_bytes)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Deserialize instance data and convert it to JSON string
|
55
|
+
#
|
56
|
+
# @param _definitions [Hash] - Definitions for serialization
|
57
|
+
# @param _field_name [String] - Field name for serialization
|
58
|
+
# @return [String] - JSON representation of the serialized data
|
59
|
+
def to_json(_definitions = nil, _field_name = nil)
|
60
|
+
to_hex
|
61
|
+
end
|
62
|
+
|
63
|
+
# Represent object as a string (hexadecimal form)
|
64
|
+
def to_s
|
65
|
+
to_hex
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.get_type_by_name(name)
|
69
|
+
type_map = {
|
70
|
+
"AccountID" => AccountId,
|
71
|
+
"Amount" => Amount,
|
72
|
+
"Blob" => Blob,
|
73
|
+
"Currency" => Currency,
|
74
|
+
"Hash128" => Hash128,
|
75
|
+
"Hash160" => Hash160,
|
76
|
+
"Hash256" => Hash256,
|
77
|
+
#"PathSet" => PathSet,
|
78
|
+
#"STArray" => STArray,
|
79
|
+
"STObject" => STObject,
|
80
|
+
"UInt8" => Uint8,
|
81
|
+
"UInt16" => Uint16,
|
82
|
+
"UInt32" => Uint32,
|
83
|
+
"UInt64" => Uint64,
|
84
|
+
#"Vector256" => Vector256
|
85
|
+
}
|
86
|
+
|
87
|
+
unless type_map.key?(name)
|
88
|
+
raise "unsupported type #{name}"
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return class
|
92
|
+
type_map[name]
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
class ComparableSerializedType < SerializedType
|
98
|
+
|
99
|
+
# Compare if `self` is less than `other`
|
100
|
+
def lt(other)
|
101
|
+
compare_to(other) < 0
|
102
|
+
end
|
103
|
+
|
104
|
+
# Compare if `self` is equal to `other`
|
105
|
+
def eq(other)
|
106
|
+
compare_to(other) == 0
|
107
|
+
end
|
108
|
+
|
109
|
+
# Compare if `self` is greater than `other`
|
110
|
+
def gt(other)
|
111
|
+
compare_to(other) > 0
|
112
|
+
end
|
113
|
+
|
114
|
+
# Compare if `self` is greater than or equal to `other`
|
115
|
+
def gte(other)
|
116
|
+
compare_to(other) >= 0
|
117
|
+
end
|
118
|
+
|
119
|
+
# Compare if `self` is less than or equal to `other`
|
120
|
+
def lte(other)
|
121
|
+
compare_to(other) <= 0
|
122
|
+
end
|
123
|
+
|
124
|
+
# Overload this method in subclasses to define comparison logic
|
125
|
+
#
|
126
|
+
# @param other [Object] - The object to compare `self` to
|
127
|
+
# @return [Integer] - Returns -1, 0, or 1 depending on the comparison
|
128
|
+
def compare_to(other)
|
129
|
+
raise NotImplementedError, "Cannot compare #{self} and #{other}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module BinaryCodec
|
2
|
+
class STObject < SerializedType
|
3
|
+
|
4
|
+
OBJECT_END_MARKER_BYTE = [225]
|
5
|
+
OBJECT_END_MARKER = 'ObjectEndMarker'.freeze
|
6
|
+
ST_OBJECT = 'STObject'.freeze
|
7
|
+
DESTINATION = 'Destination'.freeze
|
8
|
+
ACCOUNT = 'Account'.freeze
|
9
|
+
SOURCE_TAG = 'SourceTag'.freeze
|
10
|
+
DEST_TAG = 'DestinationTag'.freeze
|
11
|
+
|
12
|
+
# attr_reader :type, :bytes
|
13
|
+
#
|
14
|
+
def initialize(byte_buf = nil)
|
15
|
+
@bytes = byte_buf || Array.new(0)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Construct a STObject from a BinaryParser
|
19
|
+
#
|
20
|
+
# @param parser [BinaryParser] BinaryParser to read STObject from
|
21
|
+
# @param size_hint [Integer] Optional size hint for the object
|
22
|
+
# @return [STObject] A STObject object
|
23
|
+
def self.from_parser(parser, size_hint = nil)
|
24
|
+
list = BytesList.new
|
25
|
+
bytes = BinarySerializer.new(list)
|
26
|
+
|
27
|
+
until parser.end?
|
28
|
+
field = parser.read_field
|
29
|
+
|
30
|
+
break if field.name == OBJECT_END_MARKER
|
31
|
+
|
32
|
+
associated_value = parser.read_field_value(field)
|
33
|
+
|
34
|
+
bytes.write_field_and_value(field, associated_value)
|
35
|
+
bytes.put(OBJECT_END_MARKER_BYTE) if field.type == ST_OBJECT
|
36
|
+
end
|
37
|
+
|
38
|
+
STObject.new(list.to_bytes)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Method to get the JSON interpretation of self.bytes
|
42
|
+
#
|
43
|
+
# @return [String] A stringified JSON object
|
44
|
+
def to_json()
|
45
|
+
parser = BinaryParser.new(to_s)
|
46
|
+
accumulator = {}
|
47
|
+
|
48
|
+
until parser.end?
|
49
|
+
field = parser.read_field
|
50
|
+
break if field.name == OBJECT_END_MARKER # Break if the object end marker is reached
|
51
|
+
value = parser.read_field_value(field).to_json
|
52
|
+
value = JSON.parse(value) if field.type == ST_OBJECT || field.type == Amount
|
53
|
+
accumulator[field.name] = value
|
54
|
+
end
|
55
|
+
|
56
|
+
JSON.generate(accumulator)
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|