diff --git a/CoinCapApi/CoinCapApi.csproj b/CoinCapApi/CoinCapApi.csproj index 13c2c3e..6465c41 100644 --- a/CoinCapApi/CoinCapApi.csproj +++ b/CoinCapApi/CoinCapApi.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.1 False True 2.0.0.1 @@ -45,6 +45,7 @@ + diff --git a/CoinCapApi/CoinCapClient.cs b/CoinCapApi/CoinCapClient.cs new file mode 100644 index 0000000..e33edbd --- /dev/null +++ b/CoinCapApi/CoinCapClient.cs @@ -0,0 +1,221 @@ +// *********************************************************************** +// Assembly : CoinCapApi +// Author : ByronAP +// Created : 12-14-2022 +// +// Last Modified By : ByronAP +// Last Modified On : 12-14-2022 +// *********************************************************************** +// +// Copyright © 2022 ByronAP, CoinCap. All rights reserved. +// +// *********************************************************************** +using CoinCapApi.Exceptions; +using CoinCapApi.Imp; +using Microsoft.Extensions.Logging; +using RestSharp; +using System; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace CoinCapApi +{ + /// + /// Create an instance of this class to access the API methods. + /// + /// Methods and parameters are named as specified in the official + /// CoinCap API documentation (Ex: API call '/rates/{id}' + /// translates to 'CoinCapClient.Rates.GetRateAsync({id})'). + /// + /// By default response caching is enabled. To disable it set to false. + /// Implements the + /// + /// + public class CoinCapClient : IDisposable + { + /// + /// The RestSharp client instance used to make the API calls. + /// This is exposed in case you wish to change options such as use a proxy. + /// + /// The RestSharp client instance. + public RestClient CCRestClient { get; } + + /// + /// Provides access to the Assets API calls. + /// An instance of . + /// + /// Assets API calls. + public AssetsImp Assets { get; } + + /// + /// Provides access to the Rates API calls. + /// An instance of . + /// + /// Rates API calls. + public RatesImp Rates { get; } + + /// + /// Provides access to the Exchanges API calls. + /// An instance of . + /// + /// Exchanges API calls. + public ExchangesImp Exchanges { get; } + + /// + /// Provides access to the Markets API calls. + /// An instance of . + /// + /// Markets API calls. + public MarketsImp Markets { get; } + + /// + /// Provides access to the Candles API calls. + /// An instance of . + /// + /// Candles API calls. + public CandlesImp Candles { get; } + + /// + /// Gets or sets whether this instance is using response caching. + /// Caching is enabled by default. + /// + /// true if this instances cache is enabled; otherwise, false. + public bool IsCacheEnabled { get { return _cache.Enabled; } set { _cache.Enabled = value; } } + + private readonly MemCache _cache; + private bool _disposedValue; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// Using an API key is optional but can be obtained for free from + /// Using a logger is optional but recommended. This can be supplied via dependency injection. + /// + /// Your API key. + /// Your logger. + public CoinCapClient(string apiKey = null, ILogger logger = null) + { + _logger = logger; + _cache = new MemCache(_logger); + + CCRestClient = new RestClient(Constants.API_BASE_URL); + if (!string.IsNullOrEmpty(apiKey) && !String.IsNullOrWhiteSpace(apiKey)) + { + CCRestClient.AddDefaultHeader("Authorization", $"Bearer {apiKey}"); + } + + CCRestClient.AddDefaultHeader("Accept-Encoding", "gzip, deflate, br"); + CCRestClient.AddDefaultHeader("Accept", "application/json"); + CCRestClient.AddDefaultHeader("Connection", "keep-alive"); + CCRestClient.AddDefaultHeader("User-Agent", $"CoinCapApi .NET Client/{Assembly.GetExecutingAssembly().GetName().Version}"); + + Assets = new AssetsImp(CCRestClient, _cache, _logger); + Rates = new RatesImp(CCRestClient, _cache, _logger); + Exchanges = new ExchangesImp(CCRestClient, _cache, _logger); + Markets = new MarketsImp(CCRestClient, _cache, _logger); + Candles = new CandlesImp(CCRestClient, _cache, _logger); + } + + /// + /// Clears the response cache. + /// + public void ClearCache() => _cache.Clear(); + + internal static async Task GetStringResponseAsync(RestClient client, RestRequest request, MemCache cache, ILogger logger, int cacheTime) + { + var fullUrl = client.BuildUri(request).ToString(); + + try + { + if (cache.TryGet(fullUrl, out var cacheResponse)) + { + return (string)cacheResponse; + } + } + catch (Exception ex) + { + logger?.LogError(ex, ""); + } + + try + { + var response = await client.GetAsync(request); + + if (response.IsSuccessStatusCode) + { + cache.CacheRequest(fullUrl, response, cacheTime); + + return response.Content; + } + + if (response.ErrorException != null) + { + logger?.LogError(response.ErrorException, "GetStringResponseAsync failed."); + throw response.ErrorException; + } + + throw new UnknownException($"Unknown exception, http response code is not success, {response.StatusCode}."); + } + catch (Exception ex) + { + logger?.LogError(ex, "GetStringResponseAsync request failure."); + throw; + } + } + + internal static string BuildUrl(params string[] parts) + { + if (parts.Length > 2) + { + var sb = new StringBuilder(); + sb.Append("/v").Append(Constants.API_VERSION); + foreach (var part in parts) + { + sb.Append('/'); + sb.Append(part); + } + return sb.ToString(); + } + else + { + var result = $"/v{Constants.API_VERSION}"; + foreach (var part in parts) + { + result += $"/{part}"; + } + return result; + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + if (_cache != null) + { + try + { + _cache.Dispose(); + } + catch + { + // ignore + } + } + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/CoinCapApi/CoinCapPricesWSClient.cs b/CoinCapApi/CoinCapPricesWSClient.cs new file mode 100644 index 0000000..8c85aff --- /dev/null +++ b/CoinCapApi/CoinCapPricesWSClient.cs @@ -0,0 +1,171 @@ +// *********************************************************************** +// Assembly : CoinCapApi +// Author : ByronAP +// Created : 12-16-2022 +// +// Last Modified By : ByronAP +// Last Modified On : 12-16-2022 +// *********************************************************************** +// +// Copyright © 2022 ByronAP, CoinCap. All rights reserved. +// +// *********************************************************************** +using ByronAP.Net.WebSockets; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace CoinCapApi +{ + /// + /// The CoinCap prices endpoint is the most accurate source of real-time changes to the global price of an asset. Each time the system receives data that moves the global price in one direction or another, this change is immediately published through the websocket. These prices correspond with the values shown in /assets - a value that may change several times per second based on market activity. + /// Implements the + /// + /// + /// + public class CoinCapPricesWSClient : IDisposable + { + /// + /// Occurs when new price data arrives. + /// + public event OnPrices OnPricesEvent; + + /// + /// Occurs when the connection state changes. + /// + public event OnConnectionStateChanged OnConnectionStateChangedEvent; + + /// + /// Provides access to the underlying WebSocketClient instance. + /// + /// The WebSocket. + public WebSocketClient CCWebSocket { get; } + + /// + /// Each class instance is assigned a unique ID which allows you to keep track of multiple instances. + /// + /// The instance identifier. + public Guid InstanceID => CCWebSocket.InstanceId; + + private readonly ILogger _logger; + private readonly IEnumerable _assets; + + /// + /// Initializes a new instance of the class. + /// + /// The assets you wish to subscribe to or ALL for all assets. Names are case sensitive. + /// The WebSocketClient options. + public CoinCapPricesWSClient(IEnumerable assets, WebSocketOptions options) + { + _assets = assets; + + if (options.Logger != null) { _logger = (ILogger)options.Logger; } + + if (string.IsNullOrEmpty(options.Url) || string.IsNullOrWhiteSpace(options.Url)) { options.Url = $"{Constants.API_WS_BASE_URL}/prices?assets={string.Join(",", _assets)}"; } + + CCWebSocket = new WebSocketClient(options); + + AttachEventHandlers(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The assets you wish to subscribe to or ALL for all assets. Names are case sensitive. + /// The logger. + public CoinCapPricesWSClient(IEnumerable assets, ILogger logger = null) + { + _assets = assets; + + _logger = logger; + + var options = new WebSocketOptions($"{Constants.API_WS_BASE_URL}/prices?assets={string.Join(",", assets)}"); + if (logger != null) { options.Logger = _logger; } + + CCWebSocket = new WebSocketClient(options); + + AttachEventHandlers(); + } + + /// + /// Connects this instance to the websocket server. + /// + /// true if connection success, false otherwise. + public async Task Connect() + { + try + { + var result = await CCWebSocket.ConnectAsync(); + if (result.Item1) { return true; } + + _logger?.LogError(result.Item2, "WebSocket {ID} connection failed.", InstanceID); + } + catch (Exception ex) + { + _logger?.LogError(ex, "WebSocket {ID} connection error.", InstanceID); + } + + return false; + } + + /// + /// Disconnects this instance from the websocket server. + /// + public async Task Disconnect() + { + try + { + await CCWebSocket.DisconnectAsync(); + } + catch (Exception ex) + { + _logger?.LogError(ex, "WebSocket {ID} error while closing.", InstanceID); + } + } + + private void AttachEventHandlers() + { + CCWebSocket.ConnectionStateChanged += CCWebSocket_ConnectionStateChanged; + CCWebSocket.MessageReceived += CCWebSocket_MessageReceived; + } + + private void CCWebSocket_MessageReceived(object sender, string message) + { + _logger?.LogDebug("WebSocket {ID} Received Message: {Message}.", CCWebSocket.InstanceId, message); + try + { + var price = JsonConvert.DeserializeObject>(message); + + OnPricesEvent?.Invoke(this, InstanceID, price); + } + catch (Exception ex) + { + _logger?.LogError(ex, "WebSocket {ID} error processing message {Message}.", InstanceID, message); + } + } + + private void CCWebSocket_ConnectionStateChanged(object sender, System.Net.WebSockets.WebSocketState newWebSocketState, System.Net.WebSockets.WebSocketState oldWebSocketState) + { + OnConnectionStateChangedEvent?.Invoke(this, newWebSocketState, oldWebSocketState); + + _logger?.LogInformation("WebSocket {ID} connection state changed from {OldState} to {NewState}.", InstanceID, oldWebSocketState, newWebSocketState); + + if (newWebSocketState == System.Net.WebSockets.WebSocketState.Aborted) + { + _logger?.LogWarning("WebSocket {ID} connection was aborted.", InstanceID); + } + + if (newWebSocketState == System.Net.WebSockets.WebSocketState.Closed) + { + _logger?.LogWarning("WebSocket {ID} connection was closed.", InstanceID); + } + } + + public void Dispose() + { + ((IDisposable)CCWebSocket).Dispose(); + } + } +} diff --git a/CoinCapApi/CoinCapTradesWSClient.cs b/CoinCapApi/CoinCapTradesWSClient.cs new file mode 100644 index 0000000..7c32d4c --- /dev/null +++ b/CoinCapApi/CoinCapTradesWSClient.cs @@ -0,0 +1,171 @@ +// *********************************************************************** +// Assembly : CoinCapApi +// Author : ByronAP +// Created : 12-16-2022 +// +// Last Modified By : ByronAP +// Last Modified On : 12-16-2022 +// *********************************************************************** +// +// Copyright © 2022 ByronAP, CoinCap. All rights reserved. +// +// *********************************************************************** +using ByronAP.Net.WebSockets; +using CoinCapApi.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using System; +using System.Threading.Tasks; + +namespace CoinCapApi +{ + /// + /// The CoinCap trade websocket streams trades from other cryptocurrency exchange websockets. Users must select a specific exchange. In the /exchanges endpoint users can determine if an exchange has a socket available by noting response 'socket':true/false. See an example in the /exchanges endpoint documentation above. The trades websocket is the only way to receive individual trade data through CoinCap. + /// Implements the + /// + /// + /// + public class CoinCapTradesWSClient : IDisposable + { + /// + /// Occurs when trade data is received. + /// + public event OnTrade OnTradeEvent; + + /// + /// Occurs when the connection state changes. + /// + public event OnConnectionStateChanged OnConnectionStateChangedEvent; + + /// + /// Provides access to the underlying WebSocketClient instance. + /// + /// The WebSocket. + public WebSocketClient CCWebSocket { get; } + + /// + /// Each class instance is assigned a unique ID which allows you to keep track of multiple instances. + /// + /// The instance identifier. + public Guid InstanceID => CCWebSocket.InstanceId; + + private readonly ILogger _logger; + private readonly string _exchangeId; + + /// + /// Initializes a new instance of the class. + /// + /// The exchange you wish to subscribe to. + /// The WebSocketClient options. + public CoinCapTradesWSClient(string exchangeId, WebSocketOptions options) + { + _exchangeId = exchangeId; + + if (options.Logger != null) { _logger = (ILogger)options.Logger; } + + if (string.IsNullOrEmpty(options.Url) || string.IsNullOrWhiteSpace(options.Url)) { options.Url = $"{Constants.API_WS_BASE_URL}/trades/{_exchangeId}"; } + + CCWebSocket = new WebSocketClient(options); + + AttachEventHandlers(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The exchange you wish to subscribe to. + /// The logger. + public CoinCapTradesWSClient(string exchangeId, ILogger logger = null) + { + _exchangeId = exchangeId; + + _logger = logger; + + var options = new WebSocketOptions($"{Constants.API_WS_BASE_URL}/trades/{_exchangeId}"); + if (logger != null) { options.Logger = _logger; } + + CCWebSocket = new WebSocketClient(options); + + AttachEventHandlers(); + } + + /// + /// Connects this instance to the websocket server. + /// + /// true if connection success, false otherwise. + public async Task Connect() + { + try + { + var result = await CCWebSocket.ConnectAsync(); + if (result.Item1) { return true; } + + _logger?.LogError(result.Item2, "WebSocket {ID} connection failed.", InstanceID); + } + catch (Exception ex) + { + _logger?.LogError(ex, "WebSocket {ID} connection error.", InstanceID); + } + + return false; + } + + /// + /// Disconnects this instance from the websocket server. + /// + public async Task Disconnect() + { + try + { + await CCWebSocket.DisconnectAsync(); + } + catch (Exception ex) + { + _logger?.LogError(ex, "WebSocket {ID} error while closing.", InstanceID); + } + } + + private void AttachEventHandlers() + { + CCWebSocket.ConnectionStateChanged += CCWebSocket_ConnectionStateChanged; + CCWebSocket.MessageReceived += CCWebSocket_MessageReceived; + } + + private void CCWebSocket_MessageReceived(object sender, string message) + { + _logger?.LogDebug("WebSocket {ID} Received Message: {Message}.", CCWebSocket.InstanceId, message); + try + { + var trade = JsonConvert.DeserializeObject(message); + + OnTradeEvent?.Invoke(this, InstanceID, _exchangeId, trade); + } + catch (Exception ex) + { + _logger?.LogError(ex, "WebSocket {ID} error processing message {Message}.", InstanceID, message); + } + } + + private void CCWebSocket_ConnectionStateChanged(object sender, System.Net.WebSockets.WebSocketState newWebSocketState, System.Net.WebSockets.WebSocketState oldWebSocketState) + { + OnConnectionStateChangedEvent?.Invoke(this, newWebSocketState, oldWebSocketState); + + _logger?.LogInformation("WebSocket {ID} connection state changed from {OldState} to {NewState}.", InstanceID, oldWebSocketState, newWebSocketState); + + if (newWebSocketState == System.Net.WebSockets.WebSocketState.Aborted) + { + _logger?.LogWarning("WebSocket {ID} connection was aborted.", InstanceID); + } + + if (newWebSocketState == System.Net.WebSockets.WebSocketState.Closed) + { + _logger?.LogWarning("WebSocket {ID} connection was closed.", InstanceID); + } + } + + public void Dispose() + { + ((IDisposable)CCWebSocket).Dispose(); + } + } +} diff --git a/CoinCapApi/Constants.cs b/CoinCapApi/Constants.cs new file mode 100644 index 0000000..3797034 --- /dev/null +++ b/CoinCapApi/Constants.cs @@ -0,0 +1,39 @@ +using CoinCapApi.Properties; + +namespace CoinCapApi +{ + public static class Constants + { + /// + /// The display name of the API provider. + /// + public static readonly string API_NAME = "CoinCap"; + + /// + /// The base API URL. + /// + public static string API_BASE_URL = "https://api.coincap.io"; + + /// + /// The websocket API base URL. + /// + public static string API_WS_BASE_URL = "wss://ws.coincap.io"; + + /// + /// The API version. + /// + public static readonly uint API_VERSION = 2; + + /// + /// The API logo at 128 X 128 in PNG format. + /// This is an embedded resource. + /// + public static readonly byte[] API_LOGO_128X128_PNG = Resources.coincap_logo; + + /// + /// The API minimum cache time in milliseconds. + /// If you need to call the same endpoint more that this you are doing something wrong and should probably use websockets or a different service. + /// + public static readonly uint API_MIN_CACHE_TIME_MS = 15000; + } +} diff --git a/CoinCapApi/Delegates.cs b/CoinCapApi/Delegates.cs new file mode 100644 index 0000000..b6654f4 --- /dev/null +++ b/CoinCapApi/Delegates.cs @@ -0,0 +1,9 @@ +using CoinCapApi.Models; +using System; +using System.Collections.Generic; + +namespace CoinCapApi +{ + public delegate void OnTrade(object sender, Guid instanceId, string exchange, WSTrade trade); + public delegate void OnPrices(object sender, Guid instanceId, Dictionary prices); +} diff --git a/CoinCapApi/Exceptions/UnknownException.cs b/CoinCapApi/Exceptions/UnknownException.cs new file mode 100644 index 0000000..7bdd2f6 --- /dev/null +++ b/CoinCapApi/Exceptions/UnknownException.cs @@ -0,0 +1,16 @@ +using System; + +namespace CoinCapApi.Exceptions +{ + /// + /// An exception because I am dumb and don't know WFT it is. + /// Implements the + /// + /// + public class UnknownException : Exception + { + public UnknownException(string message) : base(message) + { + } + } +} diff --git a/CoinCapApi/Imp/AssetsImp.cs b/CoinCapApi/Imp/AssetsImp.cs new file mode 100644 index 0000000..d13d241 --- /dev/null +++ b/CoinCapApi/Imp/AssetsImp.cs @@ -0,0 +1,142 @@ +// *********************************************************************** +// Assembly : CoinCapApi +// Author : ByronAP +// Created : 12-14-2022 +// +// Last Modified By : ByronAP +// Last Modified On : 12-15-2022 +// *********************************************************************** +// +// Copyright © 2022 ByronAP, CoinCap. All rights reserved. +// +// *********************************************************************** +using CoinCapApi.Models; +using CoinCapApi.Types; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using RestSharp; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace CoinCapApi.Imp +{ + /// + /// Implementation of the '/assets' API calls. + /// Implementation classes do not have a public constructor + /// and must be accessed through an instance of . + /// + /// + public class AssetsImp + { + private readonly RestClient _restClient; + private readonly ILogger _logger; + private readonly MemCache _cache; + + internal AssetsImp(RestClient restClient, MemCache cache, ILogger logger) + { + _logger = logger; + _cache = cache; + _restClient = restClient; + } + + /// Get assets as an asynchronous operation. + /// Search by asset id (bitcoin) or symbol (BTC). + /// Query with multiple ids (bitcoin,ethereum,monero). + /// The number of items to retrieve. Default: 100 Max: 2000. + /// The number of items to skip (aka offset). + /// + /// A Task<> representing the asynchronous operation. + public async Task GetAssetsAsync(string search = null, IEnumerable ids = null, uint limit = 100, uint offset = 0) + { + if (limit > 2000) { limit = 2000; } + + var request = new RestRequest(CoinCapClient.BuildUrl("assets")); + if (!string.IsNullOrEmpty(search) && !string.IsNullOrWhiteSpace(search)) { request.AddQueryParameter("search", search); } + if (ids != null && ids.Any()) { request.AddQueryParameter("ids", string.Join(",", ids)); } + request.AddQueryParameter("limit", limit); + if (offset != 0) { request.AddQueryParameter("offset", offset); } + + + var jsonStr = await CoinCapClient.GetStringResponseAsync(_restClient, request, _cache, _logger, 60); + + return JsonConvert.DeserializeObject(jsonStr); + } + + /// Get an asset by id as an asynchronous operation. + /// The unique identifier of the asset. + /// + /// A Task<> representing the asynchronous operation. + /// id - Null or invalid value, id must be a valid asset id. + public async Task GetAssetAsync(string id) + { + if (string.IsNullOrEmpty(id) || string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException(nameof(id), "Null or invalid value, id must be a valid asset id."); } + + var request = new RestRequest(CoinCapClient.BuildUrl("assets", id)); + + var jsonStr = await CoinCapClient.GetStringResponseAsync(_restClient, request, _cache, _logger, 60); + + return JsonConvert.DeserializeObject(jsonStr); + } + + + /// + /// Get asset history as an asynchronous operation. + /// + /// The unique identifier of the asset. + /// The interval to bucket data. + /// The start time, UNIX timestamp in milliseconds (UTC). + /// The end time, UNIX timestamp in milliseconds (UTC). + /// + /// A Task<> representing the asynchronous operation. + /// id - Null or invalid value, id must be a valid asset id. + /// Invalid start/end values, start and end must both be valid timestamps. + /// end - Invalid value, end must be a valid timestamp greater than start. + /// start - Invalid value, start must be in the past. + public async Task GetAssetHistoryAsync(string id, TimeInterval interval, ulong start = 0, ulong end = 0) + { + if (string.IsNullOrEmpty(id) || string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException(nameof(id), "Null or invalid value, id must be a valid asset id."); } + + if (start > 0 && end == 0 || end > 0 && start == 0) { throw new ArgumentOutOfRangeException("Invalid start/end values, start and end must both be valid timestamps."); } + if (start > 0 && end <= start) { throw new ArgumentOutOfRangeException(nameof(end), "Invalid value, end must be a valid timestamp greater than start."); } + if (start > 0 && end > 0 & (long)start > DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()) { throw new ArgumentOutOfRangeException(nameof(start), "Invalid value, start must be in the past."); } + var request = new RestRequest(CoinCapClient.BuildUrl("assets", id, "history")); + request.AddQueryParameter("interval", interval.ToString().ToLowerInvariant()); + if (start > 0 && end > 0) + { + request.AddQueryParameter("start", start); + request.AddQueryParameter("end", end); + } + + var jsonStr = await CoinCapClient.GetStringResponseAsync(_restClient, request, _cache, _logger, 30); + + return JsonConvert.DeserializeObject(jsonStr); + } + + /// + /// Get asset markets as an asynchronous operation. + /// + /// The unique identifier of the asset. + /// The number of items to retrieve. Default: 100 Max: 2000. + /// The number of items to skip (aka offset). + /// + /// A Task<> representing the asynchronous operation. + /// id - Null or invalid value, id must be a valid asset id. + public async Task GetAssetMarketsAsync(string id, uint limit = 100, uint offset = 0) + { + if (limit > 2000) { limit = 2000; } + + if (string.IsNullOrEmpty(id) || string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException(nameof(id), "Null or invalid value, id must be a valid asset id."); } + + var request = new RestRequest(CoinCapClient.BuildUrl("assets", id, "markets")); + request.AddQueryParameter("limit", limit); + if (offset != 0) { request.AddQueryParameter("offset", offset); } + + var jsonStr = await CoinCapClient.GetStringResponseAsync(_restClient, request, _cache, _logger, 60); + + return JsonConvert.DeserializeObject(jsonStr); + } + + } +} diff --git a/CoinCapApi/Imp/CandlesImp.cs b/CoinCapApi/Imp/CandlesImp.cs new file mode 100644 index 0000000..c965b16 --- /dev/null +++ b/CoinCapApi/Imp/CandlesImp.cs @@ -0,0 +1,82 @@ +// *********************************************************************** +// Assembly : CoinCapApi +// Author : ByronAP +// Created : 12-15-2022 +// +// Last Modified By : ByronAP +// Last Modified On : 12-15-2022 +// *********************************************************************** +// +// Copyright © 2022 ByronAP, CoinCap. All rights reserved. +// +// *********************************************************************** +using CoinCapApi.Models; +using CoinCapApi.Types; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using RestSharp; +using System; +using System.Threading.Tasks; + +namespace CoinCapApi.Imp +{ + /// + /// Implementation of the '/candles' API calls. + /// Implementation classes do not have a public constructor + /// and must be accessed through an instance of . + /// + /// + public class CandlesImp + { + private readonly RestClient _restClient; + private readonly ILogger _logger; + private readonly MemCache _cache; + + internal CandlesImp(RestClient restClient, MemCache cache, ILogger logger) + { + _logger = logger; + _cache = cache; + _restClient = restClient; + } + + /// + /// Get candle data (OHLCV) as an asynchronous operation. + /// + /// The unique identifier of the exchange. + /// The interval to bucket data. + /// The base asset identifier. + /// The quote asset identifier. + /// UNIX time in milliseconds. omitting will return the most recent candles. + /// UNIX time in milliseconds. omitting will return the most recent candles. + /// A Task<> representing the asynchronous operation. + /// + /// exchangeId - Null or invalid value, exchangeId must be a valid exchange id. + /// baseId - Null or invalid value, baseId must be a valid asset id. + /// quoteId - Null or invalid value, quoteId must be a valid asset id. + /// Invalid start/end values, start and end must both be valid timestamps. + /// end - Invalid value, end must be a valid timestamp greater than start. + /// start - Invalid value, start must be in the past. + public async Task GetCandlesAsync(string exchangeId, TimeInterval interval, string baseId, string quoteId, ulong start = 0, ulong end = 0) + { + if (string.IsNullOrEmpty(exchangeId) || string.IsNullOrWhiteSpace(exchangeId)) { throw new ArgumentNullException(nameof(exchangeId), "Null or invalid value, exchangeId must be a valid exchange id."); } + if (string.IsNullOrEmpty(baseId) || string.IsNullOrWhiteSpace(baseId)) { throw new ArgumentNullException(nameof(baseId), "Null or invalid value, baseId must be a valid asset id."); } + if (string.IsNullOrEmpty(quoteId) || string.IsNullOrWhiteSpace(quoteId)) { throw new ArgumentNullException(nameof(quoteId), "Null or invalid value, quoteId must be a valid asset id."); } + + if (start > 0 && end == 0 || end > 0 && start == 0) { throw new ArgumentOutOfRangeException("Invalid start/end values, start and end must both be valid timestamps."); } + if (start > 0 && end <= start) { throw new ArgumentOutOfRangeException(nameof(end), "Invalid value, end must be a valid timestamp greater than start."); } + if (start > 0 && end > 0 & (long)start > DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()) { throw new ArgumentOutOfRangeException(nameof(start), "Invalid value, start must be in the past."); } + + var request = new RestRequest(CoinCapClient.BuildUrl("candles")); + request.AddQueryParameter("exchangeId", exchangeId); + request.AddQueryParameter("interval", interval.ToString().ToLowerInvariant()); + request.AddQueryParameter("baseId", baseId); + request.AddQueryParameter("quoteId", quoteId); + if (start > 0) { request.AddQueryParameter("start", start); } + if (end > 0) { request.AddQueryParameter("end", end); } + + var jsonStr = await CoinCapClient.GetStringResponseAsync(_restClient, request, _cache, _logger, 60); + + return JsonConvert.DeserializeObject(jsonStr); + } + } +} diff --git a/CoinCapApi/Imp/ExchangesImp.cs b/CoinCapApi/Imp/ExchangesImp.cs new file mode 100644 index 0000000..58209d2 --- /dev/null +++ b/CoinCapApi/Imp/ExchangesImp.cs @@ -0,0 +1,75 @@ +// *********************************************************************** +// Assembly : CoinCapApi +// Author : ByronAP +// Created : 12-15-2022 +// +// Last Modified By : ByronAP +// Last Modified On : 12-15-2022 +// *********************************************************************** +// +// Copyright © 2022 ByronAP, CoinCap. All rights reserved. +// +// *********************************************************************** + +using CoinCapApi.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using RestSharp; +using System; +using System.Threading.Tasks; + +namespace CoinCapApi.Imp +{ + /// + /// Implementation of the '/exchanges' API calls. + /// Implementation classes do not have a public constructor + /// and must be accessed through an instance of . + /// + /// + public class ExchangesImp + { + private readonly RestClient _restClient; + private readonly ILogger _logger; + private readonly MemCache _cache; + + internal ExchangesImp(RestClient restClient, MemCache cache, ILogger logger) + { + _logger = logger; + _cache = cache; + _restClient = restClient; + } + + /// + /// Get exchanges as an asynchronous operation. + /// + /// + /// A Task<> representing the asynchronous operation. + public async Task GetExchangesAsync() + { + var request = new RestRequest(CoinCapClient.BuildUrl("exchanges")); + + var jsonStr = await CoinCapClient.GetStringResponseAsync(_restClient, request, _cache, _logger, 120); + + return JsonConvert.DeserializeObject(jsonStr); + } + + /// + /// Get exchange as an asynchronous operation. + /// + /// The unique identifier of the exchange to get. + /// + /// A Task<> representing the asynchronous operation. + /// id - Null or invalid value, id must be a valid exchange id. + public async Task GetExchangeAsync(string id) + { + if (string.IsNullOrEmpty(id) || string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException(nameof(id), "Null or invalid value, id must be a valid exchange id."); } + + var request = new RestRequest(CoinCapClient.BuildUrl("exchanges", id)); + + var jsonStr = await CoinCapClient.GetStringResponseAsync(_restClient, request, _cache, _logger, 60); + + return JsonConvert.DeserializeObject(jsonStr); + } + + } +} diff --git a/CoinCapApi/Imp/MarketsImp.cs b/CoinCapApi/Imp/MarketsImp.cs new file mode 100644 index 0000000..3bd0b86 --- /dev/null +++ b/CoinCapApi/Imp/MarketsImp.cs @@ -0,0 +1,74 @@ +// *********************************************************************** +// Assembly : CoinCapApi +// Author : ByronAP +// Created : 12-15-2022 +// +// Last Modified By : ByronAP +// Last Modified On : 12-15-2022 +// *********************************************************************** +// +// Copyright © 2022 ByronAP, CoinCap. All rights reserved. +// +// *********************************************************************** +using CoinCapApi.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using RestSharp; +using System.Threading.Tasks; + +namespace CoinCapApi.Imp +{ + /// + /// Implementation of the '/markets' API calls. + /// Implementation classes do not have a public constructor + /// and must be accessed through an instance of . + /// + /// + public class MarketsImp + { + private readonly RestClient _restClient; + private readonly ILogger _logger; + private readonly MemCache _cache; + + internal MarketsImp(RestClient restClient, MemCache cache, ILogger logger) + { + _logger = logger; + _cache = cache; + _restClient = restClient; + } + + /// + /// Get markets as an asynchronous operation. + /// + /// Filters by exchange id. + /// Filters by all markets containing the base symbol. + /// Filters by all markets containing the quote symbol. + /// Filters by all markets containing the base id. + /// Filters by all markets containing the quote id. + /// Filters by all markets containing symbol (base and quote). + /// Filters by all markets containing id (base and quote). + /// The number of items to retrieve. Default: 100 Max: 2000. + /// The number of items to skip (aka offset). + /// + /// A Task<> representing the asynchronous operation. + public async Task GetMarketsAsync(string exchangeId = null, string baseSymbol = null, string quoteSymbol = null, string baseId = null, string quoteId = null, string assetSymbol = null, string assetId = null, uint limit = 100, uint offset = 0) + { + if (limit > 2000) { limit = 2000; } + + var request = new RestRequest(CoinCapClient.BuildUrl("markets")); + if (!string.IsNullOrEmpty(exchangeId) && !string.IsNullOrWhiteSpace(exchangeId)) { request.AddQueryParameter("exchangeId", exchangeId); } + if (!string.IsNullOrEmpty(baseSymbol) && !string.IsNullOrWhiteSpace(baseSymbol)) { request.AddQueryParameter("baseSymbol", baseSymbol); } + if (!string.IsNullOrEmpty(quoteSymbol) && !string.IsNullOrWhiteSpace(quoteSymbol)) { request.AddQueryParameter("quoteSymbol", quoteSymbol); } + if (!string.IsNullOrEmpty(baseId) && !string.IsNullOrWhiteSpace(baseId)) { request.AddQueryParameter("baseId", baseId); } + if (!string.IsNullOrEmpty(quoteId) && !string.IsNullOrWhiteSpace(quoteId)) { request.AddQueryParameter("quoteId", quoteId); } + if (!string.IsNullOrEmpty(assetSymbol) && !string.IsNullOrWhiteSpace(assetSymbol)) { request.AddQueryParameter("assetSymbol", assetSymbol); } + if (!string.IsNullOrEmpty(assetId) && !string.IsNullOrWhiteSpace(assetId)) { request.AddQueryParameter("assetId", assetId); } + request.AddQueryParameter("limit", limit); + request.AddQueryParameter("offset", offset); + + var jsonStr = await CoinCapClient.GetStringResponseAsync(_restClient, request, _cache, _logger, 60); + + return JsonConvert.DeserializeObject(jsonStr); + } + } +} diff --git a/CoinCapApi/Imp/RatesImp.cs b/CoinCapApi/Imp/RatesImp.cs new file mode 100644 index 0000000..01bc3f9 --- /dev/null +++ b/CoinCapApi/Imp/RatesImp.cs @@ -0,0 +1,73 @@ +// *********************************************************************** +// Assembly : CoinCapApi +// Author : ByronAP +// Created : 12-15-2022 +// +// Last Modified By : ByronAP +// Last Modified On : 12-15-2022 +// *********************************************************************** +// +// Copyright © 2022 ByronAP, CoinCap. All rights reserved. +// +// *********************************************************************** +using CoinCapApi.Models; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using RestSharp; +using System; +using System.Threading.Tasks; + +namespace CoinCapApi.Imp +{ + /// + /// Implementation of the '/rates' API calls. + /// Implementation classes do not have a public constructor + /// and must be accessed through an instance of . + /// + /// + public class RatesImp + { + private readonly RestClient _restClient; + private readonly ILogger _logger; + private readonly MemCache _cache; + + internal RatesImp(RestClient restClient, MemCache cache, ILogger logger) + { + _logger = logger; + _cache = cache; + _restClient = restClient; + } + + /// + /// Get rates of assets as an asynchronous operation. + /// + /// + /// A Task<> representing the asynchronous operation. + public async Task GetRatesAsync() + { + var request = new RestRequest(CoinCapClient.BuildUrl("rates")); + + var jsonStr = await CoinCapClient.GetStringResponseAsync(_restClient, request, _cache, _logger, 15); + + return JsonConvert.DeserializeObject(jsonStr); + } + + /// + /// Get the rate of an asset as an asynchronous operation. + /// + /// The asset id to get the rate for. + /// + /// A Task<> representing the asynchronous operation. + /// id - Null or invalid value, id must be a valid asset id. + public async Task GetRateAsync(string id) + { + if (string.IsNullOrEmpty(id) || string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException(nameof(id), "Null or invalid value, id must be a valid asset id."); } + + var request = new RestRequest(CoinCapClient.BuildUrl("rates", id)); + + var jsonStr = await CoinCapClient.GetStringResponseAsync(_restClient, request, _cache, _logger, 15); + + return JsonConvert.DeserializeObject(jsonStr); + } + } +} diff --git a/CoinCapApi/MemCache.cs b/CoinCapApi/MemCache.cs new file mode 100644 index 0000000..b878c6c --- /dev/null +++ b/CoinCapApi/MemCache.cs @@ -0,0 +1,132 @@ +using Microsoft.Extensions.Logging; +using RestSharp; +using System; +using System.Collections.Generic; +using System.Runtime.Caching; + +namespace CoinCapApi +{ + internal class MemCache : IDisposable + { + internal bool Enabled { get; set; } = true; + + private readonly ILogger _logger; + private readonly List _keys; + private readonly MemoryCache _cache; + private readonly object _lockObject; + + internal MemCache(ILogger logger) + { + _logger = logger; + _cache = new MemoryCache("response-cache"); + _keys = new List(); + _lockObject = new object(); + } + + private void CacheRemovedCallback(CacheEntryRemovedArguments arguments) + { + lock (_lockObject) + { + _keys.Remove(arguments.CacheItem.Key); + } + } + + internal bool Contains(string key) => _cache.Contains(key); + + internal bool TryGet(string key, out object value) + { + if (!Enabled) + { + _logger?.LogDebug("Cache Disabled for URL: {Key}", key); + + value = null; + return false; + } + + if (_cache.Contains(key)) + { + _logger?.LogDebug("Cache Hit for URL: {Key}", key); + + value = _cache.Get(key); + return true; + } + else + { + _logger?.LogDebug("Cache Miss for URL: {Key}", key); + value = null; + return false; + } + } + + internal void CacheRequest(string key, RestResponse response, int cacheSeconds) + { + if (!Enabled) { return; } + + var data = response.Content; + + if (!string.IsNullOrEmpty(data) && !string.IsNullOrWhiteSpace(data)) + { + if (cacheSeconds <= Constants.API_MIN_CACHE_TIME_MS / 1000) { cacheSeconds = Convert.ToInt16(Constants.API_MIN_CACHE_TIME_MS / 1000); } + + var expiry = DateTimeOffset.UtcNow.AddSeconds(cacheSeconds); + + if (expiry < DateTimeOffset.UtcNow.AddMinutes(2)) + { + Set(key, data, expiry); + _logger?.LogDebug("Cache Set Expires in: {Expiry} seconds for URL: {Key}", cacheSeconds, key); + } + else + { + _logger?.LogWarning("The expires is too far in the future. Expiry: {Expiry}, URL: {FullUrl}", expiry, key); + } + } + } + + private void Set(string key, object value, DateTimeOffset exp) + { + lock (_lockObject) + { + var cacheItem = new CacheItem(key, value); + var policy = new CacheItemPolicy + { + AbsoluteExpiration = exp + }; + policy.RemovedCallback += CacheRemovedCallback; + + _cache.Set(cacheItem, policy); + + if (!_keys.Contains(key)) { _keys.Add(key); } + } + } + + internal void Clear() + { + lock (_lockObject) + { + var keys = _keys.ToArray(); + foreach (var key in keys) + { + try + { + _cache.Remove(key); + } + catch + { + // ignore + } + } + + try + { + _keys.Clear(); + } + catch + { + // ignore + } + } + } + + public void Dispose() => _cache.Dispose(); + } +} diff --git a/CoinCapApi/Models/AssetData.cs b/CoinCapApi/Models/AssetData.cs new file mode 100644 index 0000000..5317f54 --- /dev/null +++ b/CoinCapApi/Models/AssetData.cs @@ -0,0 +1,79 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class AssetData + { + /// + /// The unique identifier for this asset. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The rank is in ascending order - this number is directly associated with the marketcap whereas the highest marketcap receives rank 1. + /// + [JsonProperty("rank")] + public int Rank { get; set; } + + /// + /// The most common symbol used to identify this asset on an exchange. + /// + [JsonProperty("symbol")] + public string Symbol { get; set; } + + /// + /// The proper name for this asset. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The available supply for trading. + /// + [JsonProperty("supply")] + public double? Supply { get; set; } + + /// + /// The total quantity of asset issued. + /// + [JsonProperty("maxSupply")] + public double? MaxSupply { get; set; } + + /// + /// The total value of the asset in circulation (marketcap = supply x price) in USD. + /// + [JsonProperty("marketCapUsd")] + public double? MarketCapUsd { get; set; } + + /// + /// The quantity of trading volume represented in USD over the last 24 hours. + /// + [JsonProperty("volumeUsd24Hr")] + public double? VolumeUsd24Hr { get; set; } + + /// + /// The volume-weighted price based on real-time market data, translated to USD. + /// + [JsonProperty("priceUsd")] + public decimal? PriceUsd { get; set; } + + /// + /// The direction and value change in the last 24 hours. + /// + [JsonProperty("changePercent24Hr")] + public decimal? ChangePercent24Hr { get; set; } + + /// + /// The Volume Weighted Average Price in the last 24 hours. + /// + [JsonProperty("vwap24Hr")] + public decimal? Vwap24Hr { get; set; } + + /// + /// Address of the assets blockchain explorer. + /// + [JsonProperty("explorer")] + public string Explorer { get; set; } + } +} diff --git a/CoinCapApi/Models/AssetHistoryData.cs b/CoinCapApi/Models/AssetHistoryData.cs new file mode 100644 index 0000000..264b9d3 --- /dev/null +++ b/CoinCapApi/Models/AssetHistoryData.cs @@ -0,0 +1,32 @@ +using Newtonsoft.Json; +using System; + +namespace CoinCapApi.Models +{ + public class AssetHistoryData + { + /// + /// The volume-weighted price based on real-time market data, translated to USD. + /// + [JsonProperty("priceUsd")] + public string PriceUsd { get; set; } + + /// + /// Timestamp in UNIX milliseconds. + /// + [JsonProperty("time")] + public long? Time { get; set; } + + /// + /// The available supply for trading. + /// + [JsonProperty("circulatingSupply")] + public string CirculatingSupply { get; set; } + + /// + /// The date. + /// + [JsonProperty("date")] + public DateTimeOffset? Date { get; set; } + } +} diff --git a/CoinCapApi/Models/AssetHistoryResponse.cs b/CoinCapApi/Models/AssetHistoryResponse.cs new file mode 100644 index 0000000..de591e8 --- /dev/null +++ b/CoinCapApi/Models/AssetHistoryResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public partial class AssetHistoryResponse + { + [JsonProperty("data")] + public AssetHistoryData[] Data { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } +} diff --git a/CoinCapApi/Models/AssetMarketData.cs b/CoinCapApi/Models/AssetMarketData.cs new file mode 100644 index 0000000..a27d815 --- /dev/null +++ b/CoinCapApi/Models/AssetMarketData.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class AssetMarketData + { + /// + /// The unique identifier for exchange. + /// + [JsonProperty("exchangeId")] + public string ExchangeId { get; set; } + + /// + /// The unique identifier for the base asset, base is asset purchased. + /// + [JsonProperty("baseId")] + public string BaseId { get; set; } + + /// + /// The unique identifier for the quote asset, quote is asset used to purchase base. + /// + [JsonProperty("quoteId")] + public string QuoteId { get; set; } + + /// + /// The most common symbol used to identify the base asset, base is asset purchased. + /// + [JsonProperty("baseSymbol")] + public string BaseSymbol { get; set; } + + /// + /// The most common symbol used to identify the quote asset, quote is asset used to purchase base. + /// + [JsonProperty("quoteSymbol")] + public string QuoteSymbol { get; set; } + + /// + /// The volume transacted on this market in last 24 hours. + /// + [JsonProperty("volumeUsd24Hr")] + public double? VolumeUsd24Hr { get; set; } + + /// + /// The amount of quote asset traded for one unit of base asset. + /// + [JsonProperty("priceUsd")] + public decimal? PriceUsd { get; set; } + + /// + /// The percent of quote asset volume. + /// + [JsonProperty("volumePercent")] + public decimal? VolumePercent { get; set; } + } +} diff --git a/CoinCapApi/Models/AssetMarketsResponse.cs b/CoinCapApi/Models/AssetMarketsResponse.cs new file mode 100644 index 0000000..075136d --- /dev/null +++ b/CoinCapApi/Models/AssetMarketsResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class AssetMarketsResponse + { + [JsonProperty("data")] + public AssetMarketData[] Data { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } +} diff --git a/CoinCapApi/Models/AssetResponse.cs b/CoinCapApi/Models/AssetResponse.cs new file mode 100644 index 0000000..2dfc72e --- /dev/null +++ b/CoinCapApi/Models/AssetResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class AssetResponse + { + [JsonProperty("data")] + public AssetData Data { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } +} diff --git a/CoinCapApi/Models/AssetsResponse.cs b/CoinCapApi/Models/AssetsResponse.cs new file mode 100644 index 0000000..ee28a9a --- /dev/null +++ b/CoinCapApi/Models/AssetsResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class AssetsResponse + { + [JsonProperty("data")] + public AssetData[] Data { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } +} diff --git a/CoinCapApi/Models/CandleData.cs b/CoinCapApi/Models/CandleData.cs new file mode 100644 index 0000000..4b6ffde --- /dev/null +++ b/CoinCapApi/Models/CandleData.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class CandleData + { + /// + /// The price (quote) at which the first transaction was completed in a given time period. + /// + [JsonProperty("open")] + public decimal Open { get; set; } + + /// + /// The highest price (quote) at which the base was traded during the time period. + /// + [JsonProperty("high")] + public decimal High { get; set; } + + /// + /// The lowest price (quote) at which the base was traded during the time period. + /// + [JsonProperty("low")] + public decimal Low { get; set; } + + /// + /// The price (quote) at which the last transaction was completed in a given time period. + /// + [JsonProperty("close")] + public decimal Close { get; set; } + + /// + /// The amount of base asset traded in the given time period. + /// + [JsonProperty("volume")] + public decimal Volume { get; set; } + + /// + /// The timestamp for starting of the time period (bucket), represented in UNIX milliseconds. + /// + [JsonProperty("period")] + public long Period { get; set; } + } +} diff --git a/CoinCapApi/Models/CandlesResponse.cs b/CoinCapApi/Models/CandlesResponse.cs new file mode 100644 index 0000000..1b4eb93 --- /dev/null +++ b/CoinCapApi/Models/CandlesResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class CandlesResponse + { + [JsonProperty("data")] + public CandleData[] Data { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } +} diff --git a/CoinCapApi/Models/ExchangeData.cs b/CoinCapApi/Models/ExchangeData.cs new file mode 100644 index 0000000..5248848 --- /dev/null +++ b/CoinCapApi/Models/ExchangeData.cs @@ -0,0 +1,62 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class ExchangeData + { + /// + /// The unique identifier for the exchange. + /// + [JsonProperty("exchangeId")] + public string ExchangeId { get; set; } + + /// + /// The proper name of the exchange. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The rank is in ascending order - this number is directly associated with the total exchange volume whereas the highest volume exchange receives rank 1. + /// + [JsonProperty("rank")] + public int Rank { get; set; } + + /// + /// The amount of daily volume a single exchange transacts in relation to total daily volume of all exchanges. + /// + [JsonProperty("percentTotalVolume")] + public decimal? PercentTotalVolume { get; set; } + + /// + /// The exchanges daily volume represented in USD. + /// + [JsonProperty("volumeUsd")] + public double? VolumeUsd { get; set; } + + /// + /// The number of trading pairs (or markets) offered by exchange. + /// + [JsonProperty("tradingPairs")] + public int TradingPairs { get; set; } + + /// + /// Is trade data for this exchange available via websocket. + /// true/false, true = trade socket available, false = trade socket unavailable + /// + [JsonProperty("socket")] + public bool? Socket { get; set; } + + /// + /// The web address of the exchange. + /// + [JsonProperty("exchangeUrl")] + public string ExchangeUrl { get; set; } + + /// + /// UNIX timestamp (milliseconds) since information was received from this exchange. + /// + [JsonProperty("updated")] + public long Updated { get; set; } + } +} diff --git a/CoinCapApi/Models/ExchangeResponse.cs b/CoinCapApi/Models/ExchangeResponse.cs new file mode 100644 index 0000000..bb6cd4d --- /dev/null +++ b/CoinCapApi/Models/ExchangeResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class ExchangeResponse + { + [JsonProperty("data")] + public ExchangeData Data { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } +} diff --git a/CoinCapApi/Models/ExchangesResponse.cs b/CoinCapApi/Models/ExchangesResponse.cs new file mode 100644 index 0000000..e72210d --- /dev/null +++ b/CoinCapApi/Models/ExchangesResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class ExchangesResponse + { + [JsonProperty("data")] + public ExchangeData[] Data { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } +} diff --git a/CoinCapApi/Models/MarketData.cs b/CoinCapApi/Models/MarketData.cs new file mode 100644 index 0000000..4cd641d --- /dev/null +++ b/CoinCapApi/Models/MarketData.cs @@ -0,0 +1,79 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class MarketData + { + /// + /// The unique identifier for the exchange. + /// + [JsonProperty("exchangeId")] + public string ExchangeId { get; set; } + + /// + /// The rank is in ascending order - this number represents the amount of volume transacted by this market in relation to other markets on that exchange. + /// + [JsonProperty("rank")] + public int? Rank { get; set; } + + /// + /// The most common symbol used to identify the base asset, base is asset purchased. + /// + [JsonProperty("baseSymbol")] + public string BaseSymbol { get; set; } + + /// + /// The unique identifier for the base asset, base is asset purchased. + /// + [JsonProperty("baseId")] + public string BaseId { get; set; } + + /// + /// The most common symbol used to identify the quote asset, quote is asset used to purchase base. + /// + [JsonProperty("quoteSymbol")] + public string QuoteSymbol { get; set; } + + /// + /// The unique identifier for the quote asset, quote is asset used to purchase base. + /// + [JsonProperty("quoteId")] + public string QuoteId { get; set; } + + /// + /// The amount of quote asset traded for one unit of base asset. + /// + [JsonProperty("priceQuote")] + public decimal? PriceQuote { get; set; } + + /// + /// The quote price translated to USD. + /// + [JsonProperty("priceUsd")] + public decimal? PriceUsd { get; set; } + + /// + /// The volume transacted on this market in last 24 hours. + /// + [JsonProperty("volumeUsd24Hr")] + public double? VolumeUsd24Hr { get; set; } + + /// + /// The amount of daily volume a single market transacts in relation to total daily volume of all markets on the exchange. + /// + [JsonProperty("percentExchangeVolume")] + public decimal? PercentExchangeVolume { get; set; } + + /// + /// The number of trades on this market in the last 24 hours. + /// + [JsonProperty("tradesCount24Hr")] + public int? TradesCount24Hr { get; set; } + + /// + /// UNIX timestamp (milliseconds) since information was received from this particular market. + /// + [JsonProperty("updated")] + public long? Updated { get; set; } + } +} diff --git a/CoinCapApi/Models/MarketsResponse.cs b/CoinCapApi/Models/MarketsResponse.cs new file mode 100644 index 0000000..7ca00f4 --- /dev/null +++ b/CoinCapApi/Models/MarketsResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class MarketsResponse + { + [JsonProperty("data")] + public MarketData[] Data { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } +} diff --git a/CoinCapApi/Models/RateData.cs b/CoinCapApi/Models/RateData.cs new file mode 100644 index 0000000..5446165 --- /dev/null +++ b/CoinCapApi/Models/RateData.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class RateData + { + /// + /// The unique identifier for asset or fiat. + /// + [JsonProperty("id")] + public string Id { get; set; } + + /// + /// The most common symbol used to identify asset or fiat. + /// + [JsonProperty("symbol")] + public string Symbol { get; set; } + + /// + /// The currency symbol used to identify asset or fiat. + /// + [JsonProperty("currencySymbol")] + public string CurrencySymbol { get; set; } + + /// + /// The type of currency (fiat or crypto). + /// + [JsonProperty("type")] + public string Type { get; set; } + + /// + /// The rate conversion to USD. + /// + [JsonProperty("rateUsd")] + public decimal? RateUsd { get; set; } + } +} diff --git a/CoinCapApi/Models/RateResponse.cs b/CoinCapApi/Models/RateResponse.cs new file mode 100644 index 0000000..8c0eba9 --- /dev/null +++ b/CoinCapApi/Models/RateResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class RateResponse + { + [JsonProperty("data")] + public RateData Data { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } +} diff --git a/CoinCapApi/Models/RatesResponse.cs b/CoinCapApi/Models/RatesResponse.cs new file mode 100644 index 0000000..fe7efb3 --- /dev/null +++ b/CoinCapApi/Models/RatesResponse.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace CoinCapApi.Models +{ + public class RatesResponse + { + [JsonProperty("data")] + public RateData[] Data { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + } +} diff --git a/CoinCapApi/Models/WSTrade.cs b/CoinCapApi/Models/WSTrade.cs new file mode 100644 index 0000000..7eea2c0 --- /dev/null +++ b/CoinCapApi/Models/WSTrade.cs @@ -0,0 +1,34 @@ +using Newtonsoft.Json; +using System; + +namespace CoinCapApi.Models +{ + public class WSTrade + { + public Guid WSInstanceId { get; set; } + + [JsonProperty("exchange")] + public string Exchange { get; set; } + + [JsonProperty("base")] + public string Base { get; set; } + + [JsonProperty("quote")] + public string Quote { get; set; } + + [JsonProperty("direction")] + public string Direction { get; set; } + + [JsonProperty("price")] + public decimal Price { get; set; } + + [JsonProperty("volume")] + public decimal Volume { get; set; } + + [JsonProperty("timestamp")] + public long Timestamp { get; set; } + + [JsonProperty("priceUsd")] + public decimal PriceUsd { get; set; } + } +} diff --git a/CoinCapApi/Types/TimeInterval.cs b/CoinCapApi/Types/TimeInterval.cs new file mode 100644 index 0000000..d9c1c56 --- /dev/null +++ b/CoinCapApi/Types/TimeInterval.cs @@ -0,0 +1,42 @@ +namespace CoinCapApi.Types +{ + public enum TimeInterval + { + /// + /// 1 minute + /// + M1 = 1, + /// + /// 5 minutes + /// + M5 = 2, + /// + /// 15 minutes + /// + M15 = 4, + /// + /// 30 minutes + /// + M30 = 8, + /// + /// 1 hour + /// + H1 = 16, + /// + /// 2 hours + /// + H2 = 32, + /// + /// 6 hours + /// + H6 = 64, + /// + /// 12 hours + /// + H12 = 128, + /// + /// 1 day (24 hours) + /// + D1 = 256 + } +} diff --git a/README.md b/README.md index be8084c..57e4aad 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,14 @@ ### Features -+ TODO ++ Supports all API calls ++ Supports WebSocket API ++ Method names and locations match API ++ Concrete classes ++ Fully asynchronous ++ Compatible with dependency injection and logging ++ Integrated response caching ++ Easier to use then other libraries + + +Just create an instance of 'CoinCapClient' and start making calls. diff --git a/Tests/AssetsTests.cs b/Tests/AssetsTests.cs new file mode 100644 index 0000000..16112de --- /dev/null +++ b/Tests/AssetsTests.cs @@ -0,0 +1,61 @@ +namespace Tests +{ + public class AssetsTests + { + [Test] + public async Task GetAssetsTest() + { + var requestResult = await Helpers.GetApiClient().Assets.GetAssetsAsync(); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + + requestResult = await Helpers.GetApiClient().Assets.GetAssetsAsync(); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + } + + [Test] + public async Task GetAssetTest() + { + var requestResult = await Helpers.GetApiClient().Assets.GetAssetAsync("bitcoin"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data.Id, Is.EqualTo("bitcoin")); + + requestResult = await Helpers.GetApiClient().Assets.GetAssetAsync("ethereum"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data.Id, Is.EqualTo("ethereum")); + } + + [Test] + public async Task GetAssetHistoryTest() + { + var requestResult = await Helpers.GetApiClient().Assets.GetAssetHistoryAsync("bitcoin", CoinCapApi.Types.TimeInterval.M1); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + + requestResult = await Helpers.GetApiClient().Assets.GetAssetHistoryAsync("ethereum", CoinCapApi.Types.TimeInterval.D1, Convert.ToUInt64(DateTimeOffset.UtcNow.AddYears(-1).AddDays(-60).ToUnixTimeMilliseconds()), Convert.ToUInt64(DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeMilliseconds())); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + } + + [Test] + public async Task GetAssetMarketsTest() + { + var requestResult = await Helpers.GetApiClient().Assets.GetAssetMarketsAsync("bitcoin"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + + requestResult = await Helpers.GetApiClient().Assets.GetAssetMarketsAsync("ethereum", offset: 100); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + } + } +} diff --git a/Tests/ExchangesTests.cs b/Tests/ExchangesTests.cs new file mode 100644 index 0000000..e76a497 --- /dev/null +++ b/Tests/ExchangesTests.cs @@ -0,0 +1,28 @@ +namespace Tests +{ + public class ExchangesTests + { + [Test] + public async Task GetExchangesTest() + { + var requestResult = await Helpers.GetApiClient().Exchanges.GetExchangesAsync(); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + } + + [Test] + public async Task GetExchangeTest() + { + var requestResult = await Helpers.GetApiClient().Exchanges.GetExchangeAsync("binance"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data.ExchangeId, Is.EqualTo("binance")); + + requestResult = await Helpers.GetApiClient().Exchanges.GetExchangeAsync("kraken"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data.ExchangeId, Is.EqualTo("kraken")); + } + } +} diff --git a/Tests/Helpers.cs b/Tests/Helpers.cs new file mode 100644 index 0000000..e18f49a --- /dev/null +++ b/Tests/Helpers.cs @@ -0,0 +1,24 @@ +namespace Tests +{ + internal static class Helpers + { + private static CoinCapClient? _apiClient = null; + + internal static CoinCapClient GetApiClient() + { + if (_apiClient == null) + { + var factory = LoggerFactory.Create(x => + { + x.AddConsole(); + x.SetMinimumLevel(LogLevel.Debug); + }); + var logger = factory.CreateLogger(); + + _apiClient = new CoinCapClient(logger: logger); + } + + return _apiClient; + } + } +} diff --git a/Tests/MarketsTests.cs b/Tests/MarketsTests.cs new file mode 100644 index 0000000..de3505a --- /dev/null +++ b/Tests/MarketsTests.cs @@ -0,0 +1,117 @@ +namespace Tests +{ + public class MarketsTests + { + [Test] + public async Task GetMarketsTest() + { + var requestResult = await Helpers.GetApiClient().Markets.GetMarketsAsync(); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + } + + [Test] + public async Task GetMarketsByExchangeTest() + { + var requestResult = await Helpers.GetApiClient().Markets.GetMarketsAsync(exchangeId: "gdax"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + Assert.That(requestResult.Data.First().ExchangeId.ToLowerInvariant(), Is.EqualTo("gdax")); + } + + [Test] + public async Task GetMarketsByBaseSymbolTest() + { + var requestResult = await Helpers.GetApiClient().Markets.GetMarketsAsync(baseSymbol: "eth"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + Assert.That(requestResult.Data.First().BaseSymbol.ToLowerInvariant(), Is.EqualTo("eth")); + } + + [Test] + public async Task GetMarketsByQuoteSymbolTest() + { + var requestResult = await Helpers.GetApiClient().Markets.GetMarketsAsync(quoteSymbol: "eth"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + Assert.That(requestResult.Data.First().QuoteSymbol.ToLowerInvariant(), Is.EqualTo("eth")); + } + + [Test] + public async Task GetMarketsByBaseIdTest() + { + var requestResult = await Helpers.GetApiClient().Markets.GetMarketsAsync(baseId: "ethereum"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + Assert.That(requestResult.Data.First().BaseId.ToLowerInvariant(), Is.EqualTo("ethereum")); + } + + + [Test] + public async Task GetMarketsByQuoteIdTest() + { + var requestResult = await Helpers.GetApiClient().Markets.GetMarketsAsync(quoteId: "ethereum"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + Assert.That(requestResult.Data.First().QuoteId.ToLowerInvariant(), Is.EqualTo("ethereum")); + } + + [Test] + public async Task GetMarketsByAssetSymbolTest() + { + var requestResult = await Helpers.GetApiClient().Markets.GetMarketsAsync(assetSymbol: "eth"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + + if (requestResult.Data.First().QuoteSymbol.Equals("eth", StringComparison.InvariantCultureIgnoreCase)) + { + Assert.Pass(); + } + + if (requestResult.Data.First().BaseSymbol.Equals("eth", StringComparison.InvariantCultureIgnoreCase)) + { + Assert.Pass(); + } + + Assert.Fail(); + } + + [Test] + public async Task GetMarketsByAssetIdTest() + { + var requestResult = await Helpers.GetApiClient().Markets.GetMarketsAsync(assetId: "ethereum"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + + if (requestResult.Data.First().QuoteId.Equals("ethereum", StringComparison.InvariantCultureIgnoreCase)) + { + Assert.Pass(); + } + + if (requestResult.Data.First().BaseId.Equals("ethereum", StringComparison.InvariantCultureIgnoreCase)) + { + Assert.Pass(); + } + + Assert.Fail(); + } + + [Test] + public async Task GetMarketsLimitTest() + { + var requestResult = await Helpers.GetApiClient().Markets.GetMarketsAsync(limit: 2); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + Assert.That(requestResult.Data.Length, Is.EqualTo(2)); + } + } +} diff --git a/Tests/RatesTests.cs b/Tests/RatesTests.cs new file mode 100644 index 0000000..fbcaad8 --- /dev/null +++ b/Tests/RatesTests.cs @@ -0,0 +1,28 @@ +namespace Tests +{ + public class RatesTests + { + [Test] + public async Task GetRatesTest() + { + var requestResult = await Helpers.GetApiClient().Rates.GetRatesAsync(); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data, Is.Not.Empty); + } + + [Test] + public async Task GetRateTest() + { + var requestResult = await Helpers.GetApiClient().Rates.GetRateAsync("bitcoin"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data.Id, Is.EqualTo("bitcoin")); + + requestResult = await Helpers.GetApiClient().Rates.GetRateAsync("ethereum"); + + Assert.That(requestResult, Is.Not.Null); + Assert.That(requestResult.Data.Id, Is.EqualTo("ethereum")); + } + } +} diff --git a/Tests/Tests.cs b/Tests/Tests.cs deleted file mode 100644 index d1bc16a..0000000 --- a/Tests/Tests.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Tests -{ - public class Tests - { - [Test] - public void Test1() - { - Assert.Pass(); - } - } -} \ No newline at end of file diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index cbb7690..16df28a 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -9,6 +9,7 @@ + @@ -16,4 +17,8 @@ + + + + diff --git a/Tests/Usings.cs b/Tests/Usings.cs index cefced4..3a193e8 100644 --- a/Tests/Usings.cs +++ b/Tests/Usings.cs @@ -1 +1,3 @@ +global using CoinCapApi; +global using Microsoft.Extensions.Logging; global using NUnit.Framework; \ No newline at end of file diff --git a/Tests/WSTests.cs b/Tests/WSTests.cs new file mode 100644 index 0000000..878c040 --- /dev/null +++ b/Tests/WSTests.cs @@ -0,0 +1,73 @@ +using System.Diagnostics; + +namespace Tests +{ + public class WSTests + { + [Test] + public async Task TradesWSTest() + { + var factory = LoggerFactory.Create(x => + { + x.AddConsole(); + x.SetMinimumLevel(LogLevel.Debug); + }); + var logger = factory.CreateLogger(); + using var wsClient = new CoinCapTradesWSClient("binance", logger); + + bool tradeSeen = false; + + wsClient.OnTradeEvent += (sender, instanceId, exchangeId, trade) => { tradeSeen = true; }; + + var stopWatch = new Stopwatch(); + + await wsClient.Connect(); + + stopWatch.Start(); + + do + { + await Task.Delay(50); + } while (!tradeSeen && stopWatch.Elapsed.TotalSeconds < 20); + + stopWatch.Stop(); + + await wsClient.Disconnect(); + + Assert.That(tradeSeen, Is.True); + } + + [Test] + public async Task PricesWSTest() + { + var factory = LoggerFactory.Create(x => + { + x.AddConsole(); + x.SetMinimumLevel(LogLevel.Debug); + }); + var logger = factory.CreateLogger(); + using var wsClient = new CoinCapPricesWSClient(new[] { "ALL" }, logger); + + bool priceSeen = false; + + wsClient.OnPricesEvent += (sender, instanceId, prices) => { priceSeen = true; }; + + var stopWatch = new Stopwatch(); + + await wsClient.Connect(); + + stopWatch.Start(); + + do + { + await Task.Delay(50); + } while (!priceSeen && stopWatch.Elapsed.TotalSeconds < 20); + + stopWatch.Stop(); + + await wsClient.Disconnect(); + + Assert.That(priceSeen, Is.True); + } + } +}