3
votes
Protected Overrides Function getJsonPrivate(method As String, otherParameters() As Tuple(Of String, String)) As String
    Dim base = "https://www.coinmex.com"
    Dim premethod = "/api/v1/spot/ccex/"
    Dim longmethod = premethod + method

    Dim timestampstring = getEstimatedTimeStamp().ToString

    Dim stringtosign = timestampstring + "GET" + longmethod + "{}" '1553784499976GET/api/v1/spot/ccex/account/assets{}

    Dim hasher = New System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(_secret1))
    Dim sighashbyte = hasher.ComputeHash(System.Text.Encoding.UTF8.GetBytes(stringtosign))
    Dim signature = System.Convert.ToBase64String(sighashbyte) '"FIgrJFDOQctqnkOTyuv6+uTy6xw3OZiP4waC1u6P5LU="=
    Dim url = base + longmethod 'https://www.coinmex.com/api/v1/spot/ccex/account/assets

    '_apiKey1="cmx-1027e54e4723b09810576f8e7a5413**"
    '_passphrase1= 1Us6&f%*K@Qsqr**
    '
    Dim response = CookieAwareWebClient.downloadString1(url, "", {Tuple.Create("ACCESS-KEY", _apiKey1), Tuple.Create("ACCESS-SIGN", signature), Tuple.Create("ACCESS-TIMESTAMP", timestampstring), Tuple.Create("ACCESS-PASSPHRASE", _passphrase1)})

    Return response
End Function

Public Overrides Sub readbalances()
    typicalReadBalances("account/assets", "data", "currencyCode", "available", "frozen", "", {})
End Sub

I think I did it like what's listed here https://github.com/coinmex/coinmex-official-api-docs/blob/master/README_EN.md#1-access-account-information

# Request
GET /api/v1/spot/ccex/account/assets

# Response
[
    {
        "available":"0.1",
        "balance":"0.1",
        "currencyCode":"ETH",
        "frozen":"0",
        "id":1
    },
    {
        "available":"1",
        "balance":"1",
        "currencyCode":"USDT",
        "frozen":"0",
        "id":1
    }
]

And for Signature

This is the manual says

The ACCESS-SIGN header is the output generated by using HMAC SHA256 to create the HMAC SHA256 using the BASE64 decoding secret key in the prehash string to generate timestamp + method + requestPath + "?" + queryString + body (where ‘+’ represents the string concatenation) and BASE64 encoded output. The timestamp value is the same as the ACCESS-TIMESTAMP header. This body is the request body string or omitted if there is no request body (usually the GET request). This method should be capitalized.

Remember that before using it as the key to HMAC, base64 decoding (the result is 64 bytes) is first performed on the 64-bit alphanumeric password string. In addition, the digest output is base64 encoded before sending the header.

User submitted parameters must be signed except for sign. First, the string to be signed is ordered according to the parameter name (first compare the first letter of all parameter names, in alphabetic order, if you encounter the same first letter, then you move to the second letter, and so on).

For example, if we sign the following parameters

curl "https://www.coinmex.com/api/v1/spot/ccex/orders?limit=100"       

Timestamp = 1590000000.281
Method = "POST"
requestPath = "/api/v1/spot/ccex/orders"
queryString= "?limit=100"
body = {
            'code': 'ct_usdt',
            'side': 'buy',
            'type': 'limit',
            'size': '1',
            'price': '1',
            'funds': '',
        }

Generate the string to be signed

Message = '1590000000.281GET/api/v1/spot/ccex/orders?limit=100{"code": "ct_usdt", "side": "buy", "type": "limit", "size": "1", "price": "0.1", "funds": ""}'

Then, the character to be signed is added with the private key parameters to generate the final character string to be signed.

For example:

hmac = hmac(secretkey, Message, SHA256)
Signature = base64.encode(hmac.digest())

I thought may be the _secret1 is a base64 string rather than utf8 so I changed to

Dim base = "https://www.coinmex.com"
Dim premethod = "/api/v1/spot/ccex/"
Dim longmethod = premethod + method

Dim timestampstring = getEstimatedTimeStamp().ToString

'Dim stringtosign = timestampstring + "GET" + longmethod + "{}" '1553784499976GET/api/v1/spot/ccex/account/assets{} also doesn't work
Dim stringtosign = timestampstring + "GET" + longmethod  '1553784499976GET/api/v1/spot/ccex/account/assets

Dim hasher = New System.Security.Cryptography.HMACSHA256(Convert.FromBase64String(_secret1)) 'secret looks like 43a90185f5b7ab25af045e9e64bac5dc745934f359f1806fcdd2a4af80ac2
Dim sighashbyte = hasher.ComputeHash(System.Text.Encoding.UTF8.GetBytes(stringtosign))
Dim signature = Convert.ToBase64String(sighashbyte) '"FIgrJFDOQctqnkOTyuv6+uTy6xw3OZiP4waC1u6P5LU="=
Dim url = base + longmethod 'https://www.coinmex.com/api/v1/spot/ccex/account/assets

