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
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module KeyPairs
6
+ # Secp256k1 implementation for XRPL key pairs.
7
+ module Secp256k1
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 Secp256k1 uses a specific derivation algorithm (seed -> family seed -> sequence 0 key).
13
+ # Seed is passed as a 16-byte array.
14
+ private_key = derive_private_key(seed)
15
+ public_key = derive_public_key(private_key)
16
+
17
+ {
18
+ public_key: public_key.unpack1('H*').upcase,
19
+ private_key: private_key.unpack1('H*').upcase
20
+ }
21
+ end
22
+
23
+ # Signs a message with a private key.
24
+ # @param message [Array<Integer>, String] The message to sign.
25
+ # @param private_key [String] The private key (hex string).
26
+ # @return [String] The signature (hex string).
27
+ def self.sign(message, private_key)
28
+ msg_hash = message.is_a?(String) ? [message].pack('H*') : message.pack('C*')
29
+
30
+ # Use the dsa_sign_asn1 method directly on a PKey::EC object.
31
+ # In OpenSSL 3.0, setting private_key might be blocked if not done carefully.
32
+ # We use a fresh object and set the key.
33
+ ec = OpenSSL::PKey::EC.new('secp256k1')
34
+ begin
35
+ # Use send to bypass potential visibility or immutability checks if any,
36
+ # but usually private_key= is public.
37
+ ec.private_key = OpenSSL::BN.new(private_key, 16)
38
+ ec.public_key = ec.group.generator.mul(ec.private_key)
39
+
40
+ signature = ec.dsa_sign_asn1(msg_hash)
41
+ signature.unpack1('H*').upcase
42
+ rescue OpenSSL::PKey::PKeyError => e
43
+ # If OpenSSL 3.0 is too strict, we might be unable to sign without a dedicated gem.
44
+ # However, for the sake of completion, we'll try a fallback if it exists.
45
+ raise "Secp256k1 signing failed: #{e.message}. This might be due to OpenSSL 3.0 immutability."
46
+ end
47
+ end
48
+
49
+ # Verifies a signature.
50
+ # @param message [Array<Integer>, String] The message.
51
+ # @param signature [String] The signature (hex string).
52
+ # @param public_key [String] The public key (hex string).
53
+ # @return [Boolean] True if the signature is valid.
54
+ def self.verify(message, signature, public_key)
55
+ msg_hash = message.is_a?(String) ? [message].pack('H*') : message.pack('C*')
56
+ sig_bytes = [signature].pack('H*')
57
+
58
+ ec = OpenSSL::PKey::EC.new('secp256k1')
59
+ ec.public_key = OpenSSL::PKey::EC::Point.new(ec.group, OpenSSL::BN.new(public_key, 16))
60
+
61
+ ec.dsa_verify_asn1(msg_hash, sig_bytes)
62
+ end
63
+
64
+ private
65
+
66
+ # Derives the 32-byte private key from the 16-byte seed.
67
+ # This follows the Ripple seed derivation algorithm.
68
+ def self.derive_private_key(seed, account_index = 0)
69
+ ec_key = OpenSSL::PKey::EC.new('secp256k1')
70
+ order = ec_key.group.order
71
+
72
+ # 1. Derive root private key from seed
73
+ root_private_key = derive_scalar(seed)
74
+
75
+ # 2. Derive root public key from root private key
76
+ root_public_key = derive_root_public_key(root_private_key)
77
+
78
+ # 3. Derive child scalar from root public key and account_index
79
+ child_scalar = derive_scalar(root_public_key, account_index)
80
+
81
+ # 4. child_private_key = (root_private_key + child_scalar) % order
82
+ child_private_key = (OpenSSL::BN.new(root_private_key, 16) + OpenSSL::BN.new(child_scalar, 16)) % order
83
+ child_private_key.to_s(16).rjust(64, '0')
84
+ end
85
+
86
+ def self.derive_scalar(seed, sequence = nil)
87
+ seed_bytes = seed.is_a?(Array) ? seed.pack('C*') : seed
88
+ ec_key = OpenSSL::PKey::EC.new('secp256k1')
89
+ order = ec_key.group.order
90
+ loop_count = 0
91
+ loop do
92
+ data = sequence ? seed_bytes + [sequence].pack('N') : seed_bytes
93
+ data += [loop_count].pack('N')
94
+ hash = Digest::SHA512.digest(data)[0...32]
95
+ scalar = OpenSSL::BN.new(hash.unpack1('H*'), 16)
96
+
97
+ if scalar > 0 && scalar < order
98
+ return scalar.to_s(16).rjust(64, '0')
99
+ end
100
+ loop_count += 1
101
+ raise "Too many loops" if loop_count > 100
102
+ end
103
+ end
104
+
105
+ def self.derive_root_public_key(private_key_hex)
106
+ group = OpenSSL::PKey::EC::Group.new('secp256k1')
107
+ key_bn = OpenSSL::BN.new(private_key_hex, 16)
108
+ pub_key_point = group.generator.mul(key_bn)
109
+ pub_key_point.to_octet_string(:compressed)
110
+ end
111
+
112
+ def self.derive_public_key(private_key_hex)
113
+ derive_root_public_key(private_key_hex)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'openssl'
5
+
6
+ module Wallet
7
+ # Represents an XRPL wallet, providing methods for signing and address derivation.
8
+ class Wallet
9
+ # @return [String] The public key as hex.
10
+ attr_reader :public_key
11
+ # @return [String] The private key as hex.
12
+ attr_reader :private_key
13
+ # @return [String] The encoded seed.
14
+ attr_reader :seed
15
+ # @return [String] The classic address.
16
+ attr_reader :classic_address
17
+ # @return [String] The algorithm used ('secp256k1' or 'ed25519').
18
+ attr_reader :algorithm
19
+
20
+ # Initializes a new Wallet instance.
21
+ # @param public_key [String] The public key as hex.
22
+ # @param private_key [String] The private key as hex.
23
+ # @param seed [String, nil] The encoded seed.
24
+ # @param classic_address [String, nil] The classic address (optional, derived if not provided).
25
+ def initialize(public_key, private_key, seed: nil, classic_address: nil)
26
+ @public_key = public_key
27
+ @private_key = private_key
28
+ @seed = seed
29
+ @algorithm = public_key.start_with?('ED') ? 'ed25519' : 'secp256k1'
30
+ @key_pairs = KeyPairs::KeyPairs.new
31
+ @classic_address = classic_address || @key_pairs.derive_address(public_key)
32
+ end
33
+
34
+ # Generates a new random wallet.
35
+ # @param algorithm [String] The algorithm to use ('secp256k1' or 'ed25519').
36
+ # @return [Wallet] A new Wallet instance.
37
+ def self.generate(algorithm = 'secp256k1')
38
+ kp = KeyPairs::KeyPairs.new
39
+ seed = kp.generate_seed(nil, algorithm)
40
+ from_seed(seed)
41
+ end
42
+
43
+ # Creates a wallet from a seed.
44
+ # @param seed [String] The encoded seed.
45
+ # @param options [Hash] Options for key derivation.
46
+ # @return [Wallet] A new Wallet instance.
47
+ def self.from_seed(seed, options = {})
48
+ kp = KeyPairs::KeyPairs.new
49
+ keys = kp.derive_key_pair(seed, options)
50
+ new(keys[:public_key], keys[:private_key], seed: seed)
51
+ end
52
+
53
+ # Creates a wallet from entropy.
54
+ # @param entropy [Array<Integer>] 16 bytes of entropy.
55
+ # @param algorithm [String] The algorithm to use ('secp256k1' or 'ed25519').
56
+ # @return [Wallet] A new Wallet instance.
57
+ def self.from_entropy(entropy, algorithm = 'secp256k1')
58
+ kp = KeyPairs::KeyPairs.new
59
+ seed = kp.generate_seed(entropy, algorithm)
60
+ from_seed(seed)
61
+ end
62
+
63
+ # Signs a message (hex string) with the wallet's private key.
64
+ # @param message [String, Hash] The message (hex string) or transaction (Hash) to sign.
65
+ # @param multisign [Boolean] Whether to sign for a multisigned transaction.
66
+ # @return [String] The signature as a hex string.
67
+ def sign(message, multisign: false)
68
+ algorithm = @algorithm
69
+ # Check if message is a Hash (transaction)
70
+ if message.is_a?(Hash)
71
+ prefix = multisign ? BinaryCodec::HASH_PREFIX[:transaction_multi_sig] : BinaryCodec::HASH_PREFIX[:transaction_sig]
72
+ signing_data = BinaryCodec.signing_data(message, prefix, signing_fields_only: true)
73
+ message = bytes_to_hex(signing_data)
74
+ end
75
+
76
+ @key_pairs.sign(message, @private_key, algorithm)
77
+ end
78
+
79
+ # Verifies a signature for a message.
80
+ # @param message [String] The message as a hex string.
81
+ # @param signature [String] The signature as a hex string.
82
+ # @return [Boolean] True if the signature is valid.
83
+ def verify(message, signature)
84
+ @key_pairs.verify(message, signature, @public_key)
85
+ end
86
+
87
+ # Verifies a signed transaction blob.
88
+ # @param signed_transaction [String] The signed transaction blob as a hex string.
89
+ # @return [Boolean] True if the transaction signature is valid.
90
+ def verify_transaction(signed_transaction)
91
+ decoded = BinaryCodec.binary_to_json(signed_transaction)
92
+ # The signing data is the transaction without the TxnSignature field,
93
+ # prefixed by 0x53545800 (STX\0).
94
+
95
+ tx_for_signing = decoded.dup
96
+ signature = tx_for_signing.delete('TxnSignature')
97
+ return false unless signature
98
+
99
+ signing_data = BinaryCodec.signing_data(tx_for_signing)
100
+ @key_pairs.verify(bytes_to_hex(signing_data), signature, @public_key)
101
+ end
102
+
103
+ # Derives the X-address for this wallet.
104
+ # @param tag [Integer, false, nil] The destination tag.
105
+ # @param test_network [Boolean] Whether the address is for a test network.
106
+ # @return [String] The encoded X-address.
107
+ def get_x_address(tag: nil, test_network: false)
108
+ address_codec = AddressCodec::AddressCodec.new
109
+ address_codec.classic_address_to_x_address(@classic_address, tag, test_network)
110
+ end
111
+
112
+ # @return [String] String representation of the wallet.
113
+ def to_s
114
+ "Wallet(address: #{@classic_address}, public_key: #{@public_key})"
115
+ end
116
+ end
117
+ end
data/lib/xrpl-ruby.rb CHANGED
@@ -1,14 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'core/base_x'
4
+ require_relative 'core/base_58_xrp'
3
5
  require_relative 'core/core'
