tron.rb 1.0.9 → 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 +41 -4
- 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,295 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'google/protobuf'
|
|
3
|
+
|
|
4
|
+
# This file contains proper Protocol Buffer definitions for TRON transactions
|
|
5
|
+
# These would normally be generated from .proto files, but for this implementation
|
|
6
|
+
# we'll define the essential structures needed for transaction serialization
|
|
7
|
+
|
|
8
|
+
module Tron
|
|
9
|
+
module Protobuf
|
|
10
|
+
# We need to use the google-protobuf gem to define the classes
|
|
11
|
+
# First, let's define a basic structure for TRON transaction serialization
|
|
12
|
+
|
|
13
|
+
# Rather than writing the complex protobuf definitions from scratch,
|
|
14
|
+
# we'll create a helper that properly serializes the transaction according to TRON specs
|
|
15
|
+
class TransactionRawSerializer
|
|
16
|
+
# Field numbers according to TRON's protocol buffer definitions
|
|
17
|
+
REF_BLOCK_BYTES = 1
|
|
18
|
+
# @return [Integer] reference block bytes field number
|
|
19
|
+
REF_BLOCK_NUM = 2
|
|
20
|
+
# @return [Integer] reference block number field number
|
|
21
|
+
REF_BLOCK_HASH = 3
|
|
22
|
+
# @return [Integer] reference block hash field number
|
|
23
|
+
EXPIRATION = 4
|
|
24
|
+
# @return [Integer] expiration field number
|
|
25
|
+
AUTHS = 5 # authority
|
|
26
|
+
# @return [Integer] authority field number
|
|
27
|
+
DATA = 6
|
|
28
|
+
# @return [Integer] data field number
|
|
29
|
+
CONTRACT = 7
|
|
30
|
+
# @return [Integer] contract field number
|
|
31
|
+
SCRIPTS = 8
|
|
32
|
+
# @return [Integer] scripts field number
|
|
33
|
+
FEE_LIMIT = 9
|
|
34
|
+
# @return [Integer] fee limit field number
|
|
35
|
+
|
|
36
|
+
# Contract field numbers
|
|
37
|
+
CONTRACT_TYPE = 1
|
|
38
|
+
# @return [Integer] contract type field number
|
|
39
|
+
CONTRACT_PARAMETER = 2
|
|
40
|
+
# @return [Integer] contract parameter field number
|
|
41
|
+
CONTRACT_PROVIDER = 3
|
|
42
|
+
# @return [Integer] contract provider field number
|
|
43
|
+
|
|
44
|
+
# Serializes a transaction for signing according to TRON's protocol buffer specification
|
|
45
|
+
#
|
|
46
|
+
# @param transaction [Hash] the transaction data to serialize
|
|
47
|
+
# @return [String] the serialized transaction in protocol buffer format
|
|
48
|
+
def self.serialize(transaction)
|
|
49
|
+
raw_data = transaction['raw_data']
|
|
50
|
+
result = []
|
|
51
|
+
|
|
52
|
+
# Serialize each field in the proper order with field numbers
|
|
53
|
+
if raw_data.key?('ref_block_bytes')
|
|
54
|
+
result << encode_field(REF_BLOCK_BYTES, :bytes, convert_hex_to_bytes(raw_data['ref_block_bytes']))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
if raw_data.key?('ref_block_num')
|
|
58
|
+
result << encode_field(REF_BLOCK_NUM, :varint, raw_data['ref_block_num'])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if raw_data.key?('ref_block_hash')
|
|
62
|
+
result << encode_field(REF_BLOCK_HASH, :bytes, convert_hex_to_bytes(raw_data['ref_block_hash']))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if raw_data.key?('expiration')
|
|
66
|
+
result << encode_field(EXPIRATION, :varint, raw_data['expiration'])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Add timestamp - TRON has timestamp as part of raw_data
|
|
70
|
+
if raw_data.key?('timestamp')
|
|
71
|
+
result << encode_field(10, :varint, raw_data['timestamp']) # timestamp field number is typically 10
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if raw_data.key?('fee_limit')
|
|
75
|
+
result << encode_field(FEE_LIMIT, :varint, raw_data['fee_limit'])
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Handle contracts - this is more complex
|
|
79
|
+
if raw_data.key?('contract')
|
|
80
|
+
contract_data = raw_data['contract']
|
|
81
|
+
if contract_data.is_a?(Array)
|
|
82
|
+
# TRON allows multiple contracts in one transaction
|
|
83
|
+
contract_data.each do |contract|
|
|
84
|
+
result << encode_field(CONTRACT, :embedded_message, serialize_contract(contract))
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
# Single contract
|
|
88
|
+
result << encode_field(CONTRACT, :embedded_message, serialize_contract(contract_data))
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Handle data field
|
|
93
|
+
if raw_data.key?('data')
|
|
94
|
+
result << encode_field(DATA, :bytes, convert_hex_to_bytes(raw_data['data']))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Handle auths (authority)
|
|
98
|
+
if raw_data.key?('authority')
|
|
99
|
+
# For now, just handle as bytes or varint depending on the structure
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
result.join
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
# Encodes a field in protobuf wire format
|
|
108
|
+
#
|
|
109
|
+
# @param field_number [Integer] the field number
|
|
110
|
+
# @param field_type [Symbol] the type of field (:varint, :bytes, :embedded_message)
|
|
111
|
+
# @param value [Object] the value to encode
|
|
112
|
+
# @return [String] the encoded field
|
|
113
|
+
def self.encode_field(field_number, field_type, value)
|
|
114
|
+
# In protobuf, each field is encoded as (field_number << 3 | wire_type) + value
|
|
115
|
+
key = (field_number << 3) | wire_type(field_type)
|
|
116
|
+
varint_bytes = encode_varint(key)
|
|
117
|
+
|
|
118
|
+
case field_type
|
|
119
|
+
when :varint
|
|
120
|
+
varint_bytes + encode_varint(value)
|
|
121
|
+
when :bytes
|
|
122
|
+
varint_bytes + encode_varint(value.length) + value
|
|
123
|
+
when :embedded_message
|
|
124
|
+
varint_bytes + encode_varint(value.length) + value
|
|
125
|
+
else
|
|
126
|
+
varint_bytes + value
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Maps field types to protobuf wire types
|
|
131
|
+
#
|
|
132
|
+
# @param field_type [Symbol] the field type
|
|
133
|
+
# @return [Integer] the wire type number
|
|
134
|
+
def self.wire_type(field_type)
|
|
135
|
+
case field_type
|
|
136
|
+
when :varint
|
|
137
|
+
0 # varint wire type
|
|
138
|
+
when :bytes, :embedded_message
|
|
139
|
+
2 # length-delimited wire type
|
|
140
|
+
else
|
|
141
|
+
2 # default to length-delimited
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Encodes an integer as a protobuf varint
|
|
146
|
+
#
|
|
147
|
+
# @param value [Integer] the integer to encode
|
|
148
|
+
# @return [String] the encoded varint
|
|
149
|
+
def self.encode_varint(value)
|
|
150
|
+
result = []
|
|
151
|
+
v = value
|
|
152
|
+
loop do
|
|
153
|
+
byte = v & 0x7F
|
|
154
|
+
v >>= 7
|
|
155
|
+
if v == 0
|
|
156
|
+
result << byte
|
|
157
|
+
break
|
|
158
|
+
else
|
|
159
|
+
result << (byte | 0x80)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
result.pack('C*')
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Serializes a contract according to TRON's protocol
|
|
166
|
+
#
|
|
167
|
+
# @param contract [Hash] the contract data to serialize
|
|
168
|
+
# @return [String] the serialized contract
|
|
169
|
+
def self.serialize_contract(contract)
|
|
170
|
+
result = []
|
|
171
|
+
|
|
172
|
+
if contract.key?('type')
|
|
173
|
+
# The contract type as a varint
|
|
174
|
+
type_value = contract_type_to_int(contract['type'])
|
|
175
|
+
result << encode_field(CONTRACT_TYPE, :varint, type_value)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
if contract.key?('parameter')
|
|
179
|
+
# The parameter as embedded message
|
|
180
|
+
param_bytes = serialize_contract_parameter(contract['parameter'])
|
|
181
|
+
result << encode_field(CONTRACT_PARAMETER, :embedded_message, param_bytes)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if contract.key?('provider')
|
|
185
|
+
# Provider address as bytes
|
|
186
|
+
result << encode_field(CONTRACT_PROVIDER, :bytes, convert_hex_to_bytes(contract['provider']))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
result.join
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Serializes the parameter part of a contract
|
|
193
|
+
#
|
|
194
|
+
# @param parameter [Hash] the parameter data to serialize
|
|
195
|
+
# @return [String] the serialized parameter
|
|
196
|
+
def self.serialize_contract_parameter(parameter)
|
|
197
|
+
# This is simplified - a full implementation would need to serialize
|
|
198
|
+
# each specific contract type according to its protobuf definition
|
|
199
|
+
# For the triggerSmartContract, this would serialize the function_selector and call_value
|
|
200
|
+
|
|
201
|
+
result = []
|
|
202
|
+
|
|
203
|
+
if parameter.key?('value')
|
|
204
|
+
value = parameter['value']
|
|
205
|
+
if value.is_a?(Hash)
|
|
206
|
+
# Serialize each field in the parameter value
|
|
207
|
+
# This is where we'd need the specific protobuf definition for each contract type
|
|
208
|
+
# For now we'll serialize it in a simplified way that follows protobuf conventions
|
|
209
|
+
value.each do |field_name, field_value|
|
|
210
|
+
field_number = case field_name
|
|
211
|
+
when 'owner_address' then 1
|
|
212
|
+
when 'contract_address' then 2
|
|
213
|
+
when 'data', 'function_selector' then 3
|
|
214
|
+
when 'call_value' then 4
|
|
215
|
+
when 'fee_limit' then 5
|
|
216
|
+
else 1 # Default to 1
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
case field_value
|
|
220
|
+
when String
|
|
221
|
+
# If it's a hex string, convert to bytes
|
|
222
|
+
result << encode_field(field_number, :bytes, convert_hex_to_bytes(field_value))
|
|
223
|
+
when Integer
|
|
224
|
+
result << encode_field(field_number, :varint, field_value)
|
|
225
|
+
else
|
|
226
|
+
# Convert other types appropriately
|
|
227
|
+
result << encode_field(field_number, :bytes, field_value.to_s)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Add type_url field (field number 1 in Any type)
|
|
234
|
+
type_url = parameter['type_url'] || "type.googleapis.com/protocol.#{parameter['type'] || 'TriggerSmartContract'}"
|
|
235
|
+
result << encode_field(1, :string, type_url)
|
|
236
|
+
|
|
237
|
+
result.join
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Maps contract type names to their protocol buffer enum values
|
|
241
|
+
#
|
|
242
|
+
# @param type_name [String] the contract type name
|
|
243
|
+
# @return [Integer] the protocol buffer enum value
|
|
244
|
+
def self.contract_type_to_int(type_name)
|
|
245
|
+
type_map = {
|
|
246
|
+
'AccountCreateContract' => 0,
|
|
247
|
+
'TransferContract' => 1,
|
|
248
|
+
'TransferAssetContract' => 2,
|
|
249
|
+
'VoteAssetContract' => 3,
|
|
250
|
+
'VoteWitnessContract' => 4,
|
|
251
|
+
'WitnessCreateContract' => 5,
|
|
252
|
+
'AssetIssueContract' => 6,
|
|
253
|
+
'WitnessUpdateContract' => 7,
|
|
254
|
+
'ParticipateAssetIssueContract' => 8,
|
|
255
|
+
'AccountUpdateContract' => 9,
|
|
256
|
+
'FreezeBalanceContract' => 10,
|
|
257
|
+
'UnfreezeBalanceContract' => 11,
|
|
258
|
+
'WithdrawBalanceContract' => 12,
|
|
259
|
+
'UnfreezeAssetContract' => 13,
|
|
260
|
+
'UpdateAssetContract' => 14,
|
|
261
|
+
'ProposalCreateContract' => 15,
|
|
262
|
+
'ProposalApproveContract' => 16,
|
|
263
|
+
'ProposalDeleteContract' => 17,
|
|
264
|
+
'SetAccountIdContract' => 18,
|
|
265
|
+
'CustomContract' => 19,
|
|
266
|
+
'CreateSmartContract' => 30,
|
|
267
|
+
'TriggerSmartContract' => 31,
|
|
268
|
+
'GetContract' => 32,
|
|
269
|
+
'UpdateSettingContract' => 33,
|
|
270
|
+
'ExchangeCreateContract' => 41,
|
|
271
|
+
'ExchangeInjectContract' => 42,
|
|
272
|
+
'ExchangeWithdrawContract' => 43,
|
|
273
|
+
'ExchangeTransactionContract' => 44,
|
|
274
|
+
'UpdateEnergyLimitContract' => 45,
|
|
275
|
+
'AccountPermissionUpdateContract' => 46
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
type_map[type_name] || 0 # Default to 0 if unknown
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Converts a hex string to binary format
|
|
282
|
+
#
|
|
283
|
+
# @param hex_string [String] the hex string to convert
|
|
284
|
+
# @return [String] the binary representation
|
|
285
|
+
def self.convert_hex_to_bytes(hex_string)
|
|
286
|
+
# Remove '0x' prefix if present
|
|
287
|
+
hex = hex_string.to_s
|
|
288
|
+
hex = hex[2..-1] if hex.start_with?('0x', '0X')
|
|
289
|
+
# Pad with leading zero if odd length
|
|
290
|
+
hex = '0' + hex if hex.length.odd?
|
|
291
|
+
[hex].pack('H*')
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'google/protobuf'
|
|
3
|
+
require_relative 'protobuf/transaction_raw_serializer'
|
|
4
|
+
|
|
5
|
+
module Tron
|
|
6
|
+
module Protobuf
|
|
7
|
+
# Main class to serialize TRON transactions for signing
|
|
8
|
+
class TransactionSerializer
|
|
9
|
+
# Serializes a transaction for signing
|
|
10
|
+
#
|
|
11
|
+
# @param transaction [Hash] the transaction to serialize
|
|
12
|
+
# @return [String] the serialized transaction
|
|
13
|
+
def self.serialize_for_signing(transaction)
|
|
14
|
+
TransactionRawSerializer.serialize(transaction)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -6,7 +6,11 @@ require_relative '../utils/rate_limiter'
|
|
|
6
6
|
|
|
7
7
|
module Tron
|
|
8
8
|
module Services
|
|
9
|
+
# The Balance service handles retrieving TRX and TRC20 token balances for TRON addresses
|
|
9
10
|
class Balance
|
|
11
|
+
# Creates a new instance of the Balance service
|
|
12
|
+
#
|
|
13
|
+
# @param config [Tron::Configuration] the configuration object
|
|
10
14
|
def initialize(config)
|
|
11
15
|
@config = config
|
|
12
16
|
@cache = Utils::Cache.new(max_age: config.cache_ttl) if config.cache_enabled
|
|
@@ -15,6 +19,11 @@ module Tron
|
|
|
15
19
|
@cache_misses = 0
|
|
16
20
|
end
|
|
17
21
|
|
|
22
|
+
# Gets the TRX balance for a given address
|
|
23
|
+
#
|
|
24
|
+
# @param address [String] the TRON address to check
|
|
25
|
+
# @return [String] the TRX balance as a formatted string
|
|
26
|
+
# @raise [ArgumentError] if the address is invalid
|
|
18
27
|
def get_trx(address)
|
|
19
28
|
validate_address!(address)
|
|
20
29
|
|
|
@@ -61,6 +70,12 @@ module Tron
|
|
|
61
70
|
result
|
|
62
71
|
end
|
|
63
72
|
|
|
73
|
+
# Gets all TRC20 token balances for a given address
|
|
74
|
+
#
|
|
75
|
+
# @param address [String] the TRON address to check
|
|
76
|
+
# @param strict [Boolean] whether to enable strict validation
|
|
77
|
+
# @return [Array<Hash>] an array of token information
|
|
78
|
+
# @raise [ArgumentError] if the address is invalid
|
|
64
79
|
def get_trc20_tokens(address, strict: false)
|
|
65
80
|
validate_address!(address)
|
|
66
81
|
|
|
@@ -105,6 +120,9 @@ module Tron
|
|
|
105
120
|
result
|
|
106
121
|
end
|
|
107
122
|
|
|
123
|
+
# Gets cache statistics for the balance service
|
|
124
|
+
#
|
|
125
|
+
# @return [Hash] a hash containing cache statistics
|
|
108
126
|
def cache_stats
|
|
109
127
|
total = @cache_hits + @cache_misses
|
|
110
128
|
{
|
|
@@ -115,6 +133,7 @@ module Tron
|
|
|
115
133
|
}
|
|
116
134
|
end
|
|
117
135
|
|
|
136
|
+
# Clears the cache for the balance service
|
|
118
137
|
def clear_cache
|
|
119
138
|
@cache&.clear
|
|
120
139
|
@cache_hits = 0
|
|
@@ -123,6 +142,10 @@ module Tron
|
|
|
123
142
|
|
|
124
143
|
private
|
|
125
144
|
|
|
145
|
+
# Validates the structure of token data
|
|
146
|
+
#
|
|
147
|
+
# @param token [Hash] the token data to validate
|
|
148
|
+
# @raise [RuntimeError] if the token data is invalid
|
|
126
149
|
def validate_token_data!(token)
|
|
127
150
|
unless token.is_a?(Hash)
|
|
128
151
|
raise "Invalid token data format: expected hash"
|
|
@@ -136,6 +159,11 @@ module Tron
|
|
|
136
159
|
end
|
|
137
160
|
end
|
|
138
161
|
|
|
162
|
+
# Gets all balance information for a given address
|
|
163
|
+
#
|
|
164
|
+
# @param address [String] the TRON address to check
|
|
165
|
+
# @param strict [Boolean] whether to enable strict validation
|
|
166
|
+
# @return [Hash] a hash containing all balance information
|
|
139
167
|
def get_all(address, strict: false)
|
|
140
168
|
validate_address!(address)
|
|
141
169
|
|
|
@@ -148,10 +176,19 @@ module Tron
|
|
|
148
176
|
|
|
149
177
|
private
|
|
150
178
|
|
|
179
|
+
# Validates a TRON address
|
|
180
|
+
#
|
|
181
|
+
# @param address [String] the address to validate
|
|
182
|
+
# @raise [ArgumentError] if the address is invalid
|
|
151
183
|
def validate_address!(address)
|
|
152
184
|
raise ArgumentError, "Invalid TRON address: #{address}" unless Utils::Address.validate(address)
|
|
153
185
|
end
|
|
154
186
|
|
|
187
|
+
# Formats a raw balance with the specified number of decimals
|
|
188
|
+
#
|
|
189
|
+
# @param raw_balance [Integer, String] the raw balance value
|
|
190
|
+
# @param decimals [Integer] the number of decimal places
|
|
191
|
+
# @return [String] the formatted balance
|
|
155
192
|
def format_balance(raw_balance, decimals)
|
|
156
193
|
balance = raw_balance.to_i
|
|
157
194
|
divisor = 10 ** decimals
|
|
@@ -160,14 +197,18 @@ module Tron
|
|
|
160
197
|
"#{whole}.#{fraction.to_s.rjust(decimals, '0')}"
|
|
161
198
|
end
|
|
162
199
|
|
|
163
|
-
|
|
164
|
-
|
|
200
|
+
# Creates headers for the TRON API
|
|
201
|
+
#
|
|
202
|
+
# @return [Hash] the API headers
|
|
165
203
|
def api_headers
|
|
166
204
|
headers = { 'accept' => 'application/json' }
|
|
167
205
|
headers['TRON-PRO-API-KEY'] = @config.api_key if @config.api_key
|
|
168
206
|
headers
|
|
169
207
|
end
|
|
170
208
|
|
|
209
|
+
# Creates headers for the Tronscan API
|
|
210
|
+
#
|
|
211
|
+
# @return [Hash] the Tronscan API headers
|
|
171
212
|
def tronscan_headers
|
|
172
213
|
headers = { 'accept' => 'application/json' }
|
|
173
214
|
headers['TRON-PRO-API-KEY'] = @config.tronscan_api_key if @config.tronscan_api_key
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
require_relative '../utils/http'
|
|
2
|
+
require_relative '../utils/address'
|
|
3
|
+
require_relative '../utils/abi'
|
|
4
|
+
require_relative '../abi'
|
|
5
|
+
require_relative 'transaction'
|
|
6
|
+
|
|
7
|
+
module Tron
|
|
8
|
+
module Services
|
|
9
|
+
# The Contract service handles interactions with TRON smart contracts
|
|
10
|
+
# including both read-only calls and state-changing transactions
|
|
11
|
+
class Contract
|
|
12
|
+
# Creates a new instance of the Contract service
|
|
13
|
+
#
|
|
14
|
+
# @param configuration [Tron::Configuration] the configuration object
|
|
15
|
+
def initialize(configuration)
|
|
16
|
+
@configuration = configuration
|
|
17
|
+
@base_url = configuration.base_url
|
|
18
|
+
@transaction_service = Transaction.new(configuration)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Triggers a smart contract (state-changing operation)
|
|
22
|
+
# This creates and broadcasts a transaction to the blockchain
|
|
23
|
+
#
|
|
24
|
+
# @param contract_address [String] the contract address to interact with
|
|
25
|
+
# @param function [String] the function to call on the contract
|
|
26
|
+
# @param parameters [Array] the parameters to pass to the function
|
|
27
|
+
# @param private_key [String] the private key to sign the transaction
|
|
28
|
+
# @param fee_limit [Integer] the maximum energy fee to pay (default: configuration default)
|
|
29
|
+
# @param call_value [Integer] the amount of TRX to send with the call (default: 0)
|
|
30
|
+
# @param owner_address [String] the address of the transaction originator (default: derived from private key)
|
|
31
|
+
# @return [Hash] the transaction result
|
|
32
|
+
def trigger_contract(
|
|
33
|
+
contract_address:,
|
|
34
|
+
function:,
|
|
35
|
+
parameters: [],
|
|
36
|
+
private_key:,
|
|
37
|
+
fee_limit: nil,
|
|
38
|
+
call_value: 0,
|
|
39
|
+
owner_address: nil
|
|
40
|
+
)
|
|
41
|
+
# Use configuration default if fee_limit not provided
|
|
42
|
+
fee_limit ||= @configuration.fee_limit
|
|
43
|
+
|
|
44
|
+
# Derive owner address from private key if not provided
|
|
45
|
+
owner_address ||= derive_address_from_private_key(private_key)
|
|
46
|
+
|
|
47
|
+
# Validate addresses
|
|
48
|
+
validate_address!(contract_address)
|
|
49
|
+
validate_address!(owner_address)
|
|
50
|
+
|
|
51
|
+
# Build transaction
|
|
52
|
+
transaction = build_trigger_transaction(
|
|
53
|
+
contract_address: contract_address,
|
|
54
|
+
function: function,
|
|
55
|
+
parameters: parameters,
|
|
56
|
+
owner_address: owner_address,
|
|
57
|
+
fee_limit: fee_limit,
|
|
58
|
+
call_value: call_value
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Sign and broadcast
|
|
62
|
+
@transaction_service.sign_and_broadcast(transaction, private_key)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Calls a smart contract (read-only operation)
|
|
66
|
+
# This does not create a transaction and doesn't change blockchain state
|
|
67
|
+
#
|
|
68
|
+
# @param contract_address [String] the contract address to interact with
|
|
69
|
+
# @param function [String] the function to call on the contract
|
|
70
|
+
# @param parameters [Array] the parameters to pass to the function
|
|
71
|
+
# @param owner_address [String] the address of the caller (default: configuration default)
|
|
72
|
+
# @return [Hash] the result of the function call
|
|
73
|
+
def call_contract(
|
|
74
|
+
contract_address:,
|
|
75
|
+
function:,
|
|
76
|
+
parameters: [],
|
|
77
|
+
owner_address: nil
|
|
78
|
+
)
|
|
79
|
+
# Use a default address if not provided
|
|
80
|
+
owner_address ||= @configuration.default_address || 'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb'
|
|
81
|
+
|
|
82
|
+
# Validate address
|
|
83
|
+
validate_address!(contract_address)
|
|
84
|
+
|
|
85
|
+
# Build request
|
|
86
|
+
endpoint = "#{@base_url}/wallet/triggerconstantcontract"
|
|
87
|
+
|
|
88
|
+
# Encode function call
|
|
89
|
+
encoded_data = Utils::ABI.encode_function_call(function, parameters)
|
|
90
|
+
|
|
91
|
+
# Use 'data' field instead of 'function_selector' (same as trigger_contract)
|
|
92
|
+
payload = {
|
|
93
|
+
contract_address: Utils::Address.to_hex(contract_address),
|
|
94
|
+
data: encoded_data,
|
|
95
|
+
owner_address: Utils::Address.to_hex(owner_address)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Make API call
|
|
99
|
+
response = Utils::HTTP.post(endpoint, payload, {
|
|
100
|
+
endpoint_type: :contract_call,
|
|
101
|
+
ttl: 60 # Cache for 1 minute
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
# Parse response
|
|
105
|
+
parse_constant_result(response)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Checks if a payment has been processed (example helper)
|
|
109
|
+
#
|
|
110
|
+
# @param contract_address [String] the contract address
|
|
111
|
+
# @param operator_address [String] the operator's address
|
|
112
|
+
# @param payment_id [String] the payment ID
|
|
113
|
+
# @return [Boolean] true if the payment is processed, false otherwise
|
|
114
|
+
def payment_processed?(contract_address, operator_address, payment_id)
|
|
115
|
+
result = call_contract(
|
|
116
|
+
contract_address: contract_address,
|
|
117
|
+
function: 'isPaymentProcessed(address,bytes16)',
|
|
118
|
+
parameters: [operator_address, payment_id]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
Utils::ABI.decode_output('bool', result)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Gets the fee destination (example helper)
|
|
125
|
+
#
|
|
126
|
+
# @param contract_address [String] the contract address
|
|
127
|
+
# @param operator_address [String] the operator's address
|
|
128
|
+
# @return [String] the fee destination address
|
|
129
|
+
def get_fee_destination(contract_address, operator_address)
|
|
130
|
+
result = call_contract(
|
|
131
|
+
contract_address: contract_address,
|
|
132
|
+
function: 'getFeeDestination(address)',
|
|
133
|
+
parameters: [operator_address]
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
Utils::ABI.decode_output('address', result)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Checks if an operator is registered (example helper)
|
|
140
|
+
#
|
|
141
|
+
# @param contract_address [String] the contract address
|
|
142
|
+
# @param operator_address [String] the operator's address
|
|
143
|
+
# @return [Boolean] true if the operator is registered, false otherwise
|
|
144
|
+
def operator_registered?(contract_address, operator_address)
|
|
145
|
+
result = call_contract(
|
|
146
|
+
contract_address: contract_address,
|
|
147
|
+
function: 'isOperatorRegistered(address)',
|
|
148
|
+
parameters: [operator_address]
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
Utils::ABI.decode_output('bool', result)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
# Builds a trigger transaction for a smart contract call
|
|
157
|
+
#
|
|
158
|
+
# @param contract_address [String] the contract address
|
|
159
|
+
# @param function [String] the function to call
|
|
160
|
+
# @param parameters [Array] the parameters to pass
|
|
161
|
+
# @param owner_address [String] the address of the transaction owner
|
|
162
|
+
# @param fee_limit [Integer] the maximum energy fee
|
|
163
|
+
# @param call_value [Integer] the value to send with the call
|
|
164
|
+
# @return [Hash] the transaction object
|
|
165
|
+
def build_trigger_transaction(
|
|
166
|
+
contract_address:,
|
|
167
|
+
function:,
|
|
168
|
+
parameters:,
|
|
169
|
+
owner_address:,
|
|
170
|
+
fee_limit:,
|
|
171
|
+
call_value:
|
|
172
|
+
)
|
|
173
|
+
endpoint = "#{@base_url}/wallet/triggersmartcontract"
|
|
174
|
+
|
|
175
|
+
# Encode function call (selector + parameters)
|
|
176
|
+
encoded_data = Utils::ABI.encode_function_call(function, parameters)
|
|
177
|
+
|
|
178
|
+
# IMPORTANT: Use 'data' field, not 'function_selector'
|
|
179
|
+
# When function_selector is provided, the API expects it to be the signature string
|
|
180
|
+
# with parameters in a separate 'parameter' field.
|
|
181
|
+
# Using 'data' allows us to send the complete encoded call data.
|
|
182
|
+
payload = {
|
|
183
|
+
contract_address: Utils::Address.to_hex(contract_address),
|
|
184
|
+
data: encoded_data,
|
|
185
|
+
fee_limit: fee_limit,
|
|
186
|
+
call_value: call_value,
|
|
187
|
+
owner_address: Utils::Address.to_hex(owner_address)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Get transaction from API
|
|
191
|
+
response = Utils::HTTP.post(endpoint, payload)
|
|
192
|
+
|
|
193
|
+
raise "Failed to create transaction: #{response}" unless response['result']
|
|
194
|
+
|
|
195
|
+
response['transaction']
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Parses the result of a constant function call
|
|
199
|
+
#
|
|
200
|
+
# @param response [Hash] the API response
|
|
201
|
+
# @return [String] the parsed result
|
|
202
|
+
def parse_constant_result(response)
|
|
203
|
+
return nil unless response['result']
|
|
204
|
+
|
|
205
|
+
# Extract constant result (hex string)
|
|
206
|
+
constant_result = response['constant_result']
|
|
207
|
+
return nil if constant_result.nil? || constant_result.empty?
|
|
208
|
+
|
|
209
|
+
constant_result.first
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Validates a TRON address
|
|
213
|
+
#
|
|
214
|
+
# @param address [String] the address to validate
|
|
215
|
+
# @raise [ArgumentError] if the address is invalid
|
|
216
|
+
def validate_address!(address)
|
|
217
|
+
require_relative '../utils/address'
|
|
218
|
+
raise ArgumentError, "Invalid TRON address: #{address}" unless Utils::Address.validate(address)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Derives a TRON address from a private key
|
|
222
|
+
#
|
|
223
|
+
# @param private_key [String] the private key in hex format
|
|
224
|
+
# @return [String] the derived address
|
|
225
|
+
def derive_address_from_private_key(private_key)
|
|
226
|
+
# Use the Key class to derive address from private key
|
|
227
|
+
key = Tron::Key.new(priv: private_key)
|
|
228
|
+
key.address
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|