Class: GasfreeSdk::TronEIP712Signer

Inherits:
Object
  • Object
show all
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

Class Method Details

.camel_case_to_snake_case(str) ⇒ String

Convert camelCase to snake_case

Parameters:

  • str (String)

    String to convert

Returns:

  • (String)

    Converted string



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

Parameters:

  • primary_type (Symbol)

    Primary type name

  • data (Hash)

    Data to encode

  • types (Hash) (defaults to: TYPES)

    Type definitions

Returns:

  • (String)

    Encoded data



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

Parameters:

  • primary_type (Symbol)

    Primary type name

  • types (Hash) (defaults to: TYPES)

    Type definitions

Returns:

  • (String)

    Encoded type string



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

Parameters:

  • type (String)

    Value type

  • value (Object)

    Value to encode

  • types (Hash) (defaults to: TYPES)

    Type definitions

Returns:

  • (String)

    Encoded value as hex string



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

Parameters:

  • primary_type (Symbol)

    Primary type name

  • types (Hash)

    Type definitions

  • found (Set) (defaults to: Set.new)

    Already found dependencies

Returns:

  • (Set)

    Set of dependencies



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

Parameters:

  • domain (Hash) (defaults to: DOMAIN_TESTNET)

    Domain parameters

Returns:

  • (String)

    Domain hash as binary string



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

Parameters:

  • primary_type (Symbol)

    Primary type name

  • data (Hash)

    Data to hash

  • types (Hash) (defaults to: TYPES)

    Type definitions

Returns:

  • (String)

    Hash as binary string



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

Parameters:

  • data (String)

    Data to hash

Returns:

  • (String)

    Binary hash



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

Parameters:

  • data (String)

    Data to hash

Returns:

  • (String)

    Hex encoded hash



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

Parameters:

  • private_key_hex (String)

    Private key as hex string

  • message_data (Hash)

    Message data to sign

  • domain (Hash) (defaults to: DOMAIN_TESTNET)

    Domain parameters (defaults to testnet)

  • use_ethereum_v (Boolean) (defaults to: true)

    Whether to use Ethereum-style V value (recovery_id + 27)

Returns:

  • (String)

    Signature as hex string



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, message_data, 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)
  message_hash = hash_struct(:PermitTransfer, message_data)

  # Create the final hash according to EIP-712
  prefix = "\x19\x01"
  digest_input = prefix + domain_separator + message_hash
  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

Parameters:

  • private_key_hex (String)

    Private key as hex string

  • message_data (Hash)

    Message data to sign

  • use_ethereum_v (Boolean) (defaults to: true)

    Whether to use Ethereum-style V value

Returns:

  • (String)

    Signature as hex string



246
247
248
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 246

def sign_typed_data_mainnet(private_key_hex, message_data, use_ethereum_v: true)
  sign_typed_data(private_key_hex, message_data, 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)

Parameters:

  • private_key_hex (String)

    Private key as hex string

  • message_data (Hash)

    Message data to sign

  • use_ethereum_v (Boolean) (defaults to: true)

    Whether to use Ethereum-style V value

Returns:

  • (String)

    Signature as hex string



255
256
257
# File 'lib/gasfree_sdk/tron_eip712_signer.rb', line 255

def sign_typed_data_testnet(private_key_hex, message_data, use_ethereum_v: true)
  sign_typed_data(private_key_hex, message_data, domain: DOMAIN_TESTNET, use_ethereum_v: use_ethereum_v)
end

.snake_case_to_camel_case(str) ⇒ String

Convert snake_case to camelCase

Parameters:

  • str (String)

    String to convert

Returns:

  • (String)

    Converted string



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