tron.rb 1.1.2 → 1.1.4
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/README.md +94 -308
- data/lib/tron/abi/constant.rb +18 -0
- data/lib/tron/abi/decoder.rb +174 -0
- data/lib/tron/abi/encoder.rb +293 -0
- data/lib/tron/abi/event.rb +133 -0
- data/lib/tron/abi/function.rb +135 -0
- data/lib/tron/abi/type.rb +261 -0
- data/lib/tron/abi/util.rb +100 -0
- data/lib/tron/abi.rb +153 -0
- data/lib/tron/client.rb +64 -1
- data/lib/tron/configuration.rb +39 -2
- data/lib/tron/contract.rb +157 -0
- data/lib/tron/key.rb +271 -0
- data/lib/tron/protobuf/transaction_raw_serializer.rb +295 -0
- data/lib/tron/protobuf.rb +18 -0
- data/lib/tron/services/balance.rb +43 -2
- data/lib/tron/services/contract.rb +232 -0
- data/lib/tron/services/price.rb +40 -0
- data/lib/tron/services/resources.rb +27 -0
- data/lib/tron/services/transaction.rb +104 -0
- data/lib/tron/signature.rb +21 -0
- data/lib/tron/utils/abi.rb +321 -0
- data/lib/tron/utils/address.rb +63 -10
- data/lib/tron/utils/cache.rb +18 -0
- data/lib/tron/utils/crypto.rb +67 -0
- data/lib/tron/utils/http.rb +49 -4
- data/lib/tron/utils/rate_limiter.rb +16 -0
- data/lib/tron/version.rb +1 -1
- data/lib/tron.rb +30 -1
- metadata +71 -12
data/lib/tron/configuration.rb
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
# lib/tron/configuration.rb
|
|
2
2
|
module Tron
|
|
3
|
+
# Configuration class for the TRON client
|
|
4
|
+
# Stores settings for API keys, network, timeouts, caching, etc.
|
|
3
5
|
class Configuration
|
|
4
|
-
|
|
5
|
-
attr_accessor :
|
|
6
|
+
# @return [String] TronGrid API key
|
|
7
|
+
attr_accessor :api_key
|
|
8
|
+
# @return [String] Tronscan API key
|
|
9
|
+
attr_accessor :tronscan_api_key
|
|
10
|
+
# @return [Integer] timeout for API requests in seconds
|
|
11
|
+
attr_accessor :timeout
|
|
12
|
+
# @return [String] base URL for TRON API
|
|
13
|
+
attr_accessor :base_url
|
|
14
|
+
# @return [String] base URL for Tronscan API
|
|
15
|
+
attr_accessor :tronscan_base_url
|
|
16
|
+
# @return [Boolean] whether strict validation is enabled
|
|
17
|
+
attr_accessor :strict_mode
|
|
18
|
+
# @return [Boolean] whether caching is enabled
|
|
19
|
+
attr_accessor :cache_enabled
|
|
20
|
+
# @return [Integer] cache TTL (time-to-live) in seconds
|
|
21
|
+
attr_accessor :cache_ttl
|
|
22
|
+
# @return [Integer] max stale time in seconds
|
|
23
|
+
attr_accessor :cache_max_stale
|
|
24
|
+
# @return [String] default address for read-only calls
|
|
25
|
+
attr_accessor :default_address
|
|
26
|
+
# @return [Integer] default fee limit for transactions
|
|
27
|
+
attr_accessor :fee_limit
|
|
28
|
+
# @return [Symbol] network (:mainnet, :shasta, :nile)
|
|
6
29
|
attr_reader :network
|
|
7
30
|
|
|
31
|
+
# Creates a new configuration instance with default values
|
|
8
32
|
def initialize
|
|
9
33
|
@network = :mainnet
|
|
10
34
|
@timeout = 30
|
|
@@ -13,14 +37,26 @@ module Tron
|
|
|
13
37
|
@cache_enabled = true
|
|
14
38
|
@cache_ttl = 300 # 5 minutes default TTL
|
|
15
39
|
@cache_max_stale = 600 # 10 minutes max stale
|
|
40
|
+
# Contract-related defaults
|
|
41
|
+
@default_address = nil
|
|
42
|
+
@fee_limit = 100_000_000 # 100 TRX default
|
|
16
43
|
setup_urls
|
|
17
44
|
end
|
|
18
45
|
|
|
46
|
+
# Sets the network and updates the base URLs accordingly
|
|
47
|
+
#
|
|
48
|
+
# @param network [Symbol] the network to use (:mainnet, :shasta, :nile)
|
|
19
49
|
def network=(network)
|
|
20
50
|
@network = network
|
|
21
51
|
setup_urls
|
|
22
52
|
end
|
|
23
53
|
|
|
54
|
+
# Sets cache configuration options
|
|
55
|
+
#
|
|
56
|
+
# @param options [Hash, Boolean] cache configuration or false to disable
|
|
57
|
+
# @option options [Boolean] :enabled whether caching is enabled (default: true)
|
|
58
|
+
# @option options [Integer] :ttl cache TTL in seconds (default: 300)
|
|
59
|
+
# @option options [Integer] :max_stale max stale time in seconds (default: 600)
|
|
24
60
|
def cache=(options)
|
|
25
61
|
if options.is_a?(Hash)
|
|
26
62
|
@cache_enabled = options.fetch(:enabled, true)
|
|
@@ -33,6 +69,7 @@ module Tron
|
|
|
33
69
|
|
|
34
70
|
private
|
|
35
71
|
|
|
72
|
+
# Sets up the base URLs based on the current network
|
|
36
73
|
def setup_urls
|
|
37
74
|
case @network
|
|
38
75
|
when :mainnet
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative 'abi'
|
|
3
|
+
|
|
4
|
+
module Tron
|
|
5
|
+
# The Contract class provides utilities for interacting with TRON smart contracts
|
|
6
|
+
# including calling read-only functions and executing state-changing functions
|
|
7
|
+
class Contract
|
|
8
|
+
# @return [String] the contract address
|
|
9
|
+
attr_reader :address
|
|
10
|
+
# @return [Array<Hash>] the contract ABI (Application Binary Interface)
|
|
11
|
+
attr_reader :abi
|
|
12
|
+
|
|
13
|
+
# Creates a new contract instance
|
|
14
|
+
#
|
|
15
|
+
# @param address [String] the contract address
|
|
16
|
+
# @param abi_json [Array<Hash>] the contract ABI as a JSON array
|
|
17
|
+
# @param configuration [Tron::Configuration] the configuration to use
|
|
18
|
+
def initialize(address, abi_json, configuration)
|
|
19
|
+
@address = address
|
|
20
|
+
@abi = abi_json
|
|
21
|
+
@configuration = configuration
|
|
22
|
+
@client = Client.new(@configuration)
|
|
23
|
+
|
|
24
|
+
# Parse ABI and create method wrappers
|
|
25
|
+
@functions = {}
|
|
26
|
+
parse_abi
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Create a contract instance from ABI JSON
|
|
30
|
+
#
|
|
31
|
+
# @param abi [Array<Hash>] the contract ABI
|
|
32
|
+
# @param address [String] the contract address
|
|
33
|
+
# @param configuration [Tron::Configuration] the configuration to use (optional)
|
|
34
|
+
# @return [Tron::Contract] a new contract instance
|
|
35
|
+
def self.from_abi(abi:, address:, configuration: nil)
|
|
36
|
+
config = configuration || Client.configuration
|
|
37
|
+
new(address, abi, config)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Call a read-only function on the contract
|
|
41
|
+
# This method does not change the blockchain state and doesn't require signing
|
|
42
|
+
#
|
|
43
|
+
# @param function_name [String] name of the function to call
|
|
44
|
+
# @param args [Array] arguments to pass to the function
|
|
45
|
+
# @param key [String] optional parameter (not used in current implementation)
|
|
46
|
+
# @return the decoded result from the contract function
|
|
47
|
+
def call_function(function_name, *args, key: nil)
|
|
48
|
+
function_abi = @functions[function_name]
|
|
49
|
+
raise "Function #{function_name} not found in ABI" unless function_abi
|
|
50
|
+
|
|
51
|
+
# Encode the function call
|
|
52
|
+
encoded_data = encode_function_call(function_abi, args)
|
|
53
|
+
|
|
54
|
+
# Call the contract
|
|
55
|
+
result = @client.contract_service.call_contract(
|
|
56
|
+
contract_address: @address,
|
|
57
|
+
function: encoded_data,
|
|
58
|
+
parameters: []
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Decode the result
|
|
62
|
+
decode_function_output(function_abi, result)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Execute a state-changing function on the contract
|
|
66
|
+
# This method changes the blockchain state and requires a private key for signing
|
|
67
|
+
#
|
|
68
|
+
# @param function_name [String] name of the function to execute
|
|
69
|
+
# @param args [Array] arguments to pass to the function
|
|
70
|
+
# @param private_key [String] private key to sign the transaction
|
|
71
|
+
# @param fee_limit [Integer] maximum energy fee to pay (default: 100_000_000)
|
|
72
|
+
# @param call_value [Integer] amount of TRX to send with the call (default: 0)
|
|
73
|
+
# @return the transaction result
|
|
74
|
+
def execute_function(function_name, *args, private_key:, fee_limit: 100_000_000, call_value: 0)
|
|
75
|
+
function_abi = @functions[function_name]
|
|
76
|
+
raise "Function #{function_name} not found in ABI" unless function_abi
|
|
77
|
+
|
|
78
|
+
# Encode the function call
|
|
79
|
+
encoded_data = encode_function_call(function_abi, args)
|
|
80
|
+
|
|
81
|
+
# Trigger the contract
|
|
82
|
+
@client.contract_service.trigger_contract(
|
|
83
|
+
contract_address: @address,
|
|
84
|
+
function: encoded_data,
|
|
85
|
+
parameters: [],
|
|
86
|
+
private_key: private_key,
|
|
87
|
+
fee_limit: fee_limit,
|
|
88
|
+
call_value: call_value
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
# Parses the ABI to extract available functions
|
|
95
|
+
def parse_abi
|
|
96
|
+
@abi.each do |item|
|
|
97
|
+
next unless item['type'] == 'function'
|
|
98
|
+
|
|
99
|
+
name = item['name']
|
|
100
|
+
@functions[name] = item
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Encodes a function call with the given arguments
|
|
105
|
+
#
|
|
106
|
+
# @param function_abi [Hash] the ABI definition for the function
|
|
107
|
+
# @param args [Array] arguments to encode
|
|
108
|
+
# @return [String] the encoded function call
|
|
109
|
+
def encode_function_call(function_abi, args)
|
|
110
|
+
# Get function signature
|
|
111
|
+
input_types = function_abi['inputs'].map { |input| input['type'] }
|
|
112
|
+
|
|
113
|
+
# Parse types using the new ABI system
|
|
114
|
+
parsed_types = input_types.map { |type_str| Abi::Type.parse(type_str) }
|
|
115
|
+
|
|
116
|
+
# Encode arguments
|
|
117
|
+
encoded_args = []
|
|
118
|
+
args.each_with_index do |arg, idx|
|
|
119
|
+
encoded_args << Abi::Encoder.type(parsed_types[idx], arg)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Create function selector (first 4 bytes of keccak hash of function signature)
|
|
123
|
+
signature_parts = function_abi['inputs'].map { |input| "#{input['type']} #{input['name']}" }
|
|
124
|
+
signature = "#{function_abi['name']}(#{function_abi['inputs'].map { |input| input['type'] }.join(',')})"
|
|
125
|
+
|
|
126
|
+
# Calculate function selector using keccak256
|
|
127
|
+
function_selector = calculate_function_selector(signature)
|
|
128
|
+
|
|
129
|
+
# Combine selector and encoded args
|
|
130
|
+
function_selector + encoded_args.join
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Calculates the function selector from the function signature
|
|
134
|
+
#
|
|
135
|
+
# @param signature [String] the function signature
|
|
136
|
+
# @return [String] the function selector (first 4 bytes of keccak hash)
|
|
137
|
+
def calculate_function_selector(signature)
|
|
138
|
+
# Use keccak256 to calculate the function selector (first 4 bytes)
|
|
139
|
+
hash = Utils::Crypto.keccak256(signature)
|
|
140
|
+
# Take only first 4 bytes (8 hex chars)
|
|
141
|
+
Utils::Crypto.bin_to_hex(hash[0, 4])
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Decodes the output of a function call
|
|
145
|
+
#
|
|
146
|
+
# @param function_abi [Hash] the ABI definition for the function
|
|
147
|
+
# @param output_data [String] the raw output data to decode
|
|
148
|
+
# @return the decoded output
|
|
149
|
+
def decode_function_output(function_abi, output_data)
|
|
150
|
+
# Parse output types
|
|
151
|
+
output_types = function_abi['outputs'].map { |output| Abi::Type.parse(output['type']) }
|
|
152
|
+
|
|
153
|
+
# Decode the output
|
|
154
|
+
Abi::Decoder.type(output_types.first, output_data) # Simplified for single output
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
data/lib/tron/key.rb
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'rbsecp256k1'
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
require_relative 'utils/crypto'
|
|
5
|
+
require_relative 'utils/address'
|
|
6
|
+
require_relative 'signature'
|
|
7
|
+
require 'base58-alphabets'
|
|
8
|
+
|
|
9
|
+
module Tron
|
|
10
|
+
# The Key class provides utilities for key generation, signing, and address derivation
|
|
11
|
+
# for TRON blockchain interactions using the secp256k1 elliptic curve cryptography.
|
|
12
|
+
class Key
|
|
13
|
+
# TRON address prefix (41 in hex)
|
|
14
|
+
ADDRESS_PREFIX = '41'.freeze
|
|
15
|
+
|
|
16
|
+
# @return [Secp256k1::PrivateKey] the private key object
|
|
17
|
+
attr_reader :private_key
|
|
18
|
+
# @return [Secp256k1::PublicKey] the public key object
|
|
19
|
+
attr_reader :public_key
|
|
20
|
+
|
|
21
|
+
# Creates a new key pair
|
|
22
|
+
# If no private key is provided, generates a new random key pair
|
|
23
|
+
#
|
|
24
|
+
# @param priv [String, nil] hexadecimal private key (32 bytes) or nil to generate random key
|
|
25
|
+
# @raise [ArgumentError] if the private key is invalid
|
|
26
|
+
def initialize(priv: nil)
|
|
27
|
+
# Creates a new, randomized libsecp256k1 context.
|
|
28
|
+
ctx = Secp256k1::Context.new(context_randomization_bytes: SecureRandom.random_bytes(32))
|
|
29
|
+
|
|
30
|
+
key = if priv.nil?
|
|
31
|
+
# Creates a new random key pair (public, private).
|
|
32
|
+
ctx.generate_key_pair
|
|
33
|
+
else
|
|
34
|
+
# Validate private key format and size
|
|
35
|
+
if priv.is_a?(String) && Tron::Utils::Crypto.is_hex?(priv)
|
|
36
|
+
# Convert hex private key to binary
|
|
37
|
+
priv = Tron::Utils::Crypto.hex_to_bin(priv)
|
|
38
|
+
elsif priv.is_a?(String) && !Tron::Utils::Crypto.is_hex?(priv)
|
|
39
|
+
raise ArgumentError, "Private key must be a valid hex string"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Validate private key size (must be 32 bytes)
|
|
43
|
+
raise ArgumentError, "Private key must be 32 bytes" unless priv.bytesize == 32
|
|
44
|
+
|
|
45
|
+
# Creates a keypair from existing private key data.
|
|
46
|
+
ctx.key_pair_from_private_key(priv)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Sets the attributes.
|
|
50
|
+
@private_key = key.private_key
|
|
51
|
+
@public_key = key.public_key
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns the private key in hexadecimal format
|
|
55
|
+
#
|
|
56
|
+
# @return [String] private key as hexadecimal string
|
|
57
|
+
def private_hex
|
|
58
|
+
Tron::Utils::Crypto.bin_to_hex(@private_key.data)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns the private key in binary format
|
|
62
|
+
#
|
|
63
|
+
# @return [String] private key as binary string
|
|
64
|
+
def private_bytes
|
|
65
|
+
@private_key.data
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the uncompressed public key in hexadecimal format
|
|
69
|
+
#
|
|
70
|
+
# @return [String] uncompressed public key as hexadecimal string
|
|
71
|
+
def public_hex
|
|
72
|
+
Tron::Utils::Crypto.bin_to_hex(@public_key.uncompressed)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Returns the compressed public key in hexadecimal format
|
|
76
|
+
#
|
|
77
|
+
# @return [String] compressed public key as hexadecimal string
|
|
78
|
+
def public_hex_compressed
|
|
79
|
+
Tron::Utils::Crypto.bin_to_hex(@public_key.compressed)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns the uncompressed public key in binary format
|
|
83
|
+
#
|
|
84
|
+
# @return [String] uncompressed public key as binary string
|
|
85
|
+
def public_bytes
|
|
86
|
+
@public_key.uncompressed
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns the compressed public key in binary format
|
|
90
|
+
#
|
|
91
|
+
# @return [String] compressed public key as binary string
|
|
92
|
+
def public_bytes_compressed
|
|
93
|
+
@public_key.compressed
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Derives the TRON address from the public key
|
|
97
|
+
# Uses the TRON address derivation algorithm with Keccak256 hashing and Base58Check encoding
|
|
98
|
+
#
|
|
99
|
+
# @return [String] TRON address
|
|
100
|
+
def address
|
|
101
|
+
# TRON address derivation algorithm:
|
|
102
|
+
# 1. Take uncompressed public key (64 bytes after removing prefix 0x04)
|
|
103
|
+
# 2. Hash with Keccak256
|
|
104
|
+
# 3. Take last 20 bytes
|
|
105
|
+
# 4. Add TRON prefix (0x41)
|
|
106
|
+
# 5. Calculate checksum using base58check and encode to Base58
|
|
107
|
+
|
|
108
|
+
# Get the public key without the 0x04 prefix
|
|
109
|
+
public_key_bytes = @public_key.uncompressed[1..-1]
|
|
110
|
+
|
|
111
|
+
# Hash the public key with Keccak256
|
|
112
|
+
hash = Tron::Utils::Crypto.keccak256(public_key_bytes)
|
|
113
|
+
|
|
114
|
+
# Take the last 20 bytes
|
|
115
|
+
address_bytes = hash[-20..-1]
|
|
116
|
+
|
|
117
|
+
# Add TRON prefix (0x41)
|
|
118
|
+
prefixed_address_hex = Tron::Key::ADDRESS_PREFIX + Tron::Utils::Crypto.bin_to_hex(address_bytes)
|
|
119
|
+
prefixed_address_bytes = Tron::Utils::Crypto.hex_to_bin(prefixed_address_hex)
|
|
120
|
+
|
|
121
|
+
# Use the base58check utility
|
|
122
|
+
Tron::Utils::Crypto.base58check(prefixed_address_bytes)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Signs a binary data blob with the private key
|
|
126
|
+
#
|
|
127
|
+
# @param blob [String] binary data to sign
|
|
128
|
+
# @return [String] signature as hexadecimal string
|
|
129
|
+
def sign(blob)
|
|
130
|
+
context = Secp256k1::Context.new
|
|
131
|
+
compact, recovery_id = context.sign_recoverable(@private_key, blob).compact
|
|
132
|
+
signature = compact.bytes
|
|
133
|
+
signature << recovery_id
|
|
134
|
+
|
|
135
|
+
Tron::Utils::Crypto.bin_to_hex(signature.pack('c*'))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Signs a message using the personal sign format
|
|
139
|
+
# This prefixes the message with specific data before signing
|
|
140
|
+
#
|
|
141
|
+
# @param message [String] message to sign
|
|
142
|
+
# @return [String] signature as hexadecimal string
|
|
143
|
+
def personal_sign(message)
|
|
144
|
+
prefixed_message = Tron::Signature.prefix_message(message)
|
|
145
|
+
hashed_message = Tron::Utils::Crypto.keccak256(prefixed_message)
|
|
146
|
+
sign(hashed_message)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Verifies a signature against a data blob
|
|
150
|
+
#
|
|
151
|
+
# @param blob [String] the original signed data
|
|
152
|
+
# @param signature [String] signature to verify
|
|
153
|
+
# @param public_key_or_address [String, Secp256k1::PublicKey] public key or address to verify against
|
|
154
|
+
# @return [Boolean] true if the signature is valid
|
|
155
|
+
def verify_signature(blob, signature, public_key_or_address)
|
|
156
|
+
# Implementation adapted from eth.rb's signature verification
|
|
157
|
+
recovered_key = recover_signature(blob, signature)
|
|
158
|
+
|
|
159
|
+
case public_key_or_address
|
|
160
|
+
when String
|
|
161
|
+
if public_key_or_address.length == 34 # TRON address length
|
|
162
|
+
# Verify against TRON address
|
|
163
|
+
recovered_address = public_key_to_address(recovered_key)
|
|
164
|
+
return recovered_address == public_key_or_address
|
|
165
|
+
elsif public_key_or_address.length == 130 # Uncompressed public key hex length (with 0x prefix)
|
|
166
|
+
# Verify against full public key hex
|
|
167
|
+
public_key_hex = public_key_or_address.start_with?('0x') ? public_key_or_address[2..-1] : public_key_or_address
|
|
168
|
+
return recovered_key == public_key_hex
|
|
169
|
+
elsif public_key_or_address.length == 128 # Uncompressed public key hex length (without 0x prefix)
|
|
170
|
+
# Verify against full public key hex
|
|
171
|
+
return recovered_key == public_key_or_address
|
|
172
|
+
end
|
|
173
|
+
when Secp256k1::PublicKey
|
|
174
|
+
public_hex = Tron::Utils::Crypto.bin_to_hex(public_key_or_address.uncompressed)
|
|
175
|
+
return recovered_key == public_hex
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
raise ArgumentError, "Invalid public key or address format"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Verifies a personal message signature
|
|
182
|
+
#
|
|
183
|
+
# @param message [String] the original signed message
|
|
184
|
+
# @param signature [String] signature to verify
|
|
185
|
+
# @param public_key_or_address [String, Secp256k1::PublicKey] public key or address to verify against
|
|
186
|
+
# @return [Boolean] true if the signature is valid
|
|
187
|
+
def verify_personal_signature(message, signature, public_key_or_address)
|
|
188
|
+
prefixed_message = Tron::Signature.prefix_message(message)
|
|
189
|
+
hashed_message = Tron::Utils::Crypto.keccak256(prefixed_message)
|
|
190
|
+
verify_signature(hashed_message, signature, public_key_or_address)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
# Recovers the public key from a signature and data blob
|
|
196
|
+
#
|
|
197
|
+
# @param blob [String] the original data that was signed
|
|
198
|
+
# @param signature [String] signature to recover from
|
|
199
|
+
# @return [String] recovered public key as hexadecimal string
|
|
200
|
+
def recover_signature(blob, signature)
|
|
201
|
+
context = Secp256k1::Context.new
|
|
202
|
+
r, s, v = dissect_signature(signature)
|
|
203
|
+
|
|
204
|
+
v_int = v.to_i(16)
|
|
205
|
+
recovery_id = calculate_recovery_id(v_int)
|
|
206
|
+
|
|
207
|
+
signature_rs = Tron::Utils::Crypto.hex_to_bin("#{r}#{s}")
|
|
208
|
+
recoverable_signature = context.recoverable_signature_from_compact(signature_rs, recovery_id)
|
|
209
|
+
public_key = recoverable_signature.recover_public_key(blob)
|
|
210
|
+
|
|
211
|
+
Tron::Utils::Crypto.bin_to_hex(public_key.uncompressed)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Dissects a signature into its r, s, and v components
|
|
215
|
+
#
|
|
216
|
+
# @param signature [String] signature to dissect
|
|
217
|
+
# @return [Array<String>] array containing [r, s, v] components
|
|
218
|
+
def dissect_signature(signature)
|
|
219
|
+
signature_hex = signature.start_with?('0x') ? signature[2..-1] : signature
|
|
220
|
+
if signature_hex.length < 128
|
|
221
|
+
raise ArgumentError, "Invalid signature length: #{signature_hex.length}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
r = signature_hex[0, 64]
|
|
225
|
+
s = signature_hex[64, 64]
|
|
226
|
+
v = signature_hex[128, 2] # TRON typically uses only 1 byte for v (recovery ID)
|
|
227
|
+
|
|
228
|
+
[r, s, v]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Calculates the recovery ID from the v component
|
|
232
|
+
#
|
|
233
|
+
# @param v_byte [Integer] the v component of the signature as integer
|
|
234
|
+
# @return [Integer] recovery ID
|
|
235
|
+
def calculate_recovery_id(v_byte)
|
|
236
|
+
# TRON uses different recovery ID calculation than Ethereum
|
|
237
|
+
# In most TRON implementations, v is typically 27 or 28 for recovery ID 0 or 1
|
|
238
|
+
if v_byte >= 27
|
|
239
|
+
v_byte - 27
|
|
240
|
+
else
|
|
241
|
+
v_byte
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Converts a public key in hexadecimal format to a TRON address
|
|
246
|
+
#
|
|
247
|
+
# @param public_key_hex [String] public key as hexadecimal string
|
|
248
|
+
# @return [String] TRON address
|
|
249
|
+
def public_key_to_address(public_key_hex)
|
|
250
|
+
# Convert hex public key to bytes (remove 0x prefix if present)
|
|
251
|
+
public_key_hex = public_key_hex.start_with?('0x') ? public_key_hex[2..-1] : public_key_hex
|
|
252
|
+
public_key_bytes = Tron::Utils::Crypto.hex_to_bin(public_key_hex)
|
|
253
|
+
|
|
254
|
+
# Remove the 0x04 prefix if present
|
|
255
|
+
public_key_bytes = public_key_bytes[1..-1] if public_key_bytes[0] == "\x04".b
|
|
256
|
+
|
|
257
|
+
# Hash the public key with Keccak256
|
|
258
|
+
hash = Tron::Utils::Crypto.keccak256(public_key_bytes)
|
|
259
|
+
|
|
260
|
+
# Take the last 20 bytes
|
|
261
|
+
address_bytes = hash[-20..-1]
|
|
262
|
+
|
|
263
|
+
# Add TRON prefix (0x41)
|
|
264
|
+
prefixed_address_hex = Tron::Key::ADDRESS_PREFIX + Tron::Utils::Crypto.bin_to_hex(address_bytes)
|
|
265
|
+
prefixed_address_bytes = Tron::Utils::Crypto.hex_to_bin(prefixed_address_hex)
|
|
266
|
+
|
|
267
|
+
# Use base58check encoding
|
|
268
|
+
Tron::Utils::Crypto.base58check(prefixed_address_bytes)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|