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
@@ -3,18 +3,27 @@
3
3
  module BinaryCodec
4
4
 
5
5
  class Uint < ComparableSerializedType
6
- class << self
7
- attr_reader :width
6
+ # Returns the width of the Uint type in bytes.
7
+ # @return [Integer] The width.
8
+ def self.width
9
+ @width
8
10
  end
9
11
 
10
12
  def initialize(byte_buf = nil)
11
- @bytes = byte_buf || Array.new(self.class.width, 0)
13
+ super(byte_buf || Array.new(self.class.width, 0))
12
14
  end
13
15
 
16
+ # Creates a new Uint instance from a value.
17
+ # @param value [Uint, String, Integer] The value to convert.
18
+ # @return [Uint] The created instance.
14
19
  def self.from(value)
15
20
  return value if value.is_a?(self)
16
21
 
17
22
  if value.is_a?(String)
23
+ # Handle hex strings or numeric strings
24
+ if valid_hex?(value) && value.length == self.width * 2
25
+ return new(hex_to_bytes(value))
26
+ end
18
27
  return new(int_to_bytes(value.to_i, width))
19
28
  end
20
29
 
@@ -25,9 +34,26 @@ module BinaryCodec
25
34
  raise StandardError, "Cannot construct #{self} from the value given"
26
35
  end
27
36
 
37
+ # Creates a Uint instance from a parser.
38
+ # @param parser [BinaryParser] The parser to read from.
39
+ # @param _hint [Integer, nil] Unused hint.
40
+ # @return [Uint] The created instance.
41
+ def self.from_parser(parser, _hint = nil)
42
+ new(parser.read(width))
43
+ end
44
+
45
+ # Returns the numeric value of the Uint.
46
+ # @return [Integer] The numeric value.
28
47
  def value_of
29
48
  @bytes.reduce(0) { |acc, byte| (acc << 8) + byte }
30
49
  end
50
+
51
+ # Compares this Uint to another Uint.
52
+ # @param other [Uint] The other Uint to compare to.
53
+ # @return [Integer] Comparison result (-1, 0, or 1).
54
+ def compare_to(other)
55
+ value_of <=> other.value_of
56
+ end
31
57
  end
32
58
 
33
59
  class Uint8 < Uint
@@ -50,4 +76,91 @@ module BinaryCodec
50
76
  @width = 8
51
77
  end
52
78
 
