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.
@@ -3,6 +3,7 @@
3
3
  require "base64"
4
4
  require "openssl"
5
5
  require "time"
6
+ require "uri"
6
7
 
7
8
  module GasfreeSdk
8
9
  # Client for interacting with the GasFree API
@@ -17,45 +18,50 @@ module GasfreeSdk
17
18
  f.request :retry, GasfreeSdk.config.retry_options
18
19
  f.response :json
19
20
  f.adapter Faraday.default_adapter
21
+ f.response :logger, ::Logger.new($stdout), bodies: true if ENV["DEBUG_GASFREE_SDK"]
20
22
  end
21
23
  end
22
24
 
23
25
  # Get all supported tokens
24
26
  # @return [Array<Token>] List of supported tokens
25
27
  def tokens
26
- response = get("/api/v1/config/token/all")
27
- response.dig("data", "tokens").map { |token| Models::Token.new(token) }
28
+ response = get("api/v1/config/token/all")
29
+ response.dig("data", "tokens").map do |token|
30
+ Models::Token.new(transform_token_data(token))
31
+ end
28
32
  end
29
33
 
30
34
  # Get all service providers
31
35
  # @return [Array<Provider>] List of service providers
32
36
  def providers
33
- response = get("/api/v1/config/provider/all")
34
- response.dig("data", "providers").map { |provider| Models::Provider.new(provider) }
37
+ response = get("api/v1/config/provider/all")
38
+ response.dig("data", "providers").map do |provider|
39
+ Models::Provider.new(transform_provider_data(provider))
40
+ end
35
41
  end
36
42
 
37
43
  # Get GasFree account info
38
44
  # @param account_address [String] The user's EOA address
39
45
  # @return [GasFreeAddress] The GasFree account info
40
46
  def address(account_address)
41
- response = get("/api/v1/address/#{account_address}")
42
- Models::GasFreeAddress.new(response["data"])
47
+ response = get("api/v1/address/#{account_address}")
48
+ Models::GasFreeAddress.new(transform_address_data(response["data"]))
43
49
  end
44
50
 
45
51
  # Submit a GasFree transfer
46
52
  # @param request [TransferRequest] The transfer request
47
53
  # @return [TransferResponse] The transfer response
48
54
  def submit_transfer(request)
49
- response = post("/api/v1/gasfree/submit", request.to_h)
50
- Models::TransferResponse.new(response["data"])
55
+ response = post("api/v1/gasfree/submit", transform_transfer_request_data(request.to_h))
56
+ Models::TransferResponse.new(transform_transfer_response_data(response["data"]))
51
57
  end
52
58
 
53
59
  # Get transfer status
54
60
  # @param trace_id [String] The transfer trace ID
55
61
  # @return [TransferResponse] The transfer status
56
62
  def transfer_status(trace_id)
57
- response = get("/api/v1/gasfree/#{trace_id}")
58
- Models::TransferResponse.new(response["data"])
63
+ response = get("api/v1/gasfree/#{trace_id}")
64
+ Models::TransferResponse.new(transform_transfer_response_data(response["data"]))
59
65
  end
60
66
 
61
67
  private
@@ -67,7 +73,7 @@ module GasfreeSdk
67
73
  def get(path, params = {})
68
74
  timestamp = Time.now.to_i
69
75
  response = connection.get(path, params) do |req|
70
- sign_request(req, "GET", path, timestamp)
76
+ sign_request(req, "GET", normalize_path(path), timestamp)
71
77
  end
72
78
  handle_response(response)
73
79
  end
@@ -80,23 +86,40 @@ module GasfreeSdk
80
86
  timestamp = Time.now.to_i
81
87
  response = connection.post(path) do |req|
82
88
  req.body = body
83
- sign_request(req, "POST", path, timestamp)
89
+ sign_request(req, "POST", normalize_path(path), timestamp)
84
90
  end
85
91
  handle_response(response)
