Last active
January 15, 2024 13:32
-
-
Save r4nc1d/a24d7b4baabb7857cc41e46d082b2009 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class BitstampApiException : Exception | |
{ | |
// https://www.bitstamp.net/api/#section/Response-codes | |
public BitstampApiException(string method, string message, string code) | |
: base("BitstampClient@" + method + " - " + message + " - " + code) | |
{ | |
Code = code; | |
} | |
public string Code { get; set; } | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface IBitstampClient | |
{ | |
} | |
public class BitstampClient : IBitstampClient | |
{ | |
private readonly string key; | |
private readonly string secret; | |
private readonly bool isPublicApi; | |
private readonly HttpClient httpClient; | |
private const string AccountBalancesRoute = "api/v2/account_balances/{currency}/"; | |
private const string WithdrawalRoute = "api/v2/{currency}_withdrawal/"; | |
private const string WithdrawalRequestsRoute = "api/v2/withdrawal-requests/"; | |
private const string BuyMarketRoute = "api/v2/buy/market/{market_symbol}/"; | |
private const string InstantBuyOrderRoute = "api/v2/buy/instant/{market_symbol}/"; | |
private const string MarketTickerRoute = "api/v2/ticker_hour/{market_symbol}/"; | |
private const string MarketOrderBookRoute = "api/v2/order_book/{market_symbol}/"; | |
private const string OrderStatusRoute = "api/v2/order_status/"; | |
private const string TradingFeesRoute = "api/v2/fees/trading/{market_symbol}/"; | |
public BitstampClient(IHttpClientFactory httpFactory) | |
{ | |
this.isPublicApi = true; | |
this.httpClient = httpFactory.CreateClient("BitstampClient-Public"); | |
this.httpClient.BaseAddress = new Uri("https://www.bitstamp.net"); | |
} | |
public BitstampClient(string key, string secret, IHttpClientFactory httpFactory) | |
{ | |
this.key = key; | |
this.secret = secret; | |
this.isPublicApi = false; | |
this.httpClient = httpFactory.CreateClient($"BitstampClient-{key}"); | |
this.httpClient.BaseAddress = new Uri("https://www.bitstamp.net"); | |
} | |
public async Task<AccountBalancesResponse> GetAccountBalancesForCurrencyAsync(string currency, CancellationToken cancellationToken) | |
{ | |
if (currency == null) | |
{ | |
throw new ArgumentNullException(nameof(currency)); | |
} | |
using var request = new HttpRequestMessage(HttpMethod.Post, AccountBalancesRoute.Replace("{currency}", currency)); | |
var requestAuthenticator = new RequestAuthenticator(this.httpClient.BaseAddress, this.key, this.secret, this.isPublicApi); | |
requestAuthenticator.Authenticate(request); | |
return await ExecuteAsync<AccountBalancesResponse>(request, cancellationToken); | |
} | |
public async Task<CryptoWithdrawalResponse> RequestCryptoWithdrawalAsync(CryptoWithdrawalRequest body, string currency, CancellationToken cancellationToken) | |
{ | |
if (currency == null) | |
{ | |
throw new ArgumentNullException(nameof(currency)); | |
} | |
using var request = new HttpRequestMessage(HttpMethod.Post, WithdrawalRoute.Replace("{currency}", currency)); | |
request.Content = ConvertObjectToFormUrlEncodedContent(body); | |
var requestAuthenticator = new RequestAuthenticator(this.httpClient.BaseAddress, this.key, this.secret, this.isPublicApi); | |
requestAuthenticator.Authenticate(request); | |
return await ExecuteAsync<CryptoWithdrawalResponse>(request, cancellationToken); | |
} | |
public async Task<IList<WithdrawalRequestsResponse>> GetWithdrawalRequestsAsync(WithdrawalRequestsRequest body, CancellationToken cancellationToken) | |
{ | |
using var request = new HttpRequestMessage(HttpMethod.Post, WithdrawalRequestsRoute); | |
request.Content = ConvertObjectToFormUrlEncodedContent(body); | |
var requestAuthenticator = new RequestAuthenticator(this.httpClient.BaseAddress, this.key, this.secret, this.isPublicApi); | |
requestAuthenticator.Authenticate(request); | |
return await ExecuteAsync<IList<WithdrawalRequestsResponse>>(request, cancellationToken); | |
} | |
public async Task<BuySellOrderResponse> OpenMarketBuyOrderAsync(BuySellMarketOrderRequest body, string marketSymbol, CancellationToken cancellationToken) | |
{ | |
if (marketSymbol == null) | |
{ | |
throw new ArgumentNullException(nameof(marketSymbol)); | |
} | |
using var request = new HttpRequestMessage(HttpMethod.Post, BuyMarketRoute.Replace("{market_symbol}", marketSymbol)); | |
request.Content = ConvertObjectToFormUrlEncodedContent(body); | |
var requestAuthenticator = new RequestAuthenticator(this.httpClient.BaseAddress, this.key, this.secret, this.isPublicApi); | |
requestAuthenticator.Authenticate(request); | |
return await ExecuteAsync<BuySellOrderResponse>(request, cancellationToken); | |
} | |
public async Task<InstantBuyOrderResponse> InstantBuyOrderAsync(InstantBuyOrderRequest body, string marketSymbol, CancellationToken cancellationToken) | |
{ | |
if (marketSymbol == null) | |
{ | |
throw new ArgumentNullException(nameof(marketSymbol)); | |
} | |
using var request = new HttpRequestMessage(HttpMethod.Post, InstantBuyOrderRoute.Replace("{market_symbol}", marketSymbol)); | |
request.Content = ConvertObjectToFormUrlEncodedContent(body); | |
var requestAuthenticator = new RequestAuthenticator(this.httpClient.BaseAddress, this.key, this.secret, this.isPublicApi); | |
requestAuthenticator.Authenticate(request); | |
return await ExecuteAsync<InstantBuyOrderResponse>(request, cancellationToken); | |
} | |
public async Task<OrderStatusResponse> GetOrderStatusAsync(OrderStatusRequest body, CancellationToken cancellationToken) | |
{ | |
using var request = new HttpRequestMessage(HttpMethod.Post, OrderStatusRoute); | |
request.Content = ConvertObjectToFormUrlEncodedContent(body); | |
var requestAuthenticator = new RequestAuthenticator(this.httpClient.BaseAddress, this.key, this.secret, this.isPublicApi); | |
requestAuthenticator.Authenticate(request); | |
return await ExecuteAsync<OrderStatusResponse>(request, cancellationToken); | |
} | |
public async Task<MarketTickerResult> GetTickerAsync(string marketSymbol, CancellationToken cancellationToken) | |
{ | |
using var request = new HttpRequestMessage(HttpMethod.Get, MarketTickerRoute.Replace("{market_symbol}", marketSymbol)); | |
return await ExecuteAsync<MarketTickerResult>(request, cancellationToken); | |
} | |
public async Task<FeeTradingResponse> GetTradingFeesForCurrencyAsync(string marketSymbol, CancellationToken cancellationToken) | |
{ | |
if (marketSymbol == null) | |
{ | |
throw new ArgumentNullException(nameof(marketSymbol)); | |
} | |
using var request = new HttpRequestMessage(HttpMethod.Post, TradingFeesRoute.Replace("{market_symbol}", marketSymbol)); | |
var requestAuthenticator = new RequestAuthenticator(this.httpClient.BaseAddress, this.key, this.secret, this.isPublicApi); | |
requestAuthenticator.Authenticate(request); | |
return await ExecuteAsync<FeeTradingResponse>(request, cancellationToken); | |
} | |
public async Task<OrderBookResult> GetOrderBookAsync(string marketSymbol, CancellationToken cancellationToken) | |
{ | |
using var request = new HttpRequestMessage(HttpMethod.Get, MarketOrderBookRoute.Replace("{market_symbol}", marketSymbol)); | |
return await ExecuteAsync<OrderBookResult>(request, cancellationToken); | |
} | |
private async Task<TResponse> ExecuteAsync<TResponse>(HttpRequestMessage request, CancellationToken cancellationToken) | |
{ | |
var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); | |
if (response.IsSuccessStatusCode) | |
{ | |
var content = response.Content != null ? await response.Content.ReadAsStringAsync(cancellationToken) : string.Empty; | |
var errorResponse = JsonConvert.DeserializeObject<ErrorResponse>(content); | |
if (errorResponse == null) | |
{ | |
return JsonConvert.DeserializeObject<TResponse>(content); | |
} | |
throw new BitstampApiException($"ExecuteAsync<{typeof(TResponse).Name}>", errorResponse.Reason, errorResponse.Code); | |
} | |
else | |
{ | |
var content = await response.Content.ReadAsStringAsync(cancellationToken); | |
var errorResponse = JsonConvert.DeserializeObject<ErrorResponse>(content); | |
if (errorResponse != null) | |
{ | |
throw new BitstampApiException($"ExecuteAsync<{typeof(TResponse).Name}>", errorResponse.Reason, errorResponse.Code); | |
} | |
// no idea what the content is, lets log it | |
throw new BitstampApiException($"ExecuteAsync<{typeof(TResponse).Name}>", content, errorResponse.Code); | |
} | |
} | |
private FormUrlEncodedContent ConvertObjectToFormUrlEncodedContent(object obj) | |
{ | |
if (obj == null) | |
{ | |
return null; | |
} | |
var dictionary = JsonConvert.DeserializeObject<Dictionary<string, string>>(JsonConvert.SerializeObject(obj)); | |
return new FormUrlEncodedContent(dictionary); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class RequestAuthenticator | |
{ | |
private readonly Uri baseAddress; | |
private readonly string apiKey; | |
private readonly string apiSecret; | |
private readonly bool isPublicApi; | |
public RequestAuthenticator(Uri baseAddress, string apiKey, string apiSecret, bool isPublicApi) | |
{ | |
this.baseAddress = baseAddress; | |
this.apiKey = apiKey; | |
this.apiSecret = apiSecret; | |
this.isPublicApi = isPublicApi; | |
} | |
public void Authenticate(HttpRequestMessage request) | |
{ | |
if (!this.isPublicApi) | |
{ | |
var urlBuilder = new StringBuilder(); | |
urlBuilder.Append(this.baseAddress).Append(request.RequestUri); | |
var uri = new Uri(urlBuilder.ToString()); | |
var xauth = $"{"BITSTAMP"} {this.apiKey}"; | |
var httpVerb = request.Method.ToString(); | |
var urlHost = uri.Host; | |
var urlPath = uri.AbsolutePath; | |
var urlQuery = string.Empty; | |
var time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); | |
var nonce = Guid.NewGuid().ToString(); | |
var version = "v2"; | |
var payloadString = string.Empty; | |
var contentType = string.Empty; | |
if (IsContentTypeFormUrlEncoded(request)) | |
{ | |
request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); | |
contentType = "application/x-www-form-urlencoded"; | |
payloadString = ReadHttpRequestContentAsString(request); | |
} | |
else | |
{ | |
request.Content = new StringContent(string.Empty, Encoding.UTF8, "application/json"); | |
request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); | |
request.Content.Headers.ContentType = null; | |
} | |
var signature = xauth + httpVerb + urlHost + urlPath + urlQuery + contentType + nonce + time + version + payloadString; | |
request.Headers.Add("X-Auth", xauth); | |
request.Headers.Add("X-Auth-Signature", GetXAuthSignature(this.apiSecret, signature)); | |
request.Headers.Add("X-Auth-Nonce", nonce); | |
request.Headers.Add("X-Auth-Timestamp", time.ToString()); | |
request.Headers.Add("X-Auth-Version", version); | |
} | |
} | |
private string GetXAuthSignature(string secret, string signature) | |
{ | |
return ByteArrayToString(SignHMACSHA256(secret, StringToByteArray(signature))).ToUpper(); | |
} | |
private static byte[] SignHMACSHA256(string key, byte[] data) | |
{ | |
var hashMaker = new HMACSHA256(Encoding.ASCII.GetBytes(key)); | |
return hashMaker.ComputeHash(data); | |
} | |
private static byte[] StringToByteArray(string str) | |
{ | |
return Encoding.ASCII.GetBytes(str); | |
} | |
private static string ByteArrayToString(byte[] hash) | |
{ | |
return BitConverter.ToString(hash).Replace("-", "").ToLower(); | |
} | |
private static bool IsContentTypeFormUrlEncoded(HttpRequestMessage request) | |
{ | |
return request.Content?.Headers.ContentType is { MediaType: "application/x-www-form-urlencoded" }; | |
} | |
private static string ReadHttpRequestContentAsString(HttpRequestMessage request) | |
{ | |
try | |
{ | |
var readAsStringAsync = request.Content.ReadAsStringAsync(); | |
return readAsStringAsync.GetAwaiter().GetResult(); | |
} | |
catch (Exception) | |
{ | |
return string.Empty; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment