tron.rb 1.0.5 → 1.0.6

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: 3bd7e8033f046e59d3a917a16574a0bdf10fa37d8c36e9964b6cf67c730cf94d
4
- data.tar.gz: 7b3684dc20f0e671895e8702e18552da9325d11447583299a5311a712e274b14
3
+ metadata.gz: 3e552bde0f9721d76aee5ee554e0f565dcda21275d977e675085c110e4b7cba9
4
+ data.tar.gz: 74b56a49ceca14c4e15efd5fd278e6b6bc0cbb579a68df9c48f219a7531b4979
5
5
  SHA512:
6
- metadata.gz: c5d04047d7736973110a1530ab187b6e180fe2c8fac4b6feb1bbf58f098313de2e0f328bdb6475a98f733d93d07b1806c22180def5634cfc3dd63f1ca2f1c91b
7
- data.tar.gz: 949e517a1c47970fa8c6a43367602c4eb0404e84ee4cc64923e6ba89f8e0f04dc41113fd584c38cf36d56541a8abaa0cf1f5f8e85d8ee878d6cfd30b36486a55
6
+ metadata.gz: abd8889661b18e91a9283b4f8f533794c2bdeb06c1f470c29702a2e174d0d8eae4e154d79b37d1864710e14214555cfe52655ff83199a52baa94528c2b73b8d2
7
+ data.tar.gz: 369051da2a2179f2aafc7e61f74b3c5b259b4301741ba794afa3bc3ff5ea83986687214bf70105a2c0d413613a467f954116814d818c11ca30e2154f7cc6b518
data/README.md CHANGED
@@ -41,6 +41,53 @@ Tron.configure do |config|
41
41
  end