86
92
  end
87
93
 
94
+ # Normalize path for signature calculation (needs leading slash)
95
+ # @param path [String] The API path
96
+ # @return [String] Normalized path
97
+ def normalize_path(path)
98
+ path.start_with?("/") ? path : "/#{path}"
99
+ end
100
+
88
101
  # Sign an API request
89
102
  # @param request [Faraday::Request] The request to sign
90
103
  # @param method [String] HTTP method
91
104
  # @param path [String] Request path
92
105
  # @param timestamp [Integer] Request timestamp
93
- def sign_request(request, method, path, timestamp)
106
+ def sign_request(request, method, path, timestamp) # rubocop:disable Metrics/AbcSize
94
107
  api_key = GasfreeSdk.config.api_key
95
108
  api_secret = GasfreeSdk.config.api_secret
96
109
 
97
110
  raise AuthenticationError, "API key and secret are required" if api_key.nil? || api_secret.nil?
98
111
 
99
- message = "#{method}#{path}#{timestamp}"
112
+ # Extract the base path from the endpoint URL
113
+ # For https://open-test.gasfree.io/nile/ the base path is /nile
114
+ endpoint_uri = URI(GasfreeSdk.config.api_endpoint)
115
+ base_path = endpoint_uri.path.chomp("/") # Remove trailing slash
116
+
117
+ # Combine base path with API path for signature
118
+ # e.g., /nile + /api/v1/config/token/all = /nile/api/v1/config/token/all
119
+ full_path = "#{base_path}#{path}"
120
+
121
+ # According to GasFree API documentation, the message format is: method + path + timestamp
122
+ message = "#{method}#{full_path}#{timestamp}"
100
123
  signature = Base64.strict_encode64(
101
124
  OpenSSL::HMAC.digest("SHA256", api_secret, message)
102
125
  )
@@ -120,5 +143,152 @@ module GasfreeSdk
120
143
  message: data["message"]
121
144
  )
122
145
  end
