1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-06-06 15:36:30 +00:00

Websocket refactoring (#190)

Websocket refactoring
This commit is contained in:
Jan Korf 2024-02-24 19:21:47 +01:00 committed by GitHub
parent d91755dff5
commit d533557324
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
585 changed files with 114831 additions and 2198 deletions

2
.gitignore vendored
View File

@ -287,5 +287,3 @@ __pycache__/
*.odx.cs
*.xsd.cs
CryptoExchange.Net/CryptoExchange.Net.xml
/Docs/*
Docs/

View File

@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></PackageReference>
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="NUnit" Version="3.13.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
</ItemGroup>

View File

@ -1,11 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.UnitTests.TestImplementations;
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using NUnit.Framework.Constraints;
namespace CryptoExchange.Net.UnitTests
{
@ -46,7 +52,7 @@ namespace CryptoExchange.Net.UnitTests
}
[TestCase]
public void SocketMessages_Should_BeProcessedInDataHandlers()
public async Task SocketMessages_Should_BeProcessedInDataHandlers()
{
// arrange
var client = new TestSocketClient(options => {
@ -58,28 +64,31 @@ namespace CryptoExchange.Net.UnitTests
socket.DisconnectTime = DateTime.UtcNow;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false);
JToken result = null;
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
{
result = messageEvent.JsonData;
rstEvent.Set();
}));
Dictionary<string, string> result = null;
client.SubClient.ConnectSocketSub(sub);
sub.AddSubscription(new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
{
result = messageEvent.Data;
rstEvent.Set();
}));
// act
socket.InvokeMessage("{\"property\": 123}");
await socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}");
rstEvent.WaitOne(1000);
// assert
Assert.IsTrue((int)result["property"] == 123);
Assert.IsTrue(result["property"] == "123");
}
[TestCase(false)]
[TestCase(true)]
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
public async Task SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{
// arrange
var client = new TestSocketClient(options => {
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
options.SubOptions.OutputOriginalData = enabled;
});
@ -90,15 +99,16 @@ namespace CryptoExchange.Net.UnitTests
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
var rstEvent = new ManualResetEvent(false);
string original = null;
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
client.SubClient.ConnectSocketSub(sub);
sub.AddSubscription(new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
{
original = messageEvent.OriginalData;
rstEvent.Set();
}));
client.SubClient.ConnectSocketSub(sub);
// act
socket.InvokeMessage("{\"property\": 123}");
await socket.InvokeMessage("{\"property\": 123}");
rstEvent.WaitOne(1000);
// assert
@ -109,16 +119,18 @@ namespace CryptoExchange.Net.UnitTests
public void UnsubscribingStream_Should_CloseTheSocket()
{
// arrange
var client = new TestSocketClient(options => {
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
});
});
var socket = client.CreateSocket();
socket.CanConnect = true;
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
client.SubClient.ConnectSocketSub(sub);
var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { });
var ups = new UpdateSubscription(sub, us);
sub.AddSubscription(us);
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
var ups = new UpdateSubscription(sub, subscription);
sub.AddSubscription(subscription);
// act
client.UnsubscribeAsync(ups).Wait();
@ -140,12 +152,13 @@ namespace CryptoExchange.Net.UnitTests
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
client.SubClient.ConnectSocketSub(sub1);
client.SubClient.ConnectSocketSub(sub2);
var us1 = SocketSubscription.CreateForIdentifier(10, "Test1", true, false, (e) => { });
var us2 = SocketSubscription.CreateForIdentifier(11, "Test2", true, false, (e) => { });
sub1.AddSubscription(us1);
sub2.AddSubscription(us2);
var ups1 = new UpdateSubscription(sub1, us1);
var ups2 = new UpdateSubscription(sub2, us2);
var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
sub1.AddSubscription(subscription1);
sub2.AddSubscription(subscription2);
var ups1 = new UpdateSubscription(sub1, subscription1);
var ups2 = new UpdateSubscription(sub2, subscription2);
// act
client.UnsubscribeAllAsync().Wait();

View File

@ -5,8 +5,8 @@ using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.OrderBook;
using CryptoExchange.Net.Sockets;
using NUnit.Framework;
namespace CryptoExchange.Net.UnitTests
@ -14,13 +14,13 @@ namespace CryptoExchange.Net.UnitTests
[TestFixture]
public class SymbolOrderBookTests
{
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions();
private static readonly OrderBookOptions _defaultOrderBookOptions = new OrderBookOptions();
private class TestableSymbolOrderBook : SymbolOrderBook
{
public TestableSymbolOrderBook() : base(null, "Test", "BTC/USD")
{
Initialize(defaultOrderBookOptions);
Initialize(_defaultOrderBookOptions);
}

View File

@ -0,0 +1,19 @@
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestQuery : Query<object>
{
public override HashSet<string> ListenerIdentifiers { get; set; }
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
ListenerIdentifiers = new HashSet<string> { identifier };
}
}
}

View File

@ -0,0 +1,36 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
{
internal class TestSubscription<T> : Subscription<object, object>
{
private readonly Action<DataEvent<T>> _handler;
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "topic" };
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
{
_handler = handler;
}
public override Task<CallResult> DoHandleMessageAsync(SocketConnection connection, DataEvent<object> message)
{
var data = (T)message.Data;
_handler.Invoke(message.As(data));
return Task.FromResult(new CallResult(null));
}
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
public override Query GetUnsubQuery() => new TestQuery("unsub", new object(), false, 1);
}
}

View File

@ -1,4 +1,6 @@
using System;
using System.IO;
using System.Net.WebSockets;
using System.Security.Authentication;
using System.Text;
using System.Threading.Tasks;
@ -12,16 +14,15 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
public bool CanConnect { get; set; }
public bool Connected { get; set; }
public event Action OnClose;
public event Func<Task> OnClose;
#pragma warning disable 0067
public event Action OnReconnected;
public event Action OnReconnecting;
public event Func<Task> OnReconnected;
public event Func<Task> OnReconnecting;
#pragma warning restore 0067
public event Action<int> OnRequestSent;
public event Action<string> OnMessage;
public event Action<Exception> OnError;
public event Action OnOpen;
public event Func<int, Task> OnRequestSent;
public event Func<WebSocketMessageType, Stream, Task> OnStreamMessage;
public event Func<Exception, Task> OnError;
public event Func<Task> OnOpen;
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
public int Id { get; }
@ -110,9 +111,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
OnOpen?.Invoke();
}
public void InvokeMessage(string data)
public async Task InvokeMessage(string data)
{
OnMessage?.Invoke(data);
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
await OnStreamMessage?.Invoke(WebSocketMessageType.Text, stream);
}
public void SetProxy(ApiProxy proxy)

View File

@ -5,7 +5,9 @@ using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using Moq;
using Newtonsoft.Json.Linq;
@ -88,35 +90,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
return ConnectSocketAsync(sub).Result;
}
protected internal override bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
{
throw new NotImplementedException();
}
protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message,
out CallResult<object> callResult)
{
throw new NotImplementedException();
}
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, object request)
{
throw new NotImplementedException();
}
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, string identifier)
{
return true;
}
protected internal override Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection s)
{
throw new NotImplementedException();
}
protected internal override Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription s)
{
throw new NotImplementedException();
}
public override string GetListenerIdentifier(IMessageAccessor messageAccessor) => "topic";
}
}

View File

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
@ -251,7 +249,6 @@ namespace CryptoExchange.Net
if (OutputOriginalData == true)
{
data = await reader.ReadToEndAsync().ConfigureAwait(false);
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: " + data);
var result = Deserialize<T>(data, serializer, requestId);
result.OriginalData = data;
return result;
@ -260,7 +257,6 @@ namespace CryptoExchange.Net
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
using var jsonReader = new JsonTextReader(reader);
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
}
catch (JsonReaderException jre)

View File

@ -1,5 +1,4 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net
@ -101,8 +101,10 @@ namespace CryptoExchange.Net
public string GetSubscriptionsState()
{
var result = new StringBuilder();
foreach(var client in ApiClients.OfType<SocketApiClient>())
result.AppendLine(client.GetSubscriptionsState());
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
{
result.AppendLine(client.GetSubscriptionsState());
}
return result.ToString();
}
}

View File

@ -0,0 +1,67 @@
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Clients
{
/// <summary>
/// Base crypto client
/// </summary>
public class CryptoBaseClient : IDisposable
{
private Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
/// <summary>
/// Service provider
/// </summary>
protected readonly IServiceProvider? _serviceProvider;
/// <summary>
/// ctor
/// </summary>
public CryptoBaseClient() { }
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoBaseClient(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_serviceCache = new Dictionary<Type, object>();
}
/// <summary>
/// Try get a client by type for the service collection
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T TryGet<T>(Func<T> createFunc)
{
var type = typeof(T);
if (_serviceCache.TryGetValue(type, out var value))
return (T)value;
if (_serviceProvider == null)
{
// Create with default options
var createResult = createFunc();
_serviceCache.Add(typeof(T), createResult!);
return createResult;
}
var result = _serviceProvider.GetService<T>()
?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
_serviceCache.Add(type, result!);
return result;
}
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
_serviceCache.Clear();
}
}
}

View File

@ -0,0 +1,47 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.CommonClients;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Linq;
namespace CryptoExchange.Net.Clients
{
/// <inheritdoc />
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoRestClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
/// <summary>
/// Get a list of the registered ISpotClient implementations
/// </summary>
/// <returns></returns>
public IEnumerable<ISpotClient> GetSpotClients()
{
if (_serviceProvider == null)
return new List<ISpotClient>();
return _serviceProvider.GetServices<ISpotClient>().ToList();
}
/// <summary>
/// Get an ISpotClient implementation by exchange name
/// </summary>
/// <param name="exchangeName"></param>
/// <returns></returns>
public ISpotClient? SpotClient(string exchangeName) => _serviceProvider.GetServices<ISpotClient>()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase));
}
}

View File

@ -0,0 +1,24 @@
using CryptoExchange.Net.Interfaces;
using System;
namespace CryptoExchange.Net.Clients
{
/// <inheritdoc />
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
{
/// <summary>
/// ctor
/// </summary>
public CryptoSocketClient()
{
}
/// <summary>
/// ctor
/// </summary>
/// <param name="serviceProvider"></param>
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
}
}

View File

@ -116,9 +116,9 @@ namespace CryptoExchange.Net
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
if (!result)
_logger.Log(LogLevel.Warning, $"[{result.RequestId}] Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
_logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] {result.ResponseStatusCode} Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
else
_logger.Log(LogLevel.Debug, $"[{result.RequestId}] Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
_logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] {result.ResponseStatusCode} Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
continue;
@ -170,9 +170,9 @@ namespace CryptoExchange.Net
var result = await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
if (!result)
_logger.Log(LogLevel.Warning, $"[{result.RequestId}] Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
_logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] {result.ResponseStatusCode} Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}");
else
_logger.Log(LogLevel.Debug, $"[{result.RequestId}] Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
_logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] {result.ResponseStatusCode} Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}");
if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false))
continue;
@ -224,7 +224,7 @@ namespace CryptoExchange.Net
var syncTimeResult = await syncTask.ConfigureAwait(false);
if (!syncTimeResult)
{
_logger.Log(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
_logger.Log(LogLevel.Debug, $"[Req {requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
return syncTimeResult.As<IRequest>(default);
}
}
@ -242,11 +242,11 @@ namespace CryptoExchange.Net
if (signed && AuthenticationProvider == null)
{
_logger.Log(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
_logger.Log(LogLevel.Warning, $"[Req {requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
return new CallResult<IRequest>(new NoApiCredentialsError());
}
_logger.Log(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
_logger.Log(LogLevel.Information, $"[Req {requestId}] Creating request for " + uri);
var paramsPosition = parameterPosition ?? ParameterPositions[method];
var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestBodyFormat ?? this.requestBodyFormat, requestId, additionalHeaders);
@ -259,7 +259,7 @@ namespace CryptoExchange.Net
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
TotalRequestsMade++;
_logger.Log(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}");
_logger.Log(LogLevel.Trace, $"[Req {requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}");
return new CallResult<IRequest>(request);
}

View File

@ -1,18 +1,21 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static CryptoExchange.Net.Objects.RateLimiter;
namespace CryptoExchange.Net
{
@ -40,47 +43,31 @@ namespace CryptoExchange.Net
/// </summary>
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
/// <summary>
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
/// </summary>
protected Func<byte[], string>? dataInterpreterBytes;
/// <summary>
/// Delegate used for processing string data received from socket connections before it is processed by handlers
/// </summary>
protected Func<string, string>? dataInterpreterString;
/// <summary>
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
/// </summary>
protected Dictionary<string, Action<MessageEvent>> genericHandlers = new();
/// <summary>
/// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry.
/// </summary>
protected Task? periodicTask;
/// <summary>
/// Wait event for the periodicTask
/// </summary>
protected AsyncResetEvent? periodicEvent;
/// <summary>
/// If true; data which is a response to a query will also be distributed to subscriptions
/// If false; data which is a response to a query won't get forwarded to subscriptions as well
/// </summary>
protected internal bool ContinueOnQueryResponse { get; protected set; }
protected List<SystemSubscription> systemSubscriptions = new();
/// <summary>
/// If a message is received on the socket which is not handled by a handler this boolean determines whether this logs an error message
/// </summary>
protected internal bool UnhandledMessageExpected { get; set; }
/// <summary>
/// If true a subscription will accept message before the confirmation of a subscription has been received
/// </summary>
protected bool HandleMessageBeforeConfirmation { get; set; }
/// <summary>
/// The rate limiters
/// </summary>
protected internal IEnumerable<IRateLimiter>? RateLimiters { get; set; }
/// <summary>
/// Periodic task regisrations
/// </summary>
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
/// <inheritdoc />
public double IncomingKbps
{
@ -104,16 +91,16 @@ namespace CryptoExchange.Net
if (!socketConnections.Any())
return 0;
return socketConnections.Sum(s => s.Value.SubscriptionCount);
return socketConnections.Sum(s => s.Value.UserSubscriptionCount);
}
}
/// <inheritdoc />
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
/// <inheritdoc />
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
#endregion
/// <summary>
@ -138,49 +125,50 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Set a delegate to be used for processing data received from socket connections before it is processed by handlers
/// Add a query to periodically send on each connection
/// </summary>
/// <param name="byteHandler">Handler for byte data</param>
/// <param name="stringHandler">Handler for string data</param>
protected void SetDataInterpreter(Func<byte[], string>? byteHandler, Func<string, string>? stringHandler)
/// <param name="identifier"></param>
/// <param name="interval"></param>
/// <param name="queryDelegate"></param>
/// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<CallResult>? callback)
{
dataInterpreterBytes = byteHandler;
dataInterpreterString = stringHandler;
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{
Identifier = identifier,
Callback = callback,
Interval = interval,
QueryDelegate = queryDelegate
});
}
/// <summary>
/// Connect to an url and listen for data on the BaseAddress
/// </summary>
/// <typeparam name="T">The type of the expected data</typeparam>
/// <param name="request">The optional request object to send, will be serialized to json</param>
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
/// <param name="dataHandler">The handler of update data</param>
/// <param name="subscription">The subscription</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync(Subscription subscription, CancellationToken ct)
{
return SubscribeAsync(BaseAddress, request, identifier, authenticated, dataHandler, ct);
return SubscribeAsync(BaseAddress, subscription, ct);
}
/// <summary>
/// Connect to an url and listen for data
/// </summary>
/// <typeparam name="T">The type of the expected data</typeparam>
/// <param name="url">The URL to connect to</param>
/// <param name="request">The optional request object to send, will be serialized to json</param>
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
/// <param name="dataHandler">The handler of update data</param>
/// <param name="subscription">The subscription</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync(string url, Subscription subscription, CancellationToken ct)
{
if (_disposing)
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
if (subscription.Authenticated && AuthenticationProvider == null)
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
SocketConnection socketConnection;
SocketSubscription? subscription;
var released = false;
// Wait for a semaphore here, so we only connect 1 socket at a time.
// This is necessary for being able to see if connections can be combined
@ -198,17 +186,17 @@ namespace CryptoExchange.Net
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, authenticated).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, subscription.Authenticated).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<UpdateSubscription>(null);
socketConnection = socketResult.Data;
// Add a subscription on the socket connection
subscription = AddSubscription(request, identifier, true, socketConnection, dataHandler, authenticated);
if (subscription == null)
var success = socketConnection.CanAddSubscription();
if (!success)
{
_logger.Log(LogLevel.Trace, $"Socket {socketConnection.SocketId} failed to add subscription, retrying on different connection");
_logger.Log(LogLevel.Trace, $"[Sckt {socketConnection.SocketId}] failed to add subscription, retrying on different connection");
continue;
}
@ -221,7 +209,7 @@ namespace CryptoExchange.Net
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, subscription.Authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<UpdateSubscription>(connectResult.Error!);
@ -236,75 +224,60 @@ namespace CryptoExchange.Net
if (socketConnection.PausedActivity)
{
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't subscribe at this moment");
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] has been paused, can't subscribe at this moment");
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
}
if (request != null)
var waitEvent = new AsyncResetEvent(false);
var subQuery = subscription.GetSubQuery(socketConnection);
if (subQuery != null)
{
if (HandleMessageBeforeConfirmation)
socketConnection.AddSubscription(subscription);
// Send the request and wait for answer
var subResult = await SubscribeAndWaitAsync(socketConnection, request, subscription).ConfigureAwait(false);
var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false);
if (!subResult)
{
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} failed to subscribe: {subResult.Error}");
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
waitEvent?.Set();
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] failed to subscribe: {subResult.Error}");
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
var unsubscribe = subResult.Error is CancellationRequestedError;
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
}
}
else
{
// No request to be sent, so just mark the subscription as comfirmed
subscription.Confirmed = true;
subscription.HandleSubQueryResponse(subQuery.Response!);
}
subscription.Confirmed = true;
if (ct != default)
{
subscription.CancellationTokenRegistration = ct.Register(async () =>
{
_logger.Log(LogLevel.Information, $"Socket {socketConnection.SocketId} Cancellation token set, closing subscription");
_logger.Log(LogLevel.Information, $"[Sckt {socketConnection.SocketId}] Cancellation token set, closing subscription {subscription.Id}");
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
}, false);
}
_logger.Log(LogLevel.Information, $"Socket {socketConnection.SocketId} subscription {subscription.Id} completed successfully");
if (!HandleMessageBeforeConfirmation)
socketConnection.AddSubscription(subscription);
waitEvent?.Set();
_logger.Log(LogLevel.Information, $"[Sckt {socketConnection.SocketId}] subscription {subscription.Id} completed successfully");
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
}
/// <summary>
/// Sends the subscribe request and waits for a response to that request
/// </summary>
/// <param name="socketConnection">The connection to send the request on</param>
/// <param name="request">The request to send, will be serialized to json</param>
/// <param name="subscription">The subscription the request is for</param>
/// <returns></returns>
protected internal virtual async Task<CallResult<bool>> SubscribeAndWaitAsync(SocketConnection socketConnection, object request, SocketSubscription subscription)
{
CallResult<object>? callResult = null;
await socketConnection.SendAndWaitAsync(request, ClientOptions.RequestTimeout, subscription, 1, data => HandleSubscriptionResponse(socketConnection, subscription, request, data, out callResult)).ConfigureAwait(false);
if (callResult?.Success == true)
{
subscription.Confirmed = true;
return new CallResult<bool>(true);
}
if (callResult == null)
return new CallResult<bool>(new ServerError("No response on subscription request received"));
return new CallResult<bool>(callResult.Error!);
}
/// <summary>
/// Send a query on a socket connection to the BaseAddress and wait for the response
/// </summary>
/// <typeparam name="T">Expected result type</typeparam>
/// <param name="request">The request to send, will be serialized to json</param>
/// <param name="authenticated">If the query is to an authenticated endpoint</param>
/// <param name="weight">Weight of the request</param>
/// <param name="query">The query</param>
/// <returns></returns>
protected virtual Task<CallResult<T>> QueryAsync<T>(object request, bool authenticated, int weight = 1)
protected virtual Task<CallResult<T>> QueryAsync<T>(Query<T> query)
{
return QueryAsync<T>(BaseAddress, request, authenticated, weight);
return QueryAsync(BaseAddress, query);
}
/// <summary>
@ -312,11 +285,9 @@ namespace CryptoExchange.Net
/// </summary>
/// <typeparam name="T">The expected result type</typeparam>
/// <param name="url">The url for the request</param>
/// <param name="request">The request to send</param>
/// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="weight">Weight of the request</param>
/// <param name="query">The query</param>
/// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, object request, bool authenticated, int weight = 1)
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, Query<T> query)
{
if (_disposing)
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
@ -326,7 +297,7 @@ namespace CryptoExchange.Net
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try
{
var socketResult = await GetSocketConnection(url, authenticated).ConfigureAwait(false);
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<T>(default);
@ -339,7 +310,7 @@ namespace CryptoExchange.Net
released = true;
}
var connectResult = await ConnectIfNeededAsync(socketConnection, authenticated).ConfigureAwait(false);
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<T>(connectResult.Error!);
}
@ -351,34 +322,11 @@ namespace CryptoExchange.Net
if (socketConnection.PausedActivity)
{
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} has been paused, can't send query at this moment");
_logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] has been paused, can't send query at this moment");
return new CallResult<T>(new ServerError("Socket is paused"));
}
return await QueryAndWaitAsync<T>(socketConnection, request, weight).ConfigureAwait(false);
}
/// <summary>
/// Sends the query request and waits for the result
/// </summary>
/// <typeparam name="T">The expected result type</typeparam>
/// <param name="socket">The connection to send and wait on</param>
/// <param name="request">The request to send</param>
/// <param name="weight">The weight of the query</param>
/// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAndWaitAsync<T>(SocketConnection socket, object request, int weight)
{
var dataResult = new CallResult<T>(new ServerError("No response on query received"));
await socket.SendAndWaitAsync(request, ClientOptions.RequestTimeout, null, weight, data =>
{
if (!HandleQueryResponse<T>(socket, request, data, out var callResult))
return false;
dataResult = callResult;
return true;
}).ConfigureAwait(false);
return dataResult;
return await socketConnection.SendAndWaitQueryAsync(query).ConfigureAwait(false);
}
/// <summary>
@ -402,149 +350,56 @@ namespace CryptoExchange.Net
if (!authenticated || socket.Authenticated)
return new CallResult<bool>(true);
_logger.Log(LogLevel.Debug, $"Socket {socket.SocketId} Attempting to authenticate");
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
if (!result)
{
_logger.Log(LogLevel.Warning, $"Socket {socket.SocketId} authentication failed");
if (socket.Connected)
await socket.CloseAsync().ConfigureAwait(false);
return await AuthenticateSocketAsync(socket).ConfigureAwait(false);
}
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult<bool>(result.Error);
/// <summary>
/// Authenticate a socket connection
/// </summary>
/// <param name="socket">Socket to authenticate</param>
/// <returns></returns>
public virtual async Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection socket)
{
if (AuthenticationProvider == null)
return new CallResult<bool>(new NoApiCredentialsError());
_logger.Log(LogLevel.Debug, $"[Sckt {socket.SocketId}] Attempting to authenticate");
var authRequest = GetAuthenticationRequest();
if (authRequest != null)
{
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
if (!result)
{
_logger.Log(LogLevel.Warning, $"[Sckt {socket.SocketId}] authentication failed");
if (socket.Connected)
await socket.CloseAsync().ConfigureAwait(false);
result.Error!.Message = "Authentication failed: " + result.Error.Message;
return new CallResult<bool>(result.Error)!;
}
}
_logger.Log(LogLevel.Debug, $"Socket {socket.SocketId} authenticated");
_logger.Log(LogLevel.Debug, $"[Sckt {socket.SocketId}] authenticated");
socket.Authenticated = true;
return new CallResult<bool>(true);
}
/// <summary>
/// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the query that was send (the request parameter).
/// For example; A query is sent in a request message with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an
/// anwser to any query that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages,
/// if not some other method has be implemented to match the messages).
/// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true.
/// Should return the request which can be used to authenticate a socket connection
/// </summary>
/// <typeparam name="T">The type of response that is expected on the query</typeparam>
/// <param name="socketConnection">The socket connection</param>
/// <param name="request">The request that a response is awaited for</param>
/// <param name="data">The message received from the server</param>
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
/// <returns>True if the message was a response to the query</returns>
protected internal abstract bool HandleQueryResponse<T>(SocketConnection socketConnection, object request, JToken data, [NotNullWhen(true)] out CallResult<T>? callResult);
/// <summary>
/// The socketConnection received data (the data JToken parameter). The implementation of this method should check if the received data is a response to the subscription request that was send (the request parameter).
/// For example; A subscribe request message is send with an Id parameter with value 10. The socket receives data and calls this method to see if the data it received is an
/// anwser to any subscription request that was done. The implementation of this method should check if the response.Id == request.Id to see if they match (assuming the api has some sort of Id tracking on messages,
/// if not some other method has be implemented to match the messages).
/// If the messages match, the callResult out parameter should be set with the deserialized data in the from of (T) and return true.
/// </summary>
/// <param name="socketConnection">The socket connection</param>
/// <param name="subscription">A subscription that waiting for a subscription response</param>
/// <param name="request">The request that the subscription sent</param>
/// <param name="data">The message received from the server</param>
/// <param name="callResult">The interpretation (null if message wasn't a response to the request)</param>
/// <returns>True if the message was a response to the subscription request</returns>
protected internal abstract bool HandleSubscriptionResponse(SocketConnection socketConnection, SocketSubscription subscription, object request, JToken data, out CallResult<object>? callResult);
/// <summary>
/// Needs to check if a received message matches a handler by request. After subscribing data message will come in. These data messages need to be matched to a specific connection
/// to pass the correct data to the correct handler. The implementation of this method should check if the message received matches the subscribe request that was sent.
/// </summary>
/// <param name="socketConnection">The socket connection the message was recieved on</param>
/// <param name="message">The received data</param>
/// <param name="request">The subscription request</param>
/// <returns>True if the message is for the subscription which sent the request</returns>
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, object request);
/// <summary>
/// Needs to check if a received message matches a handler by identifier. Generally used by GenericHandlers. For example; a generic handler is registered which handles ping messages
/// from the server. This method should check if the message received is a ping message and the identifer is the identifier of the GenericHandler
/// </summary>
/// <param name="socketConnection">The socket connection the message was recieved on</param>
/// <param name="message">The received data</param>
/// <param name="identifier">The string identifier of the handler</param>
/// <returns>True if the message is for the handler which has the identifier</returns>
protected internal abstract bool MessageMatchesHandler(SocketConnection socketConnection, JToken message, string identifier);
/// <summary>
/// Needs to authenticate the socket so authenticated queries/subscriptions can be made on this socket connection
/// </summary>
/// <param name="socketConnection">The socket connection that should be authenticated</param>
/// <returns></returns>
protected internal abstract Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection socketConnection);
/// <summary>
/// Needs to unsubscribe a subscription, typically by sending an unsubscribe request. If multiple subscriptions per socket is not allowed this can just return since the socket will be closed anyway
/// </summary>
/// <param name="connection">The connection on which to unsubscribe</param>
/// <param name="subscriptionToUnsub">The subscription to unsubscribe</param>
/// <returns></returns>
protected internal abstract Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription subscriptionToUnsub);
protected internal virtual Query? GetAuthenticationRequest() => throw new NotImplementedException();
/// <summary>
/// Optional handler to interpolate data before sending it to the handlers
/// Adds a system subscription. Used for example to reply to ping requests
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
protected internal virtual JToken ProcessTokenData(JToken message)
/// <param name="systemSubscription">The subscription</param>
protected void AddSystemSubscription(SystemSubscription systemSubscription)
{
return message;
}
/// <summary>
/// Add a subscription to a connection
/// </summary>
/// <typeparam name="T">The type of data the subscription expects</typeparam>
/// <param name="request">The request of the subscription</param>
/// <param name="identifier">The identifier of the subscription (can be null if request param is used)</param>
/// <param name="userSubscription">Whether or not this is a user subscription (counts towards the max amount of handlers on a socket)</param>
/// <param name="connection">The socket connection the handler is on</param>
/// <param name="dataHandler">The handler of the data received</param>
/// <param name="authenticated">Whether the subscription needs authentication</param>
/// <returns></returns>
protected virtual SocketSubscription? AddSubscription<T>(object? request, string? identifier, bool userSubscription, SocketConnection connection, Action<DataEvent<T>> dataHandler, bool authenticated)
{
void InternalHandler(MessageEvent messageEvent)
{
if (typeof(T) == typeof(string))
{
var stringData = (T)Convert.ChangeType(messageEvent.JsonData.ToString(), typeof(T));
dataHandler(new DataEvent<T>(stringData, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp, null));
return;
}
var desResult = Deserialize<T>(messageEvent.JsonData);
if (!desResult)
{
_logger.Log(LogLevel.Warning, $"Socket {connection.SocketId} Failed to deserialize data into type {typeof(T)}: {desResult.Error}");
return;
}
dataHandler(new DataEvent<T>(desResult.Data, null, OutputOriginalData ? messageEvent.OriginalData : null, messageEvent.ReceivedTimestamp, null));
}
var subscription = request == null
? SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), identifier!, userSubscription, authenticated, InternalHandler)
: SocketSubscription.CreateForRequest(ExchangeHelpers.NextId(), request, userSubscription, authenticated, InternalHandler);
if (!connection.AddSubscription(subscription))
return null;
return subscription;
}
/// <summary>
/// Adds a generic message handler. Used for example to reply to ping requests
/// </summary>
/// <param name="identifier">The name of the request handler. Needs to be unique</param>
/// <param name="action">The action to execute when receiving a message for this handler (checked by <see cref="MessageMatchesHandler(SocketConnection, Newtonsoft.Json.Linq.JToken,string)"/>)</param>
protected void AddGenericHandler(string identifier, Action<MessageEvent> action)
{
genericHandlers.Add(identifier, action);
var subscription = SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), identifier, false, false, action);
systemSubscriptions.Add(systemSubscription);
foreach (var connection in socketConnections.Values)
connection.AddSubscription(subscription);
connection.AddSubscription(systemSubscription);
}
/// <summary>
@ -569,13 +424,13 @@ namespace CryptoExchange.Net
}
/// <summary>
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
/// Update the subscription when the connection is restored after disconnecting. Can be used to update an authentication token for example.
/// </summary>
/// <param name="request">The original request</param>
/// <param name="subscription">The subscription</param>
/// <returns></returns>
protected internal virtual Task<CallResult<object>> RevitalizeRequestAsync(object request)
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
{
return Task.FromResult(new CallResult<object>(request));
return Task.FromResult(new CallResult(null));
}
/// <summary>
@ -589,11 +444,11 @@ namespace CryptoExchange.Net
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& (s.Value.ApiClient.GetType() == GetType())
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault();
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault();
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
if (result != null)
{
if (result.SubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.SubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
{
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
return new CallResult<SocketConnection>(result);
@ -614,11 +469,12 @@ namespace CryptoExchange.Net
var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
foreach (var kvp in genericHandlers)
{
var handler = SocketSubscription.CreateForIdentifier(ExchangeHelpers.NextId(), kvp.Key, false, false, kvp.Value);
socketConnection.AddSubscription(handler);
}
foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
foreach (var systemSubscription in systemSubscriptions)
socketConnection.AddSubscription(systemSubscription);
return new CallResult<SocketConnection>(socketConnection);
}
@ -626,8 +482,8 @@ namespace CryptoExchange.Net
/// <summary>
/// Process an unhandled message
/// </summary>
/// <param name="token">The token that wasn't processed</param>
protected virtual void HandleUnhandledMessage(JToken token)
/// <param name="message">The message that wasn't processed</param>
protected virtual void HandleUnhandledMessage(IMessageAccessor message)
{
}
@ -656,8 +512,6 @@ namespace CryptoExchange.Net
protected virtual WebSocketParameters GetWebSocketParameters(string address)
=> new(new Uri(address), ClientOptions.AutoReconnect)
{
DataInterpreterBytes = dataInterpreterBytes,
DataInterpreterString = dataInterpreterString,
KeepAliveInterval = KeepAliveInterval,
ReconnectInterval = ClientOptions.ReconnectInterval,
RateLimiters = RateLimiters,
@ -673,57 +527,10 @@ namespace CryptoExchange.Net
protected virtual IWebsocket CreateSocket(string address)
{
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
_logger.Log(LogLevel.Debug, $"Socket {socket.Id} new socket created for " + address);
_logger.Log(LogLevel.Debug, $"[Sckt {socket.Id}] created for " + address);
return socket;
}
/// <summary>
/// Periodically sends data over a socket connection
/// </summary>
/// <param name="identifier">Identifier for the periodic send</param>
/// <param name="interval">How often</param>
/// <param name="objGetter">Method returning the object to send</param>
protected virtual void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter)
{
if (objGetter == null)
throw new ArgumentNullException(nameof(objGetter));
periodicEvent = new AsyncResetEvent();
periodicTask = Task.Run(async () =>
{
while (!_disposing)
{
await periodicEvent.WaitAsync(interval).ConfigureAwait(false);
if (_disposing)
break;
foreach (var socketConnection in socketConnections.Values)
{
if (_disposing)
break;
if (!socketConnection.Connected)
continue;
var obj = objGetter(socketConnection);
if (obj == null)
continue;
_logger.Log(LogLevel.Trace, $"Socket {socketConnection.SocketId} sending periodic {identifier}");
try
{
socketConnection.Send(ExchangeHelpers.NextId(), obj, 1);
}
catch (Exception ex)
{
_logger.Log(LogLevel.Warning, $"Socket {socketConnection.SocketId} Periodic send {identifier} failed: " + ex.ToLogString());
}
}
}
});
}
/// <summary>
/// Unsubscribe an update subscription
/// </summary>
@ -731,7 +538,7 @@ namespace CryptoExchange.Net
/// <returns></returns>
public virtual async Task<bool> UnsubscribeAsync(int subscriptionId)
{
SocketSubscription? subscription = null;
Subscription? subscription = null;
SocketConnection? connection = null;
foreach (var socket in socketConnections.Values.ToList())
{
@ -746,7 +553,7 @@ namespace CryptoExchange.Net
if (subscription == null || connection == null)
return false;
_logger.Log(LogLevel.Information, $"Socket {connection.SocketId} Unsubscribing subscription " + subscriptionId);
_logger.Log(LogLevel.Information, $"[Sckt {connection.SocketId}] unsubscribing subscription " + subscriptionId);
await connection.CloseAsync(subscription).ConfigureAwait(false);
return true;
}
@ -761,7 +568,7 @@ namespace CryptoExchange.Net
if (subscription == null)
throw new ArgumentNullException(nameof(subscription));
_logger.Log(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
_logger.Log(LogLevel.Information, $"[Sckt {subscription.SocketId}] Unsubscribing subscription " + subscription.Id);
await subscription.CloseAsync().ConfigureAwait(false);
}
@ -771,11 +578,11 @@ namespace CryptoExchange.Net
/// <returns></returns>
public virtual async Task UnsubscribeAllAsync()
{
var sum = socketConnections.Sum(s => s.Value.SubscriptionCount);
var sum = socketConnections.Sum(s => s.Value.UserSubscriptionCount);
if (sum == 0)
return;
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.SubscriptionCount)} subscriptions");
_logger.Log(LogLevel.Information, $"Unsubscribing all {socketConnections.Sum(s => s.Value.UserSubscriptionCount)} subscriptions");
var tasks = new List<Task>();
{
var socketList = socketConnections.Values;
@ -809,12 +616,26 @@ namespace CryptoExchange.Net
public string GetSubscriptionsState()
{
var sb = new StringBuilder();
sb.AppendLine($"{socketConnections.Count} connections, {CurrentSubscriptions} subscriptions, kbps: {IncomingKbps}");
sb.AppendLine($"{GetType().Name}");
sb.AppendLine($" Connections: {socketConnections.Count}");
sb.AppendLine($" Subscriptions: {CurrentSubscriptions}");
sb.AppendLine($" Download speed: {IncomingKbps} kbps");
foreach (var connection in socketConnections)
{
sb.AppendLine($" Connection {connection.Key}: {connection.Value.SubscriptionCount} subscriptions, status: {connection.Value.Status}, authenticated: {connection.Value.Authenticated}, kbps: {connection.Value.IncomingKbps}");
sb.AppendLine($" Id: {connection.Key}");
sb.AppendLine($" Address: {connection.Value.ConnectionUri}");
sb.AppendLine($" Subscriptions: {connection.Value.UserSubscriptionCount}");
sb.AppendLine($" Status: {connection.Value.Status}");
sb.AppendLine($" Authenticated: {connection.Value.Authenticated}");
sb.AppendLine($" Download speed: {connection.Value.IncomingKbps} kbps");
sb.AppendLine($" Subscriptions:");
foreach (var subscription in connection.Value.Subscriptions)
sb.AppendLine($" Subscription {subscription.Id}, authenticated: {subscription.Authenticated}, confirmed: {subscription.Confirmed}");
{
sb.AppendLine($" Id: {subscription.Id}");
sb.AppendLine($" Confirmed: {subscription.Confirmed}");
sb.AppendLine($" Invocations: {subscription.TotalInvocations}");
sb.AppendLine($" Identifiers: [{string.Join(", ", subscription.ListenerIdentifiers)}]");
}
}
return sb.ToString();
}
@ -825,9 +646,7 @@ namespace CryptoExchange.Net
public override void Dispose()
{
_disposing = true;
periodicEvent?.Set();
periodicEvent?.Dispose();
if (socketConnections.Sum(s => s.Value.SubscriptionCount) > 0)
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
{
_logger.Log(LogLevel.Debug, "Disposing socket client, closing all subscriptions");
_ = UnsubscribeAllAsync();
@ -835,5 +654,20 @@ namespace CryptoExchange.Net
semaphoreSlim?.Dispose();
base.Dispose();
}
/// <summary>
/// Get the listener identifier for the message
/// </summary>
/// <param name="messageAccessor"></param>
/// <returns></returns>
public abstract string? GetListenerIdentifier(IMessageAccessor messageAccessor);
/// <summary>
/// Preprocess a stream message
/// </summary>
/// <param name="type"></param>
/// <param name="stream"></param>
/// <returns></returns>
public virtual Stream PreprocessStreamMessage(WebSocketMessageType type, Stream stream) => stream;
}
}

View File

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.CommonObjects
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Order type

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.CommonObjects
{

View File

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.CommonObjects
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Id of an order

View File

@ -1,6 +1,4 @@
using System;
namespace CryptoExchange.Net.CommonObjects
namespace CryptoExchange.Net.CommonObjects
{
/// <summary>
/// Ticker data

View File

@ -47,7 +47,7 @@ namespace CryptoExchange.Net.Converters
mapping = AddMapping(enumType);
var stringValue = reader.Value?.ToString();
if (stringValue == null)
if (stringValue == null || stringValue == "")
{
// Received null value
var emptyResult = GetDefaultValue(objectType, enumType);

View File

@ -6,20 +6,26 @@
<PackageId>CryptoExchange.Net</PackageId>
<Authors>JKorf</Authors>
<Description>A base package for implementing cryptocurrency API's</Description>
<PackageVersion>6.2.5</PackageVersion>
<AssemblyVersion>6.2.5</AssemblyVersion>
<FileVersion>6.2.5</FileVersion>
<PackageVersion>7.0.0-beta2</PackageVersion>
<AssemblyVersion>7.0.0</AssemblyVersion>
<FileVersion>7.0.0</FileVersion>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
<NeutralLanguage>en</NeutralLanguage>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>icon.png</PackageIcon>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageReleaseNotes>6.2.5 - Added support for deserializing null and empty string values to BoolConverter</PackageReleaseNotes>
<PackageReleaseNotes>7.0.0-beta2 - Updated RevitalizeRequestAsync signature, Removed duplicate logging</PackageReleaseNotes>
<Nullable>enable</Nullable>
<LangVersion>10.0</LangVersion>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
<None Include="..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<IncludeSymbols>true</IncludeSymbols>
@ -41,7 +47,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="8.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@ -144,6 +144,16 @@ namespace CryptoExchange.Net
}
}
/// <summary>
/// Return the last unique id that was generated
/// </summary>
/// <returns></returns>
public static int LastId()
{
lock (_idLock)
return _lastId;
}
/// <summary>
/// Generate a random string of specified length
/// </summary>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -2,7 +2,6 @@
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.CommonObjects;
using CryptoExchange.Net.Interfaces.CommonClients;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.Interfaces.CommonClients

View File

@ -1,5 +1,4 @@
using CryptoExchange.Net.CommonObjects;
using CryptoExchange.Net.Interfaces.CommonClients;
using CryptoExchange.Net.Objects;
using System.Threading;
using System.Threading.Tasks;

View File

@ -0,0 +1,34 @@
using CryptoExchange.Net.Interfaces.CommonClients;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Client for accessing REST API's for different exchanges
/// </summary>
public interface ICryptoRestClient
{
/// <summary>
/// Get a list of all registered common ISpotClient types
/// </summary>
/// <returns></returns>
IEnumerable<ISpotClient> GetSpotClients();
/// <summary>
/// Get an ISpotClient implementation by exchange name
/// </summary>
/// <param name="exchangeName"></param>
/// <returns></returns>
ISpotClient? SpotClient(string exchangeName);
/// <summary>
/// Try get
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
T TryGet<T>(Func<T> createFunc);
}
}

View File

@ -0,0 +1,21 @@
using CryptoExchange.Net.Interfaces.CommonClients;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Client for accessing Websocket API's for different exchanges
/// </summary>
public interface ICryptoSocketClient
{
/// <summary>
/// Try get a client by type for the service collection
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
T TryGet<T>(Func<T> createFunc);
}
}

View File

@ -0,0 +1,45 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Message processor
/// </summary>
public interface IMessageProcessor
{
/// <summary>
/// Id of the processor
/// </summary>
public int Id { get; }
/// <summary>
/// The identifiers for this processor
/// </summary>
public HashSet<string> ListenerIdentifiers { get; }
/// <summary>
/// Handle a message
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message);
/// <summary>
/// Get the type the message should be deserialized to
/// </summary>
/// <param name="messageAccessor"></param>
/// <returns></returns>
Type? GetMessageType(IMessageAccessor messageAccessor);
/// <summary>
/// Deserialize a message int oobject of type
/// </summary>
/// <param name="accessor"></param>
/// <param name="type"></param>
/// <returns></returns>
object Deserialize(IMessageAccessor accessor, Type type);
}
}

View File

@ -1,5 +1,4 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using System;
using System.Net.Http;

View File

@ -1,7 +1,4 @@
using CryptoExchange.Net.Objects;
using System;
namespace CryptoExchange.Net.Interfaces
namespace CryptoExchange.Net.Interfaces
{
/// <summary>
/// Base rest API client

View File

@ -1,6 +1,4 @@
using System;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Interfaces

View File

@ -1,7 +1,5 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Sockets;
using System;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
@ -28,6 +26,14 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
IWebsocketFactory SocketFactory { get; set; }
/// <summary>
/// Current client options
/// </summary>
SocketExchangeOptions ClientOptions { get; }
/// <summary>
/// Current API options
/// </summary>
SocketApiOptions ApiOptions { get; }
/// <summary>
/// Log the current state of connections and subscriptions
/// </summary>
string GetSubscriptionsState();

View File

@ -1,9 +1,7 @@
using System;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Objects.Sockets;
namespace CryptoExchange.Net.Interfaces
{

View File

@ -1,8 +1,6 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using System;
using System.Security.Authentication;
using System.Text;
using System;
using System.IO;
using System.Net.WebSockets;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Interfaces
@ -15,31 +13,31 @@ namespace CryptoExchange.Net.Interfaces
/// <summary>
/// Websocket closed event
/// </summary>
event Action OnClose;
event Func<Task> OnClose;
/// <summary>
/// Websocket message received event
/// </summary>
event Action<string> OnMessage;
event Func<WebSocketMessageType, Stream, Task> OnStreamMessage;
/// <summary>
/// Websocket sent event, RequestId as parameter
/// </summary>
event Action<int> OnRequestSent;
event Func<int, Task> OnRequestSent;
/// <summary>
/// Websocket error event
/// </summary>
event Action<Exception> OnError;
event Func<Exception, Task> OnError;
/// <summary>
/// Websocket opened event
/// </summary>
event Action OnOpen;
event Func<Task> OnOpen;
/// <summary>
/// Websocket has lost connection to the server and is attempting to reconnect
/// </summary>
event Action OnReconnecting;
event Func<Task> OnReconnecting;
/// <summary>
/// Websocket has reconnected to the server
/// </summary>
event Action OnReconnected;
event Func<Task> OnReconnected;
/// <summary>
/// Get reconntion url
/// </summary>

View File

@ -1,4 +1,4 @@
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Interfaces

View File

@ -70,7 +70,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="originalData"></param>
/// <param name="error"></param>
#pragma warning disable 8618
protected CallResult([AllowNull]T data, string? originalData, Error? error): base(error)
public CallResult([AllowNull]T data, string? originalData, Error? error): base(error)
#pragma warning restore 8618
{
OriginalData = originalData;
@ -91,6 +91,13 @@ namespace CryptoExchange.Net.Objects
/// <param name="error">The erro rto return</param>
public CallResult(Error error) : this(default, null, error) { }
/// <summary>
/// Create a new error result
/// </summary>
/// <param name="error">The error to return</param>
/// <param name="originalData">The original response data</param>
public CallResult(Error error, string? originalData) : this(default, originalData, error) { }
/// <summary>
/// Overwrite bool check so we can use if(callResult) instead of if(callResult.Success)
/// </summary>

View File

@ -278,7 +278,7 @@ namespace CryptoExchange.Net.Objects
/// <param name="code"></param>
/// <param name="message"></param>
/// <param name="data"></param>
protected CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
public CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
}
/// <summary>

View File

@ -1,7 +1,6 @@
using CryptoExchange.Net.Objects;
using System;
using System;
namespace CryptoExchange.Net.Sockets
namespace CryptoExchange.Net.Objects.Sockets
{
/// <summary>
/// An update received from a socket update subscription

View File

@ -1,8 +1,8 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets;
using System;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
namespace CryptoExchange.Net.Objects.Sockets
{
/// <summary>
/// Subscription to a data stream
@ -10,7 +10,7 @@ namespace CryptoExchange.Net.Sockets
public class UpdateSubscription
{
private readonly SocketConnection _connection;
private readonly SocketSubscription _subscription;
private readonly Subscription _listener;
/// <summary>
/// Event when the connection is lost. The socket will automatically reconnect when possible.
@ -64,8 +64,8 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public event Action<Exception> Exception
{
add => _subscription.Exception += value;
remove => _subscription.Exception -= value;
add => _listener.Exception += value;
remove => _listener.Exception -= value;
}
/// <summary>
@ -76,26 +76,26 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// The id of the subscription
/// </summary>
public int Id => _subscription.Id;
public int Id => _listener.Id;
/// <summary>
/// ctor
/// </summary>
/// <param name="connection">The socket connection the subscription is on</param>
/// <param name="subscription">The subscription</param>
public UpdateSubscription(SocketConnection connection, SocketSubscription subscription)
public UpdateSubscription(SocketConnection connection, Subscription subscription)
{
this._connection = connection;
this._subscription = subscription;
_connection = connection;
_listener = subscription;
}
/// <summary>
/// Close the subscription
/// </summary>
/// <returns></returns>
public Task CloseAsync()
{
return _connection.CloseAsync(_subscription);
return _connection.CloseAsync(_listener);
}
/// <summary>
@ -113,16 +113,16 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
internal async Task UnsubscribeAsync()
{
await _connection.UnsubscribeAsync(_subscription).ConfigureAwait(false);
await _connection.UnsubscribeAsync(_listener).ConfigureAwait(false);
}
/// <summary>
/// Resubscribe this subscription
/// </summary>
/// <returns></returns>
internal async Task<CallResult<bool>> ResubscribeAsync()
internal async Task<CallResult> ResubscribeAsync()
{
return await _connection.ResubscribeAsync(_subscription).ConfigureAwait(false);
return await _connection.ResubscribeAsync(_listener).ConfigureAwait(false);
}
}
}

View File

@ -1,11 +1,9 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
namespace CryptoExchange.Net.Objects.Sockets
{
/// <summary>
/// Parameters for a websocket
@ -57,21 +55,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public IEnumerable<IRateLimiter>? RateLimiters { get; set; }
/// <summary>
/// Origin header value to send in the connection handshake
/// </summary>
public string? Origin { get; set; }
/// <summary>
/// Delegate used for processing byte data received from socket connections before it is processed by handlers
/// </summary>
public Func<byte[], string>? DataInterpreterBytes { get; set; }
/// <summary>
/// Delegate used for processing string data received from socket connections before it is processed by handlers
/// </summary>
public Func<string, string>? DataInterpreterString { get; set; }
/// <summary>
/// Encoding for sending/receiving data
/// </summary>

View File

@ -1,6 +1,4 @@
using System.Collections.Generic;
namespace CryptoExchange.Net.Objects
namespace CryptoExchange.Net.Objects
{
/// <summary>
/// Trade environment names

View File

@ -9,7 +9,7 @@ using System.Threading.Tasks;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.OrderBook

View File

@ -1,10 +1,8 @@
using System;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.Requests
{

View File

@ -1,5 +1,6 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
@ -44,7 +45,6 @@ namespace CryptoExchange.Net.Sockets
private ProcessState _processState;
private DateTime _lastReconnectTime;
/// <summary>
/// Received messages, the size and the timstamp
/// </summary>
@ -75,10 +75,10 @@ namespace CryptoExchange.Net.Sockets
public Uri Uri => Parameters.Uri;
/// <inheritdoc />
public bool IsClosed => _socket.State == WebSocketState.Closed;
public virtual bool IsClosed => _socket.State == WebSocketState.Closed;
/// <inheritdoc />
public bool IsOpen => _socket.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested;
public virtual bool IsOpen => _socket.State == WebSocketState.Open && !_ctsSource.IsCancellationRequested;
/// <inheritdoc />
public double IncomingKbps
@ -98,25 +98,25 @@ namespace CryptoExchange.Net.Sockets
}
/// <inheritdoc />
public event Action? OnClose;
public event Func<Task>? OnClose;
/// <inheritdoc />
public event Action<string>? OnMessage;
public event Func<WebSocketMessageType, Stream, Task>? OnStreamMessage;
/// <inheritdoc />
public event Action<int>? OnRequestSent;
public event Func<int, Task>? OnRequestSent;
/// <inheritdoc />
public event Action<Exception>? OnError;
public event Func<Exception, Task>? OnError;
/// <inheritdoc />
public event Action? OnOpen;
public event Func<Task>? OnOpen;
/// <inheritdoc />
public event Action? OnReconnecting;
public event Func<Task>? OnReconnecting;
/// <inheritdoc />
public event Action? OnReconnected;
public event Func<Task>? OnReconnected;
/// <inheritdoc />
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
@ -147,7 +147,7 @@ namespace CryptoExchange.Net.Sockets
if (!await ConnectInternalAsync().ConfigureAwait(false))
return false;
OnOpen?.Invoke();
await (OnOpen?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
_processTask = ProcessAsync();
return true;
}
@ -183,7 +183,7 @@ namespace CryptoExchange.Net.Sockets
private async Task<bool> ConnectInternalAsync()
{
_logger.Log(LogLevel.Debug, $"Socket {Id} connecting");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connecting");
try
{
using CancellationTokenSource tcs = new(TimeSpan.FromSeconds(10));
@ -191,11 +191,11 @@ namespace CryptoExchange.Net.Sockets
}
catch (Exception e)
{
_logger.Log(LogLevel.Debug, $"Socket {Id} connection failed: " + e.ToLogString());
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connection failed: " + e.ToLogString());
return false;
}
_logger.Log(LogLevel.Debug, $"Socket {Id} connected to {Uri}");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] connected to {Uri}");
return true;
}
@ -204,13 +204,13 @@ namespace CryptoExchange.Net.Sockets
{
while (!_stopRequested)
{
_logger.Log(LogLevel.Debug, $"Socket {Id} starting processing tasks");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] starting processing tasks");
_processState = ProcessState.Processing;
var sendTask = SendLoopAsync();
var receiveTask = ReceiveLoopAsync();
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
_logger.Log(LogLevel.Debug, $"Socket {Id} processing tasks finished");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] processing tasks finished");
_processState = ProcessState.WaitingForClose;
while (_closeTask == null)
@ -222,14 +222,14 @@ namespace CryptoExchange.Net.Sockets
if (!Parameters.AutoReconnect)
{
_processState = ProcessState.Idle;
OnClose?.Invoke();
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
return;
}
if (!_stopRequested)
{
_processState = ProcessState.Reconnecting;
OnReconnecting?.Invoke();
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
}
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
@ -238,14 +238,14 @@ namespace CryptoExchange.Net.Sockets
while (!_stopRequested)
{
_logger.Log(LogLevel.Debug, $"Socket {Id} attempting to reconnect");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] attempting to reconnect");
var task = GetReconnectionUrl?.Invoke();
if (task != null)
{
var reconnectUri = await task.ConfigureAwait(false);
if (reconnectUri != null && Parameters.Uri != reconnectUri)
{
_logger.Log(LogLevel.Debug, $"Socket {Id} reconnect URI set to {reconnectUri}");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] reconnect URI set to {reconnectUri}");
Parameters.Uri = reconnectUri;
}
}
@ -263,7 +263,7 @@ namespace CryptoExchange.Net.Sockets
}
_lastReconnectTime = DateTime.UtcNow;
OnReconnected?.Invoke();
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
break;
}
}
@ -278,7 +278,7 @@ namespace CryptoExchange.Net.Sockets
return;
var bytes = Parameters.Encoding.GetBytes(data);
_logger.Log(LogLevel.Trace, $"Socket {Id} - msg {id} - Adding {bytes.Length} bytes to send buffer");
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] msg {id} - Adding {bytes.Length} bytes to send buffer");
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
_sendEvent.Set();
}
@ -289,7 +289,7 @@ namespace CryptoExchange.Net.Sockets
if (_processState != ProcessState.Processing && IsOpen)
return;
_logger.Log(LogLevel.Debug, $"Socket {Id} reconnect requested");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] reconnect requested");
_closeTask = CloseInternalAsync();
await _closeTask.ConfigureAwait(false);
}
@ -304,18 +304,18 @@ namespace CryptoExchange.Net.Sockets
{
if (_closeTask?.IsCompleted == false)
{
_logger.Log(LogLevel.Debug, $"Socket {Id} CloseAsync() waiting for existing close task");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] CloseAsync() waiting for existing close task");
await _closeTask.ConfigureAwait(false);
return;
}
if (!IsOpen)
{
_logger.Log(LogLevel.Debug, $"Socket {Id} CloseAsync() socket not open");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] CloseAsync() socket not open");
return;
}
_logger.Log(LogLevel.Debug, $"Socket {Id} closing");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] closing");
_closeTask = CloseInternalAsync();
}
finally
@ -326,8 +326,8 @@ namespace CryptoExchange.Net.Sockets
await _closeTask.ConfigureAwait(false);
if(_processTask != null)
await _processTask.ConfigureAwait(false);
OnClose?.Invoke();
_logger.Log(LogLevel.Debug, $"Socket {Id} closed");
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] closed");
}
/// <summary>
@ -379,11 +379,11 @@ namespace CryptoExchange.Net.Sockets
if (_disposed)
return;
_logger.Log(LogLevel.Debug, $"Socket {Id} disposing");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] disposing");
_disposed = true;
_socket.Dispose();
_ctsSource.Dispose();
_logger.Log(LogLevel.Trace, $"Socket {Id} disposed");
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] disposed");
}
/// <summary>
@ -415,7 +415,7 @@ namespace CryptoExchange.Net.Sockets
if (limitResult.Success)
{
if (limitResult.Data > 0)
_logger.Log(LogLevel.Debug, $"Socket {Id} - msg {data.Id} - send delayed {limitResult.Data}ms because of rate limit");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] msg {data.Id} - send delayed {limitResult.Data}ms because of rate limit");
}
}
}
@ -423,8 +423,8 @@ namespace CryptoExchange.Net.Sockets
try
{
await _socket.SendAsync(new ArraySegment<byte>(data.Bytes, 0, data.Bytes.Length), WebSocketMessageType.Text, true, _ctsSource.Token).ConfigureAwait(false);
OnRequestSent?.Invoke(data.Id);
_logger.Log(LogLevel.Trace, $"Socket {Id} - msg {data.Id} - sent {data.Bytes.Length} bytes");
await (OnRequestSent?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] msg {data.Id} - sent {data.Bytes.Length} bytes");
}
catch (OperationCanceledException)
{
@ -434,7 +434,7 @@ namespace CryptoExchange.Net.Sockets
catch (Exception ioe)
{
// Connection closed unexpectedly, .NET framework
OnError?.Invoke(ioe);
await (OnError?.Invoke(ioe) ?? Task.CompletedTask).ConfigureAwait(false);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
break;
@ -447,13 +447,13 @@ namespace CryptoExchange.Net.Sockets
// Because this is running in a separate task and not awaited until the socket gets closed
// any exception here will crash the send processing, but do so silently unless the socket get's stopped.
// Make sure we at least let the owner know there was an error
_logger.Log(LogLevel.Warning, $"Socket {Id} Send loop stopped with exception");
OnError?.Invoke(e);
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] send loop stopped with exception");
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
throw;
}
finally
{
_logger.Log(LogLevel.Debug, $"Socket {Id} Send loop finished");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] send loop finished");
}
}
@ -492,7 +492,7 @@ namespace CryptoExchange.Net.Sockets
catch (Exception wse)
{
// Connection closed unexpectedly
OnError?.Invoke(wse);
await (OnError?.Invoke(wse) ?? Task.CompletedTask).ConfigureAwait(false);
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
break;
@ -501,7 +501,7 @@ namespace CryptoExchange.Net.Sockets
if (receiveResult.MessageType == WebSocketMessageType.Close)
{
// Connection closed unexpectedly
_logger.Log(LogLevel.Debug, $"Socket {Id} received `Close` message");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] received `Close` message");
if (_closeTask?.IsCompleted != false)
_closeTask = CloseInternalAsync();
break;
@ -512,7 +512,7 @@ namespace CryptoExchange.Net.Sockets
// We received data, but it is not complete, write it to a memory stream for reassembling
multiPartMessage = true;
memoryStream ??= new MemoryStream();
_logger.Log(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in partial message");
await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
}
else
@ -520,13 +520,13 @@ namespace CryptoExchange.Net.Sockets
if (!multiPartMessage)
{
// Received a complete message and it's not multi part
_logger.Log(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in single message");
HandleMessage(buffer.Array!, buffer.Offset, receiveResult.Count, receiveResult.MessageType);
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in single message");
await ProcessData(receiveResult.MessageType, new MemoryStream(buffer.Array, buffer.Offset, receiveResult.Count)).ConfigureAwait(false);
}
else
{
// Received the end of a multipart message, write to memory stream for reassembling
_logger.Log(LogLevel.Trace, $"Socket {Id} received {receiveResult.Count} bytes in partial message");
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in partial message");
await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false);
}
break;
@ -554,12 +554,14 @@ namespace CryptoExchange.Net.Sockets
if (receiveResult?.EndOfMessage == true)
{
// Reassemble complete message from memory stream
_logger.Log(LogLevel.Trace, $"Socket {Id} reassembled message of {memoryStream!.Length} bytes");
HandleMessage(memoryStream!.ToArray(), 0, (int)memoryStream.Length, receiveResult.MessageType);
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] reassembled message of {memoryStream!.Length} bytes");
await ProcessData(receiveResult.MessageType, memoryStream).ConfigureAwait(false);
memoryStream.Dispose();
}
else
_logger.Log(LogLevel.Trace, $"Socket {Id} discarding incomplete message of {memoryStream!.Length} bytes");
{
_logger.Log(LogLevel.Trace, $"[Sckt {Id}] discarding incomplete message of {memoryStream!.Length} bytes");
}
}
}
}
@ -568,68 +570,29 @@ namespace CryptoExchange.Net.Sockets
// Because this is running in a separate task and not awaited until the socket gets closed
// any exception here will crash the receive processing, but do so silently unless the socket gets stopped.
// Make sure we at least let the owner know there was an error
_logger.Log(LogLevel.Warning, $"Socket {Id} Receive loop stopped with exception");
OnError?.Invoke(e);
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] receive loop stopped with exception");
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
throw;
}
finally
{
_logger.Log(LogLevel.Debug, $"Socket {Id} Receive loop finished");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] receive loop finished");
}
}
/// <summary>
/// Handles the message
/// Proccess a stream message
/// </summary>
/// <param name="data"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <param name="messageType"></param>
private void HandleMessage(byte[] data, int offset, int count, WebSocketMessageType messageType)
/// <param name="type"></param>
/// <param name="stream"></param>
/// <returns></returns>
protected async Task ProcessData(WebSocketMessageType type, Stream stream)
{
string strData;
if (messageType == WebSocketMessageType.Binary)
{
if (Parameters.DataInterpreterBytes == null)
throw new Exception("Byte interpreter not set while receiving byte data");
LastActionTime = DateTime.UtcNow;
stream.Position = 0;
try
{
var relevantData = new byte[count];
Array.Copy(data, offset, relevantData, 0, count);
strData = Parameters.DataInterpreterBytes(relevantData);
}
catch(Exception e)
{
_logger.Log(LogLevel.Error, $"Socket {Id} unhandled exception during byte data interpretation: " + e.ToLogString());
return;
}
}
else
strData = Parameters.Encoding.GetString(data, offset, count);
if (Parameters.DataInterpreterString != null)
{
try
{
strData = Parameters.DataInterpreterString(strData);
}
catch(Exception e)
{
_logger.Log(LogLevel.Error, $"Socket {Id} unhandled exception during string data interpretation: " + e.ToLogString());
return;
}
}
try
{
LastActionTime = DateTime.UtcNow;
OnMessage?.Invoke(strData);
}
catch(Exception e)
{
_logger.Log(LogLevel.Error, $"Socket {Id} unhandled exception during message processing: " + e.ToLogString());
}
if (OnStreamMessage != null)
await OnStreamMessage.Invoke(type, stream).ConfigureAwait(false);
}
/// <summary>
@ -638,7 +601,7 @@ namespace CryptoExchange.Net.Sockets
/// <returns></returns>
protected async Task CheckTimeoutAsync()
{
_logger.Log(LogLevel.Debug, $"Socket {Id} Starting task checking for no data received for {Parameters.Timeout}");
_logger.Log(LogLevel.Debug, $"[Sckt {Id}] starting task checking for no data received for {Parameters.Timeout}");
LastActionTime = DateTime.UtcNow;
try
{
@ -649,7 +612,7 @@ namespace CryptoExchange.Net.Sockets
if (DateTime.UtcNow - LastActionTime > Parameters.Timeout)
{
_logger.Log(LogLevel.Warning, $"Socket {Id} No data received for {Parameters.Timeout}, reconnecting socket");
_logger.Log(LogLevel.Warning, $"[Sckt {Id}] no data received for {Parameters.Timeout}, reconnecting socket");
_ = ReconnectAsync().ConfigureAwait(false);
return;
}
@ -669,7 +632,7 @@ namespace CryptoExchange.Net.Sockets
// Because this is running in a separate task and not awaited until the socket gets closed
// any exception here will stop the timeout checking, but do so silently unless the socket get's stopped.
// Make sure we at least let the owner know there was an error
OnError?.Invoke(e);
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
throw;
}
}

View File

@ -1,46 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Message received event
/// </summary>
public class MessageEvent
{
/// <summary>
/// The connection the message was received on
/// </summary>
public SocketConnection Connection { get; set; }
/// <summary>
/// The json object of the data
/// </summary>
public JToken JsonData { get; set; }
/// <summary>
/// The originally received string data
/// </summary>
public string? OriginalData { get; set; }
/// <summary>
/// The timestamp of when the data was received
/// </summary>
public DateTime ReceivedTimestamp { get; set; }
/// <summary>
/// ctor
/// </summary>
/// <param name="connection"></param>
/// <param name="jsonData"></param>
/// <param name="originalData"></param>
/// <param name="timestamp"></param>
public MessageEvent(SocketConnection connection, JToken jsonData, string? originalData, DateTime timestamp)
{
Connection = connection;
JsonData = jsonData;
OriginalData = originalData;
ReceivedTimestamp = timestamp;
}
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
{
/// <summary>
/// Message accessor
/// </summary>
public interface IMessageAccessor
{
/// <summary>
/// Is this a json message
/// </summary>
bool IsJson { get; }
/// <summary>
/// The underlying data object
/// </summary>
object? Underlying { get; }
/// <summary>
/// Load a stream message
/// </summary>
/// <param name="stream"></param>
void Load(Stream stream);
/// <summary>
/// Get the type of node
/// </summary>
/// <returns></returns>
NodeType? GetNodeType();
/// <summary>
/// Get the type of node
/// </summary>
/// <param name="path">Access path</param>
/// <returns></returns>
NodeType? GetNodeType(MessagePath path);
/// <summary>
/// Get the value of a path
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
T? GetValue<T>(MessagePath path);
/// <summary>
/// Get the values of an array
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="path"></param>
/// <returns></returns>
List<T?>? GetValues<T>(MessagePath path);
/// <summary>
/// Deserialize the message into this type
/// </summary>
/// <param name="type"></param>
/// <param name="path"></param>
/// <returns></returns>
object Deserialize(Type type, MessagePath? path = null);
}
}

View File

@ -0,0 +1,15 @@
namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces
{
/// <summary>
/// Serializer interface
/// </summary>
public interface IMessageSerializer
{
/// <summary>
/// Serialize an object to a string
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
string Serialize(object message);
}
}

View File

@ -0,0 +1,158 @@
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Json.Net message accessor
/// </summary>
public class JsonNetMessageAccessor : IMessageAccessor
{
private JToken? _token;
private Stream? _stream;
private static JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters);
/// <inheritdoc />
public bool IsJson { get; private set; }
/// <inheritdoc />
public object? Underlying => _token;
/// <inheritdoc />
public void Load(Stream stream)
{
_stream = stream;
using var reader = new StreamReader(stream, Encoding.UTF8, false, (int)stream.Length, true);
using var jsonTextReader = new JsonTextReader(reader);
try
{
_token = JToken.Load(jsonTextReader);
IsJson = true;
}
catch (Exception)
{
// Not a json message
IsJson = false;
}
}
/// <inheritdoc />
public object Deserialize(Type type, MessagePath? path = null)
{
if (!IsJson)
{
var sr = new StreamReader(_stream);
return sr.ReadToEnd();
}
var source = _token;
if (path != null)
source = GetPathNode(path.Value);
return source!.ToObject(type, _serializer)!;
}
/// <inheritdoc />
public NodeType? GetNodeType()
{
if (_token == null)
return null;
if (_token.Type == JTokenType.Object)
return NodeType.Object;
if (_token.Type == JTokenType.Array)
return NodeType.Array;
return NodeType.Value;
}
/// <inheritdoc />
public NodeType? GetNodeType(MessagePath path)
{
var node = GetPathNode(path);
if (node == null)
return null;
if (node.Type == JTokenType.Object)
return NodeType.Object;
if (node.Type == JTokenType.Array)
return NodeType.Array;
return NodeType.Value;
}
/// <inheritdoc />
public T? GetValue<T>(MessagePath path)
{
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Type == JTokenType.Object || value.Type == JTokenType.Array)
return default;
return value!.Value<T>();
}
/// <inheritdoc />
public List<T?>? GetValues<T>(MessagePath path)
{
var value = GetPathNode(path);
if (value == null)
return default;
if (value.Type == JTokenType.Object)
return default;
return value!.Values<T>().ToList();
}
private JToken? GetPathNode(MessagePath path)
{
var currentToken = _token;
foreach (var node in path)
{
if (node.Type == 0)
{
// Int value
var val = (int)node.Value!;
if (currentToken!.Type != JTokenType.Array || ((JArray)currentToken).Count <= val)
return null;
currentToken = currentToken[val];
}
else if (node.Type == 1)
{
// String value
if (currentToken!.Type != JTokenType.Object)
return null;
currentToken = currentToken[(string)node.Value!];
}
else
{
// Property name
if (currentToken!.Type != JTokenType.Object)
return null;
currentToken = (currentToken.First as JProperty)?.Name;
}
if (currentToken == null)
return null;
}
return currentToken;
}
}
}

View File

@ -0,0 +1,12 @@
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Newtonsoft.Json;
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <inheritdoc />
public class JsonNetSerializer : IMessageSerializer
{
/// <inheritdoc />
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None);
}
}

View File

@ -0,0 +1,44 @@
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Node accessor
/// </summary>
public struct NodeAccessor
{
/// <summary>
/// Value
/// </summary>
public object? Value { get; }
/// <summary>
/// Type (0 = int, 1 = string, 2 = prop name)
/// </summary>
public int Type { get; }
private NodeAccessor(object? value, int type)
{
Value = value;
Type = type;
}
/// <summary>
/// Create an int node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor Int(int value) { return new NodeAccessor(value, 0); }
/// <summary>
/// Create a string node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor String(string value) { return new NodeAccessor(value, 1); }
/// <summary>
/// Create a property name node accessor
/// </summary>
/// <returns></returns>
public static NodeAccessor PropertyName() { return new NodeAccessor(null, 2); }
}
}

View File

@ -0,0 +1,50 @@
using System.Collections;
using System.Collections.Generic;
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Message access definition
/// </summary>
public struct MessagePath : IEnumerable<NodeAccessor>
{
private List<NodeAccessor> _path;
internal void Add(NodeAccessor node)
{
_path.Add(node);
}
/// <summary>
/// ctor
/// </summary>
public MessagePath()
{
_path = new List<NodeAccessor>();
}
/// <summary>
/// Create a new message path
/// </summary>
/// <returns></returns>
public static MessagePath Get()
{
return new MessagePath();
}
/// <summary>
/// IEnumerable implementation
/// </summary>
/// <returns></returns>
public IEnumerator<NodeAccessor> GetEnumerator()
{
for (var i = 0; i < _path.Count; i++)
yield return _path[i];
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,43 @@
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Message path extension methods
/// </summary>
public static class MessagePathExtension
{
/// <summary>
/// Add a string node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="propName"></param>
/// <returns></returns>
public static MessagePath Property(this MessagePath path, string propName)
{
path.Add(NodeAccessor.String(propName));
return path;
}
/// <summary>
/// Add a property name node accessor
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static MessagePath PropertyName(this MessagePath path)
{
path.Add(NodeAccessor.PropertyName());
return path;
}
/// <summary>
/// Add a int node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="index"></param>
/// <returns></returns>
public static MessagePath Index(this MessagePath path, int index)
{
path.Add(NodeAccessor.Int(index));
return path;
}
}
}

View File

@ -0,0 +1,21 @@
namespace CryptoExchange.Net.Sockets.MessageParsing
{
/// <summary>
/// Message node type
/// </summary>
public enum NodeType
{
/// <summary>
/// Array node
/// </summary>
Array,
/// <summary>
/// Object node
/// </summary>
Object,
/// <summary>
/// Value node
/// </summary>
Value
}
}

View File

@ -1,57 +0,0 @@
using CryptoExchange.Net.Objects;
using Newtonsoft.Json.Linq;
using System;
using System.Threading;
namespace CryptoExchange.Net.Sockets
{
internal class PendingRequest
{
public int Id { get; set; }
public Func<JToken, bool> Handler { get; }
public JToken? Result { get; private set; }
public bool Completed { get; private set; }
public AsyncResetEvent Event { get; }
public DateTime RequestTimestamp { get; set; }
public TimeSpan Timeout { get; }
public SocketSubscription? Subscription { get; }
private CancellationTokenSource? _cts;
public PendingRequest(int id, Func<JToken, bool> handler, TimeSpan timeout, SocketSubscription? subscription)
{
Id = id;
Handler = handler;
Event = new AsyncResetEvent(false, false);
Timeout = timeout;
RequestTimestamp = DateTime.UtcNow;
Subscription = subscription;
}
public void IsSend()
{
// Start timeout countdown
_cts = new CancellationTokenSource(Timeout);
_cts.Token.Register(Fail, false);
}
public bool CheckData(JToken data)
{
return Handler(data);
}
public bool Succeed(JToken data)
{
Result = data;
Completed = true;
Event.Set();
return true;
}
public void Fail()
{
Completed = true;
Event.Set();
}
}
}

View File

@ -0,0 +1,30 @@
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Periodic task registration
/// </summary>
public class PeriodicTaskRegistration
{
/// <summary>
/// Identifier
/// </summary>
public string Identifier { get; set; } = string.Empty;
/// <summary>
/// Interval of query
/// </summary>
public TimeSpan Interval { get; set; }
/// <summary>
/// Delegate for getting the query
/// </summary>
public Func<SocketConnection, Query> QueryDelegate { get; set; } = null!;
/// <summary>
/// Callback after query
/// </summary>
public Action<CallResult>? Callback { get; set; }
}
}

View File

@ -0,0 +1,206 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Query
/// </summary>
public abstract class Query : IMessageProcessor
{
/// <summary>
/// Unique identifier
/// </summary>
public int Id { get; } = ExchangeHelpers.NextId();
/// <summary>
/// Has this query been completed
/// </summary>
public bool Completed { get; set; }
/// <summary>
/// Timestamp of when the request was send
/// </summary>
public DateTime RequestTimestamp { get; set; }
/// <summary>
/// Result
/// </summary>
public CallResult? Result { get; set; }
/// <summary>
/// Response
/// </summary>
public object? Response { get; set; }
/// <summary>
/// Wait event for the calling message processing thread
/// </summary>
public AsyncResetEvent? ContinueAwaiter { get; set; }
/// <summary>
/// Strings to match this query to a received message
/// </summary>
public abstract HashSet<string> ListenerIdentifiers { get; set; }
/// <summary>
/// The query request object
/// </summary>
public object Request { get; set; }
/// <summary>
/// If this is a private request
/// </summary>
public bool Authenticated { get; }
/// <summary>
/// Weight of the query
/// </summary>
public int Weight { get; }
/// <summary>
/// Get the type the message should be deserialized to
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public abstract Type? GetMessageType(IMessageAccessor message);
/// <summary>
/// Wait event for response
/// </summary>
protected AsyncResetEvent _event;
/// <summary>
/// Cancellation token
/// </summary>
protected CancellationTokenSource? _cts;
/// <summary>
/// ctor
/// </summary>
/// <param name="request"></param>
/// <param name="authenticated"></param>
/// <param name="weight"></param>
public Query(object request, bool authenticated, int weight = 1)
{
_event = new AsyncResetEvent(false, false);
Authenticated = authenticated;
Request = request;
Weight = weight;
}
/// <summary>
/// Signal that the request has been send and the timeout timer should start
/// </summary>
public void IsSend(TimeSpan timeout)
{
// Start timeout countdown
RequestTimestamp = DateTime.UtcNow;
_cts = new CancellationTokenSource(timeout);
_cts.Token.Register(Timeout, false);
}
/// <summary>
/// Wait untill timeout or the request is competed
/// </summary>
/// <param name="timeout"></param>
/// <returns></returns>
public async Task WaitAsync(TimeSpan timeout) => await _event.WaitAsync(timeout).ConfigureAwait(false);
/// <inheritdoc />
public virtual object Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
/// <summary>
/// Mark request as timeout
/// </summary>
public abstract void Timeout();
/// <summary>
/// Mark request as failed
/// </summary>
/// <param name="error"></param>
public abstract void Fail(string error);
/// <summary>
/// Handle a response message
/// </summary>
/// <param name="message"></param>
/// <param name="connection"></param>
/// <returns></returns>
public abstract Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message);
}
/// <summary>
/// Query
/// </summary>
/// <typeparam name="TResponse">Response object type</typeparam>
public abstract class Query<TResponse> : Query
{
/// <inheritdoc />
public override Type? GetMessageType(IMessageAccessor message) => typeof(TResponse);
/// <summary>
/// The typed call result
/// </summary>
public CallResult<TResponse>? TypedResult => (CallResult<TResponse>?)Result;
/// <summary>
/// ctor
/// </summary>
/// <param name="request"></param>
/// <param name="authenticated"></param>
/// <param name="weight"></param>
protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
}
/// <inheritdoc />
public override async Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message)
{
Completed = true;
Response = message.Data;
Result = await HandleMessageAsync(connection, message.As((TResponse)message.Data)).ConfigureAwait(false);
_event.Set();
await (ContinueAwaiter?.WaitAsync() ?? Task.CompletedTask).ConfigureAwait(false);
return Result;
}
/// <summary>
/// Handle the query response
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public virtual Task<CallResult<TResponse>> HandleMessageAsync(SocketConnection connection, DataEvent<TResponse> message) => Task.FromResult(new CallResult<TResponse>(message.Data, message.OriginalData, null));
/// <inheritdoc />
public override void Timeout()
{
if (Completed)
return;
Completed = true;
Result = new CallResult<TResponse>(new CancellationRequestedError(null, "Query timeout", null));
ContinueAwaiter?.Set();
_event.Set();
}
/// <inheritdoc />
public override void Fail(string error)
{
Result = new CallResult<TResponse>(new ServerError(error));
Completed = true;
ContinueAwaiter?.Set();
_event.Set();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,111 +0,0 @@
using System;
using System.Threading;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Socket subscription
/// </summary>
public class SocketSubscription
{
/// <summary>
/// Unique subscription id
/// </summary>
public int Id { get; }
/// <summary>
/// Exception event
/// </summary>
public event Action<Exception>? Exception;
/// <summary>
/// Message handlers for this subscription. Should return true if the message is handled and should not be distributed to the other handlers
/// </summary>
public Action<MessageEvent> MessageHandler { get; set; }
/// <summary>
/// The request object send when subscribing on the server. Either this or the `Identifier` property should be set
/// </summary>
public object? Request { get; set; }
/// <summary>
/// The subscription identifier, used instead of a `Request` object to identify the subscription
/// </summary>
public string? Identifier { get; set; }
/// <summary>
/// Whether this is a user subscription or an internal listener
/// </summary>
public bool UserSubscription { get; set; }
/// <summary>
/// If the subscription has been confirmed to be subscribed by the server
/// </summary>
public bool Confirmed { get; set; }
/// <summary>
/// Whether authentication is needed for this subscription
/// </summary>
public bool Authenticated { get; set; }
/// <summary>
/// Whether we're closing this subscription and a socket connection shouldn't be kept open for it
/// </summary>
public bool Closed { get; set; }
/// <summary>
/// Cancellation token registration, should be disposed when subscription is closed. Used for closing the subscription with
/// a provided cancelation token
/// </summary>
public CancellationTokenRegistration? CancellationTokenRegistration { get; set; }
private SocketSubscription(int id, object? request, string? identifier, bool userSubscription, bool authenticated, Action<MessageEvent> dataHandler)
{
Id = id;
UserSubscription = userSubscription;
MessageHandler = dataHandler;
Request = request;
Identifier = identifier;
Authenticated = authenticated;
}
/// <summary>
/// Create SocketSubscription for a subscribe request
/// </summary>
/// <param name="id"></param>
/// <param name="request"></param>
/// <param name="userSubscription"></param>
/// <param name="authenticated"></param>
/// <param name="dataHandler"></param>
/// <returns></returns>
public static SocketSubscription CreateForRequest(int id, object request, bool userSubscription,
bool authenticated, Action<MessageEvent> dataHandler)
{
return new SocketSubscription(id, request, null, userSubscription, authenticated, dataHandler);
}
/// <summary>
/// Create SocketSubscription for an identifier
/// </summary>
/// <param name="id"></param>
/// <param name="identifier"></param>
/// <param name="userSubscription"></param>
/// <param name="authenticated"></param>
/// <param name="dataHandler"></param>
/// <returns></returns>
public static SocketSubscription CreateForIdentifier(int id, string identifier, bool userSubscription,
bool authenticated, Action<MessageEvent> dataHandler)
{
return new SocketSubscription(id, null, identifier, userSubscription, authenticated, dataHandler);
}
/// <summary>
/// Invoke the exception event
/// </summary>
/// <param name="e"></param>
public void InvokeExceptionHandler(Exception e)
{
Exception?.Invoke(e);
}
}
}

View File

@ -0,0 +1,185 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Socket subscription
/// </summary>
public abstract class Subscription : IMessageProcessor
{
/// <summary>
/// Subscription id
/// </summary>
public int Id { get; set; }
/// <summary>
/// Total amount of invocations
/// </summary>
public int TotalInvocations { get; set; }
/// <summary>
/// Amount of invocation during this connection
/// </summary>
public int ConnectionInvocations { get; set; }
/// <summary>
/// Is it a user subscription
/// </summary>
public bool UserSubscription { get; set; }
/// <summary>
/// Has the subscription been confirmed
/// </summary>
public bool Confirmed { get; set; }
/// <summary>
/// Is the subscription closed
/// </summary>
public bool Closed { get; set; }
/// <summary>
/// Logger
/// </summary>
protected readonly ILogger _logger;
/// <summary>
/// If the subscription is a private subscription and needs authentication
/// </summary>
public bool Authenticated { get; }
/// <summary>
/// Strings to match this subscription to a received message
/// </summary>
public abstract HashSet<string> ListenerIdentifiers { get; set; }
/// <summary>
/// Cancellation token registration
/// </summary>
public CancellationTokenRegistration? CancellationTokenRegistration { get; set; }
/// <summary>
/// Exception event
/// </summary>
public event Action<Exception>? Exception;
/// <summary>
/// Get the deserialization type for this message
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
public abstract Type? GetMessageType(IMessageAccessor message);
/// <summary>
/// ctor
/// </summary>
/// <param name="logger"></param>
/// <param name="authenticated"></param>
/// <param name="userSubscription"></param>
public Subscription(ILogger logger, bool authenticated, bool userSubscription = true)
{
_logger = logger;
Authenticated = authenticated;
UserSubscription = userSubscription;
Id = ExchangeHelpers.NextId();
}
/// <summary>
/// Get the subscribe query to send when subscribing
/// </summary>
/// <returns></returns>
public abstract Query? GetSubQuery(SocketConnection connection);
/// <summary>
/// Handle a subscription query response
/// </summary>
/// <param name="message"></param>
public virtual void HandleSubQueryResponse(object message) { }
/// <summary>
/// Handle an unsubscription query response
/// </summary>
/// <param name="message"></param>
public virtual void HandleUnsubQueryResponse(object message) { }
/// <summary>
/// Get the unsubscribe query to send when unsubscribing
/// </summary>
/// <returns></returns>
public abstract Query? GetUnsubQuery();
/// <inheritdoc />
public virtual object Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
/// <summary>
/// Handle an update message
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public async Task<CallResult> HandleAsync(SocketConnection connection, DataEvent<object> message)
{
ConnectionInvocations++;
TotalInvocations++;
return await DoHandleMessageAsync(connection, message).ConfigureAwait(false);
}
/// <summary>
/// Handle the update message
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public abstract Task<CallResult> DoHandleMessageAsync(SocketConnection connection, DataEvent<object> message);
/// <summary>
/// Invoke the exception event
/// </summary>
/// <param name="e"></param>
public void InvokeExceptionHandler(Exception e)
{
Exception?.Invoke(e);
}
}
/// <inheritdoc />
public abstract class Subscription<TSubResponse, TUnsubResponse> : Subscription
{
/// <summary>
/// ctor
/// </summary>
/// <param name="logger"></param>
/// <param name="authenticated"></param>
protected Subscription(ILogger logger, bool authenticated) : base(logger, authenticated)
{
}
/// <inheritdoc />
public override void HandleSubQueryResponse(object message)
=> HandleSubQueryResponse((TSubResponse)message);
/// <summary>
/// Handle a subscription query response
/// </summary>
/// <param name="message"></param>
public virtual void HandleSubQueryResponse(TSubResponse message) { }
/// <inheritdoc />
public override void HandleUnsubQueryResponse(object message)
=> HandleUnsubQueryResponse((TUnsubResponse)message);
/// <summary>
/// Handle an unsubscription query response
/// </summary>
/// <param name="message"></param>
public virtual void HandleUnsubQueryResponse(TUnsubResponse message) { }
}
}

View File

@ -0,0 +1,58 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.MessageParsing.Interfaces;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// A system subscription
/// </summary>
public abstract class SystemSubscription : Subscription
{
/// <summary>
/// ctor
/// </summary>
/// <param name="logger"></param>
/// <param name="authenticated"></param>
public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false)
{
}
/// <inheritdoc />
public override Query? GetSubQuery(SocketConnection connection) => null;
/// <inheritdoc />
public override Query? GetUnsubQuery() => null;
}
/// <inheritdoc />
public abstract class SystemSubscription<T> : SystemSubscription
{
/// <inheritdoc />
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
/// <inheritdoc />
public override Task<CallResult> DoHandleMessageAsync(SocketConnection connection, DataEvent<object> message)
=> HandleMessageAsync(connection, message.As((T)message.Data));
/// <summary>
/// ctor
/// </summary>
/// <param name="logger"></param>
/// <param name="authenticated"></param>
protected SystemSubscription(ILogger logger, bool authenticated) : base(logger, authenticated)
{
}
/// <summary>
/// Handle an update message
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public abstract Task<CallResult> HandleMessageAsync(SocketConnection connection, DataEvent<T> message);
}
}

View File

@ -1,4 +1,5 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects.Sockets;
using Microsoft.Extensions.Logging;
namespace CryptoExchange.Net.Sockets

View File

@ -5,16 +5,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="9.1.5" />
<PackageReference Include="Bitfinex.Net" Version="7.0.4" />
<PackageReference Include="Bittrex.Net" Version="8.0.3" />
<PackageReference Include="Bybit.Net" Version="3.2.1" />
<PackageReference Include="CoinEx.Net" Version="6.0.3" />
<PackageReference Include="Huobi.Net" Version="5.0.3" />
<PackageReference Include="JK.Bitget.Net" Version="1.0.0" />
<PackageReference Include="JK.OKX.Net" Version="1.4.2" />
<PackageReference Include="KrakenExchange.Net" Version="4.1.5" />
<PackageReference Include="Kucoin.Net" Version="5.0.5" />
<PackageReference Include="Binance.Net" Version="9.5.0-beta1" />
<PackageReference Include="Bitfinex.Net" Version="7.1.0-beta1" />
<PackageReference Include="Bybit.Net" Version="3.4.0-beta1" />
<PackageReference Include="CoinEx.Net" Version="6.1.0-beta1" />
<PackageReference Include="Huobi.Net" Version="5.1.0-beta1" />
<PackageReference Include="JK.Bitget.Net" Version="1.1.0-beta2" />
<PackageReference Include="JK.OKX.Net" Version="1.6.0-beta2" />
<PackageReference Include="KrakenExchange.Net" Version="4.3.0-beta1" />
<PackageReference Include="Kucoin.Net" Version="5.2.0-beta1" />
<PackageReference Include="Serilog.AspNetCore" Version="6.0.0" />
</ItemGroup>

View File

@ -2,7 +2,6 @@
@inject IBinanceRestClient binanceClient
@inject IBitfinexRestClient bitfinexClient
@inject IBitgetRestClient bitgetClient
@inject IBittrexRestClient bittrexClient
@inject IBybitRestClient bybitClient
@inject ICoinExRestClient coinexClient
@inject IHuobiRestClient huobiClient
@ -24,7 +23,6 @@
var binanceTask = binanceClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var bitfinexTask = bitfinexClient.SpotApi.ExchangeData.GetTickerAsync("tBTCUSD");
var bitgetTask = bitgetClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT_SPBL");
var bittrexTask = bittrexClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var bybitTask = bybitClient.V5Api.ExchangeData.GetSpotTickersAsync("BTCUSDT");
var coinexTask = coinexClient.SpotApi.ExchangeData.GetTickerAsync("BTCUSDT");
var huobiTask = huobiClient.SpotApi.ExchangeData.GetTickerAsync("btcusdt");
@ -32,7 +30,7 @@
var kucoinTask = kucoinClient.SpotApi.ExchangeData.GetTickerAsync("BTC-USDT");
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTCUSDT");
await Task.WhenAll(binanceTask, bitfinexTask, bittrexTask, bybitTask, coinexTask, huobiTask, krakenTask, kucoinTask);
await Task.WhenAll(binanceTask, bitfinexTask, bybitTask, coinexTask, huobiTask, krakenTask, kucoinTask);
if (binanceTask.Result.Success)
_prices.Add("Binance", binanceTask.Result.Data.LastPrice);
@ -43,9 +41,6 @@
if (bitgetTask.Result.Success)
_prices.Add("Bitget", bitgetTask.Result.Data.ClosePrice);
if (bittrexTask.Result.Success)
_prices.Add("Bittrex", bittrexTask.Result.Data.LastPrice);
if (bybitTask.Result.Success)
_prices.Add("Bybit", bybitTask.Result.Data.List.First().LastPrice);

View File

@ -2,7 +2,6 @@
@inject IBinanceSocketClient binanceSocketClient
@inject IBitfinexSocketClient bitfinexSocketClient
@inject IBitgetSocketClient bitgetSocketClient
@inject IBittrexSocketClient bittrexSocketClient
@inject IBybitSocketClient bybitSocketClient
@inject ICoinExSocketClient coinExSocketClient
@inject IHuobiSocketClient huobiSocketClient
@ -11,6 +10,7 @@
@inject IOKXSocketClient okxSocketClient
@using System.Collections.Concurrent
@using CryptoExchange.Net.Objects
@using CryptoExchange.Net.Objects.Sockets;
@using CryptoExchange.Net.Sockets
@implements IDisposable
@ -31,13 +31,12 @@
binanceSocketClient.SpotApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Binance", data.Data.LastPrice)),
bitfinexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("tETHBTC", data => UpdateData("Bitfinex", data.Data.LastPrice)),
bitgetSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bitget", data.Data.LastPrice)),
bittrexSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Bittrex", data.Data.LastPrice)),
bybitSocketClient.V5SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("Bybit", data.Data.LastPrice)),
coinExSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETHBTC", data => UpdateData("CoinEx", data.Data.LastPrice)),
huobiSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ethbtc", data => UpdateData("Huobi", data.Data.ClosePrice ?? 0)),
krakenSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH/XBT", data => UpdateData("Kraken", data.Data.LastTrade.Price)),
kucoinSocketClient.SpotApi.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("Kucoin", data.Data.LastPrice ?? 0)),
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.LastPrice ?? 0)),
okxSocketClient.UnifiedApi.ExchangeData.SubscribeToTickerUpdatesAsync("ETH-BTC", data => UpdateData("OKX", data.Data.LastPrice ?? 0)),
};
await Task.WhenAll(tasks);

View File

@ -4,7 +4,6 @@
@using Binance.Net.Interfaces
@using Bitfinex.Net.Interfaces
@using Bitget.Net.Interfaces;
@using Bittrex.Net.Interfaces
@using Bybit.Net.Interfaces
@using CoinEx.Net.Interfaces
@using CryptoExchange.Net.Interfaces
@ -16,7 +15,6 @@
@inject IBinanceOrderBookFactory binanceFactory
@inject IBitfinexOrderBookFactory bitfinexFactory
@inject IBitgetOrderBookFactory bitgetFactory
@inject IBittrexOrderBookFactory bittrexFactory
@inject IBybitOrderBookFactory bybitFactory
@inject ICoinExOrderBookFactory coinExFactory
@inject IHuobiOrderBookFactory huobiFactory
@ -59,7 +57,6 @@
{ "Binance", binanceFactory.CreateSpot("ETHBTC") },
{ "Bitfinex", bitfinexFactory.Create("tETHBTC") },
{ "Bitget", bitgetFactory.CreateSpot("ETHBTC") },
{ "Bittrex", bittrexFactory.Create("ETH-BTC") },
{ "Bybit", bybitFactory.Create("ETHBTC", Bybit.Net.Enums.Category.Spot) },
{ "CoinEx", coinExFactory.CreateSpot("ETHBTC") },
{ "Huobi", huobiFactory.CreateSpot("ethbtc") },

View File

@ -1,26 +1,5 @@
@page "/SpotClient"
@inject IBinanceRestClient binanceClient
@inject IBitfinexRestClient bitfinexClient
@inject IBitgetRestClient bitgetClient
@inject IBittrexRestClient bittrexClient
@inject IBybitRestClient bybitClient
@inject ICoinExRestClient coinexClient
@inject IHuobiRestClient huobiClient
@inject IKrakenRestClient krakenClient
@inject IKucoinRestClient kucoinClient
@inject IOKXRestClient okxClient
@using Binance.Net.Clients.SpotApi
@using Bitfinex.Net.Clients.SpotApi
@using Bittrex.Net.Clients.SpotApi
@using Bitget.Net.Clients.SpotApi
@using Bybit.Net.Clients.SpotApi
@using CoinEx.Net.Clients.SpotApi
@using CryptoExchange.Net.Interfaces
@using CryptoExchange.Net.Interfaces.CommonClients
@using Huobi.Net.Clients.SpotApi
@using Kraken.Net.Clients.SpotApi
@using Kucoin.Net.Clients.SpotApi
@using OKX.Net.Clients.UnifiedApi
@inject ICryptoRestClient restClient
<h3>ETH-BTC prices:</h3>
@foreach(var price in _prices.OrderBy(p => p.Key))
@ -33,21 +12,7 @@
protected override async Task OnInitializedAsync()
{
var clients = new ISpotClient[]
{
binanceClient.SpotApi.CommonSpotClient,
bitfinexClient.SpotApi.CommonSpotClient,
bitgetClient.SpotApi.CommonSpotClient,
bittrexClient.SpotApi.CommonSpotClient,
bybitClient.SpotApiV1.CommonSpotClient,
coinexClient.SpotApi.CommonSpotClient,
huobiClient.SpotApi.CommonSpotClient,
krakenClient.SpotApi.CommonSpotClient,
kucoinClient.SpotApi.CommonSpotClient,
okxClient.UnifiedApi.CommonSpotClient
};
var clients = restClient.GetSpotClients();
var tasks = clients.Select(c => (c.ExchangeName, c.GetTickerAsync(c.GetSymbolName("ETH", "BTC"))));
await Task.WhenAll(tasks.Select(t => t.Item2));
foreach(var task in tasks)

View File

@ -1,23 +1,10 @@
using System.Collections.Generic;
using Binance.Net;
using Binance.Net.Clients;
using Binance.Net.Interfaces.Clients;
using Bitfinex.Net;
using Bitget.Net;
using Bittrex.Net;
using Bybit.Net;
using CoinEx.Net;
using CryptoExchange.Net.Authentication;
using Huobi.Net;
using Kraken.Net;
using Kucoin.Net;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OKX.Net;
namespace BlazorClient
{
@ -51,7 +38,6 @@ namespace BlazorClient
services.AddBitfinex();
services.AddBitget();
services.AddBittrex();
services.AddBybit();
services.AddCoinEx();
services.AddHuobi();

View File

@ -11,10 +11,10 @@
@using Binance.Net.Interfaces.Clients;
@using Bitfinex.Net.Interfaces.Clients;
@using Bitget.Net.Interfaces.Clients;
@using Bittrex.Net.Interfaces.Clients;
@using Bybit.Net.Interfaces.Clients;
@using CoinEx.Net.Interfaces.Clients;
@using Huobi.Net.Interfaces.Clients;
@using Kraken.Net.Interfaces.Clients;
@using Kucoin.Net.Interfaces.Clients;
@using OKX.Net.Interfaces.Clients;
@using OKX.Net.Interfaces.Clients;
@using CryptoExchange.Net.Interfaces;

View File

@ -6,16 +6,16 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Binance.Net" Version="9.1.5" />
<PackageReference Include="Bitfinex.Net" Version="7.0.4" />
<PackageReference Include="Binance.Net" Version="9.5.0-beta1" />
<PackageReference Include="Bitfinex.Net" Version="7.1.0-beta1" />
<PackageReference Include="Bittrex.Net" Version="8.0.3" />
<PackageReference Include="Bybit.Net" Version="3.2.1" />
<PackageReference Include="CoinEx.Net" Version="6.0.3" />
<PackageReference Include="Huobi.Net" Version="5.0.3" />
<PackageReference Include="JK.Bitget.Net" Version="1.0.0" />
<PackageReference Include="JK.OKX.Net" Version="1.4.2" />
<PackageReference Include="KrakenExchange.Net" Version="4.1.5" />
<PackageReference Include="Kucoin.Net" Version="5.0.5" />
<PackageReference Include="Bybit.Net" Version="3.4.0-beta1" />
<PackageReference Include="CoinEx.Net" Version="6.1.0-beta1" />
<PackageReference Include="Huobi.Net" Version="5.1.0-beta1" />
<PackageReference Include="JK.Bitget.Net" Version="1.1.0-beta2" />
<PackageReference Include="JK.OKX.Net" Version="1.6.0-beta2" />
<PackageReference Include="KrakenExchange.Net" Version="4.3.0-beta1" />
<PackageReference Include="Kucoin.Net" Version="5.2.0-beta1" />
</ItemGroup>
</Project>

View File

@ -7,6 +7,7 @@ using Binance.Net.Clients;
using Binance.Net.Interfaces.Clients;
using ConsoleClient.Models;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
namespace ConsoleClient.Exchanges

View File

@ -2,6 +2,7 @@
using Bybit.Net.Interfaces.Clients;
using ConsoleClient.Models;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;

View File

@ -1,5 +1,6 @@
using ConsoleClient.Models;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using System;
using System.Collections.Generic;

View File

@ -8,6 +8,7 @@ using Binance.Net.Objects;
using Bybit.Net.Clients;
using ConsoleClient.Exchanges;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
namespace ConsoleClient

View File

@ -1,26 +1,35 @@
# CryptoExchange.Net
[![.NET](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [![Nuget version](https://img.shields.io/nuget/v/CryptoExchange.Net.svg)](https://www.nuget.org/packages/CryptoExchange.Net) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.svg)](https://www.nuget.org/packages/CryptoExchange.Net)
# CryptoExchange.Net
CryptoExchange.Net is a base package which can be used to easily implement crypto currency exchange API's in C#. This library offers base classes for creating rest and websocket clients, and includes additional features like an automatically synchronizing order book implementation, error handling and automatic reconnects on websocket connections.
[![.NET](https://img.shields.io/github/actions/workflow/status/JKorf/CryptoExchange.Net/dotnet.yml?style=for-the-badge)](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [![Nuget downloads](https://img.shields.io/nuget/dt/CryptoExchange.Net.svg?style=for-the-badge)](https://www.nuget.org/packages/CryptoExchange.Net) ![License](https://img.shields.io/github/license/JKorf/CryptoExchange.Net?style=for-the-badge)
[Documentation](https://jkorf.github.io/CryptoExchange.Net/)
CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.
For more information on what CryptoExchange.Net and it's client libraries offers see the [Documentation](https://jkorf.github.io/CryptoExchange.Net/).
### Current implementations
The following API's are directly supported. Note that there are 3rd party implementations going around, but only these are created and supported by me:
|Exchange|Repository|Nuget|
|--|--|--|
|Binance|[JKorf/Binance.Net](https://github.com/JKorf/Binance.Net)|[![Nuget version](https://img.shields.io/nuget/v/Binance.net.svg?style=flat-square)](https://www.nuget.org/packages/Binance.Net)|
|Bitfinex|[JKorf/Bitfinex.Net](https://github.com/JKorf/Bitfinex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitfinex.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitfinex.Net)|
|Bitget|[JKorf/Bitget.Net](https://github.com/JKorf/Bitget.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bitget.net.svg?style=flat-square)](https://www.nuget.org/packages/Bitget.Net)|
|Bybit|[JKorf/Bybit.Net](https://github.com/JKorf/Bybit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Bybit.net.svg?style=flat-square)](https://www.nuget.org/packages/Bybit.Net)|
|CoinEx|[JKorf/CoinEx.Net](https://github.com/JKorf/CoinEx.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinEx.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinEx.Net)|
|CoinGecko|[JKorf/CoinGecko.Net](https://github.com/JKorf/CoinGecko.Net)|[![Nuget version](https://img.shields.io/nuget/v/CoinGecko.net.svg?style=flat-square)](https://www.nuget.org/packages/CoinGecko.Net)|
|Huobi/HTX|[JKorf/Huobi.Net](https://github.com/JKorf/Huobi.Net)|[![Nuget version](https://img.shields.io/nuget/v/Huobi.net.svg?style=flat-square)](https://www.nuget.org/packages/Huobi.Net)|
|Kraken|[JKorf/Kraken.Net](https://github.com/JKorf/Kraken.Net)|[![Nuget version](https://img.shields.io/nuget/v/KrakenExchange.net.svg?style=flat-square)](https://www.nuget.org/packages/KrakenExchange.Net)|
|Kucoin|[JKorf/Kucoin.Net](https://github.com/JKorf/Kucoin.Net)|[![Nuget version](https://img.shields.io/nuget/v/Kucoin.net.svg?style=flat-square)](https://www.nuget.org/packages/Kucoin.Net)|
|Mexc|[JKorf/Mexc.Net](https://github.com/JKorf/Mexc.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.Mexc.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.Mexc.Net)|
|OKX|[JKorf/OKX.Net](https://github.com/JKorf/OKX.Net)|[![Nuget version](https://img.shields.io/nuget/v/JK.OKX.net.svg?style=flat-square)](https://www.nuget.org/packages/JK.OKX.Net)|
## Discord
[![Nuget version](https://img.shields.io/discord/847020490588422145?style=for-the-badge)](https://discord.gg/MSpeEtSY8t)
A Discord server is available [here](https://discord.gg/MSpeEtSY8t). Feel free to join for discussion and/or questions around the CryptoExchange.Net and implementation libraries.
## Support the project
I develop and maintain this package on my own for free in my spare time, any support is greatly appreciated.
### Referral link
Use one of the following following referral links to signup to a new exchange to pay a small percentage of the trading fees you pay to support the project instead of paying them straight to the exchange. This doesn't cost you a thing!
[Binance](https://accounts.binance.com/en/register?ref=10153680)
[Bitfinex](https://www.bitfinex.com/sign-up?refcode=kCCe-CNBO)
[Bittrex](https://bittrex.com/discover/join?referralCode=TST-DJM-CSX)
[Bybit](https://partner.bybit.com/b/jkorf)
[CoinEx](https://www.coinex.com/register?refer_code=hd6gn)
[Huobi](https://www.huobi.com/en-us/v/register/double-invite/?inviter_id=11343840&invite_code=fxp93)
[Kucoin](https://www.kucoin.com/ucenter/signup?rcode=RguMux)
### Donate
Make a one time donation in a crypto currency of your choice. If you prefer to donate a currency not listed here please contact me.
@ -31,6 +40,19 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes
* Version 7.0.0-beta2 - 21 Feb 2024
* Updated RevitalizeRequestAsync signature
* Removed duplicate logging
* Version 7.0.0-beta1 - 06 Feb 2024
* Full overhaul of Websocket message handling
* Abstracted out Newtonsoft.Json references in preparation of moving to System.Text.Json
* Updated SendPeriodic to operate on connection level instead of client level to prevent looping when there are no connections
* Added check to not send an unsubscribe message if there is another subscription listening to the same events
* Added CryptoRestClient and CryptoSocketClient as aggregate for accessing different exchange APIs
* Updated socket client log messages
* Updated socket client GetSubscriptionState output
* Version 6.2.5 - 09 Jan 2024
* Added support for deserializing null and empty string values to BoolConverter

View File

@ -1,171 +0,0 @@
---
title: General usage
nav_order: 2
---
## How to use the library
Each implementation generally provides two different clients, which will be the access point for the API's. First of is the rest client, which is typically available via [ExchangeName]RestClient, and a socket client, which is generally named [ExchangeName]SocketClient. For example `BinanceRestClient` and `BinanceSocketClient`.
## Rest client
The rest client gives access to the Rest endpoint of the API. Rest endpoints are accessed by sending an HTTP request and receiving a response. The client is split in different sub-clients, which are named API Clients. These API clients are then again split in different topics. Typically a Rest client will look like this:
- [ExchangeName]RestClient
- SpotApi
- Account
- ExchangeData
- Trading
- FuturesApi
- Account
- ExchangeData
- Trading
This rest client has 2 different API clients, the `SpotApi` and the `FuturesApi`, each offering their own set of endpoints.
*Requesting ticker info on the spot API*
```csharp
var client = new KucoinClient();
var tickersResult = kucoinClient.SpotApi.ExchangeData.GetTickersAsync();
```
Structuring the client like this should make it easier to find endpoints and allows for separate options and functionality for different API clients. For example, some API's have totally separate API's for futures, with different base addresses and different API credentials, while other API's have implemented this in the same API. Either way, this structure can facilitate a similar interface.
### Rest API client
The Api clients are parts of the total API with a common identifier. In the previous example, it separates the Spot and the Futures API. This again is then separated into topics. Most Rest clients implement the following structure:
**Account**
Endpoints related to the user account. This can for example be endpoints for accessing account settings, or getting account balances. The endpoints in this topic will require API credentials to be provided in the client options.
**ExchangeData**
Endpoints related to exchange data. Exchange data can be tied to the exchange, for example retrieving the symbols supported by the exchange and what the trading rules are, or can be more general market endpoints, such as getting the most recent trades for a symbol.
These endpoints generally don't require API credentials as they are publicly available.
**Trading**
Endpoints related to trading. These are endpoints for placing and retrieving orders and retrieving trades made by the user. The endpoints in this topic will require API credentials to be provided in the client options.
### Processing request responses
Each request will return a WebCallResult<T> with the following properties:
`RequestHeaders`: The headers send to the server in the request message
`RequestMethod`: The Http method of the request
`RequestUrl`: The url the request was send to
`ResponseLength`: The length in bytes of the response message
`ResponseTime`: The duration between sending the request and receiving the response
`ResponseHeaders`: The headers returned from the server
`ResponseStatusCode`: The status code as returned by the server
`Success`: Whether or not the call was successful. If successful the `Data` property will contain the resulting data, if not successful the `Error` property will contain more details about what the issue was
`Error`: Details on what went wrong with a call. Only filled when `Success` == `false`
`OriginalData`: Will contain the originally received unparsed data if this has been enabled in the client options
`Data`: Data returned by the server, only available if `Success` == `true`
When processing the result of a call it should always be checked for success. Not doing so will result in `NullReference` exceptions when the call fails for whatever reason.
*Check call result*
```csharp
var callResult = await kucoinClient.SpotApi.ExchangeData.GetTickersAsync();
if(!callResult.Success)
{
Console.WriteLine("Request failed: " + callResult.Error);
return;
}
Console.WriteLine("Result: " + callResult.Data);
```
## Socket client
The socket client gives access to the websocket API of an exchange. Websocket API's offer streams to which updates are pushed to which a client can listen, and sometimes also allow request/response communication.
Just like the Rest client is divided in Rest Api clients, the Socket client is divided into Socket Api clients, each with their own range of API functionality. Socket Api clients are generally not divided into topics since the number of methods isn't as big as with the Rest client. To use the Kucoin client as example again, it looks like this:
```csharp
- KucoinSocketClient
- SpotStreams
- FuturesStreams
```
*Subscribing to updates for all tickers on the Spot Api*
```csharp
var subscribeResult = kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
```
Subscribe methods always require a data handler parameter, which is the method which will be called when an update is received from the server. This can be the name of a method or a lambda expression.
*Method reference*
```csharp
await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
private static void DataHandler(DataEvent<KucoinStreamTick> updateData)
{
// Process updateData
}
```
*Lambda*
```csharp
await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(updateData =>
{
// Process updateData
});
```
All updates are wrapped in a `DataEvent<>` object, which contain the following properties:
`Timestamp`: The timestamp when the data was received (not send!)
`OriginalData`: Will contain the originally received unparsed data if this has been enabled in the client options
`Topic`: Will contain the topic of the update, which is typically the symbol or asset the update is for
`Data`: Contains the received update data.
*[WARNING] Do not use `using` statements in combination with constructing a `SocketClient` without blocking the thread. Doing so will dispose the `SocketClient` instance when the subscription is done, which will result in the connection getting closed. Instead assign the socket client to a variable outside of the method scope.*
### Processing subscribe responses
Subscribing to a stream will return a `CallResult<UpdateSubscription>` object. This should be checked for success the same way as a [rest request](#processing-request-responses). The `UpdateSubscription` object can be used to listen for connection events of the socket connection.
```csharp
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
if(!subscriptionResult.Success)
{
Console.WriteLine("Failed to connect: " + subscriptionResult.Error);
return;
}
subscriptionResult.Data.ConnectionLost += () =>
{
Console.WriteLine("Connection lost");
};
subscriptionResult.Data.ConnectionRestored += (time) =>
{
Console.WriteLine("Connection restored");
};
```
### Unsubscribing
When no longer interested in specific updates there are a few ways to unsubscribe.
**Close subscription**
Subscribing to an update stream will respond with an `UpdateSubscription` object. You can call the `CloseAsync()` method on this to no longer receive updates from that subscription:
```csharp
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToAllTickerUpdatesAsync(DataHandler);
await subscriptionResult.Data.CloseAsync();
```
**Cancellation token**
Passing in a `CancellationToken` as parameter in the subscribe method will allow you to cancel subscriptions by canceling the token. This can be useful when you need to cancel some streams but not others. In this example, both `BTC-USDT` and `ETH-USDT` streams get canceled, while the `KCS-USDT` stream remains active.
```csharp
var cts = new CancellationTokenSource();
var subscriptionResult1 = await kucoinSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("BTC-USDT", DataHandler, cts.Token);
var subscriptionResult2 = await kucoinSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("ETH-USDT", DataHandler, cts.Token);
var subscriptionResult3 = await kucoinSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("KCS-USDT", DataHandler);
Console.ReadLine();
cts.Cancel();
```
**Client unsubscribe**
Subscriptions can also be closed by calling the `UnsubscribeAsync` method on the client, while providing either the `UpdateSubscription` object or the subscription id:
```csharp
var subscriptionResult = await kucoinSocketClient.SpotStreams.SubscribeToTickerUpdatesAsync("BTC-USDT", DataHandler);
await kucoinSocketClient.UnsubscribeAsync(subscriptionResult.Data);
// OR
await kucoinSocketClient.UnsubscribeAsync(subscriptionResult.Data.Id);
```
When you need to unsubscribe all current subscriptions on a client you can call `UnsubscribeAllAsync` on the client to unsubscribe all streams and close all connections.

View File

@ -1,60 +0,0 @@
---
title: FAQ
nav_order: 12
---
## Frequently asked questions
### I occasionally get a NullReferenceException, what's wrong?
You probably don't check the result status of a call and just assume the data is always there. `NullReferenceExecption`s will happen when you have code like this `var symbol = client.GetTickersAync().Result.Data.Symbol` because the `Data` property is null when the call fails. Instead check if the call is successful like this:
```csharp
var tickerResult = await client.GetTickersAync();
if(!tickerResult.Success)
{
// Handle error
}
else
{
// Handle result, it is now safe to access the Data property
var symbol = tickerResult.Data.Symbol;
}
```
### The socket client stops sending updates after a little while
You probably didn't keep a reference to the socket client and it got disposed.
Instead of subscribing like this:
```csharp
private void SomeMethod()
{
var socketClient = new BinanceSocketClient();
socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {
// Handle data
});
}
```
Subscribe like this:
```csharp
private BinanceSocketClient _socketClient = new BinanceSocketClient();
// .. rest of the class
private void SomeMethod()
{
_socketClient.Spot.SubscribeToOrderBookUpdates("BTCUSDT", data => {
// Handle data
});
}
```
### Can I use the TestNet/US/other API with this library
Yes, generally these are all supported and can be configured by setting the Environment in the client options. Some known environments should be available in the [Exchange]Environment class. For example:
```csharp
var client = new BinanceRestClient(options =>
{
options.Environment = BinanceEnvironment.Testnet;
});
```
### How are timezones handled / Timestamps are off by xx
Exchange API's treat all timestamps as UTC, both incoming and outgoing. The client libraries do no conversion so be sure to use UTC as well.

View File

@ -1,28 +0,0 @@
---
title: Glossary
nav_order: 11
---
## Terms and definitions
|Definition|Synonyms|Meaning|
|----------|--------|-------|
|Symbol|Market|An asset pair, for example `BTC-ETH`|
|Asset|Currency, Coin|A coin for which you can hold balance and which makes up Symbols. For example both `BTC`, `ETH` or `USD`|
|Trade|Execution, fill|The (partial) execution of an order. Orders can have multiple trades|
|Quantity|Amount, Size|The amount of asset|
|Fee|Commission|The fee paid for an order or trade|
|Kline|Candlestick, OHLC|K-line data, used for candlestick charts. Contains Open/High/Low/Close/Volume|
|KlineInterval|The time period of a single kline|
|Open order|Active order, Unexecuted order|An order which has not yet been fully filled|
|Closed order|Completed order, executed order|An order which is no longer active. Can be canceled or fully filled|
|Network|Chain|The network of an asset. For example `ETH` allows multiple networks like `ERC20` and `BEP2`|
|Order book|Market depth|A list of (the top rows of) the current best bids and asks|
|Ticker|Stats|Statistics over the last 24 hours|
|Client implementation|Library|An implementation of the `CrytpoExchange.Net` library. For example `Binance.Net` or `Bybit.Net`|
### Other naming conventions
#### PlaceOrderAsync
Methods for creating an order are always named `PlaceOrderAsync`, with and optional additional name for the type of order, for example `PlaceMarginOrderAsync`.
#### GetOrdersAsync/GetOpenOrdersAsync/GetClosedOrdersAsync
`GetOpenOrdersAsync` only retrieves orders which are still active, `GetClosedOrdersAsync` only retrieves orders which are canceled/closed. `GetOrdersAsync` retrieves both open and closed orders.

View File

@ -1,6 +0,0 @@
---
title: Creating an implementation
nav_order: 8
---
TODO

View File

@ -1,145 +0,0 @@
---
title: Common interfaces
nav_order: 7
---
## Shared interfaces
Clients have a common interface implementation to allow a shared code base to use the same functionality for different exchanges. The interface is implemented at the `API` level, for example:
```csharp
var binanceClient = new BinanceClient();
ISpotClient spotClient = binanceClient.SpotApi.CommonSpotClient;
IFuturesClient futuresClient = binanceClient.UsdFuturesApi.CommonFuturesClient;
```
For examples on this see the Examples folder.
## ISpotClient
The `ISpotClient` interface is implemented on Spot API clients. The interface exposes basic functionality like retrieving market data and managing orders. The `ISpotClient` interface will be available via the `CommonSpotClient` property on the Api client.
The spot client has the following members:
*Properties*
```csharp
// The name of the exchange this client interacts with
string ExchangeName { get; }
```
*Events*
```csharp
// Event when placing an order with this ISpotClient. Note that this is not an event handler listening on the exchange, just an event handler for when the `PlaceOrderAsync` method is called.
event Action<OrderId> OnOrderPlaced;
// Event when canceling an order with this ISpotClient. Note that this is not an event handler listening on the exchange, just an event handler for when the `CancelOrderAsync` method is called.
event Action<OrderId> OnOrderCanceled;
```
*Methods*
```csharp
// Retrieve the name of a symbol based on 2 assets. This will format them in the way the exchange expects them. For example BTC, USDT will return BTCUSDT on Binance and BTC-USDT on Kucoin
string GetSymbolName(string baseAsset, string quoteAsset);
// Get a list of symbols (trading pairs) on the exchange
Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync();
// Get the ticker (24 hour stats) for a symbol
Task<WebCallResult<Ticker>> GetTickerAsync(string symbol);
// Get a list of tickers for all symbols
Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync();
// Get a list klines (candlesticks) for a symbol
Task<WebCallResult<IEnumerable<Kline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null);
// Get the order book for a symbol
Task<WebCallResult<OrderBook>> GetOrderBookAsync(string symbol);
// Get a list of most recent trades
Task<WebCallResult<IEnumerable<Trade>>> GetRecentTradesAsync(string symbol);
// Get balances
Task<WebCallResult<IEnumerable<Balance>>> GetBalancesAsync(string? accountId = null);
// Place an order
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null);
// Get order by order id
Task<WebCallResult<Order>> GetOrderAsync(string orderId, string? symbol = null);
// Get the trades for an order
Task<WebCallResult<IEnumerable<UserTrade>>> GetOrderTradesAsync(string orderId, string? symbol = null);
// Get a list of open orders. Some exchanges require a symbol
Task<WebCallResult<IEnumerable<Order>>> GetOpenOrdersAsync(string? symbol = null);
// Get a list of closed orders. Some exchanges require a symbol
Task<WebCallResult<IEnumerable<Order>>> GetClosedOrdersAsync(string? symbol = null);
// Cancel an active order
Task<WebCallResult<OrderId>> CancelOrderAsync(string orderId, string? symbol = null);
```
## IFuturesClient
The `IFuturesClient` interface is implemented on Futures API clients. The interface exposes basic functionality like retrieving market data and managing orders. The `IFuturesClient` interface will be available via the `CommonFuturesClient` property on the Api client.
The spot client has the following members:
*Properties*
```csharp
// The name of the exchange this client interacts with
string ExchangeName { get; }
```
*Events*
```csharp
// Event when placing an order with this ISpotClient. Note that this is not an event handler listening on the exchange, just an event handler for when the `PlaceOrderAsync` method is called.
event Action<OrderId> OnOrderPlaced;
// Event when canceling an order with this ISpotClient. Note that this is not an event handler listening on the exchange, just an event handler for when the `CancelOrderAsync` method is called.
event Action<OrderId> OnOrderCanceled;
```
*Methods*
```csharp
// Retrieve the name of a symbol based on 2 assets. This will format them in the way the exchange expects them. For example BTC, USDT will return BTCUSDT on Binance and BTC-USDT on Kucoin
string GetSymbolName(string baseAsset, string quoteAsset);
// Get a list of symbols (trading pairs) on the exchange
Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync();
// Get the ticker (24 hour stats) for a symbol
Task<WebCallResult<Ticker>> GetTickerAsync(string symbol);
// Get a list of tickers for all symbols
Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync();
// Get a list klines (candlesticks) for a symbol
Task<WebCallResult<IEnumerable<Kline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null);
// Get the order book for a symbol
Task<WebCallResult<OrderBook>> GetOrderBookAsync(string symbol);
// Get a list of most recent trades
Task<WebCallResult<IEnumerable<Trade>>> GetRecentTradesAsync(string symbol);
// Get balances
Task<WebCallResult<IEnumerable<Balance>>> GetBalancesAsync(string? accountId = null);
// Get current open positions
Task<WebCallResult<IEnumerable<Position>>> GetPositionsAsync();
// Place an order
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null);
// Get order by order id
Task<WebCallResult<Order>> GetOrderAsync(string orderId, string? symbol = null);
// Get the trades for an order
Task<WebCallResult<IEnumerable<UserTrade>>> GetOrderTradesAsync(string orderId, string? symbol = null);
// Get a list of open orders. Some exchanges require a symbol
Task<WebCallResult<IEnumerable<Order>>> GetOpenOrdersAsync(string? symbol = null);
// Get a list of closed orders. Some exchanges require a symbol
Task<WebCallResult<IEnumerable<Order>>> GetClosedOrdersAsync(string? symbol = null);
// Cancel an active order
Task<WebCallResult<OrderId>> CancelOrderAsync(string orderId, string? symbol = null);
```

View File

@ -1,150 +0,0 @@
---
title: Logging
nav_order: 5
---
## Configuring logging
The library offers extensive logging, which depends on the dotnet `Microsoft.Extensions.Logging.ILogger` interface. This should provide ease of use when connecting the library logging to your existing logging implementation.
*Configure logging to write to the console*
```csharp
IServiceCollection services = new ServiceCollection();
services
.AddBinance()
.AddLogging(options =>
{
options.SetMinimumLevel(LogLevel.Trace);
options.AddConsole();
});
```
The library provides a TraceLogger ILogger implementation which writes log messages using `Trace.WriteLine`, but any other logging library can be used.
*Configure logging to use trace logging*
```csharp
IServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddBinance()
.AddLogging(options =>
{
options.SetMinimumLevel(LogLevel.Trace);
options.AddProvider(new TraceLoggerProvider());
});
```
### Using an external logging library and dotnet DI
With for example an ASP.Net Core or Blazor project the logging can be configured by the dependency container, which can then automatically be used be the clients.
The next example shows how to use Serilog. This assumes the `Serilog.AspNetCore` package (https://github.com/serilog/serilog-aspnetcore) is installed.
*Using serilog:*
```csharp
using Binance.Net;
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBinance();
builder.Host.UseSerilog();
var app = builder.Build();
// startup
app.Run();
```
### Logging without dotnet DI
If you don't have a dependency injection service available because you are for example working on a simple console application you have 2 options for logging.
#### Create a ServiceCollection manually and get the client from the service provider
```csharp
IServiceCollection serviceCollection = new ServiceCollection();
serviceCollection.AddBinance();
serviceCollection.AddLogging(options =>
{
options.SetMinimumLevel(LogLevel.Trace);
options.AddConsole();
}).BuildServiceProvider();
var client = serviceCollection.GetRequiredService<IBinanceRestClient>();
```
#### Create a LoggerFactory manually
```csharp
var logFactory = new LoggerFactory();
logFactory.AddProvider(new ConsoleLoggerProvider());
var binanceClient = new BinanceRestClient(new HttpClient(), logFactory, options => { });
```
## Providing logging for issues
A big debugging tool when opening an issue on Github is providing logging of what data caused the issue. This can be provided two ways, via the `OriginalData` property of the call result or data event, or collecting the Trace logging.
### OriginalData
This is only useful when there is an issue in deserialization. So either a call result is giving a Deserialization error, or the result has a value that is unexpected. If that is the issue, please provide the original data that is received so the deserialization issue can be resolved based on the received data.
By default the `OriginalData` property in the `WebCallResult`/`DataEvent` object is not filled as saving the original data has a (very small) performance penalty. To save the original data in the `OriginalData` property the `OutputOriginalData` option should be set to `true` in the client options.
*Enabled output data*
```csharp
var client = new BinanceClient(options =>
{
options.OutputOriginalData = true
});
```
*Accessing original data*
```csharp
// Rest request
var tickerResult = await client.SpotApi.ExchangeData.GetTickersAsync();
var originallyReceivedData = tickerResult.OriginalData;
// Socket update
await client.SpotStreams.SubscribeToAllTickerUpdatesAsync(update => {
var originallyRecievedData = update.OriginalData;
});
```
### Trace logging
Trace logging, which is the most verbose log level, will show everything the library does and includes the data that was send and received.
Output data will look something like this:
```
2021-12-17 10:40:42:296 | Debug | Binance | Client configuration: LogLevel: Trace, Writers: 1, OutputOriginalData: False, Proxy: -, AutoReconnect: True, ReconnectInterval: 00:00:05, MaxReconnectTries: , MaxResubscribeTries: 5, MaxConcurrentResubscriptionsPerSocket: 5, SocketResponseTimeout: 00:00:10, SocketNoDataTimeout: 00:00:00, SocketSubscriptionsCombineTarget: , CryptoExchange.Net: v5.0.0.0, Binance.Net: v8.0.0.0
2021-12-17 10:40:42:410 | Debug | Binance | [15] Creating request for https://api.binance.com/api/v3/ticker/24hr
2021-12-17 10:40:42:439 | Debug | Binance | [15] Sending GET request to https://api.binance.com/api/v3/ticker/24hr?symbol=BTCUSDT with headers Accept=[application/json], X-MBX-APIKEY=[XXX]
2021-12-17 10:40:43:024 | Debug | Binance | [15] Response received in 571ms: {"symbol":"BTCUSDT","priceChange":"-1726.47000000","priceChangePercent":"-3.531","weightedAvgPrice":"48061.51544204","prevClosePrice":"48901.44000000","lastPrice":"47174.97000000","lastQty":"0.00352000","bidPrice":"47174.96000000","bidQty":"0.65849000","askPrice":"47174.97000000","askQty":"0.13802000","openPrice":"48901.44000000","highPrice":"49436.43000000","lowPrice":"46749.55000000","volume":"33136.69765000","quoteVolume":"1592599905.80360790","openTime":1639647642763,"closeTime":1639734042763,"firstId":1191596486,"lastId":1192649611,"count":1053126}
```
When opening an issue, please provide this logging when available.
### Example of serilog config and minimal API's
```csharp
using Binance.Net;
using Binance.Net.Interfaces.Clients;
using Serilog;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBinance();
builder.Host.UseSerilog();
var app = builder.Build();
// startup
app.Urls.Add("http://localhost:3000");
app.MapGet("/price/{symbol}", async (string symbol) =>
{
var client = app.Services.GetRequiredService<IBinanceRestClient>();
var result = await client.SpotApi.ExchangeData.GetPriceAsync(symbol);
return result.Data.Price;
});
app.Run();
```

View File

@ -1,73 +0,0 @@
---
title: Migrate v5 to v6
nav_order: 10
---
## Migrating from version 5 to version 6
When updating your code from version 5 implementations to version 6 implementations you will encounter some breaking changes. Here is the general outline of changes made in the CryptoExchange.Net library. For more specific changes for each library visit the library migration guide.
*NOTE when updating it is not possible to have some client implementations use a V5 version and some clients a V6. When updating all libraries should be migrated*
## Rest client name
To be more clear about different clients for different API's the rest client implementations have been renamed from [Exchange]Client to [Exchange]RestClient. This makes it more clear that it only implements the Rest API and the [Exchange]SocketClient the Socket API.
## Options
Option parameters have been changed to a callback instead of an options object. This makes processing of the options easier and is in line with how dotnet handles option configurations.
**BaseAddress**
The BaseAddress option has been replaced by the Environment option. The Environment options allows for selection/switching between different trade environments more easily. For example the environment can be switched between a testnet and live by changing only a single line instead of having to change all BaseAddresses.
**LogLevel/LogWriters**
The logging options have been removed and are now inherited by the DI configuration. See [Logging](https://jkorf.github.io/CryptoExchange.Net/Logging.html) for more info.
**HttpClient**
The HttpClient will now be received by the DI container instead of having to pass it manually. When not using DI it is still possible to provide a HttpClient, but it is now located in the client constructor.
*V5*
```csharp
var client = new BinanceClient(new BinanceClientOptions(){
OutputOriginalData = true,
SpotApiOptions = new RestApiOptions {
BaseAddress = BinanceApiAddresses.TestNet.RestClientAddress
}
// Other options
});
```
*V6*
```csharp
var client = new BinanceClient(options => {
options.OutputOriginalData = true;
options.Environment = BinanceEnvironment.Testnet;
// Other options
});
```
## Socket api name
As socket API's are often more than just streams to subscribe to the name of the socket API clients have been changed from [Topic]Streams to [Topic]Api which matches the rest API client names. For example `SpotStreams` has become `SpotApi`, so `binanceSocketClient.UsdFuturesStreams.SubscribeXXX` has become `binanceSocketClient.UsdFuturesApi.SubscribeXXX`.
## Add[Exchange] extension method
With the change in options providing the DI extension methods for the IServiceCollection have also been changed slightly. Also the socket clients will now be registered as Singleton by default instead of Scoped.
*V5*
```csharp
builder.Services.AddKucoin((restOpts, socketOpts) =>
{
restOpts.LogLevel = LogLevel.Debug;
restOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
socketOpts.LogLevel = LogLevel.Debug;
socketOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
}, ServiceLifetime.Singleton);
```
*V6*
```csharp
builder.Services.AddKucoin((restOpts) =>
{
restOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
},
(socketOpts) =>
{
socketOpts.ApiCredentials = new KucoinApiCredentials("KEY", "SECRET", "PASS");
});
```

View File

@ -1,133 +0,0 @@
---
title: Client options
nav_order: 4
---
## Setting client options
Each implementation can be configured using client options. There are 2 ways to provide these, either via `[client].SetDefaultOptions([options]);`, or in the constructor of the client. The examples here use the `BinanceClient`, but usage is the same for each client.
*Set the default options to use for new clients*
```csharp
BinanceClient.SetDefaultOptions(options =>
{
options.OutputOriginalData = true;
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
// Override the api credentials for the Spot API
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
});
```
*Set the options to use for a single new client*
```csharp
var client = new BinanceClient(options =>
{
options.OutputOriginalData = true;
options.ApiCredentials = new ApiCredentials("KEY", "SECRET");
// Override the api credentials for the Spot API
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
});
```
When calling `SetDefaultOptions` each client created after that will use the options that were set, unless the specific option is overriden in the options that were provided to the client. Consider the following example:
```csharp
BinanceClient.SetDefaultOptions(options =>
{
options.OutputOriginalData = true;
});
var client = new BinanceClient(options =>
{
options.OutputOriginalData = false;
});
```
The client instance will have the following options:
`OutputOriginalData = false`
## Api options
The options are divided in two categories. The basic options, which will apply to everything the client does, and the Api options, which is limited to the specific API client (see [Clients](https://jkorf.github.io/CryptoExchange.Net/Clients.html)).
```csharp
var client = new BinanceRestClient(options =>
{
options.ApiCredentials = new ApiCredentials("GENERAL-KEY", "GENERAL-SECRET"),
options.SpotOptions.ApiCredentials = new ApiCredentials("SPOT-KEY", "SPOT-SECRET");
});
```
The options provided in the SpotApiOptions are only applied to the SpotApi (`client.SpotApi.XXX` endpoints), while the base options are applied to everything. This means that the spot endpoints will use the "SPOT-KEY" credentials, while all other endpoints (`client.UsdFuturesApi.XXX` / `client.CoinFuturesApi.XXX`) will use the "GENERAL-KEY" credentials.
## CryptoExchange.Net options definitions
All clients have access to the following options, specific implementations might have additional options.
**Base client options**
|Option|Description|Default|
|------|-----------|-------|
|`OutputOriginalData`|If set to `true` the originally received Json data will be output as well as the deserialized object. For `RestClient` calls the data will be in the `WebCallResult<T>.OriginalData` property, for `SocketClient` subscriptions the data will be available in the `DataEvent<T>.OriginalData` property when receiving an update. | `false`
|`ApiCredentials`| The API credentials to use for accessing protected endpoints. Can either be an API key/secret using Hmac encryption or an API key/private key using RSA encryption for exchanges that support that. See [Credentials](#credentials). Note that this is a `default` value for all API clients, and can be overridden per API client. See the `Base Api client options`| `null`
|`Proxy`|The proxy to use for connecting to the API.| `null`
|`RequestTimeout`|The timeout for client requests to the server| `TimeSpan.FromSeconds(20)`
**Rest client options (extension of base client options)**
|Option|Description|Default|
|------|-----------|-------|
|`AutoTimestamp`|Whether or not the library should attempt to sync the time between the client and server. If the time between server and client is not in sync authentication errors might occur. This option should be disabled when the client time sure is to be in sync.|`true`|
|`TimestampRecalculationInterval`|The interval of how often the time synchronization between client and server should be executed| `TimeSpan.FromHours(1)`
|`Environment`|The environment the library should talk to. Some exchanges have testnet/sandbox environments which can be used instead of the real exchange. The environment option can be used to switch between different trade environments|`Live environment`
**Socket client options (extension of base client options)**
|Option|Description|Default|
|------|-----------|-------|
|`AutoReconnect`|Whether or not the socket should attempt to automatically reconnect when disconnected.|`true`
|`ReconnectInterval`|The time to wait between connection tries when reconnecting.|`TimeSpan.FromSeconds(5)`
|`SocketResponseTimeout`|The time in which a response is expected on a request before giving a timeout.|`TimeSpan.FromSeconds(10)`
|`SocketNoDataTimeout`|If no data is received after this timespan then assume the connection is dropped. This is mainly used for API's which have some sort of ping/keepalive system. For example; the Bitfinex API will sent a heartbeat message every 15 seconds, so the `SocketNoDataTimeout` could be set to 20 seconds. On API's without such a mechanism this might not work because there just might not be any update while still being fully connected. | `default(TimeSpan)` (no timeout)
|`SocketSubscriptionsCombineTarget`|The amount of subscriptions that should be made on a single socket connection. Not all exchanges support multiple subscriptions on a single socket. Setting this to a higher number increases subscription speed because not every subscription needs to connect to the server, but having more subscriptions on a single connection will also increase the amount of traffic on that single connection, potentially leading to issues.| Depends on implementation
|`MaxConcurrentResubscriptionsPerSocket`|The maximum number of concurrent resubscriptions per socket when resubscribing after reconnecting|5
|`MaxSocketConnections`|The maximum amount of distinct socket connections|`null`
|`DelayAfterConnect`|The time to wait before sending messages after connecting to the server.|`TimeSpan.Zero`
|`Environment`|The environment the library should talk to. Some exchanges have testnet/sandbox environments which can be used instead of the real exchange. The environment option can be used to switch between different trade environments|`Live environment`
**Base Api client options**
|Option|Description|Default|
|------|-----------|-------|
|`ApiCredentials`|The API credentials to use for accessing protected endpoints. Can either be an API key/secret using Hmac encryption or an API key/private key using RSA encryption for exchanges that support that. See [Credentials](#credentials). Setting ApiCredentials on the Api Options will override any default ApiCredentials on the `Base client options`| `null`
|`OutputOriginalData`|If set to `true` the originally received Json data will be output as well as the deserialized object. For `RestClient` calls the data will be in the `WebCallResult<T>.OriginalData` property, for `SocketClient` subscriptions the data will be available in the `DataEvent<T>.OriginalData` property when receiving an update.|False
**Options for Rest Api Client (extension of base api client options)**
|Option|Description|Default|
|------|-----------|-------|
|`RateLimiters`|A list of `IRateLimiter`s to use.|`new List<IRateLimiter>()`|
|`RateLimitingBehaviour`|What should happen when a rate limit is reached.|`RateLimitingBehaviour.Wait`|
|`AutoTimestamp`|Whether or not the library should attempt to sync the time between the client and server. If the time between server and client is not in sync authentication errors might occur. This option should be disabled when the client time is sure to be in sync. Overrides the Rest client options `AutoTimestamp` option if set|`null`|
|`TimestampRecalculationInterval`|The interval of how often the time synchronization between client and server should be executed. Overrides the Rest client options `TimestampRecalculationInterval` option if set| `TimeSpan.FromHours(1)`
**Options for Socket Api Client (extension of base api client options)**
|Option|Description|Default|
|------|-----------|-------|
|`SocketNoDataTimeout`|If no data is received after this timespan then assume the connection is dropped. This is mainly used for API's which have some sort of ping/keepalive system. For example; the Bitfinex API will sent a heartbeat message every 15 seconds, so the `SocketNoDataTimeout` could be set to 20 seconds. On API's without such a mechanism this might not work because there just might not be any update while still being fully connected. Overrides the Socket client options `SocketNoDataTimeout` option if set | `default(TimeSpan)` (no timeout)
|`MaxSocketConnections`|The maximum amount of distinct socket connections. Overrides the Socket client options `MaxSocketConnections` option if set |`null`
## Credentials
Credentials are supported in 3 formats in the base library:
|Type|Description|Example|
|----|-----------|-------|
|`Hmac`|An API key + secret combination. The API key is send with the request and the secret is used to sign requests. This is the default authentication method on all exchanges. |`options.ApiCredentials = new ApiCredentials("51231f76e-9c503548-8fabs3f-rfgf12mkl3", "556be32-d563ba53-faa2dfd-b3n5c", CredentialType.Hmac);`|
|`RsaPem`|An API key + a public and private key pair generated by the user. The public key is shared with the exchange, while the private key is used to sign requests. This CredentialType expects the private key to be in .pem format and is only supported in .netstandard2.1 due to limitations of the framework|`options.ApiCredentials = new ApiCredentials("432vpV8daAaXAF4Qg", ""-----BEGIN PRIVATE KEY-----[PRIVATEKEY]-----END PRIVATE KEY-----", CredentialType.RsaPem);`|
|`RsaXml`|An API key + a public and private key pair generated by the user. The public key is shared with the exchange, while the private key is used to sign requests. This CredentialType expects the private key to be in xml format and is supported in .netstandard2.0 and .netstandard2.1, but it might mean the private key needs to be converted from the original format to xml|`options.ApiCredentials = new ApiCredentials("432vpV8daAaXAF4Qg", "<RSAKeyValue>[PRIVATEKEY]</RSAKeyValue>", CredentialType.RsaXml);`|

View File

@ -1,68 +0,0 @@
---
title: Order books
nav_order: 6
---
## Locally synced order book
Each exchange implementation provides an order book implementation. These implementations will provide a client side order book and will take care of synchronization with the server, and will handle reconnecting and resynchronizing in case of a dropped connection.
Order book implementations are named as `[ExchangeName][Type]SymbolOrderBook`, for example `BinanceSpotSymbolOrderBook`.
## Usage
Start the book synchronization by calling the `StartAsync` method. This returns whether the book is successfully synchronized and started. You can listen to the `OnStatusChange` event to be notified of when the status of a book changes. Note that the order book is only synchronized with the server when the state is `Synced`. When the order book has been started and the state changes from `Synced` to `Reconnecting` the book will automatically reconnect and resync itself.
*Start an order book and print the top 3 rows*
```csharp
var book = new BinanceSpotSymbolOrderBook("BTCUSDT");
book.OnStatusChange += (oldState, newState) => Console.WriteLine($"State changed from {oldState} to {newState}");
var startResult = await book.StartAsync();
if (!startResult.Success)
{
Console.WriteLine("Failed to start order book: " + startResult.Error);
return;
}
while(true)
{
Console.Clear();
Console.WriteLine(book.ToString(3);
await Task.Delay(500);
}
```
### Accessing bids/asks
You can access the current Bid/Ask lists using the responding properties:
`var currentBidList = book.Bids;`
`var currentAskList = book.Asks;`
Note that these will return copies of the internally synced lists when accessing the properties, and when accessing them in sequence like above does mean that the lists may not be in sync with eachother since they're accessed at different points in time.
When you need both lists in sync you should access the `Book` property.
`var (currentBidList, currentAskList) = book.Book;`
Because copies of the lists are made when accessing the bids/asks properties the performance impact should be considered. When only the current best ask/bid info is needed you can access the `BestOffers` property.
`var (bestBid, bestAsk) = book.BestOffers;`
### Events
The following events are available on the symbol order book:
`book.OnStatusChange`: The book has changed state. This happens during connecting, the connection was lost or the order book was detected to be out of sync. The asks/bids are only the actual with the server when state is `Synced`.
`book.OnOrderBookUpdate`: The book has changed, the arguments contain the changed entries.
`book.OnBestOffersChanged`: The best offer (best bid, best ask) has changed.
```csharp
book.OnStatusChange += (oldStatus, newStatus) => { Console.WriteLine($"State changed from {oldStatus} to {newStatus}"); };
book.OnOrderBookUpdate += (bidsAsks) => { Console.WriteLine($"Order book changed: {bidsAsks.Asks.Count()} asks, {bidsAsks.Bids.Count()} bids"); };
book.OnBestOffersChanged += (bestOffer) => { Console.WriteLine($"Best offer changed, best bid: {bestOffer.BestBid.Price}, best ask: {bestOffer.BestAsk.Price}"); };
```
### Order book factory
Each exchange implementation also provides an order book factory for creating ISymbolOrderBook instances. The naming convention for the factory is `[Exchange]OrderBookFactory`, for example `BinanceOrderBookFactory`. This type will be automatically added when using DI and can be used to facilitate easier testing.
*Creating an order book using the order book factory*
```csharp
var factory = services.GetRequiredService<IKucoinOrderBookFactory>();
var book = factory.CreateSpot("ETH-USDT");
var startResult = await book.StartAsync();
```

View File

@ -1,74 +0,0 @@
---
title: Rate limiting
nav_order: 9
---
## Rate limiting
The library has build in rate limiting. These rate limits can be configured per client. Some client implementations where the exchange has clear rate limits will also have a default rate limiter already set up.
Rate limiting is configured in the client options, and can be set on a specific client or for all clients by either providing it in the constructor for a client, or by using the `SetDefaultOptions` on a client.
What to do when a limit is reached can be configured with the `RateLimitingBehaviour` client options, which has 2 possible options. Setting it to `Fail` will cause a request to fail without sending it. Setting it to `Wait` will cause the request to wait until the request can be send in accordance to the rate limiter.
A rate limiter can be configured in the options like so:
```csharp
new ClientOptions
{
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
RateLimiters = new List<IRateLimiter>
{
new RateLimiter()
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
}
}
```
This will add a rate limiter for 50 requests per 10 seconds.
A rate limiter can have multiple limits:
```csharp
new RateLimiter()
.AddTotalRateLimit(50, TimeSpan.FromSeconds(10))
.AddEndpointLimit("/api/order", 10, TimeSpan.FromSeconds(2))
```
This adds another limit of 10 requests per 2 seconds in addition to the 50 requests per 10 seconds limit.
These are the available rate limit configurations:
### AddTotalRateLimit
|Parameter|Description|
|---------|-----------|
|limit|The request weight limit per time period. Note that requests can have a weight specified. Default requests will have a weight of 1|
|perTimePeriod|The time period over which the limit is enforced|
A rate limit for the total amount of requests for all requests send from the client.
### AddEndpointLimit
|Parameter|Description|
|---------|-----------|
|endpoint|The endpoint this limit is for|
|limit|The request weight limit per time period. Note that requests can have a weight specified. Default requests will have a weight of 1|
|perTimePeriod|The time period over which the limit is enforced|
|method|The HTTP method this limit is for. Defaults to all methods|
|excludeFromOtherRateLimits|When set to true requests to this endpoint won't be counted for other configured rate limits|
A rate limit for all requests send to a specific endpoint. Requests that do not fully match the endpoint will not be counted to this limit.
### AddPartialEndpointLimit
|Parameter|Description|
|---------|-----------|
|endpoint|The partial endpoint this limit is for. Partial means that a request will match this limiter when a part of the request URI path matches this endpoint|
|limit|The request weight limit per time period. Note that requests can have a weight specified. Default requests will have a weight of 1|
|perTimePeriod|The time period over which the limit is enforced|
|method|The HTTP method this limit is for. Defaults to all methods|
|countPerEndpoint|Whether all requests matching the endpoint pattern should be combined for this limit or each endpoint has its own limit|
|ignoreOtherRateLimits|When set to true requests to this endpoint won't be counted for other configured rate limits|
A rate limit for a partial endpoint. Requests will be counted towards this limit if the request path contains the endpoint. For example request `/api/v2/test` will match when the partial endpoint limit is set for `/api/v2`.
### AddApiKeyLimit
|Parameter|Description|
|---------|-----------|
|limit|The request weight limit per time period. Note that requests can have a weight specified. Default requests will have a weight of 1|
|perTimePeriod|The time period over which the limit is enforced|
|onlyForSignedRequests|Whether this rate limit should only be counter for signed/authenticated requests|
|excludeFromTotalRateLimit|Whether requests counted for this rate limited should not be counted towards the total rate limit|
A rate limit for an API key. Requests with the same API key will be grouped and limited.

View File

@ -0,0 +1,159 @@
/*============================
COLOR Blue
==============================*/
::selection {
background: #007bff;
}
a, a:focus {
color: #007bff;
}
a:hover, a:active {
color: #006adb;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #007bff;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #007bff;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #007bff;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #007bff;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #007bff !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #007bff;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #007bff;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #007bff;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #007bff;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #007bff;
}
.nav-pills .nav-link:not(.active):hover {
color: #007bff;
}
#footer .nav .nav-item .nav-link:focus {
color: #007bff;
}
#footer .nav .nav-link:hover {
color: #007bff;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #007bff;
}
/* Back to Top */
#back-to-top:hover {
background-color: #007bff;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #007bff !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #007bff !important;
}
.btn-link:hover {
color: #006adb !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #006adb !important;
}
.border-primary {
border-color: #007bff !important;
}
.btn-primary {
background-color: #007bff;
border-color: #007bff;
}
.btn-primary:hover {
background-color: #006adb;
border-color: #006adb;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #006adb;
border-color: #006adb;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #006adb;
border-color: #006adb;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #007bff;
border-color: #007bff;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #007bff;
border-color: #007bff;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #007bff;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #007bff;
border-color: #007bff;
}
.list-group-item.active {
background-color: #007bff;
border-color: #007bff;
}
.page-link {
color: #007bff;
}
.page-link:hover {
color: #006adb;
}

