Class: GasfreeSdk::TronEIP712Signer
- Inherits:
-
Object
- Object
- GasfreeSdk::TronEIP712Signer
- Defined in:
- lib/gasfree_sdk/tron_eip712_signer.rb
Overview
EIP-712 Signature implementation for TRON GasFree This module provides functionality to sign EIP-712 structured data according to TRON’s implementation of the standard (TIP-712)
Constant Summary collapse
- DOMAIN_TESTNET =
TRON Nile testnet parameters from GasFree documentation
{ name: "GasFreeController", version: "V1.0.0", chainId: 3_448_148_188, # TRON Nile testnet - according to GasFree documentation verifyingContract: "THQGuFzL87ZqhxkgqYEryRAd7gqFqL5rdc" }.freeze
- DOMAIN_MAINNET =
TRON Mainnet parameters
{ name: "GasFreeController", version: "V1.0.0", chainId: 728_126_428, # TRON Mainnet - according to GasFree documentation verifyingContract: "TFFAMQLZybALaLb4uxHA9RBE7pxhUAjF3U" }.freeze
- TYPES =
EIP-712 type definitions for PermitTransfer
{ PermitTransfer: [ { name: "token", type: "address" }, { name: "serviceProvider", type: "address" }, { name: "user", type: "address" }, { name: "receiver", type: "address" }, { name: "value", type: "uint256" }, { name: "maxFee", type: "uint256" }, { name: "deadline", type: "uint256" }, { name: "version", type: "uint256" }, { name: "nonce", type: "uint256" } ] }.freeze
Class Method Summary collapse
-
.camel_case_to_snake_case(str) ⇒ String
Convert camelCase to snake_case.
-
.encode_data(primary_type, data, types = TYPES) ⇒ String
Encode structured data according to EIP-712.
-
.encode_type(primary_type, types = TYPES) ⇒ String
Encode EIP-712 type definition.
-
.encode_value(type, value, types = TYPES) ⇒ String
Encode a single value according to its type.
-
.find_dependencies(primary_type, types, found = Set.new) ⇒ Set
Find type dependencies recursively.
-
.hash_domain(domain = DOMAIN_TESTNET) ⇒ String
Hash domain separator.
-
.hash_struct(primary_type, data, types = TYPES) ⇒ String
Hash structured data.
-
.keccac256(data) ⇒ String
Generate Keccak256 hash of data.
-
.keccac256_hex(data) ⇒ String
Generate Keccac256 hash of data as hex string.
-
.sign_typed_data(private_key_hex, message_data, domain: DOMAIN_TESTNET, use_ethereum_v: true) ⇒ String
Sign typed data according to EIP-712.
-
.sign_typed_data_mainnet(private_key_hex, message_data, use_ethereum_v: true) ⇒ String
Sign for mainnet.
-
.sign_typed_data_testnet(private_key_hex, message_data, use_ethereum_v: true) ⇒ String
Sign for testnet (alias for default behavior).
-
.snake_case_to_camel_case(str) ⇒ String
Convert snake_case to camelCase.
Class Method Details
.camel_case_to_snake_case(str) ⇒ String
Convert camelCase to snake_case
124 125 126 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 124 def camel_case_to_snake_case(str) str.to_s.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "") end |
.encode_data(primary_type, data, types = TYPES) ⇒ String
Encode structured data according to EIP-712
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 92 def encode_data(primary_type, data, types = TYPES) # rubocop:disable Metrics/AbcSize encoded_types = encode_type(primary_type, types) type_hash = keccac256(encoded_types) encoded_values = types[primary_type].map do |field| field_name = field[:name] # Try multiple key formats: original, symbol, snake_case conversion value = data[field_name] || data[field_name.to_sym] || data[snake_case_to_camel_case(field_name)] || data[snake_case_to_camel_case(field_name).to_sym] || data[camel_case_to_snake_case(field_name)] || data[camel_case_to_snake_case(field_name).to_sym] raise "Missing value for field '#{field_name}' in data: #{data.keys}" if value.nil? encode_value(field[:type], value, types) end.join type_hash + [encoded_values].pack("H*") end |
.encode_type(primary_type, types = TYPES) ⇒ String
Encode EIP-712 type definition
60 61 62 63 64 65 66 67 68 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 60 def encode_type(primary_type, types = TYPES) deps = find_dependencies(primary_type, types) deps.delete(primary_type) deps = [primary_type] + deps.sort deps.map do |type| "#{type}(#{types[type].map { |field| "#{field[:type]} #{field[:name]}" }.join(",")})" end.join end |
.encode_value(type, value, types = TYPES) ⇒ String
Encode a single value according to its type
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 133 def encode_value(type, value, types = TYPES) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength case type when "string" keccac256_hex(value.to_s) when "uint256" # Convert to 32-byte hex string - handle both string and integer input # For string values (like deadline), convert to integer first int_value = value.to_i int_value.to_s(16).rjust(64, "0") when "address" # TRON addresses need to be converted to hex format according to TIP-712 # TIP-712: "address: need to remove TRON unique prefix(0x41) and encoded as uint160" addr_bytes = GasfreeSdk::Base58.base58_to_binary(value.to_s) # Take the middle 20 bytes (skip version byte 0x41 and checksum) hex_addr = addr_bytes[1, 20].unpack1("H*") hex_addr.rjust(64, "0") when /\[\]$/ # Array type item_type = type.gsub(/\[\]$/, "") array_items = value.map { |item| encode_value(item_type, item, types) } keccac256_hex(array_items.join) else raise "Unknown type: #{type}" unless types[type.to_sym] # Custom type encoded_data = encode_data(type.to_sym, value, types) keccac256_hex(encoded_data) end end |
.find_dependencies(primary_type, types, found = Set.new) ⇒ Set
Find type dependencies recursively
75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 75 def find_dependencies(primary_type, types, found = Set.new) return found if found.include?(primary_type) return found unless types[primary_type] found << primary_type types[primary_type].each do |field| dep = field[:type].gsub(/\[\]$/, "") find_dependencies(dep, types, found) if types[dep] && !found.include?(dep) end found end |
.hash_domain(domain = DOMAIN_TESTNET) ⇒ String
Hash domain separator
176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 176 def hash_domain(domain = DOMAIN_TESTNET) # Create EIP712Domain type manually since it's not in TYPES eip712_domain_type = [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" } ] # Temporarily add to types for encoding temp_types = TYPES.merge({ EIP712Domain: eip712_domain_type }) hash_struct(:EIP712Domain, domain, temp_types) end |
.hash_struct(primary_type, data, types = TYPES) ⇒ String
Hash structured data
168 169 170 171 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 168 def hash_struct(primary_type, data, types = TYPES) encoded_data = encode_data(primary_type, data, types) keccac256(encoded_data) end |
.keccac256(data) ⇒ String
Generate Keccak256 hash of data
45 46 47 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 45 def keccac256(data) GasfreeSdk::Crypto::Keccak256.new.digest(data.to_s) end |
.keccac256_hex(data) ⇒ String
Generate Keccac256 hash of data as hex string
52 53 54 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 52 def keccac256_hex(data) GasfreeSdk::Crypto::Keccak256.new.hexdigest(data.to_s) end |
.sign_typed_data(private_key_hex, message_data, domain: DOMAIN_TESTNET, use_ethereum_v: true) ⇒ String
Sign typed data according to EIP-712
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 196 def sign_typed_data(private_key_hex, , domain: DOMAIN_TESTNET, use_ethereum_v: true) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength # EIP-712 signature implementation for TRON GasFree # This implementation has been verified to be mathematically correct # through local signature verification # Create the EIP-712 hash domain_separator = hash_domain(domain) = hash_struct(:PermitTransfer, ) # Create the final hash according to EIP-712 prefix = "\x19\x01" digest_input = prefix + domain_separator + digest = keccac256(digest_input) # Ensure digest is exactly 32 bytes raise "Hash is #{digest.length} bytes, expected 32" if digest.length != 32 # Sign with secp256k1 context = Secp256k1::Context.new private_key = [private_key_hex].pack("H*") key_pair = context.key_pair_from_private_key(private_key) # Create recoverable signature compact, recovery_id = context.sign_recoverable( key_pair.private_key, digest ).compact # Format signature as r + s + v signature_bytes = compact.bytes v_value = if use_ethereum_v # TRON/Ethereum style V (recovery_id + 27) recovery_id + 27 else # Standard format: just recovery_id recovery_id end signature_bytes << v_value # Convert to hex string without 0x prefix signature_bytes.pack("C*").unpack1("H*") end |
.sign_typed_data_mainnet(private_key_hex, message_data, use_ethereum_v: true) ⇒ String
Sign for mainnet
246 247 248 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 246 def sign_typed_data_mainnet(private_key_hex, , use_ethereum_v: true) sign_typed_data(private_key_hex, , domain: DOMAIN_MAINNET, use_ethereum_v: use_ethereum_v) end |
.sign_typed_data_testnet(private_key_hex, message_data, use_ethereum_v: true) ⇒ String
Sign for testnet (alias for default behavior)
255 256 257 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 255 def sign_typed_data_testnet(private_key_hex, , use_ethereum_v: true) sign_typed_data(private_key_hex, , domain: DOMAIN_TESTNET, use_ethereum_v: use_ethereum_v) end |
.snake_case_to_camel_case(str) ⇒ String
Convert snake_case to camelCase
117 118 119 |
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 117 def snake_case_to_camel_case(str) str.to_s.split("_").map.with_index { |word, i| i.zero? ? word : word.capitalize }.join end |