Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CryptoPulse.BianceApi/Attributes/AuthKeyRequiredAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CryptoPulse.BianceApi.Attributes;
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public sealed class AuthKeyRequiredAttribute: Attribute
{
}
2 changes: 1 addition & 1 deletion CryptoPulse.BianceApi/CryptoPulse.BianceApi.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net9.0;net9.0-android;net9.0-windows10.0.19041.0;</TargetFrameworks>
Expand Down
112 changes: 63 additions & 49 deletions CryptoPulse.BianceApi/Services/BinanceApiClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CryptoPulse.BianceApi.DTOs;
using CryptoPulse.BianceApi.Attributes;
using CryptoPulse.BianceApi.DTOs;
using CryptoPulse.BianceApi.Services.Interfaces;
using CryptoPulse.Infrastructure.Exceptions;
using CryptoPulse.Infrastructure.Services.Interfaces;
Expand All @@ -10,7 +11,7 @@
public class BinanceApiClient : IBinanceApiClient
{
private string _apiKey { get; set; } = string.Empty;
private string _apiSecret { get; set; } = string.Empty;
private string _privateKey { get; set; } = string.Empty;
private HttpClient _httpClient;
private HttpClient _httpClientNoAuth;
private readonly long _recvWindow = 50000;
Expand Down Expand Up @@ -38,13 +39,20 @@ private string CreateSignature(string queryString)
if (!_hasValidKeys)
GetCashedKeys();

using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret));
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_privateKey));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(queryString));
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}

#region Authorization required
// Fetch transaction history for a given pair
public async Task<List<AccountTradeListDto>> GetAccountTradeLis(string symbol)
/// <summary>
/// Fetch transaction history for a given pair
/// </summary>
/// <param name="symbol"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
[AuthKeyRequired]
public async Task<List<AccountTradeListDto>> GetAccountTradeLisAsync(string symbol)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var starttime = DateTimeOffset.UtcNow.AddDays(-30).ToUnixTimeMilliseconds();
Expand All @@ -63,13 +71,34 @@ public async Task<List<AccountTradeListDto>> GetAccountTradeLis(string symbol)
return JsonConvert.DeserializeObject<List<AccountTradeListDto>>(json) ?? throw new Exception("Response is empty");
}

[AuthKeyRequired]
public async Task<AccountTradeListDto> GetAccountTradeLastPairOperationAsync(string symbol)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var queryString = $"symbol={symbol}&limit=1&timestamp={timestamp}&recvWindow={_recvWindow}";
var signature = CreateSignature(queryString);
var url = $"/api/v3/myTrades?{queryString}&signature={signature}";

public async Task<PairPriceTickerDto> GetSymbolCurrentPrice(string symbol, CancellationTokenSource token)
var response = await _httpClient.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<AccountTradeListDto>>(json)?.FirstOrDefault() ?? throw new Exception("Response is empty");
}
else
{
throw new Exception($"API call failed: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}");
}
}
#endregion

