1

Hoping for some help as this one has me baffled...

I created a user account and API credentials at FTX.com.

They have an interesting Auth setup which is detailed here: https://docs.ftx.com/?python#authentication

They only provide code examples for python, javascript and c#, but I need to implement the integration on a RoR app.

Here's a link which also provides an example for both GET and POST calls: https://blog.ftx.com/blog/api-authentication/

I'm using:

ruby '3.0.1'

gem 'rails', '~> 6.1.4', '>= 6.1.4.1'

also,

require 'uri'
require 'net/https'
require 'net/http'
require 'json'

I got the authentication working for GET calls as follows:

def get_market
 get_market_url = 'https://ftx.com/api/markets/BTC-PERP/orderbook?depth=20'

 api_get_call(get_market_url)
end
def api_get_call(url)
    ts = (Time.now.to_f * 1000).to_i

    signature_payload = "#{ts}GET/api/markets"

    key = ENV['FTX_API_SECRET']
    data = signature_payload
    digest = OpenSSL::Digest.new('sha256')

    signature = OpenSSL::HMAC.hexdigest(digest, key, data)

    headers = {
      'FTX-KEY': ENV['FTX_API_KEY'],
      'FTX-SIGN': signature,
      'FTX-TS': ts.to_s
    }
    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.read_timeout = 1200
    http.use_ssl = true
    rsp = http.get(uri, headers)
    JSON.parse(rsp.body)
  end

This works great and I get the correct response:

=>
{"success"=>true,
"result"=>
{"bids"=>
[[64326.0, 2.0309],
...
[64303.0, 3.1067]],
"asks"=>
[[64327.0, 4.647],
...
[64352.0, 0.01]]}}

However, I can't seem to authenticate correctly for POST calls (even though as far as I can tell I am following the instructions correctly). I use the following:

  def create_subaccount
    create_subaccount_url = 'https://ftx.com/api/subaccounts'

    call_body =
      {
        "nickname": "sub2",
      }.to_json

    api_post_call(create_subaccount_url, call_body)
  end
  def api_post_call(url, body)
    ts = (Time.now.to_f * 1000).to_i

    signature_payload = "#{ts}POST/api/subaccounts#{body}"

    key = ENV['FTX_API_SECRET']
    data = signature_payload
    digest = OpenSSL::Digest.new('sha256')

    signature = OpenSSL::HMAC.hexdigest(digest, key, data)

    headers = {
      'FTX-KEY': ENV['FTX_API_KEY'],
      'FTX-SIGN': signature,
      'FTX-TS': ts.to_s
    }

    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.read_timeout = 1200
    http.use_ssl = true
    request = Net::HTTP::Post.new(uri, headers)
    request.body = body
    response = http.request(request)

    JSON.parse(response.body)
  end

Also tried passing headers via request[] directly:

  def api_post_call(url, body)
    ts = (Time.now.to_f * 1000).to_i

    signature_payload = "#{ts}POST/api/subaccounts#{body}"

    key = ENV['FTX_API_SECRET']
    data = signature_payload
    digest = OpenSSL::Digest.new('sha256')

    signature = OpenSSL::HMAC.hexdigest(digest, key, data)

    uri = URI.parse(url)
    http = Net::HTTP.new(uri.host, uri.port)
    http.read_timeout = 1200
    http.use_ssl = true
    request = Net::HTTP::Post.new(uri)
    request['FTX-KEY'] = ENV['FTX_API_KEY']
    request['FTX-SIGN'] = signature
    request['FTX-TS'] = ts.to_s
    request.body = body
    response = http.request(request)

    JSON.parse(response.body)
  end

This is the error response: => {"success"=>false, "error"=>"Not logged in: Invalid signature"}

My feeling is the issue is somewhere in adding the body to signature_payload before generating the signature via HMAC here..?: signature_payload = "#{ts}POST/api/subaccounts#{body}"

Thinking this because, if I leave out #{body} here, like so: signature_payload = "#{ts}POST/api/subaccounts" the response is: => {"success"=>false, "error"=>"Missing parameter nickname"}