6
+ require_relative 'core/utilities'
4
7
 
5
8
  require_relative 'address-codec/codec'
6
9
  require_relative 'address-codec/xrp_codec'
7
10
  require_relative 'address-codec/address_codec'
8
11
 
12
+ require_relative 'binary-codec/enums/constants'
13
+ require_relative 'binary-codec/utilities'
9
14
  require_relative 'binary-codec/enums/fields'
10
15
  require_relative 'binary-codec/enums/definitions'
11
16
  require_relative 'binary-codec/serdes/binary_parser'
12
17
  require_relative 'binary-codec/serdes/bytes_list'
18
+ require_relative 'binary-codec/serdes/binary_serializer'
13
19
  require_relative 'binary-codec/types/serialized_type'
14
- require_relative 'binary-codec/types/hash'
20
+ require_relative 'binary-codec/types/hash'
21
+ require_relative 'binary-codec/types/uint'
22
+ require_relative 'binary-codec/types/account_id'
23
+ require_relative 'binary-codec/types/amount'
24
+ require_relative 'binary-codec/types/blob'
25
+ require_relative 'binary-codec/types/currency'
26
+ require_relative 'binary-codec/types/st_array'
27
+ require_relative 'binary-codec/types/st_object'
28
+ require_relative 'binary-codec/types/vector256'
29
+ require_relative 'binary-codec/types/path_set'
30
+ require_relative 'binary-codec/types/issue'
31
+ require_relative 'binary-codec/types/xchain_bridge'
32
+ require_relative 'binary-codec/binary_codec'
33
+
34
+ require_relative 'wallet/wallet'
35
+
36
+ require_relative 'key-pairs/ed25519'
37
+ require_relative 'key-pairs/secp256k1'
38
+ require_relative 'key-pairs/key_pairs'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xrpl-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Busse
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-20 00:00:00.000000000 Z
11
+ date: 2026-03-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ed25519
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
55
69
  description: This gem provides a Ruby interface to interact with the XRP Ledger (XRPL)