79
+ class Uint96 < Uint
80
+ # Uint96 is a 12-byte unsigned integer
81
+ @width = 12
82
+ end
83
+
84
+ class Uint128 < Uint
85
+ # Uint128 is a 16-byte unsigned integer
86
+ @width = 16
87
+ end
88
+
89
+ class Uint160 < Uint
90
+ # Uint160 is a 20-byte unsigned integer
91
+ @width = 20
92
+ end
93
+
94
+ class Uint192 < Uint
95
+ # Uint192 is a 24-byte unsigned integer
96
+ @width = 24
97
+ end
98
+
99
+ class Uint256 < Uint
100
+ # Uint256 is a 32-byte unsigned integer
101
+ @width = 32
102
+ end
103
+
104
+ class Uint384 < Uint
105
+ # Uint384 is a 48-byte unsigned integer
106
+ @width = 48
107
+ end
108
+
109
+ class Uint512 < Uint
110
+ # Uint512 is a 64-byte unsigned integer
111
+ @width = 64
112
+ end
113
+
114
+ class Int32 < Uint
115
+ @width = 4
116
+ # Returns the numeric value of the Int32.
117
+ # @return [Integer] The signed 32-bit value.
118
+ def value_of
119
+ val = super
120
+ val > 0x7FFFFFFF ? val - 0x100000000 : val
121
+ end
122
+
123
+ # Creates a new Int32 instance from a value.
124
+ # @param value [Int32, Integer] The value to convert.
125
+ # @return [Int32] The created instance.
126
+ def self.from(value)
127
+ return value if value.is_a?(self)
128
+ if value.is_a?(Integer)
129
+ # Ensure it fits in 32-bit signed
130
+ if value < -2147483648 || value > 2147483647
131
+ raise StandardError, "Value #{value} out of range for Int32"
132
+ end
133
+ # Convert to unsigned 32-bit for storage
134
+ u_val = value < 0 ? value + 0x100000000 : value
135
+ return new(int_to_bytes(u_val, 4))
136
+ end
137
+ super(value)
138
+ end
139
+ end
140
+
141
+ class Int64 < Uint
142
+ @width = 8
143
+ # Returns the numeric value of the Int64.
144
+ # @return [Integer] The signed 64-bit value.
145
+ def value_of
146
+ val = super
147
+ val > 0x7FFFFFFFFFFFFFFF ? val - 0x10000000000000000 : val
148
+ end
149
+
150
+ # Creates a new Int64 instance from a value.
151
+ # @param value [Int64, Integer] The value to convert.
152
+ # @return [Int64] The created instance.
153
+ def self.from(value)
154
+ return value if value.is_a?(self)
155
+ if value.is_a?(Integer)
156
+ if value < -9223372036854775808 || value > 9223372036854775807
157
+ raise StandardError, "Value #{value} out of range for Int64"
158
+ end
159
+ u_val = value < 0 ? value + 0x10000000000000000 : value
160
+ return new(int_to_bytes(u_val, 8))
161
+ end
162
+ super(value)
163
+ end
164
+ end
165
+
53
166
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BinaryCodec
4
+ class Vector256 < SerializedType
5
+ def initialize(bytes = nil)
6
+ super(bytes || [])
7
+ end
8
+
9
+ # Creates a new Vector256 instance from a value.
10
+ # @param value [Vector256, String, Array<String>] The value to convert.
11
+ # @return [Vector256] The created instance.
12
+ def self.from(value)
13
+ return value if value.is_a?(Vector256)
14
+
15
+ if value.is_a?(String)
16
+ return Vector256.new(hex_to_bytes(value))
17
+ end
18
+
19
+ if value.is_a?(Array)
20
+ bytes = []
21
+ value.each do |item|
22
+ hash = Hash256.from(item)
23
+ bytes.concat(hash.to_bytes)
24
+ end
25
+ return Vector256.new(bytes)
26
+ end
27
+
28
+ raise StandardError, "Cannot construct Vector256 from #{value.class}"
29
+ end
30
+
31
+ # Creates a Vector256 instance from a parser.
32
+ # @param parser [BinaryParser] The parser to read from.
33
+ # @param size_hint [Integer] The expected total size in bytes.
34
+ # @return [Vector256] The created instance.
35
+ def self.from_parser(parser, size_hint = nil)
36
+ bytes = []
37
+ num_hashes = size_hint / 32
38
+ num_hashes.times do
39
+ bytes.concat(parser.read(32))
40
+ end
41
+ Vector256.new(bytes)
42
+ end
43
+
44
+ def to_json(_definitions = nil, _field_name = nil)
45
+ parser = BinaryParser.new(to_hex)
46
+ result = []
47
+ until parser.end?
48
+ result << bytes_to_hex(parser.read(32))
49
+ end
50
+ result
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BinaryCodec
4
+ class XChainBridge < SerializedType
5
+ def initialize(bytes = nil)
6
+ super(bytes || [])
7
+ end
8
+
9
+ def self.from(value)
10
+ return value if value.is_a?(XChainBridge)
11
+
12
+ if value.is_a?(String)
13
+ return XChainBridge.new(hex_to_bytes(value))
14
+ end
15
+
16
+ if value.is_a?(Hash)
17
+ bytes = []
18
+ bytes.concat(AccountId.from(value['LockingChainDoor']).to_bytes)
19
+ bytes.concat(Issue.from(value['LockingChainIssue']).to_bytes)
20
+ bytes.concat(AccountId.from(value['IssuingChainDoor']).to_bytes)
21
+ bytes.concat(Issue.from(value['IssuingChainIssue']).to_bytes)
22
+ return XChainBridge.new(bytes)
23
+ end
24
+
25
+ raise StandardError, "Cannot construct XChainBridge from #{value.class}"
26
+ end
27
+
28
+ def self.from_parser(parser, _hint = nil)
29
+ bytes = []
30
+ bytes.concat(parser.read(20)) # LockingChainDoor
31
+ bytes.concat(Issue.from_parser(parser).to_bytes) # LockingChainIssue
32
+ bytes.concat(parser.read(20)) # IssuingChainDoor
33
+ bytes.concat(Issue.from_parser(parser).to_bytes) # IssuingChainIssue
34
+ XChainBridge.new(bytes)
35
+ end
36
+
37
+ def to_json(_definitions = nil, _field_name = nil)
38
+ parser = BinaryParser.new(to_hex)
39
+ result = {}
40
+ result['LockingChainDoor'] = AccountId.from_parser(parser).to_json
41
+ result['LockingChainIssue'] = Issue.from_parser(parser).to_json
42
+ result['IssuingChainDoor'] = AccountId.from_parser(parser).to_json
43
+ result['IssuingChainIssue'] = Issue.from_parser(parser).to_json
44
+ result
45
+ end
46
+ end
47
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BinaryCodec
2
4
 