#region No authorization methods
public async Task<PairPriceTickerDto> GetSymbolCurrentPriceAsync(string symbol)
{
var queryString = $"symbol={symbol}";
var url = $"/api/v3/ticker/price?{queryString}";

var response = await _httpClientNoAuth.GetAsync(url,token.Token);
var response = await _httpClientNoAuth.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
throw new Exception($"API call failed: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}");
Expand All @@ -79,7 +108,7 @@ public async Task<PairPriceTickerDto> GetSymbolCurrentPrice(string symbol, Cance
return JsonConvert.DeserializeObject<PairPriceTickerDto>(json) ?? throw new Exception("Response is empty");
}

public async Task<PairAvgPriceDTo> GetSymbolAvgPrice(string symbol)
public async Task<PairAvgPriceDTo> GetSymbolAvgPriceAsync(string symbol)
{
var queryString = $"symbol={symbol}";
var url = $"/api/v3/avgPrice?{queryString}";
Expand All @@ -89,29 +118,10 @@ public async Task<PairAvgPriceDTo> GetSymbolAvgPrice(string symbol)
{
throw new Exception($"API call failed: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}");
}

var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<PairAvgPriceDTo>(json) ?? throw new Exception("Response is empty");
}

public async Task<AccountTradeListDto> GetAccountTradeLastPairOperation(string symbol)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var queryString = $"symbol={symbol}&limit=1&timestamp={timestamp}&recvWindow={_recvWindow}";
var signature = CreateSignature(queryString);
var url = $"/api/v3/myTrades?{queryString}&signature={signature}";

var response = await _httpClient.GetAsync(url);
if (response.IsSuccessStatusCode)
{
var json = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<List<AccountTradeListDto>>(json)?.FirstOrDefault() ?? throw new Exception("Response is empty");
}
else
{
throw new Exception($"API call failed: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}");
}
}

public async Task<List<KlineDataDto>> GetHistoricalDataAsync(string symbol, string interval, int limit)
{
Expand Down Expand Up @@ -155,14 +165,15 @@ public async Task<List<KlineDataDto>> GetHistoricalDataAsync(string symbol, stri
throw new Exception($"API call failed: {response.StatusCode} - {await response.Content.ReadAsStringAsync()}");
}
}
#endregion

#region Keys operation
private void GetCashedKeys()
{
_apiKey = _secureStorage.GetApiKeyCashed() ?? string.Empty;
_apiSecret = _secureStorage.GetApiPrivateKeyCashed() ?? string.Empty;
_privateKey = _secureStorage.GetApiPrivateKeyCashed() ?? string.Empty;

if (!(string.IsNullOrEmpty(_apiKey) || string.IsNullOrEmpty(_apiSecret)))
if (!(string.IsNullOrEmpty(_apiKey) || string.IsNullOrEmpty(_privateKey)))
{
_httpClient = new HttpClient
{
Expand All @@ -174,40 +185,24 @@ private void GetCashedKeys()
else
{
_hasValidKeys = false;
throw new NoAuthKeyExeption();
throw new NoAuthKeyException();
}
}

public void SetNewKeyApiValue(string apiKey)
{
_apiKey = apiKey;
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.binance.com")
};
_httpClient.DefaultRequestHeaders.Add("X-MBX-APIKEY", _apiKey);
}

public void SetNewPrivateKeyValue(string privateKey)
{
_apiSecret = privateKey;
}


public async Task InitializeAuthHttpClientAsync()
{
if (!_hasValidKeys)
{

_apiKey = await _secureStorage.GetApiKeyAsync() ?? string.Empty;
_apiSecret = await _secureStorage.GetApiPrivateKeyAsync() ?? string.Empty;
_privateKey = await _secureStorage.GetApiPrivateKeyAsync() ?? string.Empty;

_httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.binance.com")
};

if (!(string.IsNullOrEmpty(_apiKey) || string.IsNullOrEmpty(_apiSecret)))
if (!(string.IsNullOrEmpty(_apiKey) || string.IsNullOrEmpty(_privateKey)))
{
_httpClient.DefaultRequestHeaders.Add("X-MBX-APIKEY", _apiKey);
_hasValidKeys = true;
Expand All @@ -219,8 +214,9 @@ public async Task InitializeAuthHttpClientAsync()
}
}

public async Task<bool> GetUserKeysValidation(string apiKey, string privateKey)
public async Task<bool> ChceckUserKeysValidationAsync(string apiKey, string privateKey, bool applayNewKeys = false)
{

HttpClient tempClient = new HttpClient
{
BaseAddress = new Uri("https://api.binance.com")
Expand All @@ -236,6 +232,12 @@ public async Task<bool> GetUserKeysValidation(string apiKey, string privateKey)

var url = $"/api/v3/account?{queryString}&signature={signature}";
var response = await tempClient.GetAsync(url);
var responseSuccess = response.IsSuccessStatusCode;

if (applayNewKeys && responseSuccess)
{
SetNewKeyApiValueToHttpClient(apiKey, privateKey);
}

return response.IsSuccessStatusCode;
}
Expand All @@ -244,5 +246,17 @@ public bool GetKeysValidationInfo()
{
return _hasValidKeys;
}

public void SetNewKeyApiValueToHttpClient(string apiKey, string privateKey)
{
_apiKey = apiKey;
_privateKey = privateKey;
_httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.binance.com")
};

_httpClient.DefaultRequestHeaders.Add("X-MBX-APIKEY", _apiKey);
}
#endregion
}
14 changes: 6 additions & 8 deletions CryptoPulse.BianceApi/Services/Interfaces/IBinanceApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,12 @@
namespace CryptoPulse.BianceApi.Services.Interfaces;
public interface IBinanceApiClient
{
public Task<List<AccountTradeListDto>> GetAccountTradeLis(string symbol);
public Task<AccountTradeListDto> GetAccountTradeLastPairOperation(string symbol);
public void SetNewKeyApiValue(string apiKey);
public void SetNewPrivateKeyValue(string privateKey);
public Task<PairPriceTickerDto> GetSymbolCurrentPrice(string symbol, CancellationTokenSource token);
public Task<PairAvgPriceDTo> GetSymbolAvgPrice(string symbol);
public Task InitializeAsync();
public Task<List<AccountTradeListDto>> GetAccountTradeLisAsync(string symbol);
public Task<AccountTradeListDto> GetAccountTradeLastPairOperationAsync(string symbol);
public Task<PairPriceTickerDto> GetSymbolCurrentPriceAsync(string symbol);
public Task<PairAvgPriceDTo> GetSymbolAvgPriceAsync(string symbol);
public Task<List<KlineDataDto>> GetHistoricalDataAsync(string symbol, string interval, int limit);
public Task<bool> GetUserKeysValidation(string apiKey, string privateKey);
public Task<bool> ChceckUserKeysValidationAsync(string apiKey, string privateKey, bool applayNewKeys = false);
public bool GetKeysValidationInfo();
public Task InitializeAsync();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,25 @@
using System.Threading.Tasks;

namespace CryptoPulse.Infrastructure.Exceptions;
public class NoAuthKeyExeption : Exception
public class NoAuthKeyException : Exception
{
// Optionally, add custom properties
public int ErrorCode { get; set; } = 0;
public string ErrorDetails { get; set; } = string.Empty;

// Default constructor
public NoAuthKeyExeption()
public NoAuthKeyException()
{
}

// Constructor with a message
public NoAuthKeyExeption(string message)
public NoAuthKeyException(string message)
: base(message)
{
}

// Constructor with a message and an inner exception
public NoAuthKeyExeption(string message, Exception innerException)
public NoAuthKeyException(string message, Exception innerException)
: base(message, innerException)
{
}
Expand Down
25 changes: 25 additions & 0 deletions CryptoPulse.Tests/BaseTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CryptoPulse.Tests;
public class BaseTest
{
private static string _projectDir = AppContext.BaseDirectory.Substring(0, AppContext.BaseDirectory.IndexOf("bin"));
private static string _bianceApiOutputTestFolder = Path.Combine(_projectDir, "BianceAPI", "OutputTestContent");
private static string _bianceClientOutputTestFolder = Path.Combine(_projectDir, "BinanceClient", "OutputTestContent");

protected async Task<string> GetBianceApiTestFileContent(string filename)
{
var ext = Path.GetExtension(filename);
if (string.IsNullOrEmpty(ext))
{
filename = filename + ".json";
}
var testDataPath = Path.Combine(_bianceApiOutputTestFolder, filename);
return await File.ReadAllTextAsync(testDataPath);
}
}
Loading
Loading