42
42
  ```
43
43
 
44
+ ### Cache Configuration
45
+
46
+ The gem includes intelligent caching to prevent rate limit errors and improve performance.
47
+
48
+ #### Default Configuration (Recommended)
49
+
50
+ ```ruby
51
+ client = Tron::Client.new(
52
+ api_key: ENV['TRONGRID_API_KEY'],
53
+ tronscan_api_key: ENV['TRONSCAN_API_KEY']
54
+ )
55
+ # Cache enabled by default with 5-minute TTL
56
+ ```
57
+
58
+ #### Custom Cache Configuration
59
+
60
+ ```ruby
61
+ client = Tron::Client.new(
62
+ api_key: ENV['TRONGRID_API_KEY'],
63
+ tronscan_api_key: ENV['TRONSCAN_API_KEY'],
64
+ cache: {
65
+ enabled: true,
66
+ ttl: 60, # Cache for 1 minute
67
+ max_stale: 600 # Serve stale data up to 10 minutes if API fails
68
+ }
69
+ )
70
+ ```
71
+
72
+ #### Disable Cache
73
+
74
+ ```ruby
75
+ client = Tron::Client.new(cache: { enabled: false })
76
+ ```
77
+
78
+ #### Monitor Cache Performance
79
+
80
+ ```ruby
81
+ stats = client.cache_stats
82
+ puts "Price cache hit rate: #{stats[:price][:hit_rate]}%"
83
+ puts "Balance cache hit rate: #{stats[:balance][:hit_rate]}%"
84
+
85
+ # Clear cache manually
86
+ client.clear_cache
87
+ ```
88
+
89
+ **Performance:** Caching provides 2,000x+ faster response times for repeated requests and eliminates 429 rate limit errors.
90
+
44
91
  ## Usage
45
92
 
46
93
  ### Command Line
data/lib/tron/cache.rb CHANGED
@@ -67,7 +67,7 @@ module Tron
67
67
  }
68
68
  @@global_stats[:total_misses] += 1
69
69
  return new_value
70
- rescue => e
70
+ rescue
71
71
  # Return stale value as fallback
72
72
  # Increment miss counter since the block failed
73
73
  @@store[key][:misses] = (entry[:misses] || 0) + 1
data/lib/tron/client.rb CHANGED
@@ -120,6 +120,22 @@ module Tron
120
120
  }
121
121
  end
122
122
 
123
+ def cache_enabled?
124
+ configuration.cache_enabled
125
+ end
126
+
127
+ def cache_stats
128
+ {
129
+ price: price_service.cache_stats,
130
+ balance: balance_service.cache_stats
131
+ }
132
+ end
133
+
134
+ def clear_cache
135
+ price_service.clear_cache
136
+ balance_service.clear_cache
137
+ end
138
+
123
139
  private
124
140
 
125
141
  def validate_address!(address)
@@ -1,8 +1,9 @@
1
1
  # lib/tron/configuration.rb
2
2
  module Tron
3
3
  class Configuration
4
- attr_accessor :api_key, :tronscan_api_key, :network, :timeout, :base_url, :tronscan_base_url, :strict_mode
4
+ attr_accessor :api_key, :tronscan_api_key, :timeout, :base_url, :tronscan_base_url, :strict_mode
5
5
  attr_accessor :cache_enabled, :cache_ttl, :cache_max_stale
6
+ attr_reader :network
6
7
 
7
8
  def initialize
8
9
  @network = :mainnet
@@ -20,6 +21,16 @@ module Tron
20
21
  setup_urls
21
22
  end
22
23
 
24
+ def cache=(options)
25
+ if options.is_a?(Hash)
26
+ @cache_enabled = options.fetch(:enabled, true)
27
+ @cache_ttl = options.fetch(:ttl, 300)
28
+ @cache_max_stale = options.fetch(:max_stale, 600)
29
+ elsif options == false
30
+ @cache_enabled = false
31
+ end
32
+ end
33
+
23
34
  private
24
35
 
25
36
  def setup_urls
@@ -1,50 +1,87 @@
1
1
  # lib/tron/services/balance.rb
2
2
  require_relative '../utils/http'
3
3
  require_relative '../utils/address'
4
+ require_relative '../utils/cache'
5
+ require_relative '../utils/rate_limiter'
4
6
 
5
7
  module Tron
6
8
  module Services
7
9
  class Balance
8
10
  def initialize(config)
9
11
  @config = config
12
+ @cache = Utils::Cache.new(max_age: config.cache_ttl) if config.cache_enabled
13
+ @rate_limiter = Utils::RateLimiter.new(max_requests: 1, time_window: 1.0)
14
+ @cache_hits = 0
15
+ @cache_misses = 0
10
16
  end
11
17
 
12
18
  def get_trx(address)
13
19
  validate_address!(address)
14
-
20
+
21
+ cache_key = "balance:trx:#{address}:#{@config.network}"
22
+
23
+ # Check cache first
24
+ if @config.cache_enabled && (cached = @cache.get(cache_key))
25
+ @cache_hits += 1
26
+ return cached
27
+ end
28
+
29
+ @cache_misses += 1
30
+
31
+ # Rate limit before API call
32
+ @rate_limiter.execute_request
33
+
15
34
  url = "#{@config.base_url}/v1/accounts/#{address}"
16
35
  headers = api_headers
17
-
36
+
18
37
  response = Utils::HTTP.get(url, headers)
19
-
38
+
20
39
  # Validate response structure
21
40
  raise "Unexpected API response format" unless response.is_a?(Hash)
22
41
  raise "Missing 'data' field in response" unless response.key?('data')
23
42
  raise "Invalid 'data' format in response" unless response['data'].is_a?(Array)
24
43
  raise "Empty account data in response" if response['data'].empty?
25
-
44
+
26
45
  account_data = response['data'].first
27
46
  raise "Invalid account data format" unless account_data.is_a?(Hash)
28
-
47
+
29
48
  # The balance field is only present when > 0; defaults to 0 for new/empty accounts
30
49
  raw_balance = account_data.fetch('balance', 0)
31
- format_balance(raw_balance, 6)
50
+ result = format_balance(raw_balance, 6)
51
+
52
+ # Cache the result
53
+ @cache.set(cache_key, result) if @config.cache_enabled
54
+
55
+ result
32
56
  end
33
57
 
34
58
  def get_trc20_tokens(address, strict: false)
35
59
  validate_address!(address)
36
-
60
+
61
+ cache_key = "balance:trc20:#{address}:#{@config.network}"
62
+
63
+ # Check cache first
64
+ if @config.cache_enabled && (cached = @cache.get(cache_key))
65
+ @cache_hits += 1
66
+ return cached
67
+ end
68
+
69
+ @cache_misses += 1
70
+
71
+ # Rate limit before API call
72
+ @rate_limiter.execute_request
73
+
37
74
  url = "#{@config.tronscan_base_url}/api/account/wallet?address=#{address}&asset_type=1"
38
75
  headers = tronscan_headers
39
-
76
+
40
77
  response = Utils::HTTP.get(url, headers)
41
-
78
+
42
79
  # Validate response structure
43
80
  raise "Unexpected API response format for TRC20 tokens" unless response.is_a?(Hash)
44
81
  raise "Missing 'data' field in TRC20 response" unless response.key?('data')
45
82
  raise "Invalid 'data' format in TRC20 response" unless response['data'].is_a?(Array)
46
-
47
- response['data'].select { |token| token['token_type'] == 20 && token['balance'].to_f > 0 }
83
+
84
+ result = response['data'].select { |token| token['token_type'] == 20 && token['balance'].to_f > 0 }
48
85
  .map do |token|
49
86
  validate_token_data!(token)
50
87
  {
@@ -55,6 +92,27 @@ module Tron
55
92
  address: token['token_id']
56
93
  }
57
94
  end
95
+
96
+ # Cache the result
97
+ @cache.set(cache_key, result) if @config.cache_enabled
98
+
99
+ result
100
+ end
101
+
102
+ def cache_stats
103
+ total = @cache_hits + @cache_misses
104
+ {
105
+ hits: @cache_hits,
106
+ misses: @cache_misses,
107
+ total: total,
108
+ hit_rate: total > 0 ? (@cache_hits.to_f / total * 100).round(2) : 0.0
109
+ }
110
+ end
111
+
112
+ def clear_cache
113
+ @cache&.clear
114
+ @cache_hits = 0
115
+ @cache_misses = 0
58
116
  end
59
117
 
60
118
  private
@@ -1,26 +1,54 @@
1
1
  # lib/tron/services/price.rb
2
2
  require_relative '../utils/http'
3
+ require_relative '../utils/cache'
4
+ require_relative '../utils/rate_limiter'
3
5
 
4
6
  module Tron
5
7
  module Services
6
8
  class Price
7
9
  def initialize(config)
8
10
  @config = config
11
+ @cache = Utils::Cache.new(max_age: config.cache_ttl) if config.cache_enabled
12
+ @rate_limiter = Utils::RateLimiter.new(max_requests: 1, time_window: 1.0)
13
+ @cache_hits = 0
14
+ @cache_misses = 0
9
15
  end
10
16
 
11
17
  def get_token_price(token = 'trx')
18
+ cache_key = cache_key_for(token)
19
+
20
+ # Check cache first
21
+ if @config.cache_enabled && (cached = @cache.get(cache_key))
22
+ @cache_hits += 1
23
+ return cached
24
+ end
25
+
26
+ @cache_misses += 1
27
+
28
+ # Rate limit before API call
29
+ @rate_limiter.execute_request
30
+
12
31
  url = "#{@config.tronscan_base_url}/api/token/price?token=#{token}"
13
32
  headers = tronscan_headers
14
-
33
+
15
34
  response = Utils::HTTP.get(url, headers)
16
-
35
+
17
36
  # Validate response structure
18
37
  unless response.is_a?(Hash)
19
38
  raise "Unexpected API response format for token price"
20
39
  end
21
-
40
+
41
+ # Cache the successful response
42
+ @cache.set(cache_key, response) if @config.cache_enabled
43
+
22
44
  response
23
45
  rescue => e
46
+ # Serve stale data if available
47
+ if @config.cache_enabled && cached
48
+ warn "Warning: API error for #{token}, serving stale cache: #{e.message}"
49
+ return cached
50
+ end
51
+
24
52
  if @config.strict_mode
25
53
  raise e
26
54
  else
@@ -51,16 +79,30 @@ module Tron
51
79
  end
52
80
 
53
81
  def get_token_price_usd(token)
82
+ cache_key = "#{cache_key_for(token)}:usd"
83
+
84
+ # Check cache first for the USD price directly
85
+ if @config.cache_enabled && (cached = @cache.get(cache_key))
86
+ @cache_hits += 1
87
+ return cached
88
+ end
89
+
90
+ @cache_misses += 1
54
91
  price_data = get_token_price(token)
55
92
  return nil unless price_data.is_a?(Hash)
56
-
57
- if price_data['price_in_usd']
93
+
94
+ result = if price_data['price_in_usd']
58
95
  price_data['price_in_usd'].to_f
59
96
  elsif price_data['priceInUsd']
60
97
  price_data['priceInUsd'].to_f
61
98
  else
62
99
  nil
63
100
  end
101
+
102
+ # Cache the USD price
103
+ @cache.set(cache_key, result) if @config.cache_enabled && result
104
+
105
+ result
64
106
  end
65
107
 
66
108
  def get_token_value_usd(balance, token)
@@ -104,8 +146,28 @@ module Tron
104
146
  end
105
147
  end
106
148
 
149
+ def cache_stats
150
+ total = @cache_hits + @cache_misses
151
+ {
152
+ hits: @cache_hits,
153
+ misses: @cache_misses,
154
+ total: total,
155
+ hit_rate: total > 0 ? (@cache_hits.to_f / total * 100).round(2) : 0.0
156
+ }
157
+ end
158
+
159
+ def clear_cache
160
+ @cache&.clear
161
+ @cache_hits = 0
162
+ @cache_misses = 0
163
+ end
164
+
107
165
  private
108
166
 
167
+ def cache_key_for(token)
168
+ "price:#{token.downcase}:#{@config.network}"
169
+ end
170
+
109
171
  def tronscan_headers
110
172
  headers = { 'accept' => 'application/json' }
111
173
  headers['TRON-PRO-API-KEY'] = @config.tronscan_api_key if @config.tronscan_api_key
@@ -15,7 +15,7 @@ module Tron
15
15
  # Try getaccountresource first
16
16
  begin
17
17
  get_account_resources(address)
18
- rescue => e
18
+ rescue
19
19
  # Fallback to getaccount if getaccountresource fails
20
20
  get_account_resources_fallback(address)
21
21
  end
@@ -0,0 +1,61 @@
1
+ # lib/tron/utils/cache.rb
2
+ require 'monitor'
3
+
4
+ module Tron
5
+ module Utils
6
+ class Cache
7
+ include MonitorMixin
8
+
9
+ def initialize(max_age: 300) # 5 minutes default
10
+ super()
11
+ @cache = {}
12
+ @timestamps = {}
13
+ @max_age = max_age
14
+ end
15
+
16
+ def get(key)
17
+ synchronize do
18
+ cleanup_expired_entries
19
+
20
+ if @cache.key?(key)
21
+ @cache[key]
22
+ else
23
+ nil
24
+ end
25
+ end
26
+ end
27
+
28
+ def set(key, value)
29
+ synchronize do
30
+ @cache[key] = value
31
+ @timestamps[key] = Time.now.to_f
32
+ end
33
+ end
34
+
35
+ def delete(key)
36
+ synchronize do
37
+ @cache.delete(key)
38
+ @timestamps.delete(key)
39
+ end
40
+ end
41
+
42
+ def clear
43
+ synchronize do
44
+ @cache.clear
45
+ @timestamps.clear
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def cleanup_expired_entries
52
+ now = Time.now.to_f
53
+ expired_keys = @timestamps.select { |_, timestamp| now - timestamp > @max_age }.keys
54
+ expired_keys.each do |key|
55
+ @cache.delete(key)
56
+ @timestamps.delete(key)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,60 @@
1
+ # lib/tron/utils/rate_limiter.rb
2
+ require 'monitor'
3
+
4
+ module Tron
5
+ module Utils
6
+ class RateLimiter
7
+ include MonitorMixin
8
+
9
+ def initialize(max_requests:, time_window:)
10
+ super()
11
+ @max_requests = max_requests
12
+ @time_window = time_window
13
+ @request_timestamps = []
14
+ end
15
+
16
+ def can_make_request?
17
+ synchronize do
18
+ cleanup_old_requests
19
+ @request_timestamps.length < @max_requests
20
+ end
21
+ end
22
+
23
+ def execute_request
24
+ synchronize do
25
+ cleanup_old_requests
26
+
27
+ if @request_timestamps.length >= @max_requests
28
+ # Calculate sleep time until oldest request exits the time window
29
+ oldest_time = @request_timestamps.first
30
+ sleep_time = @time_window - (Time.now.to_f - oldest_time)
31
+ sleep(sleep_time) if sleep_time > 0
32
+ cleanup_old_requests
33
+ end
34
+
35
+ @request_timestamps << Time.now.to_f
36
+ # Return the time until next allowed request
37
+ calculate_time_to_next_request
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def cleanup_old_requests
44
+ now = Time.now.to_f
45
+ @request_timestamps.reject! { |timestamp| now - timestamp > @time_window }
46
+ end
47
+
48
+ def calculate_time_to_next_request
49
+ if @request_timestamps.length <= 1
50
+ 0
51
+ else
52
+ now = Time.now.to_f
53
+ oldest_in_window = now - @time_window
54
+ next_available = @request_timestamps.find { |ts| ts > oldest_in_window }
55
+ next_available ? [0, @time_window - (now - next_available)].max : 0
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
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.5".freeze
5
+ VERSION = "1.0.6".freeze
6
6
  end
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.5
4
+ version: 1.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Your Name
@@ -100,7 +100,9 @@ files:
100
100
  - lib/tron/services/price.rb
101
101
  - lib/tron/services/resources.rb
102
102
  - lib/tron/utils/address.rb
103
+ - lib/tron/utils/cache.rb
103
104
  - lib/tron/utils/http.rb
105
+ - lib/tron/utils/rate_limiter.rb
104
106
  - lib/tron/version.rb
105
107
  homepage: https://github.com/yourusername/tron
106
108
  licenses: