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
data/lib/tron/services/price.rb
CHANGED
|
@@ -5,7 +5,11 @@ require_relative '../utils/rate_limiter'
|
|
|
5
5
|
|
|
6
6
|
module Tron
|
|
7
7
|
module Services
|
|
8
|
+
# The Price service handles retrieving token price information for TRON tokens
|
|
8
9
|
class Price
|
|
10
|
+
# Creates a new instance of the Price service
|
|
11
|
+
#
|
|
12
|
+
# @param config [Tron::Configuration] the configuration object
|
|
9
13
|
def initialize(config)
|
|
10
14
|
@config = config
|
|
11
15
|
@cache = Utils::Cache.new(max_age: config.cache_ttl) if config.cache_enabled
|
|
@@ -14,6 +18,10 @@ module Tron
|
|
|
14
18
|
@cache_misses = 0
|
|
15
19
|
end
|
|
16
20
|
|
|
21
|
+
# Gets the price information for a specific token
|
|
22
|
+
#
|
|
23
|
+
# @param token [String] the token symbol (default: 'trx')
|
|
24
|
+
# @return [Hash] the token price information
|
|
17
25
|
def get_token_price(token = 'trx')
|
|
18
26
|
cache_key = cache_key_for(token)
|
|
19
27
|
|
|
@@ -57,6 +65,9 @@ module Tron
|
|
|
57
65
|
end
|
|
58
66
|
end
|
|
59
67
|
|
|
68
|
+
# Gets price information for all available tokens
|
|
69
|
+
#
|
|
70
|
+
# @return [Hash] the price information for all tokens
|
|
60
71
|
def get_all_prices
|
|
61
72
|
url = "#{@config.tronscan_base_url}/api/getAssetWithPriceList"
|
|
62
73
|
headers = tronscan_headers
|
|
@@ -78,6 +89,10 @@ module Tron
|
|
|
78
89
|
end
|
|
79
90
|
end
|
|
80
91
|
|
|
92
|
+
# Gets the USD price for a specific token
|
|
93
|
+
#
|
|
94
|
+
# @param token [String] the token symbol
|
|
95
|
+
# @return [Float] the token price in USD, or nil if unavailable
|
|
81
96
|
def get_token_price_usd(token)
|
|
82
97
|
cache_key = "#{cache_key_for(token)}:usd"
|
|
83
98
|
|
|
@@ -105,6 +120,11 @@ module Tron
|
|
|
105
120
|
result
|
|
106
121
|
end
|
|
107
122
|
|
|
123
|
+
# Calculates the USD value for a given token balance
|
|
124
|
+
#
|
|
125
|
+
# @param balance [String, Numeric] the token balance
|
|
126
|
+
# @param token [String] the token symbol
|
|
127
|
+
# @return [Float] the USD value, or nil if unavailable
|
|
108
128
|
def get_token_value_usd(balance, token)
|
|
109
129
|
price = get_token_price_usd(token)
|
|
110
130
|
if price
|
|
@@ -115,6 +135,10 @@ module Tron
|
|
|
115
135
|
end
|
|
116
136
|
end
|
|
117
137
|
|
|
138
|
+
# Gets prices for multiple tokens at once
|
|
139
|
+
#
|
|
140
|
+
# @param tokens [Array<String>] an array of token symbols
|
|
141
|
+
# @return [Hash] a hash mapping token symbols to their prices
|
|
118
142
|
def get_multiple_token_prices(tokens)
|
|
119
143
|
prices = {}
|
|
120
144
|
tokens.each_with_index do |token, index|
|
|
@@ -134,6 +158,11 @@ module Tron
|
|
|
134
158
|
prices
|
|
135
159
|
end
|
|
136
160
|
|
|
161
|
+
# Formats a price value for display
|
|
162
|
+
#
|
|
163
|
+
# @param price [Float, nil] the price value
|
|
164
|
+
# @param currency [String] the currency symbol (default: 'USD')
|
|
165
|
+
# @return [String] the formatted price
|
|
137
166
|
def format_price(price, currency = 'USD')
|
|
138
167
|
if price.nil?
|
|
139
168
|
'(price unavailable)'
|
|
@@ -146,6 +175,9 @@ module Tron
|
|
|
146
175
|
end
|
|
147
176
|
end
|
|
148
177
|
|
|
178
|
+
# Gets cache statistics for the price service
|
|
179
|
+
#
|
|
180
|
+
# @return [Hash] a hash containing cache statistics
|
|
149
181
|
def cache_stats
|
|
150
182
|
total = @cache_hits + @cache_misses
|
|
151
183
|
{
|
|
@@ -156,6 +188,7 @@ module Tron
|
|
|
156
188
|
}
|
|
157
189
|
end
|
|
158
190
|
|
|
191
|
+
# Clears the cache for the price service
|
|
159
192
|
def clear_cache
|
|
160
193
|
@cache&.clear
|
|
161
194
|
@cache_hits = 0
|
|
@@ -164,10 +197,17 @@ module Tron
|
|
|
164
197
|
|
|
165
198
|
private
|
|
166
199
|
|
|
200
|
+
# Generates a cache key for a specific token
|
|
201
|
+
#
|
|
202
|
+
# @param token [String] the token symbol
|
|
203
|
+
# @return [String] the cache key
|
|
167
204
|
def cache_key_for(token)
|
|
168
205
|
"price:#{token.downcase}:#{@config.network}"
|
|
169
206
|
end
|
|
170
207
|
|
|
208
|
+
# Creates headers for the Tronscan API
|
|
209
|
+
#
|
|
210
|
+
# @return [Hash] the Tronscan API headers
|
|
171
211
|
def tronscan_headers
|
|
172
212
|
headers = { 'accept' => 'application/json' }
|
|
173
213
|
headers['TRON-PRO-API-KEY'] = @config.tronscan_api_key if @config.tronscan_api_key
|
|
@@ -4,11 +4,20 @@ require_relative '../utils/address'
|
|
|
4
4
|
|
|
5
5
|
module Tron
|
|
6
6
|
module Services
|
|
7
|
+
# The Resources service handles retrieving account resources (bandwidth, energy, storage) for TRON addresses
|
|
7
8
|
class Resources
|
|
9
|
+
# Creates a new instance of the Resources service
|
|
10
|
+
#
|
|
11
|
+
# @param config [Tron::Configuration] the configuration object
|
|
8
12
|
def initialize(config)
|
|
9
13
|
@config = config
|
|
10
14
|
end
|
|
11
15
|
|
|
16
|
+
# Gets the account resources for a given address
|
|
17
|
+
#
|
|
18
|
+
# @param address [String] the TRON address to check
|
|
19
|
+
# @return [Hash] a hash containing resource information
|
|
20
|
+
# @raise [ArgumentError] if the address is invalid
|
|
12
21
|
def get(address)
|
|
13
22
|
validate_address!(address)
|
|
14
23
|
|
|
@@ -23,6 +32,10 @@ module Tron
|
|
|
23
32
|
|
|
24
33
|
private
|
|
25
34
|
|
|
35
|
+
# Gets account resources using the getaccountresource endpoint
|
|
36
|
+
#
|
|
37
|
+
# @param address [String] the TRON address to check
|
|
38
|
+
# @return [Hash] a hash containing resource information
|
|
26
39
|
def get_account_resources(address)
|
|
27
40
|
url = "#{@config.base_url}/wallet/getaccountresource"
|
|
28
41
|
headers = api_headers
|
|
@@ -68,6 +81,10 @@ module Tron
|
|
|
68
81
|
}
|
|
69
82
|
end
|
|
70
83
|
|
|
84
|
+
# Gets account resources using the getaccount endpoint as a fallback
|
|
85
|
+
#
|
|
86
|
+
# @param address [String] the TRON address to check
|
|
87
|
+
# @return [Hash] a hash containing resource information
|
|
71
88
|
def get_account_resources_fallback(address)
|
|
72
89
|
url = "#{@config.base_url}/wallet/getaccount"
|
|
73
90
|
headers = api_headers
|
|
@@ -100,16 +117,26 @@ module Tron
|
|
|
100
117
|
}
|
|
101
118
|
end
|
|
102
119
|
|
|
120
|
+
# Validates a TRON address
|
|
121
|
+
#
|
|
122
|
+
# @param address [String] the address to validate
|
|
123
|
+
# @raise [ArgumentError] if the address is invalid
|
|
103
124
|
def validate_address!(address)
|
|
104
125
|
raise ArgumentError, "Invalid TRON address: #{address}" unless Utils::Address.validate(address)
|
|
105
126
|
end
|
|
106
127
|
|
|
128
|
+
# Creates headers for the TRON API
|
|
129
|
+
#
|
|
130
|
+
# @return [Hash] the API headers
|
|
107
131
|
def api_headers
|
|
108
132
|
headers = { 'accept' => 'application/json', 'Content-Type' => 'application/json' }
|
|
109
133
|
headers['TRON-PRO-API-KEY'] = @config.api_key if @config.api_key
|
|
110
134
|
headers
|
|
111
135
|
end
|
|
112
136
|
|
|
137
|
+
# Returns default resource values
|
|
138
|
+
#
|
|
139
|
+
# @return [Hash] a hash with zero resource values
|
|
113
140
|
def default_resources
|
|
114
141
|
{
|
|
115
142
|
bandwidth: 0,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require_relative '../utils/http'
|
|
2
|
+
require_relative '../key'
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Tron
|
|
6
|
+
module Services
|
|
7
|
+
# The Transaction service handles signing and broadcasting of TRON transactions
|
|
8
|
+
class Transaction
|
|
9
|
+
# Creates a new instance of the Transaction service
|
|
10
|
+
#
|
|
11
|
+
# @param configuration [Tron::Configuration] the configuration object
|
|
12
|
+
def initialize(configuration)
|
|
13
|
+
@configuration = configuration
|
|
14
|
+
@base_url = configuration.base_url
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Signs and broadcasts a transaction
|
|
18
|
+
#
|
|
19
|
+
# @param transaction [Hash] the transaction to sign and broadcast
|
|
20
|
+
# @param private_key [String] the private key to sign the transaction with
|
|
21
|
+
# @param local_signing [Boolean] whether to sign locally (default: true)
|
|
22
|
+
# @return [Hash] the response from the broadcast
|
|
23
|
+
def sign_and_broadcast(transaction, private_key, local_signing: true)
|
|
24
|
+
if local_signing
|
|
25
|
+
signed_tx = sign_transaction_locally(transaction, private_key)
|
|
26
|
+
else
|
|
27
|
+
signed_tx = sign_transaction_via_api(transaction, private_key)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
broadcast_transaction(signed_tx)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Signs a transaction locally using the private key
|
|
36
|
+
#
|
|
37
|
+
# IMPORTANT: TRON uses SHA256 for transaction hashing, NOT Keccak256
|
|
38
|
+
# The txID provided by TronGrid API is already the correct SHA256 hash
|
|
39
|
+
# of the properly protobuf-serialized raw_data
|
|
40
|
+
#
|
|
41
|
+
# @param transaction [Hash] the transaction to sign (must have 'txID' field)
|
|
42
|
+
# @param private_key [String] the private key in hex format
|
|
43
|
+
# @return [Hash] the signed transaction
|
|
44
|
+
# @raise [ArgumentError] if transaction doesn't have txID field
|
|
45
|
+
def sign_transaction_locally(transaction, private_key)
|
|
46
|
+
# Create a key instance with the provided private key
|
|
47
|
+
key = Key.new(priv: private_key)
|
|
48
|
+
|
|
49
|
+
# Ensure transaction has txID from TronGrid API
|
|
50
|
+
unless transaction['txID']
|
|
51
|
+
raise ArgumentError, "Transaction must have 'txID' field. Create transaction via TronGrid API first."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The txID is the SHA256 hash of the protobuf-serialized raw_data
|
|
55
|
+
# Convert from hex string to binary for signing
|
|
56
|
+
tx_hash = Tron::Utils::Crypto.hex_to_bin(transaction['txID'])
|
|
57
|
+
|
|
58
|
+
# Sign the transaction hash locally
|
|
59
|
+
# SECURITY: Private key never leaves this machine!
|
|
60
|
+
signature = key.sign(tx_hash)
|
|
61
|
+
|
|
62
|
+
# Add the signature to the transaction
|
|
63
|
+
transaction['signature'] = [signature]
|
|
64
|
+
|
|
65
|
+
transaction
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Signs a transaction via the API (legacy method)
|
|
69
|
+
#
|
|
70
|
+
# @param transaction [Hash] the transaction to sign
|
|
71
|
+
# @param private_key [String] the private key in hex format
|
|
72
|
+
# @return [Hash] the signed transaction from the API
|
|
73
|
+
def sign_transaction_via_api(transaction, private_key)
|
|
74
|
+
# Legacy API-based signing
|
|
75
|
+
endpoint = "#{@base_url}/wallet/gettransactionsign"
|
|
76
|
+
payload = {
|
|
77
|
+
transaction: transaction,
|
|
78
|
+
private_key: private_key
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Utils::HTTP.post(endpoint, payload)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Broadcasts a signed transaction to the TRON network
|
|
85
|
+
#
|
|
86
|
+
# @param signed_transaction [Hash] the signed transaction to broadcast
|
|
87
|
+
# @return [Hash] the response from the broadcast
|
|
88
|
+
# @raise [RuntimeError] if the transaction fails to broadcast
|
|
89
|
+
def broadcast_transaction(signed_transaction)
|
|
90
|
+
endpoint = "#{@base_url}/wallet/broadcasttransaction"
|
|
91
|
+
|
|
92
|
+
response = Utils::HTTP.post(endpoint, signed_transaction)
|
|
93
|
+
|
|
94
|
+
# Check if the transaction was successful
|
|
95
|
+
unless response['result']
|
|
96
|
+
error = response['Error'] || response['error'] || 'Unknown error'
|
|
97
|
+
raise "Transaction failed: #{error}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
response
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tron
|
|
4
|
+
# Module for handling TRON signature operations
|
|
5
|
+
module Signature
|
|
6
|
+
# Custom error class for signature-related errors
|
|
7
|
+
class SignatureError < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Prefix byte for TRON signed messages
|
|
10
|
+
PREFIX_BYTE = "\x19".freeze
|
|
11
|
+
|
|
12
|
+
# Prefixes a message according to the TRON signed message format
|
|
13
|
+
# This format is used in personal_sign operations
|
|
14
|
+
#
|
|
15
|
+
# @param message [String] the message to prefix
|
|
16
|
+
# @return [String] the prefixed message ready for signing
|
|
17
|
+
def self.prefix_message(message)
|
|
18
|
+
"#{PREFIX_BYTE}Tron Signed Message:\n#{message.size}#{message}"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
|
|
3
|
+
module Tron
|
|
4
|
+
module Utils
|
|
5
|
+
# Utility class for handling TRON contract ABIs
|
|
6
|
+
class ABI
|
|
7
|
+
# Type mappings for Solidity to Ruby
|
|
8
|
+
SOLIDITY_TYPES = {
|
|
9
|
+
'address' => :address,
|
|
10
|
+
'uint256' => :uint256,
|
|
11
|
+
'uint' => :uint256,
|
|
12
|
+
'bool' => :bool,
|
|
13
|
+
'bytes16' => :bytes16,
|
|
14
|
+
'bytes32' => :bytes32,
|
|
15
|
+
'string' => :string
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# Parse function signature
|
|
19
|
+
#
|
|
20
|
+
# @param signature [String] the function signature to parse
|
|
21
|
+
# @return [Hash] a hash with :name and :params keys
|
|
22
|
+
# @raise [ArgumentError] if the signature is invalid
|
|
23
|
+
def self.parse_signature(signature)
|
|
24
|
+
match = signature.match(/^(\w+)\((.*)\)$/)
|
|
25
|
+
raise ArgumentError, "Invalid function signature: #{signature}" unless match
|
|
26
|
+
|
|
27
|
+
{
|
|
28
|
+
name: match[1],
|
|
29
|
+
params: match[2].empty? ? [] : match[2].split(',').map(&:strip)
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Encode function call
|
|
34
|
+
#
|
|
35
|
+
# @param signature [String] the function signature
|
|
36
|
+
# @param parameters [Array] the parameter values
|
|
37
|
+
# @return [String] the encoded function call
|
|
38
|
+
def self.encode_function_call(signature, parameters = [])
|
|
39
|
+
parsed = parse_signature(signature)
|
|
40
|
+
|
|
41
|
+
# Get function selector (first 4 bytes of keccak256 hash)
|
|
42
|
+
selector = function_selector(signature)
|
|
43
|
+
|
|
44
|
+
# Encode parameters
|
|
45
|
+
encoded_params = encode_parameters(parsed[:params], parameters)
|
|
46
|
+
|
|
47
|
+
selector + encoded_params
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Function selector (4-byte hash)
|
|
51
|
+
#
|
|
52
|
+
# @param signature [String] the function signature
|
|
53
|
+
# @return [String] the 4-byte function selector as hex
|
|
54
|
+
def self.function_selector(signature)
|
|
55
|
+
require_relative 'crypto'
|
|
56
|
+
# Note: TRON uses same ABI as Ethereum
|
|
57
|
+
# Using Keccak256 for hash
|
|
58
|
+
hash = Crypto.keccak256(signature)
|
|
59
|
+
# Take only the first 4 bytes (8 hex chars)
|
|
60
|
+
Crypto.bin_to_hex(hash[0, 4])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Encode parameters
|
|
64
|
+
#
|
|
65
|
+
# @param types [Array<String>] the parameter types
|
|
66
|
+
# @param values [Array] the parameter values
|
|
67
|
+
# @return [String] the encoded parameters as hex
|
|
68
|
+
# @raise [ArgumentError] if there's a mismatch between types and values
|
|
69
|
+
def self.encode_parameters(types, values)
|
|
70
|
+
raise ArgumentError, "Types and values length mismatch" if types.length != values.length
|
|
71
|
+
|
|
72
|
+
encoded_parts = []
|
|
73
|
+
dynamic_params = []
|
|
74
|
+
dynamic_offset = types.length * 32 # Each static param takes 32 bytes (64 hex chars)
|
|
75
|
+
|
|
76
|
+
types.each_with_index do |type, index|
|
|
77
|
+
value = values[index]
|
|
78
|
+
|
|
79
|
+
case type
|
|
80
|
+
when 'address'
|
|
81
|
+
# Address is padded to 32 bytes (64 hex chars)
|
|
82
|
+
padded_address = encode_address(value)
|
|
83
|
+
encoded_parts << padded_address
|
|
84
|
+
when 'uint256', 'uint'
|
|
85
|
+
# Convert to hex and pad to 32 bytes (64 hex chars)
|
|
86
|
+
hex_value = encode_uint256(value)
|
|
87
|
+
encoded_parts << hex_value
|
|
88
|
+
when 'bool'
|
|
89
|
+
# Boolean to uint256 (1 for true, 0 for false)
|
|
90
|
+
encoded_parts << encode_bool(value)
|
|
91
|
+
when /^bytes(\d+)$/
|
|
92
|
+
# Static, fixed-size bytes array, pad to 32 bytes
|
|
93
|
+
encoded_parts << encode_bytes($1.to_i, value)
|
|
94
|
+
when 'string', 'bytes'
|
|
95
|
+
# For dynamic types, add offset and store actual data separately
|
|
96
|
+
encoded_parts << dynamic_offset.to_s(16).rjust(64, '0') # offset
|
|
97
|
+
# Calculate the actual data for later encoding
|
|
98
|
+
dynamic_data = encode_dynamic_parameter(type, value)
|
|
99
|
+
dynamic_params << dynamic_data
|
|
100
|
+
dynamic_offset += (dynamic_data.length / 2.0).ceil # Increase offset by byte length, rounded up to nearest whole byte
|
|
101
|
+
else
|
|
102
|
+
raise ArgumentError, "Unsupported ABI type: #{type}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Encode dynamic parameters
|
|
107
|
+
dynamic_parts = []
|
|
108
|
+
dynamic_params.each do |data|
|
|
109
|
+
dynamic_parts << data
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
(encoded_parts + dynamic_parts).join
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Encode a single value
|
|
116
|
+
#
|
|
117
|
+
# @param type [String] the type to encode
|
|
118
|
+
# @param value [Object] the value to encode
|
|
119
|
+
# @return [String] the encoded value as hex
|
|
120
|
+
# @raise [ArgumentError] if the type is unsupported
|
|
121
|
+
def self.encode_value(type, value)
|
|
122
|
+
case type
|
|
123
|
+
when 'address'
|
|
124
|
+
encode_address(value)
|
|
125
|
+
when 'uint256', 'uint'
|
|
126
|
+
encode_uint256(value)
|
|
127
|
+
when 'bool'
|
|
128
|
+
encode_bool(value)
|
|
129
|
+
when /^bytes(\d+)$/
|
|
130
|
+
encode_bytes($1.to_i, value)
|
|
131
|
+
when 'string'
|
|
132
|
+
encode_string(value)
|
|
133
|
+
else
|
|
134
|
+
raise ArgumentError, "Unsupported type: #{type}"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Encode TRON address (T address to hex)
|
|
139
|
+
#
|
|
140
|
+
# @param address [String] the TRON address to encode
|
|
141
|
+
# @return [String] the encoded address as hex
|
|
142
|
+
def self.encode_address(address)
|
|
143
|
+
# Convert TRON T-address to hex address
|
|
144
|
+
# Remove 'T' prefix, convert base58 to hex, pad to 32 bytes
|
|
145
|
+
hex = Address.to_hex(address)
|
|
146
|
+
# Strip the 41 prefix for ABI encoding (only use the 20-byte address)
|
|
147
|
+
hex = hex[2..-1] if hex.start_with?('41')
|
|
148
|
+
hex.rjust(64, '0') # Pad to 64 hex chars (32 bytes)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Encode uint256
|
|
152
|
+
#
|
|
153
|
+
# @param value [Integer, String] the value to encode
|
|
154
|
+
# @return [String] the encoded uint256 as hex
|
|
155
|
+
def self.encode_uint256(value)
|
|
156
|
+
value.to_i.to_s(16).rjust(64, '0')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Encode bool
|
|
160
|
+
#
|
|
161
|
+
# @param value [Boolean] the boolean value to encode
|
|
162
|
+
# @return [String] the encoded boolean as hex
|
|
163
|
+
def self.encode_bool(value)
|
|
164
|
+
value ? '1'.rjust(64, '0') : '0'.rjust(64, '0')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Encode bytes
|
|
168
|
+
#
|
|
169
|
+
# @param size [Integer] the size of the bytes type
|
|
170
|
+
# @param value [String] the value to encode
|
|
171
|
+
# @return [String] the encoded bytes as hex
|
|
172
|
+
def self.encode_bytes(size, value)
|
|
173
|
+
hex = value.is_a?(String) ? value.unpack1('H*') : value.to_s
|
|
174
|
+
# Limit to the size of the bytes type and pad to 32 bytes
|
|
175
|
+
hex_part = hex[0...(size * 2)] # Each byte is 2 hex chars
|
|
176
|
+
hex_part.ljust(64, '0')
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Encode string
|
|
180
|
+
#
|
|
181
|
+
# @param value [String] the string to encode
|
|
182
|
+
# @raise [NotImplementedError] since string encoding is handled separately
|
|
183
|
+
def self.encode_string(value)
|
|
184
|
+
# For dynamic types like string, return the length and data separately
|
|
185
|
+
# This will be handled by the main encode_parameters method
|
|
186
|
+
raise NotImplementedError, "String encoding is handled via encode_dynamic_parameter"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Encode bytes (dynamic type)
|
|
190
|
+
#
|
|
191
|
+
# @param value [String] the bytes value to encode
|
|
192
|
+
# @return [String] the encoded dynamic bytes as hex
|
|
193
|
+
def self.encode_bytes_dynamic(value)
|
|
194
|
+
# For dynamic bytes, encode length and data
|
|
195
|
+
bytes_data = value.is_a?(String) ? value : value.to_s
|
|
196
|
+
bytes_array = bytes_data.start_with?('0x') ? bytes_data[2..-1].scan(/../) : bytes_data.scan(/../)
|
|
197
|
+
length = bytes_array.length.to_s(16).rjust(64, '0')
|
|
198
|
+
data = bytes_array.map { |b| b.rjust(2, '0') }.join
|
|
199
|
+
# Pad data to 32-byte boundaries (64 hex chars)
|
|
200
|
+
padded_length = ((data.length / 64.0).ceil * 64).to_i
|
|
201
|
+
padded_data = data.ljust(padded_length, '0')
|
|
202
|
+
|
|
203
|
+
length + padded_data
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Decode output
|
|
207
|
+
#
|
|
208
|
+
# @param type [String] the type to decode
|
|
209
|
+
# @param hex_data [String] the hex data to decode
|
|
210
|
+
# @return [Object] the decoded value
|
|
211
|
+
def self.decode_output(type, hex_data)
|
|
212
|
+
case type
|
|
213
|
+
when 'bool'
|
|
214
|
+
hex_data.to_i(16) != 0
|
|
215
|
+
when 'address'
|
|
216
|
+
Address.from_hex(hex_data)
|
|
217
|
+
when 'uint256', 'uint'
|
|
218
|
+
hex_data.to_i(16)
|
|
219
|
+
else
|
|
220
|
+
hex_data
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Decodes parameters based on ABI types
|
|
225
|
+
#
|
|
226
|
+
# @param types [Array<String>] the types to decode
|
|
227
|
+
# @param data [String] the hex data to decode
|
|
228
|
+
# @return [Array] the decoded values
|
|
229
|
+
# @raise [ArgumentError] if the data is invalid or type is unsupported
|
|
230
|
+
def self.decode_parameters(types, data)
|
|
231
|
+
# Remove '0x' prefix if present
|
|
232
|
+
raw_data = data.start_with?('0x') ? data[2..-1] : data
|
|
233
|
+
|
|
234
|
+
# Ensure the data length is valid
|
|
235
|
+
if raw_data.length % 64 != 0
|
|
236
|
+
raise ArgumentError, "Invalid data length: must be multiple of 64 hex chars"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
values = []
|
|
240
|
+
pos = 0
|
|
241
|
+
|
|
242
|
+
types.each do |type|
|
|
243
|
+
# Each parameter is 32 bytes (64 hex chars)
|
|
244
|
+
param_hex = raw_data[pos...(pos + 64)]
|
|
245
|
+
pos += 64
|
|
246
|
+
|
|
247
|
+
case type
|
|
248
|
+
when /address/
|
|
249
|
+
# Extract the address (last 40 hex chars)
|
|
250
|
+
addr_hex = param_hex[-40..-1]
|
|
251
|
+
# Add TRON prefix
|
|
252
|
+
values << "41#{addr_hex}"
|
|
253
|
+
when /uint/, /int/
|
|
254
|
+
# Convert hex to integer
|
|
255
|
+
values << param_hex.to_i(16)
|
|
256
|
+
when /bool/
|
|
257
|
+
# Boolean is 0 or 1
|
|
258
|
+
values << (param_hex.to_i(16) != 0)
|
|
259
|
+
when /string|bytes/
|
|
260
|
+
# Dynamic type - get offset first
|
|
261
|
+
offset = param_hex.to_i(16) * 2 # Convert to byte position in hex string
|
|
262
|
+
|
|
263
|
+
# Extract length of data
|
|
264
|
+
length_hex = raw_data[offset...(offset + 64)]
|
|
265
|
+
length = length_hex.to_i(16) * 2 # Each byte is 2 hex chars
|
|
266
|
+
|
|
267
|
+
# Extract actual data
|
|
268
|
+
actual_data = raw_data[(offset + 64)...(offset + 64 + length)]
|
|
269
|
+
|
|
270
|
+
if type.start_with?('string')
|
|
271
|
+
# Convert hex to string
|
|
272
|
+
values << [actual_data].pack('H*')
|
|
273
|
+
else
|
|
274
|
+
# Return hex string for bytes
|
|
275
|
+
values << actual_data
|
|
276
|
+
end
|
|
277
|
+
else
|
|
278
|
+
raise ArgumentError, "Unsupported ABI type for decoding: #{type}"
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
values
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
# Helper method to encode dynamic parameters
|
|
288
|
+
#
|
|
289
|
+
# @param type [String] the dynamic type ('string' or 'bytes')
|
|
290
|
+
# @param value [Object] the value to encode
|
|
291
|
+
# @return [String] the encoded dynamic parameter as hex
|
|
292
|
+
# @raise [ArgumentError] if the type is unsupported
|
|
293
|
+
def self.encode_dynamic_parameter(type, value)
|
|
294
|
+
if type.start_with?('string')
|
|
295
|
+
# For strings, we need to encode length and data
|
|
296
|
+
str_bytes = value.bytes
|
|
297
|
+
length = str_bytes.length.to_s(16).rjust(64, '0')
|
|
298
|
+
data = str_bytes.map { |b| b.to_s(16).rjust(2, '0') }.join
|
|
299
|
+
# Pad data to 32-byte boundaries (64 hex chars)
|
|
300
|
+
padded_length = ((data.length / 64.0).ceil * 64).to_i
|
|
301
|
+
padded_data = data.ljust(padded_length, '0')
|
|
302
|
+
|
|
303
|
+
length + padded_data
|
|
304
|
+
elsif type.start_with?('bytes')
|
|
305
|
+
# For bytes, similar to string
|
|
306
|
+
bytes_data = value.is_a?(String) ? value : value.to_s
|
|
307
|
+
bytes_array = bytes_data.start_with?('0x') ? bytes_data[2..-1].scan(/../) : bytes_data.scan(/../)
|
|
308
|
+
length = bytes_array.length.to_s(16).rjust(64, '0')
|
|
309
|
+
data = bytes_array.map { |b| b.rjust(2, '0') }.join
|
|
310
|
+
# Pad data to 32-byte boundaries (64 hex chars)
|
|
311
|
+
padded_length = ((data.length / 64.0).ceil * 64).to_i
|
|
312
|
+
padded_data = data.ljust(padded_length, '0')
|
|
313
|
+
|
|
314
|
+
length + padded_data
|
|
315
|
+
else
|
|
316
|
+
raise ArgumentError, "Unsupported dynamic type: #{type}"
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|