xrpl-ruby 0.2.4 → 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 +21 -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 +46 -22
- data/lib/binary-codec/enums/definitions.json +592 -1
- data/lib/binary-codec/enums/definitions.rb +17 -5
- data/lib/binary-codec/enums/fields.rb +2 -0
- data/lib/binary-codec/serdes/binary_parser.rb +38 -0
- data/lib/binary-codec/serdes/binary_serializer.rb +18 -7
- data/lib/binary-codec/serdes/bytes_list.rb +11 -0
- data/lib/binary-codec/types/account_id.rb +18 -37
- data/lib/binary-codec/types/amount.rb +43 -23
- data/lib/binary-codec/types/blob.rb +14 -5
- data/lib/binary-codec/types/currency.rb +15 -4
- data/lib/binary-codec/types/hash.rb +37 -36
- 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 +52 -28
- data/lib/binary-codec/types/st_array.rb +71 -0
- data/lib/binary-codec/types/st_object.rb +100 -3
- data/lib/binary-codec/types/uint.rb +116 -3
- 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 +18 -0
- data/lib/core/base_58_xrp.rb +2 -0
- data/lib/core/base_x.rb +10 -0
- data/lib/core/core.rb +44 -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 +25 -1
- metadata +26 -2
|
@@ -2,24 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
module BinaryCodec
|
|
4
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
|
|
5
10
|
|
|
6
|
-
def initialize(bytes
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if bytes.length !=
|
|
10
|
-
|
|
11
|
-
raise StandardError, "Invalid Hash length #{bytes.length}"
|
|
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}"
|
|
12
16
|
end
|
|
13
17
|
end
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
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"
|
|
17
35
|
end
|
|
18
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.
|
|
19
41
|
def self.from_parser(parser, hint = nil)
|
|
20
42
|
new(parser.read(hint || width))
|
|
21
43
|
end
|
|
22
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).
|
|
23
48
|
def compare_to(other)
|
|
24
49
|
@bytes <=> other.bytes
|
|
25
50
|
end
|
|
@@ -42,15 +67,9 @@ module BinaryCodec
|
|
|
42
67
|
|
|
43
68
|
class Hash128 < Hash
|
|
44
69
|
@width = 16
|
|
45
|
-
@zero_128 = [0] * @width # Array.new(@width, 0)
|
|
46
|
-
|
|
47
|
-
class << self
|
|
48
|
-
attr_reader :width, :zero_128
|
|
49
|
-
end
|
|
50
70
|
|
|
51
71
|
def initialize(bytes = nil)
|
|
52
|
-
bytes
|
|
53
|
-
super(bytes, self.class.width)
|
|
72
|
+
super(bytes)
|
|
54
73
|
end
|
|
55
74
|
|
|
56
75
|
def to_hex
|
|
@@ -62,43 +81,25 @@ module BinaryCodec
|
|
|
62
81
|
|
|
63
82
|
class Hash160 < Hash
|
|
64
83
|
@width = 20
|
|
65
|
-
@zero_160 = [0] * @width # Array.new(@width, 0)
|
|
66
|
-
|
|
67
|
-
class << self
|
|
68
|
-
attr_reader :width, :zero_160
|
|
69
|
-
end
|
|
70
84
|
|
|
71
85
|
def initialize(bytes = nil)
|
|
72
|
-
bytes
|
|
73
|
-
super(bytes, self.class.width)
|
|
86
|
+
super(bytes)
|
|
74
87
|
end
|
|
75
88
|
end
|
|
76
89
|
|
|
77
90
|
class Hash192 < Hash
|
|
78
91
|
@width = 24
|
|
79
|
-
@zero_192 = [0] * @width # Array.new(@width, 0)
|
|
80
|
-
|
|
81
|
-
class << self
|
|
82
|
-
attr_reader :width, :zero_192
|
|
83
|
-
end
|
|
84
92
|
|
|
85
93
|
def initialize(bytes = nil)
|
|
86
|
-
bytes
|
|
87
|
-
super(bytes, self.class.width)
|
|
94
|
+
super(bytes)
|
|
88
95
|
end
|
|
89
96
|
end
|
|
90
97
|
|
|
91
98
|
class Hash256 < Hash
|
|
92
99
|
@width = 32
|
|
93
|
-
@zero_256 = [0] * @width # Array.new(@width, 0)
|
|
94
|
-
|
|
95
|
-
class << self
|
|
96
|
-
attr_reader :width, :zero_256
|
|
97
|
-
end
|
|
98
100
|
|
|
99
101
|
def initialize(bytes = nil)
|
|
100
|
-
bytes
|
|
101
|
-
super(bytes, self.class.width)
|
|
102
|
+
super(bytes)
|
|
102
103
|
end
|
|
103
104
|
end
|
|
104
105
|
|
|
@@ -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
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative '../../core/core'
|
|
4
|
-
|
|
5
3
|
module BinaryCodec
|
|
6
4
|
class SerializedType
|
|
7
5
|
|
|
8
6
|
attr_reader :bytes
|
|
9
7
|
|
|
8
|
+
# Initializes a new SerializedType instance.
|
|
9
|
+
# @param bytes [Array<Integer>, nil] The byte array representing the serialized data.
|
|
10
10
|
def initialize(bytes = nil)
|
|
11
|
-
|
|
11
|
+
@bytes = bytes
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# Creates a new instance of the type from a value.
|
|
15
|
+
# @param value [Object] The value to convert.
|
|
16
|
+
# @return [SerializedType] The created instance.
|
|
14
17
|
def self.from(value)
|
|
15
18
|
raise NotImplementedError, 'from not implemented'
|
|
16
19
|
end
|
|
17
20
|
|
|
21
|
+
# Creates an instance of the type from a parser.
|
|
22
|
+
# @param parser [BinaryParser] The parser to read from.
|
|
23
|
+
# @param size_hint [Integer, nil] Optional size hint.
|
|
24
|
+
# @return [SerializedType] The created instance.
|
|
18
25
|
def self.from_parser(parser, size_hint = nil)
|
|
19
26
|
raise NotImplementedError, 'from_parser not implemented'
|
|
20
27
|
end
|
|
@@ -34,6 +41,8 @@ module BinaryCodec
|
|
|
34
41
|
new(bytes)
|
|
35
42
|
end
|
|
36
43
|
|
|
44
|
+
# Puts the serialized data into a byte sink.
|
|
45
|
+
# @param sink [Object] The sink to put bytes into.
|
|
37
46
|
def to_byte_sink(sink)
|
|
38
47
|
sink.put(to_bytes)
|
|
39
48
|
end
|
|
@@ -60,36 +69,51 @@ module BinaryCodec
|
|
|
60
69
|
to_hex
|
|
61
70
|
end
|
|
62
71
|
|
|
63
|
-
#
|
|
64
|
-
|
|
65
|
-
|
|
72
|
+
# Returns the value of the serialized type
|
|
73
|
+
# @return [Object] - The value of the serialized type
|
|
74
|
+
def value_of
|
|
75
|
+
@bytes
|
|
66
76
|
end
|
|
67
77
|
|
|
78
|
+
# Returns the class for a given type name.
|
|
79
|
+
# @param name [String] The name of the type.
|
|
80
|
+
# @return [Class] The class associated with the type name.
|
|
68
81
|
def self.get_type_by_name(name)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
case name
|
|
83
|
+
when "AccountID" then AccountId
|
|
84
|
+
when "Amount" then Amount
|
|
85
|
+
when "Blob" then Blob
|
|
86
|
+
when "Currency" then Currency
|
|
87
|
+
when "Hash128" then Hash128
|
|
88
|
+
when "Hash160" then Hash160
|
|
89
|
+
when "Hash192" then Hash192
|
|
90
|
+
when "Hash256" then Hash256
|
|
91
|
+
when "STArray" then STArray
|
|
92
|
+
when "STObject" then STObject
|
|
93
|
+
when "UInt8" then Uint8
|
|
94
|
+
when "UInt16" then Uint16
|
|
95
|
+
when "UInt32" then Uint32
|
|
96
|
+
when "UInt64" then Uint64
|
|
97
|
+
when "UInt96" then Uint96
|
|
98
|
+
when "UInt128" then Uint128
|
|
99
|
+
when "UInt160" then Uint160
|
|
100
|
+
when "UInt192" then Uint192
|
|
101
|
+
when "UInt256" then Uint256
|
|
102
|
+
when "UInt384" then Uint384
|
|
103
|
+
when "UInt512" then Uint512
|
|
104
|
+
when "Int32" then Int32
|
|
105
|
+
when "Int64" then Int64
|
|
106
|
+
when "PathSet" then PathSet
|
|
107
|
+
when "Vector256" then Vector256
|
|
108
|
+
when "XChainBridge" then XChainBridge
|
|
109
|
+
when "Issue" then Issue
|
|
110
|
+
when "Transaction" then Blob
|
|
111
|
+
when "LedgerEntry" then Blob
|
|
112
|
+
when "Validation" then Blob
|
|
113
|
+
when "Metadata" then Blob
|
|
114
|
+
else
|
|
88
115
|
raise "unsupported type #{name}"
|
|
89
116
|
end
|
|
90
|
-
|
|
91
|
-
# Return class
|
|
92
|
-
type_map[name]
|
|
93
117
|
end
|
|
94
118
|
|
|
95
119
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BinaryCodec
|
|
4
|
+
class STArray < SerializedType
|
|
5
|
+
def initialize(byte_buf = nil)
|
|
6
|
+
super(byte_buf || [])
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Creates a new STArray instance from a value.
|
|
10
|
+
# @param value [STArray, String, Array<Hash>] The value to convert.
|
|
11
|
+
# @param definitions [Definitions, nil] Optional definitions.
|
|
12
|
+
# @return [STArray] The created instance.
|
|
13
|
+
def self.from(value, definitions = nil)
|
|
14
|
+
return value if value.is_a?(STArray)
|
|
15
|
+
definitions ||= Definitions.instance
|
|
16
|
+
|
|
17
|
+
if value.is_a?(String)
|
|
18
|
+
return STArray.new(hex_to_bytes(value))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if value.is_a?(Array)
|
|
22
|
+
bytes = []
|
|
23
|
+
value.each do |item|
|
|
24
|
+
obj = STObject.from(item, nil, definitions)
|
|
25
|
+
bytes.concat(obj.to_bytes)
|
|
26
|
+
bytes.concat([0xF1]) # ArrayItemEndMarker
|
|
27
|
+
end
|
|
28
|
+
bytes.concat([0xF1]) # ArrayEndMarker
|
|
29
|
+
return STArray.new(bytes)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
raise StandardError, "Cannot construct STArray from #{value.class}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Creates an STArray instance from a parser.
|
|
36
|
+
# @param parser [BinaryParser] The parser to read from.
|
|
37
|
+
# @param _hint [Integer, nil] Unused hint.
|
|
38
|
+
# @return [STArray] The created instance.
|
|
39
|
+
def self.from_parser(parser, _hint = nil)
|
|
40
|
+
bytes = []
|
|
41
|
+
until parser.end?(1) # Look ahead for end marker
|
|
42
|
+
obj = STObject.from_parser(parser)
|
|
43
|
+
bytes.concat(obj.to_bytes)
|
|
44
|
+
bytes.concat(parser.read(1)) # Should be 0xF1 (ArrayItemEndMarker)
|
|
45
|
+
end
|
|
46
|
+
parser.read(1) # Consume 0xF1 (ArrayEndMarker)
|
|
47
|
+
STArray.new(bytes)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns the JSON representation of the STArray.
|
|
51
|
+
# @param _definitions [Definitions, nil] Unused.
|
|
52
|
+
# @param _field_name [String, nil] Unused.
|
|
53
|
+
# @return [Array<Hash>] The JSON representation.
|
|
54
|
+
def to_json(_definitions = nil, _field_name = nil)
|
|
55
|
+
parser = BinaryParser.new(to_hex)
|
|
56
|
+
result = []
|
|
57
|
+
until parser.end?
|
|
58
|
+
obj = STObject.from_parser(parser)
|
|
59
|
+
result << JSON.parse(obj.to_json)
|
|
60
|
+
# In xrpl.js, array items are STObjects.
|
|
61
|
+
# After each STObject, there might be an ArrayItemEndMarker if we're not at the end.
|
|
62
|
+
# But wait, STObject.from_parser already reads until ObjectEndMarker.
|
|
63
|
+
# STArray in XRPL is a list of objects, each ending with ObjectEndMarker.
|
|
64
|
+
# The whole array ends with ArrayEndMarker (0xF1).
|
|
65
|
+
# Actually, standard XRPL STArray: [FieldHeader][STObject][FieldHeader][STObject]...[0xF1]
|
|
66
|
+
# Wait, I need to check how xrpl.js handles STArray.
|
|
67
|
+
end
|
|
68
|
+
result
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module BinaryCodec
|
|
2
|
-
class STObject
|
|
4
|
+
class STObject < SerializedType
|
|
3
5
|
|
|
4
6
|
OBJECT_END_MARKER_BYTE = [225]
|
|
5
7
|
OBJECT_END_MARKER = 'ObjectEndMarker'.freeze
|
|
@@ -15,6 +17,68 @@ module BinaryCodec
|
|
|
15
17
|
@bytes = byte_buf || Array.new(0)
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
# Creates a new STObject instance from a value.
|
|
21
|
+
# @param value [STObject, String, Array<Integer>, Hash] The value to convert.
|
|
22
|
+
# @param filter [Proc, nil] Optional filter for fields.
|
|
23
|
+
# @param definitions [Definitions, nil] Optional definitions.
|
|
24
|
+
# @return [STObject] The created instance.
|
|
25
|
+
def self.from(value, filter = nil, definitions = nil)
|
|
26
|
+
return value if value.is_a?(STObject)
|
|
27
|
+
definitions ||= Definitions.instance
|
|
28
|
+
|
|
29
|
+
if value.is_a?(String)
|
|
30
|
+
return STObject.new(hex_to_bytes(value))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if value.is_a?(Array)
|
|
34
|
+
return STObject.new(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
list = BytesList.new
|
|
38
|
+
serializer = BinarySerializer.new(list)
|
|
39
|
+
is_unl_modify = false
|
|
40
|
+
|
|
41
|
+
# Handle X-Addresses and check for duplicate tags
|
|
42
|
+
processed_value = value.each_with_object({}) do |(key, val), acc|
|
|
43
|
+
if val && val.is_a?(String) && AddressCodec::AddressCodec.new.valid_x_address?(val)
|
|
44
|
+
handled = handle_x_address(key.to_s, val)
|
|
45
|
+
check_for_duplicate_tags(handled, value)
|
|
46
|
+
acc.merge!(handled)
|
|
47
|
+
else
|
|
48
|
+
acc[key.to_s] = val
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
sorted_fields = processed_value.keys.map do |field_name|
|
|
53
|
+
field = definitions.get_field_instance(field_name)
|
|
54
|
+
raise "Field #{field_name} is not defined" if field.nil?
|
|
55
|
+
field
|
|
56
|
+
end.select(&:is_serialized).sort_by(&:ordinal)
|
|
57
|
+
|
|
58
|
+
if filter
|
|
59
|
+
sorted_fields = sorted_fields.select { |f| filter.call(f.name) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sorted_fields.each do |field|
|
|
63
|
+
associated_value = processed_value[field.name]
|
|
64
|
+
next if associated_value.nil?
|
|
65
|
+
|
|
66
|
+
# Special handling for UNLModify
|
|
67
|
+
if field.name == 'UNLModify' # This might need more specific check depending on value
|
|
68
|
+
is_unl_modify = true
|
|
69
|
+
end
|
|
70
|
+
is_unl_modify_workaround = (field.name == 'Account' && is_unl_modify)
|
|
71
|
+
|
|
72
|
+
serializer.write_field_and_value(field, associated_value, is_unl_modify_workaround)
|
|
73
|
+
|
|
74
|
+
if field.type == 'STObject'
|
|
75
|
+
serializer.put(OBJECT_END_MARKER_BYTE)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
STObject.new(list.to_bytes)
|
|
80
|
+
end
|
|
81
|
+
|
|
18
82
|
# Construct a STObject from a BinaryParser
|
|
19
83
|
#
|
|
20
84
|
# @param parser [BinaryParser] BinaryParser to read STObject from
|
|
@@ -41,8 +105,8 @@ module BinaryCodec
|
|
|
41
105
|
# Method to get the JSON interpretation of self.bytes
|
|
42
106
|
#
|
|
43
107
|
# @return [String] A stringified JSON object
|
|
44
|
-
def to_json()
|
|
45
|
-
parser = BinaryParser.new(
|
|
108
|
+
def to_json(_definitions = nil, _field_name = nil)
|
|
109
|
+
parser = BinaryParser.new(to_hex)
|
|
46
110
|
accumulator = {}
|
|
47
111
|
|
|
48
112
|
until parser.end?
|
|
@@ -56,5 +120,38 @@ module BinaryCodec
|
|
|
56
120
|
JSON.generate(accumulator)
|
|
57
121
|
end
|
|
58
122
|
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# Break down an X-Address into an account and a tag
|
|
126
|
+
#
|
|
127
|
+
# @param field [String] Name of the field
|
|
128
|
+
# @param x_address [String] X-Address corresponding to the field
|
|
129
|
+
# @return [Hash] A hash with the classic address and tag (if present)
|
|
130
|
+
def handle_x_address(field, x_address)
|
|
131
|
+
address_codec = AddressCodec::AddressCodec.new
|
|
132
|
+
decoded = address_codec.x_address_to_classic_address(x_address)
|
|
133
|
+
|
|
134
|
+
tag_name = if field == 'Destination'
|
|
135
|
+
'DestinationTag'
|
|
136
|
+
elsif field == 'Account'
|
|
137
|
+
'SourceTag'
|
|
138
|
+
elsif decoded[:tag]
|
|
139
|
+
raise "#{field} cannot have an associated tag"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
decoded[:tag] ? { field => decoded[:classic_address], tag_name => decoded[:tag] } : { field => decoded[:classic_address] }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def self.check_for_duplicate_tags(obj1, obj2)
|
|
146
|
+
if obj1['SourceTag'] && obj2['SourceTag']
|
|
147
|
+
raise 'Cannot have Account X-Address and SourceTag'
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
if obj1['DestinationTag'] && obj2['DestinationTag']
|
|
151
|
+
raise 'Cannot have Destination X-Address and DestinationTag'
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
|
|
59
156
|
end
|
|
60
157
|
end
|