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.
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tron
4
+ module Abi
5
+ # Provides a class to handle and parse common ABI types.
6
+ class Type
7
+ # Provides a specific parser error if type cannot be determined.
8
+ class ParseError < StandardError; end
9
+
10
+ # The base attribute, e.g., `string` or `bytes`.
11
+ attr :base_type
12
+
13
+ # The sub-type attribute, e.g., `256` as size of an uint256.
14
+ attr :sub_type
15
+
16
+ # The dimension attribute, e.g., `[10]` for an array of size 10.
17
+ attr :dimensions
18
+
19
+ # The components of a tuple type.
20
+ attr :components
21
+
22
+ # The name of tuple component.
23
+ attr :name
24
+
25
+ # Create a new Type object for base types, sub types, and dimensions.
26
+ # Should not be used; use {Type.parse} instead.
27
+ #
28
+ # @param base_type [String] the base-type attribute.
29
+ # @param sub_type [String] the sub-type attribute.
30
+ # @param dimensions [Array] the dimension attribute.
31
+ # @param components [Array] the components attribute.
32
+ # @param component_name [String] the tuple component's name.
33
+ # @return [Tron::Abi::Type] an ABI type object.
34
+ def initialize(base_type, sub_type, dimensions, components = nil, component_name = nil)
35
+ sub_type = sub_type.to_s
36
+ @base_type = base_type
37
+ @sub_type = sub_type
38
+ @dimensions = dimensions
39
+ @components = components
40
+ @name = component_name
41
+ end
42
+
43
+ # Converts the self.parse method into a constructor.
44
+ # Using a simple constructor since konstructor gem isn't available
45
+ def self.parse(type, components = nil, component_name = nil)
46
+ new_type = new(nil, nil, nil, nil, nil)
47
+ new_type.parse(type, components, component_name)
48
+ new_type
49
+ end
50
+
51
+ # Attempts to parse a string containing a common Solidity type.
52
+ # Creates a new Type upon success.
53
+ #
54
+ # @param type [String] a common Solidity type.
55
+ # @param components [Array] the components attribute.
56
+ # @param component_name [String] the tuple component's name.
57
+ # @return [Tron::Abi::Type] a parsed Type object.
58
+ # @raise [ParseError] if it fails to parse the type.
59
+ def parse(type, components = nil, component_name = nil)
60
+ if type.is_a?(Type)
61
+ @base_type = type.base_type
62
+ @sub_type = type.sub_type
63
+ @dimensions = type.dimensions
64
+ @components = type.components
65
+ @name = type.name
66
+ return
67
+ end
68
+
69
+ # ensure the type string is reasonable before attempting to parse
70
+ raise ParseError, "Invalid type format" unless type.is_a? String
71
+
72
+ if type.start_with?("tuple(") || type.start_with?("(")
73
+ tuple_str = type.start_with?("tuple(") ? type : "tuple#{type}"
74
+ inner, rest = extract_tuple(tuple_str)
75
+ inner_types = split_tuple_types(inner)
76
+ inner_types.each { |t| Type.parse(t) }
77
+ base_type = "tuple"
78
+ sub_type = ""
79
+ dimension = rest
80
+ components ||= inner_types.map { |t| { "type" => t } }
81
+ else
82
+ match = /\A([a-z]+)([0-9]*x?[0-9]*)((?:\[\d+\]|\[\])*)\z/.match(type)
83
+ raise ParseError, "Invalid type format" unless match
84
+ _, base_type, sub_type, dimension = match.to_a
85
+ sub_type = "256" if %w[uint int].include?(base_type) && sub_type.empty?
86
+ end
87
+
88
+ # type dimension can only be numeric or empty for dynamic arrays
89
+ dims = dimension.scan(/\[\d+\]|\[\]/)
90
+ raise ParseError, "Unknown characters found in array declaration" if dims.join != dimension
91
+
92
+ # enforce base types
93
+ validate_base_type base_type, sub_type
94
+
95
+ # return a new Type
96
+ sub_type = sub_type.to_s
97
+ @base_type = base_type
98
+ @sub_type = sub_type
99
+ @dimensions = dims.map { |x| x == "[]" ? 0 : x[1...-1].to_i }
100
+ @components = components.map { |component| Abi::Type.parse(component["type"], component.dig("components"), component.dig("name")) } if components&.any?
101
+ @name = component_name
102
+ end
103
+
104
+ # Creates a new uint256 type used for size.
105
+ #
106
+ # @return [Tron::Abi::Type] a uint256 size type.
107
+ def self.size_type
108
+ @size_type ||= new("uint", 256, [])
109
+ end
110
+
111
+ # Compares two types for their attributes.
112
+ #
113
+ # @param another_type [Tron::Abi::Type] another type to be compared.
114
+ # @return [Boolean] true if all attributes match.
115
+ def ==(another_type)
116
+ base_type == another_type.base_type and
117
+ sub_type == another_type.sub_type and
118
+ dimensions == another_type.dimensions
119
+ end
120
+
121
+ # Computes the size of a type if possible.
122
+ #
123
+ # @return [Integer] the size of the type; or nil if not available.
124
+ def size
125
+ s = nil
126
+ if dimensions.empty?
127
+ if !(["string", "bytes", "tuple"].include?(base_type) and sub_type.empty?)
128
+ s = 32
129
+ elsif base_type == "tuple" and components&.none?(&:dynamic?)
130
+ s = components.sum(&:size)
131
+ end
132
+ elsif dimensions.last != 0 and !nested_sub.dynamic?
133
+ s = dimensions.last * nested_sub.size
134
+ end
135
+ @size ||= s
136
+ end
137
+
138
+ # Helps to determine whether array is of dynamic size.
139
+ #
140
+ # @return [Boolean] true if array is of dynamic size.
141
+ def dynamic?
142
+ size.nil?
143
+ end
144
+
145
+ # Types can have nested sub-types in arrays.
146
+ #
147
+ # @return [Tron::Abi::Type] nested sub-type.
148
+ def nested_sub
149
+ @nested_sub ||= self.class.new(base_type, sub_type, dimensions[0...-1], components, name)
150
+ end
151
+
152
+ # Allows exporting the type as string.
153
+ #
154
+ # @return [String] the type string.
155
+ def to_s
156
+ if base_type == "tuple"
157
+ "(" + components.map(&:to_s).join(",") + ")" + (dimensions.size > 0 ? dimensions.map { |x| "[#{x == 0 ? "" : x}]" }.join : "")
158
+ elsif dimensions.empty?
159
+ if %w[string bytes].include?(base_type) and sub_type.empty?
160
+ base_type
161
+ else
162
+ "#{base_type}#{sub_type}"
163
+ end
164
+ else
165
+ "#{base_type}#{sub_type}#{dimensions.map { |x| "[#{x == 0 ? "" : x}]" }.join}"
166
+ end
167
+ end
168
+
169
+ private
170
+
171
+ # Validates all known base types and raises if an issue occurs.
172
+ def validate_base_type(base_type, sub_type)
173
+ case base_type
174
+ when "string"
175
+ # string can not have any suffix
176
+ raise ParseError, "String type must have no suffix or numerical suffix" unless sub_type.empty?
177
+ when "bytes"
178
+ # bytes can be no longer than 32 bytes
179
+ raise ParseError, "Maximum 32 bytes for fixed-length string or bytes" unless sub_type.empty? or (sub_type.to_i <= 32 and sub_type.to_i > 0)
180
+ when "tuple"
181
+ # tuples can not have any suffix
182
+ raise ParseError, "Tuple type must have no suffix or numerical suffix" unless sub_type.empty?
183
+ when "uint", "int"
184
+ # integers must have a numerical suffix
185
+ raise ParseError, "Integer type must have numerical suffix" unless sub_type =~ /\A[0-9]+\z/
186
+
187
+ # integer size must be valid
188
+ size = sub_type.to_i
189
+ raise ParseError, "Integer size out of bounds" unless size >= 8 and size <= 256
190
+ raise ParseError, "Integer size must be multiple of 8" unless size % 8 == 0
191
+ when "ureal", "real", "fixed", "ufixed"
192
+ # floats must have valid dimensional suffix
193
+ raise ParseError, "Real type must have suffix of form <size>x<decimals>, e.g. 128x128" unless sub_type =~ /\A[0-9]+x[0-9]+\z/
194
+ size, decimals = sub_type.split("x").map(&:to_i)
195
+ total = size + decimals
196
+ raise ParseError, "Real size out of bounds (max 32 bytes)" unless total >= 8 and total <= 256
197
+ raise ParseError, "Real size must be multiples of 8" unless size % 8 == 0
198
+ when "hash"
199
+ # hashs must have numerical suffix
200
+ raise ParseError, "Hash type must have numerical suffix" unless sub_type =~ /\A[0-9]+\z/
201
+ when "address"
202
+ # addresses cannot have any suffix
203
+ raise ParseError, "Address cannot have suffix" unless sub_type.empty?
204
+ when "bool"
205
+ # booleans cannot have any suffix
206
+ raise ParseError, "Bool cannot have suffix" unless sub_type.empty?
207
+ else
208
+ # we cannot parse arbitrary types such as 'decimal' or 'hex'
209
+ raise ParseError, "Unknown base type"
210
+ end
211
+ end
212
+
213
+ # Extracts the inner type list and trailing dimensions from an inline tuple definition.
214
+ def extract_tuple(type)
215
+ idx = 6 # skip "tuple("
216
+ depth = 1
217
+ while idx < type.length && depth > 0
218
+ case type[idx]
219
+ when "("
220
+ depth += 1
221
+ when ")"
222
+ depth -= 1
223
+ end
224
+ idx += 1
225
+ end
226
+ raise ParseError, "Invalid tuple format" unless depth.zero?
227
+ inner = type[6...(idx - 1)]
228
+ rest = type[idx..] || ""
229
+ [inner, rest]
230
+ end
231
+
232
+ # Splits a tuple component list into individual type strings, handling nested tuples.
233
+ def split_tuple_types(str)
234
+ types = []
235
+ depth = 0
236
+ current = ""
237
+ str.each_char do |ch|
238
+ case ch
239
+ when "("
240
+ depth += 1
241
+ current += ch
242
+ when ")"
243
+ depth -= 1
244
+ current += ch
245
+ when ","
246
+ if depth.zero?
247
+ types << current
248
+ current = ""
249
+ else
250
+ current += ch
251
+ end
252
+ else
253
+ current += ch
254
+ end
255
+ end
256
+ types << current unless current.empty?
257
+ types
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tron
4
+ module Abi
5
+ # Provides utility functions for ABI encoding/decoding
6
+ module Util
7
+ extend self
8
+
9
+ # Maximum uint value
10
+ UINT_MAX = (2 ** 256) - 1
11
+
12
+ # Minimum uint value
13
+ UINT_MIN = 0
14
+
15
+ # Maximum int value
16
+ INT_MAX = (2 ** 255) - 1
17
+
18
+ # Minimum int value
19
+ INT_MIN = -(2 ** 255)
20
+
21
+ # Pads a length to a multiple of 32 bytes
22
+ #
23
+ # @param x [Integer] the length to pad
24
+ # @return [Integer] the padded length
25
+ def ceil32(x)
26
+ ((x + 31) / 32).floor * 32
27
+ end
28
+
29
+ # Pads an integer to 32 bytes in binary format
30
+ #
31
+ # @param x [Integer] the integer to pad
32
+ # @return [String] the padded integer as a binary string
33
+ def zpad_int(x)
34
+ # Ensure x is positive for modulo operation
35
+ x = x % (2 ** 256) if x >= 2 ** 256 || x < 0
36
+ [x.to_s(16).rjust(64, '0')].pack('H*')
37
+ end
38
+
39
+ # Pads a string to a specified length with null bytes
40
+ #
41
+ # @param s [String] the string to pad
42
+ # @param length [Integer] the target length
43
+ # @return [String] the padded string
44
+ def zpad(s, length)
45
+ s + "\x00" * (length - s.length)
46
+ end
47
+
48
+ # Pads a hex string to 32 bytes (64 hex characters), returning binary
49
+ #
50
+ # @param s [String] the hex string to pad
51
+ # @return [String] the padded hex as a binary string
52
+ def zpad_hex(s)
53
+ s = s[2..-1] if s.start_with?('0x', '0X')
54
+ [s.rjust(64, '0')].pack('H*')
55
+ end
56
+
57
+ # Checks if a string is prefixed with 0x
58
+ #
59
+ # @param s [String] the string to check
60
+ # @return [Boolean] true if prefixed with 0x or 0X
61
+ def prefixed?(s)
62
+ s.start_with?('0x', '0X')
63
+ end
64
+
65
+ # Checks if a string is a valid hex string
66
+ #
67
+ # @param s [String] the string to check
68
+ # @return [Boolean] true if it's a valid hex string
69
+ def hex?(s)
70
+ s = s[2..-1] if s.start_with?('0x', '0X')
71
+ s.match(/\A[0-9a-fA-F]*\z/)
72
+ end
73
+
74
+ # Convert hex string to binary
75
+ #
76
+ # @param s [String] the hex string to convert
77
+ # @return [String] the binary representation
78
+ def hex_to_bin(s)
79
+ s = s[2..-1] if s.start_with?('0x', '0X')
80
+ [s].pack('H*')
81
+ end
82
+
83
+ # Convert binary to hex string
84
+ #
85
+ # @param b [String] the binary to convert
86
+ # @return [String] the hexadecimal representation
87
+ def bin_to_hex(b)
88
+ b.unpack('H*').first
89
+ end
90
+
91
+ # Deserialize big endian integer from binary data
92
+ #
93
+ # @param data [String] the binary data to deserialize
94
+ # @return [Integer] the deserialized integer
95
+ def deserialize_big_endian_to_int(data)
96
+ data.unpack1('H*').to_i(16)
97
+ end
98
+ end
99
+ end
100
+ end
data/lib/tron/abi.rb ADDED
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tron
4
+ module Abi
5
+ # Base error class for ABI-related errors
6
+ class Error < StandardError; end
7
+
8
+ # Error raised when there's an issue with encoding
9
+ class EncodingError < Error; end
10
+
11
+ # Error raised when there's an issue with decoding
12
+ class DecodingError < Error; end
13
+
14
+ # Error raised when a value is out of bounds for its type
15
+ class ValueOutOfBounds < Error; end
16
+
17
+ # Require all components of the ABI module
18
+ require_relative 'abi/type'
19
+ require_relative 'abi/encoder'
20
+ require_relative 'abi/decoder'
21
+ require_relative 'abi/function'
22
+ require_relative 'abi/event'
23
+ require_relative 'abi/util'
24
+ require_relative 'abi/constant'
25
+
26
+ # For address handling functionality
27
+ require_relative 'utils/address'
28
+ require_relative 'key'
29
+
30
+ # Convenience method for encoding
31
+ def self.encode(types, values)
32
+ # Parse the types
33
+ parsed_types = types.map { |t| Type.parse(t) }
34
+
35
+ # Split into static and dynamic parts
36
+ static_parts = []
37
+ dynamic_parts = []
38
+ dynamic_offsets = []
39
+ offset_index = 0
40
+
41
+ parsed_types.each_with_index do |type, i|
42
+ if type.dynamic?
43
+ # For dynamic types, store a placeholder offset and the actual data
44
+ static_parts << nil # Placeholder for offset
45
+ dynamic_parts << Encoder.type(type, values[i])
46
+ dynamic_offsets[offset_index] = dynamic_parts.length - 1
47
+ offset_index += 1
48
+ else
49
+ # For static types, encode directly
50
+ static_parts << Encoder.type(type, values[i])
51
+ end
52
+ end
53
+
54
+ # Calculate actual offsets for dynamic parts
55
+ # The offset is the position in the encoded result where the dynamic data begins
56
+ # This is after all static parts (each parameter takes 32 bytes)
57
+ static_size = parsed_types.count * 32 # Size of all parameter slots
58
+ dynamic_offset = static_size # Start of dynamic data
59
+
60
+ # Replace the nil placeholders with actual offsets
61
+ placeholders_replaced = 0
62
+ static_parts.map! do |part|
63
+ if part.nil?
64
+ offset_value = dynamic_offset
65
+ # Update offset for next dynamic part
66
+ dynamic_part_idx = dynamic_offsets[placeholders_replaced]
67
+ dynamic_offset += dynamic_parts[dynamic_part_idx].bytesize # Use bytesize for binary
68
+ placeholders_replaced += 1
69
+ Encoder.type(Type.parse('uint256'), offset_value)
70
+ else
71
+ part
72
+ end
73
+ end
74
+
75
+ # Combine static and dynamic parts and convert to hex at the boundary
76
+ result_binary = static_parts.join + dynamic_parts.join
77
+ Util.bin_to_hex(result_binary)
78
+ end
79
+
80
+ # Convenience method for decoding
81
+ def self.decode(types, hex_data)
82
+ # Convert hex to binary at the boundary
83
+ data = Util.hex_to_bin(hex_data)
84
+ parsed_types = types.map { |t| Type.parse(t) }
85
+
86
+ # Decode each parameter, tracking the static section position as we go
87
+ results = []
88
+ static_offset = 0 # Position in the static section for both offset pointers and static values
89
+
90
+ parsed_types.each do |type|
91
+ if type.dynamic?
92
+ # Check if we have enough data in static section for this offset
93
+ raise DecodingError, "Insufficient data for dynamic type offset" if data.bytesize < static_offset + 32
94
+ # Get offset from static section for this dynamic parameter
95
+ offset_value = Util.deserialize_big_endian_to_int(data[static_offset, 32])
96
+
97
+ # Check if we have enough data at the dynamic offset location
98
+ raise DecodingError, "Insufficient data for dynamic type at offset #{offset_value}" if data.bytesize < offset_value + 32
99
+
100
+ # Determine the size of data for this dynamic parameter
101
+ # First, read the length from the dynamic data location
102
+ data_length = Util.deserialize_big_endian_to_int(data[offset_value, 32])
103
+
104
+ # Calculate total data size based on type
105
+ if %w(string bytes).include?(type.base_type) and type.sub_type.empty? and type.dimensions.empty?
106
+ # String or bytes: 32-byte length + padded content
107
+ total_size = 32 + Util.ceil32(data_length)
108
+ elsif !type.dimensions.empty? # Array type
109
+ # Dynamic array: 32-byte length + element encodings
110
+ nested_type = type.nested_sub
111
+ if nested_type.dynamic?
112
+ # Complex case for arrays with dynamic elements - use full remaining data
113
+ param_data = data[offset_value..-1]
114
+ decoded_value = Decoder.type(type, param_data)
115
+ results << decoded_value
116
+ static_offset += 32 # Advance past the offset pointer
117
+ next
118
+ else
119
+ # Array with static elements: 32-byte length + (element_size * count)
120
+ total_size = 32 + (nested_type.size || 32) * data_length
121
+ end
122
+ else
123
+ # Default for other dynamic types: 32-byte length + padded content
124
+ total_size = 32 + Util.ceil32(data_length)
125
+ end
126
+
127
+ # Verify we have enough data
128
+ raise DecodingError, "Insufficient data for dynamic type content" if data.bytesize < offset_value + total_size
129
+
130
+ # Extract the parameter's data and decode
131
+ param_data = data[offset_value, total_size]
132
+ decoded_value = Decoder.type(type, param_data)
133
+ results << decoded_value
134
+ static_offset += 32 # Advance past the offset pointer
135
+ else
136
+ # For static types, decode directly from current static position
137
+ size = type.size # Size in bytes
138
+ if size
139
+ # Check bounds before reading static data
140
+ raise DecodingError, "Insufficient data for static type" if data.bytesize < static_offset + size
141
+ decoded_value = Decoder.type(type, data[static_offset, size])
142
+ results << decoded_value
143
+ static_offset += size # Advance to next position
144
+ else
145
+ raise DecodingError, "Cannot decode static type without size"
146
+ end
147
+ end
148
+ end
149
+
150
+ results
151
+ end
152
+ end
153
+ end
data/lib/tron/client.rb CHANGED
@@ -3,11 +3,26 @@ require_relative 'configuration'
3
3
  require_relative 'services/balance'
4
4
  require_relative 'services/resources'
5
5
  require_relative 'services/price'
6
+ require_relative 'services/contract'
6
7
 
7
8
  module Tron
9
+ # The main client class for interacting with the TRON blockchain
10
+ # Provides methods for checking balances, resources, prices, and contract interactions
8
11
  class Client
9
12
  attr_reader :configuration
10
13
 
14
+ # Creates a new client instance with the given options
15
+ #
16
+ # @param options [Hash] configuration options
17
+ # @option options [String] :api_key TronGrid API key
18
+ # @option options [String] :tronscan_api_key Tronscan API key
19
+ # @option options [Symbol] :network network to use (:mainnet, :shasta, :nile)
20
+ # @option options [Integer] :timeout timeout for API requests
21
+ # @option options [Boolean] :cache_enabled whether caching is enabled
22
+ # @option options [Integer] :cache_ttl cache TTL in seconds
23
+ # @option options [Integer] :cache_max_stale max stale time in seconds
24
+ # @option options [String] :default_address default address for read-only calls
25
+ # @option options [Integer] :fee_limit default fee limit for transactions
11
26
  def initialize(options = {})
12
27
  @configuration = Configuration.new
13
28
 
@@ -23,27 +38,54 @@ module Tron
23
38
  @configuration.tronscan_api_key ||= ENV['TRONSCAN_API_KEY']
24
39
  end
25
40
 
41
+ # Configures the default client
42
+ #
43
+ # @yield [config] block to configure the client
44
+ # @yieldparam [Tron::Configuration] config the configuration object
26
45
  def self.configure
27
46
  yield configuration if block_given?
28
47
  end
29
48
 
49
+ # Returns the default configuration
50
+ #
51
+ # @return [Tron::Configuration] the configuration object
30
52
  def self.configuration
31
53
  @configuration ||= Configuration.new
32
54
  end
33
55
 
56
+ # Returns the balance service instance
57
+ #
58
+ # @return [Tron::Services::Balance] the balance service
34
59
  def balance_service
35
60
  @balance_service ||= Services::Balance.new(@configuration)
36
61
  end
37
62
 
63
+ # Returns the resources service instance
64
+ #
65
+ # @return [Tron::Services::Resources] the resources service
38
66
  def resources_service
39
67
  @resources_service ||= Services::Resources.new(@configuration)
40
68
  end
41
69
 
70
+ # Returns the price service instance
71
+ #
72
+ # @return [Tron::Services::Price] the price service
42
73
  def price_service
43
74
  @price_service ||= Services::Price.new(@configuration)
44
75
  end
45
76
 
46
- # Convenience methods that combine multiple services
77
+ # Returns the contract service instance
78
+ #
79
+ # @return [Tron::Services::Contract] the contract service
80
+ def contract_service
81
+ @contract_service ||= Services::Contract.new(@configuration)
82
+ end
83
+
84
+ # Get wallet balance information including TRX and TRC20 tokens
85
+ #
86
+ # @param address [String] TRON address to check
87
+ # @param strict [Boolean] whether to enable strict validation
88
+ # @return [Hash] balance information hash
47
89
  def get_wallet_balance(address, strict: false)
48
90
  validate_address!(address)
49
91
 
@@ -54,6 +96,11 @@ module Tron
54
96
  }
55
97
  end
56
98
 
99
+ # Get complete account information including balances and resources
100
+ #
101
+ # @param address [String] TRON address to check
102
+ # @param strict [Boolean] whether to enable strict validation
103
+ # @return [Hash] full account information hash
57
104
  def get_full_account_info(address, strict: false)
58
105
  validate_address!(address)
59
106
 
@@ -65,6 +112,11 @@ module Tron
65
112
  }
66
113
  end
67
114
 
115
+ # Get wallet portfolio including balances converted to USD values
116
+ #
117
+ # @param address [String] TRON address to check
118
+ # @param include_zero_balances [Boolean] whether to include tokens with zero balance
119
+ # @return [Hash] portfolio information with USD values
68
120
  def get_wallet_portfolio(address, include_zero_balances: false)
69
121
  validate_address!(address)
70
122
 
@@ -120,10 +172,16 @@ module Tron
120
172
  }
121
173
  end
122
174
 
175
+ # Check if caching is enabled
176
+ #
177
+ # @return [Boolean] true if caching is enabled
123
178
  def cache_enabled?
124
179
  configuration.cache_enabled
125
180
  end
126
181
 
182
+ # Get cache statistics
183
+ #
184
+ # @return [Hash] cache statistics for different services
127
185
  def cache_stats
128
186
  {
129
187
  price: price_service.cache_stats,
@@ -131,6 +189,7 @@ module Tron
131
189
  }
132
190
  end
133
191
 
192
+ # Clear all caches
134
193
  def clear_cache
135
194
  price_service.clear_cache
136
195
  balance_service.clear_cache
@@ -138,6 +197,10 @@ module Tron
138
197
 
139
198
  private
140
199
 
200
+ # Validates a TRON address
201
+ #
202
+ # @param address [String] TRON address to validate
203
+ # @raise [ArgumentError] if the address is invalid
141
204
  def validate_address!(address)
142
205
  require_relative 'utils/address'
143
206
  raise ArgumentError, "Invalid TRON address: #{address}" unless Utils::Address.validate(address)