3
5
  # Write an 8-bit unsigned integer
@@ -77,4 +79,20 @@ module BinaryCodec
77
79
  (array.length % 4).zero?
78
80
  end
79
81
 
82
+ # TODO: Marked for overhaul
83
+ def self.is_valid_x_address?(x_address)
84
+ return false unless x_address.is_a?(String) && x_address.start_with?('X')
85
+
86
+ begin
87
+ decoded = decode_x_address(x_address)
88
+ return false if decoded[:account_id].nil? || decoded[:account_id].length != 20
89
+
90
+ tag = decoded[:tag]
91
+ return false if tag && (tag < 0 || tag > MAX_32_BIT_UNSIGNED_INT)
92
+
93
+ true
94
+ rescue StandardError
95
+ false
96
+ end
97
+ end
80
98
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Core
2
4
 
3
5
  class Base58XRP < BaseX
data/lib/core/base_x.rb CHANGED
@@ -1,6 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Core
2
4
 
3
5
  class BaseX
6
+ # Initializes a new BaseX instance with the given alphabet.
7
+ # @param alphabet [String] The alphabet to use for encoding and decoding.
4
8
  def initialize(alphabet)
5
9
  @alphabet = alphabet
6
10
  @base = alphabet.length
@@ -8,6 +12,9 @@ module Core
8
12
  alphabet.chars.each_with_index { |char, index| @alphabet_map[char] = index }
9
13
  end
10
14
 
15
+ # Encodes a byte array into a string using the alphabet.
16
+ # @param buffer [String] The byte string to encode.
17
+ # @return [String] The encoded string.
11
18
  def encode(buffer)
12
19
  return @alphabet[0] if buffer.empty?
13
20
 
@@ -30,6 +37,9 @@ module Core
30
37
  digits.reverse.map { |digit| @alphabet[digit] }.join
31
38
  end
32
39
 
40
+ # Decodes a string into a byte string using the alphabet.
41
+ # @param string [String] The string to decode.
42
+ # @return [String] The decoded byte string.
33
43
  def decode(string)
34
44
  return '' if string.empty?
35
45
 
data/lib/core/core.rb CHANGED
@@ -1,42 +1,70 @@
1
- # @!attribute
2
- require_relative 'base_x'
3
- require_relative 'base_58_xrp'
1
+ # frozen_string_literal: true
2
+
4
3
  require 'securerandom'
5
4
 
5
+ # Returns a random byte array of the given size.
6
+ # @param size [Integer] The number of bytes to generate.
7
+ # @return [Array<Integer>] The generated random byte array.
6
8
  def random_bytes(size)
7
9
  SecureRandom.random_bytes(size).bytes
8
10
  end
9
11
 
12
+ # Converts a byte array to a hex string.
13
+ # @param bytes [Array<Integer>] The byte array to convert.
14
+ # @return [String] The hex string.
10
15
  def bytes_to_hex(bytes)
11
16
  bytes.pack('C*').unpack1('H*').upcase
12
17
  end
