tron.rb 1.0.4 → 1.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be59d0725833375ac579bd873677eb22814a8befb727df0e1ab001834f7d24ef
4
- data.tar.gz: 8d37550f4418c2a5b7592b86ddd74b0a1eda37b8014d9b93e5edfc337f051cae
3
+ metadata.gz: 3bd7e8033f046e59d3a917a16574a0bdf10fa37d8c36e9964b6cf67c730cf94d
4
+ data.tar.gz: 7b3684dc20f0e671895e8702e18552da9325d11447583299a5311a712e274b14
5
5
  SHA512:
6
- metadata.gz: 71f329e6b189ace240c47daf9b21ee2e47d96a88e1c8dee1123fd112128109999fabc17b335c12d8ddf43c41b5a062b6e5acfbb01e299e22cb7a5d38cfea9d38
7
- data.tar.gz: 42e4dbab34144d90091569eb66881d24ab42605bc3f69822b2d43e3bdf79a4551db191ba33577d353eac8108ef3fa2f9ad380f72e23d71692e58124b9ce7f8db
6
+ metadata.gz: c5d04047d7736973110a1530ab187b6e180fe2c8fac4b6feb1bbf58f098313de2e0f328bdb6475a98f733d93d07b1806c22180def5634cfc3dd63f1ca2f1c91b
7
+ data.tar.gz: 949e517a1c47970fa8c6a43367602c4eb0404e84ee4cc64923e6ba89f8e0f04dc41113fd584c38cf36d56541a8abaa0cf1f5f8e85d8ee878d6cfd30b36486a55
data/README.md CHANGED
@@ -33,6 +33,11 @@ Tron.configure do |config|
33
33
  config.tronscan_api_key = 'your_tronscan_api_key'
34
34
  config.network = :mainnet # or :shasta or :nile
35
35
  config.timeout = 30
36
+
37
+ # Cache configuration (optional)
38
+ config.cache_enabled = true # Enable/disable caching (default: true)
39
+ config.cache_ttl = 300 # Cache TTL in seconds (default: 300 = 5 minutes)
40
+ config.cache_max_stale = 600 # Max stale time in seconds (default: 600 = 10 minutes)
36
41
  end
