androidretrofitretrofit2android-async-httpkraken.com

Retrofit - Passing POST parameter data and headers


I'm trying to call the Kraken API using retrofit. I have a working AsyncHttpClient version I want to convert to a retrofit version and I have some problem with passing POST parameters.

As stated in the doc it needs :

The AsyncHttpClient version (works) :

String start = String.valueOf(cal.getTimeInMillis() / 1000);
String key = properties.getProperty("KRAKEN_API_PUBLIC_KEY");
String nonce = String.valueOf(System.currentTimeMillis());
String path = "/0/private/TradesHistory";

RequestParams params = new RequestParams();
params.add("nonce", nonce);
params.add("start", start);
params.add("ofs", String.valueOf(offset));

String sign = calculateSignature(path, nonce, params.toString());

AsyncHttpClient client = new AsyncHttpClient();
client.addHeader("API-Key", key);
client.addHeader("API-Sign", sign);

client.post("https://api.kraken.com"+ path, params, new JsonHttpResponseHandler() {
    @Override
    public void onSuccess(int statusCode, Header[] headers, JSONObject response) {
        // works
    }
});

The Retrofit version (EAPI:Invalid key response) :

String start = String.valueOf(cal.getTimeInMillis() / 1000);
String key = properties.getProperty("KRAKEN_API_PUBLIC_KEY");
String nonce = String.valueOf(System.currentTimeMillis());
String path = "/0/private/TradesHistory";

RequestParams params = new RequestParams();
params.add("nonce", nonce);
params.add("start", start);
params.add("ofs", String.valueOf(offset));

String sign = calculateSignature(path, nonce, params.toString());

KrakenService krakenService = KrakenService.retrofit.create(KrakenService.class);
Call<KrakenTrades> call = krakenService.getTradeHistory(key, sign, nonce, start, String.valueOf(offset));
call.enqueue(new Callback<KrakenTrades>() {
    @Override
    public void onResponse(@Nullable Call<KrakenTrades> call, @Nullable Response<KrakenTrades> response) {
        // EAPI:Invalid key
    }
});

The service :

public interface KrakenService {

    @FormUrlEncoded
    @POST("private/TradesHistory")
    Call<KrakenTrades> getTradeHistory(
        @Header("API-Key") String apiKey,
        @Header("API-Sign") String apiSign,
        @Field("nonce") String nonce,
        @Field("start") String start,
        @Field("ofs") String ofs);

    Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.kraken.com/0/")
        .addConverterFactory(GsonConverterFactory.create())
        .build();
}

Only the call is different, do I miss something ? I've tried using @query, @Body, @FieldMap, @HeaderMap etc. but cannot manage to make it work.

Edit: I just tried with other API like Poloniex and got the same problem (invalid key), while it still works with basic HTTPClient.


Solution

  • I managed to make it work but I have to admit that I don't get the point...

    I activated OkHttp logs to have a look at the requests. Here is what I got first (these are fake keys don't lose your time -_-) :

    D/OkHttp: --> POST https://api.kraken.com/0/private/TradesHistory http/1.1
    D/OkHttp: Content-Type: application/x-www-form-urlencoded
    D/OkHttp: Content-Length: 42
    D/OkHttp: API-Sign: /Eu6Sth0oa13Tr/ofme07TF4ct+TercW7uU9PxBEQhcpYTflC2c/jEW1BZuamBXco0jlgOzWt8RMh0o6kAE5SA==
    D/OkHttp: API-Key: XnmS2gW2Sr1xR/vnB0ivJuHABdXUnW4bsMTOBMREOlz8xYDh00J+D9i4
    D/OkHttp: start=1481587200&ofs=0&nonce=1511301862261
    D/OkHttp: --> END POST (42-byte body)
    D/OkHttp: <-- 200  https://api.kraken.com/0/private/TradesHistory (2049ms)
    D/OkHttp: date: Tue, 21 Nov 2017 22:04:27 GMT
    D/OkHttp: content-type: application/json; charset=utf-8
    D/OkHttp: set-cookie: __cfduid=dc9e29889eeda633314ca5aaad10ce8291511301865; expires=Wed, 21-Nov-18 22:04:25 GMT; path=/; domain=.kraken.com; HttpOnly
    D/OkHttp: vary: Accept-Encoding
    D/OkHttp: server: cloudflare-nginx
    D/OkHttp: cf-ray: 3c16f1d348503e80-ZRH
    D/OkHttp: {"error":["EAPI:Invalid key"]}
    D/OkHttp: <-- END HTTP (30-byte body)
    

    Now, instead of using params.toString() as argument to calculate the signature, I just passed manually concatenated values ("start=" + start + "&ofs=" + offset +"&nonce=" + nonce), and here is what I got :

    D/OkHttp: --> POST https://api.kraken.com/0/private/TradesHistory http/1.1
    D/OkHttp: Content-Type: application/x-www-form-urlencoded
    D/OkHttp: Content-Length: 42
    D/OkHttp: API-Sign: DuxyUuHbj4V9WrOpaoBM6Rx3mZluoqJdkg3xBLgc0A/lVotq3WL19VErctz+ugTxi0eRpCI6oBnl95+Lmh9WmQ==
    D/OkHttp: API-Key: XnmS2gW2Sr1xR/vnB0ivJuHABdXUnW4bsMTOBMREOlz8xYDh00J+D9i4
    D/OkHttp: start=1481587200&ofs=0&nonce=1511302135971
    D/OkHttp: --> END POST (42-byte body)
    D/OkHttp: <-- 200  https://api.kraken.com/0/private/TradesHistory (1340ms)
    D/OkHttp: date: Tue, 21 Nov 2017 22:09:04 GMT
    D/OkHttp: content-type: application/json; charset=utf-8
    D/OkHttp: set-cookie: __cfduid=d7916fbb113ed6f83b9382ea0a57240731511302143; expires=Wed, 21-Nov-18 22:09:03 GMT; path=/; domain=.kraken.com; HttpOnly
    D/OkHttp: vary: Accept-Encoding
    D/OkHttp: server: cloudflare-nginx
    D/OkHttp: cf-ray: 3c16f89b4e273e62-ZRH
    D/OkHttp: {"error":[],"result":{"trades":{...}}}
    D/OkHttp: <-- END HTTP (11944-byte body)
    

    See the difference in the sent data? No? There isn't...

    But it works and I don't get the invalid key response anymore.

    Another strange thing is that changing the order of the parameters makes it fall back to the invalid key reponse :

    String parameters = "start=" + start + "&ofs=" + offset +"&nonce=" + nonce; //works
    String parameters = "start=" + start + "&nonce=" + nonce + "&ofs=" + offset; //doesn't work
    

    I cannot explain this... Maybe encoding issue?

    EDIT : OK got it, it seems that parameters are sorted in the request body. Although server does not care about the order of HTTP parameters, HMAC does (of course param1=value1&param2=value2 will generate a different HMAC than param2=value2&param1=value1).

    So I have to use the exact same order in the string used to calculate the HMAC than in the request body (I hardcoded the value from the order got from the OkHttp logs but maybe a best way would be to extract it from the request body, or even better canonicalize the request body).