tron.rb 1.1.2 → 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.
- checksums.yaml +4 -4
- data/README.md +94 -308
- data/lib/tron/abi/constant.rb +18 -0
- data/lib/tron/abi/decoder.rb +174 -0
- data/lib/tron/abi/encoder.rb +293 -0
- data/lib/tron/abi/event.rb +133 -0
- data/lib/tron/abi/function.rb +135 -0
- data/lib/tron/abi/type.rb +261 -0
- data/lib/tron/abi/util.rb +100 -0
- data/lib/tron/abi.rb +153 -0
- data/lib/tron/client.rb +64 -1
- data/lib/tron/configuration.rb +39 -2
- data/lib/tron/contract.rb +157 -0
- data/lib/tron/key.rb +271 -0
- data/lib/tron/protobuf/transaction_raw_serializer.rb +295 -0
- data/lib/tron/protobuf.rb +18 -0
- data/lib/tron/services/balance.rb +43 -2
- data/lib/tron/services/contract.rb +232 -0
- data/lib/tron/services/price.rb +40 -0
- data/lib/tron/services/resources.rb +27 -0
- data/lib/tron/services/transaction.rb +104 -0
- data/lib/tron/signature.rb +21 -0
- data/lib/tron/utils/abi.rb +321 -0
- data/lib/tron/utils/address.rb +63 -10
- data/lib/tron/utils/cache.rb +18 -0
- data/lib/tron/utils/crypto.rb +67 -0
- data/lib/tron/utils/http.rb +49 -4
- data/lib/tron/utils/rate_limiter.rb +16 -0
- data/lib/tron/version.rb +1 -1
- data/lib/tron.rb +30 -1
- metadata +71 -12
|
@@ -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
|
-
#
|
|
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)
|