tron.rb 1.0.9 → 1.1.3

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.
@@ -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
- attr_accessor :api_key, :tronscan_api_key, :timeout, :base_url, :tronscan_base_url, :strict_mode
5
- attr_accessor :cache_enabled, :cache_ttl, :cache_max_stale
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
@@ -40,10 +77,10 @@ module Tron
40
77
  @tronscan_base_url = 'https://apilist.tronscanapi.com'
41
78
  when :shasta
42
79
  @base_url = 'https://api.shasta.trongrid.io'
43
- @tronscan_base_url = 'https://api.shasta.tronscanapi.com'
80
+ @tronscan_base_url = 'https://shasta.tronscan.org'
44
81
  when :nile
45
82
  @base_url = 'https://nile.trongrid.io'
46
- @tronscan_base_url = 'https://api.nileex.net'
83
+ @tronscan_base_url = 'https://nileapi.tronscan.org'
47
84
  else
48
85
  @base_url = 'https://api.trongrid.io'
49
86
  @tronscan_base_url = 'https://apilist.tronscanapi.com'
@@ -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