146
+
147
+ # Transform transfer request data from model format to API format
148
+ # @param request_data [Hash] Transfer request data from model
149
+ # @return [Hash] Transformed transfer request data for API
150
+ def transform_transfer_request_data(request_data)
151
+ {
152
+ "token" => request_data[:token],
153
+ "serviceProvider" => request_data[:service_provider], # snake_case to camelCase
154
+ "user" => request_data[:user],
155
+ "receiver" => request_data[:receiver],
156
+ "value" => request_data[:value],
157
+ "maxFee" => request_data[:max_fee], # snake_case to camelCase
158
+ "deadline" => request_data[:deadline],
159
+ "version" => request_data[:version],
160
+ "nonce" => request_data[:nonce],
161
+ "sig" => request_data[:sig]
162
+ }
163
+ end
164
+
165
+ # Transform token data from API format to model format
166
+ # @param token_data [Hash] Token data from API
167
+ # @return [Hash] Transformed token data
168
+ def transform_token_data(token_data)
169
+ {
170
+ token_address: token_data["tokenAddress"],
171
+ created_at: Types::JSON::Time.call(token_data["createdAt"]),
172
+ updated_at: Types::JSON::Time.call(token_data["updatedAt"]),
173
+ activate_fee: Types::JSON::Amount.call(token_data["activateFee"]),
174
+ transfer_fee: Types::JSON::Amount.call(token_data["transferFee"]),
175
+ supported: token_data["supported"],
176
+ symbol: token_data["symbol"],
177
+ decimal: token_data["decimal"]
178
+ }
179
+ end
180
+
181
+ # Transform provider data from API format to model format
182
+ # @param provider_data [Hash] Provider data from API
183
+ # @return [Hash] Transformed provider data
184
+ def transform_provider_data(provider_data)
185
+ {
186
+ address: provider_data["address"],
187
+ name: provider_data["name"],
188
+ icon: provider_data["icon"],
189
+ website: provider_data["website"],
190
+ config: transform_provider_config_data(provider_data["config"])
191
+ }
192
+ end
193
+
194
+ # Transform provider config data from API format to model format
195
+ # @param config_data [Hash] Provider config data from API
196
+ # @return [Hash] Transformed provider config data
197
+ def transform_provider_config_data(config_data)
198
+ {
199
+ max_pending_transfer: config_data["maxPendingTransfer"],
200
+ min_deadline_duration: config_data["minDeadlineDuration"],
201
+ max_deadline_duration: config_data["maxDeadlineDuration"],
202
+ default_deadline_duration: config_data["defaultDeadlineDuration"]
203
+ }
204
+ end
205
+
206
+ # Transform address data from API format to model format
207
+ # @param address_data [Hash] Address data from API
208
+ # @return [Hash] Transformed address data
209
+ def transform_address_data(address_data)
210
+ {
211
+ account_address: address_data["accountAddress"],
212
+ gas_free_address: address_data["gasFreeAddress"],
213
+ active: address_data["active"],
214
+ nonce: address_data["nonce"],
215
+ allow_submit: address_data["allowSubmit"],
216
+ assets: address_data["assets"]&.map { |asset| transform_asset_data(asset) } || []
217
+ }
218
+ end
219
+
220
+ # Transform asset data from API format to model format
221
+ # @param asset_data [Hash] Asset data from API
222
+ # @return [Hash] Transformed asset data
223
+ def transform_asset_data(asset_data)
224
+ {
225
+ token_address: asset_data["tokenAddress"],
226
+ token_symbol: asset_data["tokenSymbol"],
227
+ activate_fee: Types::JSON::Amount.call(asset_data["activateFee"]),
228
+ transfer_fee: Types::JSON::Amount.call(asset_data["transferFee"]),
229
+ decimal: asset_data["decimal"],
230
+ frozen: Types::JSON::Amount.call(asset_data["frozen"])
231
+ }
232
+ end
233
+
234
+ # Transform transfer response data from API format to model format
235
+ # @param response_data [Hash] Transfer response data from API
236
+ # @return [Hash] Transformed transfer response data
237
+ def transform_transfer_response_data(response_data)
238
+ basic_fields = extract_basic_transfer_fields(response_data)
239
+ transaction_fields = extract_transaction_fields(response_data)
240
+
241
+ basic_fields.merge(transaction_fields).compact
242
+ end
243
+
244
+ # Extract basic transfer fields from response data
245
+ # @param response_data [Hash] Transfer response data from API
246
+ # @return [Hash] Basic transfer fields
247
+ def extract_basic_transfer_fields(response_data)
248
+ {
249
+ id: response_data["id"],
250
+ created_at: get_field_value(response_data, "createdAt", "created_at"),
251
+ updated_at: get_field_value(response_data, "updatedAt", "updated_at"),
252
+ account_address: get_field_value(response_data, "accountAddress", "account_address"),
253
+ gas_free_address: get_field_value(response_data, "gasFreeAddress", "gas_free_address"),
254
+ provider_address: get_field_value(response_data, "providerAddress", "provider_address"),
255
+ target_address: get_field_value(response_data, "targetAddress", "target_address"),
256
+ token_address: get_field_value(response_data, "tokenAddress", "token_address"),
257
+ amount: response_data["amount"],
258
+ max_fee: get_field_value(response_data, "maxFee", "max_fee"),
259
+ signature: response_data["signature"],
260
+ nonce: response_data["nonce"],
261
+ expired_at: get_field_value(response_data, "expiredAt", "expired_at"),
262
+ state: response_data["state"]
263
+ }
264
+ end
265
+
266
+ # Extract transaction-related fields from response data
267
+ # @param response_data [Hash] Transfer response data from API
268
+ # @return [Hash] Transaction fields
269
+ def extract_transaction_fields(response_data)
270
+ {
271
+ estimated_activate_fee: get_field_value(response_data, "estimatedActivateFee", "estimated_activate_fee"),
272
+ estimated_transfer_fee: get_field_value(response_data, "estimatedTransferFee", "estimated_transfer_fee"),
273
+ txn_hash: get_field_value(response_data, "txnHash", "txn_hash"),
274
+ txn_block_num: get_field_value(response_data, "txnBlockNum", "txn_block_num"),
275
+ txn_block_timestamp: get_field_value(response_data, "txnBlockTimestamp", "txn_block_timestamp"),
276
+ txn_state: get_field_value(response_data, "txnState", "txn_state"),
277
+ txn_activate_fee: get_field_value(response_data, "txnActivateFee", "txn_activate_fee"),
278
+ txn_transfer_fee: get_field_value(response_data, "txnTransferFee", "txn_transfer_fee"),
279
+ txn_total_fee: get_field_value(response_data, "txnTotalFee", "txn_total_fee"),
280
+ txn_amount: get_field_value(response_data, "txnAmount", "txn_amount"),
281
+ txn_total_cost: get_field_value(response_data, "txnTotalCost", "txn_total_cost")
282
+ }
283
+ end
284
+
285
+ # Get field value with fallback to snake_case version
286
+ # @param data [Hash] The data hash
287
+ # @param camel_case_key [String] The camelCase key
288
+ # @param snake_case_key [String] The snake_case key
289
+ # @return [Object] The field value
290
+ def get_field_value(data, camel_case_key, snake_case_key)
291
+ data[camel_case_key] || data[snake_case_key]
292
+ end
123
293
  end