View File

@ -0,0 +1,159 @@
/*============================
COLOR Brown
==============================*/
::selection {
background: #795548;
}
a, a:focus {
color: #795548;
}
a:hover, a:active {
color: #63453b;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #795548;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #795548;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #795548;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #795548;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #795548 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #795548;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #795548;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #795548;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #795548;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #795548;
}
.nav-pills .nav-link:not(.active):hover {
color: #795548;
}
#footer .nav .nav-item .nav-link:focus {
color: #795548;
}
#footer .nav .nav-link:hover {
color: #795548;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #795548;
}
/* Back to Top */
#back-to-top:hover {
background-color: #795548;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #795548 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #795548 !important;
}
.btn-link:hover {
color: #63453b !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #63453b !important;
}
.border-primary {
border-color: #795548 !important;
}
.btn-primary {
background-color: #795548;
border-color: #795548;
}
.btn-primary:hover {
background-color: #63453b;
border-color: #63453b;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #63453b;
border-color: #63453b;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #63453b;
border-color: #63453b;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #795548;
border-color: #795548;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #795548;
border-color: #795548;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #795548;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #795548;
border-color: #795548;
}
.list-group-item.active {
background-color: #795548;
border-color: #795548;
}
.page-link {
color: #795548;
}
.page-link:hover {
color: #63453b;
}

View File

@ -0,0 +1,159 @@
/*============================
COLOR Cyan
==============================*/
::selection {
background: #17a2b8;
}
a, a:focus {
color: #17a2b8;
}
a:hover, a:active {
color: #138698;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #17a2b8;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #17a2b8;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #17a2b8;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #17a2b8;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #17a2b8 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #17a2b8;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #17a2b8;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #17a2b8;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #17a2b8;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #17a2b8;
}
.nav-pills .nav-link:not(.active):hover {
color: #17a2b8;
}
#footer .nav .nav-item .nav-link:focus {
color: #17a2b8;
}
#footer .nav .nav-link:hover {
color: #17a2b8;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #17a2b8;
}
/* Back to Top */
#back-to-top:hover {
background-color: #17a2b8;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #17a2b8 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #17a2b8 !important;
}
.btn-link:hover {
color: #138698 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #138698 !important;
}
.border-primary {
border-color: #17a2b8 !important;
}
.btn-primary {
background-color: #17a2b8;
border-color: #17a2b8;
}
.btn-primary:hover {
background-color: #138698;
border-color: #138698;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #138698;
border-color: #138698;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #138698;
border-color: #138698;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #17a2b8;
border-color: #17a2b8;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #17a2b8;
border-color: #17a2b8;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #17a2b8;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #17a2b8;
border-color: #17a2b8;
}
.list-group-item.active {
background-color: #17a2b8;
border-color: #17a2b8;
}
.page-link {
color: #17a2b8;
}
.page-link:hover {
color: #138698;
}

