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
|
@@ -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.
|
|
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:
|
|
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:
|