124
294
  end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module GasfreeSdk
6
+ module Crypto
7
+ # Keccak256 implementation for TRON EIP-712 signatures
8
+ class Keccak256 < Digest::Class
9
+ PILN = [10, 7, 11, 17, 18, 3, 5, 16,
10
+ 8, 21, 24, 4, 15, 23, 19, 13,
11
+ 12, 2, 20, 14, 22, 9, 6, 1].freeze
12
+
13
+ ROTC = [1, 3, 6, 10, 15, 21, 28, 36,
14
+ 45, 55, 2, 14, 27, 41, 56, 8,
15
+ 25, 43, 62, 18, 39, 61, 20, 44].freeze
16
+
17
+ RNDC = [0x0000000000000001, 0x0000000000008082, 0x800000000000808a,
18
+ 0x8000000080008000, 0x000000000000808b, 0x0000000080000001,
19
+ 0x8000000080008081, 0x8000000000008009, 0x000000000000008a,
20
+ 0x0000000000000088, 0x0000000080008009, 0x000000008000000a,
21
+ 0x000000008000808b, 0x800000000000008b, 0x8000000000008089,
22
+ 0x8000000000008003, 0x8000000000008002, 0x8000000000000080,
23
+ 0x000000000000800a, 0x800000008000000a, 0x8000000080008081,
24
+ 0x8000000000008080, 0x0000000080000001, 0x8000000080008008].freeze
25
+
26
+ def initialize
27
+ @size = 256 / 8
28
+ @buffer = "".dup # Use dup to ensure it's not frozen
29
+
30
+ super
31
+ end
32
+
33
+ def <<(string)
34
+ @buffer << string.to_s
35
+ self
36
+ end
37
+ alias update <<
38
+
39
+ def reset
40
+ @buffer = "".dup # Create a new unfrozen string instead of clearing
41
+ self
42
+ end
43
+
44
+ def finish # rubocop:disable Metrics/AbcSize
45
+ string = Array.new 25, 0
46
+ width = 200 - (@size * 2)
47
+ padding = "\x01".dup
48
+
49
+ buffer = @buffer.dup # Create a copy to avoid modifying the original
50
+ buffer << padding << ("\0" * (width - (buffer.size % width)))
51
+ buffer[-1] = (buffer[-1].ord | 0x80).chr
52
+
53
+ 0.step buffer.size - 1, width do |j|
54
+ quads = buffer[j, width].unpack "Q*"
55
+ (width / 8).times do |i|
56
+ string[i] ^= quads[i]
57
+ end
58
+
59
+ keccak string
60
+ end
61
+
62
+ string.pack("Q*")[0, @size]
63
+ end
64
+
65
+ private
66
+
67
+ def keccak(string) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
68
+ 24.times.each_with_object [] do |round, a|
69
+ # Theta
70
+ 5.times do |i|
71
+ a[i] = string[i] ^ string[i + 5] ^ string[i + 10] ^ string[i + 15] ^ string[i + 20]
72
+ end
73
+
74
+ 5.times do |i|
75
+ t = a[(i + 4) % 5] ^ rotate(a[(i + 1) % 5], 1)
76
+ 0.step 24, 5 do |j|
77
+ string[j + i] ^= t
78
+ end
79
+ end
80
+
81
+ # Rho Pi
82
+ t = string[1]
83
+ 24.times do |i|
84
+ j = PILN[i]
85
+ a[0] = string[j]
86
+ string[j] = rotate t, ROTC[i]
87
+ t = a[0]
88
+ end
89
+
90
+ # Chi
91
+ 0.step 24, 5 do |j|
92
+ 5.times do |i|
93
+ a[i] = string[j + i]
94
+ end
95
+
96
+ 5.times do |i|
97
+ string[j + i] ^= ~a[(i + 1) % 5] & a[(i + 2) % 5]
98
+ end
99
+ end
100
+
101
+ # Iota
102
+ string[0] ^= RNDC[round]
103
+ end
104
+ end
105
+
106
+ def rotate(x, y) # rubocop:disable Naming/MethodParameterName
107
+ ((x << y) | (x >> (64 - y))) & ((1 << 64) - 1)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -12,19 +12,19 @@ module GasfreeSdk
12
12
 