View File

@ -0,0 +1,159 @@
/*============================
COLOR Green
==============================*/
::selection {
background: #28a745;
}
a, a:focus {
color: #28a745;
}
a:hover, a:active {
color: #218a39;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #28a745;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #28a745;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #28a745;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #28a745;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #28a745 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #28a745;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #28a745;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #28a745;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #28a745;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #28a745;
}
.nav-pills .nav-link:not(.active):hover {
color: #28a745;
}
#footer .nav .nav-item .nav-link:focus {
color: #28a745;
}
#footer .nav .nav-link:hover {
color: #28a745;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #28a745;
}
/* Back to Top */
#back-to-top:hover {
background-color: #28a745;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #28a745 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #28a745 !important;
}
.btn-link:hover {
color: #218a39 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #218a39 !important;
}
.border-primary {
border-color: #28a745 !important;
}
.btn-primary {
background-color: #28a745;
border-color: #28a745;
}
.btn-primary:hover {
background-color: #218a39;
border-color: #218a39;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #218a39;
border-color: #218a39;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #218a39;
border-color: #218a39;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #28a745;
border-color: #28a745;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #28a745;
border-color: #28a745;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #28a745;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #28a745;
border-color: #28a745;
}
.list-group-item.active {
background-color: #28a745;
border-color: #28a745;
}
.page-link {
color: #28a745;
}
.page-link:hover {
color: #218a39;
}

