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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/lib/address-codec/address_codec.rb +21 -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 +46 -22
  6. data/lib/binary-codec/enums/definitions.json +592 -1
  7. data/lib/binary-codec/enums/definitions.rb +17 -5
  8. data/lib/binary-codec/enums/fields.rb +2 -0
  9. data/lib/binary-codec/serdes/binary_parser.rb +38 -0
  10. data/lib/binary-codec/serdes/binary_serializer.rb +18 -7
  11. data/lib/binary-codec/serdes/bytes_list.rb +11 -0
  12. data/lib/binary-codec/types/account_id.rb +18 -37
  13. data/lib/binary-codec/types/amount.rb +43 -23
  14. data/lib/binary-codec/types/blob.rb +14 -5
  15. data/lib/binary-codec/types/currency.rb +15 -4
  16. data/lib/binary-codec/types/hash.rb +37 -36
  17. data/lib/binary-codec/types/issue.rb +50 -0
  18. data/lib/binary-codec/types/path_set.rb +93 -0
  19. data/lib/binary-codec/types/serialized_type.rb +52 -28
  20. data/lib/binary-codec/types/st_array.rb +71 -0
  21. data/lib/binary-codec/types/st_object.rb +100 -3
  22. data/lib/binary-codec/types/uint.rb +116 -3
  23. data/lib/binary-codec/types/vector256.rb +53 -0
  24. data/lib/binary-codec/types/xchain_bridge.rb +47 -0
  25. data/lib/binary-codec/utilities.rb +18 -0
  26. data/lib/core/base_58_xrp.rb +2 -0
  27. data/lib/core/base_x.rb +10 -0
  28. data/lib/core/core.rb +44 -6
  29. data/lib/core/utilities.rb +38 -0
  30. data/lib/key-pairs/ed25519.rb +64 -0
  31. data/lib/key-pairs/key_pairs.rb +92 -0
  32. data/lib/key-pairs/secp256k1.rb +116 -0
  33. data/lib/wallet/wallet.rb +117 -0
  34. data/lib/xrpl-ruby.rb +25 -1
  35. 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, 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}"
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
- def self.from(hex_string)
16
- new(hex_to_bytes(hex_string))
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 = self.class.zero_128 if bytes&.empty?
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 = self.class.zero_160 if bytes&.empty?
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 = self.class.zero_192 if bytes&.empty?
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 = self.class.zero_256 if bytes&.empty?
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
- raise NotImplementedError, "#{self.class} is an abstract class and cannot be instantiated"
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
- # Represent object as a string (hexadecimal form)
64
- def to_s
65
- to_hex
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
- 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)
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 < SerializedType
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(to_s)
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