13
13
  # @!attribute [r] created_at
14
14
  # @return [Time] When the token was added to GasFree
15
- attribute :created_at, Types::JSON::Time
15
+ attribute :created_at, Types::Time
16
16
 
17
17
  # @!attribute [r] updated_at
18
18
  # @return [Time] When the token was last updated
19
- attribute :updated_at, Types::JSON::Time
19
+ attribute :updated_at, Types::Time
20
20
 
21
21
  # @!attribute [r] activate_fee
22
22
  # @return [String] The activation fee in the smallest unit of the token
23
- attribute :activate_fee, Types::Amount
23
+ attribute :activate_fee, Types::String
24
24
 
25
25
  # @!attribute [r] transfer_fee
26
26
  # @return [String] The transfer fee in the smallest unit of the token
27
- attribute :transfer_fee, Types::Amount
27
+ attribute :transfer_fee, Types::String
28
28
 
29
29
  # @!attribute [r] supported
30
30
  # @return [Boolean] Whether the token is currently supported
@@ -12,51 +12,51 @@ module GasfreeSdk
12
12
 
13
13
  # @!attribute [r] created_at
14
14
  # @return [Time] When the transfer was created
15
- attribute :created_at, Types::JSON::Time
15
+ attribute? :created_at, Types::JSON::Time
16
16
 
17
17
  # @!attribute [r] updated_at
18
18
  # @return [Time] When the transfer was last updated
19
- attribute :updated_at, Types::JSON::Time
19
+ attribute? :updated_at, Types::JSON::Time
20
20
 
21
21
  # @!attribute [r] account_address
22
22
  # @return [String] The user's EOA address
23
- attribute :account_address, Types::Address
23
+ attribute? :account_address, Types::Address
24
24
 
25
25
  # @!attribute [r] gas_free_address
26
26
  # @return [String] The GasFree account address