'_apiKey1="cmx-1027e54e4723b09810576f8e7a5413**"
'_passphrase1= 1Us6&f%*K@Qsq***
'
Dim response = CookieAwareWebClient.downloadString1(url, "", {Tuple.Create("ACCESS-KEY", _apiKey1), Tuple.Create("ACCESS-SIGN", signature), Tuple.Create("ACCESS-TIMESTAMP", timestampstring), Tuple.Create("ACCESS-PASSPHRASE", _passphrase1)})

Return response

Not working either.

The secret key (I truncated a few letters) look like

43a90185f5b7ab25af045e9e64bac5dc745934f359f1806fcdd2a4af80ac2

Is this something that should be decoded as base 64 or utf8 or what?

The spec says it's 64. However, it doesn't look like a 64 encoded string. It looks like the letters are from 0-f

Best answers will: 1. Tell me what went wrong in the code. I made the change. Try. Run. Works. Awesome.

A good answer will 2. A sample simulation with a fake/real signatures/nonce/passphrase and real actual headers and signatures. So I can see where exactly I have a wrong result.

Update: I modified the code again. I change the timestamp to seconds instead of milisecons. I remove the {}. I use both way.

    Dim base = "https://www.coinmex.com"
    Dim premethod = "/api/v1/spot/ccex/"
    Dim longmethod = premethod + method

    Dim timestampstring = (getEstimatedTimeStamp() / 1000).ToString

    Dim stringtosign = timestampstring + "GET" + longmethod  '1555154812.857GET/api/v1/spot/ccex/account/assets

    Dim hasher = New System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(_secret1)) '"43a90185f5b7ab25af045e9e64bac5dc745934f359f1806fcdd2a4af80ac2******
    Dim sighashbyte = hasher.ComputeHash(System.Text.Encoding.UTF8.GetBytes(stringtosign))
    Dim signature = Convert.ToBase64String(sighashbyte) '"FIgrJFDOQctqnkOTyuv6+uTy6xw3OZiP4waC1u6P5LU="=
    Dim url = base + longmethod 'https://www.coinmex.com/api/v1/spot/ccex/account/assets

    '_apiKey1="cmx-1027e54e4723b09810576f8e7a5413**"
    '_passphrase1= 1Us6&f%*K@QsqrYZ
    '
    Dim response = CookieAwareWebClient.downloadString1(url, "", {Tuple.Create("ACCESS-KEY", _apiKey1), Tuple.Create("ACCESS-SIGN", signature), Tuple.Create("ACCESS-TIMESTAMP", timestampstring), Tuple.Create("ACCESS-PASSPHRASE", _passphrase1)})

    Return response

Still doesn't work.

Current Error is

Message = "The remote server returned an error: (401) Unauthorized."

I would love to give some read-only API key. Hang on. Or create an empty account and then have a read only API key

2
Suggestion: maybe editing your tags would help. Tag api is meaningless: if you hover it, you'll see it says "DO NOT USE". Maybe add vb.net, as it looks like what you're using. - Hugues M.
Thanks. I add vb.net. However, any language, like PhP where I can verify that I compute the signature correctly would also help a lot. - user4951

2 Answers

2
votes

The documentation states

This body is the request body string or omitted if there is no request body (usually the GET request)

Note: emphasis mine

yet you include an empty JSON object on a GET request

Dim stringtosign = timestampstring + "GET" + longmethod + "{}" '1553784499976GET/api/v1/spot/ccex/account/assets{}

That {} should not be included in a GET request.

'1553784499976GET/api/v1/spot/ccex/account/assets
Dim stringtosign = timestampstring + "GET" + longmethod

So it appears you were not constructing the signature correctly as per documentation.

Noticed that the docs

The root URL for REST access:https://www.coinmex.pro

while you are trying to call "https://www.coinmex.com"

Timestamp

Unless otherwise specified, all timestamps in APIs are returned in microseconds.

The ACCESS-TIMESTAMP header must be the number of seconds since UTC's time Unix Epoch. Decimal values are allowed. Your timestamp must be within 30 seconds of the API service time, otherwise your request will be considered expired and rejected. If you think there is a large time difference between your server and the API server, then we recommend that you use the time point to check the API server time.

note: emphasis mine

Following extension method was used to calculate time stamp

private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

/// <summary>
/// Converts the value of the current <see cref="System.DateTime"/> object to Unix Time.
/// </summary>
/// <param name="dateTime"></param>
/// <remarks>
/// </remarks>
/// This method first converts the current instance to UTC before returning its Unix time.
/// <returns> 
/// A <see cref="System.Int64"/> defined as the number of seconds that have elapsed since midnight Coordinated Universal Time (UTC), January 1, 1970, not counting leap seconds.
/// </returns>
public static long ToUnixTimeSeconds(this DateTime dateTime) {
    if (dateTime.ToUniversalTime() < Epoch) {
        return 0;
    }

    var totalSeconds = dateTime.ToUniversalTime().Subtract(Epoch).TotalSeconds;
    var timestamp = Convert.ToInt64(totalSeconds);

    return timestamp;
}

I did the following Test to see if I could call the API following the documentation and it appears to have worked.

I used c# however

