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,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "bigdecimal"
|
|
3
|
+
require_relative 'util'
|
|
4
|
+
require_relative 'constant'
|
|
5
|
+
|
|
6
|
+
module Tron
|
|
7
|
+
module Abi
|
|
8
|
+
# Provides a utility module to assist encoding ABIs.
|
|
9
|
+
module Encoder
|
|
10
|
+
extend self
|
|
11
|
+
|
|
12
|
+
# Encodes a specific value, either static or dynamic.
|
|
13
|
+
#
|
|
14
|
+
# @param type [Tron::Abi::Type] type to be encoded.
|
|
15
|
+
# @param arg [String|Number] value to be encoded.
|
|
16
|
+
# @return [String] the encoded type.
|
|
17
|
+
# @raise [EncodingError] if value does not match type.
|
|
18
|
+
def type(type, arg)
|
|
19
|
+
if %w(string bytes).include? type.base_type and type.sub_type.empty? and type.dimensions.empty?
|
|
20
|
+
raise EncodingError, "Argument must be a String" unless arg.instance_of? String
|
|
21
|
+
arg = handle_hex_string arg, type
|
|
22
|
+
|
|
23
|
+
# encodes strings and bytes
|
|
24
|
+
size = type Type.size_type, arg.bytesize
|
|
25
|
+
padding = ::Tron::Abi::Constant::BYTE_ZERO * (Util.ceil32(arg.bytesize) - arg.bytesize)
|
|
26
|
+
(size + arg + padding).b
|
|
27
|
+
elsif type.base_type == "tuple" && type.dimensions.size == 1 && type.dimensions[0] != 0
|
|
28
|
+
result = ""
|
|
29
|
+
result += struct_offsets(type.nested_sub, arg)
|
|
30
|
+
result += arg.map { |x| type(type.nested_sub, x) }.join
|
|
31
|
+
result
|
|
32
|
+
elsif type.dynamic? && !type.dimensions.empty? && arg.is_a?(Array)
|
|
33
|
+
|
|
34
|
+
# encodes dynamic-sized arrays
|
|
35
|
+
head = type(Type.size_type, arg.size)
|
|
36
|
+
nested_sub = type.nested_sub
|
|
37
|
+
|
|
38
|
+
if nested_sub.dynamic?
|
|
39
|
+
tails = arg.map { |a| type(nested_sub, a) }
|
|
40
|
+
offset = arg.size * 32
|
|
41
|
+
tails.each do |t|
|
|
42
|
+
head += type(Type.size_type, offset)
|
|
43
|
+
offset += t.bytesize
|
|
44
|
+
end
|
|
45
|
+
head + tails.join
|
|
46
|
+
else
|
|
47
|
+
arg.each { |a| head += type(nested_sub, a) }
|
|
48
|
+
head
|
|
49
|
+
end
|
|
50
|
+
else
|
|
51
|
+
if type.dimensions.empty?
|
|
52
|
+
|
|
53
|
+
# encode a primitive type
|
|
54
|
+
primitive_type type, arg
|
|
55
|
+
else
|
|
56
|
+
|
|
57
|
+
# encode static-size arrays
|
|
58
|
+
arg.map { |x| type(type.nested_sub, x) }.join
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Encodes primitive types.
|
|
64
|
+
#
|
|
65
|
+
# @param type [Tron::Abi::Type] type to be encoded.
|
|
66
|
+
# @param arg [String|Number] value to be encoded.
|
|
67
|
+
# @return [String] the encoded primitive type.
|
|
68
|
+
# @raise [EncodingError] if value does not match type.
|
|
69
|
+
# @raise [ValueOutOfBounds] if value is out of bounds for type.
|
|
70
|
+
# @raise [ArgumentError] if encoding fails for type.
|
|
71
|
+
def primitive_type(type, arg)
|
|
72
|
+
case type.base_type
|
|
73
|
+
when "uint"
|
|
74
|
+
uint arg, type
|
|
75
|
+
when "int"
|
|
76
|
+
int arg, type
|
|
77
|
+
when "bool"
|
|
78
|
+
bool arg
|
|
79
|
+
when "ureal", "ufixed"
|
|
80
|
+
ufixed arg, type
|
|
81
|
+
when "real", "fixed"
|
|
82
|
+
fixed arg, type
|
|
83
|
+
when "string", "bytes"
|
|
84
|
+
bytes arg, type
|
|
85
|
+
when "tuple"
|
|
86
|
+
tuple arg, type
|
|
87
|
+
when "hash"
|
|
88
|
+
hash arg, type
|
|
89
|
+
when "address"
|
|
90
|
+
address arg
|
|
91
|
+
else
|
|
92
|
+
raise EncodingError, "Unhandled type: #{type.base_type} #{type.sub_type}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Properly encodes unsigned integers.
|
|
99
|
+
def uint(arg, type)
|
|
100
|
+
arg = coerce_number arg
|
|
101
|
+
raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
|
|
102
|
+
raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > Util::UINT_MAX or arg < Util::UINT_MIN
|
|
103
|
+
real_size = type.sub_type.to_i
|
|
104
|
+
i = arg.to_i
|
|
105
|
+
raise ValueOutOfBounds, arg unless i >= 0 and i < 2 ** real_size
|
|
106
|
+
Util.zpad_int i
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Properly encodes signed integers.
|
|
110
|
+
def int(arg, type)
|
|
111
|
+
arg = coerce_number arg
|
|
112
|
+
raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
|
|
113
|
+
raise ValueOutOfBounds, "Number out of range: #{arg}" if arg > Util::INT_MAX or arg < Util::INT_MIN
|
|
114
|
+
real_size = type.sub_type.to_i
|
|
115
|
+
i = arg.to_i
|
|
116
|
+
raise ValueOutOfBounds, arg unless i >= -2 ** (real_size - 1) and i < 2 ** (real_size - 1)
|
|
117
|
+
Util.zpad_int(i % 2 ** 256)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Properly encodes booleans.
|
|
121
|
+
def bool(arg)
|
|
122
|
+
raise EncodingError, "Argument is not bool: #{arg}" unless arg.instance_of? TrueClass or arg.instance_of? FalseClass
|
|
123
|
+
Util.zpad_int(arg ? 1 : 0)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Properly encodes unsigned fixed-point numbers.
|
|
127
|
+
def ufixed(arg, type)
|
|
128
|
+
arg = coerce_number arg
|
|
129
|
+
raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
|
|
130
|
+
high, low = type.sub_type.split("x").map(&:to_i)
|
|
131
|
+
raise ValueOutOfBounds, arg unless arg >= 0 and arg < 2 ** high
|
|
132
|
+
Util.zpad_int((arg * 2 ** low).to_i)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Properly encodes signed fixed-point numbers.
|
|
136
|
+
def fixed(arg, type)
|
|
137
|
+
arg = coerce_number arg
|
|
138
|
+
raise ArgumentError, "Don't know how to handle this input." unless arg.is_a? Numeric
|
|
139
|
+
high, low = type.sub_type.split("x").map(&:to_i)
|
|
140
|
+
raise ValueOutOfBounds, arg unless arg >= -2 ** (high - 1) and arg < 2 ** (high - 1)
|
|
141
|
+
i = (arg * 2 ** low).to_i
|
|
142
|
+
Util.zpad_int(i % 2 ** (high + low))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Properly encodes byte-strings.
|
|
146
|
+
def bytes(arg, type)
|
|
147
|
+
raise EncodingError, "Expecting String: #{arg}" unless arg.instance_of? String
|
|
148
|
+
arg = handle_hex_string arg, type
|
|
149
|
+
|
|
150
|
+
if type.sub_type.empty?
|
|
151
|
+
size = Util.zpad_int arg.bytesize
|
|
152
|
+
padding = ::Tron::Abi::Constant::BYTE_ZERO * (Util.ceil32(arg.bytesize) - arg.bytesize)
|
|
153
|
+
|
|
154
|
+
# variable length string/bytes
|
|
155
|
+
(size + arg + padding).b
|
|
156
|
+
else
|
|
157
|
+
raise ValueOutOfBounds, arg unless arg.bytesize <= type.sub_type.to_i
|
|
158
|
+
padding = ::Tron::Abi::Constant::BYTE_ZERO * (32 - arg.bytesize)
|
|
159
|
+
|
|
160
|
+
# fixed length string/bytes
|
|
161
|
+
(arg + padding).b
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Properly encodes tuples.
|
|
166
|
+
def tuple(arg, type)
|
|
167
|
+
unless arg.is_a?(Hash) || arg.is_a?(Array)
|
|
168
|
+
raise EncodingError, "Expecting Hash or Array: #{arg}"
|
|
169
|
+
end
|
|
170
|
+
raise EncodingError, "Expecting #{type.components.size} elements: #{arg}" unless arg.size == type.components.size
|
|
171
|
+
arg = arg.transform_keys(&:to_s) if arg.is_a?(Hash) # because component_type.name is String
|
|
172
|
+
|
|
173
|
+
static_size = 0
|
|
174
|
+
type.components.each_with_index do |component, i|
|
|
175
|
+
if type.components[i].dynamic?
|
|
176
|
+
static_size += 32
|
|
177
|
+
else
|
|
178
|
+
static_size += Util.ceil32(type.components[i].size || 0)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
dynamic_offset = static_size
|
|
183
|
+
offsets_and_static_values = []
|
|
184
|
+
dynamic_values = []
|
|
185
|
+
|
|
186
|
+
type.components.each_with_index do |component, i|
|
|
187
|
+
component_type = type.components[i]
|
|
188
|
+
if component_type.dynamic?
|
|
189
|
+
offsets_and_static_values << type(Type.size_type, dynamic_offset)
|
|
190
|
+
dynamic_value = type(component_type, arg.is_a?(Array) ? arg[i] : arg[component_type.name])
|
|
191
|
+
dynamic_values << dynamic_value
|
|
192
|
+
dynamic_offset += dynamic_value.bytesize
|
|
193
|
+
else
|
|
194
|
+
offsets_and_static_values << type(component_type, arg.is_a?(Array) ? arg[i] : arg.fetch(component_type.name))
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
offsets_and_static_values.join + dynamic_values.join
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def coerce_number(arg)
|
|
202
|
+
return arg if arg.is_a? Numeric
|
|
203
|
+
return arg.to_i(0) if arg.is_a?(String) && arg.match?(/^-?(0x)?[0-9a-fA-F]+$/)
|
|
204
|
+
return BigDecimal(arg) if arg.is_a?(String) && arg.match?(/^-?\d+(\.\d+)?$/)
|
|
205
|
+
arg
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Properly encode struct offsets.
|
|
209
|
+
def struct_offsets(type, arg)
|
|
210
|
+
result = ""
|
|
211
|
+
offset = arg.size
|
|
212
|
+
tails_encoding = arg.map { |a| type(type, a) }
|
|
213
|
+
arg.size.times do |i|
|
|
214
|
+
if i == 0
|
|
215
|
+
offset *= 32
|
|
216
|
+
else
|
|
217
|
+
offset += tails_encoding[i - 1].bytesize
|
|
218
|
+
end
|
|
219
|
+
offset_string = type(Type.size_type, offset)
|
|
220
|
+
result += offset_string
|
|
221
|
+
end
|
|
222
|
+
result
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Properly encodes hash-strings.
|
|
226
|
+
def hash(arg, type)
|
|
227
|
+
size = type.sub_type.to_i
|
|
228
|
+
raise EncodingError, "Argument too long: #{arg}" unless size > 0 and size <= 32
|
|
229
|
+
if arg.is_a? Integer
|
|
230
|
+
|
|
231
|
+
# hash from integer
|
|
232
|
+
Util.zpad_int arg
|
|
233
|
+
elsif arg.size == size
|
|
234
|
+
|
|
235
|
+
# hash from encoded hash
|
|
236
|
+
Util.zpad arg, 32
|
|
237
|
+
elsif arg.size == size * 2
|
|
238
|
+
|
|
239
|
+
# hash from hexadecimal hash
|
|
240
|
+
Util.zpad_hex arg
|
|
241
|
+
else
|
|
242
|
+
raise EncodingError, "Could not parse hash: #{arg}"
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Properly encodes addresses.
|
|
247
|
+
def address(arg)
|
|
248
|
+
# Handle TRON-specific address encoding
|
|
249
|
+
require_relative '../utils/address'
|
|
250
|
+
|
|
251
|
+
if arg.is_a?(String) && arg.start_with?('T') && arg.length == 34
|
|
252
|
+
# TRON address in Base58 format - convert to hex and pad
|
|
253
|
+
hex_addr = Utils::Address.to_hex(arg)
|
|
254
|
+
# Remove the 0x41 prefix and pad to 32 bytes (64 hex chars)
|
|
255
|
+
addr_without_prefix = hex_addr[2..-1] # Remove '41' prefix
|
|
256
|
+
Util.zpad_hex addr_without_prefix
|
|
257
|
+
elsif arg.is_a? Integer
|
|
258
|
+
# address from integer
|
|
259
|
+
Util.zpad_int arg
|
|
260
|
+
elsif arg.size == 20
|
|
261
|
+
# address from raw 20-byte address
|
|
262
|
+
Util.zpad arg, 32
|
|
263
|
+
elsif arg.size == 40
|
|
264
|
+
# address from hexadecimal address (without 0x prefix)
|
|
265
|
+
Util.zpad_hex arg
|
|
266
|
+
elsif arg.size == 42 and arg[0, 2] == "0x"
|
|
267
|
+
# address from hexadecimal address with 0x prefix
|
|
268
|
+
Util.zpad_hex arg[2..-1]
|
|
269
|
+
else
|
|
270
|
+
raise EncodingError, "Could not parse address: #{arg}"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# The ABI encoder needs to be able to determine between a hex `"123"`
|
|
275
|
+
# and a binary `"123"` string.
|
|
276
|
+
def handle_hex_string(arg, type)
|
|
277
|
+
if Util.prefixed? arg or
|
|
278
|
+
(arg.size === type.sub_type.to_i * 2 and Util.hex? arg)
|
|
279
|
+
|
|
280
|
+
# There is no way telling whether a string is hex or binary with certainty
|
|
281
|
+
# in Ruby. Therefore, we assume a `0x` prefix to indicate a hex string.
|
|
282
|
+
# Additionally, if the string size is exactly the double of the expected
|
|
283
|
+
# binary size, we can assume a hex value.
|
|
284
|
+
Util.hex_to_bin arg
|
|
285
|
+
else
|
|
286
|
+
|
|
287
|
+
# Everything else will be assumed binary or raw string.
|
|
288
|
+
arg.b
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest/keccak'
|
|
4
|
+
require_relative 'type'
|
|
5
|
+
require_relative 'encoder'
|
|
6
|
+
require_relative 'decoder'
|
|
7
|
+
require_relative 'util'
|
|
8
|
+
|
|
9
|
+
module Tron
|
|
10
|
+
module Abi
|
|
11
|
+
# Represents a Solidity event for ABI encoding/decoding
|
|
12
|
+
class Event
|
|
13
|
+
attr_reader :name, :inputs, :anonymous, :signature, :topic_hash
|
|
14
|
+
|
|
15
|
+
# Create a new Event instance
|
|
16
|
+
#
|
|
17
|
+
# @param name [String] the event name
|
|
18
|
+
# @param inputs [Array<Hash>] the input parameters
|
|
19
|
+
# @param anonymous [Boolean] whether the event is anonymous
|
|
20
|
+
def initialize(name:, inputs: [], anonymous: false)
|
|
21
|
+
@name = name
|
|
22
|
+
@inputs = inputs
|
|
23
|
+
@anonymous = anonymous
|
|
24
|
+
@signature = generate_signature
|
|
25
|
+
@topic_hash = generate_topic_hash
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Generate the event signature
|
|
29
|
+
#
|
|
30
|
+
# @return [String] the event signature
|
|
31
|
+
def generate_signature
|
|
32
|
+
param_types = @inputs.map { |input| input[:type] }.join(',')
|
|
33
|
+
"#{@name}(#{param_types})"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Generate the topic hash (Keccak256 hash of signature)
|
|
37
|
+
#
|
|
38
|
+
# @return [String] the topic hash as hex string
|
|
39
|
+
def generate_topic_hash
|
|
40
|
+
# Using Keccak256 hash from the gem
|
|
41
|
+
hash = Digest::Keccak.new(256).digest(@signature)
|
|
42
|
+
hash.unpack1('H*')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Decode event log data from transaction receipt
|
|
46
|
+
#
|
|
47
|
+
# @param topics [Array<String>] the topics from the event log
|
|
48
|
+
# @param data [String] the data from the event log
|
|
49
|
+
# @return [Hash] decoded event parameters
|
|
50
|
+
def decode_log(topics, data)
|
|
51
|
+
# Create a result hash with the event name
|
|
52
|
+
result = { name: @name, params: [] }
|
|
53
|
+
|
|
54
|
+
# Remove '0x' prefix if present
|
|
55
|
+
raw_data = data.start_with?('0x') ? data[2..-1] : data
|
|
56
|
+
raw_topics = topics.map { |t| t.start_with?('0x') ? t[2..-1] : t }
|
|
57
|
+
|
|
58
|
+
# Process topics and inputs
|
|
59
|
+
topic_idx = @anonymous ? 0 : 1 # Skip the first topic if not anonymous
|
|
60
|
+
data_offset = 0
|
|
61
|
+
|
|
62
|
+
@inputs.each_with_index do |input, i|
|
|
63
|
+
type = Type.parse(input[:type])
|
|
64
|
+
|
|
65
|
+
if input[:indexed] # This parameter was included in the topics
|
|
66
|
+
if topic_idx < raw_topics.length
|
|
67
|
+
# Decode the topic value
|
|
68
|
+
topic_data = raw_topics[topic_idx]
|
|
69
|
+
# For static types like uint, address, bool, etc., the value is directly in the topic
|
|
70
|
+
# For dynamic types like string, bytes, arrays, etc., the topic contains a hash of the value
|
|
71
|
+
decoded_value = if type.dynamic?
|
|
72
|
+
# For dynamic types in topics, the topic contains the hash of the value
|
|
73
|
+
# For proper decoding, you'd need the original value or to look it up elsewhere
|
|
74
|
+
topic_data
|
|
75
|
+
else
|
|
76
|
+
# For static types, decode from the 32-byte topic value
|
|
77
|
+
# Convert hex to binary before decoding
|
|
78
|
+
Decoder.type(type, Util.hex_to_bin(topic_data))
|
|
79
|
+
end
|
|
80
|
+
result[:params] << { name: input[:name], type: input[:type], value: decoded_value, indexed: true }
|
|
81
|
+
topic_idx += 1
|
|
82
|
+
end
|
|
83
|
+
else # This parameter was included in the data section
|
|
84
|
+
if type.dynamic?
|
|
85
|
+
# For dynamic types, read the offset first
|
|
86
|
+
offset_ptr = raw_data[data_offset, 64] # 32 bytes = 64 hex chars
|
|
87
|
+
# Convert hex to binary before deserializing
|
|
88
|
+
actual_offset = Util.deserialize_big_endian_to_int(Util.hex_to_bin(offset_ptr))
|
|
89
|
+
|
|
90
|
+
# Decode using the actual offset
|
|
91
|
+
# Convert hex to binary before decoding
|
|
92
|
+
decoded_value = Decoder.type(type, Util.hex_to_bin(raw_data[actual_offset * 2..-1]))
|
|
93
|
+
result[:params] << { name: input[:name], type: input[:type], value: decoded_value, indexed: false }
|
|
94
|
+
data_offset += 64 # Move by 64 hex chars for the offset pointer
|
|
95
|
+
else
|
|
96
|
+
# For static types, decode directly from current offset
|
|
97
|
+
size = type.size * 2 # size in bytes converted to hex chars
|
|
98
|
+
# Convert hex to binary before decoding
|
|
99
|
+
decoded_value = Decoder.type(type, Util.hex_to_bin(raw_data[data_offset, size]))
|
|
100
|
+
result[:params] << { name: input[:name], type: input[:type], value: decoded_value, indexed: false }
|
|
101
|
+
data_offset += size
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
result
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Create an Event instance from an ABI definition
|
|
110
|
+
#
|
|
111
|
+
# @param abi_def [Hash] the ABI definition
|
|
112
|
+
# @return [Event] a new Event instance
|
|
113
|
+
def self.from_abi(abi_def)
|
|
114
|
+
raise ArgumentError, "Not an event definition" unless abi_def[:type] == 'event' || abi_def['type'] == 'event'
|
|
115
|
+
|
|
116
|
+
new(
|
|
117
|
+
name: abi_def[:name] || abi_def['name'],
|
|
118
|
+
inputs: abi_def[:inputs] || abi_def['inputs'] || [],
|
|
119
|
+
anonymous: abi_def[:anonymous] || abi_def['anonymous'] || false
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Generate event signature from signature string
|
|
124
|
+
#
|
|
125
|
+
# @param sig [String] event signature (e.g. "Transfer(address,address,uint256)")
|
|
126
|
+
# @return [String] the topic hash
|
|
127
|
+
def self.signature(sig)
|
|
128
|
+
hash = Digest::Keccak.new(256).digest(sig)
|
|
129
|
+
hash.unpack1('H*')
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest/keccak'
|
|
4
|
+
require_relative 'type'
|
|
5
|
+
require_relative 'encoder'
|
|
6
|
+
require_relative 'decoder'
|
|
7
|
+
require_relative 'util'
|
|
8
|
+
|
|
9
|
+
module Tron
|
|
10
|
+
module Abi
|
|
11
|
+
# Represents a Solidity function for ABI encoding/decoding
|
|
12
|
+
class Function
|
|
13
|
+
attr_reader :name, :inputs, :outputs, :signature, :method_id
|
|
14
|
+
|
|
15
|
+
# Create a new Function instance
|
|
16
|
+
#
|
|
17
|
+
# @param name [String] the function name
|
|
18
|
+
# @param inputs [Array<Hash>] the input parameters
|
|
19
|
+
# @param outputs [Array<Hash>] the output parameters
|
|
20
|
+
# @param constant [Boolean] whether the function is constant (view/pure)
|
|
21
|
+
# @param payable [Boolean] whether the function is payable
|
|
22
|
+
def initialize(name:, inputs: [], outputs: [], constant: false, payable: false)
|
|
23
|
+
@name = name
|
|
24
|
+
@inputs = inputs
|
|
25
|
+
@outputs = outputs
|
|
26
|
+
@constant = constant
|
|
27
|
+
@payable = payable
|
|
28
|
+
@signature = generate_signature
|
|
29
|
+
@method_id = generate_method_id
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Generate the function signature
|
|
33
|
+
#
|
|
34
|
+
# @return [String] the function signature
|
|
35
|
+
def generate_signature
|
|
36
|
+
param_types = @inputs.map { |input| input[:type] }.join(',')
|
|
37
|
+
"#{@name}(#{param_types})"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Generate the method ID (first 4 bytes of Keccak256 hash of signature)
|
|
41
|
+
#
|
|
42
|
+
# @return [String] the method ID as hex string
|
|
43
|
+
def generate_method_id
|
|
44
|
+
# Using Keccak256 hash from the gem
|
|
45
|
+
hash = Digest::Keccak.new(256).digest(@signature)
|
|
46
|
+
# Take only first 4 bytes (8 hex chars)
|
|
47
|
+
hash[0, 4].unpack1('H*')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Encode a function call with given parameters
|
|
51
|
+
#
|
|
52
|
+
# @param parameters [Array] the parameter values to encode
|
|
53
|
+
# @return [String] encoded function call data
|
|
54
|
+
def encode_input(parameters)
|
|
55
|
+
# Verify parameter count matches input count
|
|
56
|
+
raise ArgumentError, "Expected #{@inputs.length} parameters, got #{parameters.length}" unless parameters.length == @inputs.length
|
|
57
|
+
|
|
58
|
+
# Encode each parameter according to its type
|
|
59
|
+
encoded_params = []
|
|
60
|
+
parameters.each_with_index do |param, idx|
|
|
61
|
+
input_type = @inputs[idx][:type]
|
|
62
|
+
type = Type.parse(input_type)
|
|
63
|
+
encoded_params << Encoder.type(type, param)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Combine method ID and encoded parameters
|
|
67
|
+
# Return hex string to maintain consistency with the ABI.encode function
|
|
68
|
+
Util.bin_to_hex(Util.hex_to_bin(@method_id) + encoded_params.join)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Decode function output from returned data
|
|
72
|
+
#
|
|
73
|
+
# @param data [String] the returned data from the function call
|
|
74
|
+
# @return [Array] decoded output values
|
|
75
|
+
def decode_output(data)
|
|
76
|
+
# Remove the '0x' prefix if present
|
|
77
|
+
raw_data = data.start_with?('0x') ? data[2..-1] : data
|
|
78
|
+
|
|
79
|
+
# Decode each output parameter
|
|
80
|
+
results = []
|
|
81
|
+
offset = 0
|
|
82
|
+
|
|
83
|
+
@outputs.each do |output|
|
|
84
|
+
type = Type.parse(output[:type])
|
|
85
|
+
if type.dynamic?
|
|
86
|
+
# For dynamic types, read the offset first
|
|
87
|
+
offset_ptr = raw_data[offset, 64] # 32 bytes = 64 hex chars
|
|
88
|
+
# Convert hex to binary before deserializing
|
|
89
|
+
actual_offset = Util.deserialize_big_endian_to_int(Util.hex_to_bin(offset_ptr))
|
|
90
|
+
|
|
91
|
+
# Decode using the actual offset
|
|
92
|
+
# Convert hex to binary before decoding
|
|
93
|
+
decoded_value = Decoder.type(type, Util.hex_to_bin(raw_data[actual_offset * 2..-1]))
|
|
94
|
+
results << decoded_value
|
|
95
|
+
offset += 64 # Move by 64 hex chars for the offset pointer
|
|
96
|
+
else
|
|
97
|
+
# For static types, decode directly from current offset
|
|
98
|
+
size = type.size * 2 # size in bytes converted to hex chars
|
|
99
|
+
# Convert hex to binary before decoding
|
|
100
|
+
decoded_value = Decoder.type(type, Util.hex_to_bin(raw_data[offset, size]))
|
|
101
|
+
results << decoded_value
|
|
102
|
+
offset += size
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
results
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Create a Function instance from an ABI definition
|
|
110
|
+
#
|
|
111
|
+
# @param abi_def [Hash] the ABI definition
|
|
112
|
+
# @return [Function] a new Function instance
|
|
113
|
+
def self.from_abi(abi_def)
|
|
114
|
+
raise ArgumentError, "Not a function definition" unless abi_def[:type] == 'function' || abi_def['type'] == 'function'
|
|
115
|
+
|
|
116
|
+
new(
|
|
117
|
+
name: abi_def[:name] || abi_def['name'],
|
|
118
|
+
inputs: abi_def[:inputs] || abi_def['inputs'] || [],
|
|
119
|
+
outputs: abi_def[:outputs] || abi_def['outputs'] || [],
|
|
120
|
+
constant: abi_def[:constant] || abi_def['constant'] || false,
|
|
121
|
+
payable: abi_def[:payable] || abi_def['payable'] || false
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Generate function signature from signature string
|
|
126
|
+
#
|
|
127
|
+
# @param sig [String] function signature (e.g. "transfer(address,uint256)")
|
|
128
|
+
# @return [String] the method ID
|
|
129
|
+
def self.signature(sig)
|
|
130
|
+
hash = Digest::Keccak.new(256).digest(sig)
|
|
131
|
+
hash[0, 4].unpack1('H*')
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|