37
42
  ```
38
43
 
@@ -112,11 +117,137 @@ end
112
117
  - ✓ Token prices
113
118
  - ✓ Portfolio tracking
114
119
  - ✓ Multi-network support (mainnet, shasta, nile)
120
+ - ✓ Intelligent caching with TTL and stale-while-revalidate
121
+ - ✓ Cache statistics and monitoring
115
122
  - ✓ Clean, simple output
116
123
  - ✓ Proper decimal formatting
117
124
  - ✓ Environment variable support
118
125
  - ✓ Rate limit management with API keys
119
126
 
127
+ ## Caching
128
+
129
+ The gem includes an intelligent caching system to reduce API calls and improve performance.
130
+
131
+ ### How Caching Works
132
+
133
+ The cache uses a **time-based expiration strategy** with **stale-while-revalidate** behavior:
134
+
135
+ 1. **Fresh Cache (age < TTL)**: Returns cached value immediately
136
+ 2. **Stale Cache (age > TTL but < max_stale)**: Attempts to refresh, falls back to stale value on error
137
+ 3. **Expired Cache (age > max_stale)**: Forces refresh, raises error if refresh fails
138
+
139
+ This approach ensures:
140
+ - Fast responses from fresh cache
141
+ - High availability with stale fallbacks
142
+ - Automatic background refresh for popular endpoints
143
+
144
+ ### Cache Configuration
145
+
146
+ ```ruby
147
+ Tron.configure do |config|
148
+ # Enable or disable caching globally (default: true)
149
+ config.cache_enabled = true
150
+
151
+ # Set default TTL (time-to-live) in seconds (default: 300 = 5 minutes)
152
+ config.cache_ttl = 300
153
+
154
+ # Set max stale time in seconds (default: 600 = 10 minutes)
155
+ config.cache_max_stale = 600
156
+ end
157
+ ```
158
+
159
+ ### Endpoint-Specific TTL Values
160
+
161
+ Different endpoints have optimized TTL values based on data volatility:
162
+
163
+ | Endpoint Type | TTL | Max Stale | Use Case |
164
+ |---------------|-----|-----------|----------|
165
+ | **balance** | 5 min | 10 min | Wallet balances, moderate volatility |
166
+ | **price** | 1 min | 2 min | Token prices, high volatility |
167
+ | **token_info** | 15 min | 30 min | Token metadata, low volatility |
168
+ | **resources** | 5 min | 10 min | Account resources, moderate volatility |
169
+ | **default** | 5 min | 10 min | Unclassified endpoints |
170
+
171
+ ### Cache Statistics
172
+
173
+ Monitor cache performance to optimize your configuration:
174
+
175
+ ```ruby
176
+ # Get global cache statistics
177
+ stats = Tron::Cache.global_stats
178
+ puts "Cache Hit Rate: #{stats[:hit_rate_percentage]}%"
179
+ puts "Total Hits: #{stats[:total_hits]}"
180
+ puts "Total Misses: #{stats[:total_misses]}"
181
+ puts "Cache Size: #{stats[:cache_size]} entries"
182
+
183
+ # Get statistics for a specific cache entry
184
+ key_stats = Tron::Cache.stats("specific_key")
185
+ if key_stats
186
+ puts "Entry Hits: #{key_stats[:hits]}"
187
+ puts "Entry Misses: #{key_stats[:misses]}"
188
+ puts "Cached At: #{key_stats[:cached_at]}"
189
+ puts "Expires At: #{key_stats[:expires_at]}"
190
+ puts "Expired: #{key_stats[:expired]}"
191
+ end
192
+ ```
193
+
194
+ ### Cache Management
195
+
196
+ ```ruby
197
+ # Clear entire cache
198
+ Tron::Cache.clear
199
+
200
+ # Delete specific cache entry
201
+ Tron::Cache.delete("cache_key")
202
+
203
+ # Check if key exists in cache
204
+ if Tron::Cache.exists?("cache_key")
205
+ puts "Key is cached"
206
+ end
207
+
208
+ # Get cache size
209
+ puts "Cache has #{Tron::Cache.size} entries"
210
+
211
+ # Reset statistics (useful for testing)
212
+ Tron::Cache.reset_stats
213
+ ```
214
+
215
+ ### Advanced Cache Usage
216
+
217
+ #### Disable Caching for Specific Requests
218
+
219
+ ```ruby
220
+ # Disable cache for a specific HTTP request
221
+ response = Tron::Utils::HTTP.get(url, headers, { enabled: false })
222
+ ```
223
+
224
+ #### Custom TTL for Specific Requests
225
+
226
+ ```ruby
227
+ # Use custom TTL values for a specific request
228
+ response = Tron::Utils::HTTP.get(url, headers, {
229
+ ttl: 60, # 1 minute
230
+ max_stale: 120 # 2 minutes
231
+ })
232
+ ```
233
+
234
+ #### Use Endpoint-Specific TTL
235
+
236
+ ```ruby
237
+ # Use predefined TTL for specific endpoint type
238
+ response = Tron::Utils::HTTP.get(url, headers, {
239
+ endpoint_type: :price # Uses 1 min TTL, 2 min max_stale
240
+ })
241
+ ```
242
+
243
+ ### Cache Best Practices
244
+
245
+ 1. **Enable caching in production** - Reduces API calls and improves response times
246
+ 2. **Monitor hit rates** - Aim for >70% hit rate for frequently accessed data
247
+ 3. **Adjust TTL based on data volatility** - Shorter TTL for prices, longer for token metadata
248
+ 4. **Use stale fallbacks** - Ensures high availability during API issues
249
+ 5. **Clear cache after mutations** - If you modify data, clear related cache entries
250
+
120
251
  ## Services Architecture
121
252
 
122
253
  The gem is organized into modular services:
@@ -124,8 +255,9 @@ The gem is organized into modular services:
124
255
  - `Tron::Services::Balance` - TRX and TRC20 token balances
125
256
  - `Tron::Services::Resources` - Account resources (bandwidth/energy)
126
257
  - `Tron::Services::Price` - Token price information
127
- - `Tron::Utils::HTTP` - HTTP client with error handling
258
+ - `Tron::Utils::HTTP` - HTTP client with error handling and caching
128
259
  - `Tron::Utils::Address` - TRON address validation and conversion
260
+ - `Tron::Cache` - Thread-safe caching with TTL and statistics
129
261
 
130
262
  ## API Reference
131
263
 
data/lib/tron/cache.rb ADDED
@@ -0,0 +1,186 @@
1
+ require 'thread'
2
+
3
+ module Tron
4
+ # Thread-safe, global cache implementation for TRON wallet toolkit
5
+ #
6
+ # This cache provides a simple key-value store with time-based expiration and
7
+ # failure fallback capabilities. It's designed specifically for caching API
8
+ # responses to improve performance and reduce rate limit issues.
9
+ #
10
+ # Features:
11
+ # - Thread-safe operations using Mutex synchronization
12
+ # - Time-based expiration with TTL (time-to-live) and max_stale settings
13
+ # - Fallback behavior when cache refresh fails
14
+ # - Global storage shared across all instances
15
+ # - Cache statistics tracking (hits, misses)
16
+ #
17
+ # Example usage:
18
+ # Tron::Cache.fetch("wallet_balance_#{address}", ttl: 300, max_stale: 600) do
19
+ # api_client.get_wallet_balance(address)
20
+ # end
21
+ class Cache
22
+ # Class-level storage for global cache
23
+ @@store = {}
24
+ @@mutex = Mutex.new
25
+ # Global statistics
26
+ @@global_stats = { total_hits: 0, total_misses: 0, total_fetches: 0 }
27
+
28
+ # Fetches a value from cache or executes the block to generate and cache it
29
+ #
30
+ # The fetch method implements a time-based caching strategy with fallback:
31
+ # 1. If entry exists and is fresh (age < ttl) → return cached value
32
+ # 2. If entry exists but stale (age > ttl but < max_stale) → try to update:
33
+ # - If block succeeds: cache new value and return it
34
+ # - If block raises error: return stale value (fallback)
35
+ # 3. If no cache or too old → execute block, cache result, return it
36
+ #
37
+ # @param key [String] the cache key (should be unique for the data being cached)
38
+ # @param ttl [Integer] time-to-live in seconds (how long to consider value fresh)
39
+ # @param max_stale [Integer] maximum stale time in seconds (how long to keep stale values)
40
+ # @yield [Proc] block to execute if value is not cached or expired
41
+ # @return [Object] the cached value or result from the block
42
+ # @raise [ArgumentError] if no block is provided
43
+ def self.fetch(key, ttl:, max_stale:, &block)
44
+ raise ArgumentError, "Block required for cache fetch" unless block_given?
45
+
46
+ @@mutex.synchronize do
47
+ entry = @@store[key]
48
+ @@global_stats[:total_fetches] += 1
49
+
50
+ if entry && Time.now - entry[:cached_at] < ttl
51
+ # Cache is fresh - return cached value
52
+ # Increment hit counter if present
53
+ @@store[key][:hits] = (entry[:hits] || 0) + 1
54
+ @@global_stats[:total_hits] += 1
55
+ return entry[:value]
56
+ elsif entry && Time.now - entry[:cached_at] < max_stale
57
+ # Cache is stale but within max_stale - try to update with fallback
58
+ begin
59
+ new_value = yield
60
+ @@store[key] = {
61
+ value: new_value,
62
+ cached_at: Time.now,
63
+ ttl: ttl,
64
+ max_stale: max_stale,
65
+ hits: 0,
66
+ misses: 0
67
+ }
68
+ @@global_stats[:total_misses] += 1
69
+ return new_value
70
+ rescue => e
71
+ # Return stale value as fallback
72
+ # Increment miss counter since the block failed
73
+ @@store[key][:misses] = (entry[:misses] || 0) + 1
74
+ @@global_stats[:total_hits] += 1 # Stale hit is still a hit
75
+ return entry[:value]
76
+ end
77
+ else
78
+ # No cache or too old - execute block and cache result
79
+ new_value = yield
80
+ @@store[key] = {
81
+ value: new_value,
82
+ cached_at: Time.now,
83
+ ttl: ttl,
84
+ max_stale: max_stale,
85
+ hits: 0,
86
+ misses: 0
87
+ }
88
+ @@global_stats[:total_misses] += 1
89
+ return new_value
90
+ end
91
+ end
92
+ end
93
+
94
+ # Checks if a key exists in the cache (without considering expiration)
95
+ #
96
+ # @param key [String] the cache key
97
+ # @return [Boolean] true if key exists in the cache, false otherwise
98
+ def self.exists?(key)
99
+ @@mutex.synchronize do
100
+ @@store.key?(key)
101
+ end
102
+ end
103
+
104
+ # Deletes a specific key from the cache
105
+ #
106
+ # @param key [String] the cache key to delete
107
+ # @return [void]
108
+ def self.delete(key)
109
+ @@mutex.synchronize do
110
+ @@store.delete(key)
111
+ end
112
+ end
113
+
114
+ # Clears all cached entries
115
+ #
116
+ # This is useful for testing or when a full cache refresh is needed.
117
+ # @return [void]
118
+ def self.clear
119
+ @@mutex.synchronize do
120
+ @@store.clear
121
+ end
122
+ end
123
+
124
+ # Returns the number of cached entries
125
+ # @return [Integer] number of cached entries
126
+ def self.size
127
+ @@mutex.synchronize do
128
+ @@store.size
129
+ end
130
+ end
131
+
132
+ # Returns cache statistics for a specific key
133
+ #
134
+ # @param key [String] the cache key
135
+ # @return [Hash, nil] statistics hash if key exists, nil otherwise
136
+ def self.stats(key)
137
+ @@mutex.synchronize do
138
+ entry = @@store[key]
139
+ return nil unless entry
140
+
141
+ {
142
+ hits: entry[:hits] || 0,
143
+ misses: entry[:misses] || 0,
144
+ cached_at: entry[:cached_at],
145
+ expires_at: entry[:cached_at] + entry[:ttl],
146
+ ttl: entry[:ttl],
147
+ max_stale: entry[:max_stale],
148
+ expired: Time.now - entry[:cached_at] >= entry[:ttl]
149
+ }
150
+ end
151
+ end
152
+
153
+ # Returns global cache statistics across all keys
154
+ #
155
+ # This provides an overall view of cache performance including hit rate.
156
+ # @return [Hash] global statistics including total_hits, total_misses, hit_rate
157
+ def self.global_stats
158
+ @@mutex.synchronize do
159
+ total_requests = @@global_stats[:total_fetches]
160
+ hit_rate = if total_requests > 0
161
+ (@@global_stats[:total_hits].to_f / total_requests * 100).round(2)
162
+ else
163
+ 0.0
164
+ end
165
+
166
+ {
167
+ total_hits: @@global_stats[:total_hits],
168
+ total_misses: @@global_stats[:total_misses],
169
+ total_fetches: @@global_stats[:total_fetches],
170
+ hit_rate_percentage: hit_rate,
171
+ cache_size: @@store.size,
172
+ memory_keys: @@store.keys.size
173
+ }
174
+ end
175
+ end
176
+
177
+ # Reset global statistics
178
+ # Useful for testing or when you want to start fresh statistics
179
+ # @return [void]
180
+ def self.reset_stats
181
+ @@mutex.synchronize do
182
+ @@global_stats = { total_hits: 0, total_misses: 0, total_fetches: 0 }
183
+ end
184
+ end
185
+ end
186
+ end
@@ -2,11 +2,16 @@
2
2
  module Tron
3
3
  class Configuration
4
4
  attr_accessor :api_key, :tronscan_api_key, :network, :timeout, :base_url, :tronscan_base_url, :strict_mode
5
+ attr_accessor :cache_enabled, :cache_ttl, :cache_max_stale
5
6
 
6
7
  def initialize
7
8
  @network = :mainnet
8
9
  @timeout = 30
9
10
  @strict_mode = false
11
+ # Cache configuration defaults
12
+ @cache_enabled = true
13
+ @cache_ttl = 300 # 5 minutes default TTL
14
+ @cache_max_stale = 600 # 10 minutes max stale
10
15
  setup_urls
11
16
  end
12
17
 
@@ -2,20 +2,116 @@
2
2
  require 'net/http'
3
3
  require 'uri'
4
4
  require 'json'
5
+ require 'digest'
5
6
 
6
7
  module Tron
7
8
  module Utils
8
9
  class HTTP
9
- def self.get(url, headers = {})
10
- make_request(Net::HTTP::Get, url, nil, headers)
10
+ # TTL values for different endpoint types (in seconds)
11
+ # These are optimized based on data volatility
12
+ ENDPOINT_TTL = {
13
+ # Balance endpoints - moderate volatility (5 minutes)
14
+ balance: { ttl: 300, max_stale: 600 },
15
+
16
+ # Token info endpoints - low volatility (15 minutes)
17
+ token_info: { ttl: 900, max_stale: 1800 },
18
+
19
+ # Price endpoints - high volatility (1 minute)
20
+ price: { ttl: 60, max_stale: 120 },
21
+
22
+ # Account resources - moderate volatility (5 minutes)
23
+ resources: { ttl: 300, max_stale: 600 },
24
+
25
+ # Default for unclassified endpoints
26
+ default: { ttl: 300, max_stale: 600 }
27
+ }.freeze
28
+
29
+ # GET request with optional caching
30
+ # @param url [String] the URL to request
31
+ # @param headers [Hash] request headers
32
+ # @param cache_options [Hash] optional cache configuration
33
+ # @option cache_options [Boolean] :enabled override global cache setting
34
+ # @option cache_options [Integer] :ttl custom TTL in seconds
35
+ # @option cache_options [Integer] :max_stale custom max_stale in seconds
36
+ # @option cache_options [Symbol] :endpoint_type endpoint type for TTL lookup
37
+ def self.get(url, headers = {}, cache_options = {})
38
+ if should_use_cache?(cache_options)
39
+ cache_key = generate_cache_key('GET', url, headers)
40
+ ttl_config = get_ttl_config(cache_options)
41
+
42
+ Tron::Cache.fetch(cache_key, ttl: ttl_config[:ttl], max_stale: ttl_config[:max_stale]) do
43
+ make_request(Net::HTTP::Get, url, nil, headers)
44
+ end
45
+ else
46
+ make_request(Net::HTTP::Get, url, nil, headers)
47
+ end
11
48
  end
12
49
 
50
+ # POST request (no caching for mutations)
13
51
  def self.post(url, body = nil, headers = {})
14
52
  make_request(Net::HTTP::Post, url, body, headers)
15
53
  end
16
54
 
55
+ # Clear cache for a specific endpoint
56
+ # @param url [String] the URL to clear from cache
57
+ # @param headers [Hash] request headers used in the original request
58
+ def self.clear_cache(url, headers = {})
59
+ cache_key = generate_cache_key('GET', url, headers)
60
+ Tron::Cache.delete(cache_key)
61
+ end
62
+
63
+ # Get cache statistics for an endpoint
64
+ # @param url [String] the URL to get stats for
65
+ # @param headers [Hash] request headers used in the original request
66
+ def self.cache_stats(url, headers = {})
67
+ cache_key = generate_cache_key('GET', url, headers)
68
+ Tron::Cache.stats(cache_key)
69
+ end
70
+
17
71
  private
18
72
 
73
+ # Determine if caching should be used for this request
74
+ def self.should_use_cache?(cache_options)
75
+ # Check if caching is explicitly disabled for this request
76
+ return false if cache_options[:enabled] == false
77
+
78
+ # Otherwise, use global configuration
79
+ Tron.configuration.cache_enabled
80
+ end
81
+
82
+ # Get TTL configuration for the request
83
+ def self.get_ttl_config(cache_options)
84
+ # If custom TTL provided, use it
85
+ if cache_options[:ttl] && cache_options[:max_stale]
86
+ return { ttl: cache_options[:ttl], max_stale: cache_options[:max_stale] }
87
+ end
88
+
89
+ # If endpoint type specified, use its TTL
90
+ if cache_options[:endpoint_type] && ENDPOINT_TTL[cache_options[:endpoint_type]]
91
+ return ENDPOINT_TTL[cache_options[:endpoint_type]]
92
+ end
93
+
94
+ # Use global configuration if set
95
+ if Tron.configuration.cache_ttl && Tron.configuration.cache_max_stale
96
+ return {
97
+ ttl: Tron.configuration.cache_ttl,
98
+ max_stale: Tron.configuration.cache_max_stale
99
+ }
100
+ end
101
+
102
+ # Fall back to default
103
+ ENDPOINT_TTL[:default]
104
+ end
105
+
106
+ # Generate a unique cache key based on request parameters
107
+ def self.generate_cache_key(method, url, headers)
108
+ # Include method, URL, and relevant headers in the cache key
109
+ # Exclude headers that don't affect the response (like User-Agent)
110
+ relevant_headers = headers.reject { |k, _| k.to_s.downcase == 'user-agent' }
111
+ key_parts = [method, url, relevant_headers.sort.to_h]
112
+ Digest::SHA256.hexdigest(key_parts.to_json)
113
+ end
114
+
19
115
  def self.make_request(method_class, url, body, headers)
20
116
  uri = URI(url)
21
117
  http = Net::HTTP.new(uri.host, uri.port)
@@ -26,7 +122,7 @@ module Tron
26
122
  request.body = body if body
27
123
 
28
124
  response = http.request(request)
29
-
125
+
30
126
  case response
31
127
  when Net::HTTPSuccess
32
128
  json_response = JSON.parse(response.body)
data/lib/tron/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  # lib/tron/version.rb
4
4
  module Tron
5
- VERSION = "1.0.4".freeze
5
+ VERSION = "1.0.5".freeze
6
6
  end
data/lib/tron.rb CHANGED
@@ -4,6 +4,7 @@ require 'dotenv/load' if File.exist?('.env')
4
4
  require_relative 'tron/version'
5
5
  require_relative 'tron/client'
6
6
  require_relative 'tron/configuration'
7
+ require_relative 'tron/cache'
7
8
 
8
9
  module Tron
9
10
  class << self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tron.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Your Name
@@ -93,6 +93,7 @@ files:
93
93
  - README.md
94
94
  - bin/tron-wallet
95
95
  - lib/tron.rb
96
+ - lib/tron/cache.rb
96
97
  - lib/tron/client.rb
97
98
  - lib/tron/configuration.rb
98
99
  - lib/tron/services/balance.rb