56
70
  blockchain, allowing developers to easily integrate XRPL functionality into their
57
71
  Ruby applications.
@@ -77,13 +91,23 @@ files:
77
91
  - lib/binary-codec/types/blob.rb
78
92
  - lib/binary-codec/types/currency.rb
79
93
  - lib/binary-codec/types/hash.rb
94
+ - lib/binary-codec/types/issue.rb
95
+ - lib/binary-codec/types/path_set.rb
80
96
  - lib/binary-codec/types/serialized_type.rb
97
+ - lib/binary-codec/types/st_array.rb
81
98
  - lib/binary-codec/types/st_object.rb
82
99
  - lib/binary-codec/types/uint.rb
100
+ - lib/binary-codec/types/vector256.rb
101
+ - lib/binary-codec/types/xchain_bridge.rb
83
102
  - lib/binary-codec/utilities.rb
84
103
  - lib/core/base_58_xrp.rb
85
104
  - lib/core/base_x.rb
86
105
  - lib/core/core.rb
106
+ - lib/core/utilities.rb
107
+ - lib/key-pairs/ed25519.rb
108
+ - lib/key-pairs/key_pairs.rb
109
+ - lib/key-pairs/secp256k1.rb
110
+ - lib/wallet/wallet.rb
87
111
  - lib/xrpl-ruby.rb
88
112
  homepage: https://github.com/AlexanderBuzz/xrpl-ruby
89
113
  licenses: