gasfree_sdk 0.1.0 → 1.0.0

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,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbsecp256k1"
4
+
5
+ module GasfreeSdk
6
+ # EIP-712 Signature implementation for TRON GasFree
7
+ # This module provides functionality to sign EIP-712 structured data
8
+ # according to TRON's implementation of the standard (TIP-712)
9
+ class TronEIP712Signer
10
+ # TRON Nile testnet parameters from GasFree documentation
11
+ DOMAIN_TESTNET = {
12
+ name: "GasFreeController",
13
+ version: "V1.0.0",
14
+ chainId: 3_448_148_188, # TRON Nile testnet - according to GasFree documentation
15
+ verifyingContract: "THQGuFzL87ZqhxkgqYEryRAd7gqFqL5rdc"
16
+ }.freeze
17
+
18
+ # TRON Mainnet parameters
19
+ DOMAIN_MAINNET = {
20
+ name: "GasFreeController",
21
+ version: "V1.0.0",
22
+ chainId: 728_126_428, # TRON Mainnet - according to GasFree documentation
23
+ verifyingContract: "TFFAMQLZybALaLb4uxHA9RBE7pxhUAjF3U"
24
+ }.freeze
25
+
26
+ # EIP-712 type definitions for PermitTransfer
27
+ TYPES = {
28
+ PermitTransfer: [
29
+ { name: "token", type: "address" },
30
+ { name: "serviceProvider", type: "address" },
31
+ { name: "user", type: "address" },
32
+ { name: "receiver", type: "address" },
33
+ { name: "value", type: "uint256" },
34
+ { name: "maxFee", type: "uint256" },
35
+ { name: "deadline", type: "uint256" },
36
+ { name: "version", type: "uint256" },
37
+ { name: "nonce", type: "uint256" }
38
+ ]
39
+ }.freeze
40
+
41
+ class << self
42
+ # Generate Keccak256 hash of data
43
+ # @param data [String] Data to hash
44
+ # @return [String] Binary hash
45
+ def keccac256(data)
46
+ GasfreeSdk::Crypto::Keccak256.new.digest(data.to_s)
47
+ end
48
+
49
+ # Generate Keccac256 hash of data as hex string
50
+ # @param data [String] Data to hash
51
+ # @return [String] Hex encoded hash
52
+ def keccac256_hex(data)
53
+ GasfreeSdk::Crypto::Keccak256.new.hexdigest(data.to_s)
54
+ end
55
+
56
+ # Encode EIP-712 type definition
57
+ # @param primary_type [Symbol] Primary type name
58
+ # @param types [Hash] Type definitions
59
+ # @return [String] Encoded type string
60
+ def encode_type(primary_type, types = TYPES)
61
+ deps = find_dependencies(primary_type, types)
62
+ deps.delete(primary_type)
63
+ deps = [primary_type] + deps.sort
64
+
65
+ deps.map do |type|
66
+ "#{type}(#{types[type].map { |field| "#{field[:type]} #{field[:name]}" }.join(",")})"
67
+ end.join
68
+ end
69
+
70
+ # Find type dependencies recursively
71
+ # @param primary_type [Symbol] Primary type name
72
+ # @param types [Hash] Type definitions
73
+ # @param found [Set] Already found dependencies
74
+ # @return [Set] Set of dependencies
75
+ def find_dependencies(primary_type, types, found = Set.new)
76
+ return found if found.include?(primary_type)
77
+ return found unless types[primary_type]
78
+
79
+ found << primary_type
80
+ types[primary_type].each do |field|
81
+ dep = field[:type].gsub(/\[\]$/, "")
82
+ find_dependencies(dep, types, found) if types[dep] && !found.include?(dep)
83
+ end
84
+ found
85
+ end
86
+
87
+ # Encode structured data according to EIP-712
88
+ # @param primary_type [Symbol] Primary type name
89
+ # @param data [Hash] Data to encode
90
+ # @param types [Hash] Type definitions
91
+ # @return [String] Encoded data
92
+ def encode_data(primary_type, data, types = TYPES) # rubocop:disable Metrics/AbcSize
93
+ encoded_types = encode_type(primary_type, types)
94
+ type_hash = keccac256(encoded_types)
95
+
96
+ encoded_values = types[primary_type].map do |field|
97
+ field_name = field[:name]
98
+ # Try multiple key formats: original, symbol, snake_case conversion
99
+ value = data[field_name] ||
100
+ data[field_name.to_sym] ||
101
+ data[snake_case_to_camel_case(field_name)] ||
102
+ data[snake_case_to_camel_case(field_name).to_sym] ||
103
+ data[camel_case_to_snake_case(field_name)] ||
104
+ data[camel_case_to_snake_case(field_name).to_sym]
105
+
106
+ raise "Missing value for field '#{field_name}' in data: #{data.keys}" if value.nil?
107
+
108
+ encode_value(field[:type], value, types)
109
+ end.join
110
+
111
+ type_hash + [encoded_values].pack("H*")
112
+ end
113
+
114
+ # Convert snake_case to camelCase
115
+ # @param str [String] String to convert
116
+ # @return [String] Converted string
117
+ def snake_case_to_camel_case(str)
118
+ str.to_s.split("_").map.with_index { |word, i| i.zero? ? word : word.capitalize }.join
119
+ end
120
+
121
+ # Convert camelCase to snake_case
122
+ # @param str [String] String to convert
123
+ # @return [String] Converted string
124
+ def camel_case_to_snake_case(str)
125
+ str.to_s.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "")
126
+ end
127
+
128
+ # Encode a single value according to its type
129
+ # @param type [String] Value type
130
+ # @param value [Object] Value to encode
131
+ # @param types [Hash] Type definitions
132
+ # @return [String] Encoded value as hex string
133
+ def encode_value(type, value, types = TYPES) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
134
+ case type
135
+ when "string"
136
+ keccac256_hex(value.to_s)
137
+ when "uint256"
138
+ # Convert to 32-byte hex string - handle both string and integer input
139
+ # For string values (like deadline), convert to integer first
140
+ int_value = value.to_i
141
+ int_value.to_s(16).rjust(64, "0")
142
+ when "address"
143
+ # TRON addresses need to be converted to hex format according to TIP-712
144
+ # TIP-712: "address: need to remove TRON unique prefix(0x41) and encoded as uint160"
145
+ addr_bytes = GasfreeSdk::Base58.base58_to_binary(value.to_s)
146
+ # Take the middle 20 bytes (skip version byte 0x41 and checksum)
147
+ hex_addr = addr_bytes[1, 20].unpack1("H*")
148
+ hex_addr.rjust(64, "0")
149
+ when /\[\]$/
150
+ # Array type
151
+ item_type = type.gsub(/\[\]$/, "")
152
+ array_items = value.map { |item| encode_value(item_type, item, types) }
153
+ keccac256_hex(array_items.join)
154
+ else
155
+ raise "Unknown type: #{type}" unless types[type.to_sym]
156
+
157
+ # Custom type
158
+ encoded_data = encode_data(type.to_sym, value, types)
159
+ keccac256_hex(encoded_data)
160
+ end
161
+ end
162
+
163
+ # Hash structured data
164
+ # @param primary_type [Symbol] Primary type name
165
+ # @param data [Hash] Data to hash
166
+ # @param types [Hash] Type definitions
167
+ # @return [String] Hash as binary string
168
+ def hash_struct(primary_type, data, types = TYPES)
169
+ encoded_data = encode_data(primary_type, data, types)
170
+ keccac256(encoded_data)
171
+ end
172
+
173
+ # Hash domain separator
174
+ # @param domain [Hash] Domain parameters
175
+ # @return [String] Domain hash as binary string
176
+ def hash_domain(domain = DOMAIN_TESTNET)
177
+ # Create EIP712Domain type manually since it's not in TYPES
178
+ eip712_domain_type = [
179
+ { name: "name", type: "string" },
180
+ { name: "version", type: "string" },
181
+ { name: "chainId", type: "uint256" },
182
+ { name: "verifyingContract", type: "address" }
183
+ ]
184
+
185
+ # Temporarily add to types for encoding
186
+ temp_types = TYPES.merge({ EIP712Domain: eip712_domain_type })
187
+ hash_struct(:EIP712Domain, domain, temp_types)
188
+ end
189
+
190
+ # Sign typed data according to EIP-712
191
+ # @param private_key_hex [String] Private key as hex string
192
+ # @param message_data [Hash] Message data to sign
193
+ # @param domain [Hash] Domain parameters (defaults to testnet)
194
+ # @param use_ethereum_v [Boolean] Whether to use Ethereum-style V value (recovery_id + 27)
195
+ # @return [String] Signature as hex string
196
+ def sign_typed_data(private_key_hex, message_data, domain: DOMAIN_TESTNET, use_ethereum_v: true) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
197
+ # EIP-712 signature implementation for TRON GasFree
198
+ # This implementation has been verified to be mathematically correct
199
+ # through local signature verification
200
+
201
+ # Create the EIP-712 hash
202
+ domain_separator = hash_domain(domain)
203
+ message_hash = hash_struct(:PermitTransfer, message_data)
204
+
205
+ # Create the final hash according to EIP-712
206
+ prefix = "\x19\x01"
207
+ digest_input = prefix + domain_separator + message_hash
208
+ digest = keccac256(digest_input)
209
+
210
+ # Ensure digest is exactly 32 bytes
211
+ raise "Hash is #{digest.length} bytes, expected 32" if digest.length != 32
212
+
213
+ # Sign with secp256k1
214
+ context = Secp256k1::Context.new
215
+ private_key = [private_key_hex].pack("H*")
216
+ key_pair = context.key_pair_from_private_key(private_key)
217
+
218
+ # Create recoverable signature
219
+ compact, recovery_id = context.sign_recoverable(
220
+ key_pair.private_key,
221
+ digest
222
+ ).compact
223
+
224
+ # Format signature as r + s + v
225
+ signature_bytes = compact.bytes
226
+
227
+ v_value = if use_ethereum_v
228
+ # TRON/Ethereum style V (recovery_id + 27)
229
+ recovery_id + 27
230
+ else
231
+ # Standard format: just recovery_id
232
+ recovery_id
233
+ end
234
+
235
+ signature_bytes << v_value
236
+
237
+ # Convert to hex string without 0x prefix
238
+ signature_bytes.pack("C*").unpack1("H*")
239
+ end
240
+
241
+ # Sign for mainnet
242
+ # @param private_key_hex [String] Private key as hex string
243
+ # @param message_data [Hash] Message data to sign
244
+ # @param use_ethereum_v [Boolean] Whether to use Ethereum-style V value
245
+ # @return [String] Signature as hex string
246
+ def sign_typed_data_mainnet(private_key_hex, message_data, use_ethereum_v: true)
247
+ sign_typed_data(private_key_hex, message_data, domain: DOMAIN_MAINNET, use_ethereum_v: use_ethereum_v)
248
+ end
249
+
250
+ # Sign for testnet (alias for default behavior)
251
+ # @param private_key_hex [String] Private key as hex string
252
+ # @param message_data [Hash] Message data to sign
253
+ # @param use_ethereum_v [Boolean] Whether to use Ethereum-style V value
254
+ # @return [String] Signature as hex string
255
+ def sign_typed_data_testnet(private_key_hex, message_data, use_ethereum_v: true)
256
+ sign_typed_data(private_key_hex, message_data, domain: DOMAIN_TESTNET, use_ethereum_v: use_ethereum_v)
257
+ end
258
+ end
259
+ end
260
+ end
@@ -7,8 +7,12 @@ module GasfreeSdk
7
7
  module Types
8
8
  include Dry.Types()
9
9
 
10
- # Address type with validation
11
- Address = Types::String.constrained(format: /\A(0x)?[0-9a-fA-F]{40}\z/)
10
+ # Address type with validation for both Ethereum and TRON addresses
11
+ # Ethereum: 0x followed by 40 hex characters OR just 40 hex characters
12
+ # TRON: Starts with T followed by 33 characters (base58)
13
+ Address = Types::String.constrained(
14
+ format: /\A(0x[0-9a-fA-F]{40}|[0-9a-fA-F]{40}|T[0-9A-Za-z]{33})\z/
15
+ )
12
16
 
13
17
  # Hash type with validation
14
18
  Hash = Types::String.constrained(format: /\A(0x)?[0-9a-fA-F]{64}\z/)
@@ -36,5 +40,24 @@ module GasfreeSdk
36
40
  SUCCEED
37
41
  FAILED
38
42
  ])
43
+
44
+ # JSON namespace for API response parsing
45
+ module JSON
46
+ # Convert millisecond timestamp or ISO date string to Time object
47
+ # Takes an integer (milliseconds) or string (ISO format) and returns a Time
48
+ Time = Types.Constructor(::Time) do |value|
49
+ case value
50
+ when ::Integer
51
+ ::Time.at(value / 1000.0)
52
+ when ::String
53
+ ::Time.parse(value)
54
+ else
55
+ raise ArgumentError, "Expected Integer or String, got #{value.class}"
56
+ end
57
+ end
58
+
59
+ # Convert integer amount to string
60
+ Amount = Types.Constructor(Types::String, &:to_s)
61
+ end
39
62
  end
40
63
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GasfreeSdk
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/gasfree_sdk.rb CHANGED
@@ -13,6 +13,9 @@ require_relative "gasfree_sdk/types"
13
13
  require_relative "gasfree_sdk/client"
14
14
  require_relative "gasfree_sdk/errors"
15
15
  require_relative "gasfree_sdk/models"
16
+ require_relative "gasfree_sdk/crypto"
17
+ require_relative "gasfree_sdk/base58"
18
+ require_relative "gasfree_sdk/tron_eip712_signer"
16
19
 
17
20
  # Main module for GasFree SDK
18
21
  module GasfreeSdk
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gasfree_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - madmatvey
@@ -107,6 +107,20 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '2.2'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rbsecp256k1
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '6.0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '6.0'
110
124
  - !ruby/object:Gem::Dependency
111
125
  name: rake
112
126
  requirement: !ruby/object:Gem::Requirement
@@ -236,8 +250,12 @@ files:
236
250
  - Rakefile
237
251
  - examples/basic_usage.rb
238
252
  - examples/demo.rb
253
+ - examples/simple_usage_example.rb
254
+ - examples/test_tron_signer.rb
239
255
  - lib/gasfree_sdk.rb
256
+ - lib/gasfree_sdk/base58.rb
240
257
  - lib/gasfree_sdk/client.rb
258
+ - lib/gasfree_sdk/crypto.rb
241
259
  - lib/gasfree_sdk/errors.rb
242
260
  - lib/gasfree_sdk/models.rb
243
261
  - lib/gasfree_sdk/models/gas_free_address.rb
@@ -245,12 +263,13 @@ files:
245
263
  - lib/gasfree_sdk/models/token.rb
246
264
  - lib/gasfree_sdk/models/transfer_request.rb
247
265
  - lib/gasfree_sdk/models/transfer_response.rb
266
+ - lib/gasfree_sdk/tron_eip712_signer.rb
248
267
  - lib/gasfree_sdk/types.rb
249
268
  - lib/gasfree_sdk/version.rb
250
269
  - sig/gasfree_sdk.rbs
251
270
  homepage: https://github.com/madmatvey/gasfree_sdk
252
271
  licenses:
253
- - MIT
272
+ - LGPL-3.0
254
273
  metadata:
255
274
  allowed_push_host: https://rubygems.org
256
275
  homepage_uri: https://github.com/madmatvey/gasfree_sdk