I'm implementing an exchange which has private endpoints which are protected with a signature. The first code looks better but it results into an invalid signature due to the fact that it doesn't lexicographically sort it. How do I fix it? The second code works fine btw.
Broken code
public ValueTask SubscribeToPrivateAsync()
{
var request = JsonSerializer.Serialize(new MexcSubPersonalPayload(_apiKey, _apiSecret));
return SendAsync(request);
}
internal class MexcSubPersonalPayload
{
private readonly string _apiSecret;
public MexcSubPersonalPayload(string apiKey, string apiSecret)
{
ApiKey = apiKey;
_apiSecret = apiSecret;
}
[JsonPropertyName("op")]
[JsonPropertyOrder(1)]
public string Operation => "sub.personal";
[JsonPropertyName("api_key")]
[JsonPropertyOrder(2)]
public string ApiKey { get; }
[JsonPropertyName("sign")]
[JsonPropertyOrder(3)]
public string Signature => Sign(ToString());
[JsonPropertyName("req_time")]
[JsonPropertyOrder(4)]
public long RequestTime => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
private string Sign(string payload)
{
if (string.IsNullOrWhiteSpace(payload))
{
return string.Empty;
}
using var md5 = MD5.Create();
var sourceBytes = Encoding.UTF8.GetBytes(payload);
var hash = md5.ComputeHash(sourceBytes);
return Convert.ToHexString(hash);
}
public override string ToString()
{
return $"api_key={HttpUtility.UrlEncode(ApiKey)}&req_time={RequestTime}&op={Operation}&api_secret={HttpUtility.UrlEncode(_apiSecret)}";
}
}
Working code
public ValueTask SubscribeToPrivateAsync()
{
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var @params = new Dictionary<string, object>
{
{ "api_key", _apiKey },
{ "req_time", now },
{ "op", "sub.personal" }
};
var signature = SignatureHelper.Sign(SignatureHelper.GenerateSign(@params, _apiSecret));
var request = new PersonalRequest("sub.personal", _apiKey, signature, now);
return SendAsync(request);
}
internal static class SignatureHelper
{
public static string GenerateSign(IDictionary<string, object> query, string apiSecret)
{
// Lexicographic order
query = new SortedDictionary<string, object>(query, StringComparer.Ordinal);
var sb = new StringBuilder();
var queryParameterString = string.Join("&",
query.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value.ToString()))
.Select(kvp => $"{kvp.Key}={HttpUtility.UrlEncode(kvp.Value.ToString())}"));
sb.Append(queryParameterString);
if (sb.Length > 0)
{
sb.Append('&');
}
sb.Append("api_secret=").Append(HttpUtility.UrlEncode(apiSecret));
return sb.ToString();
}
public static string Sign(string source)
{
using var md5 = MD5.Create();
var sourceBytes = Encoding.UTF8.GetBytes(source);
var hash = md5.ComputeHash(sourceBytes);
return Convert.ToHexString(hash);
}
}