FTX.com REST API POST Authentication FAILS with Ruby on Rails and net/https

593 Views Asked by At

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?

2

There are 2 best solutions below

1
user18724482 On

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',
    }
0
turkeyman84 On

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