I have tried several iterations of setting up the POST call method using various different net/https examples but have had no luck... I have also contacted FTX support but have had no response.

Would truly appreciate if anyone has some insight on what I am doing wrong here?

  • A word of advice is to use a HTTP lib like HTTParty or Faraday and separate this out into a class with methods that just do one thing. Net::HTTP is the worst part of the ruby standard library and is so clunky to work that doing HTTPS is borderline machochism. – max Nov 14 '21 at 16:22
  • I think the key problem here is that you're sending the headers in the request body as form data and not as actual headers. Net::HTTP still doesn't have a way to pass a hash of headers so you have to set them one by one. `headers.each {|k,v| request[k] = v }`. Like I said its clunky. – max Nov 14 '21 at 16:26
  • @max Thanks for the advice, tried both: `request['FTX-KEY'] = ENV['FTX_API_KEY']` + `request['FTX-SIGN'] = signature` + `request['FTX-TS'] = ts.to_s` and also, `headers.each { |k, v| request[k] = v }` but still no luck... – joergenhorse Nov 14 '21 at 16:40
  • Thinking its not a headers issue given the above attempts... – joergenhorse Nov 14 '21 at 16:49
  • I think it definitely is an issue based on the docs - it might not be the only issue though. – max Nov 14 '21 at 16:52
  • You might want to start by splitting this into separate methods and using `ENV.fetch('FTX_API_SECRET')` which will raise and tell you if its a configuration issue so you're not spending hours on a simple nil error. – max Nov 14 '21 at 16:58
  • Okay... so, what confused me was that I thought there was Auth on the GET call. But seemingly this is not the case. You can just call the end-point without any Auth: https://ftx.com/api/markets/BTC-PERP/orderbook?depth=20 So I was never able to authenticate in the 1st place... so the issue could be anywhere... Most prob in the signature generation... :( – joergenhorse Nov 14 '21 at 17:05
  • I'm not sure how to interpret this "of the following **four** strings". That could mean that you should be doing `signature = OpenSSL::HMAC.hexdigest(digest, key, signature_payload, body)` – max Nov 14 '21 at 17:15

2 Answers2

0

try this headers

headers = {
      'FTX-KEY': ENV['FTX_API_KEY'],
      'FTX-SIGN': signature,
      'FTX-TS': ts.to_s,
      'Content-Type' => 'application/json',
      'Accepts' => 'application/json',
    }
  • Remember that Stack Overflow isn't just intended to solve the immediate problem, but also to help future readers find solutions to similar problems, which requires understanding the underlying code. This is especially important for members of our community who are beginners, and not familiar with the syntax. Given that, can you edit your answer to include an explanation of what you're doing and why you believe it is the best approach? – DanielJ Apr 10 '22 at 06:02
0

Here's a working example of a class to retrieve FTX subaccounts. Modify for your own purposes. I use HTTParty.

class Balancer
  require 'uri'
  require "openssl"
  include HTTParty

  def get_ftx_subaccounts
    method = 'GET'
    path = '/subaccounts'

    url = "#{ENV['FTX_BASE_URL']}#{path}"
    return HTTParty.get(url, headers: headers(method, path, ''))        
  end

  def headers(*args)
    {
      'FTX-KEY' => ENV['FTX_API_KEY'],
      'FTX-SIGN' => signature(*args),
      'FTX-TS' => ts.to_s,
      'Content-Type' => 'application/json',
      'Accepts' => 'application/json',
    }
  end

  def signature(*args)
    OpenSSL::HMAC.hexdigest(digest, ENV['FTX_API_SECRET'], signature_payload(*args))
  end

  def signature_payload(method, path, query)
    payload = [ts, method.to_s.upcase, "/api", path].compact
    
    if method==:post
      payload << query.to_json
    elsif method==:get
      payload << ("?" + URI.encode_www_form(query))
    end unless query.empty?

    payload.join.encode("UTF-8")
  end

  def ts
    @ts ||= (Time.now.to_f * 1000).to_i
  end

  def digest
    @digest ||= OpenSSL::Digest.new('sha256')
  end

end
turkeyman84
  • 165
  • 1
  • 9