[TestClass]
public class CoinMaxAPITests {
    const string apiKey1 = "cmx-1027e54e4723b09810576f8e7a5413**";
    const string fakeSecret = "43a90185f5b7ab25af045e9e64bac5dc745934f359f1806fcdd2a4af80ac23==";
    const string passphrase1 = "1Us6&f%*K@QsqrYZ";

    Lazy<HttpClient> http = new Lazy<HttpClient>(() => {
        var rootUrl = "https://www.coinmex.pro";

        CookieContainer cookies = new CookieContainer();
        HttpClientHandler handler = new HttpClientHandler {
            CookieContainer = cookies,
            UseCookies = true,

        };
        var client = new HttpClient() {
            BaseAddress = new Uri(rootUrl)
        };
        client.DefaultRequestHeaders.TryAddWithoutValidation("ACCESS-KEY", apiKey1);
        client.DefaultRequestHeaders.TryAddWithoutValidation("ACCESS-PASSPHRASE", passphrase1);
        return client;
    });

    [TestMethod]
    public async Task Should_Accept_Signature() {
        //Arrange
        var requestPath = "/api/v1/spot/public/time";
        var method = "GET";
        var timeStamp = getEstimatedTimeStamp().ToString(); //"1555253371"

        var message = timeStamp + method + requestPath; //"1555253371GET/api/v1/spot/public/time"

        var secretKey = Convert.FromBase64String(fakeSecret);
        var hmac = new HMACSHA256(secretKey);
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
        var signature = Convert.ToBase64String(hash);//Jzui/eO3iyLTD6L9qVkUO0EBpZP/lFhx1HlsbuSNt/8=

        var request = new HttpRequestMessage(HttpMethod.Get, requestPath);
        request.Headers.TryAddWithoutValidation("ACCESS-TIMESTAMP", timeStamp);
        request.Headers.TryAddWithoutValidation("ACCESS-SIGN", signature);

        //Act
        var response = await http.Value.SendAsync(request);

        //Assert
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        //"{\"epoch\":\"1555253501.225\",\"iso\":\"2019-04-14T14:51:41.225Z\",\"timestamp\":1555253501225}"
        var server = JsonConvert.DeserializeObject<ServerTime>(json);

        server.Should().NotBeNull();
        server.Iso.Date.Should().Be(DateTime.Today);
    }

    long getEstimatedTimeStamp() {
        return DateTime.Now.ToUnixTimeSeconds(); //custom extension method
    }
}


public partial class ServerTime {
    [JsonProperty("epoch")]
    public string Epoch { get; set; }

    [JsonProperty("iso")]
    public DateTime Iso { get; set; }

    [JsonProperty("timestamp")]
    public long Timestamp { get; set; }
}

And was able to get a valid JSON response calling /api/v1/spot/public/time that I was able to deserialize for my assertion, even with the fake keys. Probably as this is the public API. This does prove that the URL called is correct.

When the request path is changed to

"/api/v1/spot/ccex/account/assets"

And tested for more secure private data from the API, the response is 400 Bad Request with the following content in the body of the response

{"message":"Encrypted key does not exist"}

which is as expected given that the keys I used were fake.

This gives me every indication that the API does in fact work as expected provided that you follow what is suggested in the linked documentation.

0
votes

I wonder what I should pick as the answer due to this conflict of interest.

What happened is he go the extra mile of trying some API. I decided to create a new API key and post it here. It's read-only and can't possibly go wrong anyway. Before I posted here I try to run it once more expecting that it won't work like usual.

It turns out my code simply works. It seems that there are error in API key, secret, or password.

Here is the code that finally work

Protected Overrides Function getJsonPrivate(method As String, otherParameters() As Tuple(Of String, String)) As String
    Dim base = "https://www.coinmex.pro"
    Dim premethod = "/api/v1/spot/ccex/"
    Dim longmethod = premethod + method

    Dim timestampstring = (getEstimatedTimeStamp() / 1000).ToString

    Dim stringtosign = timestampstring + "GET" + longmethod  '1555154812.857GET/api/v1/spot/ccex/account/assets

    Dim hasher = New System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(_secret1)) '"43a90185f5b7ab25af045e9e64bac5dc745934f359f1806fcdd2a4af80ac2******
    Dim sighashbyte = hasher.ComputeHash(System.Text.Encoding.UTF8.GetBytes(stringtosign))
    Dim signature = Convert.ToBase64String(sighashbyte) '"FIgrJFDOQctqnkOTyuv6+uTy6xw3OZiP4waC1u6P5LU="=
    Dim url = base + longmethod 'https://www.coinmex.com/api/v1/spot/ccex/account/assets

    '_apiKey1="cmx-1027e54e4723b09810576f8e7a5413**"
    '_passphrase1= 1Us6&f%*K@QsqrYZ
    '
    Dim response = CookieAwareWebClient.downloadString1(url, "", {Tuple.Create("ACCESS-KEY", _apiKey1), Tuple.Create("ACCESS-SIGN", signature), Tuple.Create("ACCESS-TIMESTAMP", timestampstring), Tuple.Create("ACCESS-PASSPHRASE", _passphrase1)})

    Return response
End Function