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.
@@ -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