View File

@ -0,0 +1,159 @@
/*============================
COLOR Indigo
==============================*/
::selection {
background: #6610f2;
}
a, a:focus {
color: #6610f2;
}
a:hover, a:active {
color: #570bd3;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #6610f2;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #6610f2;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #6610f2;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #6610f2;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #6610f2 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #6610f2;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #6610f2;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #6610f2;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #6610f2;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #6610f2;
}
.nav-pills .nav-link:not(.active):hover {
color: #6610f2;
}
#footer .nav .nav-item .nav-link:focus {
color: #6610f2;
}
#footer .nav .nav-link:hover {
color: #6610f2;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #6610f2;
}
/* Back to Top */
#back-to-top:hover {
background-color: #6610f2;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #6610f2 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #6610f2 !important;
}
.btn-link:hover {
color: #570bd3 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #570bd3 !important;
}
.border-primary {
border-color: #6610f2 !important;
}
.btn-primary {
background-color: #6610f2;
border-color: #6610f2;
}
.btn-primary:hover {
background-color: #570bd3;
border-color: #570bd3;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #570bd3;
border-color: #570bd3;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #570bd3;
border-color: #570bd3;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #6610f2;
border-color: #6610f2;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #6610f2;
border-color: #6610f2;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #6610f2;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #6610f2;
border-color: #6610f2;
}
.list-group-item.active {
background-color: #6610f2;
border-color: #6610f2;
}
.page-link {
color: #6610f2;
}
.page-link:hover {
color: #570bd3;
}

