Skip to content

Instantly share code, notes, and snippets.

@r4nc1d
Last active January 15, 2024 13:32
Show Gist options
  • Save r4nc1d/a24d7b4baabb7857cc41e46d082b2009 to your computer and use it in GitHub Desktop.
Save r4nc1d/a24d7b4baabb7857cc41e46d082b2009 to your computer and use it in GitHub Desktop.
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; }
}
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);
}
}
}
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