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 +4 -4
- data/README.md +47 -0
- data/lib/tron/cache.rb +1 -1
- data/lib/tron/client.rb +16 -0
- data/lib/tron/configuration.rb +12 -1
- data/lib/tron/services/balance.rb +69 -11
- data/lib/tron/services/price.rb +67 -5
- data/lib/tron/services/resources.rb +1 -1
- data/lib/tron/utils/cache.rb +61 -0
- data/lib/tron/utils/rate_limiter.rb +60 -0
- data/lib/tron/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e552bde0f9721d76aee5ee554e0f565dcda21275d977e675085c110e4b7cba9
|
4
|
+
data.tar.gz: 74b56a49ceca14c4e15efd5fd278e6b6bc0cbb579a68df9c48f219a7531b4979
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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)
|
data/lib/tron/configuration.rb
CHANGED
@@ -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, :
|
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
|
data/lib/tron/services/price.rb
CHANGED
@@ -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
|
@@ -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
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
|
+
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:
|