View File

@ -0,0 +1,159 @@
/*============================
COLOR Orange
==============================*/
::selection {
background: #fd7e14;
}
a, a:focus {
color: #fd7e14;
}
a:hover, a:active {
color: #eb6c02;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #fd7e14;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #fd7e14;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #fd7e14;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #fd7e14;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #fd7e14 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #fd7e14;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #fd7e14;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #fd7e14;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #fd7e14;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #fd7e14;
}
.nav-pills .nav-link:not(.active):hover {
color: #fd7e14;
}
#footer .nav .nav-item .nav-link:focus {
color: #fd7e14;
}
#footer .nav .nav-link:hover {
color: #fd7e14;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #fd7e14;
}
/* Back to Top */
#back-to-top:hover {
background-color: #fd7e14;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #fd7e14 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #fd7e14 !important;
}
.btn-link:hover {
color: #eb6c02 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #eb6c02 !important;
}
.border-primary {
border-color: #fd7e14 !important;
}
.btn-primary {
background-color: #fd7e14;
border-color: #fd7e14;
}
.btn-primary:hover {
background-color: #eb6c02;
border-color: #eb6c02;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #eb6c02;
border-color: #eb6c02;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #eb6c02;
border-color: #eb6c02;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #fd7e14;
border-color: #fd7e14;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #fd7e14;
border-color: #fd7e14;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #fd7e14;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #fd7e14;
border-color: #fd7e14;
}
.list-group-item.active {
background-color: #fd7e14;
border-color: #fd7e14;
}
.page-link {
color: #fd7e14;
}
.page-link:hover {
color: #eb6c02;
}

View File

@ -0,0 +1,159 @@
/*============================
COLOR Purple
==============================*/
::selection {
background: #6f42c1;
}
a, a:focus {
color: #6f42c1;
}
a:hover, a:active {
color: #5f37a8;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #6f42c1;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #6f42c1;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #6f42c1;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #6f42c1;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #6f42c1 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #6f42c1;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #6f42c1;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #6f42c1;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #6f42c1;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #6f42c1;
}
.nav-pills .nav-link:not(.active):hover {
color: #6f42c1;
}
#footer .nav .nav-item .nav-link:focus {
color: #6f42c1;
}
#footer .nav .nav-link:hover {
color: #6f42c1;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #6f42c1;
}
/* Back to Top */
#back-to-top:hover {
background-color: #6f42c1;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #6f42c1 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #6f42c1 !important;
}
.btn-link:hover {
color: #5f37a8 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #5f37a8 !important;
}
.border-primary {
border-color: #6f42c1 !important;
}
.btn-primary {
background-color: #6f42c1;
border-color: #6f42c1;
}
.btn-primary:hover {
background-color: #5f37a8;
border-color: #5f37a8;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #5f37a8;
border-color: #5f37a8;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #5f37a8;
border-color: #5f37a8;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #6f42c1;
border-color: #6f42c1;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #6f42c1;
border-color: #6f42c1;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #6f42c1;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #6f42c1;
border-color: #6f42c1;
}
.list-group-item.active {
background-color: #6f42c1;
border-color: #6f42c1;
}
.page-link {
color: #6f42c1;
}
.page-link:hover {
color: #5f37a8;
}

View File

@ -0,0 +1,159 @@
/*============================
COLOR Red
==============================*/
::selection {
background: #dc3545;
}
a, a:focus {
color: #dc3545;
}
a:hover, a:active {
color: #ca2333;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #dc3545;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #dc3545;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #dc3545;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #dc3545;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #dc3545 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #dc3545;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #dc3545;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #dc3545;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #dc3545;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #dc3545;
}
.nav-pills .nav-link:not(.active):hover {
color: #dc3545;
}
#footer .nav .nav-item .nav-link:focus {
color: #dc3545;
}
#footer .nav .nav-link:hover {
color: #dc3545;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #dc3545;
}
/* Back to Top */
#back-to-top:hover {
background-color: #dc3545;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #dc3545 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #dc3545 !important;
}
.btn-link:hover {
color: #ca2333 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #ca2333 !important;
}
.border-primary {
border-color: #dc3545 !important;
}
.btn-primary {
background-color: #dc3545;
border-color: #dc3545;
}
.btn-primary:hover {
background-color: #ca2333;
border-color: #ca2333;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #ca2333;
border-color: #ca2333;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #ca2333;
border-color: #ca2333;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #dc3545;
border-color: #dc3545;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #dc3545;
border-color: #dc3545;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #dc3545;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #dc3545;
border-color: #dc3545;
}
.list-group-item.active {
background-color: #dc3545;
border-color: #dc3545;
}
.page-link {
color: #dc3545;
}
.page-link:hover {
color: #ca2333;
}

View File

@ -0,0 +1,159 @@
/*============================
COLOR Teal
==============================*/
::selection {
background: #20c997;
}
a, a:focus {
color: #20c997;
}
a:hover, a:active {
color: #1baa80;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #20c997;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #20c997;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #20c997;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #20c997;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #20c997 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #20c997;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #20c997;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #20c997;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #20c997;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #20c997;
}
.nav-pills .nav-link:not(.active):hover {
color: #20c997;
}
#footer .nav .nav-item .nav-link:focus {
color: #20c997;
}
#footer .nav .nav-link:hover {
color: #20c997;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #20c997;
}
/* Back to Top */
#back-to-top:hover {
background-color: #20c997;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #20c997 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #20c997 !important;
}
.btn-link:hover {
color: #1baa80 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #1baa80 !important;
}
.border-primary {
border-color: #20c997 !important;
}
.btn-primary {
background-color: #20c997;
border-color: #20c997;
}
.btn-primary:hover {
background-color: #1baa80;
border-color: #1baa80;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #1baa80;
border-color: #1baa80;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #1baa80;
border-color: #1baa80;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #20c997;
border-color: #20c997;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #20c997;
border-color: #20c997;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #20c997;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #20c997;
border-color: #20c997;
}
.list-group-item.active {
background-color: #20c997;
border-color: #20c997;
}
.page-link {
color: #20c997;
}
.page-link:hover {
color: #1baa80;
}

View File

@ -0,0 +1,159 @@
/*============================
COLOR Yellow
==============================*/
::selection {
background: #ffc107;
}
a, a:focus {
color: #ffc107;
}
a:hover, a:active {
color: #f7b900;
}
.primary-menu ul.navbar-nav > li:hover > a:not(.btn), .primary-menu ul.navbar-nav > li > a.active:not(.btn) {
color: #ffc107;
}
.primary-menu ul.navbar-nav > li.dropdown .dropdown-menu li:hover > a:not(.btn) {
color: #ffc107;
}
.primary-menu.navbar-line-under-text ul.navbar-nav > li > a:not(.btn):after {
border-color: #ffc107;
}
/*=== Side Navigation ===*/
.idocs-navigation .nav .nav .nav-item .nav-link.active:after, .idocs-navigation.docs-navigation-dark .nav .nav .nav-item .nav-link.active:after {
border-color: #ffc107;
}
/* Accordion & Toggle */
.accordion .card-header a:hover.collapsed {
color: #ffc107 !important;
}
.accordion:not(.accordion-alternate) .card-header a {
background-color: #ffc107;
color: #fff;
}
/* Nav */
.nav:not(.nav-pills) .nav-item .nav-link.active, .nav:not(.nav-pills) .nav-item .nav-link:hover {
color: #ffc107;
}
.nav-tabs .nav-item .nav-link.active {
color: #0c2f55;
}
.nav-tabs .nav-item .nav-link.active:after {
background-color: #ffc107;
}
.nav-tabs .nav-item .nav-link:not(.active):hover {
color: #ffc107;
}
.nav-tabs.flex-column .nav-item .nav-link.active {
color: #ffc107;
}
.nav-pills .nav-link:not(.active):hover {
color: #ffc107;
}
#footer .nav .nav-item .nav-link:focus {
color: #ffc107;
}
#footer .nav .nav-link:hover {
color: #ffc107;
}
#footer .footer-copyright .nav .nav-link:hover {
color: #ffc107;
}
/* Back to Top */
#back-to-top:hover {
background-color: #ffc107;
}
/* Extras */
.bg-primary, .badge-primary {
background-color: #ffc107 !important;
}
.text-primary, .btn-light, .btn-outline-light:hover, .btn-link, .btn-outline-light:not(:disabled):not(.disabled).active, .btn-outline-light:not(:disabled):not(.disabled):active {
color: #ffc107 !important;
}
.btn-link:hover {
color: #f7b900 !important;
}
.text-muted {
color: #8e9a9d !important;
}
.text-light {
color: #dee3e4 !important;
}
a.bg-primary:focus, a.bg-primary:hover, button.bg-primary:focus, button.bg-primary:hover {
background-color: #f7b900 !important;
}
.border-primary {
border-color: #ffc107 !important;
}
.btn-primary {
background-color: #ffc107;
border-color: #ffc107;
}
.btn-primary:hover {
background-color: #f7b900;
border-color: #f7b900;
}
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled):active {
background-color: #f7b900;
border-color: #f7b900;
}
.btn-primary.focus, .btn-primary:focus {
background-color: #f7b900;
border-color: #f7b900;
}
.btn-outline-primary, .btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled):active {
color: #ffc107;
border-color: #ffc107;
}
.btn-outline-primary:hover, .btn-outline-primary:not(:disabled):not(.disabled).active:hover, .btn-outline-primary:not(:disabled):not(.disabled):active:hover {
background-color: #ffc107;
border-color: #ffc107;
color: #fff;
}
.progress-bar,
.nav-pills .nav-link.active, .nav-pills .show > .nav-link, .dropdown-item.active, .dropdown-item:active {
background-color: #ffc107;
}
.page-item.active .page-link,
.custom-radio .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label::before,
.custom-checkbox .custom-control-input:checked ~ .custom-control-label:before,
.custom-control-input:checked ~ .custom-control-label:before {
background-color: #ffc107;
border-color: #ffc107;
}
.list-group-item.active {
background-color: #ffc107;
border-color: #ffc107;
}
.page-link {
color: #ffc107;
}
.page-link:hover {
color: #f7b900;
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Some files were not shown because too many files have changed in this diff Show More