18
+ # Converts a hex string to a byte array.
19
+ # @param hex [String] The hex string to convert.
20
+ # @return [Array<Integer>] The byte array.
13
21
  def hex_to_bytes(hex)
14
22
  raise ArgumentError, 'Invalid hex string' unless valid_hex?(hex)
15
23
  [hex].pack('H*').bytes
16
24
  end
17
25
 
26
+ # Converts a binary string to a hex string.
27
+ # @param bin [String] The binary string to convert.
28
+ # @return [String] The hex string.
18
29
  def bin_to_hex(bin)
19
30
  bin.unpack("H*").first.upcase
20
31
  end
21
32
 
33
+ # Converts a hex string to a binary string.
34
+ # @param hex [String] The hex string to convert.
35
+ # @return [String] The binary string.
22
36
  def hex_to_bin(hex)
23
37
  raise ArgumentError, 'Invalid hex string' unless valid_hex?(hex)
24
38
  [hex].pack("H*")
25
39
  end
26
40
 
41
+ # Converts a hex string to a string with the given encoding.
42
+ # @param hex [String] The hex string to convert.
43
+ # @param encoding [String] The encoding to use.
44
+ # @return [String] The decoded string.
27
45
  def hex_to_string(hex, encoding = 'utf-8')
28
46
  raise ArgumentError, 'Invalid hex string' unless valid_hex?(hex)
29
47
  hex_to_bin(hex).force_encoding(encoding).encode('utf-8')
30
48
  end
31
49
 
50
+ # Converts a string to a hex string.
51
+ # @param string [String] The string to convert.
52
+ # @return [String] The hex string.
32
53
  def string_to_hex(string)
33
54
  string.unpack1('H*').upcase
34
55
  end
35
56
 
57
+ # Checks if a string is a valid hex string.
58
+ # @param str [String] The string to check.
59
+ # @return [Boolean] True if the string is a valid hex string, false otherwise.
36
60
  def valid_hex?(str)
37
61
  str =~ /\A[0-9a-fA-F]*\z/ && str.length.even?
38
62
  end
39
63
 
64
+ # Checks if a byte array has the expected length.
65
+ # @param bytes [Array<Integer>, String] The byte array or string to check.
66
+ # @param expected_length [Integer] The expected length.
67
+ # @return [Boolean] True if the length matches, false otherwise.
40
68
  def check_byte_length(bytes, expected_length)
41
69
  if bytes.respond_to?(:byte_length)
42
70
  bytes.byte_length == expected_length
@@ -45,24 +73,34 @@ def check_byte_length(bytes, expected_length)
45
73
  end
46
74
  end
47
75
 
76
+ # Concatenates multiple arguments into a single array.
77
+ # @param args [Array] The arguments to concatenate.
78
+ # @return [Array] The concatenated array.
48
79
  def concat_args(*args)
49
80
  args.flat_map do |arg|
50
81
  is_scalar?(arg) ? [arg] : arg.to_a
51
82
  end
52
83
  end
53
84
 
85
+ # Checks if a value is a scalar.
86
+ # @param val [Object] The value to check.
87
+ # @return [Boolean] True if the value is a numeric scalar, false otherwise.
54
88
  def is_scalar?(val)
55
89
  val.is_a?(Numeric)
56
90
  end
57
91
 
92
+ # Converts an integer to a byte array.
93
+ # @param number [Integer] The integer to convert.
94
+ # @param width [Integer] The number of bytes in the result.
95
+ # @param byteorder [Symbol] The byte order (:big or :little).
96
+ # @return [Array<Integer>] The byte array.
58
97
  def int_to_bytes(number, width = 1, byteorder = :big)
59
98
  bytes = []
60
99
  while number > 0
61
- bytes << (number & 0xFF) # Extract the lowest 8 bits (1 byte)
62
- number >>= 8 # Shift the number 8 bits to the right
100
+ bytes << (number & 0xFF)
101
+ number >>= 8
63
102
  end
64
103
 
65
- # Ensure the result has at least `width` bytes (pad with zeroes if necessary)
66
104
  while bytes.size < width
67
105
  bytes << 0
68
106
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Core
4
+
5
+ class Utilities
6
+
7
+ @address_codec = nil
8
+
9
+ def initialize
10
+ @address_codec = AddressCodec.new
11
+ end
12
+
13
+ # Returns the singleton instance of the Utilities class.
14
+ # @return [Utilities] The singleton instance.
15
+ def self.instance
16
+ @@instance ||= new
17
+ end
18
+
19
+ # Checks if a string is a valid X-address.
20
+ # @param x_address [String] The X-address to check.
21
+ # @return [Boolean] True if the string is a valid X-address, false otherwise.
22
+ def is_x_address?(x_address)
23
+ return false unless x_address.is_a?(String) && x_address.start_with?('X')
24
+
25
+ begin
26
+ decoded = @address_codec.decode_x_address(x_address)
27
+ return false if decoded[:account_id].nil? || decoded[:account_id].length != 20
28
+
29
+ tag = decoded[:tag]
30
+ return false if tag && (tag < 0 || tag > MAX_32_BIT_UNSIGNED_INT)
31
+
32
+ true
33
+ rescue StandardError
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ed25519'
4
+
5
+ module KeyPairs
6
+ # Ed25519 implementation for XRPL key pairs.
7
+ module Ed25519
8
+ # Derives a key pair from a seed.
9
+ # @param seed [Array<Integer>] 16 bytes of seed entropy.
10
+ # @return [Hash] A hash containing :public_key and :private_key (hex strings).
11
+ def self.derive_key_pair(seed)
12
+ # XRPL Ed25519 uses the SHA512 of the 16-byte seed as the 32-byte entropy for the signing key.
13
+ seed_bytes = seed.is_a?(Array) ? seed.pack('C*') : seed
14
+ hash = Digest::SHA512.digest(seed_bytes)[0...32]
15
+ signing_key = ::Ed25519::SigningKey.new(hash)
16
+
17
+ # Public key in XRPL is 0xED followed by 32 bytes of public key.
18
+ public_key = [0xED].pack('C') + signing_key.verify_key.to_bytes
19
+
20
+ # Private key in XRPL is the 32-byte hash.
21
+ {
22
+ public_key: public_key.unpack1('H*').upcase,
23
+ private_key: hash.unpack1('H*').upcase
24
+ }
25
+ end
26
+
27
+ # Signs a message with a private key.
28
+ # @param message [Array<Integer>, String] The message to sign.
29
+ # @param private_key [String] The private key (32-byte hash as hex).
30
+ # @return [String] The signature (hex string).
31
+ def self.sign(message, private_key)
32
+ msg_bytes = message.is_a?(String) ? [message].pack('H*') : message.pack('C*')
33
+ key_bytes = [private_key].pack('H*')
34
+ signing_key = ::Ed25519::SigningKey.new(key_bytes)
35
+
36
+ signature = signing_key.sign(msg_bytes)
37
+ signature.unpack1('H*').upcase
38
+ end
39
+
40
+ # Verifies a signature.
41
+ # @param message [Array<Integer>, String] The message.
42
+ # @param signature [String] The signature (hex string).
43
+ # @param public_key [String] The public key (33-byte hex, starts with ED).
44
+ # @return [Boolean] True if the signature is valid.
45
+ def self.verify(message, signature, public_key)
46
+ msg_bytes = message.is_a?(String) ? hex_to_bin(message) : message.pack('C*')
47
+ sig_bytes = [signature].pack('H*')
48
+
49
+ # Strip the 0xED prefix from the public key
50
+ pub_bytes = [public_key].pack('H*')
51
+ if pub_bytes[0].ord != 0xED
52
+ raise ArgumentError, "Invalid Ed25519 public key prefix"
53
+ end
54
+
55
+ verify_key = ::Ed25519::VerifyKey.new(pub_bytes[1..-1])
56
+ begin
57
+ verify_key.verify(sig_bytes, msg_bytes)
58
+ true
59
+ rescue ::Ed25519::VerifyError
60
+ false
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module KeyPairs
6
+ # Main entry point for XRPL key pair operations.
7
+ class KeyPairs
8
+ def initialize
9
+ @address_codec = AddressCodec::AddressCodec.new
10
+ end
11
+
12
+ # Generates a new seed.
13
+ # @param entropy [Array<Integer>, nil] 16 bytes of entropy.
14
+ # @param type [String] The seed type ('secp256k1' or 'ed25519').
15
+ # @return [String] The encoded seed string.
16
+ def generate_seed(entropy = nil, type = 'secp256k1')
17
+ entropy ||= SecureRandom.random_bytes(16).bytes
18
+ @address_codec.encode_seed(entropy, type)
19
+ end
20
+
21
+ # Derives a key pair from an encoded seed.
22
+ # @param seed [String] The encoded seed string.
23
+ # @param options [Hash] Options including :account_index (for secp256k1).
24
+ # @return [Hash] A hash containing :public_key and :private_key (hex strings).
25
+ def derive_key_pair(seed, options = {})
26
+ decoded = @address_codec.decode_seed(seed)
27
+ type = decoded[:type]
28
+ entropy = decoded[:bytes]
29
+
30
+ if type == 'ed25519'
31
+ Ed25519.derive_key_pair(entropy)
32
+ else
33
+ # For secp256k1, we use the entropy as seed
34
+ Secp256k1.derive_key_pair(entropy)
35
+ end
36
+ end
37
+
38
+ # Signs a message with a private key.
39
+ # @param message [String] The message to sign as hex.
40
+ # @param private_key [String] The private key as hex.
41
+ # @param algorithm [String, nil] The algorithm to use ('secp256k1' or 'ed25519').
42
+ # @return [String] The signature as hex.
43
+ def sign(message, private_key, algorithm = nil)
44
+ if algorithm == 'ed25519' || (algorithm.nil? && private_key.length == 64)
45
+ # Heuristic: Ed25519 private keys in our lib are 32 bytes (64 hex chars).
46
+ # Secp256k1 are also 32 bytes, but Ed25519 is often explicitly requested.
47
+ # Actually, let's look at the prefix of the public key if we had it.
48
+ # Since we don't have the public key here, we rely on the caller or length.
49
+ # In XRPL-Ruby, Ed25519 private keys are 64 hex chars (32 bytes).
50
+ # Secp256k1 are also 64 hex chars. This is ambiguous!
51
+ # Let's try to see if it's explicitly 'ed25519'.
52
+ begin
53
+ return Ed25519.sign(message, private_key) if algorithm == 'ed25519'
54
+ return Secp256k1.sign(message, private_key)
55
+ rescue => e
56
+ # If secp fails and we didn't specify, maybe it was ed?
57
+ # But that's dangerous.
58
+ raise e
59
+ end
60
+ else
61
+ Secp256k1.sign(message, private_key)
62
+ end
63
+ end
64
+
65
+ # Verifies a signature.
66
+ # @param message [String] The message as hex.
67
+ # @param signature [String] The signature as hex.
68
+ # @param public_key [String] The public key as hex.
69
+ # @return [Boolean] True if the signature is valid.
70
+ def verify(message, signature, public_key)
71
+ if public_key.start_with?('ED')
72
+ Ed25519.verify(message, signature, public_key)
73
+ else
74
+ Secp256k1.verify(message, signature, public_key)
75
+ end
76
+ end
77
+
78
+ # Derives an XRP address from a public key.
79
+ # @param public_key [String] The public key as hex.
80
+ # @return [String] The XRP address.
81
+ def derive_address(public_key)
82
+ public_key_bytes = [public_key].pack('H*')
83
+ # Account ID is RIPEMD160(SHA256(public_key))
84
+ sha256 = Digest::SHA256.digest(public_key_bytes)
85
+ # Ruby doesn't have RIPEMD160 in Digest by default sometimes,
86
+ # but OpenSSL has it.
87
+ ripemd160 = OpenSSL::Digest.new('RIPEMD160').digest(sha256)
88
+
89
+ @address_codec.encode_account_id(ripemd160.unpack('C*'))
90
+ end
91
+ end
92
+ end