27
- attribute :gas_free_address, Types::Address
27
+ attribute? :gas_free_address, Types::Address
28
28
 
29
29
  # @!attribute [r] provider_address
30
30
  # @return [String] The service provider's address
31
- attribute :provider_address, Types::Address
31
+ attribute? :provider_address, Types::Address
32
32
 
33
33
  # @!attribute [r] target_address
34
34
  # @return [String] The recipient's address
35
- attribute :target_address, Types::Address
35
+ attribute? :target_address, Types::Address
36
36
 
37
37
  # @!attribute [r] token_address
38
38
  # @return [String] The token contract address
39
- attribute :token_address, Types::Address
39
+ attribute? :token_address, Types::Address
40
40
 
41
41
  # @!attribute [r] amount
42
42
  # @return [String] The transfer amount
43
- attribute :amount, Types::Amount
43
+ attribute? :amount, Types::JSON::Amount
44
44
 
45
45
  # @!attribute [r] max_fee
46
46
  # @return [String] Maximum fee limit
47
- attribute :max_fee, Types::Amount
47
+ attribute? :max_fee, Types::JSON::Amount
48
48
 
49
49
  # @!attribute [r] signature
50
50
  # @return [String] User's signature
51
- attribute :signature, Types::String
51
+ attribute? :signature, Types::String
52
52
 
53
53
  # @!attribute [r] nonce
54
54
  # @return [Integer] Transfer nonce
55
- attribute :nonce, Types::Nonce
55
+ attribute? :nonce, Types::Nonce
56
56
 
57
57
  # @!attribute [r] expired_at
58
58
  # @return [Time] When the transfer expires
59
- attribute :expired_at, Types::JSON::Time
59
+ attribute? :expired_at, Types::JSON::Time
60
60
 
61
61
  # @!attribute [r] state
62
62
  # @return [String] Current transfer state
@@ -64,11 +64,11 @@ module GasfreeSdk
64
64
 
65
65
  # @!attribute [r] estimated_activate_fee
66
66
  # @return [String] Estimated activation fee
67
- attribute? :estimated_activate_fee, Types::Amount
67
+ attribute? :estimated_activate_fee, Types::JSON::Amount
68
68
 
69
69
  # @!attribute [r] estimated_transfer_fee
70
70
  # @return [String] Estimated transfer fee
71
- attribute? :estimated_transfer_fee, Types::Amount
71
+ attribute? :estimated_transfer_fee, Types::JSON::Amount
72
72
 
73
73
  # @!attribute [r] txn_hash
74
74
  # @return [String] On-chain transaction hash
@@ -90,23 +90,23 @@ module GasfreeSdk
90
90
 
91
91
  # @!attribute [r] txn_activate_fee
92
92
  # @return [String] Actual activation fee
93
- attribute? :txn_activate_fee, Types::Amount
93
+ attribute? :txn_activate_fee, Types::JSON::Amount
94
94
 
95
95
  # @!attribute [r] txn_transfer_fee
96
96
  # @return [String] Actual transfer fee
97
- attribute? :txn_transfer_fee, Types::Amount
97
+ attribute? :txn_transfer_fee, Types::JSON::Amount
98
98
 
99
99
  # @!attribute [r] txn_total_fee
100
100
  # @return [String] Total actual fee
101
- attribute? :txn_total_fee, Types::Amount
101
+ attribute? :txn_total_fee, Types::JSON::Amount
102
102
 
103
103
  # @!attribute [r] txn_amount
104
104
  # @return [String] Actual transferred amount
105
- attribute? :txn_amount, Types::Amount
105
+ attribute? :txn_amount, Types::JSON::Amount
106
106
 
107
107
  # @!attribute [r] txn_total_cost
108
108
  # @return [String] Total cost including fees
109
- attribute? :txn_total_cost, Types::Amount
109
+ attribute? :txn_total_cost, Types::JSON::Amount
110
110
  end
